@integrity-labs/agt-cli 0.28.1175 → 0.28.1177

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 (29) hide show
  1. package/dist/bin/agt.js +7 -7
  2. package/dist/{chunk-P2B2SQHD.js → chunk-45UH24I2.js} +5 -5
  3. package/dist/{chunk-OAWK4OBN.js → chunk-6HBXEJHO.js} +2 -2
  4. package/dist/{chunk-EL5MWGGH.js → chunk-AIUIMEUS.js} +87 -3
  5. package/dist/chunk-AIUIMEUS.js.map +1 -0
  6. package/dist/{chunk-EK6JVOBH.js → chunk-H37WRW2E.js} +47 -8
  7. package/dist/chunk-H37WRW2E.js.map +1 -0
  8. package/dist/{chunk-JB23A42P.js → chunk-NHUXO5H2.js} +2 -2
  9. package/dist/{chunk-B6UYVEMD.js → chunk-OO36JBFI.js} +35 -1
  10. package/dist/chunk-OO36JBFI.js.map +1 -0
  11. package/dist/{claude-code-updater-PQYMHNQT.js → claude-code-updater-H7RNX6M7.js} +3 -3
  12. package/dist/{claude-pair-runtime-RKDNAYEU.js → claude-pair-runtime-53JY6TQX.js} +4 -4
  13. package/dist/{claude-signin-reset-EF3UO5IK.js → claude-signin-reset-6IJMMVYG.js} +3 -3
  14. package/dist/lib/manager-worker.js +20 -20
  15. package/dist/mcp/index.js +17 -8
  16. package/dist/{persistent-session-2365YL7P.js → persistent-session-HVYGIS5C.js} +5 -5
  17. package/dist/{responsiveness-probe-7HARR32R.js → responsiveness-probe-OGZ6ADVR.js} +5 -5
  18. package/package.json +1 -1
  19. package/dist/chunk-B6UYVEMD.js.map +0 -1
  20. package/dist/chunk-EK6JVOBH.js.map +0 -1
  21. package/dist/chunk-EL5MWGGH.js.map +0 -1
  22. /package/dist/{chunk-P2B2SQHD.js.map → chunk-45UH24I2.js.map} +0 -0
  23. /package/dist/{chunk-OAWK4OBN.js.map → chunk-6HBXEJHO.js.map} +0 -0
  24. /package/dist/{chunk-JB23A42P.js.map → chunk-NHUXO5H2.js.map} +0 -0
  25. /package/dist/{claude-code-updater-PQYMHNQT.js.map → claude-code-updater-H7RNX6M7.js.map} +0 -0
  26. /package/dist/{claude-pair-runtime-RKDNAYEU.js.map → claude-pair-runtime-53JY6TQX.js.map} +0 -0
  27. /package/dist/{claude-signin-reset-EF3UO5IK.js.map → claude-signin-reset-6IJMMVYG.js.map} +0 -0
  28. /package/dist/{persistent-session-2365YL7P.js.map → persistent-session-HVYGIS5C.js.map} +0 -0
  29. /package/dist/{responsiveness-probe-7HARR32R.js.map → responsiveness-probe-OGZ6ADVR.js.map} +0 -0
package/dist/bin/agt.js CHANGED
@@ -40,19 +40,19 @@ import {
40
40
  success,
41
41
  table,
42
42
  warn
43
- } from "../chunk-P2B2SQHD.js";
43
+ } from "../chunk-45UH24I2.js";
44
44
  import {
45
45
  getProjectDir,
46
46
  isSessionResumeDisabled,
47
47
  readDirectChatSessionState
48
- } from "../chunk-EK6JVOBH.js";
48
+ } from "../chunk-H37WRW2E.js";
49
49
  import {
50
50
  markDailySessionSpawn,
51
51
  sessionFileExists,
52
52
  sessionTranscriptDir
53
53
  } from "../chunk-DJNCIWVG.js";
54
- import "../chunk-B6UYVEMD.js";
55
- import "../chunk-OAWK4OBN.js";
54
+ import "../chunk-OO36JBFI.js";
55
+ import "../chunk-6HBXEJHO.js";
56
56
  import "../chunk-XWVM4KPK.js";
57
57
  import {
58
58
  AnchorSessionClient,
@@ -85,7 +85,7 @@ import {
85
85
  resolveChannels,
86
86
  serializeManifestForSlackCli
87
87
  } from "../chunk-47P33SZF.js";
88
- import "../chunk-EL5MWGGH.js";
88
+ import "../chunk-AIUIMEUS.js";
89
89
  import "../chunk-DGUM43GV.js";
90
90
 
91
91
  // src/bin/agt.ts
@@ -5513,7 +5513,7 @@ import { execFileSync, execSync } from "child_process";
5513
5513
  import { existsSync as existsSync11, realpathSync as realpathSync2 } from "fs";
5514
5514
  import chalk18 from "chalk";
5515
5515
  import ora16 from "ora";
5516
- var cliVersion = true ? "0.28.1175" : "dev";
5516
+ var cliVersion = true ? "0.28.1177" : "dev";
5517
5517
  async function fetchLatestVersion() {
5518
5518
  const host2 = getHost();
5519
5519
  if (!host2) return null;
@@ -6773,7 +6773,7 @@ function handleError(err) {
6773
6773
  }
6774
6774
 
6775
6775
  // src/bin/agt.ts
6776
- var cliVersion2 = true ? "0.28.1175" : "dev";
6776
+ var cliVersion2 = true ? "0.28.1177" : "dev";
6777
6777
  var program = new Command();
6778
6778
  program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
6779
6779
  program.hook("preAction", async (thisCommand, actionCommand) => {
@@ -16,7 +16,7 @@ import {
16
16
  parseEnvFileEntries,
17
17
  parseEnvIntegrations,
18
18
  shellQuote
19
- } from "./chunk-EK6JVOBH.js";
19
+ } from "./chunk-H37WRW2E.js";
20
20
  import {
21
21
  readDailySessionPin,
22
22
  sessionTranscriptDir
@@ -26,7 +26,7 @@ import {
26
26
  isConsumerTermsDialogVisible,
27
27
  paneRemedy,
28
28
  summarizeUnanswerablePane
29
- } from "./chunk-B6UYVEMD.js";
29
+ } from "./chunk-OO36JBFI.js";
30
30
  import {
31
31
  parsePsRows
32
32
  } from "./chunk-XWVM4KPK.js";
@@ -95,7 +95,7 @@ import {
95
95
  claudeJsonPathFor,
96
96
  effectiveClaudeJsonPath,
97
97
  inspectClaudeConfig
98
- } from "./chunk-EL5MWGGH.js";
98
+ } from "./chunk-AIUIMEUS.js";
99
99
 
100
100
  // ../../packages/core/dist/provisioning/mcp-config-guards.js
101
101
  import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from "fs";
@@ -8078,7 +8078,7 @@ function exchangeFailureKind(err) {
8078
8078
  }
8079
8079
 
8080
8080
  // src/lib/api-client.ts
8081
- var agtCliVersion = true ? "0.28.1175" : "dev";
8081
+ var agtCliVersion = true ? "0.28.1177" : "dev";
8082
8082
  var lastConfigHash = null;
8083
8083
  function setConfigHash(hash) {
8084
8084
  lastConfigHash = hash && hash.length > 0 ? hash : null;
@@ -13235,4 +13235,4 @@ export {
13235
13235
  managerInstallSystemUnitCommand,
13236
13236
  managerUninstallSystemUnitCommand
13237
13237
  };
13238
- //# sourceMappingURL=chunk-P2B2SQHD.js.map
13238
+ //# sourceMappingURL=chunk-45UH24I2.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  claudeConfigDirFor
3
- } from "./chunk-EL5MWGGH.js";
3
+ } from "./chunk-AIUIMEUS.js";
4
4
 
5
5
  // src/lib/claude-signin-reset.ts
6
6
  import { readdirSync, renameSync } from "fs";
@@ -114,4 +114,4 @@ export {
114
114
  describeSigninResetForLog,
115
115
  describeSigninResetFailure
116
116
  };
117
- //# sourceMappingURL=chunk-OAWK4OBN.js.map
117
+ //# sourceMappingURL=chunk-6HBXEJHO.js.map
@@ -53,7 +53,20 @@ function effectiveClaudeJsonPath(homeDir, env = process.env) {
53
53
  }
54
54
 
55
55
  // src/lib/claude-config-location.ts
56
- import { constants, copyFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from "fs";
56
+ import {
57
+ closeSync,
58
+ constants,
59
+ copyFileSync,
60
+ existsSync,
61
+ fstatSync,
62
+ mkdirSync,
63
+ openSync,
64
+ readFileSync as readFileSync2,
65
+ renameSync,
66
+ unlinkSync,
67
+ writeFileSync
68
+ } from "fs";
69
+ import { dirname } from "path";
57
70
  function seedClaudeConfigFromLegacy(opts = {}) {
58
71
  const env = opts.env ?? process.env;
59
72
  const log = opts.log ?? (() => {
@@ -80,7 +93,8 @@ function seedClaudeConfigFromLegacy(opts = {}) {
80
93
  }
81
94
  if (!legacyUsable) {
82
95
  log(
83
- `[claude-config] legacy ${legacy} is ${legacyState} \u2014 NOT seeding. Re-pair the host, then the next spawn seeds a good file.`
96
+ `[claude-config] legacy ${legacy} is ${legacyState} \u2014 NOT seeding. Re-pair the host, then the next spawn seeds a good file.` + // Only `no-account`: a file the write refuses stays broken on every spawn.
97
+ (legacyState === "no-account" ? ` (An API-key host has nothing to pair: its next spawn writes this file itself.)` : "")
84
98
  );
85
99
  return "legacy-unusable";
86
100
  }
@@ -109,6 +123,74 @@ function seedClaudeConfigFromLegacy(opts = {}) {
109
123
  return "failed";
110
124
  }
111
125
  }
126
+ var CLAUDE_API_KEY_TAIL_CHARS = 20;
127
+ function readWithoutFollowingLink(path) {
128
+ let fd;
129
+ try {
130
+ fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
131
+ } catch (err) {
132
+ if (err.code === "ELOOP") return null;
133
+ throw err;
134
+ }
135
+ try {
136
+ return fstatSync(fd).isFile() ? readFileSync2(fd, "utf-8") : null;
137
+ } finally {
138
+ closeSync(fd);
139
+ }
140
+ }
141
+ function spawnApiKeyCredential(args) {
142
+ if (args.openRouterMode || args.policyEnvActive || args.claudeAuthMode !== "api_key") return null;
143
+ return args.anthropicApiKey || null;
144
+ }
145
+ function approveApiKeyInClaudeConfig(opts) {
146
+ const log = opts.log ?? (() => {
147
+ });
148
+ const target = effectiveClaudeJsonPath(opts.homeDir, opts.env ?? process.env);
149
+ const tail = opts.apiKey.slice(-CLAUDE_API_KEY_TAIL_CHARS);
150
+ try {
151
+ const state = inspectClaudeConfig(target);
152
+ if (state === "symlink" || state === "unreadable") {
153
+ log(`[claude-config] ${target} is ${state}: not approving the API key in it`);
154
+ return "refused";
155
+ }
156
+ const raw = state === "missing" ? "{}" : readWithoutFollowingLink(target);
157
+ if (raw === null) {
158
+ log(`[claude-config] ${target} changed to a non-file while reading: not approving the API key in it`);
159
+ return "refused";
160
+ }
161
+ const config = JSON.parse(raw);
162
+ const responses = config["customApiKeyResponses"] ?? {};
163
+ const asList = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
164
+ const approved = asList(responses["approved"]);
165
+ const rejected = asList(responses["rejected"]);
166
+ if (config["hasCompletedOnboarding"] === true && approved.includes(tail) && !rejected.includes(tail)) {
167
+ return "already-approved";
168
+ }
169
+ config["hasCompletedOnboarding"] = true;
170
+ config["customApiKeyResponses"] = {
171
+ ...responses,
172
+ approved: approved.includes(tail) ? approved : [...approved, tail],
173
+ rejected: rejected.filter((t) => t !== tail)
174
+ };
175
+ mkdirSync(dirname(target), { recursive: true });
176
+ const tmp = `${target}.approve-${process.pid}`;
177
+ try {
178
+ writeFileSync(tmp, JSON.stringify(config, null, 2), { flag: "wx", mode: 384 });
179
+ renameSync(tmp, target);
180
+ } catch (err) {
181
+ try {
182
+ if (existsSync(tmp)) unlinkSync(tmp);
183
+ } catch {
184
+ }
185
+ throw err;
186
+ }
187
+ log(`[claude-config] marked ${target} ready for this API-key host`);
188
+ return "written";
189
+ } catch (err) {
190
+ log(`[claude-config] could not approve the API key in ${target}: ${err.message}`);
191
+ return "failed";
192
+ }
193
+ }
112
194
  function claudeConfigDirIsReady(homeDir, env = process.env) {
113
195
  return inspectClaudeConfig(claudeJsonPathFor(homeDir, env)) === "usable";
114
196
  }
@@ -154,7 +236,9 @@ export {
154
236
  inspectClaudeConfig,
155
237
  effectiveClaudeJsonPath,
156
238
  seedClaudeConfigFromLegacy,
239
+ spawnApiKeyCredential,
240
+ approveApiKeyInClaudeConfig,
157
241
  claudeConfigDirIsReady,
158
242
  archiveLegacyClaudeJson
159
243
  };
160
- //# sourceMappingURL=chunk-EL5MWGGH.js.map
244
+ //# sourceMappingURL=chunk-AIUIMEUS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../packages/core/src/provisioning/frameworks/claudecode/config-location.ts","../src/lib/claude-config-location.ts"],"sourcesContent":["/**\n * ENG-10869 — where `.claude.json` lives. The lower half of the locator.\n *\n * Path resolution lives HERE, in core, rather than only in the CLI, because\n * `ensureWorkspaceTrusted` in this same directory WRITES that file — atomically,\n * temp-then-rename, on the manager's tick. A second hand-kept copy of the rule\n * would have drifted the moment the CLI's copy moved, and the symptom would be\n * the trust flag written to a file no agent reads: every new agent back on the\n * trust prompt, with the write succeeding. The CLI's `claude-config-location`\n * re-exports these and adds the seeding and readiness checks on top, so there is\n * one implementation of the path and one of the migration.\n *\n * Worth recording while it is fresh: `claude-json-health.ts` states that \"an\n * exhaustive search of apps/cli, packages/core and packages/api finds no write\n * and no unlink of `.claude.json`\". That is not correct — `ensureWorkspaceTrusted`\n * is a writer, and it is in the set that search covered. The conclusion drawn\n * from it (that there is no call site to make atomic) happens to hold anyway,\n * because that writer was already atomic; but the premise should not be relied\n * on again.\n */\n\nimport { lstatSync, readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n/** The env var Claude Code reads for its config root. Not ours; do not rename. */\nexport const CLAUDE_CONFIG_DIR_ENV = 'CLAUDE_CONFIG_DIR';\n\n/**\n * The config root Claude Code will use.\n *\n * A blank value is treated as unset rather than as the empty path: `''` would\n * make `join` produce a path relative to the process cwd, which is the same\n * class of trap ENG-5070 hit with `HOME=''` — broken, but better hidden than an\n * outright failure.\n */\nexport function claudeConfigDirFor(\n homeDir?: string,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n const explicit = env[CLAUDE_CONFIG_DIR_ENV]?.trim();\n if (explicit) return explicit;\n return join(homeDir || env.HOME?.trim() || homedir(), '.claude');\n}\n\n/** The `.claude.json` a Claude Code process will actually read and write. */\nexport function claudeJsonPathFor(homeDir?: string, env: NodeJS.ProcessEnv = process.env): string {\n return join(claudeConfigDirFor(homeDir, env), '.claude.json');\n}\n\n/**\n * Where `.claude.json` lived before ENG-10869: beside the config dir rather\n * than inside it, which is why it needed a single-file bind mount and why that\n * bind pinned each container's inode.\n */\nexport function legacyClaudeJsonPathFor(\n homeDir?: string,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n return join(homeDir || env.HOME?.trim() || homedir(), '.claude.json');\n}\n\n/**\n * ENG-10869 — what state the config at `path` is in.\n *\n * ## Why \"parses\" was the wrong test\n *\n * The first version of this change gated the whole migration on \"the file\n * exists and `JSON.parse` succeeds\". Two reviewers found the same hole from\n * opposite ends, and they were both right:\n *\n * - `ensureWorkspaceTrusted` (this very directory) CREATES the file when a\n * project needs its trust flag. That file is `{\"projects\":{…}}` — it parses\n * perfectly, and it contains no account at all. So the manager could\n * manufacture a config that looked ready, the spawn would drop the legacy\n * bind on the strength of it, and every agent on the host would come up on\n * the login picker. The fix's own writer could cause the outage the fix\n * exists to prevent.\n * - The same weakness made the documented recovery useless: an operator\n * running `claude /login` writes the LEGACY path, the seed saw a parseable\n * target and declared `already-present`, and the fresh account never\n * reached an agent.\n *\n * So readiness means **an agent can actually start from this file**, which\n * requires account-bearing state, not merely valid JSON.\n *\n * ## The rule is `hasCompletedOnboarding`, and PRODUCTION chose it\n *\n * The first version accepted `oauthAccount` **or** `hasCompletedOnboarding`, on\n * the reasoning that either proves an account. agt-aws-1 disproved it on\n * 2026-09-22 while this change was still in review:\n *\n * host /root/.claude.json 27 keys, PARSES, oauthAccount present,\n * hasCompletedOnboarding ABSENT\n * -> all 8 agents sitting on the Claude Code login picker\n *\n * Under the OR that config reads as `usable`, the spawn drops the legacy bind\n * on the strength of it, and every agent on the host is left on the picker with\n * no fallback — the exact outage this file exists to prevent, caused by it.\n *\n * **What the evidence does and does not establish.** The successful re-pair that\n * recovered the host wrote BOTH a new `hasCompletedOnboarding: true` and fresh\n * credentials, so this is a correlation, not a proof that the flag alone is\n * what Claude Code gates on. The rule is chosen on the ASYMMETRY rather than on\n * the causal claim:\n *\n * - Requiring too MUCH costs a migration that does not happen. The host stays\n * on the legacy bind, which is exactly today's behaviour.\n * - Requiring too LITTLE costs every agent on the host, with the legacy file\n * already archived out from under them.\n *\n * So it requires the onboarding flag, and `oauthAccount` is no longer\n * sufficient on its own. This also lands close to where a reviewer's instinct\n * pointed on #6368; the counter-argument made there was weaker than it read.\n *\n * ## Symlinks are refused, not followed\n *\n * `.claude.json` now lives inside a directory the agent can write. A compromised\n * agent could replace it with a symlink to a file outside its own mounts — say\n * another agent's `.mcp.json` — and this function runs as the host manager. A\n * read that follows it, followed by a write-back, copies private content into a\n * shared mount. The single-file bind made that impossible before, so the\n * relocation is what introduces the exposure and the refusal belongs here, at\n * the one place both the reader and the writer consult.\n */\nexport type ClaudeConfigState =\n /** Nothing at the path. */\n | 'missing'\n /** A symlink. Deliberately not followed — see the docblock. */\n | 'symlink'\n /** Present but not a readable, parseable regular file (includes a DIRECTORY). */\n | 'unreadable'\n /** Parses, but carries no account — an agent would land on the login picker. */\n | 'no-account'\n /** An agent can start from this. */\n | 'usable';\n\nexport function inspectClaudeConfig(path: string): ClaudeConfigState {\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(path);\n } catch {\n return 'missing';\n }\n // lstat, never stat: the point is to SEE the symlink rather than resolve it.\n if (stat.isSymbolicLink()) return 'symlink';\n if (!stat.isFile()) return 'unreadable';\n let parsed: unknown;\n try {\n parsed = JSON.parse(readFileSync(path, 'utf-8'));\n } catch {\n return 'unreadable';\n }\n if (typeof parsed !== 'object' || parsed === null) return 'unreadable';\n const config = parsed as Record<string, unknown>;\n // `=== true`, not truthiness: a config carrying `hasCompletedOnboarding: 1`\n // or `\"false\"` is a shape nobody writes, and guessing at it is how a rule\n // that must fail safe starts admitting things.\n return config['hasCompletedOnboarding'] === true ? 'usable' : 'no-account';\n}\n\n/** mtime in ms, or 0 when the path cannot be stat'ed. Never follows a symlink. */\nexport function claudeConfigMtimeMs(path: string): number {\n try {\n const stat = lstatSync(path);\n return stat.isFile() ? stat.mtimeMs : 0;\n } catch {\n return 0;\n }\n}\n\n/**\n * ENG-10869 — the `.claude.json` an agent on this host will ACTUALLY read.\n *\n * The migration is per-host and conditional: the spawn path hands a container\n * `CLAUDE_CONFIG_DIR` only once the config dir holds a usable file, and\n * otherwise falls back to binding the legacy path. So there are hosts, for as\n * long as the seed has not landed, where the legacy file is still the live one.\n *\n * Anything that WRITES this file has to resolve it the same way, or it writes\n * somewhere nothing reads. That is not hypothetical: `ensureWorkspaceTrusted`\n * was moved to the config dir unconditionally while the reader stayed\n * conditional, which on any host the seed missed put the trust flag in a file no\n * agent opens — the write succeeding, and every new agent going back to dying on\n * the trust prompt (the ENG-9573 incident, restored behind green monitoring).\n *\n * One function, consulted by both sides, is what keeps them from disagreeing.\n */\nexport function effectiveClaudeJsonPath(\n homeDir?: string,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n const inConfigDir = claudeJsonPathFor(homeDir, env);\n if (inspectClaudeConfig(inConfigDir) === 'usable') return inConfigDir;\n return legacyClaudeJsonPathFor(homeDir, env);\n}\n","/**\n * ENG-10869 — where `.claude.json` lives, in ONE place.\n *\n * ## The bug this exists to close\n *\n * `~/.claude.json` sits OUTSIDE `~/.claude`, so the container spawn path had to\n * give it its own mount — and the only available shape was a **single-file**\n * bind. A file bind pins the inode at container start, and Claude Code replaces\n * that file by **atomic rename**. So every host-side write orphaned the view of\n * every already-running container, permanently, until it respawned.\n *\n * Measured on `agt-aws-1` 2026-09-22 00:37Z: SIX live versions of one config\n * across eight agents, the oldest 12+ hours stale. Containers that booted\n * together shared an inode; containers that booted apart did not.\n *\n * Two consequences, both observed in prod:\n *\n * - **Stale identity.** `.credentials.json` is inside the `~/.claude`\n * DIRECTORY mount and propagates correctly, so a refreshed token reaches the\n * container while `oauthAccount` in `.claude.json` does not. Current\n * credentials, stale account, and Claude Code shows the login picker\n * (alyve-host-1, 2026-09-21, ~9h50m across four agents).\n * - **Corruption.** A container CANNOT rename onto its own file bind — it\n * fails EBUSY (verified on agt-runtime; the same finding ENG-8854 recorded\n * for `history.jsonl` on 2026-09-11) — so a container write falls back to\n * truncate-in-place. Agents that booted together share one inode and tear\n * each other's writes: \"Unexpected EOF\", and the 0-byte case at its limit\n * (ENG-10140).\n *\n * Fleet evidence that the mount and not the concurrency is the cause: 39\n * corruptions across 18 containerised hosts / 61 agents, and ZERO across 5\n * non-containerised hosts / 17 agents — one of which runs seven agents against\n * that same path.\n *\n * ## The fix, and why it is this one\n *\n * `CLAUDE_CONFIG_DIR` relocates the whole config root, `.claude.json` included.\n * Verified on the `agt-runtime` image: with it set, Claude Code wrote\n * `.claude.json`, `backups/`, `projects/` and `sessions/` inside that directory,\n * and authenticated from a `.credentials.json` placed there.\n *\n * Pointing it at `~/.claude` moves `.claude.json` INSIDE the directory mount\n * that already works, and moves nothing else — `settings.json`, `plugins`,\n * `backups/` and `projects/` are already there. Both directions were then\n * verified from a production host with no code changed:\n *\n * container renames a file inside a DIR mount -> SUCCEEDED (213400 -> 213401)\n * host renames it; running container re-reads -> sees the new content, no respawn\n *\n * ## Why this module exists rather than a constant\n *\n * FIVE call sites computed this path independently — the spawn mount, the pair\n * flow, the device-state probe, the account fingerprint and the config-health\n * check. That is the same drift hazard {@link CLAUDE_MANAGED_SETTINGS_DIR} was\n * made a single source of truth for, and it is worse here: if the host writes\n * one path while containers read another, pairing silently stops reaching\n * agents. Every consumer resolves through here so they cannot disagree.\n */\n\nimport {\n closeSync,\n constants,\n copyFileSync,\n existsSync,\n fstatSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs';\nimport { dirname } from 'node:path';\n/**\n * Path resolution is DELEGATED to core, not reimplemented here.\n *\n * `ensureWorkspaceTrusted` in core writes this file on the manager's tick, so\n * core needs the rule too. Two copies of it would drift, and the symptom would\n * be a write that succeeds into a file nothing reads. This module owns the\n * MIGRATION (seeding, readiness); core owns the PATH.\n */\nexport {\n CLAUDE_CONFIG_DIR_ENV,\n claudeConfigDirFor as claudeConfigDir,\n claudeJsonPathFor as claudeJsonPath,\n legacyClaudeJsonPathFor as legacyClaudeJsonPath,\n effectiveClaudeJsonPath,\n inspectClaudeConfig,\n type ClaudeConfigState,\n} from '@augmented/core/provisioning/frameworks/claudecode/config-location.js';\nimport {\n claudeConfigDirFor as claudeConfigDir,\n claudeJsonPathFor as claudeJsonPath,\n legacyClaudeJsonPathFor as legacyClaudeJsonPath,\n effectiveClaudeJsonPath,\n claudeConfigMtimeMs,\n inspectClaudeConfig,\n} from '@augmented/core/provisioning/frameworks/claudecode/config-location.js';\n\nexport type SeedOutcome =\n /** The config dir already holds a usable `.claude.json`; nothing to do. */\n | 'already-present'\n /** Copied from the legacy path. */\n | 'seeded'\n /**\n * The legacy file was NEWER and usable, so it was copied forward over an\n * existing target. This is the `claude /login` recovery path — see the\n * docblock on {@link seedClaudeConfigFromLegacy}.\n */\n | 'reseeded-from-newer-legacy'\n /** Neither location has one — a never-paired host. Claude Code will create it. */\n | 'absent'\n /** The legacy file exists but is not usable; deliberately NOT copied. */\n | 'legacy-unusable'\n /** The copy itself failed. */\n | 'failed';\n\n/**\n * Move the host onto the new location, once.\n *\n * **Ordering is the whole risk of this change.** If a container is handed\n * `CLAUDE_CONFIG_DIR` before the file exists there, Claude Code creates a fresh\n * one: credentials still resolve (they are already in the config dir) but\n * `oauthAccount` and `hasCompletedOnboarding` are gone, which is the login\n * picker — the exact outage this change exists to prevent, caused by the fix.\n * So this MUST run before the first spawn that uses the new path, and the\n * caller must treat a non-seed as a reason not to proceed.\n *\n * The copy is atomic by temp-then-rename INSIDE the destination directory, so a\n * crash mid-write cannot leave a truncated config where agents will read one —\n * the same property `.mcp.json` got in ENG-4787 and `.claude.json` never had.\n *\n * A legacy file that does not parse is NOT copied. Propagating a corrupt config\n * to the new location would migrate the incident along with the data, and the\n * corrupt original is exactly what the old mount was producing.\n */\nexport function seedClaudeConfigFromLegacy(opts: {\n homeDir?: string;\n env?: NodeJS.ProcessEnv;\n log?: (msg: string) => void;\n} = {}): SeedOutcome {\n const env = opts.env ?? process.env;\n const log = opts.log ?? (() => {});\n const target = claudeJsonPath(opts.homeDir, env);\n const legacy = legacyClaudeJsonPath(opts.homeDir, env);\n\n if (target === legacy) return 'already-present';\n\n try {\n const targetState = inspectClaudeConfig(target);\n const legacyState = inspectClaudeConfig(legacy);\n\n // ENG-10869: NOT `existsSync(target)`.\n //\n // The one-shot existence test was wrong in both directions. A file that\n // merely exists can be account-less — `ensureWorkspaceTrusted` creates\n // exactly such a file — so the seed would skip, the spawn would drop the\n // legacy bind on a config no agent can start from, and every agent on the\n // host would sit on the login picker. And once the target existed, a LATER\n // write to the legacy path could never be picked up, which silently broke\n // the documented recovery: `agt host pair` and the manager's own\n // blocked-agent message both tell an operator to run `claude /login`, and a\n // plain shell has no CLAUDE_CONFIG_DIR, so that writes the LEGACY file. The\n // seed said `already-present`, the fresh account never reached an agent,\n // and the archive step then renamed the operator's new config aside — once\n // per re-login, with Claude Code reporting a successful login each time.\n if (targetState === 'symlink') {\n log(\n `[claude-config] ${target} is a SYMLINK — refusing to seed through it. ` +\n `Remove it on the host; a config that an agent can replace with a link is not one we write to.`,\n );\n return 'failed';\n }\n\n const legacyUsable = legacyState === 'usable';\n const targetUsable = targetState === 'usable';\n // A legacy file written AFTER the target is the recovery case above.\n const legacyIsNewer = claudeConfigMtimeMs(legacy) > claudeConfigMtimeMs(target);\n\n if (targetUsable && !(legacyUsable && legacyIsNewer)) return 'already-present';\n\n if (legacyState === 'missing') {\n log(`[claude-config] no legacy ${legacy} to seed from — Claude Code will create ${target}`);\n return 'absent';\n }\n if (!legacyUsable) {\n log(\n `[claude-config] legacy ${legacy} is ${legacyState} — NOT seeding. ` +\n `Re-pair the host, then the next spawn seeds a good file.` +\n // Only `no-account`: a file the write refuses stays broken on every spawn.\n (legacyState === 'no-account'\n ? ` (An API-key host has nothing to pair: its next spawn writes this file itself.)`\n : ''),\n );\n return 'legacy-unusable';\n }\n const reseeding = targetUsable;\n if (reseeding) {\n log(\n `[claude-config] legacy ${legacy} is NEWER than ${target} and carries an account — ` +\n `copying it forward (an operator ran 'claude /login' outside the config dir)`,\n );\n }\n\n mkdirSync(claudeConfigDir(opts.homeDir, env), { recursive: true });\n const tmp = `${target}.seed-${process.pid}`;\n try {\n // ENG-10869 security: COPYFILE_EXCL, for the same reason the trust\n // writer uses `wx`. This destination sits in a directory the agent can\n // write, and the manager runs outside the container — a pre-planted\n // symlink here would have the manager copy the config through it onto an\n // arbitrary host file. EXCL fails with EEXIST rather than following.\n copyFileSync(legacy, tmp, constants.COPYFILE_EXCL);\n renameSync(tmp, target);\n } catch (err) {\n try {\n if (existsSync(tmp)) unlinkSync(tmp);\n } catch {\n // Best effort; a stray .seed-<pid> is inert.\n }\n throw err;\n }\n log(`[claude-config] seeded ${target} from ${legacy}`);\n return reseeding ? 'reseeded-from-newer-legacy' : 'seeded';\n } catch (err) {\n log(`[claude-config] seeding ${target} failed: ${(err as Error).message}`);\n return 'failed';\n }\n}\n\n/** ENG-11793: how many trailing key characters Claude Code stores and displays. */\nexport const CLAUDE_API_KEY_TAIL_CHARS = 20;\n\nexport type ApiKeyApprovalOutcome = 'written' | 'already-approved' | 'refused' | 'failed';\n\n/**\n * ENG-11793: read through one no-follow, non-blocking descriptor. An agent can swap the\n * path for a link or a FIFO after the lstat in `inspectClaudeConfig`, and following it would copy a file the\n * agent can't read into this one. Null when the path isn't a regular file by now.\n */\nfunction readWithoutFollowingLink(path: string): string | null {\n let fd: number;\n try {\n // O_NONBLOCK: opening a FIFO with no writer would otherwise block here, and this\n // call is synchronous, so an agent could stall the manager for the whole host.\n fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ELOOP') return null;\n throw err;\n }\n try {\n return fstatSync(fd).isFile() ? readFileSync(fd, 'utf-8') : null;\n } finally {\n closeSync(fd);\n }\n}\n\n/**\n * ENG-11793: the API key that is this session's credential, or null when something\n * else governs it. One rule for the config write and the dialog backstop, matching\n * the injection chain: OpenRouter and a model policy both supersede the host's key.\n */\nexport function spawnApiKeyCredential(args: {\n claudeAuthMode: string;\n openRouterMode: boolean;\n policyEnvActive: boolean;\n anthropicApiKey?: string | null;\n}): string | null {\n if (args.openRouterMode || args.policyEnvActive || args.claudeAuthMode !== 'api_key') return null;\n return args.anthropicApiKey || null;\n}\n\n/**\n * ENG-11793: let an API-key host start with nobody at the pane.\n *\n * Pairing is what normally completes onboarding, and an API-key host has no account\n * to pair. Measured on Claude Code 2.1.308 with a key in the env:\n *\n * nothing set theme picker, then \"use this API key?\" (default No)\n * tail in `rejected` theme picker, then the login picker\n * onboarded only \"use this API key?\"\n * onboarded + tail approved no key or onboarding prompt\n *\n * So this sets onboarding, approves the tail, and takes the tail out of `rejected`.\n * Only the tail is written, which is what Claude Code stores itself. The full key stays\n * in the session env, and neither reaches a log line.\n *\n * ONLY FOR API-KEY SPAWNS. `hasCompletedOnboarding` is what the ENG-10869 gate reads as\n * \"this config is usable\", so setting it on a subscription host would pass that gate\n * with no account behind it.\n *\n * Writes the file `effectiveClaudeJsonPath` resolves, the same one the trust writer\n * uses. On a fresh host that is the legacy path, which makes it usable, so the existing\n * seed copies it forward unchanged.\n */\nexport function approveApiKeyInClaudeConfig(opts: {\n apiKey: string;\n homeDir?: string;\n env?: NodeJS.ProcessEnv;\n log?: (msg: string) => void;\n}): ApiKeyApprovalOutcome {\n const log = opts.log ?? (() => {});\n const target = effectiveClaudeJsonPath(opts.homeDir, opts.env ?? process.env);\n const tail = opts.apiKey.slice(-CLAUDE_API_KEY_TAIL_CHARS);\n try {\n // This directory is agent-writable and the manager runs outside the container, so\n // never follow a link planted here, and never replace a file we can't parse.\n const state = inspectClaudeConfig(target);\n if (state === 'symlink' || state === 'unreadable') {\n log(`[claude-config] ${target} is ${state}: not approving the API key in it`);\n return 'refused';\n }\n const raw = state === 'missing' ? '{}' : readWithoutFollowingLink(target);\n if (raw === null) {\n log(`[claude-config] ${target} changed to a non-file while reading: not approving the API key in it`);\n return 'refused';\n }\n const config = JSON.parse(raw) as Record<string, unknown>;\n\n const responses = (config['customApiKeyResponses'] ?? {}) as Record<string, unknown>;\n const asList = (v: unknown) =>\n Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : [];\n const approved = asList(responses['approved']);\n const rejected = asList(responses['rejected']);\n if (\n config['hasCompletedOnboarding'] === true &&\n approved.includes(tail) &&\n !rejected.includes(tail)\n ) {\n return 'already-approved';\n }\n\n config['hasCompletedOnboarding'] = true;\n config['customApiKeyResponses'] = {\n ...responses,\n approved: approved.includes(tail) ? approved : [...approved, tail],\n rejected: rejected.filter((t) => t !== tail),\n };\n\n mkdirSync(dirname(target), { recursive: true });\n const tmp = `${target}.approve-${process.pid}`;\n try {\n // `wx` for the same reason the seed uses COPYFILE_EXCL: fail on a planted link.\n writeFileSync(tmp, JSON.stringify(config, null, 2), { flag: 'wx', mode: 0o600 });\n renameSync(tmp, target);\n } catch (err) {\n try {\n if (existsSync(tmp)) unlinkSync(tmp);\n } catch {\n // Best effort; a stray temp file is inert.\n }\n throw err;\n }\n log(`[claude-config] marked ${target} ready for this API-key host`);\n return 'written';\n } catch (err) {\n log(`[claude-config] could not approve the API key in ${target}: ${(err as Error).message}`);\n return 'failed';\n }\n}\n\n/**\n * True when the config dir holds a usable `.claude.json` — i.e. it is safe to\n * spawn against the new location.\n *\n * Deliberately more than a stat, and more than a parse. The failures this gates\n * are an agent starting against a config that is truncated (indistinguishable\n * from a healthy one by size or mtime) AND one that parses but carries no\n * account — the shape `ensureWorkspaceTrusted` creates, which would otherwise\n * let this fix's own writer manufacture a config that looks ready and sends\n * every agent on the host to the login picker. See {@link inspectClaudeConfig}.\n */\nexport function claudeConfigDirIsReady(\n homeDir?: string,\n env: NodeJS.ProcessEnv = process.env,\n): boolean {\n return inspectClaudeConfig(claudeJsonPath(homeDir, env)) === 'usable';\n}\n\nexport type ArchiveOutcome =\n /** Legacy file moved aside; a rolled-back manager will now fail LOUDLY. */\n | 'archived'\n /** Nothing at the legacy path — already migrated, or never paired. */\n | 'nothing-to-archive'\n /** Refused: the new location is not proven good, so the legacy file is the only copy. */\n | 'refused-target-not-ready'\n /**\n * Refused: the legacy file is NEWER than the target, so it is somebody's\n * fresh `claude /login` and archiving it would throw away the recovery.\n */\n | 'refused-legacy-newer'\n | 'failed';\n\n/**\n * ENG-10869 — make a CLI rollback fail loudly instead of quietly serving a\n * stale account.\n *\n * ## The hazard this closes\n *\n * After the migration, every write goes to `<configDir>/.claude.json` and the\n * legacy `<home>/.claude.json` is frozen at whatever it held on migration day.\n * Nothing syncs it. So a manager rolled back to a build without\n * `CLAUDE_CONFIG_DIR` would bind that stale file into every container and the\n * fleet would come up on a *plausible* config — the right shape, the right\n * keys, and an `oauthAccount` that may be several re-pairings out of date.\n *\n * That is a false-green: agents start, health reads fine, and they are\n * authenticated as the wrong account or not at all. Moving the file aside\n * converts it into `CLAUDE LOGIN REQUIRED`, which the manager already detects,\n * logs by name and surfaces — an outage that says what it is, and is one `mv`\n * from being undone.\n *\n * ## Why it refuses unless the target is ready\n *\n * Until the seed has landed and parsed, the legacy file is the ONLY copy of the\n * host's identity. Archiving first and seeding second would be a window in\n * which a crash loses the account outright.\n *\n * ## Why it renames rather than deletes\n *\n * The archived name is dated and predictable, so recovery is a rename and the\n * log line says so. Deleting would make the rollback path unrecoverable, which\n * is a worse trade than the one being fixed.\n */\nexport function archiveLegacyClaudeJson(opts: {\n homeDir?: string;\n env?: NodeJS.ProcessEnv;\n log?: (msg: string) => void;\n now?: () => Date;\n} = {}): ArchiveOutcome {\n const env = opts.env ?? process.env;\n const log = opts.log ?? (() => {});\n const legacy = legacyClaudeJsonPath(opts.homeDir, env);\n const target = claudeJsonPath(opts.homeDir, env);\n\n if (target === legacy) return 'nothing-to-archive';\n\n try {\n if (!existsSync(legacy)) return 'nothing-to-archive';\n if (!claudeConfigDirIsReady(opts.homeDir, env)) {\n // The legacy file is the only usable copy. Leave it exactly where a\n // rolled-back manager, and the current one, can still find it.\n log(\n `[claude-config] NOT archiving ${legacy} — ${target} is absent or does not parse, ` +\n `so the legacy file is still the only usable config`,\n );\n return 'refused-target-not-ready';\n }\n // ENG-10869: a legacy file NEWER than the target is an operator's fresh\n // `claude /login`, not a stale leftover. The seed above copies it forward;\n // if that has not happened yet, archiving here would rename the only copy\n // of a just-created account aside — repeatedly, once per re-login, while\n // Claude Code reported each login as successful.\n if (claudeConfigMtimeMs(legacy) > claudeConfigMtimeMs(target)) {\n log(\n `[claude-config] NOT archiving ${legacy} — it is NEWER than ${target}, so it is a fresh login ` +\n `that has not been copied forward yet`,\n );\n return 'refused-legacy-newer';\n }\n\n const stamp = (opts.now?.() ?? new Date()).toISOString().replace(/[:.]/g, '-');\n const archived = `${legacy}.pre-eng10869-${stamp}`;\n renameSync(legacy, archived);\n log(\n `[claude-config] archived ${legacy} -> ${archived}. ` +\n `Config now lives at ${target}. To undo a CLI rollback: mv '${archived}' '${legacy}'`,\n );\n return 'archived';\n } catch (err) {\n log(`[claude-config] archiving ${legacy} failed: ${(err as Error).message}`);\n return 'failed';\n }\n}\n\n"],"mappings":"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","names":["readFileSync","readFileSync"]}
@@ -7,22 +7,26 @@ import {
7
7
  } from "./chunk-DJNCIWVG.js";
8
8
  import {
9
9
  classifyUnanswerablePane,
10
+ customApiKeyDialogKeys,
11
+ customApiKeyDialogRefusal,
10
12
  findUsageLimitResetHint,
11
13
  focusedConfirmDialogBlock,
12
14
  isConsumerTermsDialogVisible,
15
+ isCustomApiKeyDialogVisible,
13
16
  isLoginPickerVisible,
14
17
  isResumeModeDialogVisible,
15
18
  isUnanswerableUsageLimitDialog,
16
19
  isUnclaimedConfirmDialog,
20
+ loginPickerAdvice,
17
21
  paneRemedy,
18
22
  parkedModalKind,
19
23
  sendDialogKeys,
20
24
  simpleTextHash,
21
25
  sweepDialogs
22
- } from "./chunk-B6UYVEMD.js";
26
+ } from "./chunk-OO36JBFI.js";
23
27
  import {
24
28
  signinResetInForce
25
- } from "./chunk-OAWK4OBN.js";
29
+ } from "./chunk-6HBXEJHO.js";
26
30
  import {
27
31
  reapOrphanChannelMcps
28
32
  } from "./chunk-XWVM4KPK.js";
@@ -50,13 +54,15 @@ import {
50
54
  } from "./chunk-47P33SZF.js";
51
55
  import {
52
56
  CLAUDE_CONFIG_DIR_ENV,
57
+ approveApiKeyInClaudeConfig,
53
58
  archiveLegacyClaudeJson,
54
59
  claudeConfigDirFor,
55
60
  claudeConfigDirIsReady,
56
61
  claudeJsonPathFor,
57
62
  legacyClaudeJsonPathFor,
58
- seedClaudeConfigFromLegacy
59
- } from "./chunk-EL5MWGGH.js";
63
+ seedClaudeConfigFromLegacy,
64
+ spawnApiKeyCredential
65
+ } from "./chunk-AIUIMEUS.js";
60
66
  import {
61
67
  __require
62
68
  } from "./chunk-DGUM43GV.js";
@@ -4945,6 +4951,12 @@ function spawnSession(config, session) {
4945
4951
  if (projectSettingsNotice) log2(projectSettingsNotice);
4946
4952
  const policyEnvActive = Object.keys(policyEnv).length > 0;
4947
4953
  const policyModelActive = Object.keys(policyModelEnv).length > 0;
4954
+ const apiKeyCredential = spawnApiKeyCredential({
4955
+ claudeAuthMode,
4956
+ openRouterMode,
4957
+ policyEnvActive,
4958
+ anthropicApiKey: config.anthropicApiKey
4959
+ });
4948
4960
  const tmuxSession = `agt-${codeName}`;
4949
4961
  log2(
4950
4962
  `[persistent-session] Starting tmux session '${tmuxSession}' for '${codeName}' (auth=${openRouterMode ? "openrouter" : claudeAuthMode})`
@@ -5064,6 +5076,9 @@ function spawnSession(config, session) {
5064
5076
  const tmuxSessionEnvArgs = [];
5065
5077
  tmuxSessionEnvArgs.push("-e", `AGT_AGENT_ID=${config.agentId}`);
5066
5078
  const tmuxSessionHomeDir = sessionHomeDirFor();
5079
+ if (apiKeyCredential) {
5080
+ approveApiKeyInClaudeConfig({ apiKey: apiKeyCredential, homeDir: tmuxSessionHomeDir, log: log2 });
5081
+ }
5067
5082
  if (claudeConfigDirIsReady(tmuxSessionHomeDir)) {
5068
5083
  tmuxSessionEnvArgs.push(
5069
5084
  "-e",
@@ -5356,7 +5371,15 @@ function spawnSession(config, session) {
5356
5371
  }
5357
5372
  setupPaneLog2(tmuxSession, codeName, log2);
5358
5373
  session.currentSessionId = sessionId;
5359
- acceptDialogs(tmuxSession, codeName, log2, config.primaryModel ?? null, sessionId).catch(() => {
5374
+ acceptDialogs(
5375
+ tmuxSession,
5376
+ codeName,
5377
+ log2,
5378
+ config.primaryModel ?? null,
5379
+ sessionId,
5380
+ apiKeyCredential,
5381
+ !openRouterMode && !policyEnvActive && claudeAuthMode === "api_key"
5382
+ ).catch(() => {
5360
5383
  });
5361
5384
  };
5362
5385
  const onTmuxSessionError = (err) => {
@@ -5428,7 +5451,7 @@ function hasMcpChildren(tmuxSession) {
5428
5451
  return false;
5429
5452
  }
5430
5453
  }
5431
- async function acceptDialogs(tmuxSession, codeName, log2, primaryModel = null, sessionId = null) {
5454
+ async function acceptDialogs(tmuxSession, codeName, log2, primaryModel = null, sessionId = null, injectedApiKey = null, apiKeyHost = false) {
5432
5455
  let loginPickerReported = false;
5433
5456
  let dialogIterations = 0;
5434
5457
  const MAX_DIALOG_ITERATIONS = 15;
@@ -5445,7 +5468,7 @@ async function acceptDialogs(tmuxSession, codeName, log2, primaryModel = null, s
5445
5468
  if (health) {
5446
5469
  log2(`[persistent-session] CLAUDE LOGIN REQUIRED for '${codeName}' \u2014 ${health}`);
5447
5470
  } else {
5448
- log2(`[persistent-session] CLAUDE LOGIN REQUIRED for '${codeName}' \u2014 agent cannot start until ~/.claude.json is provisioned. Pair via the Hosts page or run 'claude /login' on the host.`);
5471
+ log2(`[persistent-session] CLAUDE LOGIN REQUIRED for '${codeName}' \u2014 ${loginPickerAdvice(apiKeyHost)}.`);
5449
5472
  }
5450
5473
  loginPickerReported = true;
5451
5474
  }
@@ -5453,6 +5476,22 @@ async function acceptDialogs(tmuxSession, codeName, log2, primaryModel = null, s
5453
5476
  continue;
5454
5477
  }
5455
5478
  dialogIterations++;
5479
+ if (isCustomApiKeyDialogVisible(screen)) {
5480
+ const keys = customApiKeyDialogKeys(screen, injectedApiKey);
5481
+ if (!keys) {
5482
+ log2(`[persistent-session] BLOCKED DIALOG (custom-api-key-prompt) for '${codeName}': ${customApiKeyDialogRefusal(screen, injectedApiKey)}`);
5483
+ noteSessionBlockedOnPrompt(codeName, "unclaimed-modal", "spawn-dialogs");
5484
+ return;
5485
+ }
5486
+ await sendDialogKeys(tmuxSession, {
5487
+ kind: "custom-api-key",
5488
+ keys,
5489
+ interKeyDelayMs: 200,
5490
+ logMessage: "Auto-accepted the injected API key"
5491
+ });
5492
+ log2(`[persistent-session] Auto-accepted the injected API key for '${codeName}'`);
5493
+ continue;
5494
+ }
5456
5495
  const dialogAction = sweepDialogs(screen);
5457
5496
  if (dialogAction) {
5458
5497
  await sendDialogKeys(tmuxSession, dialogAction);
@@ -6130,4 +6169,4 @@ export {
6130
6169
  stopAllSessionsAndWait,
6131
6170
  getProjectDir
6132
6171
  };
6133
- //# sourceMappingURL=chunk-EK6JVOBH.js.map
6172
+ //# sourceMappingURL=chunk-H37WRW2E.js.map