// Name: Time Tracker + Chrome Report // Description: Tracks time spent in active mac app windows and Chrome tabs, and generates time-based reports. // Author: tayiorbeii // GitHub: tayiorbeii import "@johnlindquist/kit" type Session = { id: string app: string title?: string url?: string start: string // ISO end?: string // ISO durationMs?: number } const kv = await store("activity-tracker", { sessions: [] as Session[] }) if ((await kv.get("sessions")) === undefined) { await kv.set("sessions", []) } const mode = await arg("Select mode", [ { name: "Track (start live tracking)", value: "track", description: "Continuously record active app and Chrome URL over time", }, { name: "Report (view summary)", value: "report", description: "Generate a report over a time range and copy CSV to clipboard", }, ]) if (mode === "track") { const intervalSecStr = await arg("Polling interval (seconds)", ["5", "10", "15", "30", "60"]) const intervalMs = Math.max(1, Number(intervalSecStr)) * 1000 await div( md( `Tracking started. Polling every ${intervalMs / 1000}s. - This window will hide. Use Kit's Process Manager (${cmd}+p) to stop. - Sessions are saved to "~/.kenv/db/activity-tracker.json". ` ) ) await hide() let sessions: Session[] = (await kv.get<Session[]>("sessions")) || [] let current: Session | null = null const sameKey = (a: Session | null, b: { app: string; url?: string }) => { if (!a) return false const appSame = a.app === b.app const urlSame = (a.url || "") === (b.url || "") return appSame && urlSame } while (true) { try { const info = await getActiveAppInfo() const app = info?.localizedName || "Unknown" const title = info?.windowTitle || "" let url: string | undefined if (app === "Google Chrome") { try { url = await getActiveTab("Google Chrome") } catch { url = undefined } } const nextKey = { app, url } if (!sameKey(current, nextKey)) { const nowIso = new Date().toISOString() // finalize previous if (current) { current.end = nowIso const startMs = new Date(current.start).getTime() const endMs = new Date(current.end).getTime() current.durationMs = Math.max(0, endMs - startMs) sessions.push(current) await kv.set("sessions", sessions) } // start new current = { id: uuid(), app, title, url, start: nowIso, } } else { // update title if changed while staying in same key if (current && title && current.title !== title) { current.title = title } } } catch (err) { // ignore transient errors } await wait(intervalMs) } } if (mode === "report") { const range = await arg("Select range", [ { name: "Today", value: "today" }, { name: "Yesterday", value: "yesterday" }, { name: "Last 7 days", value: "last7" }, ]) const now = new Date() const startOfDay = (d: Date) => { const x = new Date(d) x.setHours(0, 0, 0, 0) return x } const endOfDay = (d: Date) => { const x = new Date(d) x.setHours(23, 59, 59, 999) return x } let start: Date let end: Date if (range === "today") { start = startOfDay(now) end = now } else if (range === "yesterday") { const y = new Date(now) y.setDate(y.getDate() - 1) start = startOfDay(y) end = endOfDay(y) } else { // last7 const s = new Date(now) s.setDate(s.getDate() - 6) start = startOfDay(s) end = now } const sessions: Session[] = (await kv.get<Session[]>("sessions")) || [] const overlapMs = (s: Session, startTime: number, endTime: number) => { const sStart = new Date(s.start).getTime() const sEnd = new Date(s.end || new Date().toISOString()).getTime() const a = Math.max(sStart, startTime) const b = Math.min(sEnd, endTime) return Math.max(0, b - a) } const startMs = start.getTime() const endMs = end.getTime() const filtered = sessions .map(s => { const ms = overlapMs(s, startMs, endMs) if (ms <= 0) return null const clippedStartMs = Math.max(new Date(s.start).getTime(), startMs) const clippedEndMs = Math.min(new Date(s.end || new Date().toISOString()).getTime(), endMs) return { ...s, start: new Date(clippedStartMs).toISOString(), end: new Date(clippedEndMs).toISOString(), durationMs: ms, } as Session }) .filter(Boolean) as Session[] const formatDuration = (ms: number) => { const sec = Math.floor(ms / 1000) const h = Math.floor(sec / 3600) const m = Math.floor((sec % 3600) / 60) const s = sec % 60 const parts = [] if (h) parts.push(`${h}h`) if (m) parts.push(`${m}m`) if (s || parts.length === 0) parts.push(`${s}s`) return parts.join(" ") } const totalByApp = new Map<string, number>() const domainByChrome = new Map<string, number>() let totalMs = 0 for (const s of filtered) { const ms = s.durationMs || 0 totalMs += ms totalByApp.set(s.app, (totalByApp.get(s.app) || 0) + ms) if (s.app === "Google Chrome" && s.url) { try { const u = new URL(s.url) const host = u.hostname || "unknown" domainByChrome.set(host, (domainByChrome.get(host) || 0) + ms) } catch { domainByChrome.set("unknown", (domainByChrome.get("unknown") || 0) + ms) } } } const appRows = Array.from(totalByApp.entries()).sort((a, b) => b[1] - a[1]) const domainRows = Array.from(domainByChrome.entries()).sort((a, b) => b[1] - a[1]) const rangeLabel = range === "today" ? "Today" : range === "yesterday" ? "Yesterday" : "Last 7 days" const header = `# Time Report (${rangeLabel}) From ${start.toLocaleString()} to ${end.toLocaleString()} Total Tracked: ${formatDuration(totalMs)} ` let appMd = `## By App\n` if (appRows.length === 0) { appMd += `No data found for selected range.` } else { appMd += appRows .map(([app, ms]) => `- ${app}: ${formatDuration(ms)} (${((ms / Math.max(1, totalMs)) * 100).toFixed(1)}%)`) .join("\n") } let chromeMd = `` if (domainRows.length > 0) { chromeMd += `\n\n## Chrome Domains\n` chromeMd += domainRows .map( ([host, ms]) => `- ${host}: ${formatDuration(ms)} (${((ms / Math.max(1, totalMs)) * 100).toFixed(1)}%)` ) .join("\n") } // CSV (clipped sessions in range) const csvHeader = ["app", "title", "url", "domain", "start", "end", "durationMs", "durationHuman"].join(",") const csvRows = [csvHeader] for (const s of filtered) { const app = s.app || "" const title = (s.title || "").replace(/"/g, '""') const url = (s.url || "").replace(/"/g, '""') let domain = "" if (s.url) { try { domain = new URL(s.url).hostname } catch { domain = "" } } const dMs = s.durationMs || 0 const row = [ `"${app.replace(/"/g, '""')}"`, `"${title}"`, `"${url}"`, `"${domain.replace(/"/g, '""')}"`, s.start, s.end || "", String(dMs), `"${formatDuration(dMs)}"`, ].join(",") csvRows.push(row) } const csv = csvRows.join("\n") await copy(csv) await notify("Report CSV copied to clipboard") await div( md( `${header} ${appMd} ${chromeMd} Note: A CSV with per-session rows has been copied to your clipboard.` ) ) }