codiedev 0.9.79 → 0.9.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/assets/opencode-plugin.mjs +6 -0
  2. package/assets/session-key.mjs +48 -0
  3. package/dist/cli.js +113 -9
  4. package/dist/cliSession.d.ts +108 -0
  5. package/dist/cliSession.js +513 -0
  6. package/dist/codie.js +21 -1
  7. package/dist/commands/agentTemplates.generated.d.ts +11 -0
  8. package/dist/commands/agentTemplates.generated.js +139 -0
  9. package/dist/commands/agents.d.ts +7 -0
  10. package/dist/commands/agents.js +1130 -0
  11. package/dist/commands/agentsSdk.generated.d.ts +4 -0
  12. package/dist/commands/agentsSdk.generated.js +10 -0
  13. package/dist/commands/scrub.d.ts +11 -0
  14. package/dist/commands/scrub.js +102 -0
  15. package/dist/configDir.d.ts +9 -0
  16. package/dist/configDir.js +25 -1
  17. package/dist/connect.js +45 -2
  18. package/dist/connectFlow.d.ts +21 -0
  19. package/dist/connectFlow.js +41 -0
  20. package/dist/hook.js +25 -0
  21. package/dist/knownSecrets.d.ts +43 -0
  22. package/dist/knownSecrets.js +241 -0
  23. package/dist/launcher.d.ts +7 -2
  24. package/dist/launcher.js +36 -2
  25. package/dist/localScrub.d.ts +172 -0
  26. package/dist/localScrub.js +820 -0
  27. package/dist/localScrubReport.d.ts +83 -0
  28. package/dist/localScrubReport.js +268 -0
  29. package/dist/managedPi.js +18 -2
  30. package/dist/mcp.js +3 -0
  31. package/dist/scrub/extraRules.d.ts +4 -0
  32. package/dist/scrub/extraRules.js +17 -0
  33. package/dist/scrub/rules.d.ts +86 -0
  34. package/dist/scrub/rules.js +645 -0
  35. package/dist/scrub/scrubProviderRules.d.ts +14 -0
  36. package/dist/scrub/scrubProviderRules.js +149 -0
  37. package/dist/utils.d.ts +2 -0
  38. package/dist/utils.js +9 -7
  39. package/package.json +5 -4
@@ -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() : "");
@@ -0,0 +1,11 @@
1
+ export type AgentTemplate = {
2
+ id: string;
3
+ title: string;
4
+ summary: string;
5
+ trigger: string;
6
+ uses: string[];
7
+ lang: string;
8
+ adapt: string;
9
+ files: Record<string, string>;
10
+ };
11
+ export declare const TEMPLATES: AgentTemplate[];