codiedev 0.10.0-beta.6 → 0.10.0-beta.8

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/cli.js CHANGED
@@ -2,6 +2,8 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const connect_1 = require("./connect");
5
+ const cliSession_1 = require("./cliSession");
6
+ const utils_1 = require("./utils");
5
7
  const push_1 = require("./commands/push");
6
8
  const delete_1 = require("./commands/delete");
7
9
  const pull_1 = require("./commands/pull");
@@ -31,8 +33,10 @@ Codiedev CLI
31
33
  someone left off across sessions and providers.
32
34
 
33
35
  Connect:
34
- codiedev connect Link Claude Code / Codex / Cursor /
36
+ codiedev connect Sign in with your browser and link
37
+ Claude Code / Codex / Cursor /
35
38
  GitHub Copilot / pi to your org
39
+ codiedev logout Sign this machine out
36
40
  codiedev doctor Verify setup and capture
37
41
 
38
42
  Harness:
@@ -162,8 +166,9 @@ const COMMAND_HELP = {
162
166
  connect: `
163
167
  codiedev connect — link your agent CLI to Codiedev
164
168
 
165
- Interactive setup. Prompts for an API token, then installs:
166
- - ~/.codiedev/config.json (token + org + tracked repos)
169
+ Opens your browser to sign in (approve once, no API key to copy), then
170
+ installs:
171
+ - ~/.codiedev/config.json (sign-in + org + tracked repos)
167
172
  - ~/.claude/settings.json hook (captures session transcripts)
168
173
  - ~/.claude/CLAUDE.md instructions (teaches Claude the commands)
169
174
  - ~/.codex/hooks.json hook (same, for Codex)
@@ -176,7 +181,27 @@ codiedev connect — link your agent CLI to Codiedev
176
181
 
177
182
  Re-run anytime to swap accounts or refresh the instruction block.
178
183
 
179
- Get your token: https://codiedev.com/portal/integrations/claude-code
184
+ Over SSH or on a machine with no display it shows a link and a short code
185
+ instead: open the link on your laptop or phone, check the code, approve.
186
+ Detected automatically.
187
+
188
+ Options:
189
+ --token Paste an API token instead of signing in with a browser
190
+ (CI, or a machine with no browser anywhere). Create one at
191
+ https://codiedev.com/portal/integrations/claude-code
192
+ --force, -f Sign in again even if this machine is already connected.
193
+
194
+ Browser sign-in stores a 24-hour access token and a refresh token that
195
+ rotates on every use; nothing long-lived is saved. Revoke a machine from
196
+ Integrations in the portal, or run 'codiedev logout' on it.
197
+ `.trim(),
198
+ logout: `
199
+ codiedev logout — sign this machine out
200
+
201
+ Ends this machine's browser sign-in on the server and removes it from
202
+ ~/.codiedev/config.json. Device settings (ignored folders) are kept.
203
+ For a pasted API token, it's only forgotten locally: revoke it in the
204
+ portal under Integrations.
180
205
  `.trim(),
181
206
  push: `
182
207
  codiedev push — author or update an artifact
@@ -395,13 +420,15 @@ Run 'codiedev help <command>' for each, or see the quick list:
395
420
 
396
421
  codiedev help
397
422
 
398
- ## Getting a token
423
+ ## Signing in
399
424
 
400
- 1. Log in to https://codiedev.com/portal/integrations/claude-code
401
- 2. Click "Create token"
402
- 3. Copy the cdv_... string and paste it into 'codiedev connect'
425
+ 'codiedev connect' opens your browser; approve, and you're done. Nothing
426
+ long-lived is stored: a 24-hour access token plus a rotating refresh token.
403
427
 
404
- Tokens are per-user. Rotate any time by revoking + creating a new one.
428
+ Over SSH it shows a link and code to approve from your laptop or phone,
429
+ automatically. For CI, create a token at
430
+ https://codiedev.com/portal/integrations/claude-code and run
431
+ 'codiedev connect --token'.
405
432
  `.trim();
406
433
  const REMOVED_COMMANDS = {
407
434
  promote: "Share an auto-saved spec with 'Promote to team' in the portal's My artifacts list.",
@@ -456,11 +483,32 @@ async function main() {
456
483
  console.log(`codiedev v${version_1.CLI_VERSION}`);
457
484
  return;
458
485
  }
486
+ if (command !== "connect" && command !== "logout") {
487
+ const fresh = await (0, cliSession_1.ensureFreshToken)({ configPath: utils_1.CONFIG_PATH }).catch(() => null);
488
+ if (fresh?.status === "signed_out") {
489
+ console.error("\nYour Codiedev sign-in has ended (revoked or expired). Run `codiedev connect` to sign in again.\n");
490
+ }
491
+ }
459
492
  try {
460
493
  switch (command) {
461
494
  case "connect":
462
495
  await (0, connect_1.runConnect)(rest);
463
496
  return;
497
+ case "logout": {
498
+ const out = await (0, cliSession_1.logout)({ configPath: utils_1.CONFIG_PATH });
499
+ if (!out.wasConnected) {
500
+ console.log("This machine isn't signed in.");
501
+ }
502
+ else if (out.hadBrowserAuth) {
503
+ console.log(out.serverRevoked
504
+ ? "Signed out. This machine's sign-in is revoked."
505
+ : "Signed out on this machine. Couldn't reach Codiedev to revoke the sign-in; revoke it under Integrations in the portal.");
506
+ }
507
+ else {
508
+ console.log("Removed the API token from this machine. It still works elsewhere until you revoke it under Integrations in the portal.");
509
+ }
510
+ return;
511
+ }
464
512
  case "doctor":
465
513
  case "verify":
466
514
  await (0, doctor_1.runDoctor)(rest);
@@ -0,0 +1,456 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DEVICE_CODE_GRANT = exports.LOCK_STALE_MS = exports.DEFAULT_MIN_VALIDITY_MS = void 0;
37
+ exports.readConfigAt = readConfigAt;
38
+ exports.renameWithRetry = renameWithRetry;
39
+ exports.writeConfigAtomic = writeConfigAtomic;
40
+ exports.ensureFreshToken = ensureFreshToken;
41
+ exports.pkcePair = pkcePair;
42
+ exports.openBrowser = openBrowser;
43
+ exports.parseCallback = parseCallback;
44
+ exports.browserLogin = browserLogin;
45
+ exports.isHeadless = isHeadless;
46
+ exports.deviceLogin = deviceLogin;
47
+ exports.logout = logout;
48
+ const crypto = __importStar(require("crypto"));
49
+ const fs = __importStar(require("fs"));
50
+ const http = __importStar(require("http"));
51
+ const os = __importStar(require("os"));
52
+ const path = __importStar(require("path"));
53
+ const child_process_1 = require("child_process");
54
+ exports.DEFAULT_MIN_VALIDITY_MS = 10 * 60_000;
55
+ exports.LOCK_STALE_MS = 30_000;
56
+ const LOCK_WAIT_MS = 20_000;
57
+ const REFRESH_TIMEOUT_MS = 10_000;
58
+ const LOGIN_TIMEOUT_MS = 10 * 60_000;
59
+ function readConfigAt(configPath) {
60
+ try {
61
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ const RENAME_RETRY_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
68
+ const RENAME_RETRY_MS = [10, 25, 50, 100, 200, 400, 800];
69
+ function sleepSync(ms) {
70
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
71
+ }
72
+ function renameWithRetry(from, to, rename = fs.renameSync, sleep = sleepSync) {
73
+ for (let attempt = 0;; attempt++) {
74
+ try {
75
+ rename(from, to);
76
+ return;
77
+ }
78
+ catch (err) {
79
+ const code = err?.code ?? "";
80
+ if (!RENAME_RETRY_CODES.has(code) || attempt >= RENAME_RETRY_MS.length)
81
+ throw err;
82
+ sleep(RENAME_RETRY_MS[attempt]);
83
+ }
84
+ }
85
+ }
86
+ function writeConfigAtomic(configPath, config) {
87
+ const dir = path.dirname(configPath);
88
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
89
+ const data = JSON.stringify(config, null, 2);
90
+ const tmp = path.join(dir, `.config.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`);
91
+ fs.writeFileSync(tmp, data, { encoding: "utf8", mode: 0o600 });
92
+ try {
93
+ fs.chmodSync(tmp, 0o600);
94
+ renameWithRetry(tmp, configPath);
95
+ return;
96
+ }
97
+ catch (err) {
98
+ try {
99
+ fs.unlinkSync(tmp);
100
+ }
101
+ catch { }
102
+ if (!RENAME_RETRY_CODES.has(err?.code ?? ""))
103
+ throw err;
104
+ }
105
+ fs.writeFileSync(configPath, data, { encoding: "utf8", mode: 0o600 });
106
+ fs.chmodSync(configPath, 0o600);
107
+ }
108
+ function lockPathFor(configPath) {
109
+ return path.join(path.dirname(configPath), "refresh.lock");
110
+ }
111
+ function tryLock(lockPath, now) {
112
+ try {
113
+ const fd = fs.openSync(lockPath, "wx", 0o600);
114
+ fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: now }));
115
+ fs.closeSync(fd);
116
+ return true;
117
+ }
118
+ catch (err) {
119
+ if (err?.code !== "EEXIST")
120
+ throw err;
121
+ try {
122
+ const age = now - fs.statSync(lockPath).mtimeMs;
123
+ if (age > exports.LOCK_STALE_MS)
124
+ fs.unlinkSync(lockPath);
125
+ }
126
+ catch {
127
+ }
128
+ return false;
129
+ }
130
+ }
131
+ function unlock(lockPath) {
132
+ try {
133
+ fs.unlinkSync(lockPath);
134
+ }
135
+ catch { }
136
+ }
137
+ function needsRefresh(config, now, minValidityMs) {
138
+ const auth = config.auth;
139
+ return !!auth && !!config.token && auth.accessTokenExpiresAt - now < minValidityMs;
140
+ }
141
+ async function postForm(fetchImpl, url, form, timeoutMs) {
142
+ const ctl = new AbortController();
143
+ const timer = setTimeout(() => ctl.abort(), timeoutMs);
144
+ try {
145
+ const res = await fetchImpl(url, {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
148
+ body: new URLSearchParams(form).toString(),
149
+ signal: ctl.signal,
150
+ });
151
+ return { status: res.status, body: (await res.json().catch(() => null)) };
152
+ }
153
+ finally {
154
+ clearTimeout(timer);
155
+ }
156
+ }
157
+ async function ensureFreshToken(deps, minValidityMs = exports.DEFAULT_MIN_VALIDITY_MS) {
158
+ const now = deps.now ?? Date.now;
159
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
160
+ const fetchImpl = deps.fetchImpl ?? fetch;
161
+ const first = readConfigAt(deps.configPath);
162
+ if (!first?.auth)
163
+ return { status: "none", config: first };
164
+ if (!needsRefresh(first, now(), minValidityMs))
165
+ return { status: "fresh", config: first };
166
+ const seen = first.token;
167
+ const lockPath = lockPathFor(deps.configPath);
168
+ const deadline = now() + LOCK_WAIT_MS;
169
+ let locked = false;
170
+ try {
171
+ while (!(locked = tryLock(lockPath, now()))) {
172
+ if (now() > deadline)
173
+ return { status: "unavailable", config: first };
174
+ await sleep(50 + Math.floor(Math.random() * 100));
175
+ }
176
+ const current = readConfigAt(deps.configPath);
177
+ if (!current?.auth)
178
+ return { status: "none", config: current };
179
+ if (current.token !== seen)
180
+ return { status: "adopted", config: current };
181
+ let res;
182
+ try {
183
+ res = await postForm(fetchImpl, `${current.backendUrl}/oauth/token`, {
184
+ grant_type: "refresh_token",
185
+ client_id: current.auth.clientId,
186
+ refresh_token: current.auth.refreshToken,
187
+ }, REFRESH_TIMEOUT_MS);
188
+ }
189
+ catch {
190
+ return { status: "unavailable", config: current };
191
+ }
192
+ if (res.status === 200 && typeof res.body?.access_token === "string" && typeof res.body.refresh_token === "string") {
193
+ const next = {
194
+ ...current,
195
+ token: res.body.access_token,
196
+ auth: {
197
+ ...current.auth,
198
+ refreshToken: res.body.refresh_token,
199
+ accessTokenExpiresAt: now() + Number(res.body.expires_in ?? 0) * 1000,
200
+ },
201
+ };
202
+ writeConfigAtomic(deps.configPath, next);
203
+ return { status: "refreshed", config: next };
204
+ }
205
+ if (res.status === 400 && res.body?.error === "invalid_grant") {
206
+ const out = { ...current, token: "" };
207
+ delete out.auth;
208
+ writeConfigAtomic(deps.configPath, out);
209
+ return { status: "signed_out", config: out };
210
+ }
211
+ return { status: "unavailable", config: current };
212
+ }
213
+ finally {
214
+ if (locked)
215
+ unlock(lockPath);
216
+ }
217
+ }
218
+ function base64Url(buf) {
219
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
220
+ }
221
+ function pkcePair() {
222
+ const verifier = base64Url(crypto.randomBytes(32));
223
+ const challenge = base64Url(crypto.createHash("sha256").update(verifier).digest());
224
+ return { verifier, challenge };
225
+ }
226
+ function openBrowser(url) {
227
+ try {
228
+ const [cmd, args] = process.platform === "darwin"
229
+ ? ["open", [url]]
230
+ : process.platform === "win32"
231
+ ? ["rundll32", ["url.dll,FileProtocolHandler", url]]
232
+ : ["xdg-open", [url]];
233
+ const child = (0, child_process_1.spawn)(cmd, args, { detached: true, stdio: "ignore" });
234
+ child.on("error", () => { });
235
+ child.unref();
236
+ return true;
237
+ }
238
+ catch {
239
+ return false;
240
+ }
241
+ }
242
+ const DONE_PAGE = (ok, msg) => `<!doctype html><html><head><meta charset="utf-8">
243
+ <title>Codiedev CLI</title><style>body{font-family:system-ui,sans-serif;display:grid;place-items:center;
244
+ height:100vh;margin:0;background:#fbfbfa;color:#1f1f1f}main{max-width:28rem;text-align:center}
245
+ h1{font-size:1.25rem}</style></head><body><main><h1>${ok ? "You're signed in" : "Sign-in didn't finish"}</h1>
246
+ <p>${msg}</p></main></body></html>`;
247
+ function parseCallback(input) {
248
+ try {
249
+ const u = new URL(input.trim());
250
+ return {
251
+ code: u.searchParams.get("code") ?? undefined,
252
+ state: u.searchParams.get("state") ?? undefined,
253
+ error: u.searchParams.get("error_description") ?? u.searchParams.get("error") ?? undefined,
254
+ };
255
+ }
256
+ catch {
257
+ return { error: "That isn't the address from the browser's address bar." };
258
+ }
259
+ }
260
+ async function browserLogin(opts) {
261
+ const fetchImpl = opts.fetchImpl ?? fetch;
262
+ const log = opts.log ?? ((line) => console.log(line));
263
+ const now = opts.now ?? Date.now;
264
+ const state = base64Url(crypto.randomBytes(16));
265
+ const { verifier, challenge } = pkcePair();
266
+ let settle;
267
+ const result = new Promise((r) => (settle = r));
268
+ const server = http.createServer((req, res) => {
269
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
270
+ if (url.pathname !== "/callback") {
271
+ res.writeHead(404).end();
272
+ return;
273
+ }
274
+ const cb = parseCallback(url.toString());
275
+ const ok = !cb.error && cb.state === state && !!cb.code;
276
+ res.writeHead(ok ? 200 : 400, { "Content-Type": "text/html; charset=utf-8" });
277
+ res.end(DONE_PAGE(ok, ok
278
+ ? "You can close this tab and go back to your terminal."
279
+ : "Go back to your terminal and run <code>codiedev connect</code> again."));
280
+ if (cb.error)
281
+ settle({ error: cb.error });
282
+ else if (cb.state !== state)
283
+ settle({ error: "The sign-in response didn't match this request (state mismatch)." });
284
+ else
285
+ settle({ code: cb.code });
286
+ });
287
+ await new Promise((resolve, reject) => {
288
+ server.once("error", reject);
289
+ server.listen(0, "127.0.0.1", () => resolve());
290
+ });
291
+ const port = server.address().port;
292
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
293
+ const timer = setTimeout(() => settle({ error: "Timed out waiting for the browser (10 minutes)." }), LOGIN_TIMEOUT_MS);
294
+ try {
295
+ const reg = await fetchImpl(`${opts.backendUrl}/oauth/register`, {
296
+ method: "POST",
297
+ headers: { "Content-Type": "application/json" },
298
+ body: JSON.stringify({
299
+ client_name: opts.clientName ?? `Codiedev CLI on ${os.hostname()}`,
300
+ redirect_uris: [redirectUri],
301
+ }),
302
+ });
303
+ if (reg.status !== 201)
304
+ throw new Error(`Sign-in setup failed (HTTP ${reg.status}).`);
305
+ const { client_id: clientId } = (await reg.json());
306
+ const authorize = new URL(`${opts.backendUrl}/oauth/authorize`);
307
+ authorize.search = new URLSearchParams({
308
+ client_id: clientId,
309
+ redirect_uri: redirectUri,
310
+ response_type: "code",
311
+ code_challenge: challenge,
312
+ code_challenge_method: "S256",
313
+ state,
314
+ scope: "cli",
315
+ }).toString();
316
+ const link = authorize.toString();
317
+ if (opts.onAuthorizeUrl) {
318
+ opts.onAuthorizeUrl(link);
319
+ }
320
+ else {
321
+ log("Opening your browser to sign in…");
322
+ log(`If it doesn't open, visit:\n ${link}\n`);
323
+ openBrowser(link);
324
+ }
325
+ const cb = await result;
326
+ if (cb.error || !cb.code)
327
+ throw new Error(cb.error ?? "Sign-in was cancelled.");
328
+ const tok = await postForm(fetchImpl, `${opts.backendUrl}/oauth/token`, {
329
+ grant_type: "authorization_code",
330
+ code: cb.code,
331
+ client_id: clientId,
332
+ redirect_uri: redirectUri,
333
+ code_verifier: verifier,
334
+ }, REFRESH_TIMEOUT_MS);
335
+ const body = tok.body;
336
+ if (tok.status !== 200 || typeof body?.access_token !== "string" || typeof body.refresh_token !== "string") {
337
+ throw new Error(`Sign-in failed: ${body?.error_description ?? body?.error ?? `HTTP ${tok.status}`}`);
338
+ }
339
+ if (body.scope !== "cli")
340
+ throw new Error("The server didn't grant CLI access.");
341
+ return {
342
+ accessToken: body.access_token,
343
+ refreshToken: body.refresh_token,
344
+ expiresAt: now() + Number(body.expires_in ?? 0) * 1000,
345
+ clientId,
346
+ };
347
+ }
348
+ finally {
349
+ clearTimeout(timer);
350
+ server.close();
351
+ }
352
+ }
353
+ exports.DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
354
+ function isHeadless(env = process.env, platform = process.platform) {
355
+ if (env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY)
356
+ return true;
357
+ if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY)
358
+ return true;
359
+ return false;
360
+ }
361
+ async function deviceLogin(opts) {
362
+ const fetchImpl = opts.fetchImpl ?? fetch;
363
+ const log = opts.log ?? ((line) => console.log(line));
364
+ const now = opts.now ?? Date.now;
365
+ const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
366
+ const reg = await fetchImpl(`${opts.backendUrl}/oauth/register`, {
367
+ method: "POST",
368
+ headers: { "Content-Type": "application/json" },
369
+ body: JSON.stringify({
370
+ client_name: opts.clientName ?? `Codiedev CLI on ${os.hostname()}`,
371
+ redirect_uris: [],
372
+ grant_types: [exports.DEVICE_CODE_GRANT, "refresh_token"],
373
+ }),
374
+ });
375
+ if (reg.status !== 201)
376
+ throw new Error(`Sign-in setup failed (HTTP ${reg.status}).`);
377
+ const { client_id: clientId } = (await reg.json());
378
+ const start = await fetchImpl(`${opts.backendUrl}/oauth/device_authorization`, {
379
+ method: "POST",
380
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
381
+ body: new URLSearchParams({ client_id: clientId, scope: "cli" }).toString(),
382
+ });
383
+ const auth = (await start.json().catch(() => null));
384
+ if (start.status !== 200 || !auth?.device_code || !auth.user_code || !auth.verification_uri) {
385
+ throw new Error(`Sign-in setup failed: ${auth?.error_description ?? `HTTP ${start.status}`}`);
386
+ }
387
+ const complete = auth.verification_uri_complete ?? auth.verification_uri;
388
+ if (opts.onCode) {
389
+ opts.onCode({ userCode: auth.user_code, verificationUri: auth.verification_uri, verificationUriComplete: complete });
390
+ }
391
+ log("To sign in, open this link on any device (your laptop or phone) and approve:\n");
392
+ log(` ${complete}\n`);
393
+ log(`Your code: ${auth.user_code} (check it matches the page)\n`);
394
+ log("Waiting for approval…");
395
+ let intervalMs = Math.max(1, Number(auth.interval ?? 5)) * 1000;
396
+ const deadline = now() + Number(auth.expires_in ?? 600) * 1000;
397
+ for (;;) {
398
+ await sleep(intervalMs);
399
+ if (now() > deadline)
400
+ throw new Error("The code expired before it was approved. Run `codiedev connect` again.");
401
+ let res;
402
+ try {
403
+ 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);
404
+ }
405
+ catch {
406
+ continue;
407
+ }
408
+ const body = res.body;
409
+ if (res.status === 200 && typeof body?.access_token === "string" && typeof body.refresh_token === "string") {
410
+ if (body.scope !== "cli")
411
+ throw new Error("The server didn't grant CLI access.");
412
+ return {
413
+ accessToken: body.access_token,
414
+ refreshToken: body.refresh_token,
415
+ expiresAt: now() + Number(body.expires_in ?? 0) * 1000,
416
+ clientId,
417
+ };
418
+ }
419
+ switch (body?.error) {
420
+ case "authorization_pending":
421
+ continue;
422
+ case "slow_down":
423
+ intervalMs += 5000;
424
+ continue;
425
+ case "access_denied":
426
+ throw new Error("Sign-in was denied in the browser.");
427
+ case "expired_token":
428
+ throw new Error("The code expired before it was approved. Run `codiedev connect` again.");
429
+ default:
430
+ throw new Error(`Sign-in failed: ${body?.error_description ?? body?.error ?? `HTTP ${res.status}`}`);
431
+ }
432
+ }
433
+ }
434
+ async function logout(deps) {
435
+ const fetchImpl = deps.fetchImpl ?? fetch;
436
+ const config = readConfigAt(deps.configPath);
437
+ if (!config?.token)
438
+ return { wasConnected: false, hadBrowserAuth: false, serverRevoked: false };
439
+ let serverRevoked = false;
440
+ const hadBrowserAuth = !!config.auth;
441
+ if (config.token && config.auth) {
442
+ try {
443
+ const res = await fetchImpl(`${config.backendUrl}/api/cli/logout`, {
444
+ method: "POST",
445
+ headers: { Authorization: `Bearer ${config.token}` },
446
+ });
447
+ serverRevoked = res.ok && (await res.json().catch(() => null))?.revoked === true;
448
+ }
449
+ catch {
450
+ }
451
+ }
452
+ const out = { ...config, token: "" };
453
+ delete out.auth;
454
+ writeConfigAtomic(deps.configPath, out);
455
+ return { wasConnected: true, hadBrowserAuth, serverRevoked };
456
+ }
package/dist/codie.js CHANGED
@@ -49,6 +49,8 @@ const upgradeNudge_1 = require("./upgradeNudge");
49
49
  const codie_1 = require("./packs/codie");
50
50
  const paths_1 = require("./packs/paths");
51
51
  const managedPi_1 = require("./managedPi");
52
+ const cliSession_1 = require("./cliSession");
53
+ const CODIE_MIN_TOKEN_VALIDITY_MS = 12 * 60 * 60_000;
52
54
  const USAGE_SETTLE_MS = 1200;
53
55
  function git(args) {
54
56
  const result = (0, child_process_1.spawnSync)("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -186,6 +188,12 @@ async function runNonInteractive(runtime, runtimeArgs, env) {
186
188
  }
187
189
  async function main() {
188
190
  (0, codieRuntime_1.assertCodieNodeVersion)(process.versions.node);
191
+ if (!process.env.CODIEDEV_GATEWAY_KEY && !process.env.CODIEDEV_TOKEN) {
192
+ const fresh = await (0, cliSession_1.ensureFreshToken)({ configPath: path.join((0, configDir_1.codiedevStateDir)(), "config.json") }, CODIE_MIN_TOKEN_VALIDITY_MS);
193
+ if (fresh.status === "signed_out") {
194
+ throw new Error("Your Codiedev sign-in has ended. Run `codiedev connect` to sign in again");
195
+ }
196
+ }
189
197
  const connected = connectedCodiedevConfig();
190
198
  const key = process.env.CODIEDEV_GATEWAY_KEY ||
191
199
  process.env.CODIEDEV_TOKEN ||
@@ -55,6 +55,10 @@ codiedev agents — deploy agents and run them in fresh sandboxes
55
55
  codiedev agents install <template> <name>
56
56
  Deploy a copy of a shared template as your own agent
57
57
  codiedev agents unpublish <template> Take a template you shared off the marketplace
58
+ codiedev agents connect <name> [--description "…"] [--budget <usd>]
59
+ An agent you already run on your own servers: get its key.
60
+ Its model calls (via the gateway) show in Traces with spend,
61
+ budget and a kill switch; the key also reaches approvals and state
58
62
  codiedev agents deploy [dir] Package the folder and deploy it (new version each time)
59
63
  codiedev agents run <name> [--input '<json>' | --input-file f] [--no-wait] [--json] [--quiet]
60
64
  [--idempotency-key k] [--delay 30m] [--retries n]
@@ -90,8 +94,10 @@ codiedev agents — deploy agents and run them in fresh sandboxes
90
94
  .codiedevignore lists extra files to leave out of the bundle (.env*, .git,
91
95
  node_modules and virtualenvs are always left out).
92
96
  entrypoint: a .py, .js, .mjs, .ts or .sh file. "every" (5m–7d) runs it on a
93
- schedule; omit it, or set null, for none. retries (0–5) tries a failed run
94
- again on the same run id, after 15s, 30s, 60s… cpu 0.25–8 cores, memoryMb 512–32768.
97
+ schedule; set null for none. retries (0–5) tries a failed run again on the
98
+ same run id, after 15s, 30s, 60s… cpu 0.25–8 cores, memoryMb 512–32768.
99
+ Leave a setting out to keep what it is now (it can be changed on the agent's
100
+ Setup tab in the portal); a new agent gets the defaults.
95
101
  `.trim();
96
102
  exports.AGENT_GUIDE = `# Building a Codiedev agent
97
103
 
@@ -1028,6 +1034,22 @@ async function cmdCancel(args, config) {
1028
1034
  const res = await (0, shared_1.apiRequest)("POST", "/api/agents/v1/cancel", { config, body: { id } });
1029
1035
  console.log(res.status && res.status !== "cancelled" ? `Run already ${res.status}.` : "Cancelled.");
1030
1036
  }
1037
+ async function cmdConnect(args, config) {
1038
+ const [name] = positional(args);
1039
+ if (!name)
1040
+ fail('Usage: codiedev agents connect <name> [--description "…"] [--budget <usd>]');
1041
+ const budget = flag(args, "--budget");
1042
+ const out = await (0, shared_1.apiRequest)("POST", "/api/agents/v1/connect", {
1043
+ config,
1044
+ body: { name, description: flag(args, "--description"), ...(budget ? { monthlyBudgetUsd: Number(budget) } : {}) },
1045
+ });
1046
+ console.log(`Connected ${out.agent}. Its key is shown once; put it in your secret manager.\n`);
1047
+ console.log(`CODIEDEV_API_KEY=${out.key}`);
1048
+ console.log(`CODIEDEV_GATEWAY_URL=${out.gatewayUrl}`);
1049
+ console.log(`CODIEDEV_AGENTS_URL=${out.agentsUrl}\n`);
1050
+ console.log("Point its OpenAI-compatible client at CODIEDEV_GATEWAY_URL with that key (one X-Codiedev-Run-Id per job groups its calls into a trace).");
1051
+ console.log(`The same key calls ${out.agentsUrl}/api/agents/v1/approvals and /state. See it: Agents → ${out.agent}.`);
1052
+ }
1031
1053
  async function cmdDelete(args, config) {
1032
1054
  const [name] = positional(args);
1033
1055
  if (!name)
@@ -1044,7 +1066,7 @@ async function runAgents(args, configOverride) {
1044
1066
  if (sub === "run" && rest.includes("--local"))
1045
1067
  return cmdRunLocal(rest);
1046
1068
  const commands = {
1047
- deploy: cmdDeploy, run: cmdRun, runs: cmdRuns, show: cmdShow, logs: cmdLogs, list: cmdList, ls: cmdList,
1069
+ connect: cmdConnect, deploy: cmdDeploy, run: cmdRun, runs: cmdRuns, show: cmdShow, logs: cmdLogs, list: cmdList, ls: cmdList,
1048
1070
  secrets: cmdSecrets, webhook: cmdWebhook, cancel: cmdCancel, stop: (a, c) => cmdControl(a, c, true), resume: (a, c) => cmdControl(a, c, false), delete: cmdDelete, pull: cmdPull,
1049
1071
  versions: cmdVersions, rollback: cmdRollback, state: cmdState, approvals: cmdApprovals, decide: cmdDecide,
1050
1072
  publish: cmdPublish, install: cmdInstall, unpublish: cmdUnpublish,
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.jsPackage = exports.jsTypes = exports.jsIndex = exports.python = void 0;
4
- exports.python = "\"\"\"Codiedev agents SDK — the run primitive, from Python.\n\nInside a deployed agent (everything is already configured):\n\n import codiedev_agents as agents\n\n task = agents.input() # this run's input (JSON)\n llm = agents.openai() # OpenAI client on the Codiedev gateway, traced\n child = agents.run(\"summarize\", {\"text\": ...}) # start another agent as a child run\n agents.output({\"summary\": child[\"output\"]}) # this run's result\n agents.request() # webhook runs: method, headers, raw body\n agents.respond({\"text\": \"on it\"}) # what a webhook caller waiting (?wait=) gets back\n\n agents.state.get(\"cursor\") # per-agent storage that outlives runs\n agents.state.set(\"cursor\", 42)\n agents.step(\"fetch\", lambda: fetch()) # done once per run, even across retries\n agents.start(\"triage\", {...}, delay=3600) # start a run later (up to 7 days)\n agents.request_approval(\"Refund $120?\", context={...}) # a person decides; a run acts on it\n agents.attempt() # 1, or 2+ when this run is a retry\n\nOutside a run (a script, a notebook, another service), set CODIEDEV_TOKEN\n(your CLI token) and CODIEDEV_AGENTS_URL, then call agents.run() the same way.\nStandard library only, so it drops into any project.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport time\nimport urllib.error\nimport urllib.parse\nimport urllib.request\nfrom typing import Any\n\n__all__ = [\n \"input\", \"output\", \"request\", \"respond\", \"run\", \"start\", \"get\", \"openai\", \"attempt\",\n \"state\", \"step\", \"request_approval\", \"get_approval\", \"AgentRunError\", \"StateConflict\",\n]\n\n_INPUT_PATH = os.environ.get(\"CODIEDEV_INPUT_PATH\", \"/agent/.codiedev/input.json\")\n_OUTPUT_PATH = os.environ.get(\"CODIEDEV_OUTPUT_PATH\", \"/agent/.codiedev/output.json\")\n_REQUEST_PATH = os.environ.get(\"CODIEDEV_REQUEST_PATH\", \"/agent/.codiedev/request.json\")\n_RESPONSE_PATH = os.environ.get(\"CODIEDEV_RESPONSE_PATH\", \"/agent/.codiedev/response.json\")\n# Set by `codiedev agents run --local`: state lives in this file instead of Codiedev.\n_STATE_PATH = os.environ.get(\"CODIEDEV_STATE_PATH\")\n_DONE = {\"succeeded\", \"failed\", \"cancelled\"}\n\n\nclass StateConflict(RuntimeError):\n \"\"\"state.set(..., if_version=n) found the key at another version.\"\"\"\n\n def __init__(self, key: str, version: int):\n super().__init__(f\"state {key!r} changed (now version {version})\")\n self.key = key\n self.version = version\n\n\nclass AgentRunError(RuntimeError):\n def __init__(self, run: dict):\n super().__init__(f\"{run.get('agent')} run {run.get('id')} {run.get('status')}: {run.get('error') or 'no error message'}\")\n self.run = run\n\n\ndef input(default: Any = None) -> Any: # noqa: A001 - mirrors output()\n \"\"\"This run's input, parsed from JSON (None when started without one).\"\"\"\n try:\n with open(_INPUT_PATH) as f:\n value = json.load(f)\n except FileNotFoundError:\n return default\n return default if value is None else value\n\n\ndef output(value: Any) -> None:\n \"\"\"Set this run's result. Must be JSON-serializable; the last call wins.\"\"\"\n os.makedirs(os.path.dirname(_OUTPUT_PATH), exist_ok=True)\n with open(_OUTPUT_PATH, \"w\") as f:\n json.dump(value, f)\n\n\ndef respond(body: Any = None, status: int = 200, headers: \"dict | None\" = None) -> None:\n \"\"\"What a webhook caller gets back when it waits for the run (?wait=N):\n a string is sent as text, anything else as JSON. Without respond(), a\n waiting caller gets the run's output.\"\"\"\n os.makedirs(os.path.dirname(_RESPONSE_PATH), exist_ok=True)\n with open(_RESPONSE_PATH, \"w\") as f:\n json.dump({\"status\": status, \"headers\": headers or {}, \"body\": body}, f)\n\n\ndef attempt() -> int:\n \"\"\"This run's attempt: 1, or 2+ when it's a retry of a failed attempt.\"\"\"\n return int(os.environ.get(\"CODIEDEV_ATTEMPT\", \"1\"))\n\n\ndef request() -> \"dict | None\":\n \"\"\"For a webhook run: {\"method\", \"query\", \"headers\", \"body\"} with the raw\n body, to verify the sender's signature. None for other runs.\"\"\"\n try:\n with open(_REQUEST_PATH) as f:\n return json.load(f)\n except FileNotFoundError:\n return None\n\n\ndef _base() -> str:\n url = os.environ.get(\"CODIEDEV_AGENTS_URL\", \"\").rstrip(\"/\")\n if not url:\n raise RuntimeError(\"Set CODIEDEV_AGENTS_URL (it's set for you inside a run)\")\n return url\n\n\ndef _token() -> str:\n token = os.environ.get(\"CODIEDEV_RUN_TOKEN\") or os.environ.get(\"CODIEDEV_TOKEN\")\n if not token:\n raise RuntimeError(\"Set CODIEDEV_TOKEN to your Codiedev CLI token (inside a run it's set for you)\")\n return token\n\n\ndef _request(method: str, path: str, body: dict | None = None, ok: tuple[int, ...] = ()) -> dict:\n req = urllib.request.Request(\n f\"{_base()}{path}\",\n method=method,\n data=json.dumps(body).encode() if body is not None else None,\n headers={\"Authorization\": f\"Bearer {_token()}\", \"Content-Type\": \"application/json\"},\n )\n try:\n with urllib.request.urlopen(req, timeout=30) as res:\n return json.loads(res.read() or b\"{}\")\n except urllib.error.HTTPError as err:\n detail = err.read().decode(errors=\"replace\")\n if err.code in ok:\n return {\"_status\": err.code, **json.loads(detail or \"{}\")}\n try:\n detail = json.loads(detail).get(\"error\", detail)\n except ValueError:\n pass\n raise RuntimeError(f\"Codiedev agents API {err.code}: {detail}\") from None\n\n\ndef start(\n agent: str,\n input: Any = None, # noqa: A002\n idempotency_key: str | None = None,\n delay: float | None = None,\n retries: int | None = None,\n) -> str:\n \"\"\"Start a run and return its id without waiting. With an idempotency key,\n a second start within a day returns the first run's id. delay (seconds,\n up to 7 days) starts it later; retries overrides the agent's retries.\"\"\"\n body: dict = {\"agent\": agent, \"input\": input}\n if idempotency_key:\n body[\"idempotencyKey\"] = idempotency_key\n if delay:\n body[\"delaySec\"] = int(delay)\n if retries is not None:\n body[\"retries\"] = retries\n return _request(\"POST\", \"/api/agents/v1/run\", body)[\"runId\"]\n\n\ndef get(run_id: str) -> dict:\n \"\"\"A run's status, input, output (parsed), error, log tail and children.\"\"\"\n run = _request(\"GET\", f\"/api/agents/v1/run?id={run_id}\")\n if isinstance(run.get(\"output\"), str):\n try:\n run[\"output\"] = json.loads(run[\"output\"])\n except ValueError:\n pass\n return run\n\n\ndef run(\n agent: str,\n input: Any = None, # noqa: A002\n timeout: float = 900,\n poll: float = 2.0,\n check: bool = True,\n idempotency_key: str | None = None,\n) -> dict:\n \"\"\"Run an agent and wait for it. Returns the run; its result is run[\"output\"].\n\n Inside a run this starts a child run, so the chain shows as one tree.\n Raises AgentRunError if it fails (pass check=False to get the run anyway).\n \"\"\"\n run_id = start(agent, input, idempotency_key)\n deadline = time.monotonic() + timeout\n while True:\n result = get(run_id)\n if result.get(\"status\") in _DONE:\n if check and result[\"status\"] != \"succeeded\":\n raise AgentRunError(result)\n return result\n if time.monotonic() > deadline:\n raise TimeoutError(f\"{agent} run {run_id} still {result.get('status')} after {timeout}s\")\n time.sleep(poll)\n\n\nclass _State:\n \"\"\"Per-agent key-value storage that outlives runs (values: any JSON, up\n to 64 KB; 1,000 keys). Every write bumps the key's version; pass\n if_version to write only if nobody else did since you read it.\"\"\"\n\n def get(self, key: str, default: Any = None) -> Any:\n value, _ = self.get_versioned(key)\n return default if value is None else value\n\n def get_versioned(self, key: str) -> \"tuple[Any, int]\":\n \"\"\"(value, version); (None, 0) when the key isn't set.\"\"\"\n if _STATE_PATH:\n row = _local_state().get(key)\n return (row[\"value\"], row[\"version\"]) if row else (None, 0)\n res = _request(\"GET\", f\"/api/agents/v1/state?{urllib.parse.urlencode({'key': key, 'agent': _agent()})}\", ok=(404,))\n if res.get(\"_status\") == 404:\n return None, 0\n return json.loads(res[\"value\"]), int(res[\"version\"])\n\n def set(self, key: str, value: Any, if_version: \"int | None\" = None, ttl: \"float | None\" = None) -> int:\n \"\"\"Store value; returns the new version. Raises StateConflict when\n if_version is given and the key is at another version (0 = unset).\"\"\"\n return self._write(key, value, if_version, ttl)\n\n def delete(self, key: str, if_version: \"int | None\" = None) -> None:\n self._write(key, None, if_version, None)\n\n def list(self, prefix: str = \"\", values: bool = False) -> \"list[dict]\": # noqa: A003\n \"\"\"Keys (and with values=True, values) starting with prefix.\"\"\"\n if _STATE_PATH:\n rows = sorted((k, r) for k, r in _local_state().items() if k.startswith(prefix))\n return [{\"key\": k, \"version\": r[\"version\"], **({\"value\": r[\"value\"]} if values else {})} for k, r in rows]\n keys: list = []\n after = None\n while True:\n params = {\"prefix\": prefix, \"values\": \"1\" if values else \"0\", \"limit\": 200, \"agent\": _agent()}\n if after:\n params[\"after\"] = after\n page = _request(\"GET\", f\"/api/agents/v1/state?{urllib.parse.urlencode(params)}\")\n for row in page[\"keys\"]:\n if \"value\" in row:\n row[\"value\"] = json.loads(row[\"value\"])\n keys.extend(page[\"keys\"])\n after = page.get(\"next\")\n if not after:\n return keys\n\n def update(self, key: str, fn, default: Any = None, retries: int = 10) -> Any:\n \"\"\"Read-modify-write that's safe with runs in parallel: fn(old) -> new,\n retried if another run wrote first. Returns the new value.\"\"\"\n for _ in range(retries):\n value, version = self.get_versioned(key)\n new = fn(default if value is None else value)\n try:\n self.set(key, new, if_version=version)\n return new\n except StateConflict:\n time.sleep(0.05)\n raise StateConflict(key, -1)\n\n def _write(self, key: str, value: Any, if_version: \"int | None\", ttl: \"float | None\") -> int:\n if _STATE_PATH:\n data = _local_state()\n current = data.get(key, {}).get(\"version\", 0)\n if if_version is not None and if_version != current:\n raise StateConflict(key, current)\n if value is None:\n data.pop(key, None)\n else:\n data[key] = {\"value\": value, \"version\": current + 1}\n with open(_STATE_PATH, \"w\") as f:\n json.dump(data, f, indent=2)\n return 0 if value is None else current + 1\n body: dict = {\"key\": key, \"value\": value, \"agent\": _agent()}\n if if_version is not None:\n body[\"ifVersion\"] = if_version\n if ttl:\n body[\"ttlSec\"] = int(ttl)\n res = _request(\"POST\", \"/api/agents/v1/state\", body, ok=(409,))\n if res.get(\"_status\") == 409:\n raise StateConflict(key, int(res.get(\"version\", 0)))\n return int(res.get(\"version\", 0))\n\n\ndef _agent() -> str:\n # Inside a run the run token picks the agent; a CLI token needs it named.\n return os.environ.get(\"CODIEDEV_AGENT\", \"\")\n\n\ndef _local_state() -> dict:\n try:\n with open(_STATE_PATH or \"\") as f:\n return json.load(f)\n except (FileNotFoundError, ValueError):\n return {}\n\n\nstate = _State()\n\n\ndef step(name: str, fn, ttl: float = 7 * 86400) -> Any:\n \"\"\"Run fn() once for this run and remember its (JSON) result: if the run\n is retried after a failure, finished steps return their saved result\n instead of running again. Use it around side effects (posting a reply,\n charging a card) so a retry doesn't repeat them.\"\"\"\n key = f\"step/{os.environ.get('CODIEDEV_RUN_ID', 'local')}/{name}\"\n value, version = state.get_versioned(key)\n if version:\n return value[\"result\"]\n result = fn()\n state.set(key, {\"result\": result}, ttl=ttl)\n return result\n\n\n_SELF = object()\n\n\ndef request_approval(\n title: str,\n details: Any = None,\n options: \"list[str] | None\" = None,\n approvers: \"list[str] | None\" = None,\n then: Any = _SELF,\n context: Any = None,\n expires_in: \"float | None\" = None,\n) -> dict:\n \"\"\"Ask a person to decide. It shows in Codiedev (Agents → Approvals) and\n at the returned [\"url\"], which you can post anywhere (Slack, email).\n\n When it's decided, a run of `then` (this agent by default; None for no\n run) starts with input {\"approval\": {\"decision\", \"comment\", \"decidedBy\",\n ...}, \"context\": context}. If nobody decides within expires_in seconds\n (default 7 days), it starts with decision \"expired\". Usually the run\n ends after asking, so nothing runs (or costs) while it waits.\n options default to [\"approve\", \"reject\"]; approvers (emails) limit who\n can decide (admins always can).\"\"\"\n body: dict = {\"title\": title, \"details\": details, \"options\": options, \"approvers\": approvers, \"context\": context}\n if then is not _SELF:\n body[\"onDecision\"] = then\n if expires_in:\n body[\"expiresSec\"] = int(expires_in)\n if os.environ.get(\"CODIEDEV_LOCAL\") == \"1\":\n print(f\"[local] approval requested: {title} ({', '.join(options or ['approve', 'reject'])}); not sent on a local run\", flush=True)\n return {\"id\": f\"local-{int(time.time())}\", \"status\": \"pending\", \"url\": None, \"title\": title}\n return _request(\"POST\", \"/api/agents/v1/approvals\", {k: v for k, v in body.items() if v is not None or k == \"onDecision\"})\n\n\ndef get_approval(approval_id: str) -> dict:\n \"\"\"An approval's status (\"pending\", \"decided\", \"expired\"), decision and comment.\"\"\"\n return _request(\"GET\", f\"/api/agents/v1/approvals?id={urllib.parse.quote(approval_id)}\")\n\n\ndef openai(**kwargs):\n \"\"\"An OpenAI client on the Codiedev gateway, with trace headers for this run.\"\"\"\n from openai import OpenAI\n\n headers = {\n \"X-Codiedev-Workload\": os.environ.get(\"CODIEDEV_AGENT\", \"local-agent\"),\n \"X-Codiedev-Run-Id\": os.environ.get(\"CODIEDEV_RUN_ID\", f\"local-{int(time.time())}\"),\n \"X-Codiedev-Environment\": \"production\" if os.environ.get(\"CODIEDEV_RUN_ID\") else \"development\",\n }\n headers.update(kwargs.pop(\"default_headers\", {}) or {})\n return OpenAI(\n base_url=os.environ.get(\"CODIEDEV_GATEWAY_URL\", \"https://gateway.codiedev.com/api/v1\"),\n api_key=os.environ.get(\"CODIEDEV_API_KEY\") or os.environ.get(\"CODIEDEV_TOKEN\"),\n default_headers=headers,\n **kwargs,\n )\n";
5
- exports.jsIndex = "// Codiedev agents SDK for JavaScript and TypeScript, preinstalled in every\n// run. Same primitive as the Python SDK (codiedev_agents.py):\n//\n// import * as agents from \"codiedev-agents\";\n// const task = agents.input(); // this run's input (JSON)\n// const llm = await agents.openai(); // OpenAI client on the Codiedev gateway, traced\n// const child = await agents.run(\"summarize\", { text }); // another agent, as a child run\n// agents.output({ summary: child.output }); // this run's result\n// agents.respond({ text: \"on it\" }); // what a webhook caller waiting (?wait=) gets back\n// await agents.state.get(\"cursor\"); // per-agent storage that outlives runs\n// await agents.step(\"post\", () => post()); // done once per run, even across retries\n// await agents.start(\"triage\", input, { delaySec: 3600 }); // start a run later\n// await agents.requestApproval(\"Refund $120?\", { context }); // a person decides; a run acts on it\n//\n// Outside a run, set CODIEDEV_TOKEN and CODIEDEV_AGENTS_URL and call run() the same way.\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nconst INPUT_PATH = process.env.CODIEDEV_INPUT_PATH || \"/agent/.codiedev/input.json\";\nconst OUTPUT_PATH = process.env.CODIEDEV_OUTPUT_PATH || \"/agent/.codiedev/output.json\";\nconst REQUEST_PATH = process.env.CODIEDEV_REQUEST_PATH || \"/agent/.codiedev/request.json\";\nconst RESPONSE_PATH = process.env.CODIEDEV_RESPONSE_PATH || \"/agent/.codiedev/response.json\";\n// Set by `codiedev agents run --local`: state lives in this file instead of Codiedev.\nconst STATE_PATH = process.env.CODIEDEV_STATE_PATH;\n// Inside a run the run token picks the agent; a CLI token needs it named.\nconst AGENT = process.env.CODIEDEV_AGENT || \"\";\nconst DONE = new Set([\"succeeded\", \"failed\", \"cancelled\"]);\n\n/** state.set(..., { ifVersion }) found the key at another version. */\nexport class StateConflict extends Error {\n constructor(key, version) {\n super(`state ${JSON.stringify(key)} changed (now version ${version})`);\n this.key = key;\n this.version = version;\n }\n}\n\nexport class AgentRunError extends Error {\n constructor(run) {\n super(`${run.agent} run ${run.id} ${run.status}: ${run.error || \"no error message\"}`);\n this.run = run;\n }\n}\n\nfunction readJson(file) {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch (err) {\n if (err.code === \"ENOENT\") return null;\n throw err;\n }\n}\n\n/** This run's input, parsed from JSON (`fallback` when started without one). */\nexport function input(fallback = null) {\n const value = readJson(INPUT_PATH);\n return value ?? fallback;\n}\n\n/** Set this run's result. Must be JSON-serializable; the last call wins. */\nexport function output(value) {\n fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });\n fs.writeFileSync(OUTPUT_PATH, JSON.stringify(value));\n}\n\n/** What a webhook caller gets back when it waits for the run (?wait=N): a\n * string is sent as text, anything else as JSON. Without it, a waiting\n * caller gets the run's output. */\nexport function respond(body = null, { status = 200, headers = {} } = {}) {\n fs.mkdirSync(path.dirname(RESPONSE_PATH), { recursive: true });\n fs.writeFileSync(RESPONSE_PATH, JSON.stringify({ status, headers, body }));\n}\n\n/** This run's attempt: 1, or 2+ when it's a retry of a failed attempt. */\nexport function attempt() {\n return Number(process.env.CODIEDEV_ATTEMPT || \"1\");\n}\n\n/** For a webhook run: { method, query, headers, body } with the raw body, to verify a signature. */\nexport function request() {\n return readJson(REQUEST_PATH);\n}\n\nfunction base() {\n const url = (process.env.CODIEDEV_AGENTS_URL || \"\").replace(/\\/$/, \"\");\n if (!url) throw new Error(\"Set CODIEDEV_AGENTS_URL (it's set for you inside a run)\");\n return url;\n}\n\nfunction token() {\n const t = process.env.CODIEDEV_RUN_TOKEN || process.env.CODIEDEV_TOKEN;\n if (!t) throw new Error(\"Set CODIEDEV_TOKEN to your Codiedev CLI token (inside a run it's set for you)\");\n return t;\n}\n\nasync function call(method, route, body, ok = []) {\n const res = await fetch(`${base()}${route}`, {\n method,\n headers: { Authorization: `Bearer ${token()}`, \"Content-Type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n const text = await res.text();\n let data = {};\n try {\n data = text ? JSON.parse(text) : {};\n } catch {\n data = { error: text };\n }\n if (ok.includes(res.status)) return { _status: res.status, ...data };\n if (!res.ok) throw new Error(`Codiedev agents API ${res.status}: ${data.error || text}`);\n return data;\n}\n\n/** Start a run and return its id without waiting. delaySec (up to 7 days)\n * starts it later; retries overrides the agent's retries. */\nexport async function start(agent, input = null, { idempotencyKey, delaySec, retries } = {}) {\n return (await call(\"POST\", \"/api/agents/v1/run\", {\n agent,\n input,\n ...(idempotencyKey ? { idempotencyKey } : {}),\n ...(delaySec ? { delaySec } : {}),\n ...(retries !== undefined ? { retries } : {}),\n })).runId;\n}\n\nfunction localState() {\n try {\n return JSON.parse(fs.readFileSync(STATE_PATH, \"utf8\"));\n } catch {\n return {};\n }\n}\n\nasync function writeState(key, value, { ifVersion, ttlSec } = {}) {\n if (STATE_PATH) {\n const data = localState();\n const current = data[key]?.version ?? 0;\n if (ifVersion !== undefined && ifVersion !== current) throw new StateConflict(key, current);\n if (value === null) delete data[key];\n else data[key] = { value, version: current + 1 };\n fs.writeFileSync(STATE_PATH, JSON.stringify(data, null, 2));\n return value === null ? 0 : current + 1;\n }\n const res = await call(\"POST\", \"/api/agents/v1/state\", { key, value, agent: AGENT, ...(ifVersion !== undefined ? { ifVersion } : {}), ...(ttlSec ? { ttlSec } : {}) }, [409]);\n if (res._status === 409) throw new StateConflict(key, res.version ?? 0);\n return res.version ?? 0;\n}\n\n/** Per-agent key-value storage that outlives runs (values: any JSON, up to\n * 64 KB; 1,000 keys). Every write bumps the key's version; pass ifVersion\n * to write only if nobody else did since you read it. */\nexport const state = {\n async get(key, fallback = null) {\n const { value } = await state.getVersioned(key);\n return value ?? fallback;\n },\n /** { value, version }; { value: null, version: 0 } when the key isn't set. */\n async getVersioned(key) {\n if (STATE_PATH) {\n const row = localState()[key];\n return row ? { value: row.value, version: row.version } : { value: null, version: 0 };\n }\n const res = await call(\"GET\", `/api/agents/v1/state?${new URLSearchParams({ key, agent: AGENT })}`, undefined, [404]);\n if (res._status === 404) return { value: null, version: 0 };\n return { value: JSON.parse(res.value), version: res.version };\n },\n /** Store value; returns the new version. Throws StateConflict on an ifVersion mismatch (0 = unset). */\n set(key, value, { ifVersion, ttlSec } = {}) {\n return writeState(key, value, { ifVersion, ttlSec });\n },\n async delete(key, { ifVersion } = {}) {\n await writeState(key, null, { ifVersion });\n },\n /** Keys (and with values: true, values) starting with prefix. */\n async list(prefix = \"\", { values = false } = {}) {\n if (STATE_PATH) {\n return Object.entries(localState())\n .filter(([k]) => k.startsWith(prefix))\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, row]) => ({ key, version: row.version, ...(values ? { value: row.value } : {}) }));\n }\n const out = [];\n let after = null;\n do {\n const q = new URLSearchParams({ prefix, values: values ? \"1\" : \"0\", limit: \"200\", agent: AGENT, ...(after ? { after } : {}) });\n const page = await call(\"GET\", `/api/agents/v1/state?${q}`);\n out.push(...page.keys.map((row) => (\"value\" in row ? { ...row, value: JSON.parse(row.value) } : row)));\n after = page.next;\n } while (after);\n return out;\n },\n /** Read-modify-write that's safe with runs in parallel: fn(old) => new, retried if another run wrote first. */\n async update(key, fn, fallback = null, retries = 10) {\n for (let i = 0; i < retries; i++) {\n const { value, version } = await state.getVersioned(key);\n const next = await fn(value ?? fallback);\n try {\n await state.set(key, next, { ifVersion: version });\n return next;\n } catch (err) {\n if (!(err instanceof StateConflict)) throw err;\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n }\n throw new StateConflict(key, -1);\n },\n};\n\n/** Run fn() once for this run and remember its (JSON) result: if the run is\n * retried after a failure, finished steps return their saved result instead\n * of running again. Use it around side effects so a retry doesn't repeat them. */\nexport async function step(name, fn, { ttlSec = 7 * 86400 } = {}) {\n const key = `step/${process.env.CODIEDEV_RUN_ID || \"local\"}/${name}`;\n const { value, version } = await state.getVersioned(key);\n if (version) return value.result;\n const result = await fn();\n await state.set(key, { result: result ?? null }, { ttlSec });\n return result;\n}\n\n/** Ask a person to decide. It shows in Codiedev (Agents → Approvals) and at\n * the returned .url, which you can post anywhere. When it's decided, a run of\n * `then` (this agent by default; null for none) starts with input\n * { approval: { decision, comment, decidedBy, ... }, context }. Undecided\n * after expiresSec (default 7 days): decision \"expired\". */\nexport async function requestApproval(title, { details, options, approvers, then, context, expiresSec } = {}) {\n if (process.env.CODIEDEV_LOCAL === \"1\") {\n console.log(`[local] approval requested: ${title} (${(options || [\"approve\", \"reject\"]).join(\", \")}); not sent on a local run`);\n return { id: `local-${Date.now()}`, status: \"pending\", url: null, title };\n }\n return call(\"POST\", \"/api/agents/v1/approvals\", {\n title,\n ...(details !== undefined ? { details } : {}),\n ...(options ? { options } : {}),\n ...(approvers ? { approvers } : {}),\n ...(then !== undefined ? { onDecision: then } : {}),\n ...(context !== undefined ? { context } : {}),\n ...(expiresSec ? { expiresSec } : {}),\n });\n}\n\n/** An approval's status (\"pending\", \"decided\", \"expired\"), decision and comment. */\nexport async function getApproval(id) {\n return call(\"GET\", `/api/agents/v1/approvals?id=${encodeURIComponent(id)}`);\n}\n\n/** A run's status, input, output (parsed), error, log and children. */\nexport async function get(runId) {\n const run = await call(\"GET\", `/api/agents/v1/run?id=${encodeURIComponent(runId)}`);\n if (typeof run.output === \"string\") {\n try {\n run.output = JSON.parse(run.output);\n } catch {\n // not JSON: keep the string\n }\n }\n return run;\n}\n\n/** Run an agent and wait for it; its result is `.output`. Inside a run this\n * starts a child run. Throws AgentRunError if it fails unless check is false. */\nexport async function run(agent, input = null, { timeoutMs = 900_000, pollMs = 2000, check = true, idempotencyKey } = {}) {\n const id = await start(agent, input, { idempotencyKey });\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const r = await get(id);\n if (DONE.has(r.status)) {\n if (check && r.status !== \"succeeded\") throw new AgentRunError(r);\n return r;\n }\n if (Date.now() > deadline) throw new Error(`${agent} run ${id} still ${r.status} after ${timeoutMs / 1000}s`);\n await new Promise((resolve) => setTimeout(resolve, pollMs));\n }\n}\n\n/** Options for `new OpenAI(...)` on the Codiedev gateway, with this run's trace headers. */\nexport function openaiOptions(extra = {}) {\n const inRun = !!process.env.CODIEDEV_RUN_ID;\n return {\n baseURL: process.env.CODIEDEV_GATEWAY_URL || \"https://gateway.codiedev.com/api/v1\",\n apiKey: process.env.CODIEDEV_API_KEY || process.env.CODIEDEV_TOKEN,\n ...extra,\n defaultHeaders: {\n \"X-Codiedev-Workload\": process.env.CODIEDEV_AGENT || \"local-agent\",\n \"X-Codiedev-Run-Id\": process.env.CODIEDEV_RUN_ID || `local-${Date.now()}`,\n \"X-Codiedev-Environment\": inRun ? \"production\" : \"development\",\n ...(extra.defaultHeaders || {}),\n },\n };\n}\n\n/** An OpenAI client on the Codiedev gateway (the `openai` package is preinstalled). */\nexport async function openai(extra = {}) {\n const { default: OpenAI } = await import(\"openai\");\n return new OpenAI(openaiOptions(extra));\n}\n";
6
- exports.jsTypes = "/* eslint-disable @typescript-eslint/no-explicit-any -- defaults are `any` on purpose: agent code reads its own JSON input/state without declaring types first. */\nexport type RunStatus = \"queued\" | \"running\" | \"succeeded\" | \"failed\" | \"cancelled\";\nexport interface Run<O = unknown> {\n id: string;\n agent: string;\n version: number;\n status: RunStatus;\n trigger: \"cli\" | \"api\" | \"portal\" | \"schedule\" | \"webhook\" | \"child\" | \"approval\";\n parentRunId: string | null;\n rootRunId: string | null;\n depth: number;\n attempt: number;\n maxAttempts: number;\n scheduledFor: number | null;\n input: string | null;\n output: O;\n error: string | null;\n logTail: string | null;\n durationMs: number | null;\n children: Run[];\n}\nexport interface WebhookRequest { method: string; query: Record<string, string>; headers: Record<string, string>; body: string }\nexport interface Approval {\n id: string;\n agent: string;\n title: string;\n details: string | null;\n options: string[];\n status: \"pending\" | \"decided\" | \"expired\" | \"cancelled\";\n decision: string | null;\n comment: string | null;\n decidedBy: string | null;\n url: string | null;\n resumeRunId: string | null;\n}\n/** The input a run started by a decision gets. */\nexport interface ApprovalInput<C = any> {\n approval: { id: string; title: string; decision: string; comment: string | null; decidedBy: string | null; decidedAt: number; requestedByRun: string | null };\n context: C;\n}\nexport class AgentRunError extends Error { run: Run }\nexport class StateConflict extends Error { key: string; version: number }\nexport function input<T = any>(fallback?: T): T;\nexport function output(value: unknown): void;\nexport function request(): WebhookRequest | null;\nexport function respond(body?: unknown, opts?: { status?: number; headers?: Record<string, string> }): void;\nexport function attempt(): number;\nexport function start(agent: string, input?: unknown, opts?: { idempotencyKey?: string; delaySec?: number; retries?: number }): Promise<string>;\nexport function get<O = any>(runId: string): Promise<Run<O>>;\nexport function run<O = any>(agent: string, input?: unknown, opts?: { timeoutMs?: number; pollMs?: number; check?: boolean; idempotencyKey?: string }): Promise<Run<O>>;\nexport const state: {\n get<T = any>(key: string, fallback?: T): Promise<T>;\n getVersioned<T = any>(key: string): Promise<{ value: T | null; version: number }>;\n set(key: string, value: unknown, opts?: { ifVersion?: number; ttlSec?: number }): Promise<number>;\n delete(key: string, opts?: { ifVersion?: number }): Promise<void>;\n list(prefix?: string, opts?: { values?: boolean }): Promise<Array<{ key: string; version: number; value?: any }>>;\n update<T = any>(key: string, fn: (old: T) => T | Promise<T>, fallback?: T, retries?: number): Promise<T>;\n};\nexport function step<T>(name: string, fn: () => T | Promise<T>, opts?: { ttlSec?: number }): Promise<T>;\nexport function requestApproval<C = any>(title: string, opts?: { details?: unknown; options?: string[]; approvers?: string[]; then?: string | null; context?: C; expiresSec?: number }): Promise<Approval>;\nexport function getApproval(id: string): Promise<Approval>;\nexport function openaiOptions(extra?: Record<string, unknown>): { baseURL: string; apiKey: string | undefined; defaultHeaders: Record<string, string> };\nexport function openai(extra?: Record<string, unknown>): Promise<import(\"openai\").default>;";
4
+ exports.python = "\"\"\"Codiedev agents SDK — the run primitive, from Python.\n\nInside a deployed agent (everything is already configured):\n\n import codiedev_agents as agents\n\n task = agents.input() # this run's input (JSON)\n llm = agents.openai() # OpenAI client on the Codiedev gateway, traced\n child = agents.run(\"summarize\", {\"text\": ...}) # start another agent as a child run\n agents.output({\"summary\": child[\"output\"]}) # this run's result\n agents.request() # webhook runs: method, headers, raw body\n agents.respond({\"text\": \"on it\"}) # what a webhook caller waiting (?wait=) gets back\n\n agents.state.get(\"cursor\") # per-agent storage that outlives runs\n agents.state.set(\"cursor\", 42)\n agents.step(\"fetch\", lambda: fetch()) # done once per run, even across retries\n agents.start(\"triage\", {...}, delay=3600) # start a run later (up to 7 days)\n agents.request_approval(\"Refund $120?\", context={...}) # a person decides; a run acts on it\n agents.attempt() # 1, or 2+ when this run is a retry\n\nOutside a run (a script, a notebook, another service), set CODIEDEV_TOKEN\n(your CLI token) and CODIEDEV_AGENTS_URL, then call agents.run() the same way.\nStandard library only, so it drops into any project.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport time\nimport urllib.error\nimport urllib.parse\nimport urllib.request\nfrom typing import Any\n\n__all__ = [\n \"input\", \"output\", \"request\", \"respond\", \"run\", \"start\", \"get\", \"openai\", \"attempt\",\n \"state\", \"step\", \"request_approval\", \"get_approval\", \"AgentRunError\", \"StateConflict\",\n]\n\n_INPUT_PATH = os.environ.get(\"CODIEDEV_INPUT_PATH\", \"/agent/.codiedev/input.json\")\n_OUTPUT_PATH = os.environ.get(\"CODIEDEV_OUTPUT_PATH\", \"/agent/.codiedev/output.json\")\n_REQUEST_PATH = os.environ.get(\"CODIEDEV_REQUEST_PATH\", \"/agent/.codiedev/request.json\")\n_RESPONSE_PATH = os.environ.get(\"CODIEDEV_RESPONSE_PATH\", \"/agent/.codiedev/response.json\")\n# Set by `codiedev agents run --local`: state lives in this file instead of Codiedev.\n_STATE_PATH = os.environ.get(\"CODIEDEV_STATE_PATH\")\n_DONE = {\"succeeded\", \"failed\", \"cancelled\"}\n\n\nclass StateConflict(RuntimeError):\n \"\"\"state.set(..., if_version=n) found the key at another version.\"\"\"\n\n def __init__(self, key: str, version: int):\n super().__init__(f\"state {key!r} changed (now version {version})\")\n self.key = key\n self.version = version\n\n\nclass AgentRunError(RuntimeError):\n def __init__(self, run: dict):\n super().__init__(f\"{run.get('agent')} run {run.get('id')} {run.get('status')}: {run.get('error') or 'no error message'}\")\n self.run = run\n\n\ndef input(default: Any = None) -> Any: # noqa: A001 - mirrors output()\n \"\"\"This run's input, parsed from JSON (None when started without one).\"\"\"\n try:\n with open(_INPUT_PATH) as f:\n value = json.load(f)\n except FileNotFoundError:\n return default\n return default if value is None else value\n\n\ndef output(value: Any) -> None:\n \"\"\"Set this run's result. Must be JSON-serializable; the last call wins.\"\"\"\n os.makedirs(os.path.dirname(_OUTPUT_PATH), exist_ok=True)\n with open(_OUTPUT_PATH, \"w\") as f:\n json.dump(value, f)\n\n\ndef respond(body: Any = None, status: int = 200, headers: \"dict | None\" = None) -> None:\n \"\"\"What a webhook caller gets back when it waits for the run (?wait=N):\n a string is sent as text, anything else as JSON. Without respond(), a\n waiting caller gets the run's output.\"\"\"\n os.makedirs(os.path.dirname(_RESPONSE_PATH), exist_ok=True)\n with open(_RESPONSE_PATH, \"w\") as f:\n json.dump({\"status\": status, \"headers\": headers or {}, \"body\": body}, f)\n\n\ndef attempt() -> int:\n \"\"\"This run's attempt: 1, or 2+ when it's a retry of a failed attempt.\"\"\"\n return int(os.environ.get(\"CODIEDEV_ATTEMPT\", \"1\"))\n\n\ndef request() -> \"dict | None\":\n \"\"\"For a webhook run: {\"method\", \"query\", \"headers\", \"body\"} with the raw\n body, to verify the sender's signature. None for other runs.\"\"\"\n try:\n with open(_REQUEST_PATH) as f:\n return json.load(f)\n except FileNotFoundError:\n return None\n\n\ndef _base() -> str:\n url = os.environ.get(\"CODIEDEV_AGENTS_URL\", \"\").rstrip(\"/\")\n if not url:\n raise RuntimeError(\"Set CODIEDEV_AGENTS_URL (it's set for you inside a run)\")\n return url\n\n\ndef _token() -> str:\n # Inside a run: the run's token. A connected agent (running outside Codiedev): its own key.\n # Otherwise: your CLI token.\n key = os.environ.get(\"CODIEDEV_API_KEY\", \"\")\n token = os.environ.get(\"CODIEDEV_RUN_TOKEN\") or (key if key.startswith(\"cdvm_\") else None) or os.environ.get(\"CODIEDEV_TOKEN\")\n if not token:\n raise RuntimeError(\"Set CODIEDEV_API_KEY to your connected agent's key, or CODIEDEV_TOKEN to your CLI token (inside a run it's set for you)\")\n return token\n\n\ndef _request(method: str, path: str, body: dict | None = None, ok: tuple[int, ...] = ()) -> dict:\n req = urllib.request.Request(\n f\"{_base()}{path}\",\n method=method,\n data=json.dumps(body).encode() if body is not None else None,\n headers={\"Authorization\": f\"Bearer {_token()}\", \"Content-Type\": \"application/json\"},\n )\n try:\n with urllib.request.urlopen(req, timeout=30) as res:\n return json.loads(res.read() or b\"{}\")\n except urllib.error.HTTPError as err:\n detail = err.read().decode(errors=\"replace\")\n if err.code in ok:\n return {\"_status\": err.code, **json.loads(detail or \"{}\")}\n try:\n detail = json.loads(detail).get(\"error\", detail)\n except ValueError:\n pass\n raise RuntimeError(f\"Codiedev agents API {err.code}: {detail}\") from None\n\n\ndef start(\n agent: str,\n input: Any = None, # noqa: A002\n idempotency_key: str | None = None,\n delay: float | None = None,\n retries: int | None = None,\n) -> str:\n \"\"\"Start a run and return its id without waiting. With an idempotency key,\n a second start within a day returns the first run's id. delay (seconds,\n up to 7 days) starts it later; retries overrides the agent's retries.\"\"\"\n body: dict = {\"agent\": agent, \"input\": input}\n if idempotency_key:\n body[\"idempotencyKey\"] = idempotency_key\n if delay:\n body[\"delaySec\"] = int(delay)\n if retries is not None:\n body[\"retries\"] = retries\n return _request(\"POST\", \"/api/agents/v1/run\", body)[\"runId\"]\n\n\ndef get(run_id: str) -> dict:\n \"\"\"A run's status, input, output (parsed), error, log tail and children.\"\"\"\n run = _request(\"GET\", f\"/api/agents/v1/run?id={run_id}\")\n if isinstance(run.get(\"output\"), str):\n try:\n run[\"output\"] = json.loads(run[\"output\"])\n except ValueError:\n pass\n return run\n\n\ndef run(\n agent: str,\n input: Any = None, # noqa: A002\n timeout: float = 900,\n poll: float = 2.0,\n check: bool = True,\n idempotency_key: str | None = None,\n) -> dict:\n \"\"\"Run an agent and wait for it. Returns the run; its result is run[\"output\"].\n\n Inside a run this starts a child run, so the chain shows as one tree.\n Raises AgentRunError if it fails (pass check=False to get the run anyway).\n \"\"\"\n run_id = start(agent, input, idempotency_key)\n deadline = time.monotonic() + timeout\n while True:\n result = get(run_id)\n if result.get(\"status\") in _DONE:\n if check and result[\"status\"] != \"succeeded\":\n raise AgentRunError(result)\n return result\n if time.monotonic() > deadline:\n raise TimeoutError(f\"{agent} run {run_id} still {result.get('status')} after {timeout}s\")\n time.sleep(poll)\n\n\nclass _State:\n \"\"\"Per-agent key-value storage that outlives runs (values: any JSON, up\n to 64 KB; 1,000 keys). Every write bumps the key's version; pass\n if_version to write only if nobody else did since you read it.\"\"\"\n\n def get(self, key: str, default: Any = None) -> Any:\n value, _ = self.get_versioned(key)\n return default if value is None else value\n\n def get_versioned(self, key: str) -> \"tuple[Any, int]\":\n \"\"\"(value, version); (None, 0) when the key isn't set.\"\"\"\n if _STATE_PATH:\n row = _local_state().get(key)\n return (row[\"value\"], row[\"version\"]) if row else (None, 0)\n res = _request(\"GET\", f\"/api/agents/v1/state?{urllib.parse.urlencode({'key': key, 'agent': _agent()})}\", ok=(404,))\n if res.get(\"_status\") == 404:\n return None, 0\n return json.loads(res[\"value\"]), int(res[\"version\"])\n\n def set(self, key: str, value: Any, if_version: \"int | None\" = None, ttl: \"float | None\" = None) -> int:\n \"\"\"Store value; returns the new version. Raises StateConflict when\n if_version is given and the key is at another version (0 = unset).\"\"\"\n return self._write(key, value, if_version, ttl)\n\n def delete(self, key: str, if_version: \"int | None\" = None) -> None:\n self._write(key, None, if_version, None)\n\n def list(self, prefix: str = \"\", values: bool = False) -> \"list[dict]\": # noqa: A003\n \"\"\"Keys (and with values=True, values) starting with prefix.\"\"\"\n if _STATE_PATH:\n rows = sorted((k, r) for k, r in _local_state().items() if k.startswith(prefix))\n return [{\"key\": k, \"version\": r[\"version\"], **({\"value\": r[\"value\"]} if values else {})} for k, r in rows]\n keys: list = []\n after = None\n while True:\n params = {\"prefix\": prefix, \"values\": \"1\" if values else \"0\", \"limit\": 200, \"agent\": _agent()}\n if after:\n params[\"after\"] = after\n page = _request(\"GET\", f\"/api/agents/v1/state?{urllib.parse.urlencode(params)}\")\n for row in page[\"keys\"]:\n if \"value\" in row:\n row[\"value\"] = json.loads(row[\"value\"])\n keys.extend(page[\"keys\"])\n after = page.get(\"next\")\n if not after:\n return keys\n\n def update(self, key: str, fn, default: Any = None, retries: int = 10) -> Any:\n \"\"\"Read-modify-write that's safe with runs in parallel: fn(old) -> new,\n retried if another run wrote first. Returns the new value.\"\"\"\n for _ in range(retries):\n value, version = self.get_versioned(key)\n new = fn(default if value is None else value)\n try:\n self.set(key, new, if_version=version)\n return new\n except StateConflict:\n time.sleep(0.05)\n raise StateConflict(key, -1)\n\n def _write(self, key: str, value: Any, if_version: \"int | None\", ttl: \"float | None\") -> int:\n if _STATE_PATH:\n data = _local_state()\n current = data.get(key, {}).get(\"version\", 0)\n if if_version is not None and if_version != current:\n raise StateConflict(key, current)\n if value is None:\n data.pop(key, None)\n else:\n data[key] = {\"value\": value, \"version\": current + 1}\n with open(_STATE_PATH, \"w\") as f:\n json.dump(data, f, indent=2)\n return 0 if value is None else current + 1\n body: dict = {\"key\": key, \"value\": value, \"agent\": _agent()}\n if if_version is not None:\n body[\"ifVersion\"] = if_version\n if ttl:\n body[\"ttlSec\"] = int(ttl)\n res = _request(\"POST\", \"/api/agents/v1/state\", body, ok=(409,))\n if res.get(\"_status\") == 409:\n raise StateConflict(key, int(res.get(\"version\", 0)))\n return int(res.get(\"version\", 0))\n\n\ndef _agent() -> str:\n # Inside a run the run token picks the agent; a CLI token needs it named.\n return os.environ.get(\"CODIEDEV_AGENT\", \"\")\n\n\ndef _local_state() -> dict:\n try:\n with open(_STATE_PATH or \"\") as f:\n return json.load(f)\n except (FileNotFoundError, ValueError):\n return {}\n\n\nstate = _State()\n\n\ndef step(name: str, fn, ttl: float = 7 * 86400) -> Any:\n \"\"\"Run fn() once for this run and remember its (JSON) result: if the run\n is retried after a failure, finished steps return their saved result\n instead of running again. Use it around side effects (posting a reply,\n charging a card) so a retry doesn't repeat them.\"\"\"\n key = f\"step/{os.environ.get('CODIEDEV_RUN_ID', 'local')}/{name}\"\n value, version = state.get_versioned(key)\n if version:\n return value[\"result\"]\n result = fn()\n state.set(key, {\"result\": result}, ttl=ttl)\n return result\n\n\n_SELF = object()\n\n\ndef request_approval(\n title: str,\n details: Any = None,\n options: \"list[str] | None\" = None,\n approvers: \"list[str] | None\" = None,\n teams: \"list[str] | None\" = None,\n fields: \"list[dict] | None\" = None,\n then: Any = _SELF,\n context: Any = None,\n expires_in: \"float | None\" = None,\n) -> dict:\n \"\"\"Ask a person to decide. It shows in Codiedev (Agents → Approvals) and\n at the returned [\"url\"], which you can post anywhere (Slack, email).\n\n When it's decided, a run of `then` (this agent by default; None for no\n run) starts with input {\"approval\": {\"decision\", \"comment\", \"decidedBy\",\n ...}, \"context\": context}. If nobody decides within expires_in seconds\n (default 7 days), it starts with decision \"expired\". Usually the run\n ends after asking, so nothing runs (or costs) while it waits.\n options default to [\"approve\", \"reject\"]; approvers (emails) and teams\n (team names, e.g. [\"Finance\"]) limit who can decide (admins always can).\n Apps your company connects (Agents → Approvals → Apps) are told about it\n by webhook and can decide on a person's behalf.\n\n fields asks the person to fill something in as they decide, e.g.\n [{\"name\": \"rating\", \"type\": \"rating\"}, {\"name\": \"why\", \"type\": \"textarea\"}]\n (types: text, textarea, number, rating, select, multiselect, boolean;\n required, label, help, options, min, max, default). What they entered\n arrives as input[\"approval\"][\"values\"]. With fields and no options, the\n only choice is \"submit\".\"\"\"\n body: dict = {\"title\": title, \"details\": details, \"options\": options, \"approvers\": approvers, \"teams\": teams, \"fields\": fields, \"context\": context}\n if then is not _SELF:\n body[\"onDecision\"] = then\n if expires_in:\n body[\"expiresSec\"] = int(expires_in)\n if os.environ.get(\"CODIEDEV_LOCAL\") == \"1\":\n print(f\"[local] approval requested: {title} ({', '.join(options or (['submit'] if fields else ['approve', 'reject']))}); not sent on a local run\", flush=True)\n return {\"id\": f\"local-{int(time.time())}\", \"status\": \"pending\", \"url\": None, \"title\": title}\n return _request(\"POST\", \"/api/agents/v1/approvals\", {k: v for k, v in body.items() if v is not None or k == \"onDecision\"})\n\n\ndef get_approval(approval_id: str) -> dict:\n \"\"\"An approval's status (\"pending\", \"decided\", \"expired\"), decision and comment.\"\"\"\n return _request(\"GET\", f\"/api/agents/v1/approvals?id={urllib.parse.quote(approval_id)}\")\n\n\ndef wait_for_approval(approval_id: str, timeout: float = 3600, poll: float = 15) -> dict:\n \"\"\"Wait until a person decides (or it expires), then return the approval. For agents that keep\n running while they wait, like a connected agent; a run inside Codiedev usually ends instead and\n is started again with the decision.\"\"\"\n deadline = time.time() + timeout\n while True:\n a = get_approval(approval_id)\n if a.get(\"status\") != \"pending\":\n return a\n if time.time() > deadline:\n raise TimeoutError(f\"approval {approval_id} still pending after {timeout}s\")\n time.sleep(poll)\n\n\ndef openai(**kwargs):\n \"\"\"An OpenAI client on the Codiedev gateway, with trace headers for this run.\"\"\"\n from openai import OpenAI\n\n headers = {\n \"X-Codiedev-Workload\": os.environ.get(\"CODIEDEV_AGENT\", \"local-agent\"),\n \"X-Codiedev-Run-Id\": os.environ.get(\"CODIEDEV_RUN_ID\", f\"local-{int(time.time())}\"),\n \"X-Codiedev-Environment\": \"production\" if os.environ.get(\"CODIEDEV_RUN_ID\") else \"development\",\n }\n headers.update(kwargs.pop(\"default_headers\", {}) or {})\n return OpenAI(\n base_url=os.environ.get(\"CODIEDEV_GATEWAY_URL\", \"https://gateway.codiedev.com/api/v1\"),\n api_key=os.environ.get(\"CODIEDEV_API_KEY\") or os.environ.get(\"CODIEDEV_TOKEN\"),\n default_headers=headers,\n **kwargs,\n )\n";
5
+ exports.jsIndex = "// Codiedev agents SDK for JavaScript and TypeScript, preinstalled in every\n// run. Same primitive as the Python SDK (codiedev_agents.py):\n//\n// import * as agents from \"codiedev-agents\";\n// const task = agents.input(); // this run's input (JSON)\n// const llm = await agents.openai(); // OpenAI client on the Codiedev gateway, traced\n// const child = await agents.run(\"summarize\", { text }); // another agent, as a child run\n// agents.output({ summary: child.output }); // this run's result\n// agents.respond({ text: \"on it\" }); // what a webhook caller waiting (?wait=) gets back\n// await agents.state.get(\"cursor\"); // per-agent storage that outlives runs\n// await agents.step(\"post\", () => post()); // done once per run, even across retries\n// await agents.start(\"triage\", input, { delaySec: 3600 }); // start a run later\n// await agents.requestApproval(\"Refund $120?\", { context }); // a person decides; a run acts on it\n//\n// Outside a run, set CODIEDEV_TOKEN and CODIEDEV_AGENTS_URL and call run() the same way.\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nconst INPUT_PATH = process.env.CODIEDEV_INPUT_PATH || \"/agent/.codiedev/input.json\";\nconst OUTPUT_PATH = process.env.CODIEDEV_OUTPUT_PATH || \"/agent/.codiedev/output.json\";\nconst REQUEST_PATH = process.env.CODIEDEV_REQUEST_PATH || \"/agent/.codiedev/request.json\";\nconst RESPONSE_PATH = process.env.CODIEDEV_RESPONSE_PATH || \"/agent/.codiedev/response.json\";\n// Set by `codiedev agents run --local`: state lives in this file instead of Codiedev.\nconst STATE_PATH = process.env.CODIEDEV_STATE_PATH;\n// Inside a run the run token picks the agent; a CLI token needs it named.\nconst AGENT = process.env.CODIEDEV_AGENT || \"\";\nconst DONE = new Set([\"succeeded\", \"failed\", \"cancelled\"]);\n\n/** state.set(..., { ifVersion }) found the key at another version. */\nexport class StateConflict extends Error {\n constructor(key, version) {\n super(`state ${JSON.stringify(key)} changed (now version ${version})`);\n this.key = key;\n this.version = version;\n }\n}\n\nexport class AgentRunError extends Error {\n constructor(run) {\n super(`${run.agent} run ${run.id} ${run.status}: ${run.error || \"no error message\"}`);\n this.run = run;\n }\n}\n\nfunction readJson(file) {\n try {\n return JSON.parse(fs.readFileSync(file, \"utf8\"));\n } catch (err) {\n if (err.code === \"ENOENT\") return null;\n throw err;\n }\n}\n\n/** This run's input, parsed from JSON (`fallback` when started without one). */\nexport function input(fallback = null) {\n const value = readJson(INPUT_PATH);\n return value ?? fallback;\n}\n\n/** Set this run's result. Must be JSON-serializable; the last call wins. */\nexport function output(value) {\n fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });\n fs.writeFileSync(OUTPUT_PATH, JSON.stringify(value));\n}\n\n/** What a webhook caller gets back when it waits for the run (?wait=N): a\n * string is sent as text, anything else as JSON. Without it, a waiting\n * caller gets the run's output. */\nexport function respond(body = null, { status = 200, headers = {} } = {}) {\n fs.mkdirSync(path.dirname(RESPONSE_PATH), { recursive: true });\n fs.writeFileSync(RESPONSE_PATH, JSON.stringify({ status, headers, body }));\n}\n\n/** This run's attempt: 1, or 2+ when it's a retry of a failed attempt. */\nexport function attempt() {\n return Number(process.env.CODIEDEV_ATTEMPT || \"1\");\n}\n\n/** For a webhook run: { method, query, headers, body } with the raw body, to verify a signature. */\nexport function request() {\n return readJson(REQUEST_PATH);\n}\n\nfunction base() {\n const url = (process.env.CODIEDEV_AGENTS_URL || \"\").replace(/\\/$/, \"\");\n if (!url) throw new Error(\"Set CODIEDEV_AGENTS_URL (it's set for you inside a run)\");\n return url;\n}\n\nfunction token() {\n // Inside a run: the run's token. A connected agent (running outside Codiedev): its own key.\n // Otherwise: your CLI token.\n const key = process.env.CODIEDEV_API_KEY || \"\";\n const t = process.env.CODIEDEV_RUN_TOKEN || (key.startsWith(\"cdvm_\") ? key : \"\") || process.env.CODIEDEV_TOKEN;\n if (!t) throw new Error(\"Set CODIEDEV_API_KEY to your connected agent's key, or CODIEDEV_TOKEN to your CLI token (inside a run it's set for you)\");\n return t;\n}\n\nasync function call(method, route, body, ok = []) {\n const res = await fetch(`${base()}${route}`, {\n method,\n headers: { Authorization: `Bearer ${token()}`, \"Content-Type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n const text = await res.text();\n let data = {};\n try {\n data = text ? JSON.parse(text) : {};\n } catch {\n data = { error: text };\n }\n if (ok.includes(res.status)) return { _status: res.status, ...data };\n if (!res.ok) throw new Error(`Codiedev agents API ${res.status}: ${data.error || text}`);\n return data;\n}\n\n/** Start a run and return its id without waiting. delaySec (up to 7 days)\n * starts it later; retries overrides the agent's retries. */\nexport async function start(agent, input = null, { idempotencyKey, delaySec, retries } = {}) {\n return (await call(\"POST\", \"/api/agents/v1/run\", {\n agent,\n input,\n ...(idempotencyKey ? { idempotencyKey } : {}),\n ...(delaySec ? { delaySec } : {}),\n ...(retries !== undefined ? { retries } : {}),\n })).runId;\n}\n\nfunction localState() {\n try {\n return JSON.parse(fs.readFileSync(STATE_PATH, \"utf8\"));\n } catch {\n return {};\n }\n}\n\nasync function writeState(key, value, { ifVersion, ttlSec } = {}) {\n if (STATE_PATH) {\n const data = localState();\n const current = data[key]?.version ?? 0;\n if (ifVersion !== undefined && ifVersion !== current) throw new StateConflict(key, current);\n if (value === null) delete data[key];\n else data[key] = { value, version: current + 1 };\n fs.writeFileSync(STATE_PATH, JSON.stringify(data, null, 2));\n return value === null ? 0 : current + 1;\n }\n const res = await call(\"POST\", \"/api/agents/v1/state\", { key, value, agent: AGENT, ...(ifVersion !== undefined ? { ifVersion } : {}), ...(ttlSec ? { ttlSec } : {}) }, [409]);\n if (res._status === 409) throw new StateConflict(key, res.version ?? 0);\n return res.version ?? 0;\n}\n\n/** Per-agent key-value storage that outlives runs (values: any JSON, up to\n * 64 KB; 1,000 keys). Every write bumps the key's version; pass ifVersion\n * to write only if nobody else did since you read it. */\nexport const state = {\n async get(key, fallback = null) {\n const { value } = await state.getVersioned(key);\n return value ?? fallback;\n },\n /** { value, version }; { value: null, version: 0 } when the key isn't set. */\n async getVersioned(key) {\n if (STATE_PATH) {\n const row = localState()[key];\n return row ? { value: row.value, version: row.version } : { value: null, version: 0 };\n }\n const res = await call(\"GET\", `/api/agents/v1/state?${new URLSearchParams({ key, agent: AGENT })}`, undefined, [404]);\n if (res._status === 404) return { value: null, version: 0 };\n return { value: JSON.parse(res.value), version: res.version };\n },\n /** Store value; returns the new version. Throws StateConflict on an ifVersion mismatch (0 = unset). */\n set(key, value, { ifVersion, ttlSec } = {}) {\n return writeState(key, value, { ifVersion, ttlSec });\n },\n async delete(key, { ifVersion } = {}) {\n await writeState(key, null, { ifVersion });\n },\n /** Keys (and with values: true, values) starting with prefix. */\n async list(prefix = \"\", { values = false } = {}) {\n if (STATE_PATH) {\n return Object.entries(localState())\n .filter(([k]) => k.startsWith(prefix))\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, row]) => ({ key, version: row.version, ...(values ? { value: row.value } : {}) }));\n }\n const out = [];\n let after = null;\n do {\n const q = new URLSearchParams({ prefix, values: values ? \"1\" : \"0\", limit: \"200\", agent: AGENT, ...(after ? { after } : {}) });\n const page = await call(\"GET\", `/api/agents/v1/state?${q}`);\n out.push(...page.keys.map((row) => (\"value\" in row ? { ...row, value: JSON.parse(row.value) } : row)));\n after = page.next;\n } while (after);\n return out;\n },\n /** Read-modify-write that's safe with runs in parallel: fn(old) => new, retried if another run wrote first. */\n async update(key, fn, fallback = null, retries = 10) {\n for (let i = 0; i < retries; i++) {\n const { value, version } = await state.getVersioned(key);\n const next = await fn(value ?? fallback);\n try {\n await state.set(key, next, { ifVersion: version });\n return next;\n } catch (err) {\n if (!(err instanceof StateConflict)) throw err;\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n }\n throw new StateConflict(key, -1);\n },\n};\n\n/** Run fn() once for this run and remember its (JSON) result: if the run is\n * retried after a failure, finished steps return their saved result instead\n * of running again. Use it around side effects so a retry doesn't repeat them. */\nexport async function step(name, fn, { ttlSec = 7 * 86400 } = {}) {\n const key = `step/${process.env.CODIEDEV_RUN_ID || \"local\"}/${name}`;\n const { value, version } = await state.getVersioned(key);\n if (version) return value.result;\n const result = await fn();\n await state.set(key, { result: result ?? null }, { ttlSec });\n return result;\n}\n\n/** Ask a person to decide. It shows in Codiedev (Agents → Approvals) and at\n * the returned .url, which you can post anywhere. When it's decided, a run of\n * `then` (this agent by default; null for none) starts with input\n * { approval: { decision, comment, decidedBy, ... }, context }. Undecided\n * after expiresSec (default 7 days): decision \"expired\". */\nexport async function requestApproval(title, { details, options, approvers, teams, fields, then, context, expiresSec } = {}) {\n if (process.env.CODIEDEV_LOCAL === \"1\") {\n console.log(`[local] approval requested: ${title} (${(options || (fields ? [\"submit\"] : [\"approve\", \"reject\"])).join(\", \")}); not sent on a local run`);\n return { id: `local-${Date.now()}`, status: \"pending\", url: null, title };\n }\n return call(\"POST\", \"/api/agents/v1/approvals\", {\n title,\n ...(details !== undefined ? { details } : {}),\n ...(options ? { options } : {}),\n ...(approvers ? { approvers } : {}),\n ...(teams ? { teams } : {}),\n ...(fields ? { fields } : {}),\n ...(then !== undefined ? { onDecision: then } : {}),\n ...(context !== undefined ? { context } : {}),\n ...(expiresSec ? { expiresSec } : {}),\n });\n}\n\n/** An approval's status (\"pending\", \"decided\", \"expired\"), decision and comment. */\nexport async function getApproval(id) {\n return call(\"GET\", `/api/agents/v1/approvals?id=${encodeURIComponent(id)}`);\n}\n\n/** Wait until a person decides (or it expires), then return the approval. For agents that keep\n * running while they wait, like a connected agent; a run inside Codiedev usually ends instead and\n * is started again with the decision. */\nexport async function waitForApproval(id, { timeoutMs = 3_600_000, pollMs = 15_000 } = {}) {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const a = await getApproval(id);\n if (a.status !== \"pending\") return a;\n if (Date.now() > deadline) throw new Error(`approval ${id} still pending after ${timeoutMs / 1000}s`);\n await new Promise((resolve) => setTimeout(resolve, pollMs));\n }\n}\n\n/** A run's status, input, output (parsed), error, log and children. */\nexport async function get(runId) {\n const run = await call(\"GET\", `/api/agents/v1/run?id=${encodeURIComponent(runId)}`);\n if (typeof run.output === \"string\") {\n try {\n run.output = JSON.parse(run.output);\n } catch {\n // not JSON: keep the string\n }\n }\n return run;\n}\n\n/** Run an agent and wait for it; its result is `.output`. Inside a run this\n * starts a child run. Throws AgentRunError if it fails unless check is false. */\nexport async function run(agent, input = null, { timeoutMs = 900_000, pollMs = 2000, check = true, idempotencyKey } = {}) {\n const id = await start(agent, input, { idempotencyKey });\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const r = await get(id);\n if (DONE.has(r.status)) {\n if (check && r.status !== \"succeeded\") throw new AgentRunError(r);\n return r;\n }\n if (Date.now() > deadline) throw new Error(`${agent} run ${id} still ${r.status} after ${timeoutMs / 1000}s`);\n await new Promise((resolve) => setTimeout(resolve, pollMs));\n }\n}\n\n/** Options for `new OpenAI(...)` on the Codiedev gateway, with this run's trace headers. */\nexport function openaiOptions(extra = {}) {\n const inRun = !!process.env.CODIEDEV_RUN_ID;\n return {\n baseURL: process.env.CODIEDEV_GATEWAY_URL || \"https://gateway.codiedev.com/api/v1\",\n apiKey: process.env.CODIEDEV_API_KEY || process.env.CODIEDEV_TOKEN,\n ...extra,\n defaultHeaders: {\n \"X-Codiedev-Workload\": process.env.CODIEDEV_AGENT || \"local-agent\",\n \"X-Codiedev-Run-Id\": process.env.CODIEDEV_RUN_ID || `local-${Date.now()}`,\n \"X-Codiedev-Environment\": inRun ? \"production\" : \"development\",\n ...(extra.defaultHeaders || {}),\n },\n };\n}\n\n/** An OpenAI client on the Codiedev gateway (the `openai` package is preinstalled). */\nexport async function openai(extra = {}) {\n const { default: OpenAI } = await import(\"openai\");\n return new OpenAI(openaiOptions(extra));\n}\n";
6
+ exports.jsTypes = "/* eslint-disable @typescript-eslint/no-explicit-any -- defaults are `any` on purpose: agent code reads its own JSON input/state without declaring types first. */\nexport type RunStatus = \"queued\" | \"running\" | \"succeeded\" | \"failed\" | \"cancelled\";\nexport interface Run<O = unknown> {\n id: string;\n agent: string;\n version: number;\n status: RunStatus;\n trigger: \"cli\" | \"api\" | \"portal\" | \"schedule\" | \"webhook\" | \"child\" | \"approval\";\n parentRunId: string | null;\n rootRunId: string | null;\n depth: number;\n attempt: number;\n maxAttempts: number;\n scheduledFor: number | null;\n input: string | null;\n output: O;\n error: string | null;\n logTail: string | null;\n durationMs: number | null;\n children: Run[];\n}\nexport interface WebhookRequest { method: string; query: Record<string, string>; headers: Record<string, string>; body: string }\nexport interface Approval {\n id: string;\n agent: string;\n title: string;\n details: string | null;\n options: string[];\n status: \"pending\" | \"decided\" | \"expired\" | \"cancelled\";\n decision: string | null;\n comment: string | null;\n decidedBy: string | null;\n /** The approval app that sent the decision on decidedBy's behalf; null when decided in Codiedev. */\n decidedVia: string | null;\n approvers: string[];\n approverTeams: Array<{ id: string; name: string }>;\n /** The form the person fills in, if the approval asked for one. */\n fields: ApprovalField[];\n /** What they entered, by field name (null until decided). */\n values: Record<string, any> | null;\n url: string | null;\n resumeRunId: string | null;\n}\n/** One thing a person fills in when they decide. */\nexport interface ApprovalField {\n name: string;\n type: \"text\" | \"textarea\" | \"number\" | \"rating\" | \"select\" | \"multiselect\" | \"boolean\";\n label?: string;\n help?: string;\n required?: boolean;\n /** select / multiselect choices */\n options?: string[];\n /** number bounds; rating scale 1–max (default 5) */\n min?: number;\n max?: number;\n /** prefilled, e.g. a proposed amount for the person to correct */\n default?: unknown;\n}\n/** The input a run started by a decision gets. */\nexport interface ApprovalInput<C = any, V = Record<string, any>> {\n approval: { id: string; title: string; decision: string; comment: string | null; values: V; decidedBy: string | null; decidedVia: string | null; decidedAt: number; requestedByRun: string | null };\n context: C;\n}\nexport class AgentRunError extends Error { run: Run }\nexport class StateConflict extends Error { key: string; version: number }\nexport function input<T = any>(fallback?: T): T;\nexport function output(value: unknown): void;\nexport function request(): WebhookRequest | null;\nexport function respond(body?: unknown, opts?: { status?: number; headers?: Record<string, string> }): void;\nexport function attempt(): number;\nexport function start(agent: string, input?: unknown, opts?: { idempotencyKey?: string; delaySec?: number; retries?: number }): Promise<string>;\nexport function get<O = any>(runId: string): Promise<Run<O>>;\nexport function run<O = any>(agent: string, input?: unknown, opts?: { timeoutMs?: number; pollMs?: number; check?: boolean; idempotencyKey?: string }): Promise<Run<O>>;\nexport const state: {\n get<T = any>(key: string, fallback?: T): Promise<T>;\n getVersioned<T = any>(key: string): Promise<{ value: T | null; version: number }>;\n set(key: string, value: unknown, opts?: { ifVersion?: number; ttlSec?: number }): Promise<number>;\n delete(key: string, opts?: { ifVersion?: number }): Promise<void>;\n list(prefix?: string, opts?: { values?: boolean }): Promise<Array<{ key: string; version: number; value?: any }>>;\n update<T = any>(key: string, fn: (old: T) => T | Promise<T>, fallback?: T, retries?: number): Promise<T>;\n};\nexport function step<T>(name: string, fn: () => T | Promise<T>, opts?: { ttlSec?: number }): Promise<T>;\nexport function requestApproval<C = any>(title: string, opts?: { details?: unknown; options?: string[]; approvers?: string[]; teams?: string[]; fields?: ApprovalField[]; then?: string | null; context?: C; expiresSec?: number }): Promise<Approval>;\nexport function getApproval(id: string): Promise<Approval>;\nexport function waitForApproval(id: string, opts?: { timeoutMs?: number; pollMs?: number }): Promise<Approval>;\nexport function openaiOptions(extra?: Record<string, unknown>): { baseURL: string; apiKey: string | undefined; defaultHeaders: Record<string, string> };\nexport function openai(extra?: Record<string, unknown>): Promise<import(\"openai\").default>;";
7
7
  exports.jsPackage = "{\n \"name\": \"codiedev-agents\",\n \"version\": \"0.1.0\",\n \"description\": \"Codiedev agents SDK: this run's input and output, chaining, and a traced OpenAI client.\",\n \"type\": \"module\",\n \"main\": \"./index.mjs\",\n \"types\": \"./index.d.ts\",\n \"exports\": { \".\": { \"types\": \"./index.d.ts\", \"default\": \"./index.mjs\" } }\n}\n";
package/dist/connect.js CHANGED
@@ -41,12 +41,22 @@ const crypto = __importStar(require("crypto"));
41
41
  const os = __importStar(require("os"));
42
42
  const path = __importStar(require("path"));
43
43
  const utils_1 = require("./utils");
44
+ const cliSession_1 = require("./cliSession");
44
45
  const connectFlow_1 = require("./connectFlow");
45
46
  const version_1 = require("./version");
46
47
  const upgradeNudge_1 = require("./upgradeNudge");
47
48
  const shared_1 = require("./commands/shared");
48
49
  const harnessDirs_1 = require("./harnessDirs");
49
50
  const BACKEND_URL = process.env.CODIEDEV_URL || "https://cli.codiedev.com";
51
+ function prompt(question) {
52
+ return new Promise((resolve) => {
53
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
54
+ rl.question(question, (answer) => {
55
+ rl.close();
56
+ resolve(answer);
57
+ });
58
+ });
59
+ }
50
60
  function promptHidden(question) {
51
61
  return new Promise((resolve) => {
52
62
  const stdin = process.stdin;
@@ -143,16 +153,39 @@ function postJson(url, body, extraHeaders = {}) {
143
153
  }
144
154
  async function runConnect(args = []) {
145
155
  const force = args.includes("--force") || args.includes("-f");
156
+ const pasteToken = args.includes("--token");
157
+ const useDevice = args.includes("--no-browser") || (!args.includes("--browser") && (0, cliSession_1.isHeadless)());
146
158
  console.log("\nWelcome to Codiedev CLI\n");
147
159
  await (0, upgradeNudge_1.maybePrintUpgradeNudge)();
148
160
  console.log("This will connect your coding agent to your Codiedev workspace and install");
149
161
  console.log("the session capture hook for org-wide session sharing.\n");
162
+ await (0, cliSession_1.ensureFreshToken)({ configPath: utils_1.CONFIG_PATH }).catch(() => undefined);
150
163
  const saved = (0, utils_1.readConfig)();
151
- const decision = (0, connectFlow_1.resolveConnectToken)({ savedConfig: saved, force });
164
+ const decision = (0, connectFlow_1.resolveConnectToken)({ savedConfig: saved, force: force || pasteToken });
152
165
  let token;
166
+ let auth;
153
167
  if (decision.source === "reused") {
154
168
  token = decision.token;
155
- console.log(`Reusing API token from ~/.codiedev/config.json (run with --force to use a new one).\n`);
169
+ auth = saved?.auth;
170
+ console.log(auth
171
+ ? `Reusing your browser sign-in from ~/.codiedev/config.json (run with --force to sign in again).\n`
172
+ : `Reusing API token from ~/.codiedev/config.json (run with --force to sign in again).\n`);
173
+ }
174
+ else if (!pasteToken) {
175
+ if (saved?.auth)
176
+ await (0, cliSession_1.logout)({ configPath: utils_1.CONFIG_PATH }).catch(() => undefined);
177
+ const result = await (0, connectFlow_1.signInWithTokenFallback)({
178
+ signIn: () => useDevice ? (0, cliSession_1.deviceLogin)({ backendUrl: BACKEND_URL }) : (0, cliSession_1.browserLogin)({ backendUrl: BACKEND_URL }),
179
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
180
+ askYesNo: async (q) => /^y(es)?$/i.test((await prompt(q)).trim()),
181
+ askToken: promptHidden,
182
+ log: (line) => console.log(line),
183
+ error: (line) => console.error(line),
184
+ });
185
+ if (!result)
186
+ process.exit(1);
187
+ token = result.token;
188
+ auth = result.auth;
156
189
  }
157
190
  else {
158
191
  token = await promptHidden("Enter your Codiedev API token: ");
@@ -189,6 +222,7 @@ async function runConnect(args = []) {
189
222
  deviceId: saved?.deviceId || crypto.randomUUID(),
190
223
  deviceName: saved?.deviceName || os.hostname(),
191
224
  captureIgnoredPaths: saved?.captureIgnoredPaths ?? [],
225
+ ...(auth ? { auth } : {}),
192
226
  };
193
227
  try {
194
228
  (0, utils_1.writeConfig)(config);
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveConnectToken = resolveConnectToken;
4
+ exports.signInWithTokenFallback = signInWithTokenFallback;
4
5
  function resolveConnectToken(input) {
5
6
  if (input.force)
6
7
  return { source: "prompt-required" };
@@ -10,3 +11,37 @@ function resolveConnectToken(input) {
10
11
  return { source: "prompt-required" };
11
12
  return { source: "reused", token: input.savedConfig.token };
12
13
  }
14
+ async function signInWithTokenFallback(deps) {
15
+ deps.log("Prefer an API token? Press Ctrl+C and run `codiedev connect --token`.\n");
16
+ try {
17
+ const s = await deps.signIn();
18
+ deps.log("Signed in. No API key is stored on this machine.\n");
19
+ return {
20
+ token: s.accessToken,
21
+ auth: {
22
+ kind: "browser",
23
+ clientId: s.clientId,
24
+ refreshToken: s.refreshToken,
25
+ accessTokenExpiresAt: s.expiresAt,
26
+ },
27
+ };
28
+ }
29
+ catch (err) {
30
+ deps.error(`\nBrowser sign-in didn't finish: ${err.message}`);
31
+ if (!deps.interactive) {
32
+ deps.error("To paste an API token instead, run `codiedev connect --token`.");
33
+ return null;
34
+ }
35
+ if (!(await deps.askYesNo("Paste an API token instead? [y/N] "))) {
36
+ deps.error("Run `codiedev connect` to try again, or `codiedev connect --token` to paste a token.");
37
+ return null;
38
+ }
39
+ deps.log("Create one at https://codiedev.com/portal/integrations/claude-code\n");
40
+ const token = (await deps.askToken("Enter your Codiedev API token: ")).trim();
41
+ if (!token) {
42
+ deps.error("Error: API token is required.");
43
+ return null;
44
+ }
45
+ return { token };
46
+ }
47
+ }
package/dist/hook.js CHANGED
@@ -58,6 +58,8 @@ const captureExclusions_1 = require("./captureExclusions");
58
58
  const cursorHooks_1 = require("./cursorHooks");
59
59
  const copilotHooks_1 = require("./copilotHooks");
60
60
  const piHooks_1 = require("./piHooks");
61
+ const cliSession_1 = require("./cliSession");
62
+ const utils_2 = require("./utils");
61
63
  function buildDiscoveryContext(skills) {
62
64
  if (skills.length === 0)
63
65
  return null;
@@ -380,6 +382,7 @@ function resolveMode(arg) {
380
382
  return "claude-code";
381
383
  }
382
384
  async function main() {
385
+ await (0, cliSession_1.ensureFreshToken)({ configPath: utils_2.CONFIG_PATH }).catch(() => undefined);
383
386
  function makeUploadDeps() {
384
387
  return {
385
388
  post: postJson,
package/dist/mcp.js CHANGED
@@ -41,6 +41,7 @@ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
41
41
  const fs = __importStar(require("fs"));
42
42
  const path = __importStar(require("path"));
43
43
  const utils_1 = require("./utils");
44
+ const cliSession_1 = require("./cliSession");
44
45
  const shared_1 = require("./commands/shared");
45
46
  const inboxFormat_1 = require("./inboxFormat");
46
47
  const PKG_NAME = "codiedev";
@@ -375,6 +376,7 @@ function emitTelemetry(config, event) {
375
376
  });
376
377
  }
377
378
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
379
+ await (0, cliSession_1.ensureFreshToken)({ configPath: utils_1.CONFIG_PATH }).catch(() => undefined);
378
380
  const config = (0, utils_1.readConfig)();
379
381
  if (!config) {
380
382
  return {
package/dist/utils.js CHANGED
@@ -152,6 +152,7 @@ const piHooks_1 = require("./piHooks");
152
152
  var piHooks_2 = require("./piHooks");
153
153
  Object.defineProperty(exports, "inspectPiExtension", { enumerable: true, get: function () { return piHooks_2.inspectPiExtension; } });
154
154
  Object.defineProperty(exports, "piExtensionPath", { enumerable: true, get: function () { return piHooks_2.piExtensionPath; } });
155
+ const cliSession_1 = require("./cliSession");
155
156
  const CONFIG_DIR = (0, configDir_1.codiedevStateDir)();
156
157
  exports.CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
157
158
  function readConfig() {
@@ -172,11 +173,7 @@ function writeConfigAtPath(config, configDir, configPath) {
172
173
  if (!fs.existsSync(configDir)) {
173
174
  fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
174
175
  }
175
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), {
176
- encoding: "utf8",
177
- mode: 0o600,
178
- });
179
- fs.chmodSync(configPath, 0o600);
176
+ (0, cliSession_1.writeConfigAtomic)(configPath, config);
180
177
  }
181
178
  function parseGitRemoteFromConfig(configText) {
182
179
  const remotes = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.10.0-beta.6",
3
+ "version": "0.10.0-beta.8",
4
4
  "description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration \u2014 and run the governed Codie harness",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",