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 +57 -9
- package/dist/cliSession.js +456 -0
- package/dist/codie.js +8 -0
- package/dist/commands/agents.js +25 -3
- package/dist/commands/agentsSdk.generated.js +3 -3
- package/dist/connect.js +36 -2
- package/dist/connectFlow.js +35 -0
- package/dist/hook.js +3 -0
- package/dist/mcp.js +2 -0
- package/dist/utils.js +2 -5
- package/package.json +1 -1
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
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
423
|
+
## Signing in
|
|
399
424
|
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
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 ||
|
package/dist/commands/agents.js
CHANGED
|
@@ -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;
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
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);
|
package/dist/connectFlow.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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",
|