codiedev 0.9.78 → 0.9.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,513 @@
1
+ "use strict";
2
+ // Browser sign-in for `codiedev connect`, and keeping its access token fresh.
3
+ //
4
+ // Sign-in is OAuth 2.1 authorization code + PKCE against the Codiedev
5
+ // backend with a loopback redirect (RFC 8252): the CLI listens on
6
+ // 127.0.0.1:<random port>, opens the portal's consent page, and exchanges the
7
+ // returned code for a 24h access token (cdv_at_) and a 30-day refresh token
8
+ // (cdv_rt_). No long-lived API key is saved.
9
+ //
10
+ // config.json keeps `token` = the current access token, so every existing
11
+ // reader keeps working. Each process entry point (codiedev, codiedev-hook,
12
+ // codie, the MCP server) calls ensureFreshToken() first; it refreshes when the
13
+ // token is about to expire.
14
+ //
15
+ // Refresh tokens are single-use and the server treats reuse of a spent one as
16
+ // theft (it revokes the whole sign-in). Many hooks run in parallel and share
17
+ // config.json, so a refresh goes:
18
+ // 1. read config, note the stale access token ("seen")
19
+ // 2. take ~/.codiedev/refresh.lock
20
+ // 3. re-read config; if the token is no longer "seen", another process
21
+ // already refreshed: use that, no network call
22
+ // 4. else refresh, atomically replace config.json, release
23
+ // specs/tla/CliSession.tla model-checks this: without step 2–3, two hooks can
24
+ // both present the same refresh token and sign the user out.
25
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ var desc = Object.getOwnPropertyDescriptor(m, k);
28
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
29
+ desc = { enumerable: true, get: function() { return m[k]; } };
30
+ }
31
+ Object.defineProperty(o, k2, desc);
32
+ }) : (function(o, m, k, k2) {
33
+ if (k2 === undefined) k2 = k;
34
+ o[k2] = m[k];
35
+ }));
36
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
37
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
38
+ }) : function(o, v) {
39
+ o["default"] = v;
40
+ });
41
+ var __importStar = (this && this.__importStar) || (function () {
42
+ var ownKeys = function(o) {
43
+ ownKeys = Object.getOwnPropertyNames || function (o) {
44
+ var ar = [];
45
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
46
+ return ar;
47
+ };
48
+ return ownKeys(o);
49
+ };
50
+ return function (mod) {
51
+ if (mod && mod.__esModule) return mod;
52
+ var result = {};
53
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
54
+ __setModuleDefault(result, mod);
55
+ return result;
56
+ };
57
+ })();
58
+ Object.defineProperty(exports, "__esModule", { value: true });
59
+ exports.DEVICE_CODE_GRANT = exports.LOCK_STALE_MS = exports.DEFAULT_MIN_VALIDITY_MS = void 0;
60
+ exports.readConfigAt = readConfigAt;
61
+ exports.renameWithRetry = renameWithRetry;
62
+ exports.writeConfigAtomic = writeConfigAtomic;
63
+ exports.ensureFreshToken = ensureFreshToken;
64
+ exports.pkcePair = pkcePair;
65
+ exports.openBrowser = openBrowser;
66
+ exports.parseCallback = parseCallback;
67
+ exports.browserLogin = browserLogin;
68
+ exports.isHeadless = isHeadless;
69
+ exports.deviceLogin = deviceLogin;
70
+ exports.logout = logout;
71
+ const crypto = __importStar(require("crypto"));
72
+ const fs = __importStar(require("fs"));
73
+ const http = __importStar(require("http"));
74
+ const os = __importStar(require("os"));
75
+ const path = __importStar(require("path"));
76
+ const child_process_1 = require("child_process");
77
+ /** Default: refresh when less than 10 minutes remain. */
78
+ exports.DEFAULT_MIN_VALIDITY_MS = 10 * 60_000;
79
+ /** A lock older than this was left by a crashed process. Refresh calls time
80
+ * out well before it (REFRESH_TIMEOUT_MS), so a live holder never looks stale. */
81
+ exports.LOCK_STALE_MS = 30_000;
82
+ const LOCK_WAIT_MS = 20_000;
83
+ const REFRESH_TIMEOUT_MS = 10_000;
84
+ const LOGIN_TIMEOUT_MS = 10 * 60_000;
85
+ // ─── config IO (atomic) ────────────────────────────────────────────────
86
+ function readConfigAt(configPath) {
87
+ try {
88
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ // Windows refuses to replace a file another process holds without share-delete
95
+ // (antivirus, the search indexer; Node's own readers do share). Those holds
96
+ // last milliseconds, so retry like graceful-fs/write-file-atomic do.
97
+ const RENAME_RETRY_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
98
+ const RENAME_RETRY_MS = [10, 25, 50, 100, 200, 400, 800];
99
+ function sleepSync(ms) {
100
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
101
+ }
102
+ /** rename(), retrying the transient Windows sharing errors. Exported for tests. */
103
+ function renameWithRetry(from, to, rename = fs.renameSync, sleep = sleepSync) {
104
+ for (let attempt = 0;; attempt++) {
105
+ try {
106
+ rename(from, to);
107
+ return;
108
+ }
109
+ catch (err) {
110
+ const code = err?.code ?? "";
111
+ if (!RENAME_RETRY_CODES.has(code) || attempt >= RENAME_RETRY_MS.length)
112
+ throw err;
113
+ sleep(RENAME_RETRY_MS[attempt]);
114
+ }
115
+ }
116
+ }
117
+ /**
118
+ * Write via temp file + rename so a concurrent reader never sees a torn file.
119
+ * If the rename still fails after ~1.6s of retries (a handle held that long),
120
+ * fall back to writing in place: a rare torn read beats failing `connect`.
121
+ */
122
+ function writeConfigAtomic(configPath, config) {
123
+ const dir = path.dirname(configPath);
124
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
125
+ const data = JSON.stringify(config, null, 2);
126
+ const tmp = path.join(dir, `.config.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`);
127
+ fs.writeFileSync(tmp, data, { encoding: "utf8", mode: 0o600 });
128
+ try {
129
+ fs.chmodSync(tmp, 0o600);
130
+ renameWithRetry(tmp, configPath);
131
+ return;
132
+ }
133
+ catch (err) {
134
+ try {
135
+ fs.unlinkSync(tmp);
136
+ }
137
+ catch { /* already gone */ }
138
+ if (!RENAME_RETRY_CODES.has(err?.code ?? ""))
139
+ throw err;
140
+ }
141
+ fs.writeFileSync(configPath, data, { encoding: "utf8", mode: 0o600 });
142
+ fs.chmodSync(configPath, 0o600);
143
+ }
144
+ // ─── refresh lock ──────────────────────────────────────────────────────
145
+ function lockPathFor(configPath) {
146
+ return path.join(path.dirname(configPath), "refresh.lock");
147
+ }
148
+ function tryLock(lockPath, now) {
149
+ try {
150
+ const fd = fs.openSync(lockPath, "wx", 0o600);
151
+ fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: now }));
152
+ fs.closeSync(fd);
153
+ return true;
154
+ }
155
+ catch (err) {
156
+ if (err?.code !== "EEXIST")
157
+ throw err;
158
+ // Break a lock left by a crashed process.
159
+ try {
160
+ const age = now - fs.statSync(lockPath).mtimeMs;
161
+ if (age > exports.LOCK_STALE_MS)
162
+ fs.unlinkSync(lockPath);
163
+ }
164
+ catch {
165
+ /* raced with its owner releasing it: retry */
166
+ }
167
+ return false;
168
+ }
169
+ }
170
+ function unlock(lockPath) {
171
+ try {
172
+ fs.unlinkSync(lockPath);
173
+ }
174
+ catch { /* already released */ }
175
+ }
176
+ function needsRefresh(config, now, minValidityMs) {
177
+ const auth = config.auth;
178
+ return !!auth && !!config.token && auth.accessTokenExpiresAt - now < minValidityMs;
179
+ }
180
+ async function postForm(fetchImpl, url, form, timeoutMs) {
181
+ const ctl = new AbortController();
182
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
183
+ try {
184
+ const res = await fetchImpl(url, {
185
+ method: "POST",
186
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
187
+ body: new URLSearchParams(form).toString(),
188
+ signal: ctl.signal,
189
+ });
190
+ return { status: res.status, body: (await res.json().catch(() => null)) };
191
+ }
192
+ finally {
193
+ clearTimeout(timer);
194
+ }
195
+ }
196
+ /**
197
+ * Make sure config.token is a usable access token for at least
198
+ * `minValidityMs`. Never throws: auth problems surface as the next request's
199
+ * 401, which every command already explains.
200
+ */
201
+ async function ensureFreshToken(deps, minValidityMs = exports.DEFAULT_MIN_VALIDITY_MS) {
202
+ const now = deps.now ?? Date.now;
203
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
204
+ const fetchImpl = deps.fetchImpl ?? fetch;
205
+ const first = readConfigAt(deps.configPath);
206
+ if (!first?.auth)
207
+ return { status: "none", config: first };
208
+ if (!needsRefresh(first, now(), minValidityMs))
209
+ return { status: "fresh", config: first };
210
+ const seen = first.token;
211
+ const lockPath = lockPathFor(deps.configPath);
212
+ const deadline = now() + LOCK_WAIT_MS;
213
+ let locked = false;
214
+ try {
215
+ while (!(locked = tryLock(lockPath, now()))) {
216
+ if (now() > deadline)
217
+ return { status: "unavailable", config: first };
218
+ await sleep(50 + Math.floor(Math.random() * 100));
219
+ }
220
+ // Step 3: re-read under the lock.
221
+ const current = readConfigAt(deps.configPath);
222
+ if (!current?.auth)
223
+ return { status: "none", config: current };
224
+ if (current.token !== seen)
225
+ return { status: "adopted", config: current };
226
+ let res;
227
+ try {
228
+ res = await postForm(fetchImpl, `${current.backendUrl}/oauth/token`, {
229
+ grant_type: "refresh_token",
230
+ client_id: current.auth.clientId,
231
+ refresh_token: current.auth.refreshToken,
232
+ }, REFRESH_TIMEOUT_MS);
233
+ }
234
+ catch {
235
+ return { status: "unavailable", config: current };
236
+ }
237
+ if (res.status === 200 && typeof res.body?.access_token === "string" && typeof res.body.refresh_token === "string") {
238
+ const next = {
239
+ ...current,
240
+ token: res.body.access_token,
241
+ auth: {
242
+ ...current.auth,
243
+ refreshToken: res.body.refresh_token,
244
+ accessTokenExpiresAt: now() + Number(res.body.expires_in ?? 0) * 1000,
245
+ },
246
+ };
247
+ writeConfigAtomic(deps.configPath, next);
248
+ return { status: "refreshed", config: next };
249
+ }
250
+ if (res.status === 400 && res.body?.error === "invalid_grant") {
251
+ // Revoked in the portal, signed out elsewhere, expired, or reuse was
252
+ // detected. Forget the credentials but keep device settings.
253
+ const out = { ...current, token: "" };
254
+ delete out.auth;
255
+ writeConfigAtomic(deps.configPath, out);
256
+ return { status: "signed_out", config: out };
257
+ }
258
+ return { status: "unavailable", config: current };
259
+ }
260
+ finally {
261
+ if (locked)
262
+ unlock(lockPath);
263
+ }
264
+ }
265
+ // ─── browser sign-in ───────────────────────────────────────────────────
266
+ function base64Url(buf) {
267
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
268
+ }
269
+ function pkcePair() {
270
+ const verifier = base64Url(crypto.randomBytes(32));
271
+ const challenge = base64Url(crypto.createHash("sha256").update(verifier).digest());
272
+ return { verifier, challenge };
273
+ }
274
+ function openBrowser(url) {
275
+ try {
276
+ const [cmd, args] = process.platform === "darwin"
277
+ ? ["open", [url]]
278
+ : process.platform === "win32"
279
+ ? ["rundll32", ["url.dll,FileProtocolHandler", url]]
280
+ : ["xdg-open", [url]];
281
+ const child = (0, child_process_1.spawn)(cmd, args, { detached: true, stdio: "ignore" });
282
+ child.on("error", () => { });
283
+ child.unref();
284
+ return true;
285
+ }
286
+ catch {
287
+ return false;
288
+ }
289
+ }
290
+ const DONE_PAGE = (ok, msg) => `<!doctype html><html><head><meta charset="utf-8">
291
+ <title>Codiedev CLI</title><style>body{font-family:system-ui,sans-serif;display:grid;place-items:center;
292
+ height:100vh;margin:0;background:#fbfbfa;color:#1f1f1f}main{max-width:28rem;text-align:center}
293
+ h1{font-size:1.25rem}</style></head><body><main><h1>${ok ? "You're signed in" : "Sign-in didn't finish"}</h1>
294
+ <p>${msg}</p></main></body></html>`;
295
+ /** Parse `code`/`state` (or `error`) out of a loopback callback URL. */
296
+ function parseCallback(input) {
297
+ try {
298
+ const u = new URL(input.trim());
299
+ return {
300
+ code: u.searchParams.get("code") ?? undefined,
301
+ state: u.searchParams.get("state") ?? undefined,
302
+ error: u.searchParams.get("error_description") ?? u.searchParams.get("error") ?? undefined,
303
+ };
304
+ }
305
+ catch {
306
+ return { error: "That isn't the address from the browser's address bar." };
307
+ }
308
+ }
309
+ async function browserLogin(opts) {
310
+ const fetchImpl = opts.fetchImpl ?? fetch;
311
+ const log = opts.log ?? ((line) => console.log(line));
312
+ const now = opts.now ?? Date.now;
313
+ const state = base64Url(crypto.randomBytes(16));
314
+ const { verifier, challenge } = pkcePair();
315
+ let settle;
316
+ const result = new Promise((r) => (settle = r));
317
+ const server = http.createServer((req, res) => {
318
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
319
+ if (url.pathname !== "/callback") {
320
+ res.writeHead(404).end();
321
+ return;
322
+ }
323
+ const cb = parseCallback(url.toString());
324
+ const ok = !cb.error && cb.state === state && !!cb.code;
325
+ res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
326
+ res.end(DONE_PAGE(ok, ok
327
+ ? "You can close this tab and go back to your terminal."
328
+ : "Go back to your terminal and run <code>codiedev connect</code> again."));
329
+ if (cb.error)
330
+ settle({ error: cb.error });
331
+ else if (cb.state !== state)
332
+ settle({ error: "The sign-in response didn't match this request (state mismatch)." });
333
+ else
334
+ settle({ code: cb.code });
335
+ });
336
+ await new Promise((resolve, reject) => {
337
+ server.once("error", reject);
338
+ server.listen(0, "127.0.0.1", () => resolve());
339
+ });
340
+ const port = server.address().port;
341
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
342
+ const timer = setTimeout(() => settle({ error: "Timed out waiting for the browser (10 minutes)." }), LOGIN_TIMEOUT_MS);
343
+ try {
344
+ const reg = await fetchImpl(`${opts.backendUrl}/oauth/register`, {
345
+ method: "POST",
346
+ headers: { "Content-Type": "application/json" },
347
+ body: JSON.stringify({
348
+ client_name: opts.clientName ?? `Codiedev CLI on ${os.hostname()}`,
349
+ redirect_uris: [redirectUri],
350
+ }),
351
+ });
352
+ if (reg.status !== 201)
353
+ throw new Error(`Sign-in setup failed (HTTP ${reg.status}).`);
354
+ const { client_id: clientId } = (await reg.json());
355
+ const authorize = new URL(`${opts.backendUrl}/oauth/authorize`);
356
+ authorize.search = new URLSearchParams({
357
+ client_id: clientId,
358
+ redirect_uri: redirectUri,
359
+ response_type: "code",
360
+ code_challenge: challenge,
361
+ code_challenge_method: "S256",
362
+ state,
363
+ scope: "cli",
364
+ }).toString();
365
+ const link = authorize.toString();
366
+ if (opts.onAuthorizeUrl) {
367
+ opts.onAuthorizeUrl(link);
368
+ }
369
+ else {
370
+ log("Opening your browser to sign in…");
371
+ log(`If it doesn't open, visit:\n ${link}\n`);
372
+ openBrowser(link);
373
+ }
374
+ const cb = await result;
375
+ if (cb.error || !cb.code)
376
+ throw new Error(cb.error ?? "Sign-in was cancelled.");
377
+ const tok = await postForm(fetchImpl, `${opts.backendUrl}/oauth/token`, {
378
+ grant_type: "authorization_code",
379
+ code: cb.code,
380
+ client_id: clientId,
381
+ redirect_uri: redirectUri,
382
+ code_verifier: verifier,
383
+ }, REFRESH_TIMEOUT_MS);
384
+ const body = tok.body;
385
+ if (tok.status !== 200 || typeof body?.access_token !== "string" || typeof body.refresh_token !== "string") {
386
+ throw new Error(`Sign-in failed: ${body?.error_description ?? body?.error ?? `HTTP ${tok.status}`}`);
387
+ }
388
+ if (body.scope !== "cli")
389
+ throw new Error("The server didn't grant CLI access.");
390
+ return {
391
+ accessToken: body.access_token,
392
+ refreshToken: body.refresh_token,
393
+ expiresAt: now() + Number(body.expires_in ?? 0) * 1000,
394
+ clientId,
395
+ };
396
+ }
397
+ finally {
398
+ clearTimeout(timer);
399
+ server.close();
400
+ }
401
+ }
402
+ // ─── device sign-in (SSH / headless) ──────────────────────────────────
403
+ exports.DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
404
+ /**
405
+ * True when a browser opened here can't reach the user: an SSH session, or a
406
+ * Linux box with no display. Then `connect` uses the device flow (a code the
407
+ * user approves on any device) instead of a loopback redirect, with no flag.
408
+ */
409
+ function isHeadless(env = process.env, platform = process.platform) {
410
+ if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY)
411
+ return true;
412
+ if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY)
413
+ return true;
414
+ return false;
415
+ }
416
+ async function deviceLogin(opts) {
417
+ const fetchImpl = opts.fetchImpl ?? fetch;
418
+ const log = opts.log ?? ((line) => console.log(line));
419
+ const now = opts.now ?? Date.now;
420
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
421
+ const reg = await fetchImpl(`${opts.backendUrl}/oauth/register`, {
422
+ method: "POST",
423
+ headers: { "Content-Type": "application/json" },
424
+ body: JSON.stringify({
425
+ client_name: opts.clientName ?? `Codiedev CLI on ${os.hostname()}`,
426
+ redirect_uris: [],
427
+ grant_types: [exports.DEVICE_CODE_GRANT, "refresh_token"],
428
+ }),
429
+ });
430
+ if (reg.status !== 201)
431
+ throw new Error(`Sign-in setup failed (HTTP ${reg.status}).`);
432
+ const { client_id: clientId } = (await reg.json());
433
+ const start = await fetchImpl(`${opts.backendUrl}/oauth/device_authorization`, {
434
+ method: "POST",
435
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
436
+ body: new URLSearchParams({ client_id: clientId, scope: "cli" }).toString(),
437
+ });
438
+ const auth = (await start.json().catch(() => null));
439
+ if (start.status !== 200 || !auth?.device_code || !auth.user_code || !auth.verification_uri) {
440
+ throw new Error(`Sign-in setup failed: ${auth?.error_description ?? `HTTP ${start.status}`}`);
441
+ }
442
+ const complete = auth.verification_uri_complete ?? auth.verification_uri;
443
+ if (opts.onCode) {
444
+ opts.onCode({ userCode: auth.user_code, verificationUri: auth.verification_uri, verificationUriComplete: complete });
445
+ }
446
+ log("To sign in, open this link on any device (your laptop or phone) and approve:\n");
447
+ log(` ${complete}\n`);
448
+ log(`Your code: ${auth.user_code} (check it matches the page)\n`);
449
+ log("Waiting for approval…");
450
+ let intervalMs = Math.max(1, Number(auth.interval ?? 5)) * 1000;
451
+ const deadline = now() + Number(auth.expires_in ?? 600) * 1000;
452
+ for (;;) {
453
+ await sleep(intervalMs);
454
+ if (now() > deadline)
455
+ throw new Error("The code expired before it was approved. Run `codiedev connect` again.");
456
+ let res;
457
+ try {
458
+ res = await postForm(fetchImpl, `${opts.backendUrl}/oauth/token`, { grant_type: exports.DEVICE_CODE_GRANT, device_code: auth.device_code, client_id: clientId }, REFRESH_TIMEOUT_MS);
459
+ }
460
+ catch {
461
+ continue; // network blip: keep polling until the code expires
462
+ }
463
+ const body = res.body;
464
+ if (res.status === 200 && typeof body?.access_token === "string" && typeof body.refresh_token === "string") {
465
+ if (body.scope !== "cli")
466
+ throw new Error("The server didn't grant CLI access.");
467
+ return {
468
+ accessToken: body.access_token,
469
+ refreshToken: body.refresh_token,
470
+ expiresAt: now() + Number(body.expires_in ?? 0) * 1000,
471
+ clientId,
472
+ };
473
+ }
474
+ switch (body?.error) {
475
+ case "authorization_pending":
476
+ continue;
477
+ case "slow_down":
478
+ intervalMs += 5000;
479
+ continue;
480
+ case "access_denied":
481
+ throw new Error("Sign-in was denied in the browser.");
482
+ case "expired_token":
483
+ throw new Error("The code expired before it was approved. Run `codiedev connect` again.");
484
+ default:
485
+ throw new Error(`Sign-in failed: ${body?.error_description ?? body?.error ?? `HTTP ${res.status}`}`);
486
+ }
487
+ }
488
+ }
489
+ /** `codiedev logout`: end a browser sign-in server-side, forget it locally. */
490
+ async function logout(deps) {
491
+ const fetchImpl = deps.fetchImpl ?? fetch;
492
+ const config = readConfigAt(deps.configPath);
493
+ if (!config?.token)
494
+ return { wasConnected: false, hadBrowserAuth: false, serverRevoked: false };
495
+ let serverRevoked = false;
496
+ const hadBrowserAuth = !!config.auth;
497
+ if (config.token && config.auth) {
498
+ try {
499
+ const res = await fetchImpl(`${config.backendUrl}/api/cli/logout`, {
500
+ method: "POST",
501
+ headers: { Authorization: `Bearer ${config.token}` },
502
+ });
503
+ serverRevoked = res.ok && (await res.json().catch(() => null))?.revoked === true;
504
+ }
505
+ catch {
506
+ /* offline: still forget locally; the portal can revoke it */
507
+ }
508
+ }
509
+ const out = { ...config, token: "" };
510
+ delete out.auth;
511
+ writeConfigAtomic(deps.configPath, out);
512
+ return { wasConnected: true, hadBrowserAuth, serverRevoked };
513
+ }
package/dist/codie.js CHANGED
@@ -59,6 +59,11 @@ const upgradeNudge_1 = require("./upgradeNudge");
59
59
  const codie_1 = require("./packs/codie");
60
60
  const paths_1 = require("./packs/paths");
61
61
  const managedPi_1 = require("./managedPi");
62
+ const cliSession_1 = require("./cliSession");
63
+ // The engine reads its gateway key once, at launch. With browser sign-in the
64
+ // key is a 24h access token, so start every session with at least 12h left
65
+ // (refreshing first if needed) instead of one that dies mid-task.
66
+ const CODIE_MIN_TOKEN_VALIDITY_MS = 12 * 60 * 60_000;
62
67
  // How long to let the gateway finish booking the last request before asking
63
68
  // what the session cost.
64
69
  const USAGE_SETTLE_MS = 1200;
@@ -91,7 +96,8 @@ function existingInlineConfig() {
91
96
  }
92
97
  }
93
98
  // The one credential. `codiedev connect` wrote it; the gateway validates the
94
- // same userApiTokens row, so there is no second harness token to mint.
99
+ // same userApiTokens row (a browser sign-in reports through its own row), so
100
+ // there is no second harness token to mint.
95
101
  function connectedCodiedevConfig() {
96
102
  try {
97
103
  return JSON.parse(fs.readFileSync(path.join((0, configDir_1.codiedevStateDir)(), "config.json"), "utf8"));
@@ -171,7 +177,21 @@ function installBrandWordmarkUnsafe(runtime) {
171
177
  }
172
178
  async function main() {
173
179
  (0, codieRuntime_1.assertCodieNodeVersion)(process.versions.node);
180
+ if (!process.env.CODIEDEV_GATEWAY_KEY && !process.env.CODIEDEV_TOKEN) {
181
+ const fresh = await (0, cliSession_1.ensureFreshToken)({ configPath: path.join((0, configDir_1.codiedevStateDir)(), "config.json") }, CODIE_MIN_TOKEN_VALIDITY_MS);
182
+ if (fresh.status === "signed_out") {
183
+ throw new Error("Your Codiedev sign-in has ended. Run `codiedev connect` to sign in again");
184
+ }
185
+ }
174
186
  const connected = connectedCodiedevConfig();
187
+ // Browser sign-in: let the engine's per-request hook keep the 24h key
188
+ // fresh for sessions that run longer (assets/session-key.mjs). Not for an
189
+ // explicit key from the environment, and not for pasted API tokens.
190
+ if (!process.env.CODIEDEV_GATEWAY_KEY && !process.env.CODIEDEV_TOKEN && connected.auth) {
191
+ process.env.CODIEDEV_SESSION_CONFIG = path.join((0, configDir_1.codiedevStateDir)(), "config.json");
192
+ process.env.CODIEDEV_CLI_JS = path.join(__dirname, "cli.js");
193
+ process.env.CODIEDEV_NODE = process.execPath;
194
+ }
175
195
  const key = process.env.CODIEDEV_GATEWAY_KEY ||
176
196
  process.env.CODIEDEV_TOKEN ||
177
197
  (typeof connected.token === "string" ? connected.token.trim() : "");
@@ -2,6 +2,7 @@
2
2
  // `codiedev traces agents`
3
3
  // `codiedev traces cases <agent> [--dataset name] [--model id] [--out dir] [--json]`
4
4
  // `codiedev traces report <results.json> [--harness name]`
5
+ // (scores, review queues, automatic scoring and datasets: ./tracesLabel.ts)
5
6
  //
6
7
  // Test cases are runs an engineer saved from Codiedev → Traces. They run in
7
8
  // the engineer's coding agent, not in the portal: `cases` writes them into the
@@ -51,8 +52,9 @@ exports.runTraces = runTraces;
51
52
  const fs = __importStar(require("fs"));
52
53
  const path = __importStar(require("path"));
53
54
  const shared_1 = require("./shared");
55
+ const tracesLabel_1 = require("./tracesLabel");
54
56
  exports.TRACES_HELP = `
55
- codiedev traces — test cases saved from Codiedev → Traces
57
+ codiedev traces — test cases, scores, review queues and automatic scoring from Codiedev → Traces
56
58
 
57
59
  codiedev traces agents Agents you maintain, with saved case counts
58
60
  codiedev traces cases <agent> [--dataset name [--version n]] [--case id[,id]] [--model id] [--out dir] [--json]
@@ -66,6 +68,36 @@ codiedev traces — test cases saved from Codiedev → Traces
66
68
  Send the filled-in results back, so
67
69
  Traces shows the pass rate for that model
68
70
 
71
+ codiedev traces overview [--days n] [--json]
72
+ Every agent you can see: runs, failures,
73
+ spend, and what's going wrong most
74
+ codiedev traces runs <agent> [--failed] [--flag name] [--model id] [--verdict pass|fail] [--days n] [--limit n] [--json]
75
+ An agent's runs, newest first
76
+ codiedev traces run <run> [--agent name] [--json]
77
+ One run: steps, what went wrong and the fix,
78
+ scores, prompts and replies (maintainers)
79
+ codiedev traces score-kinds [add <name> --type number|categories|boolean [--min n --max n] [--categories a,b]]
80
+ List or add the kinds of score your team uses
81
+ codiedev traces queue create <name> [--scores a,b] [--description text]
82
+ codiedev traces score <run> <score> <value> [--agent name] [--comment text] [--step n]
83
+ Score a run (a portal run id agent~run, or
84
+ the agent's own run id with --agent)
85
+ codiedev traces scores <agent> [--days n] [--json]
86
+ Each score's average or mix, and the score kinds
87
+ codiedev traces queue list Review queues and how many are left
88
+ codiedev traces queue add <queue> --agent <name> [--failed] [--days n] [--run id ...]
89
+ File runs into a queue for people to review
90
+ codiedev traces judge <agent> [--on | --off] [--scores a,b] [--instructions text]
91
+ [--model id] [--sample percent] [--daily n] [--budget usd | --key cdvm_…]
92
+ Show or set automatic scoring (a judge model
93
+ scores a sample of runs, billed to your key)
94
+ codiedev traces import <agent> <file.csv|file.jsonl> --dataset <name>
95
+ Add examples from a file (input, expected, name)
96
+ codiedev traces add-example <agent> --dataset <name> --input <text> [--expected <text>] [--name <text>]
97
+ codiedev traces versions <dataset> A dataset's saved versions and what changed
98
+ codiedev traces save-version <dataset> [--note text]
99
+ Save the dataset as its next version
100
+
69
101
  <agent> is the agent's name (its API key name) or id. Only an agent's
70
102
  maintainers can pull its cases or report on them.
71
103
  `.trim();
@@ -295,6 +327,30 @@ async function runTraces(args, configOverride) {
295
327
  return runCases(rest, configOverride ?? (0, shared_1.requireConfig)());
296
328
  if (sub === "report")
297
329
  return runReport(rest, configOverride ?? (0, shared_1.requireConfig)());
330
+ if (sub === "overview")
331
+ return (0, tracesLabel_1.runOverview)(rest, configOverride ?? (0, shared_1.requireConfig)());
332
+ if (sub === "runs")
333
+ return (0, tracesLabel_1.runRuns)(rest, configOverride ?? (0, shared_1.requireConfig)());
334
+ if (sub === "run")
335
+ return (0, tracesLabel_1.runRun)(rest, configOverride ?? (0, shared_1.requireConfig)());
336
+ if (sub === "score-kinds")
337
+ return (0, tracesLabel_1.runScoreKinds)(rest, configOverride ?? (0, shared_1.requireConfig)());
338
+ if (sub === "score")
339
+ return (0, tracesLabel_1.runScore)(rest, configOverride ?? (0, shared_1.requireConfig)());
340
+ if (sub === "scores")
341
+ return (0, tracesLabel_1.runScores)(rest, configOverride ?? (0, shared_1.requireConfig)());
342
+ if (sub === "queue")
343
+ return (0, tracesLabel_1.runQueue)(rest, configOverride ?? (0, shared_1.requireConfig)());
344
+ if (sub === "judge")
345
+ return (0, tracesLabel_1.runJudge)(rest, configOverride ?? (0, shared_1.requireConfig)());
346
+ if (sub === "import")
347
+ return (0, tracesLabel_1.runImport)(rest, configOverride ?? (0, shared_1.requireConfig)());
348
+ if (sub === "add-example")
349
+ return (0, tracesLabel_1.runAddExample)(rest, configOverride ?? (0, shared_1.requireConfig)());
350
+ if (sub === "versions")
351
+ return (0, tracesLabel_1.runVersions)(rest, configOverride ?? (0, shared_1.requireConfig)());
352
+ if (sub === "save-version")
353
+ return (0, tracesLabel_1.runSaveVersion)(rest, configOverride ?? (0, shared_1.requireConfig)());
298
354
  console.log(exports.TRACES_HELP);
299
355
  if (sub && sub !== "help")
300
356
  process.exit(1);
@@ -0,0 +1,24 @@
1
+ import type { CodiedevConfig } from "../utils";
2
+ type Parsed = {
3
+ positional: string[];
4
+ flags: Record<string, string>;
5
+ bools: Set<string>;
6
+ runs: string[];
7
+ };
8
+ export declare function parseArgs(args: string[]): Parsed;
9
+ /** "4" → 4, "true" → true, anything else as text (a category). */
10
+ export declare function scoreValue(raw: string): number | boolean | string;
11
+ export declare function runScore(args: string[], config: CodiedevConfig): Promise<void>;
12
+ export declare function runScores(args: string[], config: CodiedevConfig): Promise<void>;
13
+ export declare function runQueue(args: string[], config: CodiedevConfig): Promise<void>;
14
+ export declare function runJudge(args: string[], config: CodiedevConfig): Promise<void>;
15
+ export declare function runImport(args: string[], config: CodiedevConfig): Promise<void>;
16
+ export declare function runAddExample(args: string[], config: CodiedevConfig): Promise<void>;
17
+ export declare function runVersions(args: string[], config: CodiedevConfig): Promise<void>;
18
+ export declare function runSaveVersion(args: string[], config: CodiedevConfig): Promise<void>;
19
+ export declare function runOverview(args: string[], config: CodiedevConfig): Promise<void>;
20
+ export declare function runRuns(args: string[], config: CodiedevConfig): Promise<void>;
21
+ export declare function runRun(args: string[], config: CodiedevConfig): Promise<void>;
22
+ export declare function runScoreKinds(args: string[], config: CodiedevConfig): Promise<void>;
23
+ export declare function runQueueCreate(args: string[], config: CodiedevConfig): Promise<void>;
24
+ export {};