codeden 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +12 -5
  2. package/dist/cli.js +1 -1
  3. package/dist/drizzle/20261112140714_turn_fork_point/migration.sql +1 -0
  4. package/dist/drizzle/20261112140714_turn_fork_point/snapshot.json +3524 -0
  5. package/dist/drizzle/20261113075317_transcript_search/migration.sql +29 -0
  6. package/dist/drizzle/20261113075317_transcript_search/snapshot.json +3635 -0
  7. package/dist/drizzle/20261113083655_runners/migration.sql +17 -0
  8. package/dist/drizzle/20261113083655_runners/snapshot.json +3792 -0
  9. package/dist/drizzle/20261113165404_search_newest_first/migration.sql +43 -0
  10. package/dist/drizzle/20261113165404_search_newest_first/snapshot.json +3785 -0
  11. package/dist/{main-hdQfX9V6.js → main-BITVD_Bx.js} +7311 -3019
  12. package/dist/main-BITVD_Bx.js.map +1 -0
  13. package/dist/{main-DZBmzxSH.js → main-NN0iK2tB.js} +2 -2
  14. package/dist/{main-DZBmzxSH.js.map → main-NN0iK2tB.js.map} +1 -1
  15. package/dist/{mediaType-nJs-4z5U.js → mediaType-N0hKQqG9.js} +2 -2
  16. package/dist/{mediaType-nJs-4z5U.js.map → mediaType-N0hKQqG9.js.map} +1 -1
  17. package/dist/{runner-DYf8dV43.js → runner-CQYh087M.js} +1948 -385
  18. package/dist/runner-CQYh087M.js.map +1 -0
  19. package/dist/runner.js +1 -1
  20. package/dist/{server-test-COAiKxTV.js → server-test-BwyCFssm.js} +3 -3
  21. package/dist/{server-test-COAiKxTV.js.map → server-test-BwyCFssm.js.map} +1 -1
  22. package/dist/{tree-CF7Q8tXK.js → tree-Bjv-rXir.js} +638 -41
  23. package/dist/tree-Bjv-rXir.js.map +1 -0
  24. package/dist/web/assets/{core-Rhf-hNq6.js → core-EsMgAasL.js} +1 -1
  25. package/dist/web/assets/engine-oniguruma-C2eJcn5h.js +1 -0
  26. package/dist/web/assets/{highlighted-body-JLMTFACH-DcKwRTm5.js → highlighted-body-JLMTFACH-Co1oXN1b.js} +1 -1
  27. package/dist/web/assets/{highlights-DCtMC0YR.js → highlights-DUjJZrPQ.js} +1 -1
  28. package/dist/web/assets/index-29XyPQXD.css +1 -0
  29. package/dist/web/assets/index-DK4GsIYY.js +943 -0
  30. package/dist/web/assets/langs-BduZAjBk.js +1 -0
  31. package/dist/web/assets/mermaid-WFAXDIZX-D00b7K53.js +1 -0
  32. package/dist/web/assets/{shiki-B3-ws3OK.js → shiki-DP8JfDZC.js} +2 -2
  33. package/dist/web/assets/{stream-tokenizer-hKFrMxml.js → stream-tokenizer-IR1cpBIQ.js} +1 -1
  34. package/dist/web/assets/{themes-Cvhh4Ovr.js → themes-D2aixyve.js} +2 -2
  35. package/dist/web/index.html +2 -2
  36. package/package.json +1 -1
  37. package/dist/main-hdQfX9V6.js.map +0 -1
  38. package/dist/runner-DYf8dV43.js.map +0 -1
  39. package/dist/tree-CF7Q8tXK.js.map +0 -1
  40. package/dist/web/assets/engine-oniguruma-BbpT993F.js +0 -1
  41. package/dist/web/assets/index-BLggvVMT.js +0 -942
  42. package/dist/web/assets/index-JjF9o00P.css +0 -1
  43. package/dist/web/assets/langs-B2ZGl3f_.js +0 -1
  44. package/dist/web/assets/mermaid-WFAXDIZX-C2_MUy_a.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DegdyBIC.js";
2
- import { Bn as BROWSER_RELAY_SECRET_HEADER, Er as ReasoningEffort, Hn as RuntimeResponse, Vn as ProviderCapabilities, Wn as ProjectConfigFile, a as childEnv, d as RunnerLock, er as AgentEvent, i as listDescendants, ir as UserInput, m as ServerToRunnerMessage, n as killTree$1, nr as oneLineDescription, o as CODEDEN_VERSION, rr as winningCommands, t as isAlive$1, u as RunnerEventMessage, zn as SETUP_TAIL_LIMIT } from "./tree-CF7Q8tXK.js";
2
+ import { $n as ProjectConfigFile, Hn as SETUP_TAIL_LIMIT, Un as BROWSER_RELAY_SECRET_HEADER, Xn as ProviderCapabilities, Yn as CODEDEN_CREDENTIAL_FILES, Zn as RuntimeResponse, a as childEnv, d as RunnerEventMessage, dr as mergeItemDeltas, f as RunnerLock, gr as UserInput, h as ServerToRunnerMessage, hr as itemDeltaText, i as listDescendants, lr as AgentEvent, mr as winningCommands, n as killTree$1, o as CODEDEN_VERSION, pr as oneLineDescription, qn as MAX_FOCUS_TYPE_CHARS, t as isAlive$1, zr as ReasoningEffort } from "./tree-Bjv-rXir.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { accessSync, appendFileSync, chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
5
5
  import { pathToFileURL } from "node:url";
@@ -3612,6 +3612,8 @@ var import_websocket = /* @__PURE__ */ __toESM(require_websocket(), 1);
3612
3612
  var import_websocket_server = /* @__PURE__ */ __toESM(require_websocket_server(), 1);
3613
3613
  var wrapper_default = import_websocket.default;
3614
3614
  // ../../packages/providers/src/adapter.ts
3615
+ /** `ProviderSessionError` code: the conversation doesn't hold the fork point it was asked for. */
3616
+ const FORK_POINT_MISSING = "fork_point_missing";
3615
3617
  /**
3616
3618
  * Session options ready to open (`SessionOptions.forkFrom`): with `forkFrom` and no
3617
3619
  * `resumeToken`, the conversation is copied first (`forkConversation`) and the session resumes
@@ -4513,6 +4515,52 @@ function parseClaudeAuthStatus(stdout) {
4513
4515
  return unknown;
4514
4516
  }
4515
4517
  }
4518
+ // ../../packages/providers/src/claude/fork.ts
4519
+ const reasonOf = (error) => error instanceof Error ? error.message : String(error);
4520
+ /**
4521
+ * Copy a Claude conversation with the SDK's `forkSession()`: the transcript is written to a new
4522
+ * session file at once (fresh message ids, the original untouched) and the new session id is
4523
+ * returned for resuming. With `upTo` (a turn's fork point: its last transcript message), the
4524
+ * copy ends with that message. A conversation that no longer holds it (replaced or deleted)
4525
+ * throws `FORK_POINT_MISSING` before anything is copied, so it never becomes a whole copy
4526
+ * instead: `confirmForkPoint` refuses it from the transcript read when it can, and otherwise
4527
+ * `forkSession()` does, as it slices the whole transcript file and refuses a message the file
4528
+ * doesn't hold before writing anything (SDK 0.3.318: `Message <uuid> not found in session <id>`).
4529
+ */
4530
+ async function forkClaudeConversation(sdk, resumeToken, upTo) {
4531
+ if (upTo) await confirmForkPoint(sdk, resumeToken, upTo);
4532
+ try {
4533
+ return (await sdk.forkSession(resumeToken, upTo ? { upToMessageId: upTo } : void 0)).sessionId;
4534
+ } catch (error) {
4535
+ if (upTo && refusedForkPoint(error, upTo)) throw forkPointMissing();
4536
+ throw new Error(`Could not fork the Claude conversation ${resumeToken}: ${reasonOf(error)}`);
4537
+ }
4538
+ }
4539
+ /**
4540
+ * Read the transcript (`getSessionMessages`) and refuse a fork point it doesn't hold. That read
4541
+ * follows `parentUuid` links back from the newest message, and a context compaction's boundary
4542
+ * has none, so in a compacted conversation it starts at the latest boundary (a `system`
4543
+ * message) and leaves out every earlier message, although the transcript file still holds
4544
+ * them. A fork point missing from such a read may be before the compaction, so `forkSession()`
4545
+ * decides then. An empty read (a conversation that was deleted) holds nothing.
4546
+ */
4547
+ async function confirmForkPoint(sdk, resumeToken, upTo) {
4548
+ let messages;
4549
+ try {
4550
+ messages = await sdk.getSessionMessages(resumeToken, { includeSystemMessages: true });
4551
+ } catch (error) {
4552
+ throw new Error(`Could not read the Claude conversation ${resumeToken}: ${reasonOf(error)}`);
4553
+ }
4554
+ if (!(messages[0]?.type === "system") && !messages.some((m) => m.uuid === upTo)) throw forkPointMissing();
4555
+ }
4556
+ /** Whether `forkSession()` refused `upTo` as a message its transcript doesn't hold. */
4557
+ function refusedForkPoint(error, upTo) {
4558
+ const reason = reasonOf(error);
4559
+ return reason.includes(upTo) && /\bnot found\b/i.test(reason);
4560
+ }
4561
+ function forkPointMissing() {
4562
+ return new ProviderSessionError(FORK_POINT_MISSING, "The conversation no longer holds the end of that turn (it was replaced or deleted), so it can't be forked there");
4563
+ }
4516
4564
  // ../../packages/providers/src/util/model.ts
4517
4565
  /** The id the model pickers use for "the provider's own default model". */
4518
4566
  const DEFAULT_MODEL_ID = "default";
@@ -4524,8 +4572,26 @@ function explicitModel(model) {
4524
4572
  const id = model?.trim();
4525
4573
  return id && id !== "default" ? id : void 0;
4526
4574
  }
4575
+ // ../../packages/providers/src/policy/assessment.ts
4576
+ const SAFE = { risky: false };
4577
+ const risky = (action, reason) => ({
4578
+ risky: true,
4579
+ action,
4580
+ reason
4581
+ });
4582
+ /** True for a verdict on CodeDen's own credential files. */
4583
+ const touchesCodeden = (risk) => risk.risky && risk.codeden === true;
4584
+ /**
4585
+ * The verdict for several checks of one action: CodeDen's own credentials first (a level may
4586
+ * allow an ordinary risk that comes before them), then the first risky one, else safe.
4587
+ */
4588
+ function mostSevere(risks) {
4589
+ return risks.find(touchesCodeden) ?? risks.find((r) => r.risky) ?? SAFE;
4590
+ }
4527
4591
  // ../../packages/providers/src/policy/paths.ts
4592
+ /** True when the context's paths are Windows paths. */
4528
4593
  const isWin = (ctx) => (ctx.platform ?? process.platform) === "win32";
4594
+ /** The path functions for the context's platform. */
4529
4595
  function pathApi(ctx) {
4530
4596
  return isWin(ctx) ? path.win32 : path.posix;
4531
4597
  }
@@ -4542,6 +4608,8 @@ const HOME_VARS = /^(\$HOME|\$\{HOME\}|\$env:USERPROFILE|%USERPROFILE%)(?=$|[\\/
4542
4608
  * printed) is not the temp directory; PowerShell's and cmd's names are not.
4543
4609
  */
4544
4610
  const TEMP_VARS = [/^(\$TMPDIR|\$\{TMPDIR\}|\$TEMP|\$\{TEMP\}|\$TMP|\$\{TMP\})(?=$|[\\/])/, /^(\$env:TEMP|\$env:TMP|%TEMP%|%TMP%)(?=$|[\\/])/i];
4611
+ /** CodeDen's home variable, which agents inherit: case-sensitive in POSIX shells only. */
4612
+ const CODEDEN_HOME_VARS = [/^(\$CODEDEN_HOME|\$\{CODEDEN_HOME\})(?=$|[\\/])/, /^(\$env:CODEDEN_HOME|%CODEDEN_HOME%)(?=$|[\\/])/i];
4545
4613
  /**
4546
4614
  * Git Bash paths on Windows: `/c/Users/x` is `C:\Users\x`, and `/tmp` is the user's temp
4547
4615
  * directory.
@@ -4553,9 +4621,9 @@ function fromGitBash(value, temp) {
4553
4621
  return value;
4554
4622
  }
4555
4623
  /**
4556
- * Resolve `target` (absolute, relative to `dir`, `~`-prefixed, or starting with a home or
4557
- * temp variable) to an absolute path. On Windows the directory it resolves from may be in Git
4558
- * Bash form too: the shell reports `/c/Users/x/repo` once a command has run in it.
4624
+ * Resolve `target` (absolute, relative to `dir`, `~`-prefixed, or starting with a home, temp or
4625
+ * CodeDen home variable) to an absolute path. On Windows the directory it resolves from may be
4626
+ * in Git Bash form too: the shell reports `/c/Users/x/repo` once a command has run in it.
4559
4627
  */
4560
4628
  function resolveAgainst(target, ctx) {
4561
4629
  const p = pathApi(ctx);
@@ -4563,6 +4631,8 @@ function resolveAgainst(target, ctx) {
4563
4631
  const home = homeOf(ctx);
4564
4632
  if (value === "~" || value.startsWith("~/") || value.startsWith("~\\")) value = home + value.slice(1);
4565
4633
  value = value.replace(HOME_VARS, home);
4634
+ const codeden = ctx.codedenHome;
4635
+ if (codeden) for (const vars of CODEDEN_HOME_VARS) value = value.replace(vars, codeden);
4566
4636
  const temp = tempRootsOf(ctx)[0];
4567
4637
  if (temp) for (const vars of TEMP_VARS) value = value.replace(vars, temp);
4568
4638
  let base = ctx.dir ?? ctx.cwd;
@@ -4582,6 +4652,10 @@ function relative$1(root, target, ctx) {
4582
4652
  const norm = (s) => isWin(ctx) ? p.resolve(s).toLowerCase() : p.resolve(s);
4583
4653
  return p.relative(norm(root), norm(target));
4584
4654
  }
4655
+ /** True when two absolute paths name the same file. Case-insensitive on Windows. */
4656
+ function samePath(a, b, ctx) {
4657
+ return relative$1(a, b, ctx) === "";
4658
+ }
4585
4659
  /** True when `target` is inside the worktree or one of the extra roots. */
4586
4660
  function isInWorkspace(target, ctx) {
4587
4661
  const abs = resolveAgainst(target, ctx);
@@ -4698,13 +4772,6 @@ function isSecretPath(target) {
4698
4772
  if (SECRET_EXTENSIONS.some((ext) => base.endsWith(ext))) return true;
4699
4773
  return /(^|\/)\.(ssh|aws|gnupg|docker)\//i.test(normalized) && !base.endsWith(".pub");
4700
4774
  }
4701
- // ../../packages/providers/src/policy/assessment.ts
4702
- const SAFE = { risky: false };
4703
- const risky = (action, reason) => ({
4704
- risky: true,
4705
- action,
4706
- reason
4707
- });
4708
4775
  // ../../packages/providers/src/policy/git.ts
4709
4776
  /**
4710
4777
  * Git rules of the risk classifier (§11.2): force pushes, deleting refs, global config. `args`
@@ -5655,6 +5722,590 @@ function unixToIso(value) {
5655
5722
  const ms = value < 0xe8d4a51000 ? value * 1e3 : value;
5656
5723
  return new Date(ms).toISOString();
5657
5724
  }
5725
+ // ../../packages/providers/src/policy/codeden-home.ts
5726
+ /**
5727
+ * CodeDen's own credential files (§15): the install token, the runner lock and the remote
5728
+ * access store in its data directory. An agent holding the install token or the runner's token
5729
+ * could drive CodeDen itself and approve its own requests, so touching one asks at every level,
5730
+ * Full included, and is refused for read-only sessions (§11.2). The data directory comes from
5731
+ * the session (`SessionOptions.codedenHome`); without it, this rule is off.
5732
+ */
5733
+ /** A risky verdict on CodeDen's own credentials, which every level asks about. */
5734
+ function credentialsRisk(reason) {
5735
+ return {
5736
+ risky: true,
5737
+ action: "secrets",
5738
+ reason,
5739
+ codeden: true
5740
+ };
5741
+ }
5742
+ /** The credential files in the session's CodeDen home, as absolute paths (none without one). */
5743
+ function codedenCredentialFiles(ctx) {
5744
+ const home = ctx.codedenHome;
5745
+ if (!home) return [];
5746
+ const p = pathApi(ctx);
5747
+ return CODEDEN_CREDENTIAL_FILES.map((file) => p.join(home, file));
5748
+ }
5749
+ /** The credential files, then the folders that hold them. */
5750
+ function guardedPaths(ctx) {
5751
+ const files = codedenCredentialFiles(ctx);
5752
+ const p = pathApi(ctx);
5753
+ return [...files, ...new Set(files.map((file) => p.dirname(file)))];
5754
+ }
5755
+ const WILDCARD = /[*?[]/;
5756
+ /** `c` matched as itself in a regular expression. */
5757
+ const literalChar = (c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5758
+ /**
5759
+ * A shell wildcard (`*`, `?`, `[...]`, and `**`, which before a slash stands for zero or more
5760
+ * folders) over a `/`-separated path as a regular expression. Its first `literal` characters
5761
+ * match as themselves. Undefined when malformed.
5762
+ */
5763
+ function wildcardPattern(pattern, ignoreCase, literal = 0) {
5764
+ let source = pattern.slice(0, literal).replace(/[\s\S]/g, literalChar);
5765
+ for (let i = literal; i < pattern.length; i += 1) {
5766
+ const c = pattern[i];
5767
+ if (c === "*" && pattern[i + 1] === "*") {
5768
+ const folders = pattern[i + 2] === "/";
5769
+ source += folders ? "(?:.*/)?" : ".*";
5770
+ i += folders ? 2 : 1;
5771
+ } else if (c === "*") source += "[^/]*";
5772
+ else if (c === "?") source += "[^/]";
5773
+ else if (c === "[" && pattern.indexOf("]", i + 2) > 0) {
5774
+ const end = pattern.indexOf("]", i + 2);
5775
+ const body = pattern.slice(i + 1, end).replace(/^!/, "^").replace(/\\/g, "\\\\");
5776
+ source += `[${body}]`;
5777
+ i = end;
5778
+ } else source += literalChar(c);
5779
+ }
5780
+ try {
5781
+ return new RegExp(`^${source}$`, ignoreCase ? "i" : "");
5782
+ } catch {
5783
+ return;
5784
+ }
5785
+ }
5786
+ /** `/`-separated, for wildcard matching; Windows paths match in any case. */
5787
+ function slashes(value, ctx) {
5788
+ return isWin(ctx) ? value.replace(/\\/g, "/") : value;
5789
+ }
5790
+ /**
5791
+ * Variables left in a resolved path (`$f`, `${f}`, `$env:F`, `%F%`) could be any name, so they
5792
+ * match like `*`. `$CODEDEN_HOME`, `$HOME` and the temp variables at the start resolve first.
5793
+ */
5794
+ const PATH_VARIABLE = /\$\{\w+\}|\$env:\w+|\$\w+|%\w+%/gi;
5795
+ /**
5796
+ * How many leading characters of `abs` are CodeDen's home or a folder above it, at a folder
5797
+ * boundary. That part comes from the session (`$CODEDEN_HOME`, `~`, the working directory), so it
5798
+ * is always literal: `[`, `*`, `?` or `$` in the home's own path (`CodeDen [dev]`) are never a
5799
+ * pattern. Compared like `samePath`, in any case on Windows.
5800
+ */
5801
+ function literalPrefix(abs, ctx) {
5802
+ const home = ctx.codedenHome;
5803
+ if (!home) return 0;
5804
+ const p = pathApi(ctx);
5805
+ const value = slashes(abs, ctx);
5806
+ const fold = (s) => isWin(ctx) ? s.toLowerCase() : s;
5807
+ for (let dir = p.resolve(home);; dir = p.dirname(dir)) {
5808
+ const prefix = slashes(dir, ctx).replace(/\/$/, "");
5809
+ const n = prefix.length;
5810
+ const atBoundary = value.length === n || value[n] === "/";
5811
+ if (n > 0 && atBoundary && fold(value.slice(0, n)) === fold(prefix)) return n;
5812
+ if (p.dirname(dir) === dir) return 0;
5813
+ }
5814
+ }
5815
+ /** `target` as an absolute path, the unknown variables it adds as wildcards. */
5816
+ function resolvePattern(target, ctx) {
5817
+ const abs = resolveAgainst(target, ctx);
5818
+ const literal = literalPrefix(abs, ctx);
5819
+ return abs.slice(0, literal) + abs.slice(literal).replace(PATH_VARIABLE, "*");
5820
+ }
5821
+ /**
5822
+ * The wildcards in `abs` (from `resolvePattern`) as a regular expression over `/`-separated
5823
+ * paths, or undefined when it has none (or is malformed). Only the agent's part can be a pattern:
5824
+ * CodeDen's home, and the folders above it, match literally (`literalPrefix`).
5825
+ */
5826
+ function agentPattern(abs, ctx) {
5827
+ const value = slashes(abs, ctx);
5828
+ const literal = literalPrefix(abs, ctx);
5829
+ if (!WILDCARD.test(value.slice(literal))) return void 0;
5830
+ return wildcardPattern(value, isWin(ctx), literal);
5831
+ }
5832
+ /**
5833
+ * True when `target` reaches one of CodeDen's credential files: the file itself, a folder that
5834
+ * holds one (`<home>`, `<home>/runner`: copying or searching it reads them), or a wildcard that
5835
+ * matches either. `~`, `$HOME`, `$CODEDEN_HOME` (and its PowerShell and cmd forms) and relative
5836
+ * paths resolve as in the shell. The home itself is always compared literally.
5837
+ */
5838
+ function touchesCodedenCredentials(target, ctx) {
5839
+ if (!ctx.codedenHome || !target.trim()) return false;
5840
+ const abs = resolvePattern(target, ctx);
5841
+ const guarded = guardedPaths(ctx);
5842
+ if (guarded.some((p) => samePath(p, abs, ctx))) return true;
5843
+ const pattern = agentPattern(abs, ctx);
5844
+ return pattern !== void 0 && guarded.some((p) => pattern.test(slashes(p, ctx)));
5845
+ }
5846
+ // ../../packages/providers/src/policy/search-words.ts
5847
+ /**
5848
+ * Search commands (`grep`, `rg`, `findstr`, `Select-String`, ...) read as the tool reads them, for
5849
+ * the risk classifier (§11.2). Each word gets one role, once: an option, an option's value or an
5850
+ * operand. The pattern is an option's pattern value, or else the first operand. Because roles are
5851
+ * fixed in one pass, a value that looks like an option (`rg -r-e`: replacement text `-e`) is never
5852
+ * read as one, and a word that looks like a pattern option is never skipped as a value.
5853
+ */
5854
+ /** Search commands, whose first operand is the pattern (`grep -rn "process.env" src`). */
5855
+ const SEARCHES = /* @__PURE__ */ new Set([
5856
+ "grep",
5857
+ "egrep",
5858
+ "fgrep",
5859
+ "zgrep",
5860
+ "rg",
5861
+ "ag",
5862
+ "ack",
5863
+ "findstr",
5864
+ "select-string",
5865
+ "sls"
5866
+ ]);
5867
+ /** Options that take the next word as their value, per search tool. */
5868
+ const GREP_VALUED = [
5869
+ "-A",
5870
+ "-B",
5871
+ "-C",
5872
+ "-m",
5873
+ "-d",
5874
+ "-D",
5875
+ "--after-context",
5876
+ "--before-context",
5877
+ "--context",
5878
+ "--max-count",
5879
+ "--include",
5880
+ "--exclude",
5881
+ "--exclude-dir",
5882
+ "--label",
5883
+ "--binary-files",
5884
+ "--devices",
5885
+ "--directories"
5886
+ ];
5887
+ const RG_VALUED = [
5888
+ ...GREP_VALUED,
5889
+ "-g",
5890
+ "--glob",
5891
+ "--iglob",
5892
+ "-t",
5893
+ "--type",
5894
+ "-T",
5895
+ "--type-not",
5896
+ "-j",
5897
+ "--threads",
5898
+ "--max-depth",
5899
+ "-M",
5900
+ "--max-columns",
5901
+ "--sort",
5902
+ "--sortr",
5903
+ "-r",
5904
+ "--replace",
5905
+ "--colors",
5906
+ "-E",
5907
+ "--encoding"
5908
+ ];
5909
+ const SELECT_STRING_VALUED = [
5910
+ "-path",
5911
+ "-literalpath",
5912
+ "-include",
5913
+ "-exclude",
5914
+ "-encoding",
5915
+ "-context"
5916
+ ];
5917
+ /** Tools whose option names are matched in any case (names are kept in lower case). */
5918
+ const ANY_CASE = /* @__PURE__ */ new Set([
5919
+ "findstr",
5920
+ "select-string",
5921
+ "sls"
5922
+ ]);
5923
+ /**
5924
+ * Tools whose short options combine in one word, the getopt way (`-ne.` is `-n -e .`), and whose
5925
+ * `-e` and `-f` take a value. `ag -f` and `ack -f` take none, so their words stay as written.
5926
+ */
5927
+ const CLUSTERING = /* @__PURE__ */ new Set([
5928
+ "grep",
5929
+ "egrep",
5930
+ "fgrep",
5931
+ "zgrep",
5932
+ "rg"
5933
+ ]);
5934
+ /** Options whose value is the search pattern. */
5935
+ const PATTERN_OPTIONS = [
5936
+ "-e",
5937
+ "--regexp",
5938
+ "-pattern",
5939
+ "/c"
5940
+ ];
5941
+ /** Options that give the pattern another way, a pattern file: then no operand is the pattern. */
5942
+ const isPatternFile = (name) => name === "-f" || name.startsWith("--file") || name === "/g";
5943
+ /** True when the option `name` takes the next word as its value. */
5944
+ function takesValue(bin, name) {
5945
+ if ([
5946
+ "-e",
5947
+ "--regexp",
5948
+ "-pattern",
5949
+ "-f",
5950
+ "--file"
5951
+ ].includes(name)) return true;
5952
+ if (bin === "rg") return RG_VALUED.includes(name);
5953
+ if (bin === "select-string" || bin === "sls") return SELECT_STRING_VALUED.includes(name);
5954
+ return GREP_VALUED.includes(name);
5955
+ }
5956
+ /** An option word's value bound with `=` or `:` (`--glob=x`, `-Path:x`, `/c:x`). */
5957
+ const BOUND = /^(--?[A-Za-z][\w-]*|\/[A-Za-z])[=:]/;
5958
+ /** Parse a search command's arguments (after the program name) into roles. */
5959
+ function parseSearch(bin, args) {
5960
+ const words = [];
5961
+ const add = (text, role, arg, from = 0, option) => words.push({
5962
+ text,
5963
+ role,
5964
+ option,
5965
+ arg,
5966
+ from,
5967
+ pattern: false
5968
+ });
5969
+ const fold = (name) => ANY_CASE.has(bin) ? name.toLowerCase() : name;
5970
+ const isOption = (a) => a.startsWith("-") || bin === "findstr" && a.startsWith("/");
5971
+ let operandsOnly = false;
5972
+ for (let i = 0; i < args.length; i += 1) {
5973
+ const a = args[i];
5974
+ if (operandsOnly || !isOption(a)) add(a, "operand", i);
5975
+ else if (a === "--") {
5976
+ operandsOnly = true;
5977
+ add(a, "option", i, 0, a);
5978
+ } else if (CLUSTERING.has(bin) && /^-[^-]./.test(a)) for (let j = 1; j < a.length; j += 1) {
5979
+ const flag = `-${a[j]}`;
5980
+ add(flag, "option", -1, 0, flag);
5981
+ if (!takesValue(bin, flag)) continue;
5982
+ if (j + 1 < a.length) add(a.slice(j + 1), "value", i, j + 1, flag);
5983
+ else if (i + 1 < args.length) add(args[++i], "value", i, 0, flag);
5984
+ break;
5985
+ }
5986
+ else {
5987
+ const bound = BOUND.exec(a);
5988
+ const name = fold(bound?.[1] ?? a);
5989
+ add(a, "option", i, 0, name);
5990
+ if (!bound && takesValue(bin, name) && i + 1 < args.length) add(args[++i], "value", i, 0, name);
5991
+ }
5992
+ }
5993
+ markPatterns(words);
5994
+ return words;
5995
+ }
5996
+ /**
5997
+ * Mark the pattern words: the values of `-e`, `--regexp`, `-Pattern` and `findstr /c:`, or, when
5998
+ * no option gives the pattern (nor a pattern file, `-f`), the first operand.
5999
+ */
6000
+ function markPatterns(words) {
6001
+ let explicit = false;
6002
+ for (const w of words) {
6003
+ const name = w.option ?? "";
6004
+ if (w.role === "value" && PATTERN_OPTIONS.includes(name)) {
6005
+ w.pattern = true;
6006
+ explicit = true;
6007
+ } else if (w.role === "option" && PATTERN_OPTIONS.includes(name)) {
6008
+ explicit = true;
6009
+ if (w.text.length > name.length) w.pattern = true;
6010
+ } else if (w.role === "option" && isPatternFile(name)) explicit = true;
6011
+ }
6012
+ if (explicit) return;
6013
+ const first = words.find((w) => w.role === "operand");
6014
+ if (first) first.pattern = true;
6015
+ }
6016
+ /** The file operands of a parsed search (`grep -r -e x src lib` -> `src`, `lib`). */
6017
+ function searchOperands(words) {
6018
+ return words.filter((w) => w.role === "operand" && !w.pattern);
6019
+ }
6020
+ /** The values given to the named options: `name X`, `name=X`, or in a cluster (`-gX`). */
6021
+ function searchOptionValues(words, names) {
6022
+ return words.flatMap((w) => {
6023
+ if (!w.option || !names.includes(w.option)) return [];
6024
+ if (w.role === "value") return [w.text];
6025
+ return w.role === "option" && w.text.length > w.option.length ? [w.text.slice(w.option.length + 1)] : [];
6026
+ });
6027
+ }
6028
+ // ../../packages/providers/src/policy/secrets.ts
6029
+ /**
6030
+ * Secrets rules of the risk classifier (§11.2): which words of a command read a credentials
6031
+ * file, and which commands dump the environment. Writing a secrets file inside the worktree is
6032
+ * not access (`cp .env.example .env`); reading one is.
6033
+ */
6034
+ /**
6035
+ * The argument as a path, or undefined when it can't be one: inline code, multi-line text
6036
+ * or anything with shell or code punctuation (`selected={row.key`) is not a file name. A
6037
+ * variable in braces (`${HOME}/.env`) is a path, and so is an option's value: `--file=x`, or
6038
+ * PowerShell's colon-bound `-Path:x` and `-LiteralPath:x`.
6039
+ */
6040
+ function asPath(arg) {
6041
+ const value = arg.replace(/^-+[a-z-]*=/i, "").replace(/^-[a-z][a-z-]*:(?=.)/i, "");
6042
+ if (!value || /[\n\r{}()<>|;=`]/.test(value.replace(/\$\{\w+\}/g, ""))) return void 0;
6043
+ return value;
6044
+ }
6045
+ /** Commands whose words are data or only checked for existence (`echo .env >> .gitignore`). */
6046
+ const DATA = /* @__PURE__ */ new Set([
6047
+ "echo",
6048
+ "printf",
6049
+ "write-output",
6050
+ "write-host",
6051
+ "test",
6052
+ "[",
6053
+ "[["
6054
+ ]);
6055
+ /** Commands that list or describe files without reading them (`ls certs/*.pem`). */
6056
+ const LISTS = /* @__PURE__ */ new Set([
6057
+ "ls",
6058
+ "dir",
6059
+ "get-childitem",
6060
+ "gci",
6061
+ "find",
6062
+ "fd",
6063
+ "tree",
6064
+ "stat",
6065
+ "du",
6066
+ "test-path"
6067
+ ]);
6068
+ /**
6069
+ * The first argument that reads a credentials file, if any. `written` are the paths the command
6070
+ * writes (a copy's destination), which classifyWrite judges instead. `isSecret` decides which
6071
+ * paths count (default: secrets files by name). `paths` gives what a word may stand for (each
6072
+ * element of a PowerShell list, `shell.wordPaths`). A search's patterns are skipped whole, list
6073
+ * elements included: the tool searches for them, not in them.
6074
+ */
6075
+ function secretArgument(bin, args, written = [], isSecret = isSecretPath, paths = (word, from) => [word.slice(from)]) {
6076
+ if (DATA.has(bin)) return void 0;
6077
+ const runs = (bin === "find" || bin === "fd") && args.some((a) => /^-(exec|execdir|ok|okdir|x|X|-exec|-exec-batch)$/.test(a));
6078
+ if (LISTS.has(bin) && !runs) return void 0;
6079
+ return (SEARCHES.has(bin) ? parseSearch(bin, args) : args.map((text, arg) => ({
6080
+ text,
6081
+ arg,
6082
+ from: 0,
6083
+ pattern: false
6084
+ }))).find((w) => {
6085
+ if (w.pattern || written.includes(w.text)) return false;
6086
+ return (w.arg >= 0 ? paths(args[w.arg], w.from) : [w.text]).some((word) => braceAlternatives(word).some((alt) => {
6087
+ const value = asPath(alt);
6088
+ return value !== void 0 && isSecret(value);
6089
+ }));
6090
+ })?.text;
6091
+ }
6092
+ /** The most words one argument's brace expansion is followed to. */
6093
+ const MAX_ALTERNATIVES = 64;
6094
+ /**
6095
+ * The words a shell's brace expansion makes of `word` (`~/.ssh/{id_rsa,x}` -> `~/.ssh/id_rsa`,
6096
+ * `~/.ssh/x`), capped; just `word` when it has no `{a,b}` group.
6097
+ */
6098
+ function braceAlternatives(word) {
6099
+ const group = /\{([^{}]*,[^{}]*)\}/.exec(word);
6100
+ if (!group?.[1]) return [word];
6101
+ const before = word.slice(0, group.index);
6102
+ const after = word.slice(group.index + group[0].length);
6103
+ const out = [];
6104
+ for (const part of group[1].split(",")) for (const alt of braceAlternatives(before + part + after)) {
6105
+ if (out.length >= MAX_ALTERNATIVES) return out;
6106
+ out.push(alt);
6107
+ }
6108
+ return out;
6109
+ }
6110
+ /** Files a tool writes through its own options (`openssl req -keyout key.pem -out cert.pem`). */
6111
+ function toolOutputs(bin, args) {
6112
+ if (bin !== "openssl" && bin !== "ssh-keygen") return [];
6113
+ const flags = bin === "openssl" ? ["-out", "-keyout"] : ["-f"];
6114
+ return args.flatMap((a, i) => flags.includes(a) && args[i + 1] ? [args[i + 1]] : []);
6115
+ }
6116
+ /** Variable names that may hold a credential. */
6117
+ const SECRET_VAR = /key|token|secret|passw|pwd|credential|auth|private|session|cookie/i;
6118
+ /**
6119
+ * True for `printenv` and `Get-ChildItem env:` listings. Printing named variables that don't
6120
+ * look like credentials (`printenv PATH`, `Get-ChildItem env:NODE_ENV`) is not a dump.
6121
+ */
6122
+ function dumpsEnvironment(bin, args) {
6123
+ const named = (names) => names.length > 0 && names.every((n) => /^\w+$/.test(n) && !SECRET_VAR.test(n));
6124
+ if (bin === "printenv") return !named(args.filter((a) => !a.startsWith("-")));
6125
+ if (!/^(get-childitem|gci|ls|dir)$/.test(bin)) return false;
6126
+ const drives = args.filter((a) => /^env:/i.test(a));
6127
+ return drives.length > 0 && !named(drives.map((a) => a.slice(4)));
6128
+ }
6129
+ // ../../packages/providers/src/policy/codeden-search.ts
6130
+ /** The values of an option, given as `name X`, `name=X` or `name:X`, each time it appears. */
6131
+ function optionValues(args, names, anyCase) {
6132
+ const out = [];
6133
+ for (let i = 0; i < args.length; i += 1) {
6134
+ const a = args[i];
6135
+ const word = anyCase ? a.toLowerCase() : a;
6136
+ if (names.includes(word)) {
6137
+ if (i + 1 < args.length) out.push(args[i + 1]);
6138
+ i += 1;
6139
+ continue;
6140
+ }
6141
+ const bound = names.find((n) => word.startsWith(`${n}=`) || word.startsWith(`${n}:`));
6142
+ if (bound) out.push(a.slice(bound.length + 1));
6143
+ }
6144
+ return out;
6145
+ }
6146
+ const GREPS = /* @__PURE__ */ new Set([
6147
+ "grep",
6148
+ "egrep",
6149
+ "fgrep",
6150
+ "zgrep"
6151
+ ]);
6152
+ /** `grep -r`, `-R`, `-rn`, `--recursive`, `-d recurse`: grep reads folders only then. */
6153
+ function grepRecursive(words) {
6154
+ return words.some((w) => w.role === "option" && /^(-r|-R|--recursive|--dereference-recursive)$/.test(w.text)) || searchOptionValues(words, ["-d", "--directories"]).includes("recurse");
6155
+ }
6156
+ /**
6157
+ * The folders a search reads recursively, with its name filters. Roles come from `parseSearch`
6158
+ * (`grep -re. ~` is `grep -r -e . ~`; in `rg -r-g x ~`, `-g` is replacement text, not a filter),
6159
+ * and each root may be a PowerShell list (`paths`).
6160
+ */
6161
+ function searchWalks(bin, args, ctx, paths) {
6162
+ const words = parseSearch(bin, args);
6163
+ const operands = searchOperands(words).flatMap((w) => paths(args[w.arg], w.from));
6164
+ if (bin === "rg" || bin === "ag" || bin === "ack") return [{
6165
+ roots: operands,
6166
+ recursive: true,
6167
+ names: bin === "rg" ? searchOptionValues(words, [
6168
+ "-g",
6169
+ "--glob",
6170
+ "--iglob"
6171
+ ]).filter((n) => !n.startsWith("!")) : []
6172
+ }];
6173
+ if (GREPS.has(bin)) {
6174
+ if (!grepRecursive(words)) return [];
6175
+ return [{
6176
+ roots: operands,
6177
+ recursive: true,
6178
+ names: searchOptionValues(words, ["--include"])
6179
+ }];
6180
+ }
6181
+ const sub = words.some((w) => w.role === "option" && /^\/[a-z]*s[a-z]*$/i.test(w.text));
6182
+ if (bin === "findstr" && sub) {
6183
+ const p = pathApi(ctx);
6184
+ return operands.map((operand) => ({
6185
+ roots: [p.dirname(operand)],
6186
+ recursive: true,
6187
+ names: [p.basename(operand)]
6188
+ }));
6189
+ }
6190
+ return [];
6191
+ }
6192
+ const LISTINGS = /* @__PURE__ */ new Set([
6193
+ "get-childitem",
6194
+ "gci",
6195
+ "dir",
6196
+ "ls"
6197
+ ]);
6198
+ const LISTING_VALUED = [
6199
+ "-filter",
6200
+ "-include",
6201
+ "-exclude",
6202
+ "-depth",
6203
+ "-attributes"
6204
+ ];
6205
+ /** `find -exec cat {} +` and `fd -x cat` hand what they find to a command. */
6206
+ const RUNS = /^-(exec|execdir|ok|okdir|x|X|-exec|-exec-batch)$/;
6207
+ /** The folders a listing goes through, with its name filters. */
6208
+ function listingWalk(bin, args) {
6209
+ if (bin === "find") {
6210
+ const end = args.findIndex((a) => /^[-(!)]/.test(a));
6211
+ const either = args.some((a) => [
6212
+ "-o",
6213
+ "-or",
6214
+ "!",
6215
+ "-not"
6216
+ ].includes(a));
6217
+ return {
6218
+ roots: end < 0 ? args : args.slice(0, end),
6219
+ recursive: true,
6220
+ names: either ? [] : optionValues(args, ["-name", "-iname"], false)
6221
+ };
6222
+ }
6223
+ if (bin === "fd") return {
6224
+ roots: args.filter((a) => !a.startsWith("-")).slice(1),
6225
+ recursive: true,
6226
+ names: []
6227
+ };
6228
+ if (!LISTINGS.has(bin)) return void 0;
6229
+ const recursive = args.some((a) => /^-rec/i.test(a) || a === "-r" || /^-[a-zA-Z]*R[a-zA-Z]*$/.test(a) || /^\/s$/i.test(a));
6230
+ const named = optionValues(args, ["-path", "-literalpath"], true);
6231
+ const positional = [];
6232
+ for (let i = 0; i < args.length; i += 1) {
6233
+ const a = args[i];
6234
+ const lower = a.toLowerCase();
6235
+ if (LISTING_VALUED.includes(lower) || lower === "-path" || lower === "-literalpath") i += 1;
6236
+ else if (!a.startsWith("-") && !/^\/[a-z](:\S*)?$/i.test(a)) positional.push(a);
6237
+ }
6238
+ return {
6239
+ roots: [...named, ...positional].flatMap((root) => root.split(",")),
6240
+ recursive,
6241
+ names: optionValues(args, ["-filter", "-include"], true).flatMap((n) => n.split(","))
6242
+ };
6243
+ }
6244
+ /**
6245
+ * True when listing `dir` (and, recursively, its subfolders) reaches `folder`: by its path, or
6246
+ * through the wildcards the agent wrote in it (never ones in CodeDen's home path).
6247
+ */
6248
+ function holds(dir, folder, recursive, ctx) {
6249
+ if (recursive ? isInside$1(folder, dir, ctx) : samePath(dir, folder, ctx)) return true;
6250
+ const pattern = agentPattern(dir, ctx);
6251
+ if (!pattern) return false;
6252
+ const p = pathApi(ctx);
6253
+ for (let at = folder;; at = p.dirname(at)) {
6254
+ if (pattern.test(slashes(at, ctx))) return true;
6255
+ if (!recursive || p.dirname(at) === at) return false;
6256
+ }
6257
+ }
6258
+ /** True when `file`, found under `root`, passes one of the name filters (or there are none). */
6259
+ function keeps(names, file, root, ctx) {
6260
+ if (names.length === 0) return true;
6261
+ const p = pathApi(ctx);
6262
+ const base = p.basename(file);
6263
+ const below = slashes(p.relative(root, file), ctx);
6264
+ return names.flatMap((name) => braceAlternatives(name.trim().replace(/^\.[\\/]/, ""))).some((name) => {
6265
+ const pattern = wildcardPattern(slashes(name, ctx), true);
6266
+ if (!pattern) return true;
6267
+ return /[\\/]/.test(name) ? pattern.test(below) : pattern.test(base);
6268
+ });
6269
+ }
6270
+ /** The root of `walk` from which it reaches a credential file, if any. */
6271
+ function walkReaches(walk, ctx) {
6272
+ const files = codedenCredentialFiles(ctx);
6273
+ const p = pathApi(ctx);
6274
+ for (const root of walk.roots.length ? walk.roots : ["."]) for (const alt of braceAlternatives(root)) {
6275
+ const dir = resolvePattern(alt, ctx);
6276
+ if (files.some((file) => holds(dir, p.dirname(file), walk.recursive, ctx) && keeps(walk.names, file, dir, ctx))) return root;
6277
+ }
6278
+ }
6279
+ /**
6280
+ * A recursive search from `roots` (Claude's Grep), keeping files whose names match `names`:
6281
+ * the root it reaches a credential file from, if any.
6282
+ */
6283
+ function searchReaches(roots, names, ctx) {
6284
+ return ctx.codedenHome ? walkReaches({
6285
+ roots,
6286
+ recursive: true,
6287
+ names
6288
+ }, ctx) : void 0;
6289
+ }
6290
+ /**
6291
+ * How a command reaches CodeDen's credential files other than by naming one: a recursive search
6292
+ * from a folder above them, or a listing (or `echo`ed path) whose output `feeds` another command.
6293
+ * `paths` gives what a word may stand for (a PowerShell list's elements). Returns the word it
6294
+ * reaches them through.
6295
+ */
6296
+ function reachesThroughFolder(bin, args, ctx, feeds, paths = (word, from) => [word.slice(from)]) {
6297
+ if (!ctx.codedenHome) return void 0;
6298
+ if (SEARCHES.has(bin)) for (const walk of searchWalks(bin, args, ctx, paths)) {
6299
+ const root = walkReaches(walk, ctx);
6300
+ if (root) return root;
6301
+ }
6302
+ if (feeds || (bin === "find" || bin === "fd") && args.some((a) => RUNS.test(a))) {
6303
+ const walk = listingWalk(bin, args);
6304
+ const root = walk && walkReaches(walk, ctx);
6305
+ if (root) return root;
6306
+ }
6307
+ if (feeds && DATA.has(bin)) return args.find((a) => paths(a, 0).some((path) => touchesCodedenCredentials(path, ctx)));
6308
+ }
5658
6309
  // ../../packages/providers/src/util/git-head.ts
5659
6310
  /**
5660
6311
  * The branch checked out in the repository at `dir` (its `.git` folder, or the `gitdir:` a
@@ -5911,186 +6562,32 @@ function runsStdin(cmd) {
5911
6562
  const args = tokens.slice(1);
5912
6563
  for (let i = 0; i < args.length; i += 1) {
5913
6564
  const arg = args[i];
5914
- if (arg === "--") return isStdin(args[i + 1]);
5915
- if (!/^[-+]./.test(arg)) return isStdin(arg);
5916
- if (spec.stdin?.(arg)) return true;
5917
- if (spec.program(arg)) return !arg.includes("=") && isStdin(args[i + 1]);
5918
- if (spec.valued(arg)) i += 1;
5919
- }
5920
- return true;
5921
- }
5922
- // ../../packages/providers/src/policy/secrets.ts
5923
- /**
5924
- * Secrets rules of the risk classifier (§11.2): which words of a command read a credentials
5925
- * file, and which commands dump the environment. Writing a secrets file inside the worktree is
5926
- * not access (`cp .env.example .env`); reading one is.
5927
- */
5928
- /**
5929
- * The argument as a path, or undefined when it can't be one: inline code, multi-line text
5930
- * or anything with shell or code punctuation (`selected={row.key`) is not a file name.
5931
- */
5932
- function asPath(arg) {
5933
- const value = arg.replace(/^-+[a-z-]*=/i, "");
5934
- if (!value || /[\n\r{}()<>|;=`]/.test(value)) return void 0;
5935
- return value;
5936
- }
5937
- /** Commands whose words are data or only checked for existence (`echo .env >> .gitignore`). */
5938
- const DATA = /* @__PURE__ */ new Set([
5939
- "echo",
5940
- "printf",
5941
- "write-output",
5942
- "write-host",
5943
- "test",
5944
- "[",
5945
- "[["
5946
- ]);
5947
- /** Commands that list or describe files without reading them (`ls certs/*.pem`). */
5948
- const LISTS = /* @__PURE__ */ new Set([
5949
- "ls",
5950
- "dir",
5951
- "get-childitem",
5952
- "gci",
5953
- "find",
5954
- "fd",
5955
- "tree",
5956
- "stat",
5957
- "du",
5958
- "test-path"
5959
- ]);
5960
- /** Search commands, whose first operand is the pattern (`grep -rn "process.env" src`). */
5961
- const SEARCHES = /* @__PURE__ */ new Set([
5962
- "grep",
5963
- "egrep",
5964
- "fgrep",
5965
- "zgrep",
5966
- "rg",
5967
- "ag",
5968
- "ack",
5969
- "findstr",
5970
- "select-string",
5971
- "sls"
5972
- ]);
5973
- /** Options that take the next word as their value, per search tool. */
5974
- const GREP_VALUED = [
5975
- "-A",
5976
- "-B",
5977
- "-C",
5978
- "-m",
5979
- "-d",
5980
- "-D",
5981
- "--after-context",
5982
- "--before-context",
5983
- "--context",
5984
- "--max-count",
5985
- "--include",
5986
- "--exclude",
5987
- "--exclude-dir",
5988
- "--label",
5989
- "--binary-files",
5990
- "--devices",
5991
- "--directories"
5992
- ];
5993
- const RG_VALUED = [
5994
- ...GREP_VALUED,
5995
- "-g",
5996
- "--glob",
5997
- "--iglob",
5998
- "-t",
5999
- "--type",
6000
- "-T",
6001
- "--type-not",
6002
- "-j",
6003
- "--threads",
6004
- "--max-depth",
6005
- "-M",
6006
- "--max-columns",
6007
- "--sort",
6008
- "--sortr",
6009
- "-r",
6010
- "--replace",
6011
- "--colors",
6012
- "-E",
6013
- "--encoding"
6014
- ];
6015
- const SELECT_STRING_VALUED = [
6016
- "-path",
6017
- "-literalpath",
6018
- "-include",
6019
- "-exclude",
6020
- "-encoding",
6021
- "-context"
6022
- ];
6023
- function searchValued(bin, arg) {
6024
- if (bin === "rg") return RG_VALUED.includes(arg);
6025
- if (bin === "select-string" || bin === "sls") return SELECT_STRING_VALUED.includes(arg.toLowerCase());
6026
- return GREP_VALUED.includes(arg);
6027
- }
6028
- /**
6029
- * The search pattern words of a search command: `-e`/`--regexp`/`-Pattern` values, or the first
6030
- * operand when the pattern isn't given by an option (or a pattern file, `-f`). `findstr`'s
6031
- * `/c:"text"` is a pattern too.
6032
- */
6033
- function searchPatterns(bin, args) {
6034
- const patterns = /* @__PURE__ */ new Set();
6035
- let explicit = false;
6036
- for (let i = 0; i < args.length; i += 1) {
6037
- const a = args[i];
6038
- if (a === "-e" || a === "--regexp" || /^-pattern$/i.test(a)) {
6039
- explicit = true;
6040
- const value = args[i + 1];
6041
- if (value !== void 0) patterns.add(value);
6042
- i += 1;
6043
- } else if (a.startsWith("--regexp=") || /^\/c:/i.test(a)) {
6044
- explicit = true;
6045
- patterns.add(a);
6046
- } else if (a === "-f" || a.startsWith("--file") || /^\/g:/i.test(a)) explicit = true;
6047
- }
6048
- if (explicit) return patterns;
6049
- for (let i = 0; i < args.length; i += 1) {
6050
- const a = args[i];
6051
- if (searchValued(bin, a)) i += 1;
6052
- else if (!a.startsWith("-") && !(bin === "findstr" && a.startsWith("/"))) {
6053
- patterns.add(a);
6054
- break;
6055
- }
6565
+ if (arg === "--") return isStdin(args[i + 1]);
6566
+ if (!/^[-+]./.test(arg)) return isStdin(arg);
6567
+ if (spec.stdin?.(arg)) return true;
6568
+ if (spec.program(arg)) return !arg.includes("=") && isStdin(args[i + 1]);
6569
+ if (spec.valued(arg)) i += 1;
6056
6570
  }
6057
- return patterns;
6058
- }
6059
- /**
6060
- * The first argument that reads a credentials file, if any. `written` are the paths the command
6061
- * writes (a copy's destination), which classifyWrite judges instead.
6062
- */
6063
- function secretArgument(bin, args, written = []) {
6064
- if (DATA.has(bin)) return void 0;
6065
- const runs = (bin === "find" || bin === "fd") && args.some((a) => /^-(exec|execdir|ok|okdir|x|X|-exec|-exec-batch)$/.test(a));
6066
- if (LISTS.has(bin) && !runs) return void 0;
6067
- const patterns = SEARCHES.has(bin) ? searchPatterns(bin, args) : /* @__PURE__ */ new Set();
6068
- return args.find((a) => {
6069
- if (patterns.has(a) || written.includes(a)) return false;
6070
- const value = asPath(a);
6071
- return value !== void 0 && isSecretPath(value);
6072
- });
6073
- }
6074
- /** Files a tool writes through its own options (`openssl req -keyout key.pem -out cert.pem`). */
6075
- function toolOutputs(bin, args) {
6076
- if (bin !== "openssl" && bin !== "ssh-keygen") return [];
6077
- const flags = bin === "openssl" ? ["-out", "-keyout"] : ["-f"];
6078
- return args.flatMap((a, i) => flags.includes(a) && args[i + 1] ? [args[i + 1]] : []);
6571
+ return true;
6079
6572
  }
6080
- /** Variable names that may hold a credential. */
6081
- const SECRET_VAR = /key|token|secret|passw|pwd|credential|auth|private|session|cookie/i;
6573
+ // ../../packages/providers/src/policy/shell.ts
6082
6574
  /**
6083
- * True for `printenv` and `Get-ChildItem env:` listings. Printing named variables that don't
6084
- * look like credentials (`printenv PATH`, `Get-ChildItem env:NODE_ENV`) is not a dump.
6575
+ * The paths a word of `cmd` may stand for, from `from` on: its text and, for a PowerShell list
6576
+ * (`a,b`), each element. The whole text stays: when the shell is unknown it may be one name, or a
6577
+ * brace expansion (`{a,b}`). Callers apply it after reading the word's role, so a list given as a
6578
+ * search pattern stays data.
6085
6579
  */
6086
- function dumpsEnvironment(bin, args) {
6087
- const named = (names) => names.length > 0 && names.every((n) => /^\w+$/.test(n) && !SECRET_VAR.test(n));
6088
- if (bin === "printenv") return !named(args.filter((a) => !a.startsWith("-")));
6089
- if (!/^(get-childitem|gci|ls|dir)$/.test(bin)) return false;
6090
- const drives = args.filter((a) => /^env:/i.test(a));
6091
- return drives.length > 0 && !named(drives.map((a) => a.slice(4)));
6580
+ function wordPaths(cmd, word, from = 0) {
6581
+ const text = word.slice(from);
6582
+ const commas = cmd.lists?.get(word)?.filter((at) => at >= from) ?? [];
6583
+ if (!commas.length) return [text];
6584
+ const bounds = [
6585
+ from - 1,
6586
+ ...commas,
6587
+ word.length
6588
+ ];
6589
+ return [text, ...bounds.slice(1).map((end, k) => word.slice(bounds[k] + 1, end)).filter(Boolean)];
6092
6590
  }
6093
- // ../../packages/providers/src/policy/shell.ts
6094
6591
  /** Parse a command line into its simple commands. */
6095
6592
  function parseShell(src, dialect = "auto") {
6096
6593
  return new Lexer(src, dialect).list(null);
@@ -6111,6 +6608,8 @@ var Builder = class {
6111
6608
  target = "word";
6112
6609
  heredocStrip = false;
6113
6610
  heredocs = [];
6611
+ /** Where the word's unquoted commas are: a PowerShell list's element boundaries. */
6612
+ commas = [];
6114
6613
  constructor(out) {
6115
6614
  this.out = out;
6116
6615
  }
@@ -6118,6 +6617,11 @@ var Builder = class {
6118
6617
  this.word += text;
6119
6618
  this.inWord = true;
6120
6619
  }
6620
+ /** An unquoted comma, which separates a PowerShell list's elements (`a,b`). */
6621
+ addComma() {
6622
+ this.commas.push(this.word.length);
6623
+ this.add(",");
6624
+ }
6121
6625
  endWord() {
6122
6626
  if (!this.inWord) return;
6123
6627
  if (this.target === "write") this.cmd.writes.push(this.word);
@@ -6125,10 +6629,24 @@ var Builder = class {
6125
6629
  delim: this.word.replace(/\\/g, ""),
6126
6630
  strip: this.heredocStrip
6127
6631
  });
6128
- else if (this.target === "word") this.cmd.words.push(this.word);
6632
+ else if (this.target === "word") {
6633
+ this.cmd.words.push(this.word);
6634
+ if (this.commas.length) this.addList();
6635
+ } else if (this.target === "skip") this.cmd.input = [...this.cmd.input ?? [], this.word];
6129
6636
  this.target = "word";
6130
6637
  this.word = "";
6131
6638
  this.inWord = false;
6639
+ this.commas = [];
6640
+ }
6641
+ /**
6642
+ * Note where the current word's unquoted commas are. The same text written twice with different
6643
+ * quoting keeps every comma either one had, so its list is never missed.
6644
+ */
6645
+ addList() {
6646
+ this.cmd.lists ??= /* @__PURE__ */ new Map();
6647
+ const seen = this.cmd.lists.get(this.word) ?? [];
6648
+ const commas = [.../* @__PURE__ */ new Set([...seen, ...this.commas])].sort((a, b) => a - b);
6649
+ this.cmd.lists.set(this.word, commas);
6132
6650
  }
6133
6651
  endCommand() {
6134
6652
  this.endWord();
@@ -6205,6 +6723,9 @@ var Lexer = class {
6205
6723
  this.i += 1;
6206
6724
  b.cmd.nested.push(this.list(")"));
6207
6725
  b.add(src.slice(start, this.i));
6726
+ } else if (c === "," && this.dialect !== "posix") {
6727
+ b.addComma();
6728
+ this.i += 1;
6208
6729
  } else {
6209
6730
  b.add(c);
6210
6731
  this.i += 1;
@@ -6347,12 +6868,62 @@ const MAX_DEPTH = 4;
6347
6868
  * Classify a shell command (any shell: POSIX, PowerShell or cmd). Downloads piped into an
6348
6869
  * interpreter (`curl … | sh`, `irm … | iex`) are found on the parsed pipeline in
6349
6870
  * `classifyList`; here, a download run through a substitution (`iex (irm …)`,
6350
- * `bash -c "$(curl …)"`, `bash <(curl …)`).
6871
+ * `bash -c "$(curl …)"`, `bash <(curl …)`). CodeDen's own credentials are looked for first,
6872
+ * across the whole command, so an earlier ordinary risk (`cat .env; …`) can't hide them.
6351
6873
  */
6352
6874
  function classifyCommand(command, ctx) {
6875
+ const list = parseShell(command, ctx.shell ?? "auto");
6876
+ const credentials = credentialAccess(list, ctx, 0);
6877
+ if (credentials) return credentials;
6353
6878
  if (/\b(iex|invoke-expression)\b[^|]*\b(irm|iwr|invoke-restmethod|invoke-webrequest|downloadstring)\b/i.test(command)) return risky("network", "Executes a downloaded script");
6354
6879
  if (/\b(bash|sh|zsh)\s+(-c\s+)?["']?\$\(\s*(curl|wget)\b/i.test(command) || /<\(\s*(curl|wget)\b/i.test(command)) return risky("network", "Executes a downloaded script");
6355
- return classifyList(parseShell(command, ctx.shell ?? "auto"), ctx, 0);
6880
+ return classifyList(list, ctx, 0);
6881
+ }
6882
+ /**
6883
+ * The first access to CodeDen's own credential files anywhere in `list`: substitutions,
6884
+ * subshells and inline scripts (`bash -c "…"`) included, following `cd` like `classifyList`.
6885
+ * `feeds`: the list's output is used by another command (it sits in a substitution or a pipe).
6886
+ */
6887
+ function credentialAccess(list, ctx, depth, feeds = false) {
6888
+ let here = ctx;
6889
+ for (const [i, cmd] of list.entries()) {
6890
+ for (const nested of cmd.nested) {
6891
+ const found = credentialAccess(nested, here, depth, true);
6892
+ if (found) return found;
6893
+ }
6894
+ const piped = list[i + 1]?.piped === true;
6895
+ const found = credentialCommand(cmd, here, depth, feeds || piped);
6896
+ if (found) return found;
6897
+ here = afterCd(cmd, here);
6898
+ }
6899
+ }
6900
+ /** The paths each word of `cmd` may stand for: itself, and a PowerShell list's elements. */
6901
+ const pathsOf = (cmd) => (word, from) => wordPaths(cmd, word, from);
6902
+ /**
6903
+ * One simple command's access to CodeDen's credentials: a redirect onto one, any argument (or
6904
+ * here-string) that reaches one (read, written, copied or deleted), or a search or listing that
6905
+ * reaches one from a folder above it (`./codeden-search`). Moving into CodeDen's home with `cd`
6906
+ * reads nothing; the command that then reads is caught.
6907
+ */
6908
+ function credentialCommand(cmd, ctx, depth, feeds) {
6909
+ const { tokens } = unwrap(cmd.words);
6910
+ const bin = binName(tokens[0]);
6911
+ const args = tokens.slice(1);
6912
+ const script = inlineScript(bin, args);
6913
+ if (script && depth < MAX_DEPTH) {
6914
+ const inner = credentialAccess(parseShell(script.text, script.dialect), ctx, depth + 1, feeds);
6915
+ if (inner) return inner;
6916
+ }
6917
+ for (const target of cmd.writes) if (!DISCARD.test(target) && touchesCodedenCredentials(target, ctx)) return credentialsRisk(`Writes CodeDen's own credentials: ${target}`);
6918
+ const given = (cmd.input ?? []).find((word) => touchesCodedenCredentials(word, ctx));
6919
+ if (given) return credentialsRisk(`Passes CodeDen's own credentials: ${given}`);
6920
+ if (CD.has(bin)) return void 0;
6921
+ const paths = pathsOf(cmd);
6922
+ const reads = (a) => touchesCodedenCredentials(a, ctx);
6923
+ const touched = secretArgument(bin, args, [], reads, paths);
6924
+ if (touched) return credentialsRisk(`Accesses CodeDen's own credentials: ${touched}`);
6925
+ const through = reachesThroughFolder(bin, args, ctx, feeds, paths);
6926
+ return through ? credentialsRisk(`Reaches CodeDen's own credentials from ${through}`) : void 0;
6356
6927
  }
6357
6928
  /**
6358
6929
  * Classify commands in order. A `cd` moves where later relative paths resolve; commands in
@@ -6392,10 +6963,17 @@ function afterCd(cmd, ctx) {
6392
6963
  dir: resolveAgainst(target, ctx)
6393
6964
  };
6394
6965
  }
6395
- /** The value after a PowerShell-style named parameter (`-Path x`), if present. */
6966
+ /**
6967
+ * The value of a PowerShell-style named parameter, if present: `-Path x`, or bound with a
6968
+ * colon, `-Path:x`.
6969
+ */
6396
6970
  function namedArg(args, names) {
6397
- const i = args.findIndex((a) => names.includes(a.toLowerCase()));
6398
- return i >= 0 ? args[i + 1] : void 0;
6971
+ for (let i = 0; i < args.length; i += 1) {
6972
+ const arg = args[i].toLowerCase();
6973
+ if (names.includes(arg)) return args[i + 1];
6974
+ const bound = names.find((name) => arg.startsWith(`${name}:`));
6975
+ if (bound) return args[i].slice(bound.length + 1);
6976
+ }
6399
6977
  }
6400
6978
  /** The script of an inline shell invocation (`bash -c "…"`, `pwsh -Command …`, `cmd /c …`). */
6401
6979
  function inlineScript(bin, args) {
@@ -6478,7 +7056,7 @@ function classifySimple(cmd, ctx, depth) {
6478
7056
  const write = classifyWrite(target, ctx);
6479
7057
  if (write.risky) return write;
6480
7058
  }
6481
- const secret = secretArgument(bin, args, written);
7059
+ const secret = secretArgument(bin, args, written, isSecretPath, pathsOf(cmd));
6482
7060
  if (secret) return risky("secrets", `Accesses the secrets file ${secret}`);
6483
7061
  if (dumpsEnvironment(bin, args)) return risky("secrets", "Dumps environment variables");
6484
7062
  return SAFE;
@@ -6535,18 +7113,36 @@ function copyTargets(bin, args, dest, ctx) {
6535
7113
  * Classify a file write/edit. The worktree, extra roots and OS temp are fine, secrets files in
6536
7114
  * the worktree included: writing one (`cp .env.example .env`) reads nothing. A secrets file in
6537
7115
  * temp counts as outside the worktree, so Full autonomy (which only asks for writes outside the
6538
- * worktree) still asks for it.
7116
+ * worktree) still asks for it. CodeDen's own credential files ask at every level.
6539
7117
  */
6540
7118
  function classifyWrite(target, ctx) {
7119
+ if (touchesCodedenCredentials(target, ctx)) return credentialsRisk(`Writes CodeDen's own credentials: ${target}`);
6541
7120
  if (!isWritable(target, ctx)) return risky("outside_worktree", `Writes ${target}, outside the worktree`);
6542
7121
  if (isSecretPath(target) && !isInWorkspace(target, ctx)) return risky("outside_worktree", `Writes the secrets file ${target}, outside the worktree`);
6543
7122
  return SAFE;
6544
7123
  }
6545
- /** Classify a file read or search. Reads anywhere are fine except credential files. */
6546
- function classifyRead(target, _ctx) {
7124
+ /**
7125
+ * Classify a file read or search. Reads anywhere are fine except credential files; CodeDen's own
7126
+ * (or a folder holding them) ask at every level.
7127
+ */
7128
+ function classifyRead(target, ctx) {
7129
+ if (touchesCodedenCredentials(target, ctx)) return credentialsRisk(`Reads CodeDen's own credentials: ${target}`);
6547
7130
  if (isSecretPath(target)) return risky("secrets", `Reads the secrets file ${target}`);
6548
7131
  return SAFE;
6549
7132
  }
7133
+ /**
7134
+ * Classify a file-name search: `pattern` (Claude's Glob, or a search's `--glob` filter) from
7135
+ * `base` (default: the current directory). Listing names reads nothing, so only a pattern that
7136
+ * reaches CodeDen's own credential files asks; `{a,b}` alternatives are followed.
7137
+ */
7138
+ function classifyGlob(pattern, base, ctx) {
7139
+ if (!pattern.trim()) return SAFE;
7140
+ const dir = base ? resolveAgainst(base, ctx) : ctx.dir ?? ctx.cwd;
7141
+ return braceAlternatives(pattern).some((alt) => touchesCodedenCredentials(alt, {
7142
+ ...ctx,
7143
+ dir
7144
+ })) ? credentialsRisk(`Lists CodeDen's own credential files: ${pattern}`) : SAFE;
7145
+ }
6550
7146
  /** Classify a web fetch. Only flagged when the context has an allowlist of hosts. */
6551
7147
  function classifyFetch(url, ctx) {
6552
7148
  if (!ctx.allowedHosts) return SAFE;
@@ -6772,9 +7368,11 @@ function assessClaudeTool(name, input, ctx) {
6772
7368
  if (name === "Read") return classifyRead(str(input, "file_path") ?? "", ctx);
6773
7369
  if (name === "Grep") {
6774
7370
  const target = str(input, "path");
6775
- return target ? classifyRead(target, ctx) : { risky: false };
7371
+ const filter = str(input, "glob");
7372
+ const root = searchReaches(target ? [target] : [], filter ? [filter] : [], ctx);
7373
+ return mostSevere([target ? classifyRead(target, ctx) : SAFE, root ? credentialsRisk(`Reaches CodeDen's own credentials from ${root}`) : SAFE]);
6776
7374
  }
6777
- if (name === "Glob") return { risky: false };
7375
+ if (name === "Glob") return classifyGlob(str(input, "pattern") ?? "", str(input, "path"), ctx);
6778
7376
  if (name === "WebFetch") return classifyFetch(str(input, "url") ?? "", ctx);
6779
7377
  return { risky: false };
6780
7378
  }
@@ -6806,7 +7404,8 @@ const FINISH_PLAN = "Finish the plan and present it with ExitPlanMode; once the
6806
7404
  * - auto -> `acceptEdits`, plus the risky-action classifier
6807
7405
  * - full -> `bypassPermissions`, asking only for writes outside the worktree (the OS
6808
7406
  * temp directory counts as inside, except for secrets files there)
6809
- * Plan mode overrides all of them with `plan`.
7407
+ * Plan mode overrides all of them with `plan`. CodeDen's own credential files ask at every
7408
+ * level and are refused for read-only sessions (`../policy/codeden-home`).
6810
7409
  */
6811
7410
  function permissionModeFor(autonomy, planMode) {
6812
7411
  if (planMode) return "plan";
@@ -6824,6 +7423,8 @@ var ClaudePolicy = class {
6824
7423
  ctx;
6825
7424
  readOnly;
6826
7425
  sessionAllow = /* @__PURE__ */ new Set();
7426
+ /** `allow_session` decisions on calls that touched CodeDen's own credentials (`credentialKey`). */
7427
+ credentialAllow = /* @__PURE__ */ new Set();
6827
7428
  /** Plan mode: edits outside Claude's notes and writing commands are refused. */
6828
7429
  planning = false;
6829
7430
  constructor(autonomy, ctx, readOnly = false) {
@@ -6852,8 +7453,12 @@ var ClaudePolicy = class {
6852
7453
  setPlanning(on) {
6853
7454
  this.planning = on;
6854
7455
  }
6855
- /** Why a call is refused outright (read-only sessions, plan mode), or undefined. */
6856
- denial(toolName, input) {
7456
+ /**
7457
+ * Why a call is refused outright (read-only sessions, plan mode), or undefined. `dir` is the
7458
+ * shell's current directory, as for `gate`.
7459
+ */
7460
+ denial(toolName, input, dir) {
7461
+ if (this.isReadOnly && touchesCodeden(this.assess(toolName, input, dir))) return this.readOnly ? "This session is read-only: CodeDen's own credential files are off limits. Act through CodeDen tools instead." : "This session has read-only permissions: CodeDen's own credential files are off limits. Report what you found instead.";
6857
7462
  if (this.planning && !this.readOnly && this.isNotesEdit(toolName, input)) return void 0;
6858
7463
  if (!this.isReadOnly) return this.planning ? this.planDenial(toolName, input) : void 0;
6859
7464
  if (EDIT_TOOLS$1.has(toolName)) return this.readOnly ? "This session is read-only: it cannot edit files. Act through CodeDen tools instead." : "This session has read-only permissions: it cannot edit files, commit or push. Report what you found instead.";
@@ -6894,11 +7499,8 @@ var ClaudePolicy = class {
6894
7499
  action: "plan",
6895
7500
  reason: "The agent finished planning"
6896
7501
  };
6897
- if (this.sessionAllow.has(this.key(toolName, input))) return { ask: false };
6898
- const risk = assessClaudeTool(toolName, input, dir ? {
6899
- ...this.ctx,
6900
- dir
6901
- } : this.ctx);
7502
+ const risk = this.assess(toolName, input, dir);
7503
+ if (touchesCodeden(risk) ? this.credentialAllow.has(this.credentialKey(toolName, input, dir)) : this.sessionAllow.has(this.key(toolName, input))) return { ask: false };
6902
7504
  if (this.autonomy === "supervised") {
6903
7505
  if (risk.risky) return {
6904
7506
  ask: true,
@@ -6913,13 +7515,19 @@ var ClaudePolicy = class {
6913
7515
  return { ask: false };
6914
7516
  }
6915
7517
  if (!risk.risky) return { ask: false };
6916
- if (this.autonomy === "full" && risk.action !== "outside_worktree") return { ask: false };
7518
+ if (this.autonomy === "full" && risk.action !== "outside_worktree" && !risk.codeden) return { ask: false };
6917
7519
  return {
6918
7520
  ask: true,
6919
7521
  action: risk.action,
6920
7522
  reason: risk.reason
6921
7523
  };
6922
7524
  }
7525
+ assess(toolName, input, dir) {
7526
+ return assessClaudeTool(toolName, input, dir ? {
7527
+ ...this.ctx,
7528
+ dir
7529
+ } : this.ctx);
7530
+ }
6923
7531
  /**
6924
7532
  * Whether a `canUseTool` call (Claude's own permission prompt) needs the user. In supervised
6925
7533
  * mode everything Claude asks about goes to the user; otherwise only gated calls do.
@@ -6934,9 +7542,17 @@ var ClaudePolicy = class {
6934
7542
  reason: "Claude asked for permission"
6935
7543
  };
6936
7544
  }
6937
- /** Remember an `allow_session` decision. Commands and edits match exactly; others by name. */
6938
- allowForSession(toolName, input) {
7545
+ /**
7546
+ * Remember an `allow_session` decision. Commands and edits match exactly; others by name. A
7547
+ * call that touches CodeDen's own credentials is remembered only as itself, in `dir`.
7548
+ */
7549
+ allowForSession(toolName, input, dir) {
6939
7550
  this.sessionAllow.add(this.key(toolName, input));
7551
+ if (touchesCodeden(this.assess(toolName, input, dir))) this.credentialAllow.add(this.credentialKey(toolName, input, dir));
7552
+ }
7553
+ /** The exact call, in the directory its relative paths resolve from. */
7554
+ credentialKey(toolName, input, dir) {
7555
+ return `${toolName}\n${dir ?? this.ctx.cwd}\n${JSON.stringify(input)}`;
6940
7556
  }
6941
7557
  key(toolName, input) {
6942
7558
  if (COMMAND_TOOLS.has(toolName)) return `${toolName}:${str(input, "command") ?? ""}`;
@@ -7695,7 +8311,7 @@ function policyHook(policy, shell) {
7695
8311
  const toolInput = hookInput.tool_input ?? {};
7696
8312
  const cwd = str(hookInput, "cwd");
7697
8313
  if (cwd && (path.isAbsolute(cwd) || path.win32.isAbsolute(cwd))) shell.dir = cwd;
7698
- const denied = policy.denial(name, toolInput);
8314
+ const denied = policy.denial(name, toolInput, shell.dir);
7699
8315
  if (denied) return { hookSpecificOutput: {
7700
8316
  hookEventName: "PreToolUse",
7701
8317
  permissionDecision: "deny",
@@ -8024,23 +8640,130 @@ function statsFromUsageTrailer(text) {
8024
8640
  };
8025
8641
  return stats(field("total_tokens"), field("tool_uses"), field("duration_ms"));
8026
8642
  }
8643
+ // ../../packages/providers/src/claude/subagent-usage.ts
8644
+ /** Token counts of one call from a Messages API `usage` object; undefined when it has none. */
8645
+ function callTokens(usage) {
8646
+ if (!usage || typeof usage !== "object") return void 0;
8647
+ const u = usage;
8648
+ const details = u.output_tokens_details;
8649
+ const tokens = {
8650
+ input: count(u.input_tokens) ?? 0,
8651
+ output: count(u.output_tokens) ?? 0,
8652
+ cacheRead: count(u.cache_read_input_tokens) ?? 0,
8653
+ cacheWrite: count(u.cache_creation_input_tokens) ?? 0,
8654
+ reasoning: count(details?.thinking_tokens) ?? 0
8655
+ };
8656
+ return tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite > 0 ? tokens : void 0;
8657
+ }
8658
+ /**
8659
+ * Tokens per model across one subagent's own API calls, for its card's cost (§7.3). Claude
8660
+ * streams a call as several `assistant` frames sharing its message id (one per content block),
8661
+ * each with the call's usage as it stood when the call started: input and cache counts are
8662
+ * final, output is not. So calls are keyed by message id, frames of one call merge (the larger
8663
+ * figure wins), and the subagent's transcript, read once it has finished, supplies the final
8664
+ * figures (`applyTranscript`).
8665
+ */
8666
+ var SubagentUsage = class {
8667
+ calls = /* @__PURE__ */ new Map();
8668
+ /** Fold in one streamed frame (`message` of an `assistant` message). */
8669
+ addFrame(message) {
8670
+ const id = str(message, "id");
8671
+ const tokens = callTokens(message.usage);
8672
+ if (!id || !tokens) return;
8673
+ const model = modelOf(message);
8674
+ const prev = this.calls.get(id);
8675
+ this.calls.set(id, {
8676
+ ...model || prev?.model ? { model: model ?? prev?.model } : {},
8677
+ input: Math.max(tokens.input, prev?.input ?? 0),
8678
+ output: Math.max(tokens.output, prev?.output ?? 0),
8679
+ cacheRead: Math.max(tokens.cacheRead, prev?.cacheRead ?? 0),
8680
+ cacheWrite: Math.max(tokens.cacheWrite, prev?.cacheWrite ?? 0),
8681
+ reasoning: Math.max(tokens.reasoning, prev?.reasoning ?? 0)
8682
+ });
8683
+ }
8684
+ /**
8685
+ * Take the final figures of the calls the frames showed from the subagent's transcript
8686
+ * (`getSubagentMessages`): per message id, the transcript wins. Calls only the transcript
8687
+ * holds are left out (an earlier run of a resumed subagent); calls it doesn't hold yet keep
8688
+ * their streamed figures. Returns whether anything changed.
8689
+ */
8690
+ applyTranscript(messages) {
8691
+ let changed = false;
8692
+ for (const entry of messages) {
8693
+ const raw = str(entry, "type") === "assistant" ? entry.message : void 0;
8694
+ if (!raw || typeof raw !== "object") continue;
8695
+ const message = raw;
8696
+ const id = str(message, "id");
8697
+ const prev = id ? this.calls.get(id) : void 0;
8698
+ const tokens = callTokens(message.usage);
8699
+ if (!id || !prev || !tokens) continue;
8700
+ const model = modelOf(message) ?? prev.model;
8701
+ const next = {
8702
+ ...model ? { model } : {},
8703
+ ...tokens
8704
+ };
8705
+ if (!sameCall(prev, next)) {
8706
+ this.calls.set(id, next);
8707
+ changed = true;
8708
+ }
8709
+ }
8710
+ return changed;
8711
+ }
8712
+ /** Tokens per model in the order the models were first used; undefined with no calls. */
8713
+ summary() {
8714
+ const byModel = /* @__PURE__ */ new Map();
8715
+ for (const call of this.calls.values()) {
8716
+ const key = call.model ?? "";
8717
+ const sum = byModel.get(key);
8718
+ byModel.set(key, {
8719
+ ...call.model ? { model: call.model } : {},
8720
+ input: (sum?.input ?? 0) + call.input,
8721
+ output: (sum?.output ?? 0) + call.output,
8722
+ cacheRead: (sum?.cacheRead ?? 0) + call.cacheRead,
8723
+ cacheWrite: (sum?.cacheWrite ?? 0) + call.cacheWrite,
8724
+ reasoning: (sum?.reasoning ?? 0) + call.reasoning
8725
+ });
8726
+ }
8727
+ if (byModel.size === 0) return void 0;
8728
+ return [...byModel.values()].map((c) => ({
8729
+ ...c.model ? { model: c.model } : {},
8730
+ inputTokens: c.input,
8731
+ outputTokens: c.output,
8732
+ cacheReadTokens: c.cacheRead,
8733
+ cacheWriteTokens: c.cacheWrite,
8734
+ ...c.reasoning > 0 ? { reasoningTokens: c.reasoning } : {}
8735
+ }));
8736
+ }
8737
+ };
8738
+ /** The model that served a call; Claude Code marks its own placeholder replies `<synthetic>`. */
8739
+ function modelOf(message) {
8740
+ const model = str(message, "model")?.trim();
8741
+ return model && model !== "<synthetic>" ? model : void 0;
8742
+ }
8743
+ function sameCall(a, b) {
8744
+ return a.model === b.model && a.input === b.input && a.output === b.output && a.cacheRead === b.cacheRead && a.cacheWrite === b.cacheWrite && a.reasoning === b.reasoning;
8745
+ }
8027
8746
  // ../../packages/providers/src/claude/subagents.ts
8028
8747
  /**
8029
8748
  * Subagent (Agent/Task tool) runs as child threads (§7.3). The parent gets a `subagent` item;
8030
8749
  * messages tagged with `parent_tool_use_id` go to the child thread. Background subagents
8031
- * finish on `task_notification`, foreground ones on their `tool_result`.
8750
+ * finish on `task_notification`, foreground ones on their `tool_result`. The card's figures
8751
+ * include the run's tokens per model, from its streamed messages; `onFinished` lets the
8752
+ * session read the final figures from the run's transcript (`correctUsage`).
8032
8753
  */
8033
8754
  var SubagentTracker = class {
8034
8755
  sessionId;
8035
8756
  emit;
8036
8757
  newId;
8037
8758
  onThreadEnd;
8759
+ onFinished;
8038
8760
  runs = /* @__PURE__ */ new Map();
8039
- constructor(sessionId, emit, newId, onThreadEnd = () => {}) {
8761
+ constructor(sessionId, emit, newId, onThreadEnd = () => {}, onFinished = () => {}) {
8040
8762
  this.sessionId = sessionId;
8041
8763
  this.emit = emit;
8042
8764
  this.newId = newId;
8043
8765
  this.onThreadEnd = onThreadEnd;
8766
+ this.onFinished = onFinished;
8044
8767
  }
8045
8768
  ctx(toolUseId) {
8046
8769
  return this.runs.get(toolUseId)?.child;
@@ -8068,7 +8791,8 @@ var SubagentTracker = class {
8068
8791
  child,
8069
8792
  item,
8070
8793
  background: input.run_in_background === true,
8071
- done: false
8794
+ done: false,
8795
+ usage: new SubagentUsage()
8072
8796
  });
8073
8797
  this.emit({
8074
8798
  type: "thread.started",
@@ -8092,11 +8816,43 @@ var SubagentTracker = class {
8092
8816
  }
8093
8817
  onTaskStarted(msg) {
8094
8818
  const run = this.runs.get(str(msg, "tool_use_id") ?? "");
8095
- if (run && msg.is_backgrounded === true) run.background = true;
8819
+ if (!run) return;
8820
+ if (msg.is_backgrounded === true) run.background = true;
8821
+ run.agentId ??= str(msg, "task_id");
8822
+ }
8823
+ /** One of the run's own `assistant` messages (`parent_tool_use_id` is its tool use). */
8824
+ onAssistant(toolUseId, msg) {
8825
+ const run = this.runs.get(toolUseId);
8826
+ if (!run || run.done) return;
8827
+ run.agentId ??= str(msg, "agent_id");
8828
+ if (msg.message && typeof msg.message === "object") run.usage.addFrame(msg.message);
8829
+ }
8830
+ /**
8831
+ * The final figures of a finished run's calls, from its transcript: the card is sent again
8832
+ * when they change what it shows (§7.3).
8833
+ */
8834
+ correctUsage(toolUseId, transcript) {
8835
+ const run = this.runs.get(toolUseId);
8836
+ if (!run?.done || !run.usage.applyTranscript(transcript)) return;
8837
+ const usage = run.usage.summary();
8838
+ if (!usage) return;
8839
+ run.item = {
8840
+ ...run.item,
8841
+ stats: {
8842
+ ...run.item.stats,
8843
+ usage
8844
+ }
8845
+ };
8846
+ this.emit({
8847
+ type: "item.completed",
8848
+ ...run.parent,
8849
+ item: run.item
8850
+ });
8096
8851
  }
8097
8852
  onToolResult(toolUseId, content, isError, toolUseResult) {
8098
8853
  const run = this.runs.get(toolUseId);
8099
8854
  if (!run || run.done) return;
8855
+ run.agentId ??= str(toolUseResult, "agentId");
8100
8856
  const status = str(toolUseResult, "status");
8101
8857
  if (!isError && (status === "async_launched" || toolUseResult?.isAsync === true)) {
8102
8858
  run.background = true;
@@ -8109,6 +8865,8 @@ var SubagentTracker = class {
8109
8865
  onTaskNotification(msg) {
8110
8866
  const toolUseId = str(msg, "tool_use_id");
8111
8867
  if (!toolUseId) return;
8868
+ const run = this.runs.get(toolUseId);
8869
+ if (run) run.agentId ??= str(msg, "task_id");
8112
8870
  const status = str(msg, "status");
8113
8871
  const summary = str(msg, "summary");
8114
8872
  this.finish(toolUseId, status === "completed" ? "completed" : status === "failed" ? "failed" : "interrupted", summary, statsFromTaskNotification(msg));
@@ -8141,16 +8899,23 @@ var SubagentTracker = class {
8141
8899
  reason: "user"
8142
8900
  });
8143
8901
  const status = outcome === "completed" ? "completed" : outcome === "failed" ? "failed" : "cancelled";
8902
+ const usage = run.usage.summary();
8903
+ const figures = usage ? {
8904
+ ...stats,
8905
+ usage
8906
+ } : stats;
8907
+ run.item = {
8908
+ ...run.item,
8909
+ status,
8910
+ ...summary ? { summary } : {},
8911
+ ...figures ? { stats: figures } : {}
8912
+ };
8144
8913
  this.emit({
8145
8914
  type: "item.completed",
8146
8915
  ...run.parent,
8147
- item: {
8148
- ...run.item,
8149
- status,
8150
- ...summary ? { summary } : {},
8151
- ...stats ? { stats } : {}
8152
- }
8916
+ item: run.item
8153
8917
  });
8918
+ if (usage && run.agentId) this.onFinished(toolUseId, run.agentId);
8154
8919
  }
8155
8920
  };
8156
8921
  /** Drop the CLI's internal trailer (agent ids, usage) from a subagent result. */
@@ -8434,13 +9199,19 @@ var ClaudeEventMapper = class {
8434
9199
  retryNoticeId;
8435
9200
  /** What the current turn said about a failure, for classifying it (`errors.ts`). */
8436
9201
  signals = {};
9202
+ /**
9203
+ * The `uuid` of the current root turn's last main-chain transcript message (an `assistant`
9204
+ * or `user` message without `parent_tool_use_id`): the turn's fork point, where
9205
+ * `forkSession({ upToMessageId })` ends a copy that keeps this turn and no later one.
9206
+ */
9207
+ lastChainUuid;
8437
9208
  /** The `/` command list last reported, so an unchanged list isn't sent again. */
8438
9209
  commandsKey;
8439
9210
  constructor(opts) {
8440
9211
  this.rootThreadId = `${opts.sessionId}:thread-root`;
8441
9212
  this.emit = opts.emit;
8442
9213
  this.newId = opts.newId ?? randomUUID;
8443
- this.subagents = new SubagentTracker(opts.sessionId, this.emit, this.newId, (ctx, ok) => this.tools.finishTurn(ctx, ok));
9214
+ this.subagents = new SubagentTracker(opts.sessionId, this.emit, this.newId, (ctx, ok) => this.tools.finishTurn(ctx, ok), opts.onSubagentFinished);
8444
9215
  this.tools = new ToolTracker(opts.cwd, (ctx, phase, item) => this.item(ctx, phase, item), this.subagents, this.newId);
8445
9216
  this.stream = new StreamAssembler({
8446
9217
  rootCtx: () => this.rootCtx(),
@@ -8491,6 +9262,7 @@ var ClaudeEventMapper = class {
8491
9262
  this.rootTurnId = turnId;
8492
9263
  this.turnBaseline = this.queryTotal;
8493
9264
  this.signals = {};
9265
+ this.lastChainUuid = void 0;
8494
9266
  this.emit({
8495
9267
  type: "turn.started",
8496
9268
  threadId: this.rootThreadId,
@@ -8517,6 +9289,13 @@ var ClaudeEventMapper = class {
8517
9289
  ctxForTool(toolUseId) {
8518
9290
  return this.tools.ctx(toolUseId) ?? this.rootCtx();
8519
9291
  }
9292
+ /**
9293
+ * A finished subagent's transcript (`getSubagentMessages`): its calls' final token counts
9294
+ * replace the streamed ones, and the card is sent again when that changes it.
9295
+ */
9296
+ correctSubagentUsage(toolUseId, transcript) {
9297
+ this.subagents.correctUsage(toolUseId, transcript);
9298
+ }
8520
9299
  /** Emit an item event in a context. */
8521
9300
  item(ctx, phase, item) {
8522
9301
  this.emit({
@@ -8535,9 +9314,11 @@ var ClaudeEventMapper = class {
8535
9314
  return;
8536
9315
  case "assistant":
8537
9316
  this.onAssistant(msg);
9317
+ this.noteChainMessage(msg);
8538
9318
  return;
8539
9319
  case "user":
8540
9320
  this.onUser(msg);
9321
+ this.noteChainMessage(msg);
8541
9322
  return;
8542
9323
  case "system":
8543
9324
  this.onSystem(msg);
@@ -8560,12 +9341,23 @@ var ClaudeEventMapper = class {
8560
9341
  default: return;
8561
9342
  }
8562
9343
  }
9344
+ /**
9345
+ * Keep a root turn's latest main-chain message as its fork point. Subagents' messages don't
9346
+ * count, nor do meta or synthetic ones, which the transcript read that confirms a fork point
9347
+ * (`getSessionMessages`) may leave out.
9348
+ */
9349
+ noteChainMessage(msg) {
9350
+ if (msg.parent_tool_use_id || !this.rootTurnId || msg.isMeta || msg.isSynthetic) return;
9351
+ const uuid = str(msg, "uuid");
9352
+ if (uuid) this.lastChainUuid = uuid;
9353
+ }
8563
9354
  onAssistant(msg) {
8564
9355
  const message = msg.message ?? {};
8565
9356
  const ctx = this.ctxFor(msg.parent_tool_use_id);
8566
9357
  const isRoot = !msg.parent_tool_use_id;
8567
9358
  const messageId = str(message, "id") ?? this.newId();
8568
9359
  if (isRoot) this.contextTokens = contextTokensFromMessageUsage(message.usage) ?? this.contextTokens;
9360
+ else if (typeof msg.parent_tool_use_id === "string") this.subagents.onAssistant(msg.parent_tool_use_id, msg);
8569
9361
  const content = Array.isArray(message.content) ? message.content : [];
8570
9362
  for (const block of content) if (block.type === "text" || block.type === "thinking" || block.type === "redacted_thinking") {
8571
9363
  const type = block.type === "text" ? "text" : "thinking";
@@ -8703,19 +9495,24 @@ var ClaudeEventMapper = class {
8703
9495
  this.tools.finishTurn(ctx, outcome.type === "completed");
8704
9496
  this.rootTurnId = void 0;
8705
9497
  this.interruptRequested = false;
9498
+ const end = {
9499
+ ...ctx,
9500
+ ...this.lastChainUuid ? { forkPoint: this.lastChainUuid } : {}
9501
+ };
9502
+ this.lastChainUuid = void 0;
8706
9503
  if (outcome.type === "completed") this.emit({
8707
9504
  type: "turn.completed",
8708
- ...ctx,
9505
+ ...end,
8709
9506
  stopReason: outcome.stopReason
8710
9507
  });
8711
9508
  else if (outcome.type === "failed") this.emit({
8712
9509
  type: "turn.failed",
8713
- ...ctx,
9510
+ ...end,
8714
9511
  error: outcome.error
8715
9512
  });
8716
9513
  else this.emit({
8717
9514
  type: "turn.interrupted",
8718
- ...ctx,
9515
+ ...end,
8719
9516
  reason: outcome.reason
8720
9517
  });
8721
9518
  }
@@ -8967,7 +9764,10 @@ var ClaudeSession = class {
8967
9764
  this.mapper = new ClaudeEventMapper({
8968
9765
  sessionId: opts.sessionId,
8969
9766
  cwd: opts.cwd,
8970
- emit
9767
+ emit,
9768
+ onSubagentFinished: (toolUseId, agentId) => {
9769
+ this.correctSubagentUsage(toolUseId, agentId);
9770
+ }
8971
9771
  });
8972
9772
  this.broker = new ApprovalBroker(emit);
8973
9773
  this.autonomy = opts.autonomy;
@@ -8975,6 +9775,7 @@ var ClaudeSession = class {
8975
9775
  cwd: opts.cwd,
8976
9776
  extraRoots: opts.additionalDirectories,
8977
9777
  ...opts.ownBranch ? { ownBranch: opts.ownBranch } : {},
9778
+ ...opts.codedenHome ? { codedenHome: opts.codedenHome } : {},
8978
9779
  notesRoots: config.notesRoots ?? claudeNotesDirs({
8979
9780
  cwd: opts.cwd,
8980
9781
  ...claudeHomes(opts.env)
@@ -8989,6 +9790,20 @@ var ClaudeSession = class {
8989
9790
  return this.mapper.resumeToken ?? this.opts.resumeToken;
8990
9791
  }
8991
9792
  /**
9793
+ * A subagent's streamed messages don't carry their calls' final output counts; its
9794
+ * transcript does. Read it once the run finished and let the card take the final figures.
9795
+ * If it can't be read (another config folder, not written yet), the streamed ones stay.
9796
+ */
9797
+ async correctSubagentUsage(toolUseId, agentId) {
9798
+ const read = this.config.getSubagentMessages;
9799
+ const claudeSessionId = this.resumeToken();
9800
+ if (!read || !claudeSessionId || this.closed) return;
9801
+ try {
9802
+ const transcript = await read(claudeSessionId, agentId, { dir: this.opts.cwd });
9803
+ if (!this.closed) this.mapper.correctSubagentUsage(toolUseId, transcript);
9804
+ } catch {}
9805
+ }
9806
+ /**
8992
9807
  * Switch autonomy. The policy changes at once, so the next tool call (even in a running
8993
9808
  * turn) follows the new level; approvals already waiting stay waiting. The CLI's own
8994
9809
  * permission mode follows (`applyPermissionMode`), except while planning: the CLI stays in
@@ -9211,7 +10026,7 @@ var ClaudeSession = class {
9211
10026
  mapper: this.mapper
9212
10027
  }, input, options.toolUseID, options.signal);
9213
10028
  if (toolName === "ExitPlanMode") return this.reviewPlan(input, options);
9214
- const denied = this.policy.denial(toolName, input);
10029
+ const denied = this.policy.denial(toolName, input, this.shell.dir);
9215
10030
  if (denied) return {
9216
10031
  behavior: "deny",
9217
10032
  message: denied
@@ -9249,7 +10064,7 @@ var ClaudeSession = class {
9249
10064
  };
9250
10065
  toPermissionResult(toolName, input, response) {
9251
10066
  if (response.kind === "approval" && response.decision !== "deny") {
9252
- if (response.decision === "allow_session") this.policy.allowForSession(toolName, input);
10067
+ if (response.decision === "allow_session") this.policy.allowForSession(toolName, input, this.shell.dir);
9253
10068
  return {
9254
10069
  behavior: "allow",
9255
10070
  updatedInput: input
@@ -9393,10 +10208,11 @@ var ClaudeAdapter = class {
9393
10208
  async openSession(opts) {
9394
10209
  const executable = this.executable();
9395
10210
  if (!executable) throw new Error("Claude Code is not installed (no `claude` on PATH)");
9396
- const { query } = await (this.options.loadSdk ?? loadClaudeSdk)();
10211
+ const { query, getSubagentMessages } = await (this.options.loadSdk ?? loadClaudeSdk)();
9397
10212
  return new ClaudeSession(await withFork(this, opts), {
9398
10213
  executable,
9399
10214
  query,
10215
+ ...getSubagentMessages ? { getSubagentMessages } : {},
9400
10216
  onModels: (models) => {
9401
10217
  if (!models.length) return;
9402
10218
  this.freshness.confirm(executableFingerprint(executable));
@@ -9405,18 +10221,12 @@ var ClaudeAdapter = class {
9405
10221
  });
9406
10222
  }
9407
10223
  /**
9408
- * Copy a conversation with the SDK's `forkSession()`: the transcript is written to a new
9409
- * session file at once (fresh message ids, the original untouched) and the new session id is
9410
- * returned for resuming. The copy is found from any worktree of the project.
10224
+ * Copy a conversation (see `forkClaudeConversation`): at once, with fresh message ids, the
10225
+ * original untouched; with `upTo`, only through the turn that ended with that message. The
10226
+ * copy is found from any worktree of the project.
9411
10227
  */
9412
- async forkConversation(resumeToken) {
9413
- const { forkSession } = await (this.options.loadSdk ?? loadClaudeSdk)();
9414
- try {
9415
- return (await forkSession(resumeToken)).sessionId;
9416
- } catch (error) {
9417
- const reason = error instanceof Error ? error.message : String(error);
9418
- throw new Error(`Could not fork the Claude conversation ${resumeToken}: ${reason}`);
9419
- }
10228
+ async forkConversation(resumeToken, opts) {
10229
+ return forkClaudeConversation(await (this.options.loadSdk ?? loadClaudeSdk)(), resumeToken, opts?.upTo);
9420
10230
  }
9421
10231
  utilityModel() {
9422
10232
  return new ClaudeUtilityModel(() => this.executable());
@@ -9733,11 +10543,27 @@ async function startAppServer(exe, handlers, opts = {}) {
9733
10543
  }
9734
10544
  }
9735
10545
  // ../../packages/providers/src/codex/fork.ts
10546
+ /** Turns listed per `thread/turns/list` page, and the most pages read to find a turn. */
10547
+ const TURNS_PAGE = 100;
10548
+ const MAX_PAGES$1 = 50;
10549
+ /**
10550
+ * How the app-server answers for a thread or turn it doesn't have. Codex 0.160 says
10551
+ * `no rollout found for thread id <id>` for a deleted thread and `turn not found: <id>` for a
10552
+ * missing `lastTurnId`. Other refusals (a turn still in progress, a malformed id) don't match.
10553
+ */
10554
+ const MISSING = /\bno rollout found\b|\b(thread|turn|rollout) not found\b/i;
9736
10555
  /**
9737
10556
  * Copy a Codex thread with `thread/fork` through a short-lived `codex app-server` and return
9738
10557
  * the new thread's id. The original thread is untouched; the session that resumes the copy
9739
10558
  * sets its own cwd, policy and instructions (`thread/resume` overrides). The process is closed
9740
10559
  * before returning, so it no longer holds the new thread when that session resumes it.
10560
+ *
10561
+ * With `upTo` (a turn id), the copy ends with that turn (`lastTurnId`). A thread that no
10562
+ * longer holds the turn, or no longer exists, throws `FORK_POINT_MISSING`: when the turn list
10563
+ * (`thread/turns/list`) shows it before anything is copied, or else when `thread/fork` answers
10564
+ * so. A short-lived app-server usually can't list a thread it hasn't loaded (Codex 0.160 says
10565
+ * `thread not loaded`), so `thread/fork`'s answer is what decides then. Any other failure, and
10566
+ * every failure of a whole copy, stays a plain error.
9741
10567
  */
9742
10568
  async function forkCodexThread(exe, threadId, opts) {
9743
10569
  const { rpc } = await startAppServer(exe, {
@@ -9746,17 +10572,47 @@ async function forkCodexThread(exe, threadId, opts) {
9746
10572
  onExit: () => {}
9747
10573
  }, { cwd: opts.cwd });
9748
10574
  try {
9749
- return (await rpc.request("thread/fork", {
10575
+ if (opts.upTo && await holdsTurn(rpc, threadId, opts.upTo) === false) throw new ProviderSessionError(FORK_POINT_MISSING, "The thread no longer holds that turn (it was replaced or deleted), so it can't be forked there");
10576
+ const params = {
9750
10577
  threadId,
9751
10578
  cwd: opts.cwd,
9752
- excludeTurns: true
9753
- })).thread.id;
10579
+ excludeTurns: true,
10580
+ ...opts.upTo ? { lastTurnId: opts.upTo } : {}
10581
+ };
10582
+ return (await rpc.request("thread/fork", params)).thread.id;
9754
10583
  } catch (error) {
9755
- throw new Error(`Could not fork Codex thread ${threadId}: ${error.message}`);
10584
+ if (error instanceof ProviderSessionError) throw error;
10585
+ const message = error.message;
10586
+ if (opts.upTo && error instanceof CodexRpcError && MISSING.test(message)) throw new ProviderSessionError(FORK_POINT_MISSING, `The thread no longer holds that turn (${message}), so it can't be forked there`);
10587
+ throw new Error(`Could not fork Codex thread ${threadId}: ${message}`);
9756
10588
  } finally {
9757
10589
  await rpc.close();
9758
10590
  }
9759
10591
  }
10592
+ /**
10593
+ * Whether the thread still holds `turnId`, from its turn list. Undefined when the list can't be
10594
+ * read (a thread this app-server hasn't loaded, or an app-server without the method):
10595
+ * `thread/fork` then decides.
10596
+ */
10597
+ async function holdsTurn(rpc, threadId, turnId) {
10598
+ let cursor = null;
10599
+ try {
10600
+ for (let page = 0; page < MAX_PAGES$1; page++) {
10601
+ const res = await rpc.request("thread/turns/list", {
10602
+ threadId,
10603
+ limit: TURNS_PAGE,
10604
+ itemsView: "notLoaded",
10605
+ ...cursor ? { cursor } : {}
10606
+ });
10607
+ if (res.data.some((t) => t.id === turnId)) return true;
10608
+ cursor = res.nextCursor;
10609
+ if (!cursor) return false;
10610
+ }
10611
+ return;
10612
+ } catch {
10613
+ return;
10614
+ }
10615
+ }
9760
10616
  /** `model/list` pages are followed up to this many (a guard against a looping cursor). */
9761
10617
  const MAX_PAGES = 20;
9762
10618
  /**
@@ -9893,78 +10749,369 @@ var CodexUtilityModel = class {
9893
10749
  * same risk classifier as Claude's tool calls: what the level allows is approved by policy
9894
10750
  * (recorded with `decidedBy: "policy"`), and only the rest reaches the user.
9895
10751
  */
9896
- /** True when the permission level allows an action the classifier assessed as `risk`. */
10752
+ /**
10753
+ * True when the permission level allows an action the classifier assessed as `risk`. CodeDen's
10754
+ * own credential files are never allowed by policy. Commands Codex runs inside its sandbox
10755
+ * without asking (reads included) never reach this policy.
10756
+ */
9897
10757
  function levelAllows(autonomy, risk) {
9898
10758
  if (autonomy === "auto") return !risk.risky;
9899
- if (autonomy === "full") return !risk.risky || risk.action !== "outside_worktree";
10759
+ if (autonomy === "full") return !risk.risky || risk.action !== "outside_worktree" && !risk.codeden;
9900
10760
  return false;
9901
10761
  }
9902
- /** The first risky assessment of `items`, or safe. */
9903
- function first(items) {
9904
- return items.find((r) => r.risky) ?? { risky: false };
10762
+ /**
10763
+ * Risk of a command Codex asks to run: the command line as sent and each parsed action, so a
10764
+ * redirect or path the parsed form drops still counts. Here and below, CodeDen's own
10765
+ * credentials win over an earlier ordinary risk (`mostSevere`).
10766
+ */
10767
+ function commandRisk(texts, ctx) {
10768
+ return mostSevere(texts.filter(Boolean).map((text) => classifyCommand(text, ctx)));
10769
+ }
10770
+ /**
10771
+ * Risk of a patch Codex asks to apply: each file it changes, and the root it asks to write
10772
+ * under for the rest of the session. Unknown paths can't be judged, so they need the user.
10773
+ */
10774
+ function patchRisk(paths, grantRoot, ctx) {
10775
+ const targets = [...paths, ...grantRoot ? [grantRoot] : []];
10776
+ if (targets.length === 0) return {
10777
+ risky: true,
10778
+ action: "outside_worktree",
10779
+ reason: "Changes files Codex did not name"
10780
+ };
10781
+ return mostSevere(targets.map((target) => classifyWrite(target, ctx)));
10782
+ }
10783
+ /**
10784
+ * Risk of extra permissions Codex asks for: network access (not restricted, as for Claude,
10785
+ * while no host list applies) and folders to read or write. Patterns and special folders can't
10786
+ * be judged, so they need the user.
10787
+ */
10788
+ function permissionsRisk(permissions, ctx) {
10789
+ const fs = permissions.fileSystem;
10790
+ if (!fs) return { risky: false };
10791
+ const unknown = {
10792
+ risky: true,
10793
+ action: "outside_worktree",
10794
+ reason: "Asks for access to folders by pattern"
10795
+ };
10796
+ return mostSevere([
10797
+ ...(fs.write ?? []).map((p) => classifyWrite(p, ctx)),
10798
+ ...(fs.read ?? []).map((p) => classifyRead(p, ctx)),
10799
+ ...(fs.entries ?? []).map((entry) => {
10800
+ if (entry.path.type !== "path") return unknown;
10801
+ if (entry.access === "write") return classifyWrite(entry.path.path, ctx);
10802
+ return entry.access === "read" ? classifyRead(entry.path.path, ctx) : { risky: false };
10803
+ })
10804
+ ]);
10805
+ }
10806
+ // ../../packages/providers/src/codex/approval-context.ts
10807
+ const decisionOf = (r) => r.kind === "approval" ? r.decision : "cancel";
10808
+ /**
10809
+ * Approve `item` by policy when the permission level allows `risk`: record it as decided by
10810
+ * policy (shown in the transcript, never waited on) and return true. Otherwise return false and
10811
+ * the caller asks the user, with the classifier's reason.
10812
+ */
10813
+ function approvedByPolicy(ac, p, item, risk) {
10814
+ const autonomy = ac.autonomy?.() ?? "supervised";
10815
+ if (!levelAllows(autonomy, risk)) return false;
10816
+ ac.broker.record(ac.ctx(p.threadId, p.turnId), {
10817
+ ...item,
10818
+ status: "completed",
10819
+ reason: `Allowed by the ${autonomy === "full" ? "Full" : "Auto"} permission level`,
10820
+ decision: "allow_once",
10821
+ decidedBy: "policy"
10822
+ });
10823
+ return true;
10824
+ }
10825
+ // ../../packages/providers/src/codex/elicitation-form.ts
10826
+ /** The option that leaves an optional field empty. */
10827
+ const LEAVE_EMPTY = "Leave empty";
10828
+ /** A form without fields is a confirmation: one question, accept or decline. */
10829
+ const CONFIRM_ID = "confirm";
10830
+ const ACCEPT = "Accept";
10831
+ const DECLINE = "Decline";
10832
+ function formFields(schema) {
10833
+ const required = new Set(schema.required ?? []);
10834
+ return Object.entries(schema.properties).flatMap(([name, prop], i) => prop ? [field(name, i, prop, required.has(name))] : []);
10835
+ }
10836
+ function field(name, index, s, required) {
10837
+ const description = s.description?.trim();
10838
+ const base = {
10839
+ name,
10840
+ id: name.length > 0 && name.length <= 200 ? name : `field-${index}`,
10841
+ label: s.title?.trim() || name,
10842
+ ...description ? { description } : {},
10843
+ required,
10844
+ choices: []
10845
+ };
10846
+ if (s.type === "boolean") return {
10847
+ ...base,
10848
+ type: "boolean"
10849
+ };
10850
+ if (s.type === "number" || s.type === "integer") return {
10851
+ ...base,
10852
+ type: s.type
10853
+ };
10854
+ if (s.type === "array") {
10855
+ const items = s.items;
10856
+ const choices = "anyOf" in items ? items.anyOf.map((o) => ({
10857
+ value: o.const,
10858
+ label: o.title
10859
+ })) : items.enum.map((value) => ({
10860
+ value,
10861
+ label: value
10862
+ }));
10863
+ return {
10864
+ ...base,
10865
+ type: "choices",
10866
+ choices: unique(choices)
10867
+ };
10868
+ }
10869
+ if ("oneOf" in s) return {
10870
+ ...base,
10871
+ type: "choice",
10872
+ choices: unique(s.oneOf.map((o) => ({
10873
+ value: o.const,
10874
+ label: o.title
10875
+ })))
10876
+ };
10877
+ if ("enum" in s) {
10878
+ const names = "enumNames" in s ? s.enumNames : void 0;
10879
+ const choices = s.enum.map((value, i) => ({
10880
+ value,
10881
+ label: names?.[i] ?? value
10882
+ }));
10883
+ return {
10884
+ ...base,
10885
+ type: "choice",
10886
+ choices: unique(choices)
10887
+ };
10888
+ }
10889
+ return {
10890
+ ...base,
10891
+ type: "text"
10892
+ };
10893
+ }
10894
+ /**
10895
+ * Labels are how answers come back, so each must name exactly one value: a repeated label (or
10896
+ * one that reads as {@link LEAVE_EMPTY}) gets its value added, then a number, until it is free.
10897
+ */
10898
+ function unique(choices) {
10899
+ const seen = /* @__PURE__ */ new Set([LEAVE_EMPTY]);
10900
+ return choices.map((c) => {
10901
+ const label = c.label.trim() || c.value;
10902
+ let free = seen.has(label) ? `${label} (${c.value})` : label;
10903
+ for (let n = 2; seen.has(free); n++) free = `${label} (${c.value}) ${n}`;
10904
+ seen.add(free);
10905
+ return {
10906
+ value: c.value,
10907
+ label: free
10908
+ };
10909
+ });
10910
+ }
10911
+ const HINTS = {
10912
+ integer: " (a whole number)",
10913
+ number: " (a number)"
10914
+ };
10915
+ /** The form as questions; the first one carries the server's message. */
10916
+ function formQuestions(server, message, fields) {
10917
+ const lead = `${server} asks: ${message.trim()}`;
10918
+ if (fields.length === 0) return [{
10919
+ id: CONFIRM_ID,
10920
+ header: server,
10921
+ question: lead,
10922
+ options: [{ label: ACCEPT }, { label: DECLINE }],
10923
+ multiSelect: false,
10924
+ allowFreeText: false
10925
+ }];
10926
+ return fields.map((f, i) => {
10927
+ const ask = `${f.description ?? f.label}${HINTS[f.type] ?? ""}`;
10928
+ const plain = fields.length === 1 && ask === f.label;
10929
+ const question = i > 0 ? ask : plain ? lead : `${lead} — ${ask}`;
10930
+ const options = f.type === "boolean" ? [{ label: "Yes" }, { label: "No" }] : f.choices.map((c) => ({ label: c.label }));
10931
+ return {
10932
+ id: f.id,
10933
+ header: f.label.length > 40 ? `${f.label.slice(0, 39)}…` : f.label,
10934
+ question,
10935
+ options: f.required ? options : [...options, { label: LEAVE_EMPTY }],
10936
+ multiSelect: f.type === "choices",
10937
+ allowFreeText: f.type === "text" || f.type === "number" || f.type === "integer"
10938
+ };
10939
+ });
9905
10940
  }
9906
10941
  /**
9907
- * Risk of a command Codex asks to run: the command line as sent and each parsed action, so a
9908
- * redirect or path the parsed form drops still counts.
10942
+ * The form's content from the answers (keyed by question id), or undefined when a required
10943
+ * field has no answer or an answer doesn't fit its field.
9909
10944
  */
9910
- function commandRisk(texts, ctx) {
9911
- return first(texts.filter(Boolean).map((text) => classifyCommand(text, ctx)));
10945
+ function formContent(fields, answers) {
10946
+ const content = {};
10947
+ for (const f of fields) {
10948
+ const given = (answers[f.id] ?? []).map((a) => a.trim()).filter(Boolean);
10949
+ const values = f.required ? given : given.filter((a) => a !== LEAVE_EMPTY);
10950
+ if (values.length === 0) {
10951
+ if (f.required) return void 0;
10952
+ continue;
10953
+ }
10954
+ const value = parse$1(f, values);
10955
+ if (value === void 0) return void 0;
10956
+ content[f.name] = value;
10957
+ }
10958
+ return content;
10959
+ }
10960
+ function parse$1(f, values) {
10961
+ const [one] = values;
10962
+ switch (f.type) {
10963
+ case "text": return values.join("\n");
10964
+ case "number":
10965
+ case "integer": {
10966
+ const n = values.length === 1 ? Number(one) : NaN;
10967
+ return Number.isFinite(n) && (f.type === "number" || Number.isInteger(n)) ? n : void 0;
10968
+ }
10969
+ case "boolean": {
10970
+ const v = values.length === 1 ? one?.toLowerCase() : void 0;
10971
+ return v === "yes" || v === "true" ? true : v === "no" || v === "false" ? false : void 0;
10972
+ }
10973
+ case "choice": return values.length === 1 && one !== void 0 ? choice(f, one) : void 0;
10974
+ case "choices": {
10975
+ const picked = values.map((v) => choice(f, v));
10976
+ return picked.every((v) => v !== void 0) ? picked : void 0;
10977
+ }
10978
+ }
9912
10979
  }
9913
10980
  /**
9914
- * Risk of a patch Codex asks to apply: each file it changes, and the root it asks to write
9915
- * under for the rest of the session. Unknown paths can't be judged, so they need the user.
10981
+ * The value of the option the user picked. The card sends labels, so labels are matched first,
10982
+ * across every option; a raw value (a PM may answer with one) only when no label matches.
9916
10983
  */
9917
- function patchRisk(paths, grantRoot, ctx) {
9918
- const targets = [...paths, ...grantRoot ? [grantRoot] : []];
9919
- if (targets.length === 0) return {
9920
- risky: true,
9921
- action: "outside_worktree",
9922
- reason: "Changes files Codex did not name"
10984
+ const choice = (f, answer) => (f.choices.find((c) => c.label === answer) ?? f.choices.find((c) => c.value === answer))?.value;
10985
+ // ../../packages/providers/src/codex/elicitation.ts
10986
+ const accept = (content, meta = null) => ({
10987
+ action: "accept",
10988
+ content,
10989
+ _meta: meta
10990
+ });
10991
+ const DECLINED = {
10992
+ action: "decline",
10993
+ content: null,
10994
+ _meta: null
10995
+ };
10996
+ const CANCELLED = {
10997
+ action: "cancel",
10998
+ content: null,
10999
+ _meta: null
11000
+ };
11001
+ const READ_ONLY = "This session has read-only permissions: it can't run MCP tools that may change things.";
11002
+ async function handleElicitation(p, ac) {
11003
+ if (!p.turnId) return DECLINED;
11004
+ const where = {
11005
+ threadId: p.threadId,
11006
+ turnId: p.turnId
9923
11007
  };
9924
- return first(targets.map((target) => classifyWrite(target, ctx)));
11008
+ if (p.mode === "url") return openLink(p, where, ac);
11009
+ const meta = asMeta(p._meta);
11010
+ if (meta.codex_approval_kind === "mcp_tool_call") return toolApproval(p, meta, where, ac);
11011
+ if (p.mode === "form") return fillForm(p, where, ac);
11012
+ return DECLINED;
9925
11013
  }
9926
11014
  /**
9927
- * Risk of extra permissions Codex asks for: network access (not restricted, as for Claude,
9928
- * while no host list applies) and folders to read or write. Patterns and special folders can't
9929
- * be judged, so they need the user.
11015
+ * Codex asks before it runs one of the user's MCP tools that isn't marked read-only (its
11016
+ * `tool_call_mcp_elicitation` feature, on by default).
9930
11017
  */
9931
- function permissionsRisk(permissions, ctx) {
9932
- const fs = permissions.fileSystem;
9933
- if (!fs) return { risky: false };
9934
- const unknown = {
9935
- risky: true,
9936
- action: "outside_worktree",
9937
- reason: "Asks for access to folders by pattern"
11018
+ async function toolApproval(p, meta, where, ac) {
11019
+ const server = p.serverName;
11020
+ const tool = text$1(meta.tool_name) ?? text$1(meta.tool_title) ?? /run tool "([^"]+)"/.exec(p.message)?.[1];
11021
+ const persist = meta.persist;
11022
+ const offersSession = persist === "session" || Array.isArray(persist) && persist.includes("session");
11023
+ const requestId = newRequestId();
11024
+ const item = {
11025
+ id: `approval-${requestId}`,
11026
+ kind: "approval_request",
11027
+ status: "in_progress",
11028
+ requestId,
11029
+ action: "tool",
11030
+ title: tool ? `Use ${server}: ${shorten(tool, 80)}` : `Use a tool of ${server}`,
11031
+ ...p.message.trim() ? { reason: p.message.trim() } : {},
11032
+ ...tool ? { toolName: `mcp__${server}__${tool}` } : {},
11033
+ ...meta.tool_params !== void 0 ? { toolInput: toJson(meta.tool_params) } : {},
11034
+ options: offersSession ? [
11035
+ "allow_once",
11036
+ "allow_session",
11037
+ "deny"
11038
+ ] : ["allow_once", "deny"]
9938
11039
  };
9939
- return first([
9940
- ...(fs.write ?? []).map((p) => classifyWrite(p, ctx)),
9941
- ...(fs.read ?? []).map((p) => classifyRead(p, ctx)),
9942
- ...(fs.entries ?? []).map((entry) => {
9943
- if (entry.path.type !== "path") return unknown;
9944
- if (entry.access === "write") return classifyWrite(entry.path.path, ctx);
9945
- return entry.access === "read" ? classifyRead(entry.path.path, ctx) : { risky: false };
9946
- })
9947
- ]);
11040
+ const ctx = ac.ctx(where.threadId, where.turnId);
11041
+ if ((ac.autonomy?.() ?? "supervised") === "read_only") {
11042
+ ac.broker.record(ctx, {
11043
+ ...item,
11044
+ status: "declined",
11045
+ reason: READ_ONLY,
11046
+ decision: "deny",
11047
+ decidedBy: "policy"
11048
+ });
11049
+ return DECLINED;
11050
+ }
11051
+ if (!(meta.codex_sensitive_action === true || meta.codex_requires_user_input === true) && approvedByPolicy(ac, where, item, { risky: false })) return accept({});
11052
+ const decision = decisionOf(await ac.broker.request(ctx, item));
11053
+ if (decision === "allow_session" && offersSession) return accept({}, { persist: "session" });
11054
+ if (decision === "allow_once" || decision === "allow_session") return accept({});
11055
+ return decision === "deny" ? DECLINED : CANCELLED;
11056
+ }
11057
+ /** A server's form: a question card built from its schema, the answers sent as its content. */
11058
+ async function fillForm(p, where, ac) {
11059
+ const fields = formFields(p.requestedSchema);
11060
+ const requestId = newRequestId();
11061
+ const response = await ac.broker.request(ac.ctx(where.threadId, where.turnId), {
11062
+ id: `question-${requestId}`,
11063
+ kind: "user_question",
11064
+ status: "in_progress",
11065
+ requestId,
11066
+ mcpServer: p.serverName,
11067
+ questions: formQuestions(p.serverName, p.message, fields)
11068
+ });
11069
+ if (response.kind !== "answer") return CANCELLED;
11070
+ if (fields.length === 0) return response.answers["confirm"]?.includes("Accept") ? accept({}) : DECLINED;
11071
+ const content = formContent(fields, response.answers);
11072
+ return content ? accept(content) : DECLINED;
9948
11073
  }
9949
- // ../../packages/providers/src/codex/approvals.ts
9950
- const decisionOf = (r) => r.kind === "approval" ? r.decision : "cancel";
9951
11074
  /**
9952
- * Approve `item` by policy when the permission level allows `risk`: record it as decided by
9953
- * policy (shown in the transcript, never waited on) and return true. Otherwise return false and
9954
- * the caller asks the user, with the classifier's reason.
11075
+ * A server asks the user to open a link (to sign in, say). The card shows the link and the user
11076
+ * opens it: never decided by policy, and nothing opens it on the user's behalf.
9955
11077
  */
9956
- function approvedByPolicy(ac, p, item, risk) {
9957
- const autonomy = ac.autonomy?.() ?? "supervised";
9958
- if (!levelAllows(autonomy, risk)) return false;
9959
- ac.broker.record(ac.ctx(p.threadId, p.turnId), {
9960
- ...item,
9961
- status: "completed",
9962
- reason: `Allowed by the ${autonomy === "full" ? "Full" : "Auto"} permission level`,
9963
- decision: "allow_once",
9964
- decidedBy: "policy"
11078
+ async function openLink(p, where, ac) {
11079
+ const url = webUrl(p.url);
11080
+ if (!url) return DECLINED;
11081
+ const requestId = newRequestId();
11082
+ const response = await ac.broker.request(ac.ctx(where.threadId, where.turnId), {
11083
+ id: `approval-${requestId}`,
11084
+ kind: "approval_request",
11085
+ status: "in_progress",
11086
+ requestId,
11087
+ action: "network",
11088
+ title: `${p.serverName} asks you to open ${url.host}`,
11089
+ ...p.message.trim() ? { reason: p.message.trim() } : {},
11090
+ url: url.href,
11091
+ options: ["allow_once", "deny"]
9965
11092
  });
9966
- return true;
11093
+ const decision = decisionOf(response);
11094
+ if (decision === "allow_once" || decision === "allow_session") return accept(null);
11095
+ return decision === "deny" ? DECLINED : CANCELLED;
11096
+ }
11097
+ /** Elicitations carry no item id, and app-server request ids restart with each process. */
11098
+ const newRequestId = () => `mcp-${randomUUID()}`;
11099
+ function asMeta(value) {
11100
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
11101
+ }
11102
+ function text$1(value) {
11103
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
11104
+ }
11105
+ /** Web links only: a card never links to a script or a local file. */
11106
+ function webUrl(raw) {
11107
+ try {
11108
+ const url = new URL(raw);
11109
+ return url.protocol === "https:" || url.protocol === "http:" ? url : void 0;
11110
+ } catch {
11111
+ return;
11112
+ }
9967
11113
  }
11114
+ // ../../packages/providers/src/codex/approvals.ts
9968
11115
  /** The item with the classifier's verdict, for the user to decide. */
9969
11116
  function withRisk(item, risk) {
9970
11117
  if (!risk.risky) return item;
@@ -10099,11 +11246,7 @@ async function handleServerRequest(req, ac) {
10099
11246
  if (response.kind === "answer") for (const [id, values] of Object.entries(response.answers)) answers[id] = { answers: values };
10100
11247
  return { answers };
10101
11248
  }
10102
- case "mcpServer/elicitation/request": return {
10103
- action: "decline",
10104
- content: null,
10105
- _meta: null
10106
- };
11249
+ case "mcpServer/elicitation/request": return handleElicitation(req.params, ac);
10107
11250
  case "execCommandApproval":
10108
11251
  case "applyPatchApproval": return { decision: { denied: { rejection: "Unsupported approval request" } } };
10109
11252
  default: throw new CodexRpcError(-32601, `CodeDen does not handle ${req.method}`);
@@ -10877,6 +12020,21 @@ function tokenUsage(u) {
10877
12020
  if (u.modelContextWindow && u.modelContextWindow > 0) usage.contextWindow = Math.round(u.modelContextWindow);
10878
12021
  return usage;
10879
12022
  }
12023
+ /**
12024
+ * A sub-agent's tokens for its card (§7.3), from its thread's running `total`, in the same
12025
+ * terms as `tokenUsage`. `model` is the one its spawn asked for; absent, it runs the session's.
12026
+ */
12027
+ function subagentUsage(u, model) {
12028
+ const t = tokenUsage(u);
12029
+ return {
12030
+ ...model ? { model } : {},
12031
+ inputTokens: t.inputTokens ?? 0,
12032
+ outputTokens: t.outputTokens ?? 0,
12033
+ cacheReadTokens: t.cachedInputTokens ?? 0,
12034
+ cacheWriteTokens: t.cacheWriteTokens ?? 0,
12035
+ ...t.reasoningTokens ? { reasoningTokens: t.reasoningTokens } : {}
12036
+ };
12037
+ }
10880
12038
  function windowId(minutes, fallback) {
10881
12039
  if (minutes === 300) return "5h";
10882
12040
  if (minutes === 10080) return "weekly";
@@ -11005,7 +12163,7 @@ var CodexThreadMapper = class {
11005
12163
  this.updatePlan(n.params.turnId, n.params.plan);
11006
12164
  return;
11007
12165
  case "thread/tokenUsage/updated": {
11008
- this.o.onTokens?.(n.params.tokenUsage.total.totalTokens);
12166
+ this.o.onTokens?.(n.params.tokenUsage);
11009
12167
  const usage = this.turnUsage.add(n.params.turnId, n.params.tokenUsage);
11010
12168
  if (!this.o.root) {
11011
12169
  delete usage.contextTokens;
@@ -11115,10 +12273,16 @@ var CodexThreadMapper = class {
11115
12273
  });
11116
12274
  }
11117
12275
  }
12276
+ /**
12277
+ * Codex ended a turn. On the session's own thread the turn's id is its fork point:
12278
+ * `thread/fork { lastTurnId }` keeps it and drops later turns. Turns CodeDen closes itself
12279
+ * (the process died, the session closed) report none, as Codex may not have finished them.
12280
+ */
11118
12281
  completeTurn(turn) {
11119
12282
  this.beginTurn(turn.id);
11120
- if (turn.status === "completed") this.endTurn({ type: "completed" }, turn.id);
11121
- else if (turn.status === "interrupted") this.endTurn({ type: "interrupted" }, turn.id);
12283
+ const forkPoint = this.o.root ? turn.id : void 0;
12284
+ if (turn.status === "completed") this.endTurn({ type: "completed" }, turn.id, forkPoint);
12285
+ else if (turn.status === "interrupted") this.endTurn({ type: "interrupted" }, turn.id, forkPoint);
11122
12286
  else {
11123
12287
  const info = turn.error?.codexErrorInfo;
11124
12288
  const kind = classifyCodexError(info);
@@ -11130,13 +12294,17 @@ var CodexThreadMapper = class {
11130
12294
  code: codexErrorCode(info),
11131
12295
  kind,
11132
12296
  retryAt
11133
- }, turn.id);
12297
+ }, turn.id, forkPoint);
11134
12298
  }
11135
12299
  }
11136
12300
  /** Close the running turn (also used when the process dies or the session closes). */
11137
- endTurn(outcome, turnId = this.current) {
12301
+ endTurn(outcome, turnId = this.current, forkPoint) {
11138
12302
  if (!turnId || turnId !== this.current) return;
11139
12303
  const ctx = this.ctx(turnId);
12304
+ const end = {
12305
+ ...ctx,
12306
+ ...forkPoint ? { forkPoint } : {}
12307
+ };
11140
12308
  const ok = outcome.type === "completed";
11141
12309
  for (const item of this.open.values()) this.o.emit({
11142
12310
  type: "item.completed",
@@ -11163,17 +12331,17 @@ var CodexThreadMapper = class {
11163
12331
  this.current = void 0;
11164
12332
  if (outcome.type === "completed") this.o.emit({
11165
12333
  type: "turn.completed",
11166
- ...ctx,
12334
+ ...end,
11167
12335
  stopReason: "end_turn"
11168
12336
  });
11169
12337
  else if (outcome.type === "interrupted") this.o.emit({
11170
12338
  type: "turn.interrupted",
11171
- ...ctx,
12339
+ ...end,
11172
12340
  reason: "user"
11173
12341
  });
11174
12342
  else this.o.emit({
11175
12343
  type: "turn.failed",
11176
- ...ctx,
12344
+ ...end,
11177
12345
  error: {
11178
12346
  code: outcome.code ?? "turn_failed",
11179
12347
  message: outcome.message,
@@ -11248,6 +12416,7 @@ var CodexSubagents = class {
11248
12416
  return false;
11249
12417
  }
11250
12418
  const run = this.claim(from, turnId, item.id, spawnDetails(item));
12419
+ if (item.model) run.model ??= item.model;
11251
12420
  const child = item.receiverThreadIds[0];
11252
12421
  if (child) this.bind(run, child);
11253
12422
  if (phase !== "completed") return true;
@@ -11386,8 +12555,8 @@ var CodexSubagents = class {
11386
12555
  prompt: item.input.text
11387
12556
  };
11388
12557
  },
11389
- onTokens: (total) => {
11390
- run.totalTokens = total;
12558
+ onTokens: (usage) => {
12559
+ run.tokens = usage;
11391
12560
  }
11392
12561
  });
11393
12562
  run.thread = thread;
@@ -11488,9 +12657,10 @@ var CodexSubagents = class {
11488
12657
  stats(run) {
11489
12658
  const end = run.endedAt ?? this.o.now();
11490
12659
  return {
11491
- ...run.totalTokens !== void 0 ? { totalTokens: Math.max(0, Math.round(run.totalTokens)) } : {},
12660
+ ...run.tokens ? { totalTokens: Math.max(0, Math.round(run.tokens.total.totalTokens)) } : {},
11492
12661
  toolUses: run.toolCalls.size,
11493
- durationMs: Math.max(0, Math.round(end - run.startedAt))
12662
+ durationMs: Math.max(0, Math.round(end - run.startedAt)),
12663
+ ...run.tokens ? { usage: [subagentUsage(run.tokens, run.model)] } : {}
11494
12664
  };
11495
12665
  }
11496
12666
  };
@@ -11837,7 +13007,8 @@ var CodexSession = class {
11837
13007
  risk: {
11838
13008
  cwd: this.opts.cwd,
11839
13009
  extraRoots: this.opts.additionalDirectories,
11840
- ...this.opts.ownBranch ? { ownBranch: this.opts.ownBranch } : {}
13010
+ ...this.opts.ownBranch ? { ownBranch: this.opts.ownBranch } : {},
13011
+ ...this.opts.codedenHome ? { codedenHome: this.opts.codedenHome } : {}
11841
13012
  },
11842
13013
  autonomy: () => this.opts.readOnly ? "read_only" : this.autonomy,
11843
13014
  ctx: (threadId, turnId) => this.mapper.requestCtx(threadId, turnId),
@@ -12054,7 +13225,10 @@ var CodexAdapter = class {
12054
13225
  }
12055
13226
  return session;
12056
13227
  }
12057
- /** Copy a thread with `thread/fork` (see `forkCodexThread`); returns the new thread's id. */
13228
+ /**
13229
+ * Copy a thread with `thread/fork` (see `forkCodexThread`); returns the new thread's id. With
13230
+ * `upTo` (a turn id), the copy ends with that turn.
13231
+ */
12058
13232
  async forkConversation(resumeToken, opts) {
12059
13233
  const executable = this.executable();
12060
13234
  if (!executable) throw new Error("Codex is not installed (no `codex` on PATH)");
@@ -12316,6 +13490,7 @@ var FakeSession = class {
12316
13490
  }
12317
13491
  async runTurn(turn, input) {
12318
13492
  const { threadId, turnId } = turn;
13493
+ const forkPoint = turnId;
12319
13494
  const prompt = input.text;
12320
13495
  try {
12321
13496
  this.emit({
@@ -12342,6 +13517,7 @@ var FakeSession = class {
12342
13517
  type: "turn.failed",
12343
13518
  threadId,
12344
13519
  turnId,
13520
+ forkPoint,
12345
13521
  error: {
12346
13522
  code: "fake_failure",
12347
13523
  message: "Scripted failure",
@@ -12357,6 +13533,7 @@ var FakeSession = class {
12357
13533
  type: "turn.failed",
12358
13534
  threadId,
12359
13535
  turnId,
13536
+ forkPoint,
12360
13537
  error: usage ? {
12361
13538
  code: "usage_limit",
12362
13539
  message: "Scripted usage limit",
@@ -12378,6 +13555,7 @@ var FakeSession = class {
12378
13555
  type: "turn.completed",
12379
13556
  threadId,
12380
13557
  turnId,
13558
+ forkPoint,
12381
13559
  stopReason: "end_turn"
12382
13560
  });
12383
13561
  return;
@@ -12426,6 +13604,7 @@ var FakeSession = class {
12426
13604
  type: "turn.completed",
12427
13605
  threadId,
12428
13606
  turnId,
13607
+ forkPoint,
12429
13608
  stopReason: "end_turn"
12430
13609
  });
12431
13610
  } catch (error) {
@@ -12434,6 +13613,7 @@ var FakeSession = class {
12434
13613
  type: "turn.interrupted",
12435
13614
  threadId,
12436
13615
  turnId,
13616
+ forkPoint,
12437
13617
  reason: "user"
12438
13618
  });
12439
13619
  return;
@@ -12443,6 +13623,7 @@ var FakeSession = class {
12443
13623
  type: "turn.failed",
12444
13624
  threadId,
12445
13625
  turnId,
13626
+ forkPoint,
12446
13627
  error: { message }
12447
13628
  });
12448
13629
  }
@@ -12843,10 +14024,13 @@ var FakeProvider = class {
12843
14024
  async openSession(opts) {
12844
14025
  return new FakeSession(await withFork(this, opts), this.options);
12845
14026
  }
12846
- /** A fork gets a conversation of its own, named after the one it copies. */
12847
- async forkConversation(resumeToken) {
14027
+ /**
14028
+ * A fork gets a conversation of its own, named after the one it copies and, with `upTo`,
14029
+ * the turn it ends with (`<token>:fork-<n>@<turn>`).
14030
+ */
14031
+ async forkConversation(resumeToken, opts) {
12848
14032
  this.forks += 1;
12849
- return `${resumeToken}:fork-${this.forks}`;
14033
+ return `${resumeToken}:fork-${this.forks}${opts?.upTo ? `@${opts.upTo}` : ""}`;
12850
14034
  }
12851
14035
  forks = 0;
12852
14036
  };
@@ -14525,25 +15709,32 @@ var OpenCodePolicy = class {
14525
15709
  };
14526
15710
  }
14527
15711
  }
14528
- /** Remember an `allow_session` decision: commands and paths match exactly. */
15712
+ /** Remember an `allow_session` decision: commands (in their directory) and paths match exactly. */
14529
15713
  allowForSession(call) {
14530
15714
  this.sessionAllow.add(this.key(call));
14531
15715
  }
14532
15716
  shell(call) {
14533
15717
  const command = commandOf(call);
15718
+ const risk = classifyCommand(command, {
15719
+ ...this.ctx,
15720
+ dir: this.workdir(call)
15721
+ });
14534
15722
  if (this.isReadOnly) {
15723
+ if (touchesCodeden(risk)) return this.credentialsDenial();
14535
15724
  const reason = readOnlyCommandDenial(command, { allowChecks: !this.readOnly });
14536
15725
  return reason ? {
14537
15726
  kind: "deny",
14538
15727
  message: `This session is read-only: ${reason}.`
14539
15728
  } : ALLOW;
14540
15729
  }
14541
- const workdir = str(call.toolInput, "workdir");
14542
- const dir = workdir ? path.resolve(this.ctx.cwd, workdir) : void 0;
14543
- return this.gate(classifyCommand(command, dir ? {
14544
- ...this.ctx,
14545
- dir
14546
- } : this.ctx), "command");
15730
+ return this.gate(risk, "command");
15731
+ }
15732
+ /** Read-only sessions never touch CodeDen's own credential files. */
15733
+ credentialsDenial() {
15734
+ return {
15735
+ kind: "deny",
15736
+ message: this.readOnly ? "This session is read-only: CodeDen's own credential files are off limits. Act through CodeDen tools instead." : "This session has read-only permissions: CodeDen's own credential files are off limits. Report what you found instead."
15737
+ };
14547
15738
  }
14548
15739
  edit(paths) {
14549
15740
  if (this.isReadOnly) return {
@@ -14552,19 +15743,19 @@ var OpenCodePolicy = class {
14552
15743
  };
14553
15744
  return this.gate(this.firstRisk(paths, classifyWrite), "file_change");
14554
15745
  }
15746
+ /** The verdict for every target: CodeDen's own credentials first, then the first risky one. */
14555
15747
  firstRisk(targets, classify) {
14556
- for (const target of targets) {
14557
- const risk = classify(target, this.ctx);
14558
- if (risk.risky) return risk;
14559
- }
14560
- return { risky: false };
15748
+ return mostSevere(targets.map((target) => classify(target, this.ctx)));
14561
15749
  }
14562
15750
  /**
14563
15751
  * The level's gate (the same as Claude's): edits and commands (`supervisedAction`) always
14564
15752
  * ask in supervised mode; `alwaysRisky` asks for flagged calls even at full autonomy
14565
- * (secrets reads).
15753
+ * (secrets reads). CodeDen's own credential files ask at every level and are refused for
15754
+ * read-only sessions.
14566
15755
  */
14567
15756
  gate(risk, supervisedAction, alwaysRisky = false) {
15757
+ const codeden = touchesCodeden(risk);
15758
+ if (codeden && this.isReadOnly) return this.credentialsDenial();
14568
15759
  if (this.autonomy === "supervised") {
14569
15760
  if (risk.risky) return {
14570
15761
  kind: "ask",
@@ -14579,18 +15770,28 @@ var OpenCodePolicy = class {
14579
15770
  return ALLOW;
14580
15771
  }
14581
15772
  if (!risk.risky) return ALLOW;
14582
- if (this.autonomy === "full" && risk.action !== "outside_worktree" && !alwaysRisky) return ALLOW;
15773
+ const always = alwaysRisky || codeden;
15774
+ if (this.autonomy === "full" && risk.action !== "outside_worktree" && !always) return ALLOW;
14583
15775
  return {
14584
15776
  kind: "ask",
14585
15777
  action: risk.action,
14586
15778
  reason: risk.reason
14587
15779
  };
14588
15780
  }
15781
+ /**
15782
+ * What an approval for the session covers: a command in its working directory (the same
15783
+ * relative path elsewhere is another file), or the exact paths.
15784
+ */
14589
15785
  key(call) {
14590
15786
  const { action, resources } = call.request;
14591
- if (action === "shell") return `shell:${commandOf(call)}`;
15787
+ if (action === "shell") return `shell:${this.workdir(call)}\n${commandOf(call)}`;
14592
15788
  return `${action}:${[...resources].sort().join("\n")}`;
14593
15789
  }
15790
+ /** Where a shell call runs: its `workdir`, else the worktree. */
15791
+ workdir(call) {
15792
+ const workdir = str(call.toolInput, "workdir");
15793
+ return workdir ? resolveAgainst(workdir, this.ctx) : this.ctx.cwd;
15794
+ }
14594
15795
  };
14595
15796
  /** The command an OpenCode `shell` ask is about: the tool input, else its parsed parts. */
14596
15797
  function commandOf(call) {
@@ -15677,7 +16878,8 @@ var OpenCodeSession = class {
15677
16878
  this.broker = new ApprovalBroker(emit);
15678
16879
  this.policy = new OpenCodePolicy(opts.autonomy, {
15679
16880
  cwd: opts.cwd,
15680
- extraRoots: opts.additionalDirectories
16881
+ extraRoots: opts.additionalDirectories,
16882
+ ...opts.codedenHome ? { codedenHome: opts.codedenHome } : {}
15681
16883
  }, opts.readOnly ?? false);
15682
16884
  this.model = explicitModel(opts.model);
15683
16885
  this.effort = opts.effort;
@@ -21832,6 +23034,7 @@ async function executeCommand(core, command) {
21832
23034
  if (added.length > 0) core.log?.(`PATH: picked up ${added.join(", ")}`);
21833
23035
  const adapters = command.providerId ? registry.list().filter((a) => a.id === command.providerId) : registry.list();
21834
23036
  const statuses = await Promise.all(adapters.map((a) => a.detect()));
23037
+ if (command.refreshModels === false) return { statuses };
21835
23038
  const refreshed = registry.refreshModels(adapters.map((a) => a.id));
21836
23039
  if (!command.waitForModels) return { statuses };
21837
23040
  await refreshed;
@@ -21858,9 +23061,12 @@ async function executeCommand(core, command) {
21858
23061
  const adapter = registry.get(command.providerId);
21859
23062
  if (!adapter?.capabilities().fork || !adapter.forkConversation) throw new CommandError("unsupported", `Provider ${command.providerId} cannot fork`);
21860
23063
  try {
21861
- return { resumeToken: await adapter.forkConversation(command.resumeToken, { cwd: command.cwd }) };
23064
+ return { resumeToken: await adapter.forkConversation(command.resumeToken, {
23065
+ cwd: command.cwd,
23066
+ ...command.upTo ? { upTo: command.upTo } : {}
23067
+ }) };
21862
23068
  } catch (error) {
21863
- throw new CommandError("fork_failed", error.message);
23069
+ throw new CommandError(error.code === FORK_POINT_MISSING ? FORK_POINT_MISSING : "fork_failed", error.message);
21864
23070
  }
21865
23071
  }
21866
23072
  case "workspace.prepare":
@@ -21943,7 +23149,7 @@ async function executeCommand(core, command) {
21943
23149
  case "checkpoint.restoreFiles": return restoreFiles(command.cwd, command.sessionId, command.index, command.paths);
21944
23150
  case "ports.list": return { ports: await listPorts(command.probe) };
21945
23151
  case "mcp.test": {
21946
- const { testMcpServer } = await import("./server-test-COAiKxTV.js");
23152
+ const { testMcpServer } = await import("./server-test-BwyCFssm.js");
21947
23153
  return testMcpServer({
21948
23154
  spec: command.spec,
21949
23155
  cwd: command.cwd,
@@ -22004,21 +23210,44 @@ function removeOwnLock(lockFile) {
22004
23210
  * monotonically increasing seq and kept until the server acks them, so nothing is lost
22005
23211
  * while the server is away. `state.json` holds the stable runner id and seq counters.
22006
23212
  *
22007
- * TODO(M1): batch fsyncs and compact lazily instead of rewriting on every ack.
23213
+ * An ack only moves the acked position in memory. While acks flow, the journal persists at
23214
+ * most every `persistMs`: it writes `state.json`, then, when enough acked events have piled
23215
+ * up and they outweigh what is left, rewrites the file without them (compaction), so each
23216
+ * ack costs no more than its append did. State always goes first, so `lastSeq` outlives the
23217
+ * events a compaction drops. `close` persists and compacts at once. After a crash the journal
23218
+ * may still hold events the server already acked; the server's `resume` acks them again and
23219
+ * they are not replayed.
22008
23220
  */
22009
23221
  var Journal = class {
22010
23222
  file;
22011
23223
  stateFile;
23224
+ compactEntries;
23225
+ compactBytes;
23226
+ persistMs;
22012
23227
  state;
23228
+ /** Every event in `journal.jsonl`, oldest first; those before `head` are acked. */
22013
23229
  entries = [];
22014
- constructor(dir) {
23230
+ head = 0;
23231
+ fileBytes = 0;
23232
+ ackedBytes = 0;
23233
+ persistTimer;
23234
+ constructor(dir, opts = {}) {
22015
23235
  mkdirSync(dir, { recursive: true });
22016
23236
  this.file = join(dir, "journal.jsonl");
22017
23237
  this.stateFile = join(dir, "state.json");
23238
+ this.compactEntries = opts.compactEntries ?? 1e3;
23239
+ this.compactBytes = opts.compactBytes ?? 1048576;
23240
+ this.persistMs = opts.persistMs ?? 250;
22018
23241
  this.state = this.loadState();
22019
- this.entries = this.loadEntries().filter((e) => e.seq > this.state.ackedSeq);
22020
- const maxSeq = this.entries.at(-1)?.seq ?? 0;
22021
- this.state.lastSeq = Math.max(this.state.lastSeq, maxSeq);
23242
+ const { entries, torn } = this.loadEntries();
23243
+ this.entries = entries;
23244
+ for (const e of entries) this.fileBytes += e.bytes;
23245
+ this.state.lastSeq = Math.max(this.state.lastSeq, entries.at(-1)?.msg.seq ?? 0);
23246
+ this.advanceHead(this.state.ackedSeq);
23247
+ if (torn) {
23248
+ this.writeState(this.state);
23249
+ this.compact();
23250
+ }
22022
23251
  }
22023
23252
  get runnerId() {
22024
23253
  return this.state.runnerId;
@@ -22027,7 +23256,7 @@ var Journal = class {
22027
23256
  return this.state.lastSeq;
22028
23257
  }
22029
23258
  get unacked() {
22030
- return this.entries.length;
23259
+ return this.entries.length - this.head;
22031
23260
  }
22032
23261
  /** Assign the next seq, persist the event, and return the wire message. */
22033
23262
  append(event) {
@@ -22038,20 +23267,84 @@ var Journal = class {
22038
23267
  ts: (/* @__PURE__ */ new Date()).toISOString(),
22039
23268
  event
22040
23269
  };
22041
- appendFileSync(this.file, `${JSON.stringify(msg)}\n`);
22042
- this.entries.push(msg);
23270
+ const line = `${JSON.stringify(msg)}\n`;
23271
+ appendFileSync(this.file, line);
23272
+ const bytes = Buffer.byteLength(line);
23273
+ this.entries.push({
23274
+ msg,
23275
+ bytes
23276
+ });
23277
+ this.fileBytes += bytes;
22043
23278
  return msg;
22044
23279
  }
22045
- /** Drop everything with `seq <= seq` (cumulative ack). */
23280
+ /** Acknowledge everything with `seq <= seq` (cumulative ack). */
22046
23281
  ack(seq) {
22047
- if (seq <= this.state.ackedSeq) return;
22048
- this.state.ackedSeq = Math.min(seq, this.state.lastSeq);
22049
- this.entries = this.entries.filter((e) => e.seq > seq);
22050
- this.rewrite();
23282
+ const upTo = Math.min(seq, this.state.lastSeq);
23283
+ if (upTo <= this.state.ackedSeq) return;
23284
+ this.state.ackedSeq = upTo;
23285
+ this.advanceHead(upTo);
23286
+ this.schedulePersist();
22051
23287
  }
22052
23288
  /** Unacked events after `seq`, oldest first (for replay on resume). */
22053
23289
  after(seq) {
22054
- return this.entries.filter((e) => e.seq > seq);
23290
+ const out = [];
23291
+ for (let i = this.head; i < this.entries.length; i++) {
23292
+ const msg = this.entries[i]?.msg;
23293
+ if (msg && msg.seq > seq) out.push(msg);
23294
+ }
23295
+ return out;
23296
+ }
23297
+ /** Persist the acked position and drop every acked event from the file (runner shutdown). */
23298
+ close() {
23299
+ this.persist(true);
23300
+ }
23301
+ advanceHead(ackedSeq) {
23302
+ for (;;) {
23303
+ const entry = this.entries[this.head];
23304
+ if (!entry || entry.msg.seq > ackedSeq) return;
23305
+ this.ackedBytes += entry.bytes;
23306
+ this.head += 1;
23307
+ }
23308
+ }
23309
+ /** Enough acked events piled up, and rewriting what is left costs no more than they do. */
23310
+ compactDue() {
23311
+ return (this.head >= this.compactEntries || this.ackedBytes >= this.compactBytes) && this.ackedBytes >= this.fileBytes - this.ackedBytes;
23312
+ }
23313
+ schedulePersist() {
23314
+ if (this.persistTimer) return;
23315
+ this.persistTimer = setTimeout(() => {
23316
+ this.persistTimer = void 0;
23317
+ try {
23318
+ this.persist(false);
23319
+ } catch {}
23320
+ }, this.persistMs);
23321
+ this.persistTimer.unref();
23322
+ }
23323
+ /**
23324
+ * Write the acked position, then compact when it is due (`all`: whenever anything is acked).
23325
+ * State first, so `lastSeq` outlives the events the compaction drops.
23326
+ */
23327
+ persist(all) {
23328
+ if (this.persistTimer) clearTimeout(this.persistTimer);
23329
+ this.persistTimer = void 0;
23330
+ this.writeState(this.state);
23331
+ if (all ? this.head > 0 : this.compactDue()) this.compact();
23332
+ }
23333
+ /** Rewrite the file without its acked events. Callers write the state first. */
23334
+ compact() {
23335
+ const live = this.entries.slice(this.head);
23336
+ const tmp = `${this.file}.tmp`;
23337
+ writeFileSync(tmp, live.map((e) => `${JSON.stringify(e.msg)}\n`).join(""));
23338
+ renameSync(tmp, this.file);
23339
+ this.entries = live;
23340
+ this.head = 0;
23341
+ this.fileBytes -= this.ackedBytes;
23342
+ this.ackedBytes = 0;
23343
+ }
23344
+ writeState(state) {
23345
+ const tmp = `${this.stateFile}.tmp`;
23346
+ writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
23347
+ renameSync(tmp, this.stateFile);
22055
23348
  }
22056
23349
  loadState() {
22057
23350
  try {
@@ -22071,25 +23364,26 @@ var Journal = class {
22071
23364
  return fresh;
22072
23365
  }
22073
23366
  loadEntries() {
22074
- if (!existsSync(this.file)) return [];
22075
- const out = [];
22076
- for (const line of readFileSync(this.file, "utf8").split("\n")) {
23367
+ if (!existsSync(this.file)) return {
23368
+ entries: [],
23369
+ torn: false
23370
+ };
23371
+ const raw = readFileSync(this.file, "utf8");
23372
+ const entries = [];
23373
+ let torn = raw.length > 0 && !raw.endsWith("\n");
23374
+ for (const line of raw.split("\n")) {
22077
23375
  if (!line.trim()) continue;
22078
23376
  const parsed = RunnerEventMessage.safeParse(safeJson(line));
22079
- if (parsed.success) out.push(parsed.data);
23377
+ if (parsed.success) entries.push({
23378
+ msg: parsed.data,
23379
+ bytes: Buffer.byteLength(line) + 1
23380
+ });
23381
+ else torn = true;
22080
23382
  }
22081
- return out.sort((a, b) => a.seq - b.seq);
22082
- }
22083
- rewrite() {
22084
- const tmp = `${this.file}.tmp`;
22085
- writeFileSync(tmp, this.entries.map((e) => `${JSON.stringify(e)}\n`).join(""));
22086
- renameSync(tmp, this.file);
22087
- this.writeState(this.state);
22088
- }
22089
- writeState(state) {
22090
- const tmp = `${this.stateFile}.tmp`;
22091
- writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
22092
- renameSync(tmp, this.stateFile);
23383
+ return {
23384
+ entries: entries.sort((a, b) => a.msg.seq - b.msg.seq),
23385
+ torn
23386
+ };
22093
23387
  }
22094
23388
  };
22095
23389
  function safeJson(line) {
@@ -22584,9 +23878,24 @@ async function click(session, ref, opts = {}) {
22584
23878
  const options = opts.button ? { button: opts.button } : {};
22585
23879
  await withRef(session.page(opts.tabId), ref, (locator) => opts.doubleClick ? locator.dblclick(options) : locator.click(options));
22586
23880
  }
22587
- /** Replace the value of an editable element; `submit` presses Enter afterwards. */
23881
+ /**
23882
+ * Replace the value of an editable element; `submit` presses Enter afterwards. Without a ref,
23883
+ * type the text as key presses into whatever has focus (a canvas editor, a game, a field
23884
+ * clicked at coordinates).
23885
+ */
22588
23886
  async function type(session, ref, text, opts = {}) {
22589
- await withRef(session.page(opts.tabId), ref, async (locator) => {
23887
+ const page = session.page(opts.tabId);
23888
+ if (ref === void 0) {
23889
+ if (text.length > 2e3) throw new CommandError("invalid_text", `Without a ref, type at most ${MAX_FOCUS_TYPE_CHARS} characters`);
23890
+ try {
23891
+ await page.keyboard.type(text);
23892
+ if (opts.submit) await page.keyboard.press("Enter");
23893
+ } catch (error) {
23894
+ throw new CommandError("action_failed", `Typing at the focused element failed: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`);
23895
+ }
23896
+ return;
23897
+ }
23898
+ await withRef(page, ref, async (locator) => {
22590
23899
  await locator.fill(text);
22591
23900
  if (opts.submit) await locator.press("Enter");
22592
23901
  });
@@ -22681,6 +23990,150 @@ async function wait(session, opts) {
22681
23990
  throw new CommandError("action_failed", error instanceof Error ? error.message : String(error));
22682
23991
  }
22683
23992
  }
23993
+ // ../runner/src/browser/pointer.ts
23994
+ /** Drags move in about one step per this many CSS pixels, so drag thresholds see motion. */
23995
+ const DRAG_STEP_PX = 20;
23996
+ const MIN_DRAG_STEPS = 5;
23997
+ const MAX_DRAG_STEPS = 50;
23998
+ /** A scroll returns once no scroll event came for this long (smooth scrolling has ended). */
23999
+ const SCROLL_QUIET_MS = 100;
24000
+ /** ... or after this long in the page, whatever happens. */
24001
+ const SCROLL_SETTLE_MAX_MS = 1e3;
24002
+ /** The runner stops waiting for the page itself after this (a busy or hidden page). */
24003
+ const SCROLL_SETTLE_TIMEOUT_MS = 1500;
24004
+ /**
24005
+ * Resolves once the page has had no `scroll` event (on any scroller: captured on `window`) for
24006
+ * `SCROLL_QUIET_MS`, at most `SCROLL_SETTLE_MAX_MS` (a timer too: hidden tabs get no animation
24007
+ * frames). A string, so it runs in the page's realm.
24008
+ */
24009
+ const SCROLL_SETTLED = `new Promise((resolve) => {
24010
+ const start = performance.now();
24011
+ let last = start;
24012
+ const onScroll = () => { last = performance.now(); };
24013
+ const done = () => {
24014
+ removeEventListener("scroll", onScroll, { capture: true });
24015
+ clearTimeout(timer);
24016
+ resolve(true);
24017
+ };
24018
+ const timer = setTimeout(done, ${SCROLL_SETTLE_MAX_MS});
24019
+ addEventListener("scroll", onScroll, { capture: true, passive: true });
24020
+ const tick = () => {
24021
+ const now = performance.now();
24022
+ if (now - last < ${SCROLL_QUIET_MS} && now - start < ${SCROLL_SETTLE_MAX_MS}) {
24023
+ requestAnimationFrame(tick);
24024
+ } else done();
24025
+ };
24026
+ requestAnimationFrame(tick);
24027
+ })`;
24028
+ /** Intermediate moves for a drag: about one per `DRAG_STEP_PX`, within 5 to 50. */
24029
+ function dragSteps(from, to) {
24030
+ const distance = Math.hypot(to.x - from.x, to.y - from.y);
24031
+ return Math.min(Math.max(Math.round(distance / DRAG_STEP_PX), MIN_DRAG_STEPS), MAX_DRAG_STEPS);
24032
+ }
24033
+ const fmt = (n) => String(Math.round(n * 100) / 100);
24034
+ /** Refuse a point outside the viewport: it would hit nothing, or the wrong thing. */
24035
+ function assertInViewport(point, viewport) {
24036
+ if (point.x >= 0 && point.y >= 0 && point.x < viewport.width && point.y < viewport.height) return;
24037
+ throw new CommandError("out_of_bounds", `Point (${fmt(point.x)}, ${fmt(point.y)}) is outside the viewport (${fmt(viewport.width)}x${fmt(viewport.height)} CSS pixels). Take a new browser_screenshot.`);
24038
+ }
24039
+ /** The points an action touches: its point, and a drag's end. */
24040
+ function pointsOf(command) {
24041
+ const points = [{
24042
+ x: command.x,
24043
+ y: command.y
24044
+ }];
24045
+ if (command.action === "drag") points.push({
24046
+ x: command.toX ?? NaN,
24047
+ y: command.toY ?? NaN
24048
+ });
24049
+ return points;
24050
+ }
24051
+ /**
24052
+ * Run `action` with `modifiers` held, releasing every key it tried to press, also on failure.
24053
+ * A key counts as held before its press resolves: Playwright marks it down before dispatching,
24054
+ * so a press that fails can still leave it held for the next action in the shared browser.
24055
+ */
24056
+ async function withModifiers(page, modifiers, action) {
24057
+ const held = [];
24058
+ try {
24059
+ for (const key of new Set(modifiers)) {
24060
+ held.push(key);
24061
+ await page.keyboard.down(key);
24062
+ }
24063
+ await action();
24064
+ } finally {
24065
+ for (const key of held.reverse()) await page.keyboard.up(key).catch(() => void 0);
24066
+ }
24067
+ }
24068
+ /**
24069
+ * Run steps that press `button`, releasing it if any of them fails, the press included (like
24070
+ * keys, Playwright marks a button held before the press is dispatched). The error is kept.
24071
+ */
24072
+ async function releasingOnFailure(page, button, steps) {
24073
+ try {
24074
+ await steps();
24075
+ } catch (error) {
24076
+ await page.mouse.up({ button }).catch(() => void 0);
24077
+ throw error;
24078
+ }
24079
+ }
24080
+ async function drag(page, from, to, button) {
24081
+ await page.mouse.move(from.x, from.y);
24082
+ await releasingOnFailure(page, button, async () => {
24083
+ await page.mouse.down({ button });
24084
+ await page.mouse.move(to.x, to.y, { steps: dragSteps(from, to) });
24085
+ });
24086
+ await page.mouse.up({ button });
24087
+ }
24088
+ /** Wait until a scroll has been rendered, bounded; a page that never answers is not an error. */
24089
+ async function scrollSettled(page) {
24090
+ let timer;
24091
+ const timeout = new Promise((resolve) => {
24092
+ timer = setTimeout(resolve, SCROLL_SETTLE_TIMEOUT_MS);
24093
+ });
24094
+ try {
24095
+ await Promise.race([page.evaluate(SCROLL_SETTLED).catch(() => void 0), timeout]);
24096
+ } finally {
24097
+ clearTimeout(timer);
24098
+ }
24099
+ }
24100
+ /** Run one pointer action on a page whose viewport is `viewport` (CSS pixels). */
24101
+ async function mouseAction(page, command, viewport) {
24102
+ const points = pointsOf(command);
24103
+ for (const point of points) assertInViewport(point, viewport);
24104
+ const [at, end] = points;
24105
+ try {
24106
+ await withModifiers(page, command.modifiers ?? [], async () => {
24107
+ switch (command.action) {
24108
+ case "click":
24109
+ await releasingOnFailure(page, command.button, () => page.mouse.click(at.x, at.y, {
24110
+ button: command.button,
24111
+ clickCount: command.clickCount ?? 1
24112
+ }));
24113
+ return;
24114
+ case "hover":
24115
+ await page.mouse.move(at.x, at.y);
24116
+ return;
24117
+ case "drag":
24118
+ await drag(page, at, end ?? at, command.button);
24119
+ return;
24120
+ case "scroll":
24121
+ await page.mouse.move(at.x, at.y);
24122
+ await page.mouse.wheel(command.deltaX ?? 0, command.deltaY ?? 0);
24123
+ return;
24124
+ }
24125
+ });
24126
+ } catch (error) {
24127
+ if (error instanceof CommandError) throw error;
24128
+ const message = error instanceof Error ? error.message.split("\n")[0] : String(error);
24129
+ throw new CommandError("action_failed", `Mouse ${command.action} at (${fmt(at.x)}, ${fmt(at.y)}) failed: ${message}`);
24130
+ }
24131
+ if (command.action === "scroll") await scrollSettled(page);
24132
+ }
24133
+ /** `browser.mouse`: act at coordinates in a session's tab, checked against its current viewport. */
24134
+ async function mouse(session, command) {
24135
+ await mouseAction(session.page(command.tabId), command, await session.currentViewport(command.tabId));
24136
+ }
22684
24137
  // ../runner/src/browser/handlers.ts
22685
24138
  /**
22686
24139
  * Handlers for the `browser.*` runner commands (§4.3, §12.2). Wire `execute` into the
@@ -22771,6 +24224,10 @@ function createBrowserCommandHandlers(deps) {
22771
24224
  await press(manager.get(c.sessionId), c.key, c.tabId);
22772
24225
  return {};
22773
24226
  },
24227
+ "browser.mouse": async ({ name: _name, sessionId, ...command }) => {
24228
+ await mouse(manager.get(sessionId), command);
24229
+ return {};
24230
+ },
22774
24231
  "browser.screenshot": (c) => screenshot(manager.get(c.sessionId), c.path, {
22775
24232
  tabId: c.tabId,
22776
24233
  fullPage: c.fullPage,
@@ -23634,21 +25091,34 @@ var BrowserSession = class {
23634
25091
  await (await this.cdp(state)).send("Emulation.setDeviceMetricsOverride", deviceMetricsParams(this.viewport));
23635
25092
  } catch {}
23636
25093
  }
23637
- /** A native view's actual size (CSS pixels and scale), as the desktop panel laid it out. */
25094
+ /**
25095
+ * A view's own size (CSS pixels and scale); undefined while it has none. Throws when the page
25096
+ * doesn't answer in time.
25097
+ */
25098
+ async viewSize(page) {
25099
+ const [width, height, scale] = await withTimeout(page.evaluate("[window.innerWidth, window.innerHeight, window.devicePixelRatio]"), NATIVE_MEASURE_TIMEOUT_MS);
25100
+ return width && height ? {
25101
+ width,
25102
+ height,
25103
+ deviceScaleFactor: scale || 1
25104
+ } : void 0;
25105
+ }
25106
+ /** The shown native view's size, just measured: the session's viewport follows it. */
25107
+ adopt(next) {
25108
+ const changed = !sameViewport(next, this.viewport);
25109
+ this.viewport = next;
25110
+ this.measuredAt = Date.now();
25111
+ if (changed) this.screencast.setFrameSize(nativeFrame(), next);
25112
+ }
25113
+ /**
25114
+ * The shown native view's actual size, as the desktop panel laid it out. Tolerant, for viewer
25115
+ * input and viewport answers: a view that doesn't answer keeps its last size.
25116
+ */
23638
25117
  async measure() {
23639
25118
  try {
23640
- const [width, height, scale] = await withTimeout(this.page().evaluate("[window.innerWidth, window.innerHeight, window.devicePixelRatio]"), NATIVE_MEASURE_TIMEOUT_MS);
23641
- if (width && height) {
23642
- const next = {
23643
- width,
23644
- height,
23645
- deviceScaleFactor: scale || 1
23646
- };
23647
- const changed = !sameViewport(next, this.viewport);
23648
- this.viewport = next;
23649
- if (changed) this.screencast.setFrameSize(nativeFrame(), next);
23650
- }
23651
- this.measuredAt = Date.now();
25119
+ const next = await this.viewSize(this.page());
25120
+ if (next) this.adopt(next);
25121
+ else this.measuredAt = Date.now();
23652
25122
  } catch {}
23653
25123
  return { ...this.viewport };
23654
25124
  }
@@ -23659,6 +25129,22 @@ var BrowserSession = class {
23659
25129
  async syncViewport() {
23660
25130
  if (this.native) await this.measure();
23661
25131
  }
25132
+ /**
25133
+ * The viewport of `tabId` (default: the active tab) that an agent's coordinates refer to, now:
25134
+ * the emulated size every tab shares, or a native view's own size measured fresh (agent actions
25135
+ * are few, and the panel may have just resized). A native view in the background can differ
25136
+ * from the shown one, so only the shown view's size updates the session's viewport. Strict,
25137
+ * unlike `measure`: a view that can't be measured (busy in a script, navigating) fails with a
25138
+ * retryable `viewport_unavailable` instead of borrowing another size, so no action is sent.
25139
+ */
25140
+ async currentViewport(tabId) {
25141
+ const state = this.tabState(tabId);
25142
+ if (!this.native) return { ...this.viewport };
25143
+ const size = await this.viewSize(state.page).catch(() => void 0);
25144
+ if (!size) throw new CommandError("viewport_unavailable", `Could not measure the size of tab ${state.tabId} (the page may be busy or loading), so nothing was done. Try again in a moment.`);
25145
+ if (state.tabId === this.activeTabId) this.adopt(size);
25146
+ return { ...size };
25147
+ }
23662
25148
  /** The native view's size for input, measured at most once a second (pointer moves are many). */
23663
25149
  async nativeViewport() {
23664
25150
  if (Date.now() - this.measuredAt > NATIVE_MEASURE_MS) return this.measure();
@@ -24047,6 +25533,68 @@ function connectionHandlers(core, register, unregister, onAttach) {
24047
25533
  }
24048
25534
  };
24049
25535
  }
25536
+ /**
25537
+ * Batches streamed `item.delta` events on their way to the journal. Every event the runner
25538
+ * journals is also a send, a server transaction and an ack, and providers stream an answer a
25539
+ * few characters at a time. Per session, consecutive deltas that extend the same item the
25540
+ * same way (`mergeItemDeltas`) are merged and passed on as one: after `windowMs`, once the
25541
+ * batch holds `maxChars`, or right before any other event of that session, whoever emits it
25542
+ * (the session's provider, a checkpoint command, workspace setup). An event that belongs to
25543
+ * no session (a terminal exiting, a drain) passes every batch on first. So no event overtakes
25544
+ * a delta that came before it, and only deltas ever wait.
25545
+ */
25546
+ var DeltaBatcher = class {
25547
+ out;
25548
+ opts;
25549
+ batches = /* @__PURE__ */ new Map();
25550
+ constructor(out, opts = {}) {
25551
+ this.out = out;
25552
+ this.opts = opts;
25553
+ }
25554
+ push(event) {
25555
+ if (event.kind === "agent" && event.event.type === "item.delta") {
25556
+ this.add(event.sessionId, event.event);
25557
+ return;
25558
+ }
25559
+ this.flush("sessionId" in event ? event.sessionId : void 0);
25560
+ this.out(event);
25561
+ }
25562
+ /** Pass on the pending batch of one session, or of every session. */
25563
+ flush(sessionId) {
25564
+ if (sessionId === void 0) {
25565
+ for (const id of [...this.batches.keys()]) this.flush(id);
25566
+ return;
25567
+ }
25568
+ const batch = this.batches.get(sessionId);
25569
+ if (!batch) return;
25570
+ clearTimeout(batch.timer);
25571
+ this.batches.delete(sessionId);
25572
+ this.out({
25573
+ kind: "agent",
25574
+ sessionId,
25575
+ event: batch.delta
25576
+ });
25577
+ }
25578
+ add(sessionId, delta) {
25579
+ const chars = itemDeltaText(delta.delta).length;
25580
+ let batch = this.batches.get(sessionId);
25581
+ const merged = batch && mergeItemDeltas(batch.delta, delta);
25582
+ if (batch && merged) {
25583
+ batch.delta = merged;
25584
+ batch.chars += chars;
25585
+ } else {
25586
+ this.flush(sessionId);
25587
+ const windowMs = this.opts.windowMs ?? 50;
25588
+ batch = {
25589
+ delta,
25590
+ chars,
25591
+ timer: setTimeout(() => this.flush(sessionId), windowMs)
25592
+ };
25593
+ this.batches.set(sessionId, batch);
25594
+ }
25595
+ if (batch.chars >= (this.opts.maxChars ?? 16384)) this.flush(sessionId);
25596
+ }
25597
+ };
24050
25598
  // ../runner/src/session-host.ts
24051
25599
  /**
24052
25600
  * Owns live provider sessions on this runner and pumps their normalized events into the
@@ -24137,7 +25685,12 @@ var SessionHost = class {
24137
25685
  async send(cmd) {
24138
25686
  const hosted = this.get(cmd.sessionId);
24139
25687
  await hosted.checkpointChain;
24140
- await hosted.session.send(cmd.input);
25688
+ try {
25689
+ await hosted.session.send(cmd.input);
25690
+ } catch (error) {
25691
+ if (error instanceof ProviderSessionError && error.code === "turn_running") throw new CommandError("turn_running", error.message);
25692
+ throw error;
25693
+ }
24141
25694
  }
24142
25695
  async steer(cmd) {
24143
25696
  const { session } = this.get(cmd.sessionId);
@@ -24346,11 +25899,15 @@ async function startRunner(opts) {
24346
25899
  let mcpUpstream = opts.mcpUpstreamUrl ? new HttpMcpUpstream(opts.mcpUpstreamUrl) : void 0;
24347
25900
  let port = 0;
24348
25901
  let stopped = false;
24349
- const emit = (event) => {
25902
+ const journalEvent = (event) => {
24350
25903
  if (stopped) return;
24351
25904
  const msg = journal.append(event);
24352
25905
  attached?.deliver(msg);
24353
25906
  };
25907
+ const deltas = new DeltaBatcher(journalEvent, opts.deltaBatching);
25908
+ const emit = (event) => {
25909
+ if (!stopped) deltas.push(event);
25910
+ };
24354
25911
  const stream = (data) => {
24355
25912
  attached?.deliver({
24356
25913
  type: "stream",
@@ -24481,7 +26038,13 @@ async function startRunner(opts) {
24481
26038
  if ("closeAllConnections" in server) server.closeAllConnections();
24482
26039
  });
24483
26040
  process.off("exit", removeLock);
26041
+ deltas.flush();
24484
26042
  stopped = true;
26043
+ try {
26044
+ journal.close();
26045
+ } catch (error) {
26046
+ log(`journal: ${String(error)}`);
26047
+ }
24485
26048
  log("runner stopped");
24486
26049
  })();
24487
26050
  return closing;
@@ -24494,6 +26057,6 @@ async function startRunner(opts) {
24494
26057
  close
24495
26058
  };
24496
26059
  }
24497
- export { isElectronRuntime as A, installClaudeSdk as C, acquireInstallLock as D, resolveNpm as E, import_websocket_server as M, wrapper_default as N, brokenInstallMessage as O, importClaudeSdkFrom as S, runNpm as T, claudeMemoryDir as _, bufferToFormData as a, claudeSdkSource as b, refreshProcessPath as c, ImageEngineRegistry as d, createImageEngineRegistry as f, ImageGenerationError as g, resolveImageOutput as h, tryDecodeURIComponent as i, ownNodeEnv as j, findExecutable as k, defaultShell as l, imageInfo as m, readLock as n, createAdaptorServer as o, IMAGE_ASPECT_RATIOS as p, Hono as r, upgradeWebSocket as s, startRunner as t, shellCommandLine as u, findGitRoot as v, isClaudeSdkInstalled as w, CLAUDE_SDK_VERSION as x, mainRepoRoot as y };
26060
+ export { findExecutable as A, installClaudeSdk as C, acquireInstallLock as D, resolveNpm as E, ownNodeEnv as M, import_websocket_server as N, isSecretPath as O, wrapper_default as P, importClaudeSdkFrom as S, runNpm as T, claudeMemoryDir as _, bufferToFormData as a, claudeSdkSource as b, refreshProcessPath as c, ImageEngineRegistry as d, createImageEngineRegistry as f, ImageGenerationError as g, resolveImageOutput as h, tryDecodeURIComponent as i, isElectronRuntime as j, brokenInstallMessage as k, defaultShell as l, imageInfo as m, readLock as n, createAdaptorServer as o, IMAGE_ASPECT_RATIOS as p, Hono as r, upgradeWebSocket as s, startRunner as t, shellCommandLine as u, findGitRoot as v, isClaudeSdkInstalled as w, CLAUDE_SDK_VERSION as x, mainRepoRoot as y };
24498
26061
 
24499
- //# sourceMappingURL=runner-DYf8dV43.js.map
26062
+ //# sourceMappingURL=runner-CQYh087M.js.map