codiedev 0.9.78 → 0.9.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ import {
10
10
  trackWorkPath,
11
11
  workDirBySession,
12
12
  } from "./git-refs.mjs";
13
+ import { freshGatewayKey } from "./session-key.mjs";
13
14
 
14
15
  const skillUseBySession = new Map();
15
16
 
@@ -60,6 +61,11 @@ export const CodiedevMetadata = async () => ({
60
61
  });
61
62
  },
62
63
  "chat.headers": async (input, output) => {
64
+ // Browser sign-in: the current 24h token, refreshed as it nears expiry,
65
+ // so a session open for days keeps working (see session-key.mjs). Exact
66
+ // "Authorization" key: per-call headers win over the apiKey-derived one.
67
+ const key = await freshGatewayKey();
68
+ if (key) output.headers.Authorization = `Bearer ${key}`;
63
69
  Object.assign(
64
70
  output.headers,
65
71
  buildAttributionHeaders({
@@ -0,0 +1,48 @@
1
+ // The gateway key for one model request, kept fresh for the whole session.
2
+ //
3
+ // With browser sign-in (`codiedev connect`), the key is a 24h access token.
4
+ // The engine (OpenCode, pi) would otherwise hold the one it was launched
5
+ // with until the session ends, so a session open for more than a day would
6
+ // start failing. `codie` sets these variables only for a browser sign-in:
7
+ // CODIEDEV_SESSION_CONFIG path to ~/.codiedev/config.json
8
+ // CODIEDEV_CLI_JS the CLI entry (dist/cli.js)
9
+ // CODIEDEV_NODE the node binary that runs it
10
+ // Each request reads the current token from config (other codiedev
11
+ // processes may have refreshed it). When it has under 30 minutes left, this
12
+ // runs `codiedev session-refresh`, which refreshes under the shared lock
13
+ // (cliSession.ts, specs/tla/CliSession.tla), then reads again.
14
+ // No variables (pasted API token, CI): returns null and the launch key stands.
15
+
16
+ import { execFile } from "node:child_process";
17
+ import { readFileSync } from "node:fs";
18
+
19
+ export const REFRESH_WHEN_LEFT_MS = 30 * 60_000;
20
+
21
+ function readSession(path) {
22
+ try {
23
+ const c = JSON.parse(readFileSync(path, "utf8"));
24
+ return c && c.auth && typeof c.token === "string" && c.token ? c : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function runRefresh(env) {
31
+ return new Promise((resolve) => {
32
+ execFile(env.CODIEDEV_NODE, [env.CODIEDEV_CLI_JS, "session-refresh"], { timeout: 30_000, env }, () => resolve());
33
+ });
34
+ }
35
+
36
+ /** Fresh gateway key, or null when the session isn't a browser sign-in. */
37
+ export async function freshGatewayKey(env = process.env, now = Date.now, refresh = runRefresh) {
38
+ const path = env.CODIEDEV_SESSION_CONFIG;
39
+ if (!path || !env.CODIEDEV_CLI_JS || !env.CODIEDEV_NODE) return null;
40
+ let session = readSession(path);
41
+ if (!session) return null;
42
+ const left = Number(session.auth.accessTokenExpiresAt) - now();
43
+ if (!(left > REFRESH_WHEN_LEFT_MS)) {
44
+ await refresh(env);
45
+ session = readSession(path) ?? session;
46
+ }
47
+ return session.token;
48
+ }
package/dist/cli.js CHANGED
@@ -15,6 +15,8 @@
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  const connect_1 = require("./connect");
18
+ const cliSession_1 = require("./cliSession");
19
+ const utils_1 = require("./utils");
18
20
  const push_1 = require("./commands/push");
19
21
  const delete_1 = require("./commands/delete");
20
22
  const pull_1 = require("./commands/pull");
@@ -45,8 +47,10 @@ Codiedev CLI
45
47
  someone left off across sessions and providers.
46
48
 
47
49
  Connect:
48
- codiedev connect Link Claude Code / Codex / Cursor /
50
+ codiedev connect Sign in with your browser and link
51
+ Claude Code / Codex / Cursor /
49
52
  GitHub Copilot / pi to your org
53
+ codiedev logout Sign this machine out
50
54
  codiedev doctor Verify your setup after connecting
51
55
 
52
56
  Harness:
@@ -95,11 +99,14 @@ Starter packs:
95
99
 
96
100
  Traces (agents on the gateway):
97
101
  codiedev traces agents Agents you maintain, with saved test cases
98
- codiedev traces cases <agent> [--dataset name] [--model id]
102
+ codiedev traces cases <agent> [--dataset name [--version n]] [--model id]
99
103
  Pull an agent's test cases (or one dataset)
100
104
  into this repo for your coding agent to
101
105
  run, optionally on another model
102
106
  codiedev traces report <results.json> Send the results back to Traces
107
+ codiedev traces overview | runs | run | score | scores | score-kinds | queue | judge | import | add-example | versions | save-version
108
+ Agents, runs, failures, scores, review queues,
109
+ automatic scoring and datasets (codiedev traces help)
103
110
  codiedev evals report <run.json> File one variant's eval scores under an experiment
104
111
  codiedev evals last <subject> Latest experiment on a subject + its champion
105
112
 
@@ -169,8 +176,9 @@ const COMMAND_HELP = {
169
176
  connect: `
170
177
  codiedev connect — link your agent CLI to Codiedev
171
178
 
172
- Interactive setup. Prompts for an API token, then installs:
173
- - ~/.codiedev/config.json (token + org + tracked repos)
179
+ Opens your browser to sign in (approve once, no API key to copy), then
180
+ installs:
181
+ - ~/.codiedev/config.json (sign-in + org + tracked repos)
174
182
  - ~/.claude/settings.json hook (captures session transcripts)
175
183
  - ~/.claude/CLAUDE.md instructions (teaches Claude the commands)
176
184
  - ~/.codex/hooks.json hook (same, for Codex)
@@ -183,7 +191,27 @@ codiedev connect — link your agent CLI to Codiedev
183
191
 
184
192
  Re-run anytime to swap accounts or refresh the instruction block.
185
193
 
186
- Get your token: https://codiedev.com/portal/integrations/claude-code
194
+ Over SSH or on a machine with no display it shows a link and a short code
195
+ instead: open the link on your laptop or phone, check the code, approve.
196
+ Detected automatically.
197
+
198
+ Options:
199
+ --token Paste an API token instead of signing in with a browser
200
+ (CI, or a machine with no browser anywhere). Create one at
201
+ https://codiedev.com/portal/integrations/claude-code
202
+ --force, -f Sign in again even if this machine is already connected.
203
+
204
+ Browser sign-in stores a 24-hour access token and a refresh token that
205
+ rotates on every use; nothing long-lived is saved. Revoke a machine from
206
+ Integrations in the portal, or run 'codiedev logout' on it.
207
+ `.trim(),
208
+ logout: `
209
+ codiedev logout — sign this machine out
210
+
211
+ Ends this machine's browser sign-in on the server and removes it from
212
+ ~/.codiedev/config.json. Device settings (ignored folders) are kept.
213
+ For a pasted API token, it's only forgotten locally: revoke it in the
214
+ portal under Integrations.
187
215
  `.trim(),
188
216
  push: `
189
217
  codiedev push — author or update an artifact
@@ -402,13 +430,15 @@ Run 'codiedev help <command>' for each, or see the quick list:
402
430
 
403
431
  codiedev help
404
432
 
405
- ## Getting a token
433
+ ## Signing in
406
434
 
407
- 1. Log in to https://codiedev.com/portal/integrations/claude-code
408
- 2. Click "Create token"
409
- 3. Copy the cdv_... string and paste it into 'codiedev connect'
435
+ 'codiedev connect' opens your browser; approve, and you're done. Nothing
436
+ long-lived is stored: a 24-hour access token plus a rotating refresh token.
410
437
 
411
- Tokens are per-user. Rotate any time by revoking + creating a new one.
438
+ Over SSH it shows a link and code to approve from your laptop or phone,
439
+ automatically. For CI, create a token at
440
+ https://codiedev.com/portal/integrations/claude-code and run
441
+ 'codiedev connect --token'.
412
442
  `.trim();
413
443
  // Retired commands: say so instead of "Unknown command", since agent
414
444
  // instructions written by older installs may still reference them.
@@ -471,11 +501,41 @@ async function main() {
471
501
  console.log(`codiedev v${version_1.CLI_VERSION}`);
472
502
  return;
473
503
  }
504
+ // Browser sign-in: refresh the access token (under the shared lock) before
505
+ // the command reads config. No-op for pasted API tokens.
506
+ if (command !== "connect" && command !== "logout") {
507
+ const fresh = await (0, cliSession_1.ensureFreshToken)({ configPath: utils_1.CONFIG_PATH }).catch(() => null);
508
+ if (fresh?.status === "signed_out") {
509
+ console.error("\nYour Codiedev sign-in has ended (revoked or expired). Run `codiedev connect` to sign in again.\n");
510
+ }
511
+ }
474
512
  try {
475
513
  switch (command) {
476
514
  case "connect":
477
515
  await (0, connect_1.runConnect)(rest);
478
516
  return;
517
+ case "session-refresh": {
518
+ // Internal: the Codie engine's per-request hook asks for a fresh
519
+ // token when the current one has under 30 minutes left. Refreshes
520
+ // under the shared lock; prints nothing.
521
+ await (0, cliSession_1.ensureFreshToken)({ configPath: utils_1.CONFIG_PATH }, 60 * 60_000).catch(() => null);
522
+ return;
523
+ }
524
+ case "logout": {
525
+ const out = await (0, cliSession_1.logout)({ configPath: utils_1.CONFIG_PATH });
526
+ if (!out.wasConnected) {
527
+ console.log("This machine isn't signed in.");
528
+ }
529
+ else if (out.hadBrowserAuth) {
530
+ console.log(out.serverRevoked
531
+ ? "Signed out. This machine's sign-in is revoked."
532
+ : "Signed out on this machine. Couldn't reach Codiedev to revoke the sign-in; revoke it under Integrations in the portal.");
533
+ }
534
+ else {
535
+ console.log("Removed the API token from this machine. It still works elsewhere until you revoke it under Integrations in the portal.");
536
+ }
537
+ return;
538
+ }
479
539
  case "doctor":
480
540
  case "verify":
481
541
  await (0, doctor_1.runDoctor)(rest);
@@ -0,0 +1,108 @@
1
+ import type { CodiedevConfig } from "./utils";
2
+ export interface BrowserAuth {
3
+ kind: "browser";
4
+ clientId: string;
5
+ refreshToken: string;
6
+ /** epoch ms when `token` (the access token) stops working */
7
+ accessTokenExpiresAt: number;
8
+ }
9
+ export interface TokenSet {
10
+ accessToken: string;
11
+ refreshToken: string;
12
+ expiresAt: number;
13
+ clientId: string;
14
+ }
15
+ /** Default: refresh when less than 10 minutes remain. */
16
+ export declare const DEFAULT_MIN_VALIDITY_MS: number;
17
+ /** A lock older than this was left by a crashed process. Refresh calls time
18
+ * out well before it (REFRESH_TIMEOUT_MS), so a live holder never looks stale. */
19
+ export declare const LOCK_STALE_MS = 30000;
20
+ export interface SessionDeps {
21
+ configPath: string;
22
+ fetchImpl?: typeof fetch;
23
+ now?: () => number;
24
+ sleep?: (ms: number) => Promise<void>;
25
+ }
26
+ export declare function readConfigAt(configPath: string): CodiedevConfig | null;
27
+ /** rename(), retrying the transient Windows sharing errors. Exported for tests. */
28
+ export declare function renameWithRetry(from: string, to: string, rename?: (a: string, b: string) => void, sleep?: (ms: number) => void): void;
29
+ /**
30
+ * Write via temp file + rename so a concurrent reader never sees a torn file.
31
+ * If the rename still fails after ~1.6s of retries (a handle held that long),
32
+ * fall back to writing in place: a rare torn read beats failing `connect`.
33
+ */
34
+ export declare function writeConfigAtomic(configPath: string, config: CodiedevConfig): void;
35
+ export type FreshResult = {
36
+ status: "fresh";
37
+ config: CodiedevConfig;
38
+ } | {
39
+ status: "refreshed";
40
+ config: CodiedevConfig;
41
+ } | {
42
+ status: "adopted";
43
+ config: CodiedevConfig;
44
+ } | {
45
+ status: "unavailable";
46
+ config: CodiedevConfig;
47
+ } | {
48
+ status: "signed_out";
49
+ config: CodiedevConfig;
50
+ } | {
51
+ status: "none";
52
+ config: CodiedevConfig | null;
53
+ };
54
+ /**
55
+ * Make sure config.token is a usable access token for at least
56
+ * `minValidityMs`. Never throws: auth problems surface as the next request's
57
+ * 401, which every command already explains.
58
+ */
59
+ export declare function ensureFreshToken(deps: SessionDeps, minValidityMs?: number): Promise<FreshResult>;
60
+ export declare function pkcePair(): {
61
+ verifier: string;
62
+ challenge: string;
63
+ };
64
+ export declare function openBrowser(url: string): boolean;
65
+ export interface LoginOptions {
66
+ backendUrl: string;
67
+ clientName?: string;
68
+ fetchImpl?: typeof fetch;
69
+ /** Test hook: called with the authorize URL instead of opening a browser. */
70
+ onAuthorizeUrl?: (url: string) => void;
71
+ log?: (line: string) => void;
72
+ now?: () => number;
73
+ }
74
+ /** Parse `code`/`state` (or `error`) out of a loopback callback URL. */
75
+ export declare function parseCallback(input: string): {
76
+ code?: string;
77
+ state?: string;
78
+ error?: string;
79
+ };
80
+ export declare function browserLogin(opts: LoginOptions): Promise<TokenSet>;
81
+ export declare const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
82
+ /**
83
+ * True when a browser opened here can't reach the user: an SSH session, or a
84
+ * Linux box with no display. Then `connect` uses the device flow (a code the
85
+ * user approves on any device) instead of a loopback redirect, with no flag.
86
+ */
87
+ export declare function isHeadless(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
88
+ export interface DeviceLoginOptions {
89
+ backendUrl: string;
90
+ clientName?: string;
91
+ fetchImpl?: typeof fetch;
92
+ log?: (line: string) => void;
93
+ now?: () => number;
94
+ sleep?: (ms: number) => Promise<void>;
95
+ /** Test hook: receives the code + link instead of only printing them. */
96
+ onCode?: (info: {
97
+ userCode: string;
98
+ verificationUri: string;
99
+ verificationUriComplete: string;
100
+ }) => void;
101
+ }
102
+ export declare function deviceLogin(opts: DeviceLoginOptions): Promise<TokenSet>;
103
+ /** `codiedev logout`: end a browser sign-in server-side, forget it locally. */
104
+ export declare function logout(deps: SessionDeps): Promise<{
105
+ wasConnected: boolean;
106
+ hadBrowserAuth: boolean;
107
+ serverRevoked: boolean;
108
+ }>;