exe-watcher 0.2.68 → 0.2.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/dist/cli.js +145 -30
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -31,7 +31,7 @@ npm install -g exe-watcher
31
31
  exe-watcher report
32
32
  ```
33
33
 
34
- Version 0.2.68 resumes large Claude history scans and repairs previously skipped days within the existing resource limits. Recorded API-equivalent costs and saved totals are preserved through refresh failures, restarts and day changes. AI time and the floating quota display are included. Terminal installs require Node 22.13 or newer; the Mac app includes its own compatible runtime. Download the matching app from [Releases](https://github.com/AskExe/exe-watcher/releases/latest) or [odd.community/watcher](https://odd.community/watcher). To run the current CLI from source:
34
+ Version 0.2.70 adds local active AI app tracking and broader, clearly labeled historical personal-time estimates. It also explains that first-time history setup can take 10 minutes or more for large AI histories, with saved progress and recovery details. Large Claude history scans resume within the existing resource limits. Recorded API-equivalent costs and saved totals are preserved through refresh failures, restarts and day changes. AI time and the floating quota display are included. Terminal installs require Node 22.13 or newer; the Mac app includes its own compatible runtime. Download the matching app from [Releases](https://github.com/AskExe/exe-watcher/releases/latest) or [odd.community/watcher](https://odd.community/watcher). To run the current CLI from source:
35
35
 
36
36
  ```bash
37
37
  git clone https://github.com/AskExe/exe-watcher.git
@@ -45,7 +45,7 @@ node dist/cli.js report
45
45
 
46
46
  ## Built for AI employee teams
47
47
 
48
- - **Floating display** — a compact on-screen quota display for Claude, Codex, Kimi, Cursor, GitHub Copilot and Gemini, with **You + AI ≈** (your estimated interaction time) and **AI Time** (work time added across all agents). Quota rings show **quota left**, default to **Weekly**, and have a **Weekly / 5h** toggle. Click either time button for its explanation. Time totals default to Today; switch to Week, Month or All Time. Enable it from the menu bar footer’s **Display settings → Floating quota display**. [Details](mac/README.md#floating-display).
48
+ - **Floating display** — a compact on-screen quota display for Claude, Codex, Kimi, Cursor, GitHub Copilot and Gemini, with **You + AI ≈** (historical sessions plus tracked app activity) and **AI Time** (work time added across all agents). Quota rings show **quota left**, default to **Weekly**, and have a **Weekly / 5h** toggle. Click either time button for its explanation. Time totals default to Today; switch to Week, Month or All Time. Enable it from the menu bar footer’s **Display settings → Floating quota display**. [Details](mac/README.md#floating-display).
49
49
  - **AI running time** — adds recorded work across agents, so parallel work is counted as agent-hours.
50
50
  - **Elapsed busy time** — merges overlapping runs to show how long at least one agent was working.
51
51
  - **Your time ≈** — estimates interaction from short reply-to-next-prompt gaps, with known subagents excluded. It is not an attention or productivity measurement.
@@ -123,7 +123,11 @@ The menu bar dashboard and full terminal report show three times beneath the spe
123
123
 
124
124
  - **AI running:** recorded run time added across agents. Two agents running for one hour add two hours. Duplicate run IDs copied into forks count once.
125
125
  - **Elapsed busy:** time when at least one agent was running. Those two parallel agents count as one elapsed hour.
126
- - **Your time ≈:** an interaction estimate from reply-to-next-prompt gaps of up to five minutes, merged across chats. Known subagents and longer idle gaps are excluded. This does not measure attention, typing or time reading while an agent runs; unidentified automation may be included.
126
+ - **Your time ≈:** historical conversation sessions plus foreground app activity tracked on this Mac. Historical sessions join nearby main-chat turn starts with a 15-minute gap; the displayed sensitivity range uses 10–30 minutes. It includes some AI waiting, excludes known subagents and long unattended gaps, and may include main-chat automation. Isolated turns get no invented padding. This is an estimate of engagement, not attention or typing. The older five-minute reply-gap estimate remains available in JSON as `userActiveMs` and in CLI details.
127
+
128
+ **Track active AI app time on this Mac** can be switched off in the AI time section. It counts foreground Codex, Hansel, Claude and ChatGPT app activity, with a two-minute idle cutoff. Cursor, supported editors and terminals count only alongside a recent matching AI turn; these ambiguous intervals are marked as contextual estimates. Browsers and unsupported apps remain unknown. No keyboard contents, window titles, screenshots, accessibility access or Terminal control are needed. App switching, sleep and console-session notifications drive tracking; a single 30-second idle check runs only while an eligible app is in front. Late checks never fill sleep or scheduler gaps. Daily totals are saved privately, at most every five minutes while continuously active and on app switch/exit; a crash can lose the unsaved tail. No usage scan or extra CLI process is triggered. A writer lock prevents two Watcher copies counting the same foreground activity.
129
+
130
+ The historical estimate stops when app tracking first begins, so it is never added over the same time as app activity. Period and provider views use the same boundaries. App tracking gaps while Watcher is closed or tracking is off stay unknown. Daily app totals have no project attribution; project-filtered CLI reports show historical sessions only. The stored tracking timezone is fixed to avoid overlapping days after travel. Custom partial-day CLI queries omit daily app totals they cannot precisely locate.
127
131
 
128
132
  Timing recovery starts July 1, 2025 and uses retained local evidence. **Partial history** stays visible; missing timing is **Unknown**, never zero. Recorded durations may include tool execution and waiting, rather than only time generating tokens. Open runs are not extended to the present.
129
133
 
package/dist/cli.js CHANGED
@@ -6125,7 +6125,7 @@ init_cache_dir();
6125
6125
  init_file_lock();
6126
6126
  init_resource_budget();
6127
6127
  import { existsSync as existsSync8, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
6128
- import { join as join31 } from "path";
6128
+ import { join as join32 } from "path";
6129
6129
  import { homedir as homedir17 } from "os";
6130
6130
  import { Command } from "commander";
6131
6131
 
@@ -9227,7 +9227,7 @@ function owlFrame(tick) {
9227
9227
  }
9228
9228
 
9229
9229
  // src/dashboard.tsx
9230
- import { join as join30 } from "path";
9230
+ import { join as join31 } from "path";
9231
9231
 
9232
9232
  // src/time-index.ts
9233
9233
  init_cache_dir();
@@ -9235,14 +9235,86 @@ init_file_lock();
9235
9235
  init_resource_budget();
9236
9236
  import { createHash as createHash9, randomUUID as randomUUID5 } from "crypto";
9237
9237
  import { mkdir as mkdir13, open as open9, readFile as readFile14, rename as rename11, stat as stat19, unlink as unlink9, readdir as readdir15 } from "fs/promises";
9238
- import { basename as basename11, dirname as dirname6, join as join29 } from "path";
9238
+ import { basename as basename11, dirname as dirname6, join as join30 } from "path";
9239
9239
  import { gzip as gzip2, gunzip as gunzip2 } from "zlib";
9240
9240
  import { promisify as promisify2 } from "util";
9241
9241
 
9242
9242
  // src/time-tracking.ts
9243
9243
  import { createHash as createHash8 } from "crypto";
9244
+
9245
+ // src/personal-activity.ts
9246
+ init_cache_dir();
9247
+ import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2, constants as constants2, fstatSync as fstatSync2 } from "fs";
9248
+ import { join as join29 } from "path";
9249
+ var ACTIVITY_LIMIT = 4 * 1024 ** 2;
9250
+ function readActivityHistory() {
9251
+ let fd;
9252
+ try {
9253
+ fd = openSync2(join29(getCacheDir(), "personal-activity-v1.json"), constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
9254
+ const stat20 = fstatSync2(fd);
9255
+ if (!stat20.isFile() || stat20.size > ACTIVITY_LIMIT) return void 0;
9256
+ const buffer = Buffer.alloc(stat20.size);
9257
+ if (readSync2(fd, buffer, 0, buffer.length, 0) !== buffer.length) return void 0;
9258
+ const x = JSON.parse(buffer.toString("utf8"));
9259
+ if (x.version !== 1 || typeof x.enabled !== "boolean" || typeof x.startedAt !== "string" || typeof x.updatedAt !== "string" || !Array.isArray(x.days) || x.days.length > 36600 || !Number.isFinite(Date.parse(x.startedAt)) || !Number.isFinite(Date.parse(x.updatedAt))) return void 0;
9260
+ let end = 0;
9261
+ for (const day of x.days) {
9262
+ if (!Number.isFinite(day.start) || !Number.isFinite(day.end) || day.start < end || day.end <= day.start || day.end - day.start > 26 * 36e5) return void 0;
9263
+ let total = 0;
9264
+ for (const map of [day.active, day.contextual]) {
9265
+ if (!map || typeof map !== "object" || Array.isArray(map) || Object.keys(map).length > 32) return void 0;
9266
+ for (const value of Object.values(map)) {
9267
+ if (!Number.isFinite(value) || value < 0) return void 0;
9268
+ total += value;
9269
+ }
9270
+ }
9271
+ if (total > day.end - day.start) return void 0;
9272
+ end = day.end;
9273
+ }
9274
+ return x;
9275
+ } catch {
9276
+ return void 0;
9277
+ } finally {
9278
+ if (fd !== void 0) closeSync2(fd);
9279
+ }
9280
+ }
9281
+ function summarizeActivity(history, range, provider = "all") {
9282
+ if (!history) return void 0;
9283
+ let activeMs = 0, contextualMs = 0;
9284
+ for (const day of history.days) {
9285
+ const overlap = Math.max(0, Math.min(day.end, range.end.getTime()) - Math.max(day.start, range.start.getTime()));
9286
+ if (!overlap) continue;
9287
+ if (range.start.getTime() > day.start || range.end.getTime() < Math.min(day.end, Date.parse(history.updatedAt))) continue;
9288
+ const sum2 = (map) => provider === "all" ? Object.values(map).reduce((a, b) => a + b, 0) : Object.hasOwn(map, provider) ? map[provider] : 0;
9289
+ activeMs += sum2(day.active);
9290
+ contextualMs += sum2(day.contextual);
9291
+ }
9292
+ return { activeMs, contextualMs, startedAt: history.startedAt, updatedAt: history.updatedAt, enabled: history.enabled };
9293
+ }
9294
+
9295
+ // src/time-tracking.ts
9244
9296
  var TIME_HISTORY_START = (/* @__PURE__ */ new Date("2025-07-01T00:00:00")).getTime();
9245
9297
  var TIME_PROVIDERS = ["claude", "codex", "cursor", "cursor-agent", "copilot", "opencode", "kimi", "pi", "omp"];
9298
+ function personalTimeMs(time) {
9299
+ if (!time) return null;
9300
+ if (time.personalTimeVersion !== 1 && !time.sessionEstimate) return time.userActiveMs;
9301
+ if (!time.sessionEstimate && !time.appActivity) return null;
9302
+ return (time.sessionEstimate?.milliseconds ?? 0) + (time.appActivity?.activeMs ?? 0) + (time.appActivity?.contextualMs ?? 0);
9303
+ }
9304
+ function estimateSessions(points, range, through = range.end.getTime()) {
9305
+ const sorted = [...new Set(points.filter(Number.isFinite))].sort((a, b) => a - b);
9306
+ const end = Math.min(through, range.end.getTime(), Date.now());
9307
+ const start = Math.max(range.start.getTime(), TIME_HISTORY_START);
9308
+ let milliseconds = 0, lowerMs = 0, upperMs = 0;
9309
+ for (let i = 1; i < sorted.length; i++) {
9310
+ const a = sorted[i - 1], b = sorted[i], gap = b - a;
9311
+ const duration = Math.max(0, Math.min(b, end) - Math.max(a, start));
9312
+ if (gap <= 30 * 6e4) upperMs += duration;
9313
+ if (gap <= 15 * 6e4) milliseconds += duration;
9314
+ if (gap <= 10 * 6e4) lowerMs += duration;
9315
+ }
9316
+ return upperMs > 0 ? { milliseconds, lowerMs, upperMs, method: "main-chat-gaps-v1", through: new Date(end).toISOString() } : void 0;
9317
+ }
9246
9318
  function timestamp(value) {
9247
9319
  const n = typeof value === "number" ? value < 1e11 ? value * 1e3 : value : typeof value === "string" ? Date.parse(value) : NaN;
9248
9320
  return Number.isFinite(n) && n >= TIME_HISTORY_START && n <= Date.now() + 864e5 ? n : void 0;
@@ -9394,7 +9466,7 @@ function unionIntervals(intervals, range) {
9394
9466
  return out;
9395
9467
  }
9396
9468
  var sum = (rows2) => rows2.reduce((n, [a, b]) => n + b - a, 0);
9397
- function summarizeTime(index, range, provider = "all", include, exclude) {
9469
+ function summarizeTime(index, range, provider = "all", include, exclude, activity) {
9398
9470
  const matches = (name, terms) => terms?.some((t) => name.toLowerCase().includes(t.toLowerCase()));
9399
9471
  const files = Object.values(index.files).filter((f) => (provider === "all" || f.provider === provider) && (!include?.length || matches(f.project, include)) && !matches(f.project, exclude));
9400
9472
  const agents = /* @__PURE__ */ new Map(), estimatedAgents = /* @__PURE__ */ new Map(), user = [];
@@ -9414,9 +9486,16 @@ function summarizeTime(index, range, provider = "all", include, exclude) {
9414
9486
  states.set(file, copies);
9415
9487
  }
9416
9488
  const consumed = /* @__PURE__ */ new Set();
9489
+ const mainTurns = [];
9490
+ const recentMainTurns = /* @__PURE__ */ new Map();
9417
9491
  const byProvider = /* @__PURE__ */ new Map();
9418
9492
  let pendingFiles = 0;
9419
9493
  for (const file of files) {
9494
+ for (const original of [file.state, ...Object.values(file.state.children ?? {})]) {
9495
+ if (!original.child && original.pending !== void 0 && original.pending <= Date.now()) {
9496
+ recentMainTurns.set(file.provider, Math.max(recentMainTurns.get(file.provider) ?? 0, original.pending));
9497
+ }
9498
+ }
9420
9499
  if (file.offset < file.size || file.unavailable) pendingFiles++;
9421
9500
  const row = byProvider.get(file.provider) ?? { files: 0, timedFiles: 0, agents: /* @__PURE__ */ new Map() };
9422
9501
  row.files++;
@@ -9431,6 +9510,12 @@ function summarizeTime(index, range, provider = "all", include, exclude) {
9431
9510
  return true;
9432
9511
  });
9433
9512
  const ai = unionIntervals(unique, range);
9513
+ if (!state.child) {
9514
+ for (const interval2 of unique) {
9515
+ mainTurns.push(interval2[0]);
9516
+ if (interval2[0] <= Date.now()) recentMainTurns.set(file.provider, Math.max(recentMainTurns.get(file.provider) ?? 0, interval2[0]));
9517
+ }
9518
+ }
9434
9519
  const inferred = estimatedAgents.get(state.agent) ?? [];
9435
9520
  inferred.push(...unique.filter((interval2) => interval2[2]));
9436
9521
  estimatedAgents.set(state.agent, inferred);
@@ -9447,6 +9532,9 @@ function summarizeTime(index, range, provider = "all", include, exclude) {
9447
9532
  const all = [...agents.values()].flatMap((v) => unionIntervals(v, range));
9448
9533
  const estimated = [...estimatedAgents.values()].flatMap((v) => unionIntervals(v, range));
9449
9534
  const busy = unionIntervals(all, range), human = unionIntervals(user, range);
9535
+ const cutover = activity ? Date.parse(activity.startedAt) : range.end.getTime();
9536
+ const sessionEstimate = estimateSessions(mainTurns, range, cutover);
9537
+ const appActivity = include?.length || exclude?.length ? void 0 : summarizeActivity(activity, range, provider);
9450
9538
  const pending = files.filter((f) => f.offset < f.size || f.unavailable);
9451
9539
  const waitingFiles = pending.filter((f) => f.waitingForAppend && !f.unavailable).length;
9452
9540
  const recovery = {
@@ -9464,6 +9552,9 @@ function summarizeTime(index, range, provider = "all", include, exclude) {
9464
9552
  elapsedBusyMs: busy.length ? sum(busy) : null,
9465
9553
  userActiveMs: human.length ? sum(human) : null,
9466
9554
  estimatedAiMs: sum(estimated),
9555
+ personalTimeVersion: 1,
9556
+ sessionEstimate,
9557
+ appActivity,
9467
9558
  coverage: all.length ? "partial" : "missing",
9468
9559
  pendingFiles,
9469
9560
  indexedFiles: files.length,
@@ -9472,11 +9563,13 @@ function summarizeTime(index, range, provider = "all", include, exclude) {
9472
9563
  providers: (provider === "all" ? TIME_PROVIDERS : [provider]).map((name) => {
9473
9564
  const row = byProvider.get(name);
9474
9565
  const duration = row ? [...row.agents.values()].reduce((n, a) => n + sum(unionIntervals(a, range)), 0) : 0;
9475
- return { name, files: row?.files ?? 0, timedFiles: row?.timedFiles ?? 0, aiRunningMs: duration || null, status: duration ? "partial" : "missing" };
9566
+ return { name, files: row?.files ?? 0, timedFiles: row?.timedFiles ?? 0, aiRunningMs: duration || null, status: duration ? "partial" : "missing", recentMainTurnAt: recentMainTurns.get(name) };
9476
9567
  }),
9477
9568
  notes: [
9478
9569
  "Retained local records only, since July 1, 2025. Missing history is unknown, not zero. Run time can include tools and waiting.",
9479
- "Your time is an estimate of reply-to-next-prompt gaps up to 5 minutes, merged across chats. Longer idle gaps and subagents are excluded; unidentified automation may be included. It does not measure attention.",
9570
+ "Legacy short reply gaps estimate reply-to-next-prompt gaps up to 5 minutes, merged across chats. Longer gaps and subagents are excluded; unidentified automation may be included. It does not measure attention.",
9571
+ ...sessionEstimate ? ["Historical session time uses gaps between main-chat turn starts: 15-minute continuity, with a 10\u201330-minute sensitivity range. Overlap counts once; subagents, isolated turns and long gaps are excluded. This includes some AI waiting, may include main-chat automation, and cannot measure attention."] : [],
9572
+ ...appActivity ? ["App activity uses foreground app and idle metadata from this Mac. Terminal/editor time needs a recent matching AI session and is separately marked as contextual. Browsers and unsupported apps are unknown. Daily totals have no project attribution; tracking gaps remain unknown."] : [],
9480
9573
  ...estimated.length ? ["Some AI time is inferred from prompt/response timestamps, limited to spans up to 30 minutes. Longer unmeasured gaps stay unknown."] : [],
9481
9574
  ...recovery.runnableFiles ? ["Timing recovery is in progress. Queued timing work continues in bounded steps; usage costs are unaffected."] : [],
9482
9575
  ...waitingFiles ? ["Some logs end mid-record. Their incomplete records remain unknown until more data is saved."] : [],
@@ -9586,7 +9679,7 @@ var MAX_DISK2 = 16 * 1024 ** 2;
9586
9679
  var MAX_FILES = 2e4;
9587
9680
  var BLOCK = 2 * 1024 ** 2;
9588
9681
  var adapterVersion = (provider) => provider === "opencode" ? 2 : 1;
9589
- var indexPath2 = () => join29(getCacheDir(), "time-index-v1.json.gz");
9682
+ var indexPath2 = () => join30(getCacheDir(), "time-index-v1.json.gz");
9590
9683
  var empty = () => ({ version: 1, files: /* @__PURE__ */ Object.create(null), updatedAt: (/* @__PURE__ */ new Date()).toISOString(), warnings: [] });
9591
9684
  async function readTimeIndex() {
9592
9685
  try {
@@ -9649,11 +9742,11 @@ async function expandSources(sources, warnings) {
9649
9742
  continue;
9650
9743
  }
9651
9744
  for (const entry of await readdir15(source.path, { withFileTypes: true }).catch(() => [])) {
9652
- if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push({ ...source, path: join29(source.path, entry.name) });
9745
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push({ ...source, path: join30(source.path, entry.name) });
9653
9746
  else if (entry.isDirectory()) {
9654
- const folder = join29(source.path, entry.name, "subagents");
9747
+ const folder = join30(source.path, entry.name, "subagents");
9655
9748
  for (const agent of await readdir15(folder, { withFileTypes: true }).catch(() => [])) {
9656
- if (agent.isFile() && agent.name.endsWith(".jsonl")) out.push({ ...source, path: join29(folder, agent.name) });
9749
+ if (agent.isFile() && agent.name.endsWith(".jsonl")) out.push({ ...source, path: join30(folder, agent.name) });
9657
9750
  }
9658
9751
  }
9659
9752
  }
@@ -9750,7 +9843,7 @@ async function refreshTimeIndex(options = {}) {
9750
9843
  let current;
9751
9844
  let release;
9752
9845
  try {
9753
- release = await acquireFileLock(join29(getCacheDir(), "time-index.lock"), 100);
9846
+ release = await acquireFileLock(join30(getCacheDir(), "time-index.lock"), 100);
9754
9847
  } catch {
9755
9848
  current = await readTimeIndex();
9756
9849
  current.warnings.push("Another timing update is running; showing the saved timing snapshot.");
@@ -9853,12 +9946,13 @@ async function refreshTimeIndex(options = {}) {
9853
9946
  return current;
9854
9947
  }
9855
9948
  async function getTimeReport(range, provider = "all", include, exclude, signal) {
9949
+ const activity = readActivityHistory();
9856
9950
  try {
9857
- return summarizeTime(await refreshTimeIndex({ signal }), range, provider, include, exclude);
9951
+ return summarizeTime(await refreshTimeIndex({ signal }), range, provider, include, exclude, activity);
9858
9952
  } catch (error) {
9859
9953
  const index = await readTimeIndex();
9860
9954
  index.warnings.push(`Timing unavailable: ${error instanceof Error ? error.message : String(error)}`);
9861
- return summarizeTime(index, range, provider, include, exclude);
9955
+ return summarizeTime(index, range, provider, include, exclude, activity);
9862
9956
  }
9863
9957
  }
9864
9958
 
@@ -10572,8 +10666,25 @@ function TimePanel({ time, width }) {
10572
10666
  ] }),
10573
10667
  /* @__PURE__ */ jsxs2(Text2, { children: [
10574
10668
  /* @__PURE__ */ jsx2(Text2, { bold: true, children: "Your time \u2248 " }),
10575
- formatDuration(time?.userActiveMs ?? null),
10576
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: " \xB7 interaction-gap estimate, not attention" })
10669
+ formatDuration(personalTimeMs(time)),
10670
+ /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
10671
+ " \xB7 ",
10672
+ time?.personalTimeVersion === 1 || time?.sessionEstimate ? "historical sessions + tracked app activity" : "legacy short reply gaps"
10673
+ ] })
10674
+ ] }),
10675
+ time?.sessionEstimate && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
10676
+ "Historical range: ",
10677
+ formatDuration(time.sessionEstimate.lowerMs),
10678
+ " \u2013 ",
10679
+ formatDuration(time.sessionEstimate.upperMs),
10680
+ " (10\u201330 min session gaps)"
10681
+ ] }),
10682
+ time?.appActivity && /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
10683
+ "App activity: ",
10684
+ formatDuration(time.appActivity.activeMs + time.appActivity.contextualMs),
10685
+ " \xB7 ",
10686
+ formatDuration(time.appActivity.contextualMs),
10687
+ " inferred terminal/editor context"
10577
10688
  ] }),
10578
10689
  /* @__PURE__ */ jsxs2(Text2, { color: ORANGE3, children: [
10579
10690
  time?.aiRunningMs != null ? "Partial history" : "Timing unknown",
@@ -10697,11 +10808,11 @@ function InteractiveDashboard({ initialPeriod, initialProvider, refreshSeconds,
10697
10808
  useEffect2(() => {
10698
10809
  let cancelled = false;
10699
10810
  async function loadBudgets() {
10700
- const claudeDir = join30(homedir16(), ".claude", "projects");
10811
+ const claudeDir = join31(homedir16(), ".claude", "projects");
10701
10812
  const budgets = /* @__PURE__ */ new Map();
10702
10813
  for (const project of projects.slice(0, 8)) {
10703
10814
  if (cancelled) return;
10704
- const cwd = await discoverProjectCwd(join30(claudeDir, project.project));
10815
+ const cwd = await discoverProjectCwd(join31(claudeDir, project.project));
10705
10816
  if (!cwd) continue;
10706
10817
  budgets.set(project.project, await estimateContextBudget(cwd));
10707
10818
  }
@@ -11246,8 +11357,8 @@ function watchMenubarParent(parentPid, runtime = live) {
11246
11357
  }
11247
11358
 
11248
11359
  // src/time-snapshots.ts
11249
- function buildTimeSnapshots(index, ranges, provider = "all", include, exclude) {
11250
- const views = (scope) => Object.fromEntries(Object.entries(ranges).map(([key2, range]) => [key2, summarizeTime(index, range, scope, include, exclude)]));
11360
+ function buildTimeSnapshots(index, ranges, provider = "all", include, exclude, activity) {
11361
+ const views = (scope) => Object.fromEntries(Object.entries(ranges).map(([key2, range]) => [key2, summarizeTime(index, range, scope, include, exclude, activity)]));
11251
11362
  const periods = views(provider), allPeriods = provider === "all" ? void 0 : views("all");
11252
11363
  return { generated: (/* @__PURE__ */ new Date()).toISOString(), provider, periods, allPeriods, recovery: (allPeriods ?? periods)["all"].recovery };
11253
11364
  }
@@ -11875,12 +11986,12 @@ program.command("status").description("Compact status output (today + week + mon
11875
11986
  `);
11876
11987
  }
11877
11988
  }
11878
- const exeOsDir = join31(homedir17(), ".exe-os");
11989
+ const exeOsDir = join32(homedir17(), ".exe-os");
11879
11990
  const exeOsDetected = existsSync8(exeOsDir);
11880
11991
  let agentStats = null;
11881
11992
  let statsFileAge = null;
11882
11993
  try {
11883
- const statsPath = join31(exeOsDir, "agent-stats.json");
11994
+ const statsPath = join32(exeOsDir, "agent-stats.json");
11884
11995
  if (existsSync8(statsPath)) {
11885
11996
  statsFileAge = Math.round((Date.now() - statSync3(statsPath).mtimeMs) / 1e3);
11886
11997
  const parsed = JSON.parse(readFileSync2(statsPath, "utf-8"));
@@ -11940,9 +12051,10 @@ program.command("status").description("Compact status output (today + week + mon
11940
12051
  warnings
11941
12052
  ) : void 0;
11942
12053
  const timeIndex = await refreshTimeIndex();
11943
- const time = summarizeTime(timeIndex, getDateRange2(opts.period, now).range, pf, opts.project, opts.exclude);
12054
+ const activity = readActivityHistory();
12055
+ const time = summarizeTime(timeIndex, getDateRange2(opts.period, now).range, pf, opts.project, opts.exclude, activity);
11944
12056
  if (periodSnapshots) for (const [key2, snapshot] of Object.entries(periodSnapshots)) {
11945
- snapshot.time = summarizeTime(timeIndex, getDateRange2(key2, now).range, pf, opts.project, opts.exclude);
12057
+ snapshot.time = summarizeTime(timeIndex, getDateRange2(key2, now).range, pf, opts.project, opts.exclude, activity);
11946
12058
  }
11947
12059
  const json = JSON.stringify({
11948
12060
  ...payload,
@@ -12250,20 +12362,23 @@ program.command("plan [action] [id]").description("Show or configure a subscript
12250
12362
  });
12251
12363
  program.command("time").description("AI running time, elapsed busy time and estimated interaction time since July 1, 2025").option("-p, --period <period>", "today, week, 30days, month, all", "all").option("--provider <provider>", "Filter by coding client", "all").option("--project <names...>", "Include projects by name").option("--exclude <names...>", "Exclude projects by name").option("--recover", "Advance one bounded historical recovery pass; safe to resume").option("--snapshots", "Return timing-only period snapshots for the menu bar").option("--format <format>", "text or json", "text").action(async (opts) => {
12252
12364
  const index = await refreshTimeIndex({ history: opts.recover === true });
12365
+ const activity = readActivityHistory();
12253
12366
  if (opts.snapshots) {
12254
12367
  const now = /* @__PURE__ */ new Date();
12255
12368
  const ranges = Object.fromEntries(["today", "week", "30days", "month", "all"].map((key2) => [key2, getDateRange2(key2, now).range]));
12256
- console.log(JSON.stringify(buildTimeSnapshots(index, ranges, opts.provider, opts.project, opts.exclude)));
12369
+ console.log(JSON.stringify(buildTimeSnapshots(index, ranges, opts.provider, opts.project, opts.exclude, activity)));
12257
12370
  return;
12258
12371
  }
12259
- const time = summarizeTime(index, getDateRange2(opts.period).range, opts.provider, opts.project, opts.exclude);
12372
+ const time = summarizeTime(index, getDateRange2(opts.period).range, opts.provider, opts.project, opts.exclude, activity);
12260
12373
  if (opts.format === "json") console.log(JSON.stringify(time));
12261
12374
  else {
12262
12375
  console.log(`
12263
12376
  AI time \xB7 ${getDateRange2(opts.period).label}`);
12264
12377
  console.log(` AI running: ${formatDuration(time.aiRunningMs)} (added across agents)`);
12265
12378
  console.log(` Elapsed busy: ${formatDuration(time.elapsedBusyMs)} (overlap counted once)`);
12266
- console.log(` Your time: ${formatDuration(time.userActiveMs)} (estimate)`);
12379
+ if (time.sessionEstimate) console.log(` Past sessions \u2248 ${formatDuration(time.sessionEstimate.milliseconds)} (10\u201330 min range: ${formatDuration(time.sessionEstimate.lowerMs)} \u2013 ${formatDuration(time.sessionEstimate.upperMs)})`);
12380
+ if (time.appActivity) console.log(` App activity: ${formatDuration(time.appActivity.activeMs + time.appActivity.contextualMs)} (${formatDuration(time.appActivity.contextualMs)} inferred terminal/editor context; tracking ${time.appActivity.enabled ? "on" : "off"})`);
12381
+ console.log(` Short reply gaps: ${formatDuration(time.userActiveMs)} (legacy interaction estimate)`);
12267
12382
  console.log(` Partial history \xB7 ${time.indexedFiles} files \xB7 ${time.pendingFiles} awaiting recovery`);
12268
12383
  for (const row of time.providers) console.log(` ${row.name}: ${formatDuration(row.aiRunningMs)} \xB7 ${row.timedFiles}/${row.files} files with timing in this period`);
12269
12384
  for (const note of time.notes) console.log(` ${note}`);
@@ -12282,9 +12397,9 @@ program.command("compare").description("Compare two AI models side-by-side").opt
12282
12397
  await renderCompare(range, opts.provider);
12283
12398
  });
12284
12399
  program.command("agent-status").description("Diagnose exe-os agent data pipeline").action(() => {
12285
- const exeOsDir = join31(homedir17(), ".exe-os");
12286
- const statsPath = join31(exeOsDir, "agent-stats.json");
12287
- const pidPath = join31(exeOsDir, "exed.pid");
12400
+ const exeOsDir = join32(homedir17(), ".exe-os");
12401
+ const statsPath = join32(exeOsDir, "agent-stats.json");
12402
+ const pidPath = join32(exeOsDir, "exed.pid");
12288
12403
  const dirExists = existsSync8(exeOsDir);
12289
12404
  const statsExists = dirExists && existsSync8(statsPath);
12290
12405
  const pidExists = dirExists && existsSync8(pidPath);
@@ -12319,7 +12434,7 @@ program.command("agent-status").description("Diagnose exe-os agent data pipeline
12319
12434
  var releaseScan;
12320
12435
  var stopParentWatch = watchMenubarParent(Number(process.env["EXE_WATCHER_PARENT_PID"]));
12321
12436
  program.hook("preAction", async (_root, command) => {
12322
- if (command.name() === "status" && !command.opts().cachedOnly) releaseScan = await acquireFileLock(join31(getCacheDir(), "scan.lock"));
12437
+ if (command.name() === "status" && !command.opts().cachedOnly) releaseScan = await acquireFileLock(join32(getCacheDir(), "scan.lock"));
12323
12438
  });
12324
12439
  program.hook("postAction", async () => {
12325
12440
  await releaseScan?.();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "exe-watcher",
3
- "version": "0.2.68",
4
- "description": "The owl watching your AI coding spend — cost, tokens, agents, and projects in your terminal and menu bar",
3
+ "version": "0.2.70",
4
+ "description": "The owl watching your AI coding spend \u2014 cost, tokens, agents, and projects in your terminal and menu bar",
5
5
  "type": "module",
6
6
  "main": "./dist/cli.js",
7
7
  "bin": {