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.
- package/README.md +7 -3
- package/dist/cli.js +145 -30
- 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.
|
|
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 ≈** (
|
|
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 ≈:**
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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 = () =>
|
|
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:
|
|
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 =
|
|
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:
|
|
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(
|
|
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
|
|
10576
|
-
/* @__PURE__ */
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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(`
|
|
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 =
|
|
12286
|
-
const statsPath =
|
|
12287
|
-
const pidPath =
|
|
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(
|
|
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.
|
|
4
|
-
"description": "The owl watching your AI coding spend
|
|
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": {
|