@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.
- package/dist/timetrakkr.cjs +898 -443
- package/package.json +1 -1
package/dist/timetrakkr.cjs
CHANGED
|
@@ -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/
|
|
80
|
-
var
|
|
81
|
-
var
|
|
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/
|
|
121
|
-
var
|
|
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
|
|
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,
|
|
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,
|
|
102
|
+
return (0, import_node_path.join)(home, "sessions");
|
|
263
103
|
}
|
|
264
104
|
function hostDir(home, host) {
|
|
265
|
-
return (0,
|
|
105
|
+
return (0, import_node_path.join)(sessionsDir(home), host);
|
|
266
106
|
}
|
|
267
107
|
function logPath(home, host, sessionId) {
|
|
268
|
-
return (0,
|
|
108
|
+
return (0, import_node_path.join)(hostDir(home, host), `${sessionId}.jsonl`);
|
|
269
109
|
}
|
|
270
110
|
function metaPath(home, host, sessionId) {
|
|
271
|
-
return (0,
|
|
111
|
+
return (0, import_node_path.join)(hostDir(home, host), `${sessionId}.json`);
|
|
272
112
|
}
|
|
273
113
|
function tokensPath(home, host, sessionId) {
|
|
274
|
-
return (0,
|
|
114
|
+
return (0, import_node_path.join)(hostDir(home, host), `${sessionId}.tokens`);
|
|
275
115
|
}
|
|
276
116
|
function authPath(home) {
|
|
277
|
-
return (0,
|
|
117
|
+
return (0, import_node_path.join)(home, "auth.json");
|
|
278
118
|
}
|
|
279
119
|
function loginLockPath(home) {
|
|
280
|
-
return (0,
|
|
120
|
+
return (0, import_node_path.join)(home, "login.lock");
|
|
281
121
|
}
|
|
282
122
|
function loginLinkPath(home) {
|
|
283
|
-
return (0,
|
|
123
|
+
return (0, import_node_path.join)(home, "login-link.json");
|
|
284
124
|
}
|
|
285
125
|
function signInPromptPath(home) {
|
|
286
|
-
return (0,
|
|
126
|
+
return (0, import_node_path.join)(home, "sign-in-prompt.json");
|
|
287
127
|
}
|
|
288
128
|
function authLockPath(home) {
|
|
289
|
-
return (0,
|
|
129
|
+
return (0, import_node_path.join)(home, "auth.lock");
|
|
290
130
|
}
|
|
291
131
|
function configCachePath(home) {
|
|
292
|
-
return (0,
|
|
132
|
+
return (0, import_node_path.join)(home, "config.json");
|
|
293
133
|
}
|
|
294
134
|
function errorLogPath(home) {
|
|
295
|
-
return (0,
|
|
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
|
-
|
|
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/
|
|
427
|
-
var
|
|
428
|
-
function
|
|
429
|
-
let
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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 {
|
|
358
|
+
return { ...file, hooks };
|
|
468
359
|
}
|
|
469
360
|
|
|
470
|
-
// packages/agent-cli/src/
|
|
471
|
-
var
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
476
|
-
|
|
477
|
-
|
|
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
|
|
493
|
-
|
|
494
|
-
|
|
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
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|
508
|
-
|
|
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
|
|
511
|
-
|
|
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
|
|
514
|
-
|
|
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
|
|
517
|
-
const
|
|
518
|
-
if (
|
|
519
|
-
return
|
|
510
|
+
async function setUp(rt, args, path) {
|
|
511
|
+
const choices = readChoices(rt, args);
|
|
512
|
+
if (!choices) {
|
|
513
|
+
return 1;
|
|
520
514
|
}
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
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/
|
|
693
|
-
var
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
1009
|
-
(0,
|
|
1010
|
-
(0,
|
|
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
|
|
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,
|
|
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,
|
|
1417
|
+
const fd = (0, import_node_fs9.openSync)(path, "r");
|
|
1032
1418
|
try {
|
|
1033
|
-
(0,
|
|
1419
|
+
(0, import_node_fs9.readSync)(fd, buffer, 0, length, offset);
|
|
1034
1420
|
} finally {
|
|
1035
|
-
(0,
|
|
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,
|
|
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(
|
|
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
|
|
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,
|
|
1660
|
-
(0,
|
|
1661
|
-
(0,
|
|
1662
|
-
return () => (0,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
2076
|
-
(0,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
2663
|
-
(0,
|
|
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,
|
|
2685
|
-
(0,
|
|
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
|
|
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(
|
|
3246
|
+
rt.out(USAGE2);
|
|
2806
3247
|
return 0;
|
|
2807
3248
|
default:
|
|
2808
|
-
rt.err(
|
|
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) {
|