@timetrakkr/cli 0.0.0-stage → 1.0.2

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.
@@ -0,0 +1,2869 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // packages/agent-cli/src/cli.ts
5
+ var import_node_child_process2 = require("node:child_process");
6
+
7
+ // packages/agent-cli/src/browser.ts
8
+ var OPENABLE = /^https?:\/\/[A-Za-z0-9._~:/?&=%-]+$/;
9
+ function browserCommand(platform, url) {
10
+ if (!OPENABLE.test(url)) {
11
+ return null;
12
+ }
13
+ if (platform === "darwin") {
14
+ return { command: "open", args: [url] };
15
+ }
16
+ if (platform === "win32") {
17
+ return { command: "rundll32", args: ["url.dll,FileProtocolHandler", url] };
18
+ }
19
+ return { command: "xdg-open", args: [url] };
20
+ }
21
+
22
+ // packages/agent-cli/src/constants.ts
23
+ var HOOK_BUDGET_MS = 1200;
24
+ var NETWORK_BUDGET_MS = 900;
25
+ var STDIN_BUDGET_MS = 300;
26
+ var COMMAND_NETWORK_MS = 5e3;
27
+ var GIT_TIMEOUT_MS = 400;
28
+ var CONFIG_TTL_MS = 5 * 6e4;
29
+ var STALE_SESSION_MS = 10 * 6e4;
30
+ var RETRY_WINDOW_MS = 7 * 24 * 60 * 6e4;
31
+ var PRUNE_AFTER_MS = 30 * 24 * 60 * 6e4;
32
+ var TRANSCRIPT_READ_BYTES = 8 * 1024 * 1024;
33
+ var RECENT_MESSAGES = 500;
34
+ var REPLY_SLACK_MS = 5e3;
35
+ var RENEW_MARGIN_MS = 5 * 6e4;
36
+ var RENEW_POLL_MS = 40;
37
+ var EARLY_RENEW_WAIT_MS = 400;
38
+ var RENEW_TIMEOUT_MS = 1e4;
39
+ var LOCK_POLL_MS = 50;
40
+ var LOCK_STALE_MS = 15e3;
41
+ var LOGIN_TIMEOUT_MS = 5 * 6e4;
42
+ var LOGIN_LOCK_STALE_MS = LOGIN_TIMEOUT_MS + 6e4;
43
+ var LOGIN_LINK_WAIT_MS = 5e3;
44
+ var MAX_TOKEN_MODELS = 20;
45
+ var HOURS_PER_DAY = 8;
46
+ var DAYS_PER_WEEK = 5;
47
+ var MAX_ESTIMATE_SECONDS = 31536e3;
48
+ var MAX_ESTIMATE_LABEL = 200;
49
+ var ERROR_LOG_MAX_BYTES = 256 * 1024;
50
+ var MAPPING_FILE = ".timetrakkr.json";
51
+ var MAPPING_MAX_BYTES = 16 * 1024;
52
+ var MAX_REPOSITORY_LENGTH = 500;
53
+ var MAX_BRANCH_LENGTH = 255;
54
+ var MAX_MODEL_LENGTH = 100;
55
+ var SECONDS_PER_DAY = 86400;
56
+ var MAX_TURN_COUNT = 1e4;
57
+ var WINDOW_SLACK_SECONDS = 60;
58
+ var MAX_DAY_AGE_DAYS = 10;
59
+ var HOSTS = ["claude-code", "codex", "cursor"];
60
+ var HOST_LABELS = {
61
+ "claude-code": "Claude Code",
62
+ codex: "Codex",
63
+ cursor: "Cursor"
64
+ };
65
+ var HOST_SESSION_ENV = {
66
+ "claude-code": "CLAUDE_CODE_SESSION_ID",
67
+ codex: "CODEX_SESSION_ID",
68
+ cursor: null
69
+ };
70
+ var HOST_SIGN_IN_HINT = {
71
+ "claude-code": "Run /timetrakkr:login to sign in.",
72
+ codex: 'Run "timetrakkr login" in a terminal, or ask Codex to use the timetrakkr-login skill.',
73
+ cursor: 'Run /timetrakkr-login in the agent chat, or "timetrakkr login" in a terminal.'
74
+ };
75
+ function isHost(value) {
76
+ return HOSTS.some((host) => host === value);
77
+ }
78
+
79
+ // packages/agent-cli/src/private-fs.ts
80
+ var import_node_fs = require("node:fs");
81
+ var import_node_path = require("node:path");
82
+ var DIR_MODE = 448;
83
+ var FILE_MODE = 384;
84
+ function ensurePrivateDir(dir) {
85
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: DIR_MODE });
86
+ if (((0, import_node_fs.statSync)(dir).mode & 511) !== DIR_MODE) {
87
+ (0, import_node_fs.chmodSync)(dir, DIR_MODE);
88
+ }
89
+ }
90
+ function writePrivateJson(path, value) {
91
+ ensurePrivateDir((0, import_node_path.dirname)(path));
92
+ const temporary = `${path}.${process.pid}.tmp`;
93
+ (0, import_node_fs.writeFileSync)(temporary, `${JSON.stringify(value)}
94
+ `, { mode: FILE_MODE });
95
+ (0, import_node_fs.renameSync)(temporary, path);
96
+ }
97
+ function appendPrivateLine(path, line) {
98
+ ensurePrivateDir((0, import_node_path.dirname)(path));
99
+ (0, import_node_fs.appendFileSync)(path, `${line}
100
+ `, { mode: FILE_MODE });
101
+ }
102
+ function readJson(path) {
103
+ try {
104
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+ function isRecord(value) {
110
+ return typeof value === "object" && value !== null && !Array.isArray(value);
111
+ }
112
+
113
+ // packages/agent-cli/src/text.ts
114
+ var MAX_LINE = 200;
115
+ function oneLine(text, max = MAX_LINE) {
116
+ const flat = text.replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
117
+ return flat.slice(0, max);
118
+ }
119
+
120
+ // packages/agent-cli/src/api.ts
121
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 408, 425, 429]);
122
+ var MAX_ERROR_LENGTH = 200;
123
+ var MEMBER_ID = /^[0-9a-f-]{36}$/i;
124
+ async function request(credentials, method, path, signal, body) {
125
+ return fetch(`${credentials.baseUrl}${path}`, {
126
+ method,
127
+ signal,
128
+ // A redirect would hand the token to wherever it points.
129
+ redirect: "error",
130
+ headers: {
131
+ authorization: `Bearer ${credentials.token}`,
132
+ accept: "application/json",
133
+ ...body === void 0 ? {} : { "content-type": "application/json" }
134
+ },
135
+ body: body === void 0 ? void 0 : JSON.stringify(body)
136
+ });
137
+ }
138
+ async function readJsonBody(response) {
139
+ try {
140
+ return await response.json();
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ function failureText(status, body) {
146
+ const message = isRecord(body) && typeof body.message === "string" ? body.message : "";
147
+ const text = message ? `HTTP ${status}: ${message}` : `HTTP ${status}`;
148
+ return text.replace(/\s+/g, " ").slice(0, MAX_ERROR_LENGTH);
149
+ }
150
+ async function describeFailure(response) {
151
+ return failureText(response.status, await readJsonBody(response));
152
+ }
153
+ function disabledReason(body) {
154
+ const reason = isRecord(body) ? body.disabled_reason : null;
155
+ const message = isRecord(reason) && typeof reason.message === "string" ? reason.message : "";
156
+ return oneLine(message) || null;
157
+ }
158
+ function describeThrown(error) {
159
+ if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
160
+ return "timed out";
161
+ }
162
+ return "no connection";
163
+ }
164
+ function parseBlocked(value) {
165
+ const { id, remote_sha256: remoteSha256, branch_pattern: branchPattern } = value;
166
+ if (typeof id !== "string" || typeof remoteSha256 !== "string" || !/^[0-9a-f]{64}$/.test(remoteSha256) || branchPattern !== null && typeof branchPattern !== "string") {
167
+ return null;
168
+ }
169
+ return { id, blocked: true, branchPattern, remoteSha256 };
170
+ }
171
+ function parseMapping(value) {
172
+ if (!isRecord(value)) {
173
+ return null;
174
+ }
175
+ if (value.blocked === true) {
176
+ return parseBlocked(value);
177
+ }
178
+ const {
179
+ id,
180
+ remote,
181
+ branch_pattern: branchPattern,
182
+ project_id: projectId,
183
+ task_id: taskId
184
+ } = value;
185
+ if (typeof id !== "string" || typeof remote !== "string" || typeof projectId !== "string" || typeof taskId !== "string" || branchPattern !== null && typeof branchPattern !== "string") {
186
+ return null;
187
+ }
188
+ const name = (key) => typeof value[key] === "string" ? oneLine(String(value[key])) : "";
189
+ return {
190
+ id,
191
+ remote,
192
+ branchPattern,
193
+ projectId,
194
+ taskId,
195
+ projectName: name("project_name"),
196
+ clientName: name("client_name") || null,
197
+ taskName: name("task_name")
198
+ };
199
+ }
200
+ function parseMappings(value) {
201
+ return Array.isArray(value) ? value.map(parseMapping).filter((rule) => rule !== null) : [];
202
+ }
203
+ async function fetchConfig(credentials, signal) {
204
+ try {
205
+ const response = await request(credentials, "GET", "/api/v1/agent/config", signal);
206
+ if (!response.ok) {
207
+ return { ok: false, error: await describeFailure(response), status: response.status };
208
+ }
209
+ const body = await readJsonBody(response);
210
+ if (!isRecord(body) || typeof body.enabled !== "boolean" || !isRecord(body.workspace)) {
211
+ return { ok: false, error: "unexpected response", status: response.status };
212
+ }
213
+ const name = typeof body.workspace.name === "string" ? body.workspace.name : "";
214
+ const memberId = isRecord(body.member) ? body.member.id : null;
215
+ return {
216
+ ok: true,
217
+ enabled: body.enabled,
218
+ workspaceName: name,
219
+ memberId: typeof memberId === "string" && MEMBER_ID.test(memberId) ? memberId : null,
220
+ // Only meaningful while off.
221
+ offReason: body.enabled ? null : disabledReason(body),
222
+ mappings: parseMappings(body.mappings)
223
+ };
224
+ } catch (error) {
225
+ return { ok: false, error: describeThrown(error) };
226
+ }
227
+ }
228
+ async function putSessionDay(credentials, body, signal) {
229
+ try {
230
+ const response = await request(credentials, "PUT", "/api/v1/agent/session_days", signal, body);
231
+ if (response.ok) {
232
+ return { kind: "ok" };
233
+ }
234
+ const answer = await readJsonBody(response);
235
+ const off = response.status === 403 ? disabledReason(answer) : null;
236
+ if (off) {
237
+ return { kind: "off", reason: off };
238
+ }
239
+ const error = failureText(response.status, answer);
240
+ const retry = response.status >= 500 || RETRYABLE_STATUSES.has(response.status);
241
+ return retry ? { kind: "retry", error, status: response.status } : { kind: "rejected", status: response.status, error };
242
+ } catch (error) {
243
+ return { kind: "retry", error: describeThrown(error) };
244
+ }
245
+ }
246
+
247
+ // packages/agent-cli/src/auth-store.ts
248
+ var import_node_crypto = require("node:crypto");
249
+ var import_node_fs2 = require("node:fs");
250
+
251
+ // packages/agent-cli/src/paths.ts
252
+ var import_node_os = require("node:os");
253
+ var import_node_path2 = require("node:path");
254
+ var SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
255
+ function isSafeSessionId(id) {
256
+ return typeof id === "string" && SAFE_ID.test(id);
257
+ }
258
+ function stateHome(env) {
259
+ return env.TIMETRAKKR_HOME || (0, import_node_path2.join)((0, import_node_os.homedir)(), ".timetrakkr");
260
+ }
261
+ function sessionsDir(home) {
262
+ return (0, import_node_path2.join)(home, "sessions");
263
+ }
264
+ function hostDir(home, host) {
265
+ return (0, import_node_path2.join)(sessionsDir(home), host);
266
+ }
267
+ function logPath(home, host, sessionId) {
268
+ return (0, import_node_path2.join)(hostDir(home, host), `${sessionId}.jsonl`);
269
+ }
270
+ function metaPath(home, host, sessionId) {
271
+ return (0, import_node_path2.join)(hostDir(home, host), `${sessionId}.json`);
272
+ }
273
+ function tokensPath(home, host, sessionId) {
274
+ return (0, import_node_path2.join)(hostDir(home, host), `${sessionId}.tokens`);
275
+ }
276
+ function authPath(home) {
277
+ return (0, import_node_path2.join)(home, "auth.json");
278
+ }
279
+ function loginLockPath(home) {
280
+ return (0, import_node_path2.join)(home, "login.lock");
281
+ }
282
+ function loginLinkPath(home) {
283
+ return (0, import_node_path2.join)(home, "login-link.json");
284
+ }
285
+ function signInPromptPath(home) {
286
+ return (0, import_node_path2.join)(home, "sign-in-prompt.json");
287
+ }
288
+ function authLockPath(home) {
289
+ return (0, import_node_path2.join)(home, "auth.lock");
290
+ }
291
+ function configCachePath(home) {
292
+ return (0, import_node_path2.join)(home, "config.json");
293
+ }
294
+ function errorLogPath(home) {
295
+ return (0, import_node_path2.join)(home, "hook.log");
296
+ }
297
+
298
+ // packages/agent-cli/src/auth-store.ts
299
+ var STRINGS = [
300
+ "baseUrl",
301
+ "clientId",
302
+ "accessToken",
303
+ "refreshToken",
304
+ "tokenEndpoint",
305
+ "revocationEndpoint"
306
+ ];
307
+ function isSignedOut(state) {
308
+ return "signedOut" in state;
309
+ }
310
+ function readAuth(home) {
311
+ const value = readJson(authPath(home));
312
+ if (!isRecord(value) || value.version !== 1 || typeof value.baseUrl !== "string") {
313
+ return null;
314
+ }
315
+ if (value.signedOut === true) {
316
+ return { version: 1, baseUrl: value.baseUrl, signedOut: true };
317
+ }
318
+ const complete = STRINGS.every((key) => typeof value[key] === "string" && value[key] !== "") && typeof value.expiresAt === "number";
319
+ return complete ? value : null;
320
+ }
321
+ function writeAuth(home, state) {
322
+ writePrivateJson(authPath(home), state);
323
+ }
324
+ function deleteAuth(home) {
325
+ (0, import_node_fs2.rmSync)(authPath(home), { force: true });
326
+ }
327
+ function fingerprint(token) {
328
+ return (0, import_node_crypto.createHash)("sha256").update(token).digest("hex").slice(0, 16);
329
+ }
330
+
331
+ // packages/agent-cli/src/credentials.ts
332
+ var import_node_crypto2 = require("node:crypto");
333
+
334
+ // packages/agent-cli/src/address.ts
335
+ var DEFAULT_WORKSPACE_URL = "https://timetrakkr.com";
336
+ function workspaceAddress(env, home, flag) {
337
+ return flag ?? (env.TIMETRAKKR_URL?.trim() || readAuth(home)?.baseUrl || DEFAULT_WORKSPACE_URL);
338
+ }
339
+
340
+ // packages/agent-cli/src/credentials.ts
341
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
342
+ function parseWorkspaceUrl(raw, name) {
343
+ let url;
344
+ try {
345
+ url = new URL(raw);
346
+ } catch {
347
+ return { ok: false, reason: `${name} is not a valid URL` };
348
+ }
349
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
350
+ return { ok: false, reason: `${name} is not a valid URL` };
351
+ }
352
+ if (url.protocol === "http:" && !LOOPBACK_HOSTS.has(url.hostname)) {
353
+ return { ok: false, reason: `${name} must use https` };
354
+ }
355
+ return {
356
+ ok: true,
357
+ baseUrl: `${url.origin}${url.pathname.replace(/\/+$/, "")}`,
358
+ origin: url.origin
359
+ };
360
+ }
361
+ function digest(text) {
362
+ return (0, import_node_crypto2.createHash)("sha256").update(text).digest("hex").slice(0, 16);
363
+ }
364
+ function fromEnvironment(token, rawUrl, home) {
365
+ const address = rawUrl ?? readAuth(home)?.baseUrl ?? DEFAULT_WORKSPACE_URL;
366
+ const url = parseWorkspaceUrl(address, "TIMETRAKKR_URL");
367
+ if (!url.ok) {
368
+ return url;
369
+ }
370
+ return {
371
+ ok: true,
372
+ credentials: {
373
+ baseUrl: url.baseUrl,
374
+ token,
375
+ source: "environment",
376
+ cacheId: digest(`${url.baseUrl}
377
+ ${token}`)
378
+ }
379
+ };
380
+ }
381
+ function fromSignIn(rawUrl, home) {
382
+ const auth = readAuth(home);
383
+ const wanted = rawUrl ? parseWorkspaceUrl(rawUrl, "TIMETRAKKR_URL") : null;
384
+ if (wanted && !wanted.ok) {
385
+ return wanted;
386
+ }
387
+ if (!auth) {
388
+ const signIn2 = { url: wanted?.origin ?? DEFAULT_WORKSPACE_URL, why: "not signed in" };
389
+ return { ok: false, reason: "not signed in; run timetrakkr login", signIn: signIn2 };
390
+ }
391
+ if (wanted && wanted.origin !== auth.baseUrl) {
392
+ const why = `not signed in to ${wanted.origin}`;
393
+ return {
394
+ ok: false,
395
+ reason: `${why}; run timetrakkr login`,
396
+ signIn: { url: wanted.origin, why }
397
+ };
398
+ }
399
+ if (isSignedOut(auth)) {
400
+ const signIn2 = { url: auth.baseUrl, why: "signed out" };
401
+ return { ok: false, reason: "signed out; run timetrakkr login", signIn: signIn2 };
402
+ }
403
+ const url = parseWorkspaceUrl(auth.baseUrl, "The stored workspace address");
404
+ if (!url.ok) {
405
+ return url;
406
+ }
407
+ return {
408
+ ok: true,
409
+ credentials: {
410
+ baseUrl: url.origin,
411
+ token: auth.accessToken,
412
+ source: "sign-in",
413
+ cacheId: digest(`${url.origin}
414
+ ${auth.clientId}`),
415
+ expiresAt: auth.expiresAt
416
+ }
417
+ };
418
+ }
419
+ function readCredentials(env) {
420
+ const home = stateHome(env);
421
+ const token = env.TIMETRAKKR_TOKEN?.trim();
422
+ const rawUrl = env.TIMETRAKKR_URL?.trim() || void 0;
423
+ return token ? fromEnvironment(token, rawUrl, home) : fromSignIn(rawUrl, home);
424
+ }
425
+
426
+ // packages/agent-cli/src/auth-session.ts
427
+ var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
428
+ function authSession(rt, home, now, signal) {
429
+ let attempted = false;
430
+ async function waitForRenewal(oldToken, rejected, maxWaitMs) {
431
+ attempted = true;
432
+ rt.background(rejected === null ? ["refresh"] : ["refresh", "--rejected", rejected]);
433
+ const giveUpAt = Date.now() + maxWaitMs;
434
+ while (!signal.aborted && Date.now() < giveUpAt) {
435
+ const auth = readAuth(home);
436
+ if (!auth || isSignedOut(auth) || auth.accessToken !== oldToken) {
437
+ return;
438
+ }
439
+ await sleep(RENEW_POLL_MS);
440
+ }
441
+ }
442
+ async function fresh() {
443
+ const current = readCredentials(rt.env);
444
+ if (!current.ok || current.credentials.source !== "sign-in") {
445
+ return current;
446
+ }
447
+ const { expiresAt, token } = current.credentials;
448
+ if (expiresAt === void 0 || expiresAt - now > RENEW_MARGIN_MS) {
449
+ return current;
450
+ }
451
+ const stillValid = expiresAt > now;
452
+ await waitForRenewal(token, null, stillValid ? EARLY_RENEW_WAIT_MS : Number.POSITIVE_INFINITY);
453
+ return readCredentials(rt.env);
454
+ }
455
+ async function renew(rejected) {
456
+ if (rejected.source !== "sign-in") {
457
+ return null;
458
+ }
459
+ const usable = (result) => result.ok && result.credentials.token !== rejected.token ? result.credentials : null;
460
+ const already = usable(readCredentials(rt.env));
461
+ if (already || attempted) {
462
+ return already;
463
+ }
464
+ await waitForRenewal(rejected.token, fingerprint(rejected.token), Number.POSITIVE_INFINITY);
465
+ return usable(readCredentials(rt.env));
466
+ }
467
+ return { fresh, renew };
468
+ }
469
+
470
+ // packages/agent-cli/src/config-cache.ts
471
+ var import_node_fs3 = require("node:fs");
472
+ function cacheKey(credentials) {
473
+ return credentials.cacheId;
474
+ }
475
+ function readCache(home, key) {
476
+ const value = readJson(configCachePath(home));
477
+ if (!isRecord(value) || value.key !== key || typeof value.fetchedAt !== "number" || typeof value.enabled !== "boolean" || typeof value.workspaceName !== "string" || !Array.isArray(value.mappings)) {
478
+ return null;
479
+ }
480
+ return {
481
+ key,
482
+ fetchedAt: value.fetchedAt,
483
+ enabled: value.enabled,
484
+ workspaceName: value.workspaceName,
485
+ // Absent from an answer remembered by a version that did not keep it.
486
+ memberId: typeof value.memberId === "string" ? value.memberId : null,
487
+ offReason: typeof value.offReason === "string" ? value.offReason : null,
488
+ // Written by this program from validated rules, in an owner-only file.
489
+ mappings: value.mappings
490
+ };
491
+ }
492
+ function peekConfig(home, credentials, now) {
493
+ const cached = readCache(home, cacheKey(credentials));
494
+ if (!cached) {
495
+ return null;
496
+ }
497
+ const age = now - cached.fetchedAt;
498
+ return age >= 0 && age < CONFIG_TTL_MS ? {
499
+ ok: true,
500
+ enabled: cached.enabled,
501
+ workspaceName: cached.workspaceName,
502
+ memberId: cached.memberId,
503
+ offReason: cached.offReason,
504
+ mappings: cached.mappings
505
+ } : null;
506
+ }
507
+ function lastKnownMappings(home, credentials) {
508
+ return readCache(home, cacheKey(credentials))?.mappings ?? null;
509
+ }
510
+ function lastKnownMemberId(home, credentials) {
511
+ return readCache(home, cacheKey(credentials))?.memberId ?? null;
512
+ }
513
+ function forgetConfig(home) {
514
+ (0, import_node_fs3.rmSync)(configCachePath(home), { force: true });
515
+ }
516
+ async function getConfig(home, credentials, now, signal, renew = null) {
517
+ const cached = peekConfig(home, credentials, now);
518
+ if (cached) {
519
+ return cached;
520
+ }
521
+ let fresh = await fetchConfig(credentials, signal);
522
+ if (!fresh.ok && fresh.status === 401 && renew) {
523
+ const renewed = await renew(credentials);
524
+ if (renewed) {
525
+ fresh = await fetchConfig(renewed, signal);
526
+ }
527
+ }
528
+ if (fresh.ok) {
529
+ const entry = {
530
+ key: cacheKey(credentials),
531
+ fetchedAt: now,
532
+ enabled: fresh.enabled,
533
+ workspaceName: fresh.workspaceName,
534
+ memberId: fresh.memberId,
535
+ offReason: fresh.offReason,
536
+ mappings: fresh.mappings
537
+ };
538
+ writePrivateJson(configCachePath(home), entry);
539
+ }
540
+ return fresh;
541
+ }
542
+
543
+ // packages/agent-cli/src/context.ts
544
+ var import_node_crypto3 = require("node:crypto");
545
+
546
+ // packages/core/src/agent-time/turn-clock.ts
547
+ var KIND_ORDER = {
548
+ turn_start: 0,
549
+ activity: 1,
550
+ wait: 2,
551
+ turn_end: 3
552
+ };
553
+ function inTimeOrder(events) {
554
+ return [...events].sort((a, b) => a.at - b.at || KIND_ORDER[a.kind] - KIND_ORDER[b.kind]);
555
+ }
556
+ function closed(turn, complete) {
557
+ return {
558
+ startedAt: turn.startedAt,
559
+ endedAt: turn.countedTo,
560
+ activeMs: turn.activeMs,
561
+ complete
562
+ };
563
+ }
564
+ function advance(turn, event) {
565
+ if (event.at < turn.countedTo) {
566
+ return turn;
567
+ }
568
+ if (turn.waitingSince === null) {
569
+ return { ...turn, activeMs: turn.activeMs + (event.at - turn.countedTo), countedTo: event.at };
570
+ }
571
+ const resumedAt = Math.max(turn.waitingSince, event.at - (event.durationMs ?? 0));
572
+ return {
573
+ ...turn,
574
+ activeMs: turn.activeMs + (event.at - resumedAt),
575
+ countedTo: event.at,
576
+ waitingSince: null
577
+ };
578
+ }
579
+ function turnsFromEvents(events) {
580
+ const turns = [];
581
+ let open = null;
582
+ for (const event of inTimeOrder(events)) {
583
+ if (event.kind === "turn_start") {
584
+ if (open?.startedAt === event.at) {
585
+ continue;
586
+ }
587
+ if (open) {
588
+ turns.push(closed(open, false));
589
+ }
590
+ open = { startedAt: event.at, countedTo: event.at, activeMs: 0, waitingSince: null };
591
+ continue;
592
+ }
593
+ if (!open) {
594
+ continue;
595
+ }
596
+ if (event.kind === "activity") {
597
+ open = advance(open, event);
598
+ } else if (event.kind === "wait") {
599
+ if (open.waitingSince === null) {
600
+ open = { ...advance(open, event), waitingSince: event.at };
601
+ }
602
+ } else {
603
+ const ended = open.waitingSince === null ? advance(open, event) : { ...open, countedTo: Math.max(open.countedTo, event.at) };
604
+ turns.push(closed(ended, true));
605
+ open = null;
606
+ }
607
+ }
608
+ if (open) {
609
+ turns.push(closed(open, false));
610
+ }
611
+ return turns;
612
+ }
613
+ function summarizeSessionDays(events, dayOf) {
614
+ const byDay = /* @__PURE__ */ new Map();
615
+ for (const turn of turnsFromEvents(events)) {
616
+ if (turn.activeMs <= 0) {
617
+ continue;
618
+ }
619
+ const day = dayOf(turn.startedAt);
620
+ byDay.set(day, [...byDay.get(day) ?? [], turn]);
621
+ }
622
+ return [...byDay.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([day, turns]) => ({
623
+ day,
624
+ activeSeconds: Math.round(turns.reduce((sum, t) => sum + t.activeMs, 0) / 1e3),
625
+ turnCount: turns.length,
626
+ firstTurnStartedAt: Math.min(...turns.map((t) => t.startedAt)),
627
+ lastTurnEndedAt: Math.max(...turns.map((t) => t.endedAt))
628
+ }));
629
+ }
630
+
631
+ // packages/core/src/agent-time/repository-mapping.ts
632
+ var SCP_FORM = /^(?:[^@/\s]+@)?([^:/\s]+):(?!\/\/)(.+)$/;
633
+ var URL_FORM = /^[a-z][a-z0-9+.-]*:\/\/(?:[^@/\s]+@)?([^:/\s]+)(?::\d+)?\/(.+)$/i;
634
+ var BARE_FORM = /^([a-z0-9.-]+\.[a-z]{2,})\/(.+)$/i;
635
+ function hostAndPath(remote) {
636
+ if (/^file:/i.test(remote)) {
637
+ return null;
638
+ }
639
+ const match = URL_FORM.exec(remote) ?? SCP_FORM.exec(remote) ?? BARE_FORM.exec(remote);
640
+ const host = match?.[1];
641
+ const path = match?.[2];
642
+ return host && path ? [host, path] : null;
643
+ }
644
+ function normalizeRemote(remote) {
645
+ const parts = hostAndPath(remote.trim().replace(/[?#].*$/, ""));
646
+ if (!parts) {
647
+ return null;
648
+ }
649
+ const [host, rawPath] = parts;
650
+ const path = rawPath.replace(/\/+$/, "").replace(/\.git$/i, "").replace(/^\/+/, "");
651
+ if (!path.includes("/") || /\s/.test(path) || !host.includes(".")) {
652
+ return null;
653
+ }
654
+ return `${host}/${path}`.toLowerCase();
655
+ }
656
+ function branchMatches(pattern, branch) {
657
+ const parts = pattern.split("*");
658
+ const first = parts[0] ?? "";
659
+ if (parts.length === 1) {
660
+ return branch === first;
661
+ }
662
+ const last = parts[parts.length - 1] ?? "";
663
+ if (branch.length < first.length + last.length || !branch.startsWith(first) || !branch.endsWith(last)) {
664
+ return false;
665
+ }
666
+ const end = branch.length - last.length;
667
+ let from = first.length;
668
+ for (const part of parts.slice(1, -1)) {
669
+ const at = branch.indexOf(part, from);
670
+ if (at === -1 || at + part.length > end) {
671
+ return false;
672
+ }
673
+ from = at + part.length;
674
+ }
675
+ return true;
676
+ }
677
+ function specificity(pattern) {
678
+ return pattern.replace(/\*/g, "").length;
679
+ }
680
+ function resolveMapping(mappings, remote, branch) {
681
+ const wanted = remote === null ? null : normalizeRemote(remote);
682
+ if (wanted === null) {
683
+ return null;
684
+ }
685
+ const forRepository = mappings.filter((m) => normalizeRemote(m.remote) === wanted);
686
+ const byBranch = forRepository.filter(
687
+ (m) => m.branchPattern !== null && branch !== null && branchMatches(m.branchPattern, branch)
688
+ ).sort((a, b) => specificity(b.branchPattern) - specificity(a.branchPattern));
689
+ return byBranch[0] ?? forRepository.find((m) => m.branchPattern === null) ?? null;
690
+ }
691
+
692
+ // packages/core/src/agent-time/model-prices.ts
693
+ var usd = (model, input, output, cacheRead, cacheWrite) => ({ model, currency: "USD", input, output, cacheRead, cacheWrite });
694
+ var DEFAULT_MODEL_PRICES = [
695
+ usd("claude-fable-5-1", "10", "50", "0.25", "12.50"),
696
+ usd("claude-opus-5-5", "4", "20", "0.20", "5"),
697
+ usd("claude-opus-5", "5", "25", "0.50", "6.25"),
698
+ usd("claude-opus-4-8", "5", "25", "0.50", "6.25"),
699
+ usd("claude-opus-4-7", "5", "25", "0.50", "6.25"),
700
+ usd("claude-opus-4-6", "5", "25", "0.50", "6.25"),
701
+ usd("claude-sonnet-5-5", "2", "10", "0.20", "2.50"),
702
+ usd("claude-sonnet-5", "2", "10", "0.20", "2.50"),
703
+ usd("claude-sonnet-4-6", "3", "15", "0.30", "3.75"),
704
+ usd("claude-sonnet-4-5", "3", "15", "0.30", "3.75"),
705
+ usd("claude-haiku-4-5", "1", "5", "0.10", "1.25"),
706
+ usd("gpt-6-astra", "10", "50", "1.00", "12.50"),
707
+ usd("gpt-6.1-sol", "2", "10", "0.10", "2.50"),
708
+ usd("gpt-6-luna", "0.10", "0.50", "0.01", "0.125"),
709
+ usd("gpt-5.6-sol", "4", "20", "0.40", "5.00"),
710
+ usd("gpt-5.3-codex", "1.75", "14", "0.175", null)
711
+ ];
712
+
713
+ // packages/core/src/agent-time/untrusted-text.ts
714
+ var FORMAT = new RegExp("\\p{Cf}", "gu");
715
+ var CONTROL = new RegExp("\\p{Cc}", "gu");
716
+
717
+ // packages/agent-cli/src/git.ts
718
+ var import_node_child_process = require("node:child_process");
719
+ function git(root, env, args) {
720
+ try {
721
+ const out = (0, import_node_child_process.execFileSync)("git", ["-C", root, ...args], {
722
+ encoding: "utf8",
723
+ timeout: GIT_TIMEOUT_MS,
724
+ stdio: ["ignore", "pipe", "ignore"],
725
+ env: { ...env, GIT_TERMINAL_PROMPT: "0" }
726
+ });
727
+ return out.trim() || null;
728
+ } catch {
729
+ return null;
730
+ }
731
+ }
732
+ function withoutQuery(remote) {
733
+ return remote.replace(/[?#].*$/s, "");
734
+ }
735
+ function repositoryRemote(root, env) {
736
+ const raw = git(root, env, ["config", "--get", "remote.origin.url"]);
737
+ const remote = raw === null ? null : withoutQuery(raw);
738
+ return remote !== null && remote.length <= MAX_REPOSITORY_LENGTH ? remote : null;
739
+ }
740
+ function currentBranch(root, env) {
741
+ const branch = git(root, env, ["symbolic-ref", "--short", "-q", "HEAD"]);
742
+ return branch !== null && branch.length <= MAX_BRANCH_LENGTH ? branch : null;
743
+ }
744
+
745
+ // packages/agent-cli/src/mapping.ts
746
+ var import_node_fs4 = require("node:fs");
747
+ var import_node_path3 = require("node:path");
748
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
749
+ function findRepositoryRoot(cwd) {
750
+ let dir = cwd;
751
+ for (; ; ) {
752
+ if ((0, import_node_fs4.existsSync)((0, import_node_path3.join)(dir, ".git"))) {
753
+ return dir;
754
+ }
755
+ const parent = (0, import_node_path3.dirname)(dir);
756
+ if (parent === dir) {
757
+ return null;
758
+ }
759
+ dir = parent;
760
+ }
761
+ }
762
+ function readMappingText(path) {
763
+ try {
764
+ if ((0, import_node_fs4.statSync)(path).size > MAPPING_MAX_BYTES) {
765
+ return null;
766
+ }
767
+ return (0, import_node_fs4.readFileSync)(path, "utf8");
768
+ } catch {
769
+ return null;
770
+ }
771
+ }
772
+ function parseMapping2(text) {
773
+ let value;
774
+ try {
775
+ value = JSON.parse(text);
776
+ } catch {
777
+ return { reason: `${MAPPING_FILE} is not valid JSON` };
778
+ }
779
+ if (!isRecord(value)) {
780
+ return { reason: `${MAPPING_FILE} must be an object with projectId and taskId` };
781
+ }
782
+ const { projectId, taskId } = value;
783
+ if (typeof projectId !== "string" || !UUID.test(projectId)) {
784
+ return { reason: `${MAPPING_FILE}: projectId must be a UUID` };
785
+ }
786
+ if (typeof taskId !== "string" || !UUID.test(taskId)) {
787
+ return { reason: `${MAPPING_FILE}: taskId must be a UUID` };
788
+ }
789
+ return { mapping: { projectId: projectId.toLowerCase(), taskId: taskId.toLowerCase() } };
790
+ }
791
+ function readMapping(root) {
792
+ const path = (0, import_node_path3.join)(root, MAPPING_FILE);
793
+ if (!(0, import_node_fs4.existsSync)(path)) {
794
+ return { missing: true };
795
+ }
796
+ const text = readMappingText(path);
797
+ return text === null ? { reason: `${MAPPING_FILE} could not be read` } : parseMapping2(text);
798
+ }
799
+
800
+ // packages/agent-cli/src/context.ts
801
+ var NOT_MAPPED = "this repository is not mapped to a project";
802
+ var BLOCKED = "this branch is mapped to a project you cannot log time to";
803
+ function readLocalContext(cwd, env) {
804
+ const root = findRepositoryRoot(cwd);
805
+ if (root === null) {
806
+ return { root, file: null, remote: null, branch: null };
807
+ }
808
+ const file = readMapping(root);
809
+ if ("reason" in file) {
810
+ return { root, file, remote: null, branch: null };
811
+ }
812
+ return { root, file, remote: repositoryRemote(root, env), branch: currentBranch(root, env) };
813
+ }
814
+ function couldBeTracked(local) {
815
+ if (local.file === null || "reason" in local.file) {
816
+ return false;
817
+ }
818
+ return "mapping" in local.file || local.remote !== null && normalizeRemote(local.remote) !== null;
819
+ }
820
+ function labelOf(rule) {
821
+ return [rule.clientName, rule.projectName, rule.taskName].filter(Boolean).join(" / ");
822
+ }
823
+ function untracked(root, untrackedReason) {
824
+ return { root, mapping: null, untrackedReason, repository: null, branch: null };
825
+ }
826
+ function candidatesFor(rules, remote) {
827
+ const own = normalizeRemote(remote);
828
+ if (own === null) {
829
+ return [];
830
+ }
831
+ const digest2 = (0, import_node_crypto3.createHash)("sha256").update(own).digest("hex");
832
+ return rules.flatMap((rule) => {
833
+ if (!("blocked" in rule)) {
834
+ return [rule];
835
+ }
836
+ return rule.remoteSha256 === digest2 ? [
837
+ {
838
+ id: rule.id,
839
+ remote: own,
840
+ branchPattern: rule.branchPattern,
841
+ projectId: "",
842
+ taskId: "",
843
+ blocked: true
844
+ }
845
+ ] : [];
846
+ });
847
+ }
848
+ function fromRules(local, rules, credentials) {
849
+ const { root, remote, branch } = local;
850
+ if (remote === null || normalizeRemote(remote) === null) {
851
+ return untracked(root, `no ${MAPPING_FILE} and no git remote to look up`);
852
+ }
853
+ if (!credentials.ok) {
854
+ return untracked(root, credentials.reason);
855
+ }
856
+ if (rules === null) {
857
+ return untracked(root, "could not reach Time Trakkr to look up this repository");
858
+ }
859
+ const rule = resolveMapping(candidatesFor(rules, remote), remote, branch);
860
+ if (rule === null) {
861
+ return untracked(root, NOT_MAPPED);
862
+ }
863
+ if ("blocked" in rule) {
864
+ return untracked(root, BLOCKED);
865
+ }
866
+ return {
867
+ root,
868
+ mapping: {
869
+ projectId: rule.projectId,
870
+ taskId: rule.taskId,
871
+ mappingId: rule.id,
872
+ source: "platform",
873
+ label: labelOf(rule) || null
874
+ },
875
+ untrackedReason: null,
876
+ repository: normalizeRemote(remote),
877
+ branch
878
+ };
879
+ }
880
+ function resolveContext(local, rules, credentials) {
881
+ const { root, file } = local;
882
+ if (root === null || file === null) {
883
+ return untracked(root, "not inside a git repository");
884
+ }
885
+ if ("reason" in file) {
886
+ return untracked(root, file.reason);
887
+ }
888
+ if ("missing" in file) {
889
+ return fromRules(local, rules, credentials);
890
+ }
891
+ const { projectId, taskId } = file.mapping;
892
+ const named = rules?.find(
893
+ (rule) => !("blocked" in rule) && rule.projectId === projectId && rule.taskId === taskId
894
+ );
895
+ return {
896
+ root,
897
+ mapping: {
898
+ projectId,
899
+ taskId,
900
+ mappingId: null,
901
+ source: "file",
902
+ label: named ? labelOf(named) || null : null
903
+ },
904
+ untrackedReason: null,
905
+ // Null for a remote that is not a hosted repository, such as a local path.
906
+ repository: local.remote === null ? null : normalizeRemote(local.remote),
907
+ branch: local.branch
908
+ };
909
+ }
910
+
911
+ // packages/agent-cli/src/day.ts
912
+ var pad = (n) => String(n).padStart(2, "0");
913
+ function localDay(at) {
914
+ const date = new Date(at);
915
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
916
+ }
917
+
918
+ // packages/agent-cli/src/drop.ts
919
+ var import_node_fs7 = require("node:fs");
920
+
921
+ // packages/agent-cli/src/identity.ts
922
+ var import_node_crypto4 = require("node:crypto");
923
+ var MEMBER_PREFIX = "member:";
924
+ function memberIdentity(credentials, memberId) {
925
+ const { origin } = new URL(credentials.baseUrl);
926
+ const digest2 = (0, import_node_crypto4.createHash)("sha256").update(`${origin}
927
+ ${memberId}`).digest("hex").slice(0, 16);
928
+ return `${MEMBER_PREFIX}${digest2}`;
929
+ }
930
+ function isMemberBound(identity) {
931
+ return identity.startsWith(MEMBER_PREFIX);
932
+ }
933
+ function memberIsKnown(home, credentials) {
934
+ return lastKnownMemberId(home, credentials) !== null;
935
+ }
936
+ function identitiesOf(home, credentials) {
937
+ const memberId = lastKnownMemberId(home, credentials);
938
+ return memberId === null ? [credentials.cacheId] : [memberIdentity(credentials, memberId), credentials.cacheId];
939
+ }
940
+ function ownedBy(session, identities) {
941
+ const { identity, credentialId } = session;
942
+ return typeof identity === "string" && identities.includes(identity) || typeof credentialId === "string" && identities.includes(credentialId);
943
+ }
944
+
945
+ // packages/agent-cli/src/store.ts
946
+ var import_node_fs5 = require("node:fs");
947
+ var EVENT_KINDS = ["turn_start", "activity", "wait", "turn_end"];
948
+ function parseEvent(line) {
949
+ try {
950
+ const value = JSON.parse(line);
951
+ if (!isRecord(value) || typeof value.at !== "number") {
952
+ return null;
953
+ }
954
+ const kind = EVENT_KINDS.find((k) => k === value.kind);
955
+ if (!kind) {
956
+ return null;
957
+ }
958
+ return typeof value.durationMs === "number" ? { kind, at: value.at, durationMs: value.durationMs } : { kind, at: value.at };
959
+ } catch {
960
+ return null;
961
+ }
962
+ }
963
+ function appendEvent(home, host, sessionId, event) {
964
+ appendPrivateLine(logPath(home, host, sessionId), JSON.stringify(event));
965
+ }
966
+ function readLog(home, host, sessionId) {
967
+ let raw;
968
+ try {
969
+ raw = (0, import_node_fs5.readFileSync)(logPath(home, host, sessionId));
970
+ } catch {
971
+ return { events: [], bytes: 0 };
972
+ }
973
+ const events = raw.toString("utf8").split("\n").map(parseEvent).filter((event) => event !== null);
974
+ return { events, bytes: raw.length };
975
+ }
976
+ function logSize(home, host, sessionId) {
977
+ try {
978
+ return (0, import_node_fs5.statSync)(logPath(home, host, sessionId)).size;
979
+ } catch {
980
+ return 0;
981
+ }
982
+ }
983
+ function isMeta(value, host, sessionId) {
984
+ return isRecord(value) && value.version === 1 && value.host === host && value.sessionId === sessionId && isRecord(value.posted);
985
+ }
986
+ function readMeta(home, host, sessionId) {
987
+ const value = readJson(metaPath(home, host, sessionId));
988
+ return isMeta(value, host, sessionId) ? value : null;
989
+ }
990
+ function writeMeta(home, meta) {
991
+ writePrivateJson(metaPath(home, meta.host, meta.sessionId), meta);
992
+ }
993
+ function sessionIdsOf(home, host) {
994
+ try {
995
+ return (0, import_node_fs5.readdirSync)(hostDir(home, host)).filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -".json".length)).filter(isSafeSessionId);
996
+ } catch {
997
+ return [];
998
+ }
999
+ }
1000
+ function listMetas(home) {
1001
+ return HOSTS.flatMap(
1002
+ (host) => sessionIdsOf(home, host).map((sessionId) => readMeta(home, host, sessionId)).filter((meta) => meta !== null)
1003
+ );
1004
+ }
1005
+ function pruneSessions(home, before) {
1006
+ for (const meta of listMetas(home)) {
1007
+ if (meta.updatedAt < before) {
1008
+ (0, import_node_fs5.rmSync)(metaPath(home, meta.host, meta.sessionId), { force: true });
1009
+ (0, import_node_fs5.rmSync)(logPath(home, meta.host, meta.sessionId), { force: true });
1010
+ (0, import_node_fs5.rmSync)(tokensPath(home, meta.host, meta.sessionId), { force: true });
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ // packages/agent-cli/src/hosts/transcript.ts
1016
+ var import_node_fs6 = require("node:fs");
1017
+ var NEWLINE = 10;
1018
+ function countOf(value, optional) {
1019
+ if (value === void 0 && optional) {
1020
+ return 0;
1021
+ }
1022
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
1023
+ }
1024
+ function readWholeLines(path, offset) {
1025
+ const stat = (0, import_node_fs6.statSync)(path);
1026
+ if (!stat.isFile() || stat.size < offset) {
1027
+ return null;
1028
+ }
1029
+ const length = Math.min(stat.size - offset, TRANSCRIPT_READ_BYTES);
1030
+ const buffer = Buffer.alloc(length);
1031
+ const fd = (0, import_node_fs6.openSync)(path, "r");
1032
+ try {
1033
+ (0, import_node_fs6.readSync)(fd, buffer, 0, length, offset);
1034
+ } finally {
1035
+ (0, import_node_fs6.closeSync)(fd);
1036
+ }
1037
+ const end = buffer.lastIndexOf(NEWLINE);
1038
+ if (end === -1) {
1039
+ return length === TRANSCRIPT_READ_BYTES ? null : { lines: [], offset };
1040
+ }
1041
+ const lines = buffer.subarray(0, end).toString("utf8").split("\n").filter((line) => line.trim() !== "");
1042
+ return { lines, offset: offset + end + 1 };
1043
+ }
1044
+
1045
+ // packages/agent-cli/src/hosts/claude-code-transcript.ts
1046
+ function parseUsage(usage) {
1047
+ const input = countOf(usage.input_tokens, false);
1048
+ const output = countOf(usage.output_tokens, false);
1049
+ const cacheRead = countOf(usage.cache_read_input_tokens, true);
1050
+ const cacheWrite = countOf(usage.cache_creation_input_tokens, true);
1051
+ if (input === null || output === null || cacheRead === null || cacheWrite === null) {
1052
+ return null;
1053
+ }
1054
+ return { input, output, cache_read: cacheRead, cache_write: cacheWrite };
1055
+ }
1056
+ function parseLine(line) {
1057
+ let row;
1058
+ try {
1059
+ row = JSON.parse(line);
1060
+ } catch {
1061
+ return null;
1062
+ }
1063
+ if (!isRecord(row)) {
1064
+ return null;
1065
+ }
1066
+ if (row.type !== "assistant" || !isRecord(row.message) || !isRecord(row.message.usage)) {
1067
+ return "skip";
1068
+ }
1069
+ const { id, model, usage } = row.message;
1070
+ if (typeof model === "string" && model.startsWith("<")) {
1071
+ return "skip";
1072
+ }
1073
+ const at = typeof row.timestamp === "string" ? Date.parse(row.timestamp) : Number.NaN;
1074
+ const counts = isRecord(usage) ? parseUsage(usage) : null;
1075
+ if (typeof id !== "string" || id.length === 0 || typeof model !== "string" || model.length === 0 || model.length > MAX_MODEL_LENGTH || Number.isNaN(at) || counts === null) {
1076
+ return null;
1077
+ }
1078
+ return { messageId: id, model, at, counts };
1079
+ }
1080
+ function readClaudeCodeUsage(path, offset) {
1081
+ try {
1082
+ const chunk = readWholeLines(path, offset);
1083
+ if (chunk === null) {
1084
+ return { ok: false };
1085
+ }
1086
+ const entries = [];
1087
+ for (const line of chunk.lines) {
1088
+ const parsed = parseLine(line);
1089
+ if (parsed === null) {
1090
+ return { ok: false };
1091
+ }
1092
+ if (parsed !== "skip") {
1093
+ entries.push(parsed);
1094
+ }
1095
+ }
1096
+ return { ok: true, entries, offset: chunk.offset };
1097
+ } catch {
1098
+ return { ok: false };
1099
+ }
1100
+ }
1101
+
1102
+ // packages/agent-cli/src/hosts/signal.ts
1103
+ function durationOf(value) {
1104
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
1105
+ }
1106
+ function modelOf(value) {
1107
+ return typeof value === "string" && value.length > 0 && value.length <= MAX_MODEL_LENGTH ? value : null;
1108
+ }
1109
+ function pathOf(value) {
1110
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1111
+ }
1112
+
1113
+ // packages/agent-cli/src/hosts/codex-rollout.ts
1114
+ function parseUsage2(usage) {
1115
+ const allInput = countOf(usage.input_tokens, false);
1116
+ const output = countOf(usage.output_tokens, false);
1117
+ const cacheRead = countOf(usage.cached_input_tokens, true);
1118
+ const cacheWrite = countOf(usage.cache_write_input_tokens, true);
1119
+ if (allInput === null || output === null || cacheRead === null || cacheWrite === null) {
1120
+ return null;
1121
+ }
1122
+ if (cacheRead > allInput) {
1123
+ return null;
1124
+ }
1125
+ return { input: allInput - cacheRead, output, cache_read: cacheRead, cache_write: cacheWrite };
1126
+ }
1127
+ function parseRecord(row, model) {
1128
+ const { payload } = row;
1129
+ const at = typeof row.timestamp === "string" ? Date.parse(row.timestamp) : Number.NaN;
1130
+ if (!isRecord(payload) || !isRecord(payload.usage) || model === null || Number.isNaN(at)) {
1131
+ return null;
1132
+ }
1133
+ const counts = parseUsage2(payload.usage);
1134
+ const id = payload.response_id;
1135
+ if (typeof id !== "string" || id.length === 0 || counts === null) {
1136
+ return null;
1137
+ }
1138
+ return { messageId: id, model, at, counts };
1139
+ }
1140
+ function readCodexUsage(path, offset, seedModel) {
1141
+ try {
1142
+ const chunk = readWholeLines(path, offset);
1143
+ if (chunk === null) {
1144
+ return { ok: false };
1145
+ }
1146
+ const entries = [];
1147
+ let model = seedModel;
1148
+ for (const line of chunk.lines) {
1149
+ const row = JSON.parse(line);
1150
+ if (!isRecord(row)) {
1151
+ return { ok: false };
1152
+ }
1153
+ if (row.type === "turn_context" && isRecord(row.payload)) {
1154
+ model = modelOf(row.payload.model) ?? model;
1155
+ }
1156
+ if (row.type === "token_usage_record") {
1157
+ const entry = parseRecord(row, model);
1158
+ if (entry === null) {
1159
+ return { ok: false };
1160
+ }
1161
+ entries.push(entry);
1162
+ }
1163
+ }
1164
+ return { ok: true, entries, offset: chunk.offset };
1165
+ } catch {
1166
+ return { ok: false };
1167
+ }
1168
+ }
1169
+
1170
+ // packages/agent-cli/src/tokens.ts
1171
+ var READERS = {
1172
+ "claude-code": readClaudeCodeUsage,
1173
+ codex: readCodexUsage,
1174
+ cursor: null
1175
+ };
1176
+ var EMPTY = {
1177
+ version: 1,
1178
+ offset: 0,
1179
+ unreliable: false,
1180
+ lastModel: null,
1181
+ days: {},
1182
+ recent: []
1183
+ };
1184
+ function readTokenState(home, host, sessionId) {
1185
+ const value = readJson(tokensPath(home, host, sessionId));
1186
+ return isRecord(value) && value.version === 1 && isRecord(value.days) && Array.isArray(value.recent) ? value : EMPTY;
1187
+ }
1188
+ function writeTokenState(home, host, sessionId, state) {
1189
+ writePrivateJson(tokensPath(home, host, sessionId), state);
1190
+ }
1191
+ function markTokensUnreliable(home, host, sessionId) {
1192
+ const state = readTokenState(home, host, sessionId);
1193
+ writeTokenState(home, host, sessionId, { ...state, unreliable: true });
1194
+ }
1195
+ function add(a, b, sign) {
1196
+ return {
1197
+ input: a.input + sign * b.input,
1198
+ output: a.output + sign * b.output,
1199
+ cache_read: a.cache_read + sign * b.cache_read,
1200
+ cache_write: a.cache_write + sign * b.cache_write
1201
+ };
1202
+ }
1203
+ var ZERO = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
1204
+ function adjust(days, message, sign) {
1205
+ const day = days[message.day] ?? {};
1206
+ const next = add(day[message.model] ?? ZERO, message.counts, sign);
1207
+ return { ...days, [message.day]: { ...day, [message.model]: next } };
1208
+ }
1209
+ function dayOfReply(events, at, dayOf) {
1210
+ const turns = turnsFromEvents(events);
1211
+ const index = turns.findLastIndex((turn2) => turn2.startedAt <= at);
1212
+ const turn = turns[index];
1213
+ if (!turn) {
1214
+ return null;
1215
+ }
1216
+ const next = turns[index + 1];
1217
+ const until = turn.complete ? turn.endedAt + REPLY_SLACK_MS : next?.startedAt ?? Number.POSITIVE_INFINITY;
1218
+ return at <= until ? dayOf(turn.startedAt) : null;
1219
+ }
1220
+ function count(state, entry, day) {
1221
+ const earlier = state.recent.find((message2) => message2.id === entry.messageId);
1222
+ const withoutEarlier = earlier ? adjust(state.days, earlier, -1) : state.days;
1223
+ const recent = state.recent.filter((message2) => message2.id !== entry.messageId);
1224
+ if (day === null) {
1225
+ return { ...state, days: withoutEarlier, recent };
1226
+ }
1227
+ const message = {
1228
+ id: entry.messageId,
1229
+ day,
1230
+ model: entry.model,
1231
+ counts: entry.counts
1232
+ };
1233
+ return {
1234
+ ...state,
1235
+ lastModel: entry.model,
1236
+ days: adjust(withoutEarlier, message, 1),
1237
+ recent: [...recent, message].slice(-RECENT_MESSAGES)
1238
+ };
1239
+ }
1240
+ function updateTokens(home, host, sessionId, transcriptPath, events, dayOf, hookModel) {
1241
+ const state = readTokenState(home, host, sessionId);
1242
+ const reader = READERS[host];
1243
+ if (state.unreliable || reader === null) {
1244
+ return state;
1245
+ }
1246
+ try {
1247
+ const read = reader(transcriptPath, state.offset, hookModel ?? state.lastModel);
1248
+ const next = read.ok ? read.entries.reduce(
1249
+ (acc, entry) => count(acc, entry, dayOfReply(events, entry.at, dayOf)),
1250
+ { ...state, offset: read.offset }
1251
+ ) : { ...state, unreliable: true };
1252
+ writeTokenState(home, host, sessionId, next);
1253
+ return next;
1254
+ } catch {
1255
+ return { ...state, unreliable: true };
1256
+ }
1257
+ }
1258
+ function tokensForDay(state, day) {
1259
+ const totals = state.unreliable ? void 0 : state.days[day];
1260
+ if (!totals) {
1261
+ return null;
1262
+ }
1263
+ const models = Object.entries(totals).filter(([, c]) => totalOf({ model: c }) > 0);
1264
+ return models.length > 0 && models.length <= MAX_TOKEN_MODELS ? Object.fromEntries(models) : null;
1265
+ }
1266
+ function totalOf(totals) {
1267
+ return Object.values(totals ?? {}).reduce(
1268
+ (sum, c) => sum + c.input + c.output + c.cache_read + c.cache_write,
1269
+ 0
1270
+ );
1271
+ }
1272
+
1273
+ // packages/agent-cli/src/drop.ts
1274
+ function dropPending(home, meta) {
1275
+ const { host, sessionId } = meta;
1276
+ const size = logSize(home, host, sessionId);
1277
+ const unsentEstimate = meta.estimate && !meta.estimate.sent;
1278
+ if (size === meta.syncedBytes && !meta.unsent && !unsentEstimate) {
1279
+ return;
1280
+ }
1281
+ if (size > meta.syncedBytes) {
1282
+ (0, import_node_fs7.truncateSync)(logPath(home, host, sessionId), meta.syncedBytes);
1283
+ markTokensUnreliable(home, host, sessionId);
1284
+ }
1285
+ const last = readLog(home, host, sessionId).events.at(-1);
1286
+ if (last && last.kind !== "turn_end") {
1287
+ appendEvent(home, host, sessionId, { kind: "turn_end", at: last.at });
1288
+ }
1289
+ const latest = readMeta(home, host, sessionId) ?? meta;
1290
+ writeMeta(home, {
1291
+ ...latest,
1292
+ estimate: unsentEstimate ? null : latest.estimate,
1293
+ syncedBytes: logSize(home, host, sessionId),
1294
+ unsent: false,
1295
+ lastError: null
1296
+ });
1297
+ }
1298
+ function dropAllPending(home, identities) {
1299
+ for (const meta of listMetas(home)) {
1300
+ if (ownedBy(meta, identities)) {
1301
+ dropPending(home, meta);
1302
+ }
1303
+ }
1304
+ }
1305
+
1306
+ // packages/agent-cli/src/flush.ts
1307
+ function toPosted(summary, item) {
1308
+ const tokenTotal = totalOf(tokensForDay(item.tokens, summary.day));
1309
+ return {
1310
+ activeSeconds: summary.activeSeconds,
1311
+ turnCount: summary.turnCount,
1312
+ firstTurnStartedAt: summary.firstTurnStartedAt,
1313
+ lastTurnEndedAt: summary.lastTurnEndedAt,
1314
+ projectId: item.mapping.projectId,
1315
+ taskId: item.mapping.taskId,
1316
+ ...tokenTotal > 0 ? { tokenTotal } : {}
1317
+ };
1318
+ }
1319
+ function alreadyPosted(posted, next) {
1320
+ return posted !== void 0 && posted.activeSeconds === next.activeSeconds && posted.turnCount === next.turnCount && posted.firstTurnStartedAt === next.firstTurnStartedAt && posted.lastTurnEndedAt === next.lastTurnEndedAt && posted.projectId === next.projectId && posted.taskId === next.taskId && (posted.tokenTotal ?? 0) === (next.tokenTotal ?? 0);
1321
+ }
1322
+ function toBody(item, summary, withEstimate) {
1323
+ const { meta, mapping } = item;
1324
+ const tokens = tokensForDay(item.tokens, summary.day);
1325
+ return {
1326
+ host: meta.host,
1327
+ session_id: meta.sessionId,
1328
+ day: summary.day,
1329
+ active_seconds: Math.min(summary.activeSeconds, SECONDS_PER_DAY, windowSeconds(summary)),
1330
+ turn_count: Math.min(summary.turnCount, MAX_TURN_COUNT),
1331
+ first_turn_started_at: new Date(summary.firstTurnStartedAt).toISOString(),
1332
+ last_turn_ended_at: new Date(summary.lastTurnEndedAt).toISOString(),
1333
+ project_id: mapping.projectId,
1334
+ task_id: mapping.taskId,
1335
+ repository: meta.repository,
1336
+ branch: meta.branch,
1337
+ // Nothing safe to summarize from yet: the prompt is never read.
1338
+ summary: null,
1339
+ model: meta.model ?? item.tokens.lastModel,
1340
+ mapping_id: mapping.mappingId ?? null,
1341
+ // Left out, not zeroed, when the transcript could not be read.
1342
+ ...tokens ? { tokens } : {},
1343
+ ...withEstimate && item.estimate ? { estimated_effort: { seconds: item.estimate.seconds, label: item.estimate.label } } : {}
1344
+ };
1345
+ }
1346
+ function needsAttention(home, meta, now, isOwn) {
1347
+ if (isOwn || meta.unsent) {
1348
+ return true;
1349
+ }
1350
+ if (logSize(home, meta.host, meta.sessionId) === meta.syncedBytes) {
1351
+ return false;
1352
+ }
1353
+ const last = readLog(home, meta.host, meta.sessionId).events.at(-1);
1354
+ return last !== void 0 && last.at <= now - STALE_SESSION_MS;
1355
+ }
1356
+ function windowSeconds(summary) {
1357
+ const window = Math.floor((summary.lastTurnEndedAt - summary.firstTurnStartedAt) / 1e3);
1358
+ return Math.max(0, window) + WINDOW_SLACK_SECONDS;
1359
+ }
1360
+ function stillAccepted(day, now) {
1361
+ const oldest = new Date(now - MAX_DAY_AGE_DAYS * SECONDS_PER_DAY * 1e3);
1362
+ return day >= oldest.toISOString().slice(0, 10);
1363
+ }
1364
+ function workFor(home, meta, mapping, now) {
1365
+ const log = readLog(home, meta.host, meta.sessionId);
1366
+ const tokens = readTokenState(home, meta.host, meta.sessionId);
1367
+ const estimate = meta.estimate && !meta.estimate.sent ? meta.estimate : null;
1368
+ const summaries = summarizeSessionDays(log.events, localDay).filter(
1369
+ (summary) => stillAccepted(summary.day, now)
1370
+ );
1371
+ const changed = summaries.filter(
1372
+ (summary) => !alreadyPosted(
1373
+ meta.posted[summary.day],
1374
+ toPosted(summary, { meta, mapping, tokens, estimate })
1375
+ )
1376
+ );
1377
+ const latest = summaries.at(-1);
1378
+ const pending = changed.length === 0 && estimate && latest ? [latest] : changed;
1379
+ return { meta, mapping, tokens, bytes: log.bytes, pending, estimate };
1380
+ }
1381
+ function collectWork(options, identities) {
1382
+ const { home, now, own } = options;
1383
+ const work = [];
1384
+ for (const meta of listMetas(home)) {
1385
+ const { mapping } = meta;
1386
+ if (!ownedBy(meta, identities)) {
1387
+ continue;
1388
+ }
1389
+ if (!mapping || meta.off || meta.updatedAt < now - RETRY_WINDOW_MS) {
1390
+ continue;
1391
+ }
1392
+ const isOwn = own?.host === meta.host && own.sessionId === meta.sessionId;
1393
+ if (needsAttention(home, meta, now, isOwn)) {
1394
+ work.push(workFor(home, meta, mapping, now));
1395
+ }
1396
+ }
1397
+ return work;
1398
+ }
1399
+ function patchMeta(home, key, patch, sentEstimate = null) {
1400
+ const latest = readMeta(home, key.host, key.sessionId) ?? key;
1401
+ const estimate = sentEstimate && latest.estimate?.at === sentEstimate.at ? { ...latest.estimate, sent: true } : latest.estimate;
1402
+ writeMeta(home, {
1403
+ ...latest,
1404
+ ...patch,
1405
+ estimate,
1406
+ posted: { ...latest.posted, ...patch.posted }
1407
+ });
1408
+ }
1409
+ function holdBack(home, work, error) {
1410
+ for (const { meta } of work) {
1411
+ if (!meta.unsent || meta.lastError !== error) {
1412
+ patchMeta(home, meta, { unsent: true, lastError: error });
1413
+ }
1414
+ }
1415
+ }
1416
+ function settle(home, work) {
1417
+ for (const { meta, bytes } of work) {
1418
+ if (meta.unsent || meta.syncedBytes !== bytes) {
1419
+ patchMeta(home, meta, { unsent: false, syncedBytes: bytes });
1420
+ }
1421
+ }
1422
+ }
1423
+ async function postSession(home, credentials, item, signal, renew) {
1424
+ let current = credentials;
1425
+ const posted = {};
1426
+ let failure = null;
1427
+ let rejection = null;
1428
+ let sentEstimate = null;
1429
+ for (const summary of item.pending) {
1430
+ const withEstimate = summary === item.pending.at(-1);
1431
+ const body = toBody(item, summary, withEstimate);
1432
+ let result = await putSessionDay(current, body, signal);
1433
+ if (result.kind === "retry" && result.status === 401 && renew) {
1434
+ const renewed = await renew(current);
1435
+ if (renewed) {
1436
+ current = renewed;
1437
+ result = await putSessionDay(current, body, signal);
1438
+ }
1439
+ }
1440
+ if (result.kind === "off") {
1441
+ return { credentials: current, off: true };
1442
+ }
1443
+ if (result.kind === "retry") {
1444
+ failure = result.error;
1445
+ break;
1446
+ }
1447
+ const acknowledged = toPosted(summary, item);
1448
+ if (result.kind === "rejected") {
1449
+ rejection = result.error;
1450
+ posted[summary.day] = { ...acknowledged, rejected: result.status };
1451
+ } else {
1452
+ posted[summary.day] = acknowledged;
1453
+ }
1454
+ if (withEstimate) {
1455
+ sentEstimate = item.estimate;
1456
+ }
1457
+ }
1458
+ patchMeta(
1459
+ home,
1460
+ item.meta,
1461
+ failure === null ? { posted, unsent: false, syncedBytes: item.bytes, lastError: rejection } : { posted, unsent: true, lastError: failure },
1462
+ sentEstimate
1463
+ );
1464
+ return { credentials: current, off: false };
1465
+ }
1466
+ async function flush(options) {
1467
+ const { home, env, now, signal } = options;
1468
+ const credentials = readCredentials(env);
1469
+ if (!credentials.ok) {
1470
+ return { config: null };
1471
+ }
1472
+ const identities = identitiesOf(home, credentials.credentials);
1473
+ const work = collectWork(options, identities);
1474
+ const sendable = work.filter((item) => item.pending.length > 0);
1475
+ const config = options.config ?? (sendable.length > 0 ? await getConfig(home, credentials.credentials, now, signal, options.renew ?? null) : null);
1476
+ if (config?.ok && !config.enabled) {
1477
+ dropAllPending(home, identities);
1478
+ return { config };
1479
+ }
1480
+ settle(
1481
+ home,
1482
+ work.filter((item) => item.pending.length === 0)
1483
+ );
1484
+ if (!config || sendable.length === 0) {
1485
+ return { config };
1486
+ }
1487
+ if (!config.ok) {
1488
+ holdBack(home, sendable, config.error);
1489
+ return { config };
1490
+ }
1491
+ let current = credentials.credentials;
1492
+ for (const item of sendable) {
1493
+ if (signal.aborted) {
1494
+ holdBack(home, [item], "timed out");
1495
+ continue;
1496
+ }
1497
+ const posted = await postSession(home, current, item, signal, options.renew ?? null);
1498
+ if (posted.off) {
1499
+ forgetConfig(home);
1500
+ dropAllPending(home, identities);
1501
+ return { config };
1502
+ }
1503
+ current = posted.credentials;
1504
+ }
1505
+ return { config };
1506
+ }
1507
+
1508
+ // packages/agent-cli/src/hosts/claude-code.ts
1509
+ function mapClaudeCodeHook(event, payload) {
1510
+ if (!isRecord(payload)) {
1511
+ return null;
1512
+ }
1513
+ const { session_id: sessionId, cwd } = payload;
1514
+ if (!isSafeSessionId(sessionId) || typeof cwd !== "string" || cwd.length === 0) {
1515
+ return null;
1516
+ }
1517
+ const signal = (kind) => ({
1518
+ type: "event",
1519
+ sessionId,
1520
+ cwd,
1521
+ event: { kind }
1522
+ });
1523
+ const transcriptPath = pathOf(payload.transcript_path);
1524
+ const transcript = transcriptPath ? { transcriptPath } : {};
1525
+ switch (event) {
1526
+ case "SessionStart":
1527
+ return {
1528
+ type: "session_start",
1529
+ sessionId,
1530
+ cwd,
1531
+ // Present on some starts only; a headless start sends none.
1532
+ model: modelOf(payload.model),
1533
+ // A compaction restarts the session under the person's feet, mid-work.
1534
+ announce: payload.source !== "compact"
1535
+ };
1536
+ case "UserPromptSubmit":
1537
+ return signal("turn_start");
1538
+ case "PreToolUse":
1539
+ return signal("activity");
1540
+ case "PostToolUse": {
1541
+ const durationMs = durationOf(payload.duration_ms);
1542
+ return durationMs === void 0 ? signal("activity") : { type: "event", sessionId, cwd, event: { kind: "activity", durationMs } };
1543
+ }
1544
+ case "Notification":
1545
+ return payload.notification_type === "permission_prompt" ? signal("wait") : null;
1546
+ case "Stop":
1547
+ return { type: "event", sessionId, cwd, event: { kind: "turn_end" }, ...transcript };
1548
+ case "SessionEnd":
1549
+ return { type: "session_end", sessionId, cwd, ...transcript };
1550
+ default:
1551
+ return null;
1552
+ }
1553
+ }
1554
+
1555
+ // packages/agent-cli/src/hosts/codex.ts
1556
+ function mapCodexHook(event, payload) {
1557
+ if (!isRecord(payload)) {
1558
+ return null;
1559
+ }
1560
+ const { session_id: sessionId, cwd } = payload;
1561
+ if (!isSafeSessionId(sessionId) || typeof cwd !== "string" || cwd.length === 0) {
1562
+ return null;
1563
+ }
1564
+ const model = modelOf(payload.model);
1565
+ const transcriptPath = pathOf(payload.transcript_path);
1566
+ const signal = (kind, withTranscript = false) => ({
1567
+ type: "event",
1568
+ sessionId,
1569
+ cwd,
1570
+ event: { kind },
1571
+ ...model ? { model } : {},
1572
+ ...withTranscript && transcriptPath ? { transcriptPath } : {}
1573
+ });
1574
+ switch (event) {
1575
+ case "SessionStart":
1576
+ return {
1577
+ type: "session_start",
1578
+ sessionId,
1579
+ cwd,
1580
+ model,
1581
+ // A compaction restarts the session under the person's feet, mid-work.
1582
+ announce: payload.source !== "compact"
1583
+ };
1584
+ case "UserPromptSubmit":
1585
+ return signal("turn_start");
1586
+ case "PreToolUse":
1587
+ case "PostToolUse":
1588
+ return signal("activity");
1589
+ case "Stop":
1590
+ case "Interrupt":
1591
+ return signal("turn_end", true);
1592
+ case "SessionEnd":
1593
+ return { type: "session_end", sessionId, cwd, ...transcriptPath ? { transcriptPath } : {} };
1594
+ default:
1595
+ return null;
1596
+ }
1597
+ }
1598
+
1599
+ // packages/agent-cli/src/hosts/cursor.ts
1600
+ function directoryOf(payload) {
1601
+ if (typeof payload.cwd === "string" && payload.cwd.length > 0) {
1602
+ return payload.cwd;
1603
+ }
1604
+ const roots = payload.workspace_roots;
1605
+ const first = Array.isArray(roots) ? roots[0] : void 0;
1606
+ return typeof first === "string" && first.length > 0 ? first : null;
1607
+ }
1608
+ function mapCursorHook(event, payload) {
1609
+ if (!isRecord(payload)) {
1610
+ return null;
1611
+ }
1612
+ const sessionId = payload.conversation_id ?? payload.session_id;
1613
+ const cwd = directoryOf(payload);
1614
+ if (!isSafeSessionId(sessionId) || cwd === null) {
1615
+ return null;
1616
+ }
1617
+ const model = modelOf(payload.model);
1618
+ const signal = (kind, durationMs) => ({
1619
+ type: "event",
1620
+ sessionId,
1621
+ cwd,
1622
+ event: durationMs === void 0 ? { kind } : { kind, durationMs },
1623
+ ...model ? { model } : {}
1624
+ });
1625
+ switch (event) {
1626
+ case "sessionStart":
1627
+ return { type: "session_start", sessionId, cwd, model, announce: false };
1628
+ case "beforeSubmitPrompt":
1629
+ return signal("turn_start");
1630
+ case "preToolUse":
1631
+ case "postToolUseFailure":
1632
+ return signal("activity");
1633
+ case "postToolUse":
1634
+ return signal("activity", durationOf(payload.duration));
1635
+ case "stop":
1636
+ return signal("turn_end");
1637
+ case "sessionEnd":
1638
+ return { type: "session_end", sessionId, cwd };
1639
+ default:
1640
+ return null;
1641
+ }
1642
+ }
1643
+
1644
+ // packages/agent-cli/src/login-background.ts
1645
+ var import_node_fs8 = require("node:fs");
1646
+ function lockAge(home, now) {
1647
+ const value = readJson(loginLockPath(home));
1648
+ return isRecord(value) && typeof value.at === "number" ? now - value.at : null;
1649
+ }
1650
+ function loginInProgress(home, now) {
1651
+ const age = lockAge(home, now);
1652
+ return age !== null && age < LOGIN_LOCK_STALE_MS;
1653
+ }
1654
+ function takeLoginLock(home, now) {
1655
+ ensurePrivateDir(home);
1656
+ const path = loginLockPath(home);
1657
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1658
+ try {
1659
+ const fd = (0, import_node_fs8.openSync)(path, "wx", 384);
1660
+ (0, import_node_fs8.writeSync)(fd, JSON.stringify({ at: now }));
1661
+ (0, import_node_fs8.closeSync)(fd);
1662
+ return () => (0, import_node_fs8.rmSync)(path, { force: true });
1663
+ } catch {
1664
+ if (loginInProgress(home, now)) {
1665
+ return null;
1666
+ }
1667
+ (0, import_node_fs8.rmSync)(path, { force: true });
1668
+ }
1669
+ }
1670
+ return null;
1671
+ }
1672
+ function readLoginLink(home) {
1673
+ const value = readJson(loginLinkPath(home));
1674
+ if (isRecord(value) && typeof value.url === "string") {
1675
+ return { url: value.url };
1676
+ }
1677
+ return isRecord(value) && typeof value.error === "string" ? { error: value.error } : null;
1678
+ }
1679
+ function writeLoginLink(home, link) {
1680
+ writePrivateJson(loginLinkPath(home), link);
1681
+ }
1682
+ function clearLoginLink(home) {
1683
+ (0, import_node_fs8.rmSync)(loginLinkPath(home), { force: true });
1684
+ }
1685
+ async function waitForLoginLink(home, now) {
1686
+ const deadline = Date.now() + LOGIN_LINK_WAIT_MS;
1687
+ let started = false;
1688
+ while (Date.now() < deadline) {
1689
+ const link = readLoginLink(home);
1690
+ if (link) {
1691
+ return link;
1692
+ }
1693
+ const running = loginInProgress(home, now);
1694
+ if (started && !running) {
1695
+ return null;
1696
+ }
1697
+ started = started || running;
1698
+ await new Promise((done) => setTimeout(done, LOCK_POLL_MS));
1699
+ }
1700
+ return null;
1701
+ }
1702
+ function quiet(rt, errors) {
1703
+ return { ...rt, out: () => void 0, err: (text) => errors.push(text) };
1704
+ }
1705
+
1706
+ // packages/agent-cli/src/sign-in-prompt.ts
1707
+ function readRecord(home) {
1708
+ const value = readJson(signInPromptPath(home));
1709
+ return {
1710
+ lastOpenedDay: isRecord(value) && typeof value.lastOpenedDay === "string" ? value.lastOpenedDay : null,
1711
+ declined: isRecord(value) && value.declined === true
1712
+ };
1713
+ }
1714
+ function setDeclined(home, declined) {
1715
+ writePrivateJson(signInPromptPath(home), { ...readRecord(home), declined });
1716
+ }
1717
+ function promptSignIn(rt, home, at, credentials, couldTrack) {
1718
+ if (credentials.ok || !credentials.signIn || !couldTrack) {
1719
+ return null;
1720
+ }
1721
+ const record = readRecord(home);
1722
+ const today = localDay(at);
1723
+ if (record.declined || record.lastOpenedDay === today || loginInProgress(home, at)) {
1724
+ return "not-now";
1725
+ }
1726
+ writePrivateJson(signInPromptPath(home), { ...record, lastOpenedDay: today });
1727
+ rt.background(["login", "--child"]);
1728
+ return "opened";
1729
+ }
1730
+
1731
+ // packages/agent-cli/src/status-line.ts
1732
+ var NAME = "Time Trakkr";
1733
+ var MAX_WORKSPACE_NAME = 80;
1734
+ function notTracked(reason) {
1735
+ return `${NAME}: not tracked (${reason}).`;
1736
+ }
1737
+ function workspaceOf(config) {
1738
+ const name = config?.ok ? oneLine(config.workspaceName, MAX_WORKSPACE_NAME) : "";
1739
+ return name || "this workspace";
1740
+ }
1741
+ function switchedOffLine(config) {
1742
+ const reason = config?.ok ? config.offReason : null;
1743
+ return reason ? `${NAME}: not tracked. ${reason}` : notTracked(`agent time is not switched on for ${workspaceOf(config)}`);
1744
+ }
1745
+ function describeMapping(mapping) {
1746
+ return mapping.label ?? `project ${mapping.projectId}, task ${mapping.taskId}`;
1747
+ }
1748
+ function signInLine(host, why, prompt) {
1749
+ return prompt === "opened" ? `${NAME}: ${why}. A browser window has opened to sign in; once you allow it, tracking starts at your next prompt.` : `${notTracked(why)} ${HOST_SIGN_IN_HINT[host]}`;
1750
+ }
1751
+ function sessionStartLine(meta, credentials, config) {
1752
+ const switchedOff = config?.ok === true && !config.enabled;
1753
+ if (!meta.mapping) {
1754
+ return switchedOff && meta.untrackedReason === NOT_MAPPED ? switchedOffLine(config) : notTracked(meta.untrackedReason ?? "no mapping");
1755
+ }
1756
+ if (!credentials.ok) {
1757
+ return notTracked(credentials.reason);
1758
+ }
1759
+ if (meta.off) {
1760
+ return `${NAME}: tracking is off for this session. Run "timetrakkr on" to resume.`;
1761
+ }
1762
+ if (switchedOff) {
1763
+ return switchedOffLine(config);
1764
+ }
1765
+ const target = describeMapping(meta.mapping);
1766
+ if (config && !config.ok) {
1767
+ const problem = config.status === void 0 ? `could not reach ${NAME} (${config.error})` : `sign-in failed (${oneLine(config.error)})`;
1768
+ return `${NAME}: tracking to ${target}, but ${problem}. Nothing is sent until that works.`;
1769
+ }
1770
+ return `${NAME}: tracking to ${target}. Run "timetrakkr off" to stop.`;
1771
+ }
1772
+
1773
+ // packages/agent-cli/src/hook.ts
1774
+ var ADAPTERS = {
1775
+ "claude-code": mapClaudeCodeHook,
1776
+ codex: mapCodexHook,
1777
+ cursor: mapCursorHook
1778
+ };
1779
+ function modelFrom(signal) {
1780
+ if (signal.type === "session_start") {
1781
+ return signal.model;
1782
+ }
1783
+ return signal.type === "event" ? signal.model ?? null : null;
1784
+ }
1785
+ function parsePayload(text) {
1786
+ try {
1787
+ return JSON.parse(text);
1788
+ } catch {
1789
+ return null;
1790
+ }
1791
+ }
1792
+ function newMeta(host, sessionId, cwd) {
1793
+ return {
1794
+ version: 1,
1795
+ host,
1796
+ sessionId,
1797
+ cwd,
1798
+ root: null,
1799
+ repository: null,
1800
+ branch: null,
1801
+ mapping: null,
1802
+ untrackedReason: null,
1803
+ off: false,
1804
+ model: null,
1805
+ estimate: null,
1806
+ posted: {},
1807
+ syncedBytes: 0,
1808
+ unsent: false,
1809
+ lastError: null,
1810
+ updatedAt: 0
1811
+ };
1812
+ }
1813
+ function refreshesContext(signal) {
1814
+ return signal.type === "session_start" || signal.type === "event" && signal.event.kind === "turn_start";
1815
+ }
1816
+ function reachesServer(signal) {
1817
+ return signal.type !== "event" || signal.event.kind === "turn_start" || signal.event.kind === "turn_end";
1818
+ }
1819
+ var FOREIGN_SESSION = "this session began under a different sign-in; start a new session to track it";
1820
+ function isTrackable(home, meta, credentials) {
1821
+ return meta.mapping !== null && !meta.off && credentials.ok && ownedBy(meta, identitiesOf(home, credentials.credentials));
1822
+ }
1823
+ async function isForeign(run2, existing) {
1824
+ const { home, credentials, auth, at, signal } = run2;
1825
+ if (!credentials.ok || typeof existing.identity !== "string") {
1826
+ return false;
1827
+ }
1828
+ const mine = () => ownedBy(existing, identitiesOf(home, credentials.credentials));
1829
+ if (mine()) {
1830
+ return false;
1831
+ }
1832
+ if (!isMemberBound(existing.identity) || memberIsKnown(home, credentials.credentials)) {
1833
+ return true;
1834
+ }
1835
+ await getConfig(home, credentials.credentials, at, signal, auth.renew);
1836
+ return !mine();
1837
+ }
1838
+ async function refresh(run2, existing) {
1839
+ const { rt, home, host, hookSignal, credentials, auth, at, signal } = run2;
1840
+ if (existing && await isForeign(run2, existing)) {
1841
+ const meta2 = { ...existing, mapping: null, untrackedReason: FOREIGN_SESSION };
1842
+ return { meta: meta2, config: null, couldTrack: false };
1843
+ }
1844
+ const local = readLocalContext(hookSignal.cwd, rt.env);
1845
+ const couldTrack = couldBeTracked(local);
1846
+ const config = credentials.ok && couldTrack ? await getConfig(home, credentials.credentials, at, signal, auth.renew) : null;
1847
+ const rules = config?.ok ? config.mappings : credentials.ok ? lastKnownMappings(home, credentials.credentials) : null;
1848
+ const context = resolveContext(local, rules, credentials);
1849
+ if (existing?.mapping && !context.mapping) {
1850
+ await flush({
1851
+ home,
1852
+ env: rt.env,
1853
+ now: at,
1854
+ signal,
1855
+ own: existing,
1856
+ config,
1857
+ renew: auth.renew
1858
+ });
1859
+ }
1860
+ const base = readMeta(home, host, hookSignal.sessionId) ?? newMeta(host, hookSignal.sessionId, "");
1861
+ const model = modelFrom(hookSignal) ?? base.model;
1862
+ const tracked = context.mapping !== null;
1863
+ const meta = {
1864
+ ...base,
1865
+ ...context,
1866
+ // Where an untracked session is running is not written down.
1867
+ cwd: tracked ? hookSignal.cwd : base.cwd,
1868
+ root: tracked ? context.root : base.root,
1869
+ // The member, once the server has said who that is; the credentials until then.
1870
+ identity: tracked && credentials.ok ? identitiesOf(home, credentials.credentials)[0] ?? null : base.identity ?? null,
1871
+ credentialId: tracked && credentials.ok ? credentials.credentials.cacheId : base.credentialId ?? null,
1872
+ model,
1873
+ updatedAt: at
1874
+ };
1875
+ if (tracked || existing) {
1876
+ writeMeta(home, meta);
1877
+ }
1878
+ return { meta, config, couldTrack };
1879
+ }
1880
+ async function load(run2) {
1881
+ const { home, host, hookSignal, credentials, at } = run2;
1882
+ const existing = readMeta(home, host, hookSignal.sessionId);
1883
+ if (refreshesContext(hookSignal)) {
1884
+ return refresh(run2, existing);
1885
+ }
1886
+ if (!existing) {
1887
+ return { meta: newMeta(host, hookSignal.sessionId, ""), config: null, couldTrack: false };
1888
+ }
1889
+ const config = credentials.ok && isTrackable(home, existing, credentials) ? peekConfig(home, credentials.credentials, at) : null;
1890
+ return { meta: existing, config, couldTrack: false };
1891
+ }
1892
+ function readTokens(run2, meta) {
1893
+ const { home, host, hookSignal } = run2;
1894
+ if (hookSignal.type === "session_start" || !hookSignal.transcriptPath) {
1895
+ return;
1896
+ }
1897
+ const { events } = readLog(home, host, meta.sessionId);
1898
+ updateTokens(
1899
+ home,
1900
+ host,
1901
+ meta.sessionId,
1902
+ hookSignal.transcriptPath,
1903
+ events,
1904
+ localDay,
1905
+ modelFrom(hookSignal) ?? meta.model
1906
+ );
1907
+ }
1908
+ async function handleHook(rt, host, event, signal = AbortSignal.timeout(NETWORK_BUDGET_MS)) {
1909
+ const at = rt.now();
1910
+ const hookSignal = ADAPTERS[host](event, parsePayload(await rt.stdin()));
1911
+ if (!hookSignal) {
1912
+ return;
1913
+ }
1914
+ const home = stateHome(rt.env);
1915
+ const auth = authSession(rt, home, at, signal);
1916
+ const credentials = reachesServer(hookSignal) ? await auth.fresh() : readCredentials(rt.env);
1917
+ const run2 = { rt, home, host, hookSignal, credentials, auth, at, signal };
1918
+ const { meta, config, couldTrack } = await load(run2);
1919
+ const switchedOff = config?.ok === true && !config.enabled;
1920
+ const recording = isTrackable(home, meta, credentials) && !switchedOff;
1921
+ if (hookSignal.type === "event" && recording) {
1922
+ appendEvent(home, host, meta.sessionId, { ...hookSignal.event, at });
1923
+ }
1924
+ if (!reachesServer(hookSignal)) {
1925
+ return;
1926
+ }
1927
+ if (recording) {
1928
+ readTokens(run2, meta);
1929
+ }
1930
+ const prompt = hookSignal.type === "session_start" ? promptSignIn(rt, home, at, credentials, couldTrack) : null;
1931
+ if (hookSignal.type === "session_start") {
1932
+ pruneSessions(home, at - PRUNE_AFTER_MS);
1933
+ }
1934
+ const outcome = await flush({
1935
+ home,
1936
+ env: rt.env,
1937
+ now: at,
1938
+ signal,
1939
+ own: { host, sessionId: meta.sessionId },
1940
+ config,
1941
+ renew: auth.renew
1942
+ });
1943
+ if (hookSignal.type === "session_start" && hookSignal.announce) {
1944
+ const line = prompt && !credentials.ok && credentials.signIn ? signInLine(host, credentials.signIn.why, prompt) : sessionStartLine(meta, credentials, outcome.config);
1945
+ rt.out(JSON.stringify({ systemMessage: line }));
1946
+ }
1947
+ }
1948
+
1949
+ // packages/agent-cli/src/session-choice.ts
1950
+ var NO_SESSION = "No session found for this directory. Run this from inside the agent session.";
1951
+ function sessionsFromEnvironment(rt, home) {
1952
+ return HOSTS.flatMap((host) => {
1953
+ const variable = HOST_SESSION_ENV[host];
1954
+ const sessionId = variable ? rt.env[variable] : void 0;
1955
+ return variable && isSafeSessionId(sessionId) ? [{ host, sessionId, because: `from ${variable}`, meta: readMeta(home, host, sessionId) }] : [];
1956
+ });
1957
+ }
1958
+ function sessionFromEnvironment(rt, home) {
1959
+ const named = sessionsFromEnvironment(rt, home);
1960
+ const active = named.filter((session) => session.meta !== null).sort((a, b) => (b.meta?.updatedAt ?? 0) - (a.meta?.updatedAt ?? 0));
1961
+ return active[0] ?? named[0] ?? null;
1962
+ }
1963
+ function mostRecentSessionHere(rt, home) {
1964
+ const root = findRepositoryRoot(rt.cwd);
1965
+ const here = listMetas(home).filter((meta2) => meta2.cwd === rt.cwd || root !== null && meta2.root === root).sort((a, b) => b.updatedAt - a.updatedAt);
1966
+ const meta = here[0];
1967
+ return meta ? {
1968
+ host: meta.host,
1969
+ sessionId: meta.sessionId,
1970
+ because: "the most recent session in this directory; the host did not name one",
1971
+ meta
1972
+ } : null;
1973
+ }
1974
+ function chooseSession(rt, home) {
1975
+ return sessionFromEnvironment(rt, home) ?? mostRecentSessionHere(rt, home);
1976
+ }
1977
+
1978
+ // packages/agent-cli/src/commands.ts
1979
+ function switchTracking(rt, off) {
1980
+ const home = stateHome(rt.env);
1981
+ const chosen = chooseSession(rt, home);
1982
+ if (!chosen) {
1983
+ rt.err(NO_SESSION);
1984
+ return 1;
1985
+ }
1986
+ const meta = chosen.meta ?? newMeta(chosen.host, chosen.sessionId, "");
1987
+ if (off && !meta.off && meta.mapping) {
1988
+ appendEvent(home, meta.host, meta.sessionId, { kind: "turn_end", at: rt.now() });
1989
+ }
1990
+ writeMeta(home, { ...meta, off, updatedAt: rt.now() });
1991
+ rt.out(
1992
+ `Tracking is ${off ? "off" : "on"} for ${HOST_LABELS[chosen.host]} session ${chosen.sessionId} (${chosen.because}).`
1993
+ );
1994
+ return 0;
1995
+ }
1996
+ function sourceNote(credentials, home) {
1997
+ if (credentials.source === "sign-in") {
1998
+ return "signed in with timetrakkr login";
1999
+ }
2000
+ const stored = readAuth(home);
2001
+ return stored && !isSignedOut(stored) ? "TIMETRAKKR_TOKEN from the environment; the stored sign-in is not used" : "TIMETRAKKR_TOKEN from the environment";
2002
+ }
2003
+ function signInLines(credentials, config, home) {
2004
+ if (!credentials.ok) {
2005
+ return [`Sign-in: ${credentials.reason}`];
2006
+ }
2007
+ if (!config) {
2008
+ return ["Sign-in: not checked (nothing here could be tracked)"];
2009
+ }
2010
+ if (!config.ok) {
2011
+ return [`Sign-in: failed (${oneLine(config.error)})`];
2012
+ }
2013
+ const workspace = oneLine(config.workspaceName) || "workspace";
2014
+ return [
2015
+ `Sign-in: OK (${sourceNote(credentials.credentials, home)})`,
2016
+ `Workspace: ${workspace}, agent time is ${config.enabled ? "on" : "off"}`,
2017
+ ...config.offReason ? [`Why off: ${config.offReason}`] : []
2018
+ ];
2019
+ }
2020
+ function mappingLines(context) {
2021
+ const { mapping } = context;
2022
+ if (!mapping) {
2023
+ return [`Not tracked: ${context.untrackedReason ?? "no mapping"}`];
2024
+ }
2025
+ const source = mapping.source === "file" ? ".timetrakkr.json" : "a rule on the platform";
2026
+ return [
2027
+ `Tracked to: ${describeMapping(mapping)} (${source})`,
2028
+ `Project: ${mapping.projectId}`,
2029
+ `Task: ${mapping.taskId}`
2030
+ ];
2031
+ }
2032
+ async function askConfig(credentials, renew, signal) {
2033
+ const first = await fetchConfig(credentials, signal);
2034
+ if (first.ok || first.status !== 401) {
2035
+ return first;
2036
+ }
2037
+ const renewed = await renew(credentials);
2038
+ return renewed ? fetchConfig(renewed, signal) : first;
2039
+ }
2040
+ async function printStatus(rt) {
2041
+ const home = stateHome(rt.env);
2042
+ const signal = AbortSignal.timeout(COMMAND_NETWORK_MS);
2043
+ const auth = authSession(rt, home, rt.now(), signal);
2044
+ const credentials = await auth.fresh();
2045
+ const local = readLocalContext(rt.cwd, rt.env);
2046
+ const config = credentials.ok && couldBeTracked(local) ? await askConfig(credentials.credentials, auth.renew, signal) : null;
2047
+ const rules = config?.ok ? config.mappings : credentials.ok ? lastKnownMappings(home, credentials.credentials) : null;
2048
+ const context = resolveContext(local, rules, credentials);
2049
+ const lines = [`Directory: ${rt.cwd}`];
2050
+ if (context.root) {
2051
+ lines.push(`Repository: ${context.root}`);
2052
+ }
2053
+ lines.push(...mappingLines(context), ...signInLines(credentials, config, home));
2054
+ const session = chooseSession(rt, home);
2055
+ if (session?.meta) {
2056
+ const state = session.meta.off ? "tracking off" : "tracking on";
2057
+ lines.push(`Session: ${session.sessionId}, ${state} (${session.because})`);
2058
+ if (session.meta.lastError) {
2059
+ lines.push(`Last error: ${oneLine(session.meta.lastError)}`);
2060
+ }
2061
+ }
2062
+ for (const line of lines) {
2063
+ rt.out(line);
2064
+ }
2065
+ return 0;
2066
+ }
2067
+
2068
+ // packages/agent-cli/src/error-log.ts
2069
+ var import_node_fs9 = require("node:fs");
2070
+ function logHookError(home, now, where, error) {
2071
+ try {
2072
+ const path = errorLogPath(home);
2073
+ const message = error instanceof Error ? error.message : "unexpected error";
2074
+ try {
2075
+ if ((0, import_node_fs9.statSync)(path).size > ERROR_LOG_MAX_BYTES) {
2076
+ (0, import_node_fs9.truncateSync)(path);
2077
+ }
2078
+ } catch {
2079
+ }
2080
+ appendPrivateLine(path, `${new Date(now).toISOString()} ${where}: ${message}`);
2081
+ } catch {
2082
+ }
2083
+ }
2084
+
2085
+ // packages/agent-cli/src/estimate.ts
2086
+ var UNITS = [
2087
+ { flag: "--weeks", word: "week", hours: DAYS_PER_WEEK * HOURS_PER_DAY },
2088
+ { flag: "--days", word: "day", hours: HOURS_PER_DAY },
2089
+ { flag: "--hours", word: "hour", hours: 1 }
2090
+ ];
2091
+ var FLAGS = [...UNITS.map((unit) => unit.flag), "--label"];
2092
+ function readFlags(args) {
2093
+ const flags = /* @__PURE__ */ new Map();
2094
+ for (let i = 0; i < args.length; i += 1) {
2095
+ const [name = "", inline] = (args[i] ?? "").split(/=(.*)/s, 2);
2096
+ if (!FLAGS.includes(name)) {
2097
+ return `Unknown option ${name}.`;
2098
+ }
2099
+ const value = inline ?? args[++i];
2100
+ if (value === void 0) {
2101
+ return `${name} needs a value.`;
2102
+ }
2103
+ flags.set(name, value);
2104
+ }
2105
+ return flags;
2106
+ }
2107
+ function parseEstimate(args) {
2108
+ const flags = readFlags(args);
2109
+ if (typeof flags === "string") {
2110
+ return { ok: false, error: flags };
2111
+ }
2112
+ let hours = 0;
2113
+ const words = [];
2114
+ for (const unit of UNITS) {
2115
+ const raw = flags.get(unit.flag);
2116
+ if (raw === void 0) {
2117
+ continue;
2118
+ }
2119
+ const amount = Number(raw);
2120
+ if (raw.trim() === "" || !Number.isFinite(amount)) {
2121
+ return { ok: false, error: `${unit.flag} must be a number.` };
2122
+ }
2123
+ if (amount <= 0) {
2124
+ return { ok: false, error: `${unit.flag} must be greater than zero.` };
2125
+ }
2126
+ hours += amount * unit.hours;
2127
+ words.push(`${amount} ${unit.word}${amount === 1 ? "" : "s"}`);
2128
+ }
2129
+ if (words.length === 0) {
2130
+ return { ok: false, error: "Give a duration: --hours, --days or --weeks." };
2131
+ }
2132
+ const seconds = Math.round(hours * 3600);
2133
+ if (seconds < 1 || seconds > MAX_ESTIMATE_SECONDS) {
2134
+ return {
2135
+ ok: false,
2136
+ error: `The estimate must be at most ${MAX_ESTIMATE_SECONDS / 3600} hours.`
2137
+ };
2138
+ }
2139
+ const label = oneLine(flags.get("--label") ?? "", Number.MAX_SAFE_INTEGER);
2140
+ if (label === "") {
2141
+ return { ok: false, error: "Give a short name for the work with --label." };
2142
+ }
2143
+ if (label.length > MAX_ESTIMATE_LABEL) {
2144
+ return { ok: false, error: `--label must be at most ${MAX_ESTIMATE_LABEL} characters.` };
2145
+ }
2146
+ return { ok: true, seconds, label, words: words.join(" ") };
2147
+ }
2148
+ function recordEstimate(rt, args) {
2149
+ const parsed = parseEstimate(args);
2150
+ if (!parsed.ok) {
2151
+ rt.err(parsed.error);
2152
+ return 1;
2153
+ }
2154
+ const home = stateHome(rt.env);
2155
+ const chosen = chooseSession(rt, home);
2156
+ if (!chosen) {
2157
+ rt.err(NO_SESSION);
2158
+ return 1;
2159
+ }
2160
+ const { meta } = chosen;
2161
+ if (!meta?.mapping || meta.off) {
2162
+ rt.out("This session is not tracked, so the estimate was not recorded.");
2163
+ return 0;
2164
+ }
2165
+ const estimate = { seconds: parsed.seconds, label: parsed.label, at: rt.now(), sent: false };
2166
+ writeMeta(home, { ...meta, estimate });
2167
+ rt.out(
2168
+ `Recorded an estimate of ${parsed.words} ("${parsed.label}") for ${HOST_LABELS[chosen.host]} session ${chosen.sessionId}.`
2169
+ );
2170
+ return 0;
2171
+ }
2172
+
2173
+ // packages/agent-cli/src/login.ts
2174
+ var import_node_crypto5 = require("node:crypto");
2175
+ var import_node_os2 = require("node:os");
2176
+
2177
+ // packages/agent-cli/src/login-listener.ts
2178
+ var import_node_http = require("node:http");
2179
+ var STYLE = "body{font:16px/1.5 system-ui,sans-serif;max-width:32rem;margin:15vh auto;padding:0 1.5rem;color:#1a1a1a}h1{font-size:1.4rem}";
2180
+ var PAGE = (title, body) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${title}</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>${STYLE}</style></head><body><h1>${title}</h1><p>${body}</p></body></html>`;
2181
+ var PAGES = {
2182
+ signedIn: PAGE(
2183
+ "You are signed in",
2184
+ "Time Trakkr is connected on this computer. You can close this tab and go back to your terminal."
2185
+ ),
2186
+ failed: PAGE(
2187
+ "Sign-in did not finish",
2188
+ "Time Trakkr could not complete the sign-in. Go back to your terminal for the reason, then try again."
2189
+ ),
2190
+ denied: PAGE(
2191
+ "Sign-in was declined",
2192
+ "Nothing was connected. You can close this tab. Run the sign-in again if that was a mistake."
2193
+ ),
2194
+ mismatch: PAGE(
2195
+ "This link is not for this sign-in",
2196
+ "It does not match the sign-in waiting in your terminal. Go back to the terminal and use the link shown there."
2197
+ ),
2198
+ notFound: PAGE("Nothing here", "This address only finishes a Time Trakkr sign-in.")
2199
+ };
2200
+ function respond(res, status, page) {
2201
+ res.writeHead(status, {
2202
+ "content-type": "text/html; charset=utf-8",
2203
+ "cache-control": "no-store",
2204
+ "referrer-policy": "no-referrer",
2205
+ "x-content-type-options": "nosniff",
2206
+ "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'",
2207
+ connection: "close"
2208
+ });
2209
+ res.end(page);
2210
+ }
2211
+ function closeServer(server) {
2212
+ return new Promise((done) => {
2213
+ server.closeAllConnections();
2214
+ server.close(() => done());
2215
+ });
2216
+ }
2217
+ async function startLoginListener() {
2218
+ let handler = (_req, res) => respond(res, 404, PAGES.notFound);
2219
+ const server = (0, import_node_http.createServer)((req, res) => handler(req, res));
2220
+ await new Promise((done, fail) => {
2221
+ server.once("error", fail);
2222
+ server.listen(0, "127.0.0.1", done);
2223
+ });
2224
+ const { port } = server.address();
2225
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
2226
+ const wait = (state, timeoutMs, settle2) => new Promise((done) => {
2227
+ let finished = false;
2228
+ const finish = (outcome) => {
2229
+ finished = true;
2230
+ clearTimeout(timer);
2231
+ done(outcome);
2232
+ };
2233
+ const timer = setTimeout(() => finish({ kind: "timeout" }), timeoutMs);
2234
+ handler = (req, res) => {
2235
+ const url = new URL(req.url ?? "/", redirectUri);
2236
+ if (finished || req.method !== "GET" || url.pathname !== "/callback") {
2237
+ return respond(res, 404, PAGES.notFound);
2238
+ }
2239
+ if (url.searchParams.get("state") !== state) {
2240
+ return respond(res, 400, PAGES.mismatch);
2241
+ }
2242
+ const code = url.searchParams.get("code");
2243
+ if (!code) {
2244
+ respond(res, 200, PAGES.denied);
2245
+ return finish({ kind: "denied" });
2246
+ }
2247
+ finished = true;
2248
+ settle2(code).catch(() => "failed").then((verdict) => {
2249
+ respond(res, 200, verdict === "signed-in" ? PAGES.signedIn : PAGES.failed);
2250
+ finish({ kind: "code", code });
2251
+ });
2252
+ };
2253
+ });
2254
+ return { redirectUri, wait, close: () => closeServer(server) };
2255
+ }
2256
+
2257
+ // packages/agent-cli/src/oauth.ts
2258
+ var FORM = { "content-type": "application/x-www-form-urlencoded", accept: "application/json" };
2259
+ async function readJsonBody2(response) {
2260
+ try {
2261
+ return await response.json();
2262
+ } catch {
2263
+ return null;
2264
+ }
2265
+ }
2266
+ function describeThrown2(error) {
2267
+ const timedOut = error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError");
2268
+ return timedOut ? "timed out" : "no connection";
2269
+ }
2270
+ async function discover(origin, signal) {
2271
+ let body;
2272
+ try {
2273
+ const response = await fetch(`${origin}/.well-known/oauth-authorization-server`, {
2274
+ signal,
2275
+ redirect: "error",
2276
+ headers: { accept: "application/json" }
2277
+ });
2278
+ if (!response.ok) {
2279
+ return { ok: false, error: `the server answered HTTP ${response.status}` };
2280
+ }
2281
+ body = await readJsonBody2(response);
2282
+ } catch (error) {
2283
+ return { ok: false, error: describeThrown2(error) };
2284
+ }
2285
+ if (!isRecord(body)) {
2286
+ return { ok: false, error: "the server did not describe its sign-in endpoints" };
2287
+ }
2288
+ const endpoints = {
2289
+ authorize: body.authorization_endpoint,
2290
+ token: body.token_endpoint,
2291
+ register: body.registration_endpoint,
2292
+ revoke: body.revocation_endpoint
2293
+ };
2294
+ for (const url of Object.values(endpoints)) {
2295
+ if (typeof url !== "string") {
2296
+ return { ok: false, error: "the server did not describe its sign-in endpoints" };
2297
+ }
2298
+ if (!URL.canParse(url) || new URL(url).origin !== origin) {
2299
+ return { ok: false, error: "the server pointed sign-in at another host" };
2300
+ }
2301
+ }
2302
+ return { ok: true, endpoints };
2303
+ }
2304
+ async function registerClient(endpoint, clientName, redirectUri, signal) {
2305
+ try {
2306
+ const response = await fetch(endpoint, {
2307
+ method: "POST",
2308
+ signal,
2309
+ redirect: "error",
2310
+ headers: { "content-type": "application/json", accept: "application/json" },
2311
+ body: JSON.stringify({
2312
+ client_name: clientName,
2313
+ redirect_uris: [redirectUri],
2314
+ token_endpoint_auth_method: "none"
2315
+ })
2316
+ });
2317
+ const body = await readJsonBody2(response);
2318
+ if (response.ok && isRecord(body) && typeof body.client_id === "string" && body.client_id) {
2319
+ return { ok: true, clientId: body.client_id };
2320
+ }
2321
+ const code = isRecord(body) && typeof body.error === "string" ? `: ${body.error}` : "";
2322
+ return { ok: false, error: `the server answered HTTP ${response.status}${code}` };
2323
+ } catch (error) {
2324
+ return { ok: false, error: describeThrown2(error) };
2325
+ }
2326
+ }
2327
+ function parsePair(body) {
2328
+ if (!isRecord(body) || typeof body.access_token !== "string" || typeof body.refresh_token !== "string" || typeof body.expires_in !== "number" || body.access_token === "" || body.refresh_token === "" || !(body.expires_in > 0)) {
2329
+ return { ok: false, kind: "unavailable", error: "unexpected response" };
2330
+ }
2331
+ if (body.scope !== "agent") {
2332
+ return { ok: false, kind: "wrong_scope" };
2333
+ }
2334
+ return {
2335
+ ok: true,
2336
+ pair: {
2337
+ accessToken: body.access_token,
2338
+ refreshToken: body.refresh_token,
2339
+ expiresIn: body.expires_in
2340
+ }
2341
+ };
2342
+ }
2343
+ async function tokenRequest(endpoint, form, signal) {
2344
+ try {
2345
+ const response = await fetch(endpoint, {
2346
+ method: "POST",
2347
+ signal,
2348
+ redirect: "error",
2349
+ headers: FORM,
2350
+ body: new URLSearchParams(form).toString()
2351
+ });
2352
+ const body = await readJsonBody2(response);
2353
+ if (response.ok) {
2354
+ return parsePair(body);
2355
+ }
2356
+ const code = isRecord(body) && typeof body.error === "string" ? body.error : "";
2357
+ return code === "invalid_grant" || code === "invalid_client" ? { ok: false, kind: "invalid_grant" } : { ok: false, kind: "unavailable", error: `HTTP ${response.status}` };
2358
+ } catch (error) {
2359
+ return { ok: false, kind: "unavailable", error: describeThrown2(error) };
2360
+ }
2361
+ }
2362
+ function exchangeCode(endpoint, grant, signal) {
2363
+ return tokenRequest(
2364
+ endpoint,
2365
+ {
2366
+ grant_type: "authorization_code",
2367
+ code: grant.code,
2368
+ client_id: grant.clientId,
2369
+ redirect_uri: grant.redirectUri,
2370
+ code_verifier: grant.codeVerifier
2371
+ },
2372
+ signal
2373
+ );
2374
+ }
2375
+ function refreshPair(endpoint, clientId, refreshToken, signal) {
2376
+ return tokenRequest(
2377
+ endpoint,
2378
+ { grant_type: "refresh_token", refresh_token: refreshToken, client_id: clientId },
2379
+ signal
2380
+ );
2381
+ }
2382
+ async function revoke(endpoint, clientId, refreshToken, signal) {
2383
+ try {
2384
+ const response = await fetch(endpoint, {
2385
+ method: "POST",
2386
+ signal,
2387
+ redirect: "error",
2388
+ headers: FORM,
2389
+ body: new URLSearchParams({ token: refreshToken, client_id: clientId }).toString()
2390
+ });
2391
+ return response.ok ? { ok: true } : { ok: false, error: `HTTP ${response.status}` };
2392
+ } catch (error) {
2393
+ return { ok: false, error: describeThrown2(error) };
2394
+ }
2395
+ }
2396
+
2397
+ // packages/agent-cli/src/login.ts
2398
+ var MAX_CLIENT_NAME = 200;
2399
+ var MODE_FLAGS = { "--detach": "detach", "--child": "child" };
2400
+ function parseOptions(args) {
2401
+ let url;
2402
+ let timeoutMs = LOGIN_TIMEOUT_MS;
2403
+ let browser = true;
2404
+ let mode = "interactive";
2405
+ for (let i = 0; i < args.length; i += 1) {
2406
+ const [name = "", inline] = (args[i] ?? "").split(/=(.*)/s, 2);
2407
+ if (name === "--no-browser") {
2408
+ browser = false;
2409
+ continue;
2410
+ }
2411
+ const flagMode = MODE_FLAGS[name];
2412
+ if (flagMode) {
2413
+ mode = flagMode;
2414
+ continue;
2415
+ }
2416
+ if (name !== "--url" && name !== "--timeout") {
2417
+ return { ok: false, error: `Unknown option ${name}.` };
2418
+ }
2419
+ const value = inline ?? args[++i];
2420
+ if (value === void 0) {
2421
+ return { ok: false, error: `${name} needs a value.` };
2422
+ }
2423
+ if (name === "--url") {
2424
+ url = value;
2425
+ } else {
2426
+ const seconds = Number(value);
2427
+ if (!Number.isFinite(seconds) || seconds <= 0) {
2428
+ return { ok: false, error: "--timeout must be a number of seconds." };
2429
+ }
2430
+ timeoutMs = seconds * 1e3;
2431
+ }
2432
+ }
2433
+ return { ok: true, url, timeoutMs, browser, mode };
2434
+ }
2435
+ function authorizeUrl(endpoints, clientId, redirectUri, state, codeVerifier) {
2436
+ const url = new URL(endpoints.authorize);
2437
+ url.search = new URLSearchParams({
2438
+ response_type: "code",
2439
+ client_id: clientId,
2440
+ redirect_uri: redirectUri,
2441
+ scope: "agent",
2442
+ state,
2443
+ code_challenge: (0, import_node_crypto5.createHash)("sha256").update(codeVerifier).digest("base64url"),
2444
+ code_challenge_method: "S256"
2445
+ }).toString();
2446
+ return url.toString();
2447
+ }
2448
+ function describeTokenFailure(result) {
2449
+ if (result.kind === "wrong_scope") {
2450
+ return "The server granted a scope other than agent time, so the sign-in was not kept.";
2451
+ }
2452
+ if (result.kind === "invalid_grant") {
2453
+ return "The server refused the sign-in code. Run timetrakkr login again.";
2454
+ }
2455
+ return `The sign-in could not be completed (${result.error}). Run timetrakkr login again.`;
2456
+ }
2457
+ async function welcomeLine(rt) {
2458
+ const credentials = readCredentials({
2459
+ ...rt.env,
2460
+ TIMETRAKKR_TOKEN: void 0,
2461
+ TIMETRAKKR_URL: void 0
2462
+ });
2463
+ if (!credentials.ok) {
2464
+ return "Signed in.";
2465
+ }
2466
+ const config = await getConfig(
2467
+ stateHome(rt.env),
2468
+ credentials.credentials,
2469
+ rt.now(),
2470
+ AbortSignal.timeout(COMMAND_NETWORK_MS)
2471
+ );
2472
+ if (!config.ok) {
2473
+ return "Signed in.";
2474
+ }
2475
+ const workspace = oneLine(config.workspaceName, 80) || "your workspace";
2476
+ return config.enabled ? `Signed in to ${workspace}. Agent time is on for that workspace.` : `Signed in to ${workspace}. Agent time is not switched on for that workspace, so nothing is tracked yet.`;
2477
+ }
2478
+ async function signIn(rt, options, onLink = () => void 0) {
2479
+ const home = stateHome(rt.env);
2480
+ const flagName = options.url ? "--url" : "TIMETRAKKR_URL";
2481
+ const workspace = parseWorkspaceUrl(workspaceAddress(rt.env, home, options.url), flagName);
2482
+ if (!workspace.ok) {
2483
+ rt.err(`${workspace.reason}.`);
2484
+ return 1;
2485
+ }
2486
+ const { origin } = workspace;
2487
+ const setup = AbortSignal.timeout(COMMAND_NETWORK_MS);
2488
+ const discovered = await discover(origin, setup);
2489
+ if (!discovered.ok) {
2490
+ rt.err(`Could not start sign-in with ${origin}: ${discovered.error}.`);
2491
+ return 1;
2492
+ }
2493
+ const { endpoints } = discovered;
2494
+ const listener = await startLoginListener();
2495
+ try {
2496
+ const clientName = `Time Trakkr CLI on ${(0, import_node_os2.hostname)()}`.slice(0, MAX_CLIENT_NAME);
2497
+ const client = await registerClient(endpoints.register, clientName, listener.redirectUri, setup);
2498
+ if (!client.ok) {
2499
+ rt.err(`Could not start sign-in with ${origin}: ${client.error}.`);
2500
+ return 1;
2501
+ }
2502
+ const state = (0, import_node_crypto5.randomBytes)(24).toString("base64url");
2503
+ const codeVerifier = (0, import_node_crypto5.randomBytes)(48).toString("base64url");
2504
+ const url = authorizeUrl(endpoints, client.clientId, listener.redirectUri, state, codeVerifier);
2505
+ onLink(url);
2506
+ rt.out(
2507
+ options.browser ? "Opening your browser to sign in. If it does not open, go to:" : "Open this link in a browser to sign in:"
2508
+ );
2509
+ rt.out(url);
2510
+ if (options.browser) {
2511
+ rt.openBrowser(url);
2512
+ }
2513
+ const exchange = { failure: null };
2514
+ const outcome = await listener.wait(
2515
+ state,
2516
+ options.timeoutMs,
2517
+ async (code) => {
2518
+ const result = await exchangeCode(
2519
+ endpoints.token,
2520
+ { code, clientId: client.clientId, redirectUri: listener.redirectUri, codeVerifier },
2521
+ AbortSignal.timeout(COMMAND_NETWORK_MS)
2522
+ );
2523
+ if (!result.ok) {
2524
+ exchange.failure = describeTokenFailure(result);
2525
+ return "failed";
2526
+ }
2527
+ writeAuth(home, {
2528
+ version: 1,
2529
+ baseUrl: origin,
2530
+ clientId: client.clientId,
2531
+ accessToken: result.pair.accessToken,
2532
+ refreshToken: result.pair.refreshToken,
2533
+ expiresAt: rt.now() + result.pair.expiresIn * 1e3,
2534
+ tokenEndpoint: endpoints.token,
2535
+ revocationEndpoint: endpoints.revoke
2536
+ });
2537
+ return "signed-in";
2538
+ }
2539
+ );
2540
+ if (outcome.kind === "timeout") {
2541
+ rt.err(
2542
+ "Sign-in was not completed in time. If the browser showed an error, or said agent time is not switched on for the workspace, that is why. Run timetrakkr login to try again."
2543
+ );
2544
+ return 1;
2545
+ }
2546
+ if (outcome.kind === "denied") {
2547
+ rt.err("Sign-in was declined in the browser. Nothing was connected.");
2548
+ return 1;
2549
+ }
2550
+ if (exchange.failure !== null) {
2551
+ rt.err(exchange.failure);
2552
+ return 1;
2553
+ }
2554
+ setDeclined(home, false);
2555
+ if (rt.env.TIMETRAKKR_TOKEN?.trim()) {
2556
+ rt.out("Note: TIMETRAKKR_TOKEN is set and takes precedence over this sign-in.");
2557
+ }
2558
+ rt.out(await welcomeLine(rt));
2559
+ return 0;
2560
+ } finally {
2561
+ await listener.close();
2562
+ }
2563
+ }
2564
+ async function childSignIn(rt, options) {
2565
+ const home = stateHome(rt.env);
2566
+ const release = takeLoginLock(home, rt.now());
2567
+ if (!release) {
2568
+ return 0;
2569
+ }
2570
+ const errors = [];
2571
+ let linked = false;
2572
+ try {
2573
+ const code = await signIn(quiet(rt, errors), options, (url) => {
2574
+ linked = true;
2575
+ writeLoginLink(home, { url });
2576
+ });
2577
+ if (!linked && code !== 0) {
2578
+ writeLoginLink(home, { error: errors[0] ?? "the sign-in stopped" });
2579
+ }
2580
+ return code;
2581
+ } finally {
2582
+ release();
2583
+ }
2584
+ }
2585
+ function printLink(rt, url) {
2586
+ rt.out("Opening your browser to sign in. If it does not open, go to:");
2587
+ rt.out(url);
2588
+ rt.out("Come back here once you have allowed it. Tracking starts at your next prompt.");
2589
+ }
2590
+ async function detachedSignIn(rt, url) {
2591
+ const home = stateHome(rt.env);
2592
+ const waiting = loginInProgress(home, rt.now()) ? readLoginLink(home) : null;
2593
+ if (waiting && "url" in waiting) {
2594
+ rt.openBrowser(waiting.url);
2595
+ printLink(rt, waiting.url);
2596
+ return 0;
2597
+ }
2598
+ clearLoginLink(home);
2599
+ rt.background(["login", "--child", ...url ? ["--url", url] : []]);
2600
+ const link = await waitForLoginLink(home, rt.now());
2601
+ if (!link || "error" in link) {
2602
+ const why = link ? ` (${oneLine(link.error)})` : "";
2603
+ rt.err(`The sign-in could not be started${why}. Run "timetrakkr login" in a terminal to retry.`);
2604
+ return 1;
2605
+ }
2606
+ printLink(rt, link.url);
2607
+ return 0;
2608
+ }
2609
+ async function login(rt, args) {
2610
+ const options = parseOptions(args);
2611
+ if (!options.ok) {
2612
+ rt.err(options.error);
2613
+ return 1;
2614
+ }
2615
+ if (options.mode === "detach") {
2616
+ return detachedSignIn(rt, options.url);
2617
+ }
2618
+ return options.mode === "child" ? childSignIn(rt, options) : signIn(rt, options);
2619
+ }
2620
+ async function logout(rt) {
2621
+ const home = stateHome(rt.env);
2622
+ setDeclined(home, true);
2623
+ const auth = readAuth(home);
2624
+ const environmentNote = rt.env.TIMETRAKKR_TOKEN?.trim() ? " TIMETRAKKR_TOKEN is still set in the environment and will keep being used." : "";
2625
+ if (!auth) {
2626
+ rt.out(`Not signed in.${environmentNote}`);
2627
+ return 0;
2628
+ }
2629
+ const revoked = isSignedOut(auth) ? null : await revoke(
2630
+ auth.revocationEndpoint,
2631
+ auth.clientId,
2632
+ auth.refreshToken,
2633
+ AbortSignal.timeout(COMMAND_NETWORK_MS)
2634
+ );
2635
+ deleteAuth(home);
2636
+ if (revoked === null) {
2637
+ rt.out(`Signed out on this machine.${environmentNote}`);
2638
+ } else if (revoked.ok) {
2639
+ rt.out(`Signed out. The sign-in was revoked on the server.${environmentNote}`);
2640
+ } else {
2641
+ rt.out(
2642
+ `Signed out on this machine. Revoking it on the server could not be confirmed (${revoked.error}); you can remove it under Developer \u2192 Connected apps.${environmentNote}`
2643
+ );
2644
+ }
2645
+ return 0;
2646
+ }
2647
+
2648
+ // packages/agent-cli/src/renew.ts
2649
+ var import_node_crypto6 = require("node:crypto");
2650
+ var import_node_fs10 = require("node:fs");
2651
+ var sleep2 = (ms) => new Promise((done) => setTimeout(done, ms));
2652
+ function tryTakeLock(path, owner) {
2653
+ try {
2654
+ (0, import_node_fs10.writeFileSync)(path, owner, { flag: "wx", mode: 384 });
2655
+ return true;
2656
+ } catch {
2657
+ return false;
2658
+ }
2659
+ }
2660
+ function clearStaleLock(path) {
2661
+ try {
2662
+ if (Date.now() - (0, import_node_fs10.statSync)(path).mtimeMs > LOCK_STALE_MS) {
2663
+ (0, import_node_fs10.rmSync)(path, { force: true });
2664
+ }
2665
+ } catch {
2666
+ }
2667
+ }
2668
+ async function takeLock(home, signal) {
2669
+ ensurePrivateDir(home);
2670
+ const path = authLockPath(home);
2671
+ const owner = `${process.pid}-${(0, import_node_crypto6.randomBytes)(8).toString("hex")}`;
2672
+ while (!signal.aborted) {
2673
+ if (tryTakeLock(path, owner)) {
2674
+ return owner;
2675
+ }
2676
+ clearStaleLock(path);
2677
+ await sleep2(LOCK_POLL_MS);
2678
+ }
2679
+ return null;
2680
+ }
2681
+ function releaseLock(home, owner) {
2682
+ const path = authLockPath(home);
2683
+ try {
2684
+ if ((0, import_node_fs10.readFileSync)(path, "utf8") === owner) {
2685
+ (0, import_node_fs10.rmSync)(path, { force: true });
2686
+ }
2687
+ } catch {
2688
+ }
2689
+ }
2690
+ async function renewSignIn(home, now, rejected, signal) {
2691
+ const owner = await takeLock(home, signal).catch(() => null);
2692
+ if (owner === null) {
2693
+ return;
2694
+ }
2695
+ try {
2696
+ const auth = readAuth(home);
2697
+ if (!auth || isSignedOut(auth)) {
2698
+ return;
2699
+ }
2700
+ const due = auth.expiresAt - now <= RENEW_MARGIN_MS;
2701
+ const refused = rejected !== null && fingerprint(auth.accessToken) === rejected;
2702
+ if (!due && !refused) {
2703
+ return;
2704
+ }
2705
+ const result = await refreshPair(auth.tokenEndpoint, auth.clientId, auth.refreshToken, signal);
2706
+ if (result.ok) {
2707
+ writeAuth(home, {
2708
+ ...auth,
2709
+ accessToken: result.pair.accessToken,
2710
+ refreshToken: result.pair.refreshToken,
2711
+ expiresAt: now + result.pair.expiresIn * 1e3
2712
+ });
2713
+ return;
2714
+ }
2715
+ const latest = readAuth(home);
2716
+ const stillOurs = latest !== null && !isSignedOut(latest) && latest.refreshToken === auth.refreshToken;
2717
+ if (result.kind === "invalid_grant" && stillOurs) {
2718
+ writeAuth(home, { version: 1, baseUrl: auth.baseUrl, signedOut: true });
2719
+ }
2720
+ } catch {
2721
+ } finally {
2722
+ releaseLock(home, owner);
2723
+ }
2724
+ }
2725
+
2726
+ // packages/agent-cli/src/version.ts
2727
+ var CLI_VERSION = true ? "1.0.2" : "development";
2728
+
2729
+ // packages/agent-cli/src/main.ts
2730
+ var USAGE = [
2731
+ "Usage: timetrakkr <command>",
2732
+ "",
2733
+ " login [--url <address>] [--no-browser] [--detach]",
2734
+ " Sign in through your browser, picking the workspace there.",
2735
+ " --url is only for staging or a custom domain, and is",
2736
+ " remembered. --no-browser only prints the link, to open on",
2737
+ " this machine yourself. --detach returns once the link is",
2738
+ " known and waits for the browser in the background, for use",
2739
+ " from inside an agent session. On a machine with no browser",
2740
+ " at all, set TIMETRAKKR_TOKEN instead.",
2741
+ " logout Revoke the sign-in and delete it from this machine. Sessions",
2742
+ " stop opening the browser to sign in until you sign in again.",
2743
+ " status What this directory is tracked to, and whether sign-in works",
2744
+ " off Stop tracking the current agent session",
2745
+ " on Resume tracking the current agent session",
2746
+ ' estimate --hours <n> --label "<text>"',
2747
+ " Record how long the agent said a piece of work would take a",
2748
+ " person or a team. Sent with the session for comparison; never",
2749
+ " billed. --days and --weeks also work and can be combined:",
2750
+ ` a day is ${HOURS_PER_DAY} hours and a week is ${DAYS_PER_WEEK} days.`,
2751
+ " hook <host> <event> Called by an agent host; reads the hook payload on stdin",
2752
+ " --version Print the version"
2753
+ ].join("\n");
2754
+ async function runHook(rt, host, event) {
2755
+ if (!isHost(host) || !event) {
2756
+ return;
2757
+ }
2758
+ let timer;
2759
+ const budget = new Promise((done) => {
2760
+ timer = setTimeout(done, HOOK_BUDGET_MS);
2761
+ });
2762
+ const work = handleHook(rt, host, event).catch((error) => {
2763
+ logHookError(stateHome(rt.env), rt.now(), `hook ${host} ${event}`, error);
2764
+ });
2765
+ await Promise.race([work, budget]);
2766
+ clearTimeout(timer);
2767
+ }
2768
+ async function runRefresh(rt, args) {
2769
+ const rejected = args[0] === "--rejected" && args[1] ? args[1] : null;
2770
+ const signal = AbortSignal.timeout(RENEW_TIMEOUT_MS);
2771
+ await renewSignIn(stateHome(rt.env), rt.now(), rejected, signal);
2772
+ }
2773
+ async function run(argv, rt) {
2774
+ const [command, ...rest] = argv;
2775
+ if (command === "hook") {
2776
+ await runHook(rt, rest[0], rest[1]);
2777
+ return 0;
2778
+ }
2779
+ if (command === "refresh") {
2780
+ await runRefresh(rt, rest);
2781
+ return 0;
2782
+ }
2783
+ try {
2784
+ switch (command) {
2785
+ case "login":
2786
+ return await login(rt, rest);
2787
+ case "logout":
2788
+ return await logout(rt);
2789
+ case "status":
2790
+ return await printStatus(rt);
2791
+ case "off":
2792
+ return switchTracking(rt, true);
2793
+ case "on":
2794
+ return switchTracking(rt, false);
2795
+ case "estimate":
2796
+ return recordEstimate(rt, rest);
2797
+ case "version":
2798
+ case "--version":
2799
+ case "-v":
2800
+ rt.out(CLI_VERSION);
2801
+ return 0;
2802
+ case "help":
2803
+ case "--help":
2804
+ case "-h":
2805
+ rt.out(USAGE);
2806
+ return 0;
2807
+ default:
2808
+ rt.err(USAGE);
2809
+ return 1;
2810
+ }
2811
+ } catch (error) {
2812
+ rt.err(`timetrakkr: ${error instanceof Error ? error.message : "unexpected error"}`);
2813
+ return 1;
2814
+ }
2815
+ }
2816
+
2817
+ // packages/agent-cli/src/cli.ts
2818
+ function readStdin() {
2819
+ return new Promise((done) => {
2820
+ let data = "";
2821
+ const timer = setTimeout(() => done(data), STDIN_BUDGET_MS);
2822
+ const finish = () => {
2823
+ clearTimeout(timer);
2824
+ done(data);
2825
+ };
2826
+ process.stdin.setEncoding("utf8");
2827
+ process.stdin.on("data", (chunk) => {
2828
+ data += chunk;
2829
+ });
2830
+ process.stdin.on("end", finish);
2831
+ process.stdin.on("error", finish);
2832
+ });
2833
+ }
2834
+ function detached(command, args) {
2835
+ try {
2836
+ const child = (0, import_node_child_process2.spawn)(command, [...args], { detached: true, stdio: "ignore" });
2837
+ child.on("error", () => void 0);
2838
+ child.unref();
2839
+ } catch {
2840
+ }
2841
+ }
2842
+ function openBrowser(url) {
2843
+ const opener = browserCommand(process.platform, url);
2844
+ if (opener) {
2845
+ detached(opener.command, opener.args);
2846
+ }
2847
+ }
2848
+ var runtime = {
2849
+ env: process.env,
2850
+ cwd: process.cwd(),
2851
+ now: Date.now,
2852
+ stdin: readStdin,
2853
+ out: (text) => process.stdout.write(`${text}
2854
+ `),
2855
+ err: (text) => process.stderr.write(`${text}
2856
+ `),
2857
+ background: (args) => {
2858
+ const script = process.argv[1];
2859
+ if (script) {
2860
+ detached(process.execPath, [script, ...args]);
2861
+ }
2862
+ },
2863
+ openBrowser
2864
+ };
2865
+ var isHook = process.argv[2] === "hook";
2866
+ function exit(code) {
2867
+ process.stdout.write("", () => process.exit(code));
2868
+ }
2869
+ run(process.argv.slice(2), runtime).then(exit, () => exit(isHook ? 0 : 1));