@timetrakkr/cli 1.0.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/timetrakkr.cjs +898 -443
  2. package/package.json +1 -1
@@ -3,6 +3,7 @@
3
3
 
4
4
  // packages/agent-cli/src/cli.ts
5
5
  var import_node_child_process2 = require("node:child_process");
6
+ var import_promises = require("node:readline/promises");
6
7
 
7
8
  // packages/agent-cli/src/browser.ts
8
9
  var OPENABLE = /^https?:\/\/[A-Za-z0-9._~:/?&=%-]+$/;
@@ -76,173 +77,12 @@ function isHost(value) {
76
77
  return HOSTS.some((host) => host === value);
77
78
  }
78
79
 
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
- }
80
+ // packages/agent-cli/src/cloud-init.ts
81
+ var import_node_fs5 = require("node:fs");
82
+ var import_node_path4 = require("node:path");
119
83
 
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
- }
84
+ // packages/agent-cli/src/credentials.ts
85
+ var import_node_crypto2 = require("node:crypto");
246
86
 
247
87
  // packages/agent-cli/src/auth-store.ts
248
88
  var import_node_crypto = require("node:crypto");
@@ -250,49 +90,83 @@ var import_node_fs2 = require("node:fs");
250
90
 
251
91
  // packages/agent-cli/src/paths.ts
252
92
  var import_node_os = require("node:os");
253
- var import_node_path2 = require("node:path");
93
+ var import_node_path = require("node:path");
254
94
  var SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;
255
95
  function isSafeSessionId(id) {
256
96
  return typeof id === "string" && SAFE_ID.test(id);
257
97
  }
258
98
  function stateHome(env) {
259
- return env.TIMETRAKKR_HOME || (0, import_node_path2.join)((0, import_node_os.homedir)(), ".timetrakkr");
99
+ return env.TIMETRAKKR_HOME || (0, import_node_path.join)((0, import_node_os.homedir)(), ".timetrakkr");
260
100
  }
261
101
  function sessionsDir(home) {
262
- return (0, import_node_path2.join)(home, "sessions");
102
+ return (0, import_node_path.join)(home, "sessions");
263
103
  }
264
104
  function hostDir(home, host) {
265
- return (0, import_node_path2.join)(sessionsDir(home), host);
105
+ return (0, import_node_path.join)(sessionsDir(home), host);
266
106
  }
267
107
  function logPath(home, host, sessionId) {
268
- return (0, import_node_path2.join)(hostDir(home, host), `${sessionId}.jsonl`);
108
+ return (0, import_node_path.join)(hostDir(home, host), `${sessionId}.jsonl`);
269
109
  }
270
110
  function metaPath(home, host, sessionId) {
271
- return (0, import_node_path2.join)(hostDir(home, host), `${sessionId}.json`);
111
+ return (0, import_node_path.join)(hostDir(home, host), `${sessionId}.json`);
272
112
  }
273
113
  function tokensPath(home, host, sessionId) {
274
- return (0, import_node_path2.join)(hostDir(home, host), `${sessionId}.tokens`);
114
+ return (0, import_node_path.join)(hostDir(home, host), `${sessionId}.tokens`);
275
115
  }
276
116
  function authPath(home) {
277
- return (0, import_node_path2.join)(home, "auth.json");
117
+ return (0, import_node_path.join)(home, "auth.json");
278
118
  }
279
119
  function loginLockPath(home) {
280
- return (0, import_node_path2.join)(home, "login.lock");
120
+ return (0, import_node_path.join)(home, "login.lock");
281
121
  }
282
122
  function loginLinkPath(home) {
283
- return (0, import_node_path2.join)(home, "login-link.json");
123
+ return (0, import_node_path.join)(home, "login-link.json");
284
124
  }
285
125
  function signInPromptPath(home) {
286
- return (0, import_node_path2.join)(home, "sign-in-prompt.json");
126
+ return (0, import_node_path.join)(home, "sign-in-prompt.json");
287
127
  }
288
128
  function authLockPath(home) {
289
- return (0, import_node_path2.join)(home, "auth.lock");
129
+ return (0, import_node_path.join)(home, "auth.lock");
290
130
  }
291
131
  function configCachePath(home) {
292
- return (0, import_node_path2.join)(home, "config.json");
132
+ return (0, import_node_path.join)(home, "config.json");
293
133
  }
294
134
  function errorLogPath(home) {
295
- return (0, import_node_path2.join)(home, "hook.log");
135
+ return (0, import_node_path.join)(home, "hook.log");
136
+ }
137
+
138
+ // packages/agent-cli/src/private-fs.ts
139
+ var import_node_fs = require("node:fs");
140
+ var import_node_path2 = require("node:path");
141
+ var DIR_MODE = 448;
142
+ var FILE_MODE = 384;
143
+ function ensurePrivateDir(dir) {
144
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: DIR_MODE });
145
+ if (((0, import_node_fs.statSync)(dir).mode & 511) !== DIR_MODE) {
146
+ (0, import_node_fs.chmodSync)(dir, DIR_MODE);
147
+ }
148
+ }
149
+ function writePrivateJson(path, value) {
150
+ ensurePrivateDir((0, import_node_path2.dirname)(path));
151
+ const temporary = `${path}.${process.pid}.tmp`;
152
+ (0, import_node_fs.writeFileSync)(temporary, `${JSON.stringify(value)}
153
+ `, { mode: FILE_MODE });
154
+ (0, import_node_fs.renameSync)(temporary, path);
155
+ }
156
+ function appendPrivateLine(path, line) {
157
+ ensurePrivateDir((0, import_node_path2.dirname)(path));
158
+ (0, import_node_fs.appendFileSync)(path, `${line}
159
+ `, { mode: FILE_MODE });
160
+ }
161
+ function readJson(path) {
162
+ try {
163
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+ function isRecord(value) {
169
+ return typeof value === "object" && value !== null && !Array.isArray(value);
296
170
  }
297
171
 
298
172
  // packages/agent-cli/src/auth-store.ts
@@ -328,15 +202,17 @@ function fingerprint(token) {
328
202
  return (0, import_node_crypto.createHash)("sha256").update(token).digest("hex").slice(0, 16);
329
203
  }
330
204
 
331
- // packages/agent-cli/src/credentials.ts
332
- var import_node_crypto2 = require("node:crypto");
333
-
334
205
  // packages/agent-cli/src/address.ts
335
206
  var DEFAULT_WORKSPACE_URL = "https://timetrakkr.com";
336
207
  function workspaceAddress(env, home, flag) {
337
208
  return flag ?? (env.TIMETRAKKR_URL?.trim() || readAuth(home)?.baseUrl || DEFAULT_WORKSPACE_URL);
338
209
  }
339
210
 
211
+ // packages/agent-cli/src/cloud.ts
212
+ function inClaudeCloud(env) {
213
+ return env.CLAUDE_CODE_REMOTE === "true";
214
+ }
215
+
340
216
  // packages/agent-cli/src/credentials.ts
341
217
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
342
218
  function parseWorkspaceUrl(raw, name) {
@@ -378,6 +254,22 @@ ${token}`)
378
254
  }
379
255
  };
380
256
  }
257
+ function fromCloudProxy(rawUrl) {
258
+ const url = parseWorkspaceUrl(rawUrl ?? DEFAULT_WORKSPACE_URL, "TIMETRAKKR_URL");
259
+ if (!url.ok) {
260
+ return url;
261
+ }
262
+ return {
263
+ ok: true,
264
+ credentials: {
265
+ baseUrl: url.baseUrl,
266
+ token: "",
267
+ source: "cloud-proxy",
268
+ cacheId: digest(`${url.baseUrl}
269
+ cloud-proxy`)
270
+ }
271
+ };
272
+ }
381
273
  function fromSignIn(rawUrl, home) {
382
274
  const auth = readAuth(home);
383
275
  const wanted = rawUrl ? parseWorkspaceUrl(rawUrl, "TIMETRAKKR_URL") : null;
@@ -420,128 +312,271 @@ function readCredentials(env) {
420
312
  const home = stateHome(env);
421
313
  const token = env.TIMETRAKKR_TOKEN?.trim();
422
314
  const rawUrl = env.TIMETRAKKR_URL?.trim() || void 0;
423
- return token ? fromEnvironment(token, rawUrl, home) : fromSignIn(rawUrl, home);
315
+ if (token) {
316
+ return fromEnvironment(token, rawUrl, home);
317
+ }
318
+ return inClaudeCloud(env) ? fromCloudProxy(rawUrl) : fromSignIn(rawUrl, home);
424
319
  }
425
320
 
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
- }
321
+ // packages/agent-cli/src/hooks-file.ts
322
+ var import_node_fs3 = require("node:fs");
323
+ function parseHooksFile(text) {
324
+ let value;
325
+ try {
326
+ value = JSON.parse(text);
327
+ } catch {
328
+ return { why: "it is not valid JSON" };
441
329
  }
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;
330
+ if (!isRecord(value)) {
331
+ return { why: "it is not a JSON object" };
332
+ }
333
+ const hooks = value.hooks ?? {};
334
+ if (!isRecord(hooks)) {
335
+ return { why: '"hooks" is not an object' };
336
+ }
337
+ for (const [event, entries] of Object.entries(hooks)) {
338
+ if (!Array.isArray(entries)) {
339
+ return { why: `"hooks.${event}" is not a list` };
450
340
  }
451
- const stillValid = expiresAt > now;
452
- await waitForRenewal(token, null, stillValid ? EARLY_RENEW_WAIT_MS : Number.POSITIVE_INFINITY);
453
- return readCredentials(rt.env);
454
341
  }
455
- async function renew(rejected) {
456
- if (rejected.source !== "sign-in") {
457
- return null;
342
+ return { file: { ...value, hooks } };
343
+ }
344
+ function writeHooksFile(path, file) {
345
+ const temporary = `${path}.timetrakkr-tmp`;
346
+ (0, import_node_fs3.writeFileSync)(temporary, `${JSON.stringify(file, null, 2)}
347
+ `);
348
+ (0, import_node_fs3.renameSync)(temporary, path);
349
+ }
350
+ function withoutEntries(file, isOurs2) {
351
+ const hooks = {};
352
+ for (const [event, entries] of Object.entries(file.hooks)) {
353
+ const kept = entries.filter((entry) => !isOurs2(entry));
354
+ if (kept.length > 0 || entries.length === 0) {
355
+ hooks[event] = kept;
458
356
  }
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
357
  }
467
- return { fresh, renew };
358
+ return { ...file, hooks };
468
359
  }
469
360
 
470
- // packages/agent-cli/src/config-cache.ts
471
- var import_node_fs3 = require("node:fs");
472
- function cacheKey(credentials) {
473
- return credentials.cacheId;
361
+ // packages/agent-cli/src/mapping.ts
362
+ var import_node_fs4 = require("node:fs");
363
+ var import_node_path3 = require("node:path");
364
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
365
+ function findRepositoryRoot(cwd) {
366
+ let dir = cwd;
367
+ for (; ; ) {
368
+ if ((0, import_node_fs4.existsSync)((0, import_node_path3.join)(dir, ".git"))) {
369
+ return dir;
370
+ }
371
+ const parent = (0, import_node_path3.dirname)(dir);
372
+ if (parent === dir) {
373
+ return null;
374
+ }
375
+ dir = parent;
376
+ }
474
377
  }
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)) {
378
+ function readMappingText(path) {
379
+ try {
380
+ if ((0, import_node_fs4.statSync)(path).size > MAPPING_MAX_BYTES) {
381
+ return null;
382
+ }
383
+ return (0, import_node_fs4.readFileSync)(path, "utf8");
384
+ } catch {
478
385
  return null;
479
386
  }
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
387
  }
492
- function peekConfig(home, credentials, now) {
493
- const cached = readCache(home, cacheKey(credentials));
494
- if (!cached) {
388
+ function parseMapping(text) {
389
+ let value;
390
+ try {
391
+ value = JSON.parse(text);
392
+ } catch {
393
+ return { reason: `${MAPPING_FILE} is not valid JSON` };
394
+ }
395
+ if (!isRecord(value)) {
396
+ return { reason: `${MAPPING_FILE} must be an object with projectId and taskId` };
397
+ }
398
+ const { projectId, taskId } = value;
399
+ if (typeof projectId !== "string" || !UUID.test(projectId)) {
400
+ return { reason: `${MAPPING_FILE}: projectId must be a UUID` };
401
+ }
402
+ if (typeof taskId !== "string" || !UUID.test(taskId)) {
403
+ return { reason: `${MAPPING_FILE}: taskId must be a UUID` };
404
+ }
405
+ return { mapping: { projectId: projectId.toLowerCase(), taskId: taskId.toLowerCase() } };
406
+ }
407
+ function readMapping(root) {
408
+ const path = (0, import_node_path3.join)(root, MAPPING_FILE);
409
+ if (!(0, import_node_fs4.existsSync)(path)) {
410
+ return { missing: true };
411
+ }
412
+ const text = readMappingText(path);
413
+ return text === null ? { reason: `${MAPPING_FILE} could not be read` } : parseMapping(text);
414
+ }
415
+
416
+ // packages/agent-cli/src/version.ts
417
+ var CLI_VERSION = true ? "1.2.0" : "development";
418
+
419
+ // packages/agent-cli/src/cloud-init.ts
420
+ var MARK = "timetrakkr-cloud";
421
+ var GUARD = '[ "$CLAUDE_CODE_REMOTE" = "true" ] || exit 0';
422
+ var TIMEOUT_SECONDS = 60;
423
+ var RELEASE = /^\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$/;
424
+ var SHELL_SAFE_URL = /^https?:\/\/[A-Za-z0-9.:[\]-]+(\/[A-Za-z0-9._~/-]*)?$/;
425
+ var EVENTS = [
426
+ { event: "SessionStart" },
427
+ { event: "UserPromptSubmit" },
428
+ { event: "PreToolUse" },
429
+ { event: "PostToolUse" },
430
+ { event: "Notification", matcher: "permission_prompt" },
431
+ { event: "Stop" },
432
+ { event: "SessionEnd" }
433
+ ];
434
+ var USAGE = "Usage: timetrakkr init --cloud [--pin <version>] [--url <address>] [--yes] [--undo]";
435
+ function hookCommand(event, { pin, url }) {
436
+ const address = url ? `TIMETRAKKR_URL=${url} ` : "";
437
+ return `${GUARD}; NODE_USE_ENV_PROXY=1 ${address}npx -y @timetrakkr/cli@${pin} hook claude-code ${event} || true # ${MARK}`;
438
+ }
439
+ var isOurs = (group) => isRecord(group) && Array.isArray(group.hooks) && group.hooks.some(
440
+ (hook) => isRecord(hook) && typeof hook.command === "string" && hook.command.trim().endsWith(`# ${MARK}`)
441
+ );
442
+ function withOurs(file, choices) {
443
+ const base = withoutEntries(file, isOurs);
444
+ const hooks = { ...base.hooks };
445
+ for (const { event, matcher } of EVENTS) {
446
+ const hook = {
447
+ type: "command",
448
+ command: hookCommand(event, choices),
449
+ async: true,
450
+ timeout: TIMEOUT_SECONDS
451
+ };
452
+ hooks[event] = [...hooks[event] ?? [], { ...matcher ? { matcher } : {}, hooks: [hook] }];
453
+ }
454
+ return { ...base, hooks };
455
+ }
456
+ function option(args, name) {
457
+ const at = args.indexOf(name);
458
+ return at === -1 ? void 0 : args[at + 1];
459
+ }
460
+ function readChoices(rt, args) {
461
+ const pin = option(args, "--pin") ?? CLI_VERSION;
462
+ if (!RELEASE.test(pin)) {
463
+ rt.err(
464
+ `timetrakkr: "${pin}" is not a release to pin. Pass --pin with a version such as 1.2.0. Nothing was changed.`
465
+ );
495
466
  return null;
496
467
  }
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;
468
+ const raw = option(args, "--url");
469
+ if (raw === void 0) {
470
+ return { pin, url: null };
471
+ }
472
+ const url = parseWorkspaceUrl(raw, "--url");
473
+ if (!url.ok || !SHELL_SAFE_URL.test(url.baseUrl)) {
474
+ rt.err(
475
+ `timetrakkr: ${url.ok ? "--url has characters a hook cannot use" : url.reason}. Nothing was changed.`
476
+ );
477
+ return null;
478
+ }
479
+ return { pin, url: url.baseUrl };
506
480
  }
507
- function lastKnownMappings(home, credentials) {
508
- return readCache(home, cacheKey(credentials))?.mappings ?? null;
481
+ function readSettings(path) {
482
+ if (!(0, import_node_fs5.existsSync)(path)) {
483
+ return { kind: "missing" };
484
+ }
485
+ const parsed = parseHooksFile((0, import_node_fs5.readFileSync)(path, "utf8"));
486
+ return "why" in parsed ? { kind: "invalid", why: parsed.why } : { kind: "ok", file: parsed.file };
509
487
  }
510
- function lastKnownMemberId(home, credentials) {
511
- return readCache(home, cacheKey(credentials))?.memberId ?? null;
488
+ async function confirmed(rt, args, path) {
489
+ if (args.includes("--yes")) {
490
+ return true;
491
+ }
492
+ const answer = await rt.ask(`Write them to ${path}? [y/N] `);
493
+ if (answer === null) {
494
+ rt.err("timetrakkr: nobody to ask. Run this again with --yes to write them.");
495
+ return false;
496
+ }
497
+ return /^y(es)?$/i.test(answer.trim());
512
498
  }
513
- function forgetConfig(home) {
514
- (0, import_node_fs3.rmSync)(configCachePath(home), { force: true });
499
+ function nextSteps(choices) {
500
+ const host = choices.url ? new URL(choices.url).host : "your workspace\u2019s address";
501
+ return [
502
+ "Next:",
503
+ " 1. Commit .claude/settings.json.",
504
+ " 2. In Claude Code on the web, edit the environment this repository uses and add a",
505
+ ` network secret for ${host}: header Authorization, prefix Bearer, and as the value`,
506
+ " a personal access token with only the Agent time scope.",
507
+ "The token stays outside the session: the proxy adds it to requests for that host."
508
+ ];
515
509
  }
516
- async function getConfig(home, credentials, now, signal, renew = null) {
517
- const cached = peekConfig(home, credentials, now);
518
- if (cached) {
519
- return cached;
510
+ async function setUp(rt, args, path) {
511
+ const choices = readChoices(rt, args);
512
+ if (!choices) {
513
+ return 1;
520
514
  }
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
- }
515
+ const settings = readSettings(path);
516
+ if (settings.kind === "invalid") {
517
+ rt.err(
518
+ `timetrakkr: ${path} cannot be read safely: ${settings.why}. Fix it, then run this again. Nothing was changed.`
519
+ );
520
+ return 1;
527
521
  }
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);
522
+ const file = settings.kind === "ok" ? settings.file : { hooks: {} };
523
+ const next = withOurs(file, choices);
524
+ if (settings.kind === "ok" && JSON.stringify(next) === JSON.stringify(file)) {
525
+ rt.out(`Claude Code cloud sessions are already set up in ${path}.`);
526
+ return 0;
539
527
  }
540
- return fresh;
528
+ rt.out(
529
+ [
530
+ "Claude Code cloud sessions do not load plugins, but they run the hooks in the",
531
+ "repository\u2019s .claude/settings.json. This adds one hook for each event the plugin uses.",
532
+ "Each exits at once unless it is running in a cloud session, so nothing is counted twice",
533
+ `on a laptop. Each runs @timetrakkr/cli@${choices.pin} in the background, for example:`,
534
+ "",
535
+ ` ${hookCommand("Stop", choices)}`,
536
+ "",
537
+ `They go in ${path}, next to anything already there.`
538
+ ].join("\n")
539
+ );
540
+ if (!await confirmed(rt, args, path)) {
541
+ rt.out("Nothing was changed.");
542
+ return 1;
543
+ }
544
+ (0, import_node_fs5.mkdirSync)((0, import_node_path4.join)(path, ".."), { recursive: true });
545
+ writeHooksFile(path, next);
546
+ rt.out(
547
+ ["Written.", ...nextSteps(choices), 'Undo with "timetrakkr init --cloud --undo".'].join("\n")
548
+ );
549
+ return 0;
550
+ }
551
+ function undo(rt, path) {
552
+ const settings = readSettings(path);
553
+ if (settings.kind === "invalid") {
554
+ rt.err(`timetrakkr: ${path} cannot be read safely: ${settings.why}. Nothing was changed.`);
555
+ return 1;
556
+ }
557
+ if (settings.kind === "missing" || !Object.values(settings.file.hooks).flat().some(isOurs)) {
558
+ rt.out(`Nothing to undo: there are no Time Trakkr hooks in ${path}.`);
559
+ return 0;
560
+ }
561
+ writeHooksFile(path, withoutEntries(settings.file, isOurs));
562
+ rt.out(`Removed the Time Trakkr hooks from ${path}.`);
563
+ return 0;
564
+ }
565
+ async function initCommand(rt, args) {
566
+ if (!args.includes("--cloud")) {
567
+ rt.err(USAGE);
568
+ return 1;
569
+ }
570
+ const root = findRepositoryRoot(rt.cwd);
571
+ if (!root) {
572
+ rt.err(
573
+ "timetrakkr: run this inside a git repository: cloud sessions run hooks from the repository. Nothing was changed."
574
+ );
575
+ return 1;
576
+ }
577
+ const path = (0, import_node_path4.join)(root, ".claude", "settings.json");
578
+ return args.includes("--undo") ? undo(rt, path) : setUp(rt, args, path);
541
579
  }
542
-
543
- // packages/agent-cli/src/context.ts
544
- var import_node_crypto3 = require("node:crypto");
545
580
 
546
581
  // packages/core/src/agent-time/turn-clock.ts
547
582
  var KIND_ORDER = {
@@ -646,73 +681,340 @@ function normalizeRemote(remote) {
646
681
  if (!parts) {
647
682
  return null;
648
683
  }
649
- const [host, rawPath] = parts;
650
- const path = rawPath.replace(/\/+$/, "").replace(/\.git$/i, "").replace(/^\/+/, "");
651
- if (!path.includes("/") || /\s/.test(path) || !host.includes(".")) {
684
+ const [host, rawPath] = parts;
685
+ const path = rawPath.replace(/\/+$/, "").replace(/\.git$/i, "").replace(/^\/+/, "");
686
+ if (!path.includes("/") || /\s/.test(path) || !host.includes(".")) {
687
+ return null;
688
+ }
689
+ return `${host}/${path}`.toLowerCase();
690
+ }
691
+ function branchMatches(pattern, branch) {
692
+ const parts = pattern.split("*");
693
+ const first = parts[0] ?? "";
694
+ if (parts.length === 1) {
695
+ return branch === first;
696
+ }
697
+ const last = parts[parts.length - 1] ?? "";
698
+ if (branch.length < first.length + last.length || !branch.startsWith(first) || !branch.endsWith(last)) {
699
+ return false;
700
+ }
701
+ const end = branch.length - last.length;
702
+ let from = first.length;
703
+ for (const part of parts.slice(1, -1)) {
704
+ const at = branch.indexOf(part, from);
705
+ if (at === -1 || at + part.length > end) {
706
+ return false;
707
+ }
708
+ from = at + part.length;
709
+ }
710
+ return true;
711
+ }
712
+ function specificity(pattern) {
713
+ return pattern.replace(/\*/g, "").length;
714
+ }
715
+ function resolveMapping(mappings, remote, branch) {
716
+ const wanted = remote === null ? null : normalizeRemote(remote);
717
+ if (wanted === null) {
718
+ return null;
719
+ }
720
+ const forRepository = mappings.filter((m) => normalizeRemote(m.remote) === wanted);
721
+ const byBranch = forRepository.filter(
722
+ (m) => m.branchPattern !== null && branch !== null && branchMatches(m.branchPattern, branch)
723
+ ).sort((a, b) => specificity(b.branchPattern) - specificity(a.branchPattern));
724
+ return byBranch[0] ?? forRepository.find((m) => m.branchPattern === null) ?? null;
725
+ }
726
+
727
+ // packages/core/src/agent-time/model-prices.ts
728
+ var usd = (model, input, output, cacheRead, cacheWrite) => ({ model, currency: "USD", input, output, cacheRead, cacheWrite });
729
+ var DEFAULT_MODEL_PRICES = [
730
+ usd("claude-fable-5-1", "10", "50", "0.25", "12.50"),
731
+ usd("claude-opus-5-5", "4", "20", "0.20", "5"),
732
+ usd("claude-opus-5", "5", "25", "0.50", "6.25"),
733
+ usd("claude-opus-4-8", "5", "25", "0.50", "6.25"),
734
+ usd("claude-opus-4-7", "5", "25", "0.50", "6.25"),
735
+ usd("claude-opus-4-6", "5", "25", "0.50", "6.25"),
736
+ usd("claude-sonnet-5-5", "2", "10", "0.20", "2.50"),
737
+ usd("claude-sonnet-5", "2", "10", "0.20", "2.50"),
738
+ usd("claude-sonnet-4-6", "3", "15", "0.30", "3.75"),
739
+ usd("claude-sonnet-4-5", "3", "15", "0.30", "3.75"),
740
+ usd("claude-haiku-4-5", "1", "5", "0.10", "1.25"),
741
+ usd("gpt-6-astra", "10", "50", "1.00", "12.50"),
742
+ usd("gpt-6.1-sol", "2", "10", "0.10", "2.50"),
743
+ usd("gpt-6-luna", "0.10", "0.50", "0.01", "0.125"),
744
+ usd("gpt-5.6-sol", "4", "20", "0.40", "5.00"),
745
+ usd("gpt-5.3-codex", "1.75", "14", "0.175", null)
746
+ ];
747
+
748
+ // packages/core/src/agent-time/untrusted-text.ts
749
+ var FORMAT = new RegExp("\\p{Cf}", "gu");
750
+ var CONTROL = new RegExp("\\p{Cc}", "gu");
751
+
752
+ // packages/core/src/agent-time/check-in-clock.ts
753
+ var CHECK_IN_BOUNDS = {
754
+ /** A longer silence between check-ins counts 0 and closes the turn. */
755
+ gapSeconds: 900,
756
+ /** No turn counts for longer than this. */
757
+ turnCapSeconds: 14400,
758
+ /** Check-in sessions one person may have open at once. */
759
+ maxOpenSessions: 20
760
+ };
761
+ var GAP_MS = CHECK_IN_BOUNDS.gapSeconds * 1e3;
762
+ var TURN_CAP_MS = CHECK_IN_BOUNDS.turnCapSeconds * 1e3;
763
+
764
+ // packages/agent-cli/src/text.ts
765
+ var MAX_LINE = 200;
766
+ function oneLine(text, max = MAX_LINE) {
767
+ const flat = text.replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
768
+ return flat.slice(0, max);
769
+ }
770
+
771
+ // packages/agent-cli/src/api.ts
772
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 408, 425, 429]);
773
+ var MAX_ERROR_LENGTH = 200;
774
+ var MEMBER_ID = /^[0-9a-f-]{36}$/i;
775
+ async function request(credentials, method, path, signal, body) {
776
+ return fetch(`${credentials.baseUrl}${path}`, {
777
+ method,
778
+ signal,
779
+ // A redirect would hand the token to wherever it points.
780
+ redirect: "error",
781
+ headers: {
782
+ // The proxy's own header would otherwise compete with an empty one.
783
+ ...credentials.source === "cloud-proxy" ? {} : { authorization: `Bearer ${credentials.token}` },
784
+ accept: "application/json",
785
+ ...body === void 0 ? {} : { "content-type": "application/json" }
786
+ },
787
+ body: body === void 0 ? void 0 : JSON.stringify(body)
788
+ });
789
+ }
790
+ async function readJsonBody(response) {
791
+ try {
792
+ return await response.json();
793
+ } catch {
794
+ return null;
795
+ }
796
+ }
797
+ function failureText(status, body) {
798
+ const message = isRecord(body) && typeof body.message === "string" ? body.message : "";
799
+ const text = message ? `HTTP ${status}: ${message}` : `HTTP ${status}`;
800
+ return text.replace(/\s+/g, " ").slice(0, MAX_ERROR_LENGTH);
801
+ }
802
+ async function describeFailure(response) {
803
+ return failureText(response.status, await readJsonBody(response));
804
+ }
805
+ function disabledReason(body) {
806
+ const reason = isRecord(body) ? body.disabled_reason : null;
807
+ const message = isRecord(reason) && typeof reason.message === "string" ? reason.message : "";
808
+ return oneLine(message) || null;
809
+ }
810
+ function describeThrown(error) {
811
+ if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
812
+ return "timed out";
813
+ }
814
+ return "no connection";
815
+ }
816
+ function parseBlocked(value) {
817
+ const { id, remote_sha256: remoteSha256, branch_pattern: branchPattern } = value;
818
+ if (typeof id !== "string" || typeof remoteSha256 !== "string" || !/^[0-9a-f]{64}$/.test(remoteSha256) || branchPattern !== null && typeof branchPattern !== "string") {
819
+ return null;
820
+ }
821
+ return { id, blocked: true, branchPattern, remoteSha256 };
822
+ }
823
+ function parseMapping2(value) {
824
+ if (!isRecord(value)) {
825
+ return null;
826
+ }
827
+ if (value.blocked === true) {
828
+ return parseBlocked(value);
829
+ }
830
+ const {
831
+ id,
832
+ remote,
833
+ branch_pattern: branchPattern,
834
+ project_id: projectId,
835
+ task_id: taskId
836
+ } = value;
837
+ if (typeof id !== "string" || typeof remote !== "string" || typeof projectId !== "string" || typeof taskId !== "string" || branchPattern !== null && typeof branchPattern !== "string") {
838
+ return null;
839
+ }
840
+ const name = (key) => typeof value[key] === "string" ? oneLine(String(value[key])) : "";
841
+ return {
842
+ id,
843
+ remote,
844
+ branchPattern,
845
+ projectId,
846
+ taskId,
847
+ projectName: name("project_name"),
848
+ clientName: name("client_name") || null,
849
+ taskName: name("task_name")
850
+ };
851
+ }
852
+ function parseMappings(value) {
853
+ return Array.isArray(value) ? value.map(parseMapping2).filter((rule) => rule !== null) : [];
854
+ }
855
+ async function fetchConfig(credentials, signal) {
856
+ try {
857
+ const response = await request(credentials, "GET", "/api/v1/agent/config", signal);
858
+ if (!response.ok) {
859
+ return { ok: false, error: await describeFailure(response), status: response.status };
860
+ }
861
+ const body = await readJsonBody(response);
862
+ if (!isRecord(body) || typeof body.enabled !== "boolean" || !isRecord(body.workspace)) {
863
+ return { ok: false, error: "unexpected response", status: response.status };
864
+ }
865
+ const name = typeof body.workspace.name === "string" ? body.workspace.name : "";
866
+ const memberId = isRecord(body.member) ? body.member.id : null;
867
+ return {
868
+ ok: true,
869
+ enabled: body.enabled,
870
+ workspaceName: name,
871
+ memberId: typeof memberId === "string" && MEMBER_ID.test(memberId) ? memberId : null,
872
+ // Only meaningful while off.
873
+ offReason: body.enabled ? null : disabledReason(body),
874
+ mappings: parseMappings(body.mappings)
875
+ };
876
+ } catch (error) {
877
+ return { ok: false, error: describeThrown(error) };
878
+ }
879
+ }
880
+ async function putSessionDay(credentials, body, signal) {
881
+ try {
882
+ const response = await request(credentials, "PUT", "/api/v1/agent/session_days", signal, body);
883
+ if (response.ok) {
884
+ return { kind: "ok" };
885
+ }
886
+ const answer = await readJsonBody(response);
887
+ const off = response.status === 403 ? disabledReason(answer) : null;
888
+ if (off) {
889
+ return { kind: "off", reason: off };
890
+ }
891
+ const error = failureText(response.status, answer);
892
+ const retry = response.status >= 500 || RETRYABLE_STATUSES.has(response.status);
893
+ return retry ? { kind: "retry", error, status: response.status } : { kind: "rejected", status: response.status, error };
894
+ } catch (error) {
895
+ return { kind: "retry", error: describeThrown(error) };
896
+ }
897
+ }
898
+
899
+ // packages/agent-cli/src/auth-session.ts
900
+ var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
901
+ function authSession(rt, home, now, signal) {
902
+ let attempted = false;
903
+ async function waitForRenewal(oldToken, rejected, maxWaitMs) {
904
+ attempted = true;
905
+ rt.background(rejected === null ? ["refresh"] : ["refresh", "--rejected", rejected]);
906
+ const giveUpAt = Date.now() + maxWaitMs;
907
+ while (!signal.aborted && Date.now() < giveUpAt) {
908
+ const auth = readAuth(home);
909
+ if (!auth || isSignedOut(auth) || auth.accessToken !== oldToken) {
910
+ return;
911
+ }
912
+ await sleep(RENEW_POLL_MS);
913
+ }
914
+ }
915
+ async function fresh() {
916
+ const current = readCredentials(rt.env);
917
+ if (!current.ok || current.credentials.source !== "sign-in") {
918
+ return current;
919
+ }
920
+ const { expiresAt, token } = current.credentials;
921
+ if (expiresAt === void 0 || expiresAt - now > RENEW_MARGIN_MS) {
922
+ return current;
923
+ }
924
+ const stillValid = expiresAt > now;
925
+ await waitForRenewal(token, null, stillValid ? EARLY_RENEW_WAIT_MS : Number.POSITIVE_INFINITY);
926
+ return readCredentials(rt.env);
927
+ }
928
+ async function renew(rejected) {
929
+ if (rejected.source !== "sign-in") {
930
+ return null;
931
+ }
932
+ const usable = (result) => result.ok && result.credentials.token !== rejected.token ? result.credentials : null;
933
+ const already = usable(readCredentials(rt.env));
934
+ if (already || attempted) {
935
+ return already;
936
+ }
937
+ await waitForRenewal(rejected.token, fingerprint(rejected.token), Number.POSITIVE_INFINITY);
938
+ return usable(readCredentials(rt.env));
939
+ }
940
+ return { fresh, renew };
941
+ }
942
+
943
+ // packages/agent-cli/src/config-cache.ts
944
+ var import_node_fs6 = require("node:fs");
945
+ function cacheKey(credentials) {
946
+ return credentials.cacheId;
947
+ }
948
+ function readCache(home, key) {
949
+ const value = readJson(configCachePath(home));
950
+ if (!isRecord(value) || value.key !== key || typeof value.fetchedAt !== "number" || typeof value.enabled !== "boolean" || typeof value.workspaceName !== "string" || !Array.isArray(value.mappings)) {
951
+ return null;
952
+ }
953
+ return {
954
+ key,
955
+ fetchedAt: value.fetchedAt,
956
+ enabled: value.enabled,
957
+ workspaceName: value.workspaceName,
958
+ // Absent from an answer remembered by a version that did not keep it.
959
+ memberId: typeof value.memberId === "string" ? value.memberId : null,
960
+ offReason: typeof value.offReason === "string" ? value.offReason : null,
961
+ // Written by this program from validated rules, in an owner-only file.
962
+ mappings: value.mappings
963
+ };
964
+ }
965
+ function peekConfig(home, credentials, now) {
966
+ const cached = readCache(home, cacheKey(credentials));
967
+ if (!cached) {
652
968
  return null;
653
969
  }
654
- return `${host}/${path}`.toLowerCase();
970
+ const age = now - cached.fetchedAt;
971
+ return age >= 0 && age < CONFIG_TTL_MS ? {
972
+ ok: true,
973
+ enabled: cached.enabled,
974
+ workspaceName: cached.workspaceName,
975
+ memberId: cached.memberId,
976
+ offReason: cached.offReason,
977
+ mappings: cached.mappings
978
+ } : null;
655
979
  }
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;
980
+ function lastKnownMappings(home, credentials) {
981
+ return readCache(home, cacheKey(credentials))?.mappings ?? null;
982
+ }
983
+ function lastKnownMemberId(home, credentials) {
984
+ return readCache(home, cacheKey(credentials))?.memberId ?? null;
985
+ }
986
+ function forgetConfig(home) {
987
+ (0, import_node_fs6.rmSync)(configCachePath(home), { force: true });
988
+ }
989
+ async function getConfig(home, credentials, now, signal, renew = null) {
990
+ const cached = peekConfig(home, credentials, now);
991
+ if (cached) {
992
+ return cached;
665
993
  }
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;
994
+ let fresh = await fetchConfig(credentials, signal);
995
+ if (!fresh.ok && fresh.status === 401 && renew) {
996
+ const renewed = await renew(credentials);
997
+ if (renewed) {
998
+ fresh = await fetchConfig(renewed, signal);
672
999
  }
673
- from = at + part.length;
674
1000
  }
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;
1001
+ if (fresh.ok) {
1002
+ const entry = {
1003
+ key: cacheKey(credentials),
1004
+ fetchedAt: now,
1005
+ enabled: fresh.enabled,
1006
+ workspaceName: fresh.workspaceName,
1007
+ memberId: fresh.memberId,
1008
+ offReason: fresh.offReason,
1009
+ mappings: fresh.mappings
1010
+ };
1011
+ writePrivateJson(configCachePath(home), entry);
684
1012
  }
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;
1013
+ return fresh;
690
1014
  }
691
1015
 
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");
1016
+ // packages/agent-cli/src/context.ts
1017
+ var import_node_crypto3 = require("node:crypto");
716
1018
 
717
1019
  // packages/agent-cli/src/git.ts
718
1020
  var import_node_child_process = require("node:child_process");
@@ -742,61 +1044,6 @@ function currentBranch(root, env) {
742
1044
  return branch !== null && branch.length <= MAX_BRANCH_LENGTH ? branch : null;
743
1045
  }
744
1046
 
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
1047
  // packages/agent-cli/src/context.ts
801
1048
  var NOT_MAPPED = "this repository is not mapped to a project";
802
1049
  var BLOCKED = "this branch is mapped to a project you cannot log time to";
@@ -908,6 +1155,145 @@ function resolveContext(local, rules, credentials) {
908
1155
  };
909
1156
  }
910
1157
 
1158
+ // packages/agent-cli/src/cursor-setup.ts
1159
+ var import_node_fs7 = require("node:fs");
1160
+ var import_node_os2 = require("node:os");
1161
+ var import_node_path5 = require("node:path");
1162
+ var GATE_MARK = "timetrakkr-cursor-full-turns";
1163
+ var CURSOR_GATE_COMMAND = `node -e 0 ${GATE_MARK}`;
1164
+ var CURSOR_GATE_EVENTS = ["beforeSubmitPrompt", "stop"];
1165
+ var BACKUP_SUFFIX = ".timetrakkr-backup";
1166
+ function cursorDir(env) {
1167
+ return (0, import_node_path5.join)(env.HOME || (0, import_node_os2.homedir)(), ".cursor");
1168
+ }
1169
+ var isGate = (entry) => isRecord(entry) && typeof entry.command === "string" && entry.command.trim().split(/\s+/).at(-1) === GATE_MARK;
1170
+ function readCursorHooks(env) {
1171
+ const dir = cursorDir(env);
1172
+ if (!(0, import_node_fs7.existsSync)(dir)) {
1173
+ return { kind: "no-cursor" };
1174
+ }
1175
+ const path = (0, import_node_path5.join)(dir, "hooks.json");
1176
+ let text;
1177
+ try {
1178
+ text = (0, import_node_fs7.readFileSync)(path, "utf8");
1179
+ } catch {
1180
+ return { kind: "missing", path };
1181
+ }
1182
+ const parsed = parseHooksFile(text);
1183
+ if ("why" in parsed) {
1184
+ return { kind: "invalid", path, why: parsed.why };
1185
+ }
1186
+ const missing = CURSOR_GATE_EVENTS.filter(
1187
+ (event) => !(parsed.file.hooks[event] ?? []).some(isGate)
1188
+ );
1189
+ return { kind: "ok", path, file: parsed.file, missing };
1190
+ }
1191
+ function added(file, events) {
1192
+ const hooks = { ...file.hooks };
1193
+ for (const event of events) {
1194
+ hooks[event] = [...hooks[event] ?? [], { command: CURSOR_GATE_COMMAND }];
1195
+ }
1196
+ return { ...file, version: file.version ?? 1, hooks };
1197
+ }
1198
+ function removed(file) {
1199
+ return withoutEntries(file, isGate);
1200
+ }
1201
+ function preview(events) {
1202
+ return events.map((event) => ` "${event}": [{ "command": "${CURSOR_GATE_COMMAND}" }]`);
1203
+ }
1204
+ function refuse(rt, path, why) {
1205
+ rt.err(
1206
+ `timetrakkr: ${path} cannot be read safely: ${why}. Fix or move it, then run this again. Nothing was changed.`
1207
+ );
1208
+ return 1;
1209
+ }
1210
+ async function confirmed2(rt, args, path) {
1211
+ if (args.includes("--yes")) {
1212
+ return true;
1213
+ }
1214
+ const answer = await rt.ask(`Add them to ${path}? [y/N] `);
1215
+ if (answer === null) {
1216
+ rt.err('timetrakkr: nobody to ask. Run "timetrakkr cursor setup --yes" to add them.');
1217
+ return false;
1218
+ }
1219
+ return /^y(es)?$/i.test(answer.trim());
1220
+ }
1221
+ async function setUp2(rt, args, hooks) {
1222
+ if (hooks.kind === "invalid") {
1223
+ return refuse(rt, hooks.path, hooks.why);
1224
+ }
1225
+ if (hooks.kind === "no-cursor") {
1226
+ rt.err("timetrakkr: Cursor is not installed here (no ~/.cursor). Nothing was changed.");
1227
+ return 1;
1228
+ }
1229
+ const file = hooks.kind === "ok" ? hooks.file : { version: 1, hooks: {} };
1230
+ const missing = hooks.kind === "ok" ? hooks.missing : [...CURSOR_GATE_EVENTS];
1231
+ if (missing.length === 0) {
1232
+ rt.out(`Cursor full-turn hooks are already set up in ${hooks.path}.`);
1233
+ return 0;
1234
+ }
1235
+ rt.out(
1236
+ [
1237
+ "The Cursor CLI runs the Time Trakkr plugin\u2019s prompt and stop hooks only when your own",
1238
+ "hooks.json defines those events too. Without them, a turn counts from its first tool call",
1239
+ "to its last. These entries do nothing themselves; they let Cursor run the plugin\u2019s hooks:",
1240
+ "",
1241
+ ...preview(missing),
1242
+ "",
1243
+ `They go in ${hooks.path}, next to anything already there.`
1244
+ ].join("\n")
1245
+ );
1246
+ if (!await confirmed2(rt, args, hooks.path)) {
1247
+ rt.out("Nothing was changed.");
1248
+ return 1;
1249
+ }
1250
+ if (hooks.kind === "ok") {
1251
+ (0, import_node_fs7.copyFileSync)(hooks.path, `${hooks.path}${BACKUP_SUFFIX}`);
1252
+ }
1253
+ writeHooksFile(hooks.path, added(file, missing));
1254
+ rt.out(
1255
+ hooks.kind === "ok" ? `Added. The file as it was is at ${hooks.path}${BACKUP_SUFFIX}. Undo with "timetrakkr cursor setup --undo".` : `Added. Undo with "timetrakkr cursor setup --undo".`
1256
+ );
1257
+ return 0;
1258
+ }
1259
+ function undo2(rt, hooks) {
1260
+ if (hooks.kind === "invalid") {
1261
+ return refuse(rt, hooks.path, hooks.why);
1262
+ }
1263
+ if (hooks.kind !== "ok" || hooks.missing.length === CURSOR_GATE_EVENTS.length) {
1264
+ rt.out("Nothing to undo: there are no Time Trakkr entries in ~/.cursor/hooks.json.");
1265
+ return 0;
1266
+ }
1267
+ writeHooksFile(hooks.path, removed(hooks.file));
1268
+ rt.out(`Removed the Time Trakkr entries from ${hooks.path}.`);
1269
+ return 0;
1270
+ }
1271
+ async function cursorCommand(rt, args) {
1272
+ if (args[0] !== "setup") {
1273
+ rt.err("Usage: timetrakkr cursor setup [--yes] [--undo]");
1274
+ return 1;
1275
+ }
1276
+ const hooks = readCursorHooks(rt.env);
1277
+ return args.includes("--undo") ? undo2(rt, hooks) : setUp2(rt, args.slice(1), hooks);
1278
+ }
1279
+ function cursorStatusLine(env) {
1280
+ const hooks = readCursorHooks(env);
1281
+ switch (hooks.kind) {
1282
+ case "no-cursor":
1283
+ return null;
1284
+ case "invalid":
1285
+ return `Cursor: ${hooks.path} cannot be read (${hooks.why})`;
1286
+ case "ok":
1287
+ if (hooks.missing.length === 0) {
1288
+ return "Cursor: whole turns counted (timetrakkr cursor setup is done)";
1289
+ }
1290
+ break;
1291
+ default:
1292
+ break;
1293
+ }
1294
+ return 'Cursor: only the time from the first to the last tool call of each turn is counted in the CLI; run "timetrakkr cursor setup" to count whole turns';
1295
+ }
1296
+
911
1297
  // packages/agent-cli/src/day.ts
912
1298
  var pad = (n) => String(n).padStart(2, "0");
913
1299
  function localDay(at) {
@@ -916,7 +1302,7 @@ function localDay(at) {
916
1302
  }
917
1303
 
918
1304
  // packages/agent-cli/src/drop.ts
919
- var import_node_fs7 = require("node:fs");
1305
+ var import_node_fs10 = require("node:fs");
920
1306
 
921
1307
  // packages/agent-cli/src/identity.ts
922
1308
  var import_node_crypto4 = require("node:crypto");
@@ -943,7 +1329,7 @@ function ownedBy(session, identities) {
943
1329
  }
944
1330
 
945
1331
  // packages/agent-cli/src/store.ts
946
- var import_node_fs5 = require("node:fs");
1332
+ var import_node_fs8 = require("node:fs");
947
1333
  var EVENT_KINDS = ["turn_start", "activity", "wait", "turn_end"];
948
1334
  function parseEvent(line) {
949
1335
  try {
@@ -966,7 +1352,7 @@ function appendEvent(home, host, sessionId, event) {
966
1352
  function readLog(home, host, sessionId) {
967
1353
  let raw;
968
1354
  try {
969
- raw = (0, import_node_fs5.readFileSync)(logPath(home, host, sessionId));
1355
+ raw = (0, import_node_fs8.readFileSync)(logPath(home, host, sessionId));
970
1356
  } catch {
971
1357
  return { events: [], bytes: 0 };
972
1358
  }
@@ -975,7 +1361,7 @@ function readLog(home, host, sessionId) {
975
1361
  }
976
1362
  function logSize(home, host, sessionId) {
977
1363
  try {
978
- return (0, import_node_fs5.statSync)(logPath(home, host, sessionId)).size;
1364
+ return (0, import_node_fs8.statSync)(logPath(home, host, sessionId)).size;
979
1365
  } catch {
980
1366
  return 0;
981
1367
  }
@@ -992,7 +1378,7 @@ function writeMeta(home, meta) {
992
1378
  }
993
1379
  function sessionIdsOf(home, host) {
994
1380
  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);
1381
+ return (0, import_node_fs8.readdirSync)(hostDir(home, host)).filter((name) => name.endsWith(".json")).map((name) => name.slice(0, -".json".length)).filter(isSafeSessionId);
996
1382
  } catch {
997
1383
  return [];
998
1384
  }
@@ -1005,15 +1391,15 @@ function listMetas(home) {
1005
1391
  function pruneSessions(home, before) {
1006
1392
  for (const meta of listMetas(home)) {
1007
1393
  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 });
1394
+ (0, import_node_fs8.rmSync)(metaPath(home, meta.host, meta.sessionId), { force: true });
1395
+ (0, import_node_fs8.rmSync)(logPath(home, meta.host, meta.sessionId), { force: true });
1396
+ (0, import_node_fs8.rmSync)(tokensPath(home, meta.host, meta.sessionId), { force: true });
1011
1397
  }
1012
1398
  }
1013
1399
  }
1014
1400
 
1015
1401
  // packages/agent-cli/src/hosts/transcript.ts
1016
- var import_node_fs6 = require("node:fs");
1402
+ var import_node_fs9 = require("node:fs");
1017
1403
  var NEWLINE = 10;
1018
1404
  function countOf(value, optional) {
1019
1405
  if (value === void 0 && optional) {
@@ -1022,17 +1408,17 @@ function countOf(value, optional) {
1022
1408
  return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
1023
1409
  }
1024
1410
  function readWholeLines(path, offset) {
1025
- const stat = (0, import_node_fs6.statSync)(path);
1411
+ const stat = (0, import_node_fs9.statSync)(path);
1026
1412
  if (!stat.isFile() || stat.size < offset) {
1027
1413
  return null;
1028
1414
  }
1029
1415
  const length = Math.min(stat.size - offset, TRANSCRIPT_READ_BYTES);
1030
1416
  const buffer = Buffer.alloc(length);
1031
- const fd = (0, import_node_fs6.openSync)(path, "r");
1417
+ const fd = (0, import_node_fs9.openSync)(path, "r");
1032
1418
  try {
1033
- (0, import_node_fs6.readSync)(fd, buffer, 0, length, offset);
1419
+ (0, import_node_fs9.readSync)(fd, buffer, 0, length, offset);
1034
1420
  } finally {
1035
- (0, import_node_fs6.closeSync)(fd);
1421
+ (0, import_node_fs9.closeSync)(fd);
1036
1422
  }
1037
1423
  const end = buffer.lastIndexOf(NEWLINE);
1038
1424
  if (end === -1) {
@@ -1279,7 +1665,7 @@ function dropPending(home, meta) {
1279
1665
  return;
1280
1666
  }
1281
1667
  if (size > meta.syncedBytes) {
1282
- (0, import_node_fs7.truncateSync)(logPath(home, host, sessionId), meta.syncedBytes);
1668
+ (0, import_node_fs10.truncateSync)(logPath(home, host, sessionId), meta.syncedBytes);
1283
1669
  markTokensUnreliable(home, host, sessionId);
1284
1670
  }
1285
1671
  const last = readLog(home, host, sessionId).events.at(-1);
@@ -1319,7 +1705,7 @@ function toPosted(summary, item) {
1319
1705
  function alreadyPosted(posted, next) {
1320
1706
  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
1707
  }
1322
- function toBody(item, summary, withEstimate) {
1708
+ function toBody(item, summary, withEstimate, cloud) {
1323
1709
  const { meta, mapping } = item;
1324
1710
  const tokens = tokensForDay(item.tokens, summary.day);
1325
1711
  return {
@@ -1340,7 +1726,9 @@ function toBody(item, summary, withEstimate) {
1340
1726
  mapping_id: mapping.mappingId ?? null,
1341
1727
  // Left out, not zeroed, when the transcript could not be read.
1342
1728
  ...tokens ? { tokens } : {},
1343
- ...withEstimate && item.estimate ? { estimated_effort: { seconds: item.estimate.seconds, label: item.estimate.label } } : {}
1729
+ ...withEstimate && item.estimate ? { estimated_effort: { seconds: item.estimate.seconds, label: item.estimate.label } } : {},
1730
+ // Left out on a laptop, so a server that predates the field is unaffected.
1731
+ ...cloud ? { runtime: "cloud" } : {}
1344
1732
  };
1345
1733
  }
1346
1734
  function needsAttention(home, meta, now, isOwn) {
@@ -1420,7 +1808,7 @@ function settle(home, work) {
1420
1808
  }
1421
1809
  }
1422
1810
  }
1423
- async function postSession(home, credentials, item, signal, renew) {
1811
+ async function postSession(home, credentials, item, signal, renew, cloud) {
1424
1812
  let current = credentials;
1425
1813
  const posted = {};
1426
1814
  let failure = null;
@@ -1428,7 +1816,7 @@ async function postSession(home, credentials, item, signal, renew) {
1428
1816
  let sentEstimate = null;
1429
1817
  for (const summary of item.pending) {
1430
1818
  const withEstimate = summary === item.pending.at(-1);
1431
- const body = toBody(item, summary, withEstimate);
1819
+ const body = toBody(item, summary, withEstimate, cloud);
1432
1820
  let result = await putSessionDay(current, body, signal);
1433
1821
  if (result.kind === "retry" && result.status === 401 && renew) {
1434
1822
  const renewed = await renew(current);
@@ -1494,7 +1882,14 @@ async function flush(options) {
1494
1882
  holdBack(home, [item], "timed out");
1495
1883
  continue;
1496
1884
  }
1497
- const posted = await postSession(home, current, item, signal, options.renew ?? null);
1885
+ const posted = await postSession(
1886
+ home,
1887
+ current,
1888
+ item,
1889
+ signal,
1890
+ options.renew ?? null,
1891
+ inClaudeCloud(env)
1892
+ );
1498
1893
  if (posted.off) {
1499
1894
  forgetConfig(home);
1500
1895
  dropAllPending(home, identities);
@@ -1507,7 +1902,7 @@ async function flush(options) {
1507
1902
 
1508
1903
  // packages/agent-cli/src/hosts/claude-code.ts
1509
1904
  function mapClaudeCodeHook(event, payload) {
1510
- if (!isRecord(payload)) {
1905
+ if (!isRecord(payload) || "cursor_version" in payload) {
1511
1906
  return null;
1512
1907
  }
1513
1908
  const { session_id: sessionId, cwd } = payload;
@@ -1597,6 +1992,7 @@ function mapCodexHook(event, payload) {
1597
1992
  }
1598
1993
 
1599
1994
  // packages/agent-cli/src/hosts/cursor.ts
1995
+ var AUTO_MODEL = "default";
1600
1996
  function directoryOf(payload) {
1601
1997
  if (typeof payload.cwd === "string" && payload.cwd.length > 0) {
1602
1998
  return payload.cwd;
@@ -1614,13 +2010,15 @@ function mapCursorHook(event, payload) {
1614
2010
  if (!isSafeSessionId(sessionId) || cwd === null) {
1615
2011
  return null;
1616
2012
  }
1617
- const model = modelOf(payload.model);
2013
+ const model = payload.model === AUTO_MODEL ? null : modelOf(payload.model);
2014
+ const turnId = isSafeSessionId(payload.generation_id) ? payload.generation_id : void 0;
1618
2015
  const signal = (kind, durationMs) => ({
1619
2016
  type: "event",
1620
2017
  sessionId,
1621
2018
  cwd,
1622
2019
  event: durationMs === void 0 ? { kind } : { kind, durationMs },
1623
- ...model ? { model } : {}
2020
+ ...model ? { model } : {},
2021
+ ...turnId ? { turnId } : {}
1624
2022
  });
1625
2023
  switch (event) {
1626
2024
  case "sessionStart":
@@ -1642,7 +2040,7 @@ function mapCursorHook(event, payload) {
1642
2040
  }
1643
2041
 
1644
2042
  // packages/agent-cli/src/login-background.ts
1645
- var import_node_fs8 = require("node:fs");
2043
+ var import_node_fs11 = require("node:fs");
1646
2044
  function lockAge(home, now) {
1647
2045
  const value = readJson(loginLockPath(home));
1648
2046
  return isRecord(value) && typeof value.at === "number" ? now - value.at : null;
@@ -1656,15 +2054,15 @@ function takeLoginLock(home, now) {
1656
2054
  const path = loginLockPath(home);
1657
2055
  for (let attempt = 0; attempt < 2; attempt += 1) {
1658
2056
  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 });
2057
+ const fd = (0, import_node_fs11.openSync)(path, "wx", 384);
2058
+ (0, import_node_fs11.writeSync)(fd, JSON.stringify({ at: now }));
2059
+ (0, import_node_fs11.closeSync)(fd);
2060
+ return () => (0, import_node_fs11.rmSync)(path, { force: true });
1663
2061
  } catch {
1664
2062
  if (loginInProgress(home, now)) {
1665
2063
  return null;
1666
2064
  }
1667
- (0, import_node_fs8.rmSync)(path, { force: true });
2065
+ (0, import_node_fs11.rmSync)(path, { force: true });
1668
2066
  }
1669
2067
  }
1670
2068
  return null;
@@ -1680,7 +2078,7 @@ function writeLoginLink(home, link) {
1680
2078
  writePrivateJson(loginLinkPath(home), link);
1681
2079
  }
1682
2080
  function clearLoginLink(home) {
1683
- (0, import_node_fs8.rmSync)(loginLinkPath(home), { force: true });
2081
+ (0, import_node_fs11.rmSync)(loginLinkPath(home), { force: true });
1684
2082
  }
1685
2083
  async function waitForLoginLink(home, now) {
1686
2084
  const deadline = Date.now() + LOGIN_LINK_WAIT_MS;
@@ -1870,6 +2268,7 @@ async function refresh(run2, existing) {
1870
2268
  identity: tracked && credentials.ok ? identitiesOf(home, credentials.credentials)[0] ?? null : base.identity ?? null,
1871
2269
  credentialId: tracked && credentials.ok ? credentials.credentials.cacheId : base.credentialId ?? null,
1872
2270
  model,
2271
+ turnId: (hookSignal.type === "event" ? hookSignal.turnId : void 0) ?? base.turnId ?? null,
1873
2272
  updatedAt: at
1874
2273
  };
1875
2274
  if (tracked || existing) {
@@ -1905,12 +2304,32 @@ function readTokens(run2, meta) {
1905
2304
  modelFrom(hookSignal) ?? meta.model
1906
2305
  );
1907
2306
  }
2307
+ function unopenedTurn(home, host, hookSignal) {
2308
+ if (hookSignal.type !== "event" || hookSignal.event.kind !== "activity" || hookSignal.turnId === void 0 || readMeta(home, host, hookSignal.sessionId)?.turnId === hookSignal.turnId) {
2309
+ return null;
2310
+ }
2311
+ return { ...hookSignal, event: { kind: "turn_start" } };
2312
+ }
2313
+ function isOpenAlready(home, host, hookSignal) {
2314
+ return hookSignal.type === "event" && hookSignal.event.kind === "turn_start" && hookSignal.turnId !== void 0 && readMeta(home, host, hookSignal.sessionId)?.turnId === hookSignal.turnId;
2315
+ }
1908
2316
  async function handleHook(rt, host, event, signal = AbortSignal.timeout(NETWORK_BUDGET_MS)) {
1909
2317
  const at = rt.now();
1910
2318
  const hookSignal = ADAPTERS[host](event, parsePayload(await rt.stdin()));
1911
2319
  if (!hookSignal) {
1912
2320
  return;
1913
2321
  }
2322
+ const home = stateHome(rt.env);
2323
+ if (isOpenAlready(home, host, hookSignal)) {
2324
+ return;
2325
+ }
2326
+ const opened = unopenedTurn(home, host, hookSignal);
2327
+ if (opened) {
2328
+ await handleSignal(rt, host, opened, at, signal);
2329
+ }
2330
+ await handleSignal(rt, host, hookSignal, at, signal);
2331
+ }
2332
+ async function handleSignal(rt, host, hookSignal, at, signal) {
1914
2333
  const home = stateHome(rt.env);
1915
2334
  const auth = authSession(rt, home, at, signal);
1916
2335
  const credentials = reachesServer(hookSignal) ? await auth.fresh() : readCredentials(rt.env);
@@ -1997,6 +2416,9 @@ function sourceNote(credentials, home) {
1997
2416
  if (credentials.source === "sign-in") {
1998
2417
  return "signed in with timetrakkr login";
1999
2418
  }
2419
+ if (credentials.source === "cloud-proxy") {
2420
+ return "a Claude Code cloud session's network secret, added outside the VM";
2421
+ }
2000
2422
  const stored = readAuth(home);
2001
2423
  return stored && !isSignedOut(stored) ? "TIMETRAKKR_TOKEN from the environment; the stored sign-in is not used" : "TIMETRAKKR_TOKEN from the environment";
2002
2424
  }
@@ -2037,6 +2459,7 @@ async function askConfig(credentials, renew, signal) {
2037
2459
  const renewed = await renew(credentials);
2038
2460
  return renewed ? fetchConfig(renewed, signal) : first;
2039
2461
  }
2462
+ var CLOUD_SHELL_HINT = "Cloud: in a cloud session, run this as the hooks do: NODE_USE_ENV_PROXY=1 TIMETRAKKR_URL=<your workspace address> npx -y @timetrakkr/cli status";
2040
2463
  async function printStatus(rt) {
2041
2464
  const home = stateHome(rt.env);
2042
2465
  const signal = AbortSignal.timeout(COMMAND_NETWORK_MS);
@@ -2048,7 +2471,8 @@ async function printStatus(rt) {
2048
2471
  const context = resolveContext(local, rules, credentials);
2049
2472
  const lines = [`Directory: ${rt.cwd}`];
2050
2473
  if (context.root) {
2051
- lines.push(`Repository: ${context.root}`);
2474
+ const remote = local.remote === null ? null : normalizeRemote(local.remote);
2475
+ lines.push(`Repository: ${remote ?? `${context.root} (no remote)`}`);
2052
2476
  }
2053
2477
  lines.push(...mappingLines(context), ...signInLines(credentials, config, home));
2054
2478
  const session = chooseSession(rt, home);
@@ -2059,6 +2483,13 @@ async function printStatus(rt) {
2059
2483
  lines.push(`Last error: ${oneLine(session.meta.lastError)}`);
2060
2484
  }
2061
2485
  }
2486
+ if (inClaudeCloud(rt.env) && config?.ok === false) {
2487
+ lines.push(CLOUD_SHELL_HINT);
2488
+ }
2489
+ const cursor = cursorStatusLine(rt.env);
2490
+ if (cursor) {
2491
+ lines.push(cursor);
2492
+ }
2062
2493
  for (const line of lines) {
2063
2494
  rt.out(line);
2064
2495
  }
@@ -2066,14 +2497,14 @@ async function printStatus(rt) {
2066
2497
  }
2067
2498
 
2068
2499
  // packages/agent-cli/src/error-log.ts
2069
- var import_node_fs9 = require("node:fs");
2500
+ var import_node_fs12 = require("node:fs");
2070
2501
  function logHookError(home, now, where, error) {
2071
2502
  try {
2072
2503
  const path = errorLogPath(home);
2073
2504
  const message = error instanceof Error ? error.message : "unexpected error";
2074
2505
  try {
2075
- if ((0, import_node_fs9.statSync)(path).size > ERROR_LOG_MAX_BYTES) {
2076
- (0, import_node_fs9.truncateSync)(path);
2506
+ if ((0, import_node_fs12.statSync)(path).size > ERROR_LOG_MAX_BYTES) {
2507
+ (0, import_node_fs12.truncateSync)(path);
2077
2508
  }
2078
2509
  } catch {
2079
2510
  }
@@ -2172,7 +2603,7 @@ function recordEstimate(rt, args) {
2172
2603
 
2173
2604
  // packages/agent-cli/src/login.ts
2174
2605
  var import_node_crypto5 = require("node:crypto");
2175
- var import_node_os2 = require("node:os");
2606
+ var import_node_os3 = require("node:os");
2176
2607
 
2177
2608
  // packages/agent-cli/src/login-listener.ts
2178
2609
  var import_node_http = require("node:http");
@@ -2493,7 +2924,7 @@ async function signIn(rt, options, onLink = () => void 0) {
2493
2924
  const { endpoints } = discovered;
2494
2925
  const listener = await startLoginListener();
2495
2926
  try {
2496
- const clientName = `Time Trakkr CLI on ${(0, import_node_os2.hostname)()}`.slice(0, MAX_CLIENT_NAME);
2927
+ const clientName = `Time Trakkr CLI on ${(0, import_node_os3.hostname)()}`.slice(0, MAX_CLIENT_NAME);
2497
2928
  const client = await registerClient(endpoints.register, clientName, listener.redirectUri, setup);
2498
2929
  if (!client.ok) {
2499
2930
  rt.err(`Could not start sign-in with ${origin}: ${client.error}.`);
@@ -2647,11 +3078,11 @@ async function logout(rt) {
2647
3078
 
2648
3079
  // packages/agent-cli/src/renew.ts
2649
3080
  var import_node_crypto6 = require("node:crypto");
2650
- var import_node_fs10 = require("node:fs");
3081
+ var import_node_fs13 = require("node:fs");
2651
3082
  var sleep2 = (ms) => new Promise((done) => setTimeout(done, ms));
2652
3083
  function tryTakeLock(path, owner) {
2653
3084
  try {
2654
- (0, import_node_fs10.writeFileSync)(path, owner, { flag: "wx", mode: 384 });
3085
+ (0, import_node_fs13.writeFileSync)(path, owner, { flag: "wx", mode: 384 });
2655
3086
  return true;
2656
3087
  } catch {
2657
3088
  return false;
@@ -2659,8 +3090,8 @@ function tryTakeLock(path, owner) {
2659
3090
  }
2660
3091
  function clearStaleLock(path) {
2661
3092
  try {
2662
- if (Date.now() - (0, import_node_fs10.statSync)(path).mtimeMs > LOCK_STALE_MS) {
2663
- (0, import_node_fs10.rmSync)(path, { force: true });
3093
+ if (Date.now() - (0, import_node_fs13.statSync)(path).mtimeMs > LOCK_STALE_MS) {
3094
+ (0, import_node_fs13.rmSync)(path, { force: true });
2664
3095
  }
2665
3096
  } catch {
2666
3097
  }
@@ -2681,8 +3112,8 @@ async function takeLock(home, signal) {
2681
3112
  function releaseLock(home, owner) {
2682
3113
  const path = authLockPath(home);
2683
3114
  try {
2684
- if ((0, import_node_fs10.readFileSync)(path, "utf8") === owner) {
2685
- (0, import_node_fs10.rmSync)(path, { force: true });
3115
+ if ((0, import_node_fs13.readFileSync)(path, "utf8") === owner) {
3116
+ (0, import_node_fs13.rmSync)(path, { force: true });
2686
3117
  }
2687
3118
  } catch {
2688
3119
  }
@@ -2723,11 +3154,8 @@ async function renewSignIn(home, now, rejected, signal) {
2723
3154
  }
2724
3155
  }
2725
3156
 
2726
- // packages/agent-cli/src/version.ts
2727
- var CLI_VERSION = true ? "1.0.2" : "development";
2728
-
2729
3157
  // packages/agent-cli/src/main.ts
2730
- var USAGE = [
3158
+ var USAGE2 = [
2731
3159
  "Usage: timetrakkr <command>",
2732
3160
  "",
2733
3161
  " login [--url <address>] [--no-browser] [--detach]",
@@ -2748,6 +3176,15 @@ var USAGE = [
2748
3176
  " person or a team. Sent with the session for comparison; never",
2749
3177
  " billed. --days and --weeks also work and can be combined:",
2750
3178
  ` a day is ${HOURS_PER_DAY} hours and a week is ${DAYS_PER_WEEK} days.`,
3179
+ " cursor setup [--yes] [--undo]",
3180
+ " Let the Cursor CLI report whole turns: adds two entries that do",
3181
+ " nothing themselves to ~/.cursor/hooks.json, after showing them",
3182
+ " and asking. --undo removes exactly those entries.",
3183
+ " init --cloud [--pin <version>] [--url <address>] [--yes] [--undo]",
3184
+ " Count Claude Code cloud sessions in this repository: writes",
3185
+ " hooks to .claude/settings.json that run only in a cloud",
3186
+ " session, after showing them and asking. Commit the file.",
3187
+ " --undo removes exactly those hooks.",
2751
3188
  " hook <host> <event> Called by an agent host; reads the hook payload on stdin",
2752
3189
  " --version Print the version"
2753
3190
  ].join("\n");
@@ -2794,6 +3231,10 @@ async function run(argv, rt) {
2794
3231
  return switchTracking(rt, false);
2795
3232
  case "estimate":
2796
3233
  return recordEstimate(rt, rest);
3234
+ case "cursor":
3235
+ return await cursorCommand(rt, rest);
3236
+ case "init":
3237
+ return await initCommand(rt, rest);
2797
3238
  case "version":
2798
3239
  case "--version":
2799
3240
  case "-v":
@@ -2802,10 +3243,10 @@ async function run(argv, rt) {
2802
3243
  case "help":
2803
3244
  case "--help":
2804
3245
  case "-h":
2805
- rt.out(USAGE);
3246
+ rt.out(USAGE2);
2806
3247
  return 0;
2807
3248
  default:
2808
- rt.err(USAGE);
3249
+ rt.err(USAGE2);
2809
3250
  return 1;
2810
3251
  }
2811
3252
  } catch (error) {
@@ -2845,6 +3286,19 @@ function openBrowser(url) {
2845
3286
  detached(opener.command, opener.args);
2846
3287
  }
2847
3288
  }
3289
+ async function ask(question) {
3290
+ if (!process.stdin.isTTY) {
3291
+ return null;
3292
+ }
3293
+ const prompt = (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout });
3294
+ try {
3295
+ return await prompt.question(question);
3296
+ } catch {
3297
+ return null;
3298
+ } finally {
3299
+ prompt.close();
3300
+ }
3301
+ }
2848
3302
  var runtime = {
2849
3303
  env: process.env,
2850
3304
  cwd: process.cwd(),
@@ -2860,7 +3314,8 @@ var runtime = {
2860
3314
  detached(process.execPath, [script, ...args]);
2861
3315
  }
2862
3316
  },
2863
- openBrowser
3317
+ openBrowser,
3318
+ ask
2864
3319
  };
2865
3320
  var isHook = process.argv[2] === "hook";
2866
3321
  function exit(code) {