@layorixinc/axterm 0.5.0 → 0.6.1

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 (2) hide show
  1. package/dist/cli.js +2085 -721
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -46,6 +46,179 @@ var __export = (target, all) => {
46
46
  };
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
 
49
+ // packages/core/src/menu-keys.ts
50
+ function buildMenuSelectKeys(index, normalizePresses = MENU_NORMALIZE_PRESSES) {
51
+ if (!Number.isInteger(index) || index < 0) {
52
+ throw new RangeError(`menu index must be a non-negative integer, got ${index}`);
53
+ }
54
+ return MENU_UP.repeat(normalizePresses) + MENU_DOWN.repeat(index) + MENU_SELECT;
55
+ }
56
+ var MENU_UP = "\x1B[A", MENU_DOWN = "\x1B[B", MENU_SELECT = "\r", MENU_NORMALIZE_PRESSES = 24;
57
+
58
+ // packages/core/src/agent-registry.ts
59
+ function agentProfile(id) {
60
+ const known = PROFILES[id];
61
+ if (known)
62
+ return known;
63
+ return {
64
+ id,
65
+ displayName: id,
66
+ title: {
67
+ workingLabel: id,
68
+ permissionLabel: `${id} - action required`,
69
+ idleLabel: `${id} ready`
70
+ },
71
+ hostHookSource: id
72
+ };
73
+ }
74
+ function menuChoiceKeys(profile, optionIndex) {
75
+ const keys = profile.approvalKeys;
76
+ if (keys !== undefined) {
77
+ return optionIndex === 0 ? keys.allow : optionIndex === 1 ? keys.deny : undefined;
78
+ }
79
+ return profile.clampingApprovalMenu === true ? buildMenuSelectKeys(optionIndex) : undefined;
80
+ }
81
+ function renderAgentTitle(profile, state, frame) {
82
+ if (state === undefined)
83
+ return;
84
+ const { title } = profile;
85
+ if (state === "working") {
86
+ if (title.synthesizeWorking === false)
87
+ return;
88
+ const spinner = SPINNER_FRAMES[(frame % SPINNER_FRAMES.length + SPINNER_FRAMES.length) % SPINNER_FRAMES.length];
89
+ return `${spinner} ${title.workingLabel}`;
90
+ }
91
+ if (title.synthesizeTerminal === false)
92
+ return;
93
+ return state === "done" ? title.idleLabel : title.permissionLabel;
94
+ }
95
+ function oscTitle(title) {
96
+ const safe = title.replace(/[\x00-\x1f\x7f]/g, "").slice(0, MAX_TITLE_LENGTH);
97
+ return `\x1B]0;${safe}\x07`;
98
+ }
99
+ var SPINNER_FRAMES, MENU_APPROVE = "\r", PROFILES, REGISTERED_AGENTS, MAX_TITLE_LENGTH = 200;
100
+ var init_agent_registry = __esm(() => {
101
+ SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
102
+ PROFILES = {
103
+ omp: {
104
+ id: "omp",
105
+ displayName: "OMP",
106
+ title: {
107
+ workingLabel: "OMP",
108
+ permissionLabel: "OMP - action required",
109
+ idleLabel: "OMP ready",
110
+ synthesizeWorking: false,
111
+ synthesizeTerminal: false
112
+ },
113
+ hostHookSource: "omp",
114
+ clampingApprovalMenu: true,
115
+ approvalBypassArgs: ["--approval-mode", "yolo"],
116
+ quota: "gauge",
117
+ resume: { argv: ["--resume", "{ref}"], ref: "sessionFile" }
118
+ },
119
+ pi: {
120
+ id: "pi",
121
+ displayName: "Pi",
122
+ title: { workingLabel: "Pi", permissionLabel: "Pi - action required", idleLabel: "Pi ready" },
123
+ hostHookSource: "pi",
124
+ resume: { argv: ["--session", "{ref}"], ref: "sessionId" }
125
+ },
126
+ codex: {
127
+ id: "codex",
128
+ displayName: "Codex",
129
+ title: {
130
+ workingLabel: "Codex",
131
+ permissionLabel: "Codex - action required",
132
+ idleLabel: "Codex ready",
133
+ synthesizeWorking: false
134
+ },
135
+ hostHookSource: "codex",
136
+ quota: "gauge",
137
+ approvalBypassArgs: ["--dangerously-bypass-approvals-and-sandbox"],
138
+ structuredApprovals: true,
139
+ resume: { argv: ["resume", "{ref}"], ref: "sessionId", form: "subcommand" }
140
+ },
141
+ opencode: {
142
+ id: "opencode",
143
+ displayName: "OpenCode",
144
+ title: {
145
+ workingLabel: "OpenCode",
146
+ permissionLabel: "OpenCode - action required",
147
+ idleLabel: "OpenCode ready",
148
+ synthesizeTerminal: false
149
+ },
150
+ hostHookSource: "opencode",
151
+ resume: { argv: ["--session", "{ref}"], ref: "sessionId" }
152
+ },
153
+ cursor: {
154
+ id: "cursor",
155
+ displayName: "Cursor Agent",
156
+ title: {
157
+ workingLabel: "Cursor Agent",
158
+ permissionLabel: "Cursor - action required",
159
+ idleLabel: "Cursor ready"
160
+ },
161
+ hostHookSource: "cursor",
162
+ resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
163
+ },
164
+ droid: {
165
+ id: "droid",
166
+ displayName: "Droid",
167
+ title: { workingLabel: "Droid", permissionLabel: "Droid - action required", idleLabel: "Droid ready" },
168
+ hostHookSource: "droid",
169
+ resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
170
+ },
171
+ claude: {
172
+ id: "claude",
173
+ displayName: "Claude Code",
174
+ title: {
175
+ workingLabel: "Claude",
176
+ permissionLabel: "Claude - action required",
177
+ idleLabel: "Claude ready",
178
+ synthesizeWorking: false,
179
+ synthesizeTerminal: false
180
+ },
181
+ hostHookSource: "claude",
182
+ quota: "event",
183
+ approvalBypassArgs: ["--permission-mode", "bypassPermissions"],
184
+ approvalKeys: { allow: MENU_APPROVE, deny: "\x1B" },
185
+ structuredApprovals: true,
186
+ resume: { argv: ["--resume", "{ref}"], ref: "sessionId", selfNamed: "uuid" }
187
+ },
188
+ gemini: {
189
+ id: "gemini",
190
+ displayName: "Gemini CLI",
191
+ title: {
192
+ workingLabel: "Gemini",
193
+ permissionLabel: "Gemini - action required",
194
+ idleLabel: "Gemini ready",
195
+ synthesizeWorking: false,
196
+ synthesizeTerminal: false
197
+ },
198
+ hostHookSource: "gemini",
199
+ approvalBypassArgs: ["--approval-mode", "yolo"],
200
+ hookAck: "{}",
201
+ approvalKeys: { allow: MENU_APPROVE, deny: "\x1B" },
202
+ resume: { argv: ["--resume", "{ref}"], ref: "sessionId", selfNamed: "bare" }
203
+ }
204
+ };
205
+ REGISTERED_AGENTS = Object.keys(PROFILES);
206
+ });
207
+
208
+ // packages/core/src/session-id.ts
209
+ function uuidFromSessionId(sessionId) {
210
+ const hex = sessionId.replace(/-/g, "");
211
+ if (hex.length !== 32)
212
+ throw new Error(`session id is not 32 hex characters: ${sessionId}`);
213
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
214
+ }
215
+ function sessionIdFromUuid(ref) {
216
+ if (!ref.includes("-"))
217
+ return;
218
+ const stripped = ref.replace(/-/g, "");
219
+ return /^[0-9a-f]{4,32}$/i.test(stripped) ? stripped.toLowerCase() : undefined;
220
+ }
221
+
49
222
  // packages/core/src/mcp-config.ts
50
223
  function mcpServerEntry(launch, contextPath) {
51
224
  return {
@@ -76,18 +249,6 @@ function codexMcpArgs(launch, contextPath) {
76
249
  var MCP_SERVER_NAME = "axterm", MCP_CONTEXT_ENV = "AXTERM_MCP_CONTEXT";
77
250
 
78
251
  // packages/core/src/agent-launch.ts
79
- function uuidFromSessionId(sessionId) {
80
- const hex = sessionId.replace(/-/g, "");
81
- if (hex.length !== 32)
82
- throw new Error(`session id is not 32 hex characters: ${sessionId}`);
83
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
84
- }
85
- function sessionIdFromUuid(ref) {
86
- if (!ref.includes("-"))
87
- return;
88
- const stripped = ref.replace(/-/g, "");
89
- return /^[0-9a-f]{4,32}$/i.test(stripped) ? stripped.toLowerCase() : undefined;
90
- }
91
252
  function withAppServer(argv, url) {
92
253
  const out = [];
93
254
  for (let i = 0;i < argv.length; i++) {
@@ -102,7 +263,13 @@ function withAppServer(argv, url) {
102
263
  return [out[0], "--remote", url, ...out.slice(1)];
103
264
  }
104
265
  function resumesAConversation(args) {
105
- return args.some((arg) => CLAUDE_RESUME_FLAGS.has(arg) || arg.startsWith("--resume=") || arg.startsWith("--continue="));
266
+ return args.some((arg) => RESUME_FLAGS.has(arg) || arg.startsWith("--resume=") || arg.startsWith("--continue="));
267
+ }
268
+ function namingFlag(agent, sessionId, extra) {
269
+ const named = agentProfile(agent).resume?.selfNamed;
270
+ if (named === undefined || resumesAConversation(extra))
271
+ return [];
272
+ return ["--session-id", named === "uuid" ? uuidFromSessionId(sessionId) : sessionId];
106
273
  }
107
274
  function buildAgentCommand(input) {
108
275
  const extra = [...input.extraArgs ?? []];
@@ -112,17 +279,14 @@ function buildAgentCommand(input) {
112
279
  "--settings",
113
280
  input.claudeSettingsPath,
114
281
  ...input.mcpConfigPath === undefined ? [] : ["--mcp-config", input.mcpConfigPath],
115
- "--session-id",
116
- uuidFromSessionId(input.sessionId),
117
- ...resumesAConversation(extra) ? ["--fork-session"] : [],
282
+ ...namingFlag("claude", input.sessionId, extra),
118
283
  ...extra
119
284
  ];
120
285
  }
121
286
  if (input.agent === "gemini") {
122
287
  return [
123
288
  "gemini",
124
- "--session-id",
125
- input.sessionId,
289
+ ...namingFlag("gemini", input.sessionId, extra),
126
290
  ...extra
127
291
  ];
128
292
  }
@@ -159,61 +323,16 @@ function resolveAgentArgs(input) {
159
323
  });
160
324
  return groups.filter((group, index) => group.flag === undefined || lastIndex.get(group.flag) === index).flatMap((group) => group.tokens);
161
325
  }
162
- function operatorArgsFrom(storedArgv) {
163
- if (storedArgv === undefined || storedArgv.length === 0)
164
- return [];
165
- const kept = [];
166
- for (let i = 1;i < storedArgv.length; i++) {
167
- const token = storedArgv[i];
168
- const width = AXTERM_OWNED_FLAGS[token];
169
- if (width !== undefined) {
170
- i += width - 1;
171
- continue;
172
- }
173
- if (/^--(resume|continue)=/.test(token))
174
- continue;
175
- kept.push(token);
176
- }
177
- return kept;
178
- }
179
- function buildAgentResumeCommand(input) {
180
- const operator = input.extraArgs ?? operatorArgsFrom(input.storedArgv);
181
- if (input.agent === "claude") {
182
- return [
183
- "claude",
184
- "--settings",
185
- input.claudeSettingsPath,
186
- ...input.mcpConfigPath === undefined ? [] : ["--mcp-config", input.mcpConfigPath],
187
- ...operator,
188
- "--resume",
189
- uuidFromSessionId(input.sessionId)
190
- ];
191
- }
192
- if (input.agent === "gemini") {
193
- return ["gemini", ...operator, "--resume", input.sessionId];
194
- }
195
- return buildAgentCommand({ ...input, extraArgs: operator });
196
- }
197
326
  function isAgentId(value) {
198
327
  return AGENT_IDS.includes(value);
199
328
  }
200
329
  function supportedAgents() {
201
330
  return AGENT_IDS;
202
331
  }
203
- var CLAUDE_RESUME_FLAGS, AXTERM_OWNED_FLAGS, AGENT_IDS;
332
+ var RESUME_FLAGS, AGENT_IDS;
204
333
  var init_agent_launch = __esm(() => {
205
- CLAUDE_RESUME_FLAGS = new Set(["--resume", "-r", "--continue", "-c"]);
206
- AXTERM_OWNED_FLAGS = {
207
- "--settings": 2,
208
- "--mcp-config": 2,
209
- "--session-id": 2,
210
- "--extension": 2,
211
- "--fork-session": 1,
212
- "--resume": 2,
213
- "-r": 2,
214
- "--continue": 1,
215
- "-c": 1
216
- };
334
+ init_agent_registry();
335
+ RESUME_FLAGS = new Set(["--resume", "-r", "--continue", "-c"]);
217
336
  AGENT_IDS = ["omp", "claude", "codex", "gemini"];
218
337
  });
219
338
 
@@ -917,6 +1036,7 @@ usage:
917
1036
  axterm rm <id\u2026> [--force] forget a session: its row, scrollback and topic binding
918
1037
  axterm rm --exited [--all] forget every exited session this listing would show
919
1038
  axterm daemon status|stop
1039
+ axterm service install|status|uninstall start at login and restart daemon failures
920
1040
  axterm project add <dir> [alias] | list | rm <alias> register a project for channel use
921
1041
  axterm run [--worktree BRANCH] [--base REF] ... worktree-backed session
922
1042
  axterm fanout --count 3 [--prefix try] "prompt"
@@ -944,6 +1064,8 @@ locale: set AXTERM_LANG=en|ko, or \`axterm config set locale ko\`
944
1064
  "cli.session.sent": "sent",
945
1065
  "cli.session.sentKey": "sent {key}",
946
1066
  "cli.session.selected": "selected option {index}",
1067
+ "cli.session.selectedDialog": "answered the agent's first-run dialog",
1068
+ "cli.choose.daemonTooOld": "this daemon is too old to answer a dialog safely \u2014 its `choose` sent keystrokes that land on the row below on claude and gemini, which is a wider grant. Restart the daemon, or send the keys yourself with `axterm key`",
947
1069
  "cli.resume.done": "resumed session {id} ({command})",
948
1070
  "cli.update.current": "axterm {version} is the latest version",
949
1071
  "cli.update.available": "axterm {latest} is available (you have {current})",
@@ -987,6 +1109,7 @@ locale: set AXTERM_LANG=en|ko, or \`axterm config set locale ko\`
987
1109
  "cli.usage.worktree": "usage: axterm worktree list|rm <branch> [--force]",
988
1110
  "cli.usage.worktreeRm": "usage: axterm worktree rm <branch> [--force]",
989
1111
  "cli.usage.daemon": "usage: axterm daemon status|stop",
1112
+ "cli.usage.service": "usage: axterm service install|status|uninstall",
990
1113
  "cli.usage.telegram": "usage: axterm telegram link|doctor|status|discover",
991
1114
  "cli.usage.fanout": "usage: axterm fanout --count N <prompt>",
992
1115
  "cli.config.set": "set {path}",
@@ -1003,6 +1126,13 @@ locale: set AXTERM_LANG=en|ko, or \`axterm config set locale ko\`
1003
1126
  "cli.worktree.refused": "refused: {reason}",
1004
1127
  "cli.daemon.stopping": "daemon stopping",
1005
1128
  "cli.daemon.notReady": "daemon did not become ready within 15s (see {log})",
1129
+ "cli.service.installed": "installed {kind} service; the daemon is running under the supervisor",
1130
+ "cli.service.uninstalled": "removed service registration; the current daemon keeps running",
1131
+ "cli.service.status": "service: {kind}; installed={installed}; active={active}",
1132
+ "cli.service.yes": "yes",
1133
+ "cli.service.no": "no",
1134
+ "cli.service.commandFailed": "service command failed ({command}): {error}",
1135
+ "cli.service.notReady": "the service started but the daemon was not ready within 15s",
1006
1136
  "cli.telegram.missingSettings": "telegram link: no token/group/user found. Provide --token/--group/--user flags, TELEGRAM_* environment variables, or a .env file in the current directory.",
1007
1137
  "cli.telegram.discoverHint": "Create a group with Topics enabled, add your bot to it, then run this again. Being added is enough \u2014 you do not have to send anything.",
1008
1138
  "cli.telegram.discoverNone": "the bot cannot see any group yet.",
@@ -1013,7 +1143,7 @@ locale: set AXTERM_LANG=en|ko, or \`axterm config set locale ko\`
1013
1143
  "err.telegram.noToken": "no bot token is configured \u2014 run `axterm setup` or pass --token",
1014
1144
  "tg.help.general.header": "axterm commands (General topic):",
1015
1145
  "tg.help.general.projects": "/projects \u2014 registered projects",
1016
- "tg.help.general.new": "/new <project> [name] \u2014 new omp session",
1146
+ "tg.help.general.new": "/new <project> [name] [-- agent flags] \u2014 new session (e.g. --resume <conversation>)",
1017
1147
  "tg.help.general.wt": "/wt <project> <branch> \u2014 worktree session",
1018
1148
  "tg.help.general.fanout": "/fanout <project> <count|branches> <prompt> \u2014 run in parallel",
1019
1149
  "tg.help.general.worktrees": "/worktrees <project> \xB7 /rm <project> <branch> [force]",
@@ -1033,8 +1163,13 @@ locale: set AXTERM_LANG=en|ko, or \`axterm config set locale ko\`
1033
1163
  "tg.status.inferred": "\u2139\uFE0F Guessed from what is running under the shell \u2014 this agent reports nothing, so there are no approval buttons or replies here (axterm attach shows everything).",
1034
1164
  "tg.session.emptyScreen": "(empty screen)",
1035
1165
  "tg.prompt.confirm": "the session is waiting on a confirmation:",
1166
+ "tg.poll.conflict": "\u26A0\uFE0F {machine} cannot receive messages: another axterm is polling this bot token, and Telegram allows only one. Stop the other instance, or give this machine its own bot.",
1167
+ "tg.poll.failed": "\u26A0\uFE0F {machine} cannot receive messages right now: {reason}",
1036
1168
  "tg.prompt.secretAsk": "the session is asking for a secret. Reply with it and the message will be deleted:",
1037
1169
  "tg.prompt.secretDisabled": "the session is waiting for a password. Answering from chat is off (channels.telegram.secretPrompts); attach and type it, or use an SSH key",
1170
+ "tg.prompt.secretSubmitted": "the secret reached the session",
1171
+ "tg.prompt.secretStale": "that prompt is no longer waiting, so nothing was typed into the session",
1172
+ "tg.prompt.secretUndeleted": "\u26A0\uFE0F Telegram refused to delete that message \u2014 the secret is still in the chat; delete it yourself",
1038
1173
  "tg.session.screenFile": "terminal screen",
1039
1174
  "tg.session.none": "no sessions",
1040
1175
  "tg.prompt.failed": "\u26A0\uFE0F Delivery failed: {error}",
@@ -1079,6 +1214,7 @@ locale: set AXTERM_LANG=en|ko, or \`axterm config set locale ko\`
1079
1214
  "tg.answer.pickAtLeastOne": "pick at least one",
1080
1215
  "tg.answer.confirmed": "confirmed",
1081
1216
  "tg.answer.unknownOption": "no such option",
1217
+ "tg.answer.noSafeKey": "axterm cannot press this row safely \u2014 use /key or /type",
1082
1218
  "tg.answer.picked": "{label} selected",
1083
1219
  "tg.answer.toggledOn": "{label} selected",
1084
1220
  "tg.answer.toggledOff": "{label} cleared",
@@ -1127,6 +1263,7 @@ usage: /fanout <project> <count|branches> <prompt>`,
1127
1263
  "doctor.codex.trustStale": "codex hooks: {stale}/{total} trust hashes stale \u2014 codex reports them Modified and silently skips them; re-grant via /hooks in the codex TUI",
1128
1264
  "doctor.codex.trustPending": "codex hooks: {pending}/{total} not yet granted \u2014 appended at the next codex launch",
1129
1265
  "doctor.telegram.ok": "telegram: connected",
1266
+ "doctor.telegram.pollFailing": "telegram cannot receive: {reason}",
1130
1267
  "doctor.telegram.off": "telegram: not running",
1131
1268
  "doctor.telegram.problems": "telegram: {problems}",
1132
1269
  "quota.gauge": "{agent}: {percent}% of {window} used, resets in {resets}",
@@ -1152,10 +1289,12 @@ usage: /fanout <project> <count|branches> <prompt>`,
1152
1289
  "err.session.dialogOpen": "session {id} is waiting on a dialog ({state}); answer it with the buttons or /esc first",
1153
1290
  "err.agent.notInstalled": "the agent command '{command}' was not found on PATH \u2014 install it, or pass --cmd to run something else",
1154
1291
  "err.agent.unknown": "unknown agent '{agent}' \u2014 supported: {supported}",
1292
+ "err.choose.noSafeKey": "axterm cannot select row {index} of a {agent} dialog: its menu wraps, so an absolute row cannot be addressed from an unknown cursor position \u2014 use `axterm key` for the keys you want",
1155
1293
  "err.staging.dir": "could not create the session's staging directory {dir} ({error}) \u2014 fix the permissions, or set defaults.staging to runtime",
1156
1294
  "err.staging.notIgnored": "git can see inside {dir}, so a staged file would show up in `git add -A` \u2014 let axterm write its own {file}, or set defaults.staging to runtime",
1295
+ "err.claude.conversationBusy": "session {session} is already in conversation {id} \u2014 two sessions writing one transcript would corrupt it; kill that one first",
1157
1296
  "err.claude.noConversation": "claude has no conversation {id} in its store on this machine \u2014 a conversation lives on the machine it ran on, so this one cannot be resumed here",
1158
- "codex.approval.denied": "Denied from the axterm channel",
1297
+ "agent.approval.denied": "Denied from the axterm channel",
1159
1298
  "err.codex.noAuth": "codex is not logged in ({authFile} not found) \u2014 run `codex login` once, then start the session again",
1160
1299
  "err.codex.authLink": "could not link codex credentials into axterm's CODEX_HOME ({reason}) \u2014 refusing to start a logged-out agent",
1161
1300
  "health.config.invalid": "the configuration file could not be understood",
@@ -1218,6 +1357,7 @@ usage: /fanout <project> <count|branches> <prompt>`,
1218
1357
  axterm rm <id\u2026> [--force] \uC138\uC158\uC744 \uC78A\uB294\uB2E4: \uBAA9\uB85D\xB7\uC2A4\uD06C\uB864\uBC31\xB7\uD1A0\uD53D \uC5F0\uACB0\uAE4C\uC9C0
1219
1358
  axterm rm --exited [--all] \uC774 \uBAA9\uB85D\uC5D0 \uBCF4\uC774\uB294 \uC885\uB8CC\uB41C \uC138\uC158\uC744 \uBAA8\uB450 \uC78A\uB294\uB2E4
1220
1359
  axterm daemon status|stop
1360
+ axterm service install|status|uninstall \uB85C\uADF8\uC778 \uC2DC \uC2DC\uC791\uD558\uACE0 \uB370\uBAAC \uC2E4\uD328 \uC2DC \uC7AC\uC2DC\uC791
1221
1361
  axterm project add <\uD3F4\uB354> [\uBCC4\uCE6D] | list | rm <\uBCC4\uCE6D> \uCC44\uB110\uC5D0\uC11C \uC4F8 \uD504\uB85C\uC81D\uD2B8 \uB4F1\uB85D
1222
1362
  axterm run [--worktree \uBE0C\uB79C\uCE58] [--base REF] ... worktree \uAE30\uBC18 \uC138\uC158
1223
1363
  axterm fanout --count 3 [--prefix try] "\uD504\uB86C\uD504\uD2B8"
@@ -1245,6 +1385,8 @@ usage: /fanout <project> <count|branches> <prompt>`,
1245
1385
  "cli.session.sent": "\uC804\uB2EC\uB428",
1246
1386
  "cli.session.sentKey": "{key} \uC804\uB2EC\uB428",
1247
1387
  "cli.session.selected": "{index}\uBC88 \uD56D\uBAA9 \uC120\uD0DD\uB428",
1388
+ "cli.session.selectedDialog": "\uC5D0\uC774\uC804\uD2B8\uC758 \uCCAB \uC2E4\uD589 \uB300\uD654\uC0C1\uC790\uC5D0 \uC751\uB2F5\uD588\uC2B5\uB2C8\uB2E4",
1389
+ "cli.choose.daemonTooOld": "\uC774 \uB370\uBAAC\uC740 \uB300\uD654\uC0C1\uC790\uB97C \uC548\uC804\uD558\uAC8C \uC751\uB2F5\uD560 \uC218 \uC5C6\uB294 \uAD6C\uBC84\uC804\uC785\uB2C8\uB2E4. \uC608\uC804 `choose` \uB294 claude \uC640 gemini \uC5D0\uC11C \uD55C \uD589 \uC544\uB798\uB97C \uC120\uD0DD\uD588\uACE0 \uADF8\uAC83\uC740 \uB354 \uB113\uC740 \uC2B9\uC778\uC785\uB2C8\uB2E4. \uB370\uBAAC\uC744 \uC7AC\uC2DC\uC791\uD558\uAC70\uB098 `axterm key` \uB85C \uC9C1\uC811 \uD0A4\uB97C \uBCF4\uB0B4\uC138\uC694",
1248
1390
  "cli.resume.done": "\uC138\uC158 {id} \uC7AC\uAC1C\uB428 ({command})",
1249
1391
  "cli.update.current": "axterm {version} \uC774 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4",
1250
1392
  "cli.update.available": "axterm {latest} \uC744(\uB97C) \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4 (\uD604\uC7AC {current})",
@@ -1288,6 +1430,7 @@ usage: /fanout <project> <count|branches> <prompt>`,
1288
1430
  "cli.usage.worktree": "\uC0AC\uC6A9\uBC95: axterm worktree list|rm <\uBE0C\uB79C\uCE58> [--force]",
1289
1431
  "cli.usage.worktreeRm": "\uC0AC\uC6A9\uBC95: axterm worktree rm <\uBE0C\uB79C\uCE58> [--force]",
1290
1432
  "cli.usage.daemon": "\uC0AC\uC6A9\uBC95: axterm daemon status|stop",
1433
+ "cli.usage.service": "\uC0AC\uC6A9\uBC95: axterm service install|status|uninstall",
1291
1434
  "cli.usage.telegram": "\uC0AC\uC6A9\uBC95: axterm telegram link|doctor|status|discover",
1292
1435
  "cli.usage.fanout": "\uC0AC\uC6A9\uBC95: axterm fanout --count N <\uD504\uB86C\uD504\uD2B8>",
1293
1436
  "cli.config.set": "{path} \uC124\uC815\uB428",
@@ -1304,10 +1447,17 @@ usage: /fanout <project> <count|branches> <prompt>`,
1304
1447
  "cli.worktree.refused": "\uAC70\uBD80\uB428: {reason}",
1305
1448
  "cli.daemon.stopping": "\uB370\uBAAC \uC885\uB8CC \uC911",
1306
1449
  "cli.daemon.notReady": "15\uCD08 \uC548\uC5D0 \uB370\uBAAC\uC774 \uC900\uBE44\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4 ({log} \uD655\uC778)",
1450
+ "cli.service.installed": "{kind} \uC11C\uBE44\uC2A4 \uC124\uCE58\uB428. \uB370\uBAAC\uC740 supervisor \uC544\uB798\uC5D0\uC11C \uC2E4\uD589 \uC911\uC785\uB2C8\uB2E4",
1451
+ "cli.service.uninstalled": "\uC11C\uBE44\uC2A4 \uB4F1\uB85D\uC744 \uC81C\uAC70\uD588\uC2B5\uB2C8\uB2E4. \uD604\uC7AC \uB370\uBAAC\uC740 \uACC4\uC18D \uC2E4\uD589\uB429\uB2C8\uB2E4",
1452
+ "cli.service.status": "\uC11C\uBE44\uC2A4: {kind}; \uC124\uCE58={installed}; \uD65C\uC131={active}",
1453
+ "cli.service.yes": "\uC608",
1454
+ "cli.service.no": "\uC544\uB2C8\uC694",
1455
+ "cli.service.commandFailed": "\uC11C\uBE44\uC2A4 \uBA85\uB839 \uC2E4\uD328 ({command}): {error}",
1456
+ "cli.service.notReady": "\uC11C\uBE44\uC2A4\uAC00 \uC2DC\uC791\uB410\uC9C0\uB9CC 15\uCD08 \uC548\uC5D0 \uB370\uBAAC\uC774 \uC900\uBE44\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4",
1307
1457
  "cli.telegram.missingSettings": "telegram link: \uD1A0\uD070/\uADF8\uB8F9/\uC0AC\uC6A9\uC790 \uC815\uBCF4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --token/--group/--user \uD50C\uB798\uADF8, TELEGRAM_* \uD658\uACBD\uBCC0\uC218, \uB610\uB294 \uD604\uC7AC \uD3F4\uB354\uC758 .env \uD30C\uC77C \uC911 \uD558\uB098\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.",
1308
1458
  "tg.help.general.header": "axterm \uBA85\uB839 (General \uD1A0\uD53D):",
1309
1459
  "tg.help.general.projects": "/projects \u2014 \uB4F1\uB85D\uB41C \uD504\uB85C\uC81D\uD2B8",
1310
- "tg.help.general.new": "/new <\uD504\uB85C\uC81D\uD2B8> [\uC774\uB984] \u2014 \uC0C8 omp \uC138\uC158",
1460
+ "tg.help.general.new": "/new <\uD504\uB85C\uC81D\uD2B8> [\uC774\uB984] [\uC5D0\uC774\uC804\uD2B8 \uD50C\uB798\uADF8] \u2014 \uC0C8 \uC138\uC158 (\uC608: --resume <\uB300\uD654>)",
1311
1461
  "tg.help.general.wt": "/wt <\uD504\uB85C\uC81D\uD2B8> <\uBE0C\uB79C\uCE58> \u2014 worktree \uC138\uC158",
1312
1462
  "tg.help.general.fanout": "/fanout <\uD504\uB85C\uC81D\uD2B8> <\uAC1C\uC218|\uBE0C\uB79C\uCE58\uB4E4> <\uD504\uB86C\uD504\uD2B8> \u2014 \uBCD1\uB82C \uC2E4\uD589",
1313
1463
  "tg.help.general.worktrees": "/worktrees <\uD504\uB85C\uC81D\uD2B8> \xB7 /rm <\uD504\uB85C\uC81D\uD2B8> <\uBE0C\uB79C\uCE58> [force]",
@@ -1334,8 +1484,13 @@ usage: /fanout <project> <count|branches> <prompt>`,
1334
1484
  "tg.status.inferred": "\u2139\uFE0F \uC178\uC5D0\uC11C \uC2E4\uD589 \uC911\uC778 \uD504\uB85C\uC138\uC2A4\uB85C \uCD94\uC815\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4 \u2014 \uC774 \uC5D0\uC774\uC804\uD2B8\uB294 \uC0C1\uD0DC\uB97C \uBCF4\uACE0\uD558\uC9C0 \uC54A\uC544 \uC2B9\uC778 \uBC84\uD2BC\uB3C4, \uB2F5\uBCC0\uB3C4 \uC5C6\uC2B5\uB2C8\uB2E4 (\uC804\uCCB4\uB294 axterm attach\uB85C \uD655\uC778\uD558\uC138\uC694).",
1335
1485
  "tg.session.emptyScreen": "(\uBE48 \uD654\uBA74)",
1336
1486
  "tg.prompt.confirm": "\uC138\uC158\uC774 \uD655\uC778\uC744 \uAE30\uB2E4\uB9AC\uACE0 \uC788\uC2B5\uB2C8\uB2E4:",
1487
+ "tg.poll.conflict": "\u26A0\uFE0F {machine} \uC774 \uBA54\uC2DC\uC9C0\uB97C \uBC1B\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: \uB2E4\uB978 axterm \uC774 \uC774 \uBD07 \uD1A0\uD070\uC744 \uD3F4\uB9C1\uD558\uACE0 \uC788\uACE0 Telegram \uC740 \uD558\uB098\uB9CC \uD5C8\uC6A9\uD569\uB2C8\uB2E4. \uB2E4\uB978 \uC778\uC2A4\uD134\uC2A4\uB97C \uB044\uAC70\uB098 \uC774 \uBA38\uC2E0\uC5D0 \uC804\uC6A9 \uBD07\uC744 \uC8FC\uC138\uC694.",
1488
+ "tg.poll.failed": "\u26A0\uFE0F {machine} \uC774 \uC9C0\uAE08 \uBA54\uC2DC\uC9C0\uB97C \uBC1B\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: {reason}",
1337
1489
  "tg.prompt.secretAsk": "\uC138\uC158\uC774 \uBE44\uBC00\uAC12\uC744 \uC694\uAD6C\uD569\uB2C8\uB2E4. \uB2F5\uC7A5\uD558\uBA74 \uADF8 \uBA54\uC2DC\uC9C0\uB294 \uC0AD\uC81C\uB429\uB2C8\uB2E4:",
1338
1490
  "tg.prompt.secretDisabled": "\uC138\uC158\uC774 \uBE44\uBC00\uBC88\uD638\uB97C \uAE30\uB2E4\uB9AC\uACE0 \uC788\uC2B5\uB2C8\uB2E4. \uCC44\uD305 \uC785\uB825\uC740 \uAEBC\uC838 \uC788\uC2B5\uB2C8\uB2E4 (channels.telegram.secretPrompts). attach \uD574\uC11C \uC785\uB825\uD558\uAC70\uB098 SSH \uD0A4\uB97C \uC4F0\uC138\uC694",
1491
+ "tg.prompt.secretSubmitted": "\uBE44\uBC00\uAC12\uC774 \uC138\uC158\uC5D0 \uC804\uB2EC\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
1492
+ "tg.prompt.secretStale": "\uADF8 \uD504\uB86C\uD504\uD2B8\uB294 \uB354 \uC774\uC0C1 \uAE30\uB2E4\uB9AC\uACE0 \uC788\uC9C0 \uC54A\uC544 \uC138\uC158\uC5D0 \uC544\uBB34\uAC83\uB3C4 \uC785\uB825\uD558\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4",
1493
+ "tg.prompt.secretUndeleted": "\u26A0\uFE0F Telegram\uC774 \uBA54\uC2DC\uC9C0 \uC0AD\uC81C\uB97C \uAC70\uBD80\uD588\uC2B5\uB2C8\uB2E4 \u2014 \uBE44\uBC00\uAC12\uC774 \uC544\uC9C1 \uCC44\uD305\uC5D0 \uB0A8\uC544 \uC788\uC2B5\uB2C8\uB2E4. \uC9C1\uC811 \uC0AD\uC81C\uD558\uC138\uC694",
1339
1494
  "tg.session.screenFile": "\uD130\uBBF8\uB110 \uD654\uBA74",
1340
1495
  "tg.session.none": "\uC138\uC158 \uC5C6\uC74C",
1341
1496
  "tg.prompt.failed": "\u26A0\uFE0F \uC804\uB2EC \uC2E4\uD328: {error}",
@@ -1380,6 +1535,7 @@ usage: /fanout <project> <count|branches> <prompt>`,
1380
1535
  "tg.answer.pickAtLeastOne": "\uCD5C\uC18C 1\uAC1C\uB294 \uC120\uD0DD\uD558\uC138\uC694",
1381
1536
  "tg.answer.confirmed": "\uD655\uC815\uB428",
1382
1537
  "tg.answer.unknownOption": "\uC5C6\uB294 \uC120\uD0DD\uC9C0",
1538
+ "tg.answer.noSafeKey": "\uC774 \uD589\uC740 \uC548\uC804\uD558\uAC8C \uB204\uB97C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 \u2014 /key \uB610\uB294 /type\uC744 \uC4F0\uC138\uC694",
1383
1539
  "tg.answer.picked": "{label} \uC120\uD0DD\uB428",
1384
1540
  "tg.answer.toggledOn": "{label} \uC120\uD0DD",
1385
1541
  "tg.answer.toggledOff": "{label} \uD574\uC81C",
@@ -1428,6 +1584,7 @@ usage: /fanout <project> <count|branches> <prompt>`,
1428
1584
  "doctor.codex.trustStale": "codex \uD6C5: {stale}/{total} \uC2E0\uB8B0 \uD574\uC2DC \uBD88\uC77C\uCE58 \u2014 codex\uAC00 Modified\uB85C \uBCF4\uACE0\uD558\uACE0 \uC870\uC6A9\uD788 \uAC74\uB108\uB701\uB2C8\uB2E4; codex TUI\uC758 /hooks\uC5D0\uC11C \uC7AC\uC2B9\uC778\uD558\uC138\uC694",
1429
1585
  "doctor.codex.trustPending": "codex \uD6C5: {pending}/{total} \uBBF8\uC2B9\uC778 \u2014 \uB2E4\uC74C codex \uC2E4\uD589 \uC2DC \uAE30\uB85D\uB429\uB2C8\uB2E4",
1430
1586
  "doctor.telegram.ok": "\uD154\uB808\uADF8\uB7A8: \uC5F0\uACB0\uB428",
1587
+ "doctor.telegram.pollFailing": "\uD154\uB808\uADF8\uB7A8 \uC218\uC2E0 \uBD88\uAC00: {reason}",
1431
1588
  "doctor.telegram.off": "\uD154\uB808\uADF8\uB7A8: \uBBF8\uC2E4\uD589",
1432
1589
  "doctor.telegram.problems": "\uD154\uB808\uADF8\uB7A8: {problems}",
1433
1590
  "quota.gauge": "{agent}: {window} \uD55C\uB3C4\uC758 {percent}% \uC0AC\uC6A9, {resets} \uD6C4 \uCD08\uAE30\uD654",
@@ -1453,10 +1610,12 @@ usage: /fanout <project> <count|branches> <prompt>`,
1453
1610
  "err.session.dialogOpen": "\uC138\uC158 {id} \uC774(\uAC00) \uB2E4\uC774\uC5BC\uB85C\uADF8\uB97C \uAE30\uB2E4\uB9AC\uB294 \uC911\uC785\uB2C8\uB2E4 ({state}). \uBC84\uD2BC\uC73C\uB85C \uB2F5\uD558\uAC70\uB098 /esc \uB97C \uBA3C\uC800 \uBCF4\uB0B4\uC138\uC694",
1454
1611
  "err.agent.notInstalled": "\uC5D0\uC774\uC804\uD2B8 \uBA85\uB839 '{command}' \uC744(\uB97C) PATH \uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC124\uCE58\uD558\uAC70\uB098 --cmd \uB85C \uB2E4\uB978 \uBA85\uB839\uC744 \uC9C0\uC815\uD558\uC138\uC694",
1455
1612
  "err.agent.unknown": "\uC54C \uC218 \uC5C6\uB294 \uC5D0\uC774\uC804\uD2B8 '{agent}' \uC785\uB2C8\uB2E4. \uC9C0\uC6D0: {supported}",
1613
+ "err.choose.noSafeKey": "{agent} \uB300\uD654\uC0C1\uC790\uC758 {index}\uBC88 \uD589\uC740 \uC120\uD0DD\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA54\uB274\uAC00 \uC21C\uD658\uD558\uBBC0\uB85C \uCEE4\uC11C \uC704\uCE58\uB97C \uBAA8\uB974\uB294 \uC0C1\uD0DC\uC5D0\uC11C \uC808\uB300 \uD589\uC744 \uC9C0\uC815\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC6D0\uD558\uB294 \uD0A4\uB294 `axterm key`\uB85C \uBCF4\uB0B4\uC138\uC694",
1456
1614
  "err.staging.dir": "\uC138\uC158\uC758 \uC2A4\uD14C\uC774\uC9D5 \uB514\uB809\uD130\uB9AC {dir} \uC744(\uB97C) \uB9CC\uB4E4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 ({error}). \uAD8C\uD55C\uC744 \uACE0\uCE58\uAC70\uB098 defaults.staging \uC744 runtime \uC73C\uB85C \uC124\uC815\uD558\uC138\uC694",
1457
1615
  "err.staging.notIgnored": "git\uC774 {dir} \uC548\uC744 \uBCFC \uC218 \uC788\uC5B4\uC11C, \uC2A4\uD14C\uC774\uC9D5\uD55C \uD30C\uC77C\uC774 `git add -A` \uC5D0 \uC7A1\uD799\uB2C8\uB2E4. axterm\uC774 \uC790\uCCB4 {file} \uC744(\uB97C) \uC4F0\uAC8C \uD558\uAC70\uB098 defaults.staging \uC744 runtime \uC73C\uB85C \uC124\uC815\uD558\uC138\uC694",
1616
+ "err.claude.conversationBusy": "\uC138\uC158 {session} \uC774(\uAC00) \uC774\uBBF8 \uB300\uD654 {id} \uC5D0 \uB4E4\uC5B4\uAC00 \uC788\uC2B5\uB2C8\uB2E4. \uD55C transcript\uC5D0 \uB450 \uC138\uC158\uC774 \uC4F0\uBA74 \uC190\uC0C1\uB418\uBBC0\uB85C \uBA3C\uC800 \uADF8 \uC138\uC158\uC744 \uC885\uB8CC\uD558\uC138\uC694",
1458
1617
  "err.claude.noConversation": "\uC774 \uBA38\uC2E0\uC758 claude \uC800\uC7A5\uC18C\uC5D0 \uB300\uD654 {id} \uC774(\uAC00) \uC5C6\uC2B5\uB2C8\uB2E4. \uB300\uD654\uB294 \uC2E4\uD589\uB41C \uBA38\uC2E0\uC5D0 \uB0A8\uC73C\uBBC0\uB85C \uC5EC\uAE30\uC11C\uB294 \uC774\uC5B4\uAC08 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
1459
- "codex.approval.denied": "axterm \uCC44\uB110\uC5D0\uC11C \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
1618
+ "agent.approval.denied": "axterm \uCC44\uB110\uC5D0\uC11C \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
1460
1619
  "err.codex.noAuth": "codex\uC5D0 \uB85C\uADF8\uC778\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 ({authFile} \uC5C6\uC74C). `codex login` \uC744 \uD55C \uBC88 \uC2E4\uD589\uD55C \uB4A4 \uC138\uC158\uC744 \uB2E4\uC2DC \uC2DC\uC791\uD558\uC138\uC694",
1461
1620
  "err.codex.authLink": "codex \uC790\uACA9 \uC99D\uBA85\uC744 axterm\uC758 CODEX_HOME\uC5D0 \uC5F0\uACB0\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 ({reason}). \uB85C\uADF8\uC544\uC6C3 \uC0C1\uD0DC\uC758 \uC5D0\uC774\uC804\uD2B8\uB294 \uC2DC\uC791\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4",
1462
1621
  "health.config.invalid": "\uC124\uC815 \uD30C\uC77C\uC744 \uC774\uD574\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
@@ -1645,144 +1804,16 @@ var init_setup_wizard = __esm(() => {
1645
1804
  wizardConfig = new WeakMap;
1646
1805
  });
1647
1806
 
1648
- // packages/core/src/agent-registry.ts
1649
- function agentProfile(id) {
1650
- const known = PROFILES[id];
1651
- if (known)
1652
- return known;
1653
- return {
1654
- id,
1655
- displayName: id,
1656
- title: {
1657
- workingLabel: id,
1658
- permissionLabel: `${id} - action required`,
1659
- idleLabel: `${id} ready`
1660
- },
1661
- hostHookSource: id
1662
- };
1663
- }
1664
- function renderAgentTitle(profile, state, frame) {
1665
- if (state === undefined)
1666
- return;
1667
- const { title } = profile;
1668
- if (state === "working") {
1669
- if (title.synthesizeWorking === false)
1670
- return;
1671
- const spinner = SPINNER_FRAMES[(frame % SPINNER_FRAMES.length + SPINNER_FRAMES.length) % SPINNER_FRAMES.length];
1672
- return `${spinner} ${title.workingLabel}`;
1673
- }
1674
- if (title.synthesizeTerminal === false)
1675
- return;
1676
- return state === "done" ? title.idleLabel : title.permissionLabel;
1677
- }
1678
- function oscTitle(title) {
1679
- const safe = title.replace(/[\x00-\x1f\x7f]/g, "").slice(0, MAX_TITLE_LENGTH);
1680
- return `\x1B]0;${safe}\x07`;
1681
- }
1682
- var SPINNER_FRAMES, MENU_APPROVE = "\r", PROFILES, REGISTERED_AGENTS, MAX_TITLE_LENGTH = 200;
1683
- var init_agent_registry = __esm(() => {
1684
- SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1685
- PROFILES = {
1686
- omp: {
1687
- id: "omp",
1688
- displayName: "OMP",
1689
- title: { workingLabel: "OMP", permissionLabel: "OMP - action required", idleLabel: "OMP ready" },
1690
- hostHookSource: "omp",
1691
- approvalBypassArgs: ["--approval-mode", "yolo"],
1692
- quota: "gauge",
1693
- resume: { argv: ["--resume", "{ref}"], ref: "sessionFile" }
1694
- },
1695
- pi: {
1696
- id: "pi",
1697
- displayName: "Pi",
1698
- title: { workingLabel: "Pi", permissionLabel: "Pi - action required", idleLabel: "Pi ready" },
1699
- hostHookSource: "pi",
1700
- resume: { argv: ["--session", "{ref}"], ref: "sessionId" }
1701
- },
1702
- codex: {
1703
- id: "codex",
1704
- displayName: "Codex",
1705
- title: {
1706
- workingLabel: "Codex",
1707
- permissionLabel: "Codex - action required",
1708
- idleLabel: "Codex ready",
1709
- synthesizeWorking: false
1710
- },
1711
- hostHookSource: "codex",
1712
- quota: "gauge",
1713
- approvalBypassArgs: ["--dangerously-bypass-approvals-and-sandbox"],
1714
- structuredApprovals: true,
1715
- resume: { argv: ["resume", "{ref}"], ref: "sessionId", form: "subcommand" }
1716
- },
1717
- opencode: {
1718
- id: "opencode",
1719
- displayName: "OpenCode",
1720
- title: {
1721
- workingLabel: "OpenCode",
1722
- permissionLabel: "OpenCode - action required",
1723
- idleLabel: "OpenCode ready",
1724
- synthesizeTerminal: false
1725
- },
1726
- hostHookSource: "opencode",
1727
- resume: { argv: ["--session", "{ref}"], ref: "sessionId" }
1728
- },
1729
- cursor: {
1730
- id: "cursor",
1731
- displayName: "Cursor Agent",
1732
- title: {
1733
- workingLabel: "Cursor Agent",
1734
- permissionLabel: "Cursor - action required",
1735
- idleLabel: "Cursor ready"
1736
- },
1737
- hostHookSource: "cursor",
1738
- resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
1739
- },
1740
- droid: {
1741
- id: "droid",
1742
- displayName: "Droid",
1743
- title: { workingLabel: "Droid", permissionLabel: "Droid - action required", idleLabel: "Droid ready" },
1744
- hostHookSource: "droid",
1745
- resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
1746
- },
1747
- claude: {
1748
- id: "claude",
1749
- displayName: "Claude Code",
1750
- title: {
1751
- workingLabel: "Claude",
1752
- permissionLabel: "Claude - action required",
1753
- idleLabel: "Claude ready",
1754
- synthesizeWorking: false,
1755
- synthesizeTerminal: false
1756
- },
1757
- hostHookSource: "claude",
1758
- quota: "event",
1759
- approvalBypassArgs: ["--permission-mode", "bypassPermissions"],
1760
- resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
1761
- },
1762
- gemini: {
1763
- id: "gemini",
1764
- displayName: "Gemini CLI",
1765
- title: {
1766
- workingLabel: "Gemini",
1767
- permissionLabel: "Gemini - action required",
1768
- idleLabel: "Gemini ready",
1769
- synthesizeWorking: false,
1770
- synthesizeTerminal: false
1771
- },
1772
- hostHookSource: "gemini",
1773
- approvalBypassArgs: ["--approval-mode", "yolo"],
1774
- hookAck: "{}",
1775
- approvalKeys: { allow: MENU_APPROVE, deny: "\x1B" },
1776
- resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
1777
- }
1778
- };
1779
- REGISTERED_AGENTS = Object.keys(PROFILES);
1780
- });
1781
-
1782
1807
  // packages/core/src/agent-resume.ts
1783
1808
  function resumeReference(profile, source) {
1784
- const value = profile.resume?.ref === "sessionId" ? source.agentSessionId : source.agentSessionFile;
1785
- return value !== undefined && value.length > 0 ? value : undefined;
1809
+ const reported = profile.resume?.ref === "sessionId" ? source.agentSessionId : source.agentSessionFile;
1810
+ if (reported !== undefined && reported.length > 0)
1811
+ return reported;
1812
+ const named = profile.resume?.selfNamed;
1813
+ const own = source.ownSessionId;
1814
+ if (named === undefined || own === undefined || own.length === 0)
1815
+ return;
1816
+ return named === "uuid" ? uuidFromSessionId(own) : own;
1786
1817
  }
1787
1818
  function agentIdFromCommand(command) {
1788
1819
  const head = command[0] ?? "";
@@ -1809,17 +1840,23 @@ function withoutExistingResume(command, fragment) {
1809
1840
  }
1810
1841
  return command;
1811
1842
  }
1843
+ function withoutNamingFlag(command) {
1844
+ const at = command.indexOf("--session-id");
1845
+ return at < 0 ? command : [...command.slice(0, at), ...command.slice(at + 2)];
1846
+ }
1812
1847
  function buildResumeCommand(command, profile, reference) {
1813
1848
  const resume = profile.resume;
1814
1849
  const binary = command[0];
1815
1850
  if (!resume || binary === undefined || reference.length === 0)
1816
1851
  return;
1817
1852
  const template = resume.argv;
1818
- const base = withoutExistingResume(command, template);
1853
+ const stripped = withoutExistingResume(command, template);
1854
+ const base = resume.selfNamed === undefined ? stripped : withoutNamingFlag(stripped);
1819
1855
  const fragment = template.map((token) => token.replaceAll(REF, reference));
1820
1856
  return resume.form === "subcommand" ? [base[0], ...fragment, ...base.slice(1)] : [...base, ...fragment];
1821
1857
  }
1822
1858
  var REF = "{ref}";
1859
+ var init_agent_resume = () => {};
1823
1860
 
1824
1861
  // packages/core/src/self-spawn.ts
1825
1862
  function daemonLaunch(input) {
@@ -1848,9 +1885,9 @@ function ptyHostLaunch(input) {
1848
1885
  function isPtyHostInvocation(args) {
1849
1886
  return args[0] === PTY_HOST_ARGV_FLAG;
1850
1887
  }
1851
- function hookPostTarget(env, agent) {
1852
- const port = Number(env["AXTERM_HOOK_PORT"]);
1853
- const token = env["AXTERM_HOOK_TOKEN"];
1888
+ function hookPostTarget(env, agent, override) {
1889
+ const port = override?.port ?? Number(env["AXTERM_HOOK_PORT"]);
1890
+ const token = override?.token ?? env["AXTERM_HOOK_TOKEN"];
1854
1891
  const sessionId = env["AXTERM_SESSION_ID"];
1855
1892
  const launchToken = env["AXTERM_LAUNCH_TOKEN"];
1856
1893
  if (!Number.isSafeInteger(port) || port <= 0)
@@ -1867,6 +1904,13 @@ function hookPostTarget(env, agent) {
1867
1904
  }
1868
1905
  };
1869
1906
  }
1907
+ function parseHookEndpoint(text) {
1908
+ const port = Number(/^AXTERM_HOOK_PORT=(\d+)$/m.exec(text)?.[1]);
1909
+ const token = /^AXTERM_HOOK_TOKEN=(\S+)$/m.exec(text)?.[1];
1910
+ if (!Number.isSafeInteger(port) || port <= 0 || !token)
1911
+ return;
1912
+ return { port, token };
1913
+ }
1870
1914
  function isDaemonInvocation(args) {
1871
1915
  return args[0] === DAEMON_ARGV_FLAG;
1872
1916
  }
@@ -1875,6 +1919,222 @@ var init_self_spawn = __esm(() => {
1875
1919
  RUNTIME_NAMES = new Set(["bun", "bun.exe", "bun-debug", "bun-debug.exe", "node", "node.exe"]);
1876
1920
  });
1877
1921
 
1922
+ // packages/core/src/service.ts
1923
+ import path2 from "path";
1924
+ function xml(value) {
1925
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
1926
+ }
1927
+ function systemdArgument(value) {
1928
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("%", "%%")}"`;
1929
+ }
1930
+ function windowsArgument(value) {
1931
+ if (value !== "" && !/[\s"]/u.test(value))
1932
+ return value;
1933
+ let out = '"';
1934
+ let slashes = 0;
1935
+ for (const char of value) {
1936
+ if (char === "\\") {
1937
+ slashes++;
1938
+ continue;
1939
+ }
1940
+ if (char === '"') {
1941
+ out += "\\".repeat(slashes * 2 + 1) + '"';
1942
+ slashes = 0;
1943
+ continue;
1944
+ }
1945
+ out += "\\".repeat(slashes) + char;
1946
+ slashes = 0;
1947
+ }
1948
+ return out + "\\".repeat(slashes * 2) + '"';
1949
+ }
1950
+ function windowsCommandLine(args) {
1951
+ return args.map(windowsArgument).join(" ");
1952
+ }
1953
+ function environmentEntries(input) {
1954
+ return Object.entries(input.environment ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => {
1955
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key))
1956
+ throw new Error(`invalid service environment key: ${key}`);
1957
+ if (/[\0\r\n]/u.test(value))
1958
+ throw new Error(`invalid service environment value for ${key}`);
1959
+ return [key, value];
1960
+ });
1961
+ }
1962
+ function systemdEnvironment(input) {
1963
+ return environmentEntries(input).map(([key, value]) => `Environment="${`${key}=${value}`.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("%", "%%")}"`).join(`
1964
+ `);
1965
+ }
1966
+ function launchdEnvironment(input) {
1967
+ const entries = environmentEntries(input);
1968
+ if (entries.length === 0)
1969
+ return "";
1970
+ return ` <key>EnvironmentVariables</key>
1971
+ <dict>
1972
+ ${entries.map(([key, value]) => ` <key>${xml(key)}</key>
1973
+ <string>${xml(value)}</string>`).join(`
1974
+ `)}
1975
+ </dict>
1976
+ `;
1977
+ }
1978
+ function systemdDefinition(input) {
1979
+ const argv = [input.launch.command, ...input.launch.args].map(systemdArgument).join(" ");
1980
+ const environment = systemdEnvironment(input);
1981
+ return {
1982
+ kind: "systemd",
1983
+ id: SYSTEMD_SERVICE,
1984
+ file: path2.posix.join(input.homeDir, ".config/systemd/user", SYSTEMD_SERVICE),
1985
+ contents: `[Unit]
1986
+ Description=axterm daemon
1987
+ After=network-online.target
1988
+ Wants=network-online.target
1989
+
1990
+ [Service]
1991
+ Type=simple
1992
+ ExecStart=:${argv}
1993
+ ${environment ? `${environment}
1994
+ ` : ""}Restart=on-failure
1995
+ RestartSec=2
1996
+ TimeoutStopSec=20
1997
+ # The PTY host is detached but remains in this cgroup. Stop only the daemon.
1998
+ KillMode=process
1999
+
2000
+ [Install]
2001
+ WantedBy=default.target
2002
+ `
2003
+ };
2004
+ }
2005
+ function launchdDefinition(input) {
2006
+ const argumentsXml = [input.launch.command, ...input.launch.args].map((arg) => ` <string>${xml(arg)}</string>`).join(`
2007
+ `);
2008
+ const logFile = path2.posix.join(input.stateDir, "logs/service.log");
2009
+ return {
2010
+ kind: "launchd",
2011
+ id: LAUNCHD_LABEL,
2012
+ file: path2.posix.join(input.homeDir, "Library/LaunchAgents", `${LAUNCHD_LABEL}.plist`),
2013
+ contents: `<?xml version="1.0" encoding="UTF-8"?>
2014
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2015
+ <plist version="1.0">
2016
+ <dict>
2017
+ <key>Label</key>
2018
+ <string>${LAUNCHD_LABEL}</string>
2019
+ <key>ProgramArguments</key>
2020
+ <array>
2021
+ ${argumentsXml}
2022
+ </array>
2023
+ <key>WorkingDirectory</key>
2024
+ <string>${xml(input.homeDir)}</string>
2025
+ ${launchdEnvironment(input)} <key>RunAtLoad</key>
2026
+ <true/>
2027
+ <key>KeepAlive</key>
2028
+ <dict>
2029
+ <key>SuccessfulExit</key>
2030
+ <false/>
2031
+ </dict>
2032
+ <key>ThrottleInterval</key>
2033
+ <integer>2</integer>
2034
+ <key>AbandonProcessGroup</key>
2035
+ <true/>
2036
+ <key>StandardOutPath</key>
2037
+ <string>${xml(logFile)}</string>
2038
+ <key>StandardErrorPath</key>
2039
+ <string>${xml(logFile)}</string>
2040
+ </dict>
2041
+ </plist>
2042
+ `
2043
+ };
2044
+ }
2045
+ function powershellLiteral(value) {
2046
+ return `'${value.replaceAll("'", "''")}'`;
2047
+ }
2048
+ function taskDefinition(input) {
2049
+ if (!input.userId)
2050
+ throw new Error("Task Scheduler requires userId");
2051
+ const entries = environmentEntries(input);
2052
+ let command = input.launch.command;
2053
+ let argumentsLine = windowsCommandLine(input.launch.args);
2054
+ let companionFiles;
2055
+ if (entries.length > 0) {
2056
+ const wrapperFile = path2.win32.join(input.stateDir, "service", "axterm-service.ps1");
2057
+ const environment = entries.map(([key, value]) => `$env:${key} = ${powershellLiteral(value)}`).join(`\r
2058
+ `);
2059
+ const invocation = [input.launch.command, ...input.launch.args].map(powershellLiteral).join(" ");
2060
+ companionFiles = [
2061
+ {
2062
+ file: wrapperFile,
2063
+ contents: `$ErrorActionPreference = 'Stop'\r
2064
+ ${environment}\r
2065
+ & ${invocation}\r
2066
+ exit $LASTEXITCODE\r
2067
+ `
2068
+ }
2069
+ ];
2070
+ command = "powershell.exe";
2071
+ argumentsLine = windowsCommandLine([
2072
+ "-NoProfile",
2073
+ "-NonInteractive",
2074
+ "-ExecutionPolicy",
2075
+ "Bypass",
2076
+ "-File",
2077
+ wrapperFile
2078
+ ]);
2079
+ }
2080
+ return {
2081
+ kind: "task-scheduler",
2082
+ id: WINDOWS_TASK,
2083
+ file: path2.win32.join(input.stateDir, "service", "axterm-task.xml"),
2084
+ contents: `<?xml version="1.0" encoding="UTF-16"?>
2085
+ <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
2086
+ <RegistrationInfo>
2087
+ <Description>axterm daemon</Description>
2088
+ </RegistrationInfo>
2089
+ <Triggers>
2090
+ <LogonTrigger>
2091
+ <Enabled>true</Enabled>
2092
+ <UserId>${xml(input.userId)}</UserId>
2093
+ </LogonTrigger>
2094
+ </Triggers>
2095
+ <Principals>
2096
+ <Principal id="Author">
2097
+ <UserId>${xml(input.userId)}</UserId>
2098
+ <LogonType>InteractiveToken</LogonType>
2099
+ <RunLevel>LeastPrivilege</RunLevel>
2100
+ </Principal>
2101
+ </Principals>
2102
+ <Settings>
2103
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
2104
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
2105
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
2106
+ <AllowHardTerminate>false</AllowHardTerminate>
2107
+ <StartWhenAvailable>true</StartWhenAvailable>
2108
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
2109
+ <RestartOnFailure>
2110
+ <Interval>PT1M</Interval>
2111
+ <Count>255</Count>
2112
+ </RestartOnFailure>
2113
+ </Settings>
2114
+ <Actions Context="Author">
2115
+ <Exec>
2116
+ <Command>${xml(command)}</Command>
2117
+ <Arguments>${xml(argumentsLine)}</Arguments>
2118
+ <WorkingDirectory>${xml(input.homeDir)}</WorkingDirectory>
2119
+ </Exec>
2120
+ </Actions>
2121
+ </Task>
2122
+ `,
2123
+ ...companionFiles ? { companionFiles } : {}
2124
+ };
2125
+ }
2126
+ function serviceDefinition(input) {
2127
+ if (input.platform === "linux")
2128
+ return systemdDefinition(input);
2129
+ if (input.platform === "darwin")
2130
+ return launchdDefinition(input);
2131
+ if (input.platform === "win32")
2132
+ return taskDefinition(input);
2133
+ throw new Error(`service management is not supported on ${input.platform}`);
2134
+ }
2135
+ var SYSTEMD_SERVICE = "axterm.service", LAUNCHD_LABEL = "com.layorix.axterm.daemon", WINDOWS_TASK = "axterm-daemon";
2136
+ var init_service = () => {};
2137
+
1878
2138
  // packages/core/src/attachment.ts
1879
2139
  function megabytes(bytes) {
1880
2140
  return `${Math.round(bytes / (1024 * 1024))} MB`;
@@ -1992,6 +2252,7 @@ function planUpdateRestart(input) {
1992
2252
  var SEMVER;
1993
2253
  var init_update = __esm(() => {
1994
2254
  init_agent_registry();
2255
+ init_agent_resume();
1995
2256
  SEMVER = /^\d+\.\d+\.\d+/;
1996
2257
  });
1997
2258
 
@@ -2064,6 +2325,22 @@ function parseProjectRef(raw) {
2064
2325
  return { machine: text.slice(0, split), alias: text.slice(split + 1) };
2065
2326
  return { alias: text };
2066
2327
  }
2328
+ function parseCommandAddress(token) {
2329
+ const at = token.indexOf("@");
2330
+ if (at < 0)
2331
+ return { command: token };
2332
+ const target = token.slice(at + 1);
2333
+ if (!/^[A-Za-z0-9_]+$/.test(target))
2334
+ return { command: token };
2335
+ return { command: token.slice(0, at), target };
2336
+ }
2337
+ function commandIsForBot(target, self) {
2338
+ if (target === undefined)
2339
+ return true;
2340
+ if (self === undefined)
2341
+ return false;
2342
+ return target.toLowerCase() === self.toLowerCase();
2343
+ }
2067
2344
  var MAX_NAME_LENGTH = 32;
2068
2345
 
2069
2346
  // packages/core/src/fanout.ts
@@ -2173,6 +2450,9 @@ function buildApprovalQuestion(toolName) {
2173
2450
  function isModalState(state) {
2174
2451
  return state === "blocked" || state === "waiting";
2175
2452
  }
2453
+ function offersKeystrokeApproval(profile, state) {
2454
+ return state === "blocked" && profile.structuredApprovals !== true;
2455
+ }
2176
2456
  function dialogTransition(nextState, dialogOpen) {
2177
2457
  if (isModalState(nextState))
2178
2458
  return { open: true, resolved: false };
@@ -2183,14 +2463,29 @@ function isPromptInjectable(state) {
2183
2463
  }
2184
2464
  var init_choice = () => {};
2185
2465
 
2186
- // packages/core/src/menu-keys.ts
2187
- function buildMenuSelectKeys(index, normalizePresses = MENU_NORMALIZE_PRESSES) {
2188
- if (!Number.isInteger(index) || index < 0) {
2189
- throw new RangeError(`menu index must be a non-negative integer, got ${index}`);
2190
- }
2191
- return MENU_UP.repeat(normalizePresses) + MENU_DOWN.repeat(index) + MENU_SELECT;
2466
+ // packages/core/src/adopt.ts
2467
+ function resolveSessionCwd(input) {
2468
+ if (!input.preferConversation)
2469
+ return input.requested;
2470
+ const own = input.conversationCwd;
2471
+ return own !== undefined && own.length > 0 ? own : input.requested;
2472
+ }
2473
+ function conversationHeldBy(sessions, conversationId) {
2474
+ if (conversationId.length === 0)
2475
+ return;
2476
+ return sessions.find((session) => {
2477
+ if (session.state !== "running")
2478
+ return false;
2479
+ if (session.agentSessionId === conversationId)
2480
+ return true;
2481
+ const argv = session.agentCommand;
2482
+ return argv.some((token, index) => {
2483
+ if (token === "--resume" || token === "-r" || token === "--session-id")
2484
+ return argv[index + 1] === conversationId;
2485
+ return token === `--resume=${conversationId}`;
2486
+ });
2487
+ });
2192
2488
  }
2193
- var MENU_UP = "\x1B[A", MENU_DOWN = "\x1B[B", MENU_SELECT = "\r", MENU_NORMALIZE_PRESSES = 24;
2194
2489
 
2195
2490
  // packages/core/src/agent-status.ts
2196
2491
  function next(prev, state, now) {
@@ -2201,17 +2496,29 @@ function next(prev, state, now) {
2201
2496
  stateStartedAt: prev?.state === state ? prev.stateStartedAt : now
2202
2497
  };
2203
2498
  }
2499
+ function withConversationId(events, identity) {
2500
+ const id = identity.sessionId;
2501
+ const subagent = identity.subagentId;
2502
+ if (typeof id !== "string" || id === "" || typeof subagent === "string" && subagent !== "") {
2503
+ return [...events];
2504
+ }
2505
+ return events.map((event) => ({ session_id: id, ...event }));
2506
+ }
2204
2507
  function reduceAgentState(prev, payload, now) {
2508
+ const result = reduceState(prev, payload, now);
2509
+ const identity = {
2510
+ ...payload.session_id !== undefined ? { agentSessionId: payload.session_id } : {},
2511
+ ...payload.session_file !== undefined ? { agentSessionFile: payload.session_file } : {},
2512
+ ...payload.session_name !== undefined ? { sessionName: payload.session_name } : {}
2513
+ };
2514
+ if (Object.keys(identity).length === 0)
2515
+ return result;
2516
+ return { ...result, meta: { ...identity, ...result.meta } };
2517
+ }
2518
+ function reduceState(prev, payload, now) {
2205
2519
  switch (payload.event) {
2206
2520
  case "session_start":
2207
- return {
2208
- entry: prev,
2209
- meta: {
2210
- agentSessionId: payload.session_id,
2211
- agentSessionFile: payload.session_file,
2212
- sessionName: payload.session_name
2213
- }
2214
- };
2521
+ return { entry: prev };
2215
2522
  case "before_agent_start": {
2216
2523
  const entry = next(prev, prev?.state ?? "working", now);
2217
2524
  entry.prompt = payload.prompt ?? "";
@@ -2378,7 +2685,18 @@ var init_pty_protocol = __esm(() => {
2378
2685
  Data: 2
2379
2686
  };
2380
2687
  MAX_FRAME_BYTES = 8 * 1024 * 1024;
2381
- KNOWN_TYPES = new Set(["hello", "spawn", "resize", "kill", "welcome", "spawned", "exit", "error"]);
2688
+ KNOWN_TYPES = new Set([
2689
+ "hello",
2690
+ "spawn",
2691
+ "subscribe",
2692
+ "resize",
2693
+ "kill",
2694
+ "welcome",
2695
+ "spawned",
2696
+ "subscribed",
2697
+ "exit",
2698
+ "error"
2699
+ ]);
2382
2700
  });
2383
2701
 
2384
2702
  // packages/core/src/workspace.ts
@@ -2511,7 +2829,7 @@ function mapClaudeHook(input) {
2511
2829
  }
2512
2830
  }
2513
2831
  function buildClaudeSettings(input) {
2514
- const handler = {
2832
+ const handler = (timeout) => ({
2515
2833
  type: "http",
2516
2834
  url: `http://127.0.0.1:${input.port}/hook/claude`,
2517
2835
  headers: {
@@ -2519,16 +2837,36 @@ function buildClaudeSettings(input) {
2519
2837
  "x-axterm-session": input.sessionId,
2520
2838
  "x-axterm-launch": input.launchToken
2521
2839
  },
2522
- timeout: HOOK_TIMEOUT_SECONDS
2523
- };
2840
+ timeout
2841
+ });
2842
+ const observe = handler(HOOK_TIMEOUT_SECONDS);
2524
2843
  const hooks = {};
2525
- for (const event of TOOL_EVENTS)
2526
- hooks[event] = [{ matcher: "*", hooks: [handler] }];
2844
+ for (const event of TOOL_EVENTS) {
2845
+ const single = event === "PermissionRequest" ? handler(APPROVAL_TIMEOUT_SECONDS) : observe;
2846
+ hooks[event] = [{ matcher: "*", hooks: [single] }];
2847
+ }
2527
2848
  for (const event of PLAIN_EVENTS)
2528
- hooks[event] = [{ hooks: [handler] }];
2849
+ hooks[event] = [{ hooks: [observe] }];
2529
2850
  return { hooks };
2530
2851
  }
2531
- var MAX_RESULT_TEXT = 2000, HOOK_TIMEOUT_SECONDS = 5, TOOL_EVENTS, PLAIN_EVENTS;
2852
+ function hookPermissionDecision(question, answers, denyMessage) {
2853
+ const picked = answers?.find((a) => a.questionIndex === 0)?.selected[0];
2854
+ if (picked === undefined)
2855
+ return;
2856
+ if (picked === question.options[0]?.label) {
2857
+ return { hookSpecificOutput: { hookEventName: "PermissionRequest", decision: { behavior: "allow" } } };
2858
+ }
2859
+ if (picked === question.options[1]?.label) {
2860
+ return {
2861
+ hookSpecificOutput: {
2862
+ hookEventName: "PermissionRequest",
2863
+ decision: { behavior: "deny", ...denyMessage !== undefined ? { message: denyMessage } : {} }
2864
+ }
2865
+ };
2866
+ }
2867
+ return;
2868
+ }
2869
+ var MAX_RESULT_TEXT = 2000, HOOK_TIMEOUT_SECONDS = 5, APPROVAL_TIMEOUT_SECONDS = 120, APPROVAL_HOLD_MS = 110000, TOOL_EVENTS, PLAIN_EVENTS;
2532
2870
  var init_claude_hooks = __esm(() => {
2533
2871
  TOOL_EVENTS = ["PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionRequest"];
2534
2872
  PLAIN_EVENTS = [
@@ -2554,18 +2892,25 @@ function detectOnboarding(agent, screen) {
2554
2892
  if (!dialog)
2555
2893
  continue;
2556
2894
  const options = [];
2895
+ let lastOption = -1;
2557
2896
  const end = Math.min(lines.length, i + 1 + OPTION_WINDOW_LINES + 1);
2558
2897
  for (let j = i + 1;j < end; j++) {
2559
2898
  const row = OPTION_ROW.exec(lines[j]);
2560
- if (row)
2899
+ if (row) {
2561
2900
  options.push({ digit: row[1], label: row[2].trim() });
2901
+ lastOption = j;
2902
+ }
2562
2903
  }
2563
- if (options.length >= 2)
2564
- return { kind: dialog.kind, question: dialog.question, options };
2904
+ if (options.length < 2)
2905
+ continue;
2906
+ const noise = lines.slice(lastOption + 1).filter((row) => row !== "" && !DIALOG_FOOTER.test(row));
2907
+ if (noise.length > QUIET_AFTER_OPTIONS)
2908
+ continue;
2909
+ return { kind: dialog.kind, question: dialog.question, options };
2565
2910
  }
2566
2911
  return;
2567
2912
  }
2568
- var DIALOGS, OPTION_ROW, OPTION_WINDOW_LINES = 8;
2913
+ var DIALOGS, OPTION_ROW, OPTION_WINDOW_LINES = 8, QUIET_AFTER_OPTIONS = 3, DIALOG_FOOTER;
2569
2914
  var init_onboarding = __esm(() => {
2570
2915
  DIALOGS = {
2571
2916
  gemini: [
@@ -2573,12 +2918,105 @@ var init_onboarding = __esm(() => {
2573
2918
  { kind: "authMethod", question: "How would you like to authenticate for this project?" }
2574
2919
  ],
2575
2920
  claude: [
2921
+ { kind: "folderTrust", question: "Quick safety check: Is this a project you created or one you trust?" },
2922
+ { kind: "autoMode", question: "Set up auto mode for your environment?" },
2576
2923
  { kind: "apiKey", question: "Do you want to use this API key?" },
2577
2924
  { kind: "loginMethod", question: "Select login method:" }
2578
2925
  ],
2579
2926
  codex: [{ kind: "folderTrust", question: "Do you trust the contents of this directory?" }]
2580
2927
  };
2581
- OPTION_ROW = /^[\u25CF\u25CB\u276F>\u203A]?\s*(\d)\.\s+(.+)$/;
2928
+ OPTION_ROW = /^[\u25CF\u25CB\u276F>\u203A]?\s*(\d)\.\s+(.+)$/;
2929
+ DIALOG_FOOTER = /^[\s\u2502\u2500\u2570\u256F\u256D\u256E\u250C\u2510\u2514\u2518|]*$|enter to (confirm|continue|select)|esc to cancel|use enter to select|press enter|\u2191\/\u2193|arrow keys|tab to|^\(.*\)$/i;
2930
+ });
2931
+
2932
+ // packages/core/src/diff-view.ts
2933
+ function scanStructure(lines) {
2934
+ const meta = new Set;
2935
+ let proven = false;
2936
+ let inHunk = false;
2937
+ for (let i = 0;i < lines.length; i++) {
2938
+ const line = lines[i] ?? "";
2939
+ if (line.startsWith("@@")) {
2940
+ inHunk = true;
2941
+ proven ||= HUNK_HEADER.test(line);
2942
+ continue;
2943
+ }
2944
+ if (FILE_SECTION_START.test(line)) {
2945
+ inHunk = false;
2946
+ proven ||= line.startsWith("diff --git ");
2947
+ continue;
2948
+ }
2949
+ if (inHunk || !line.startsWith("--- "))
2950
+ continue;
2951
+ if ((lines[i + 1] ?? "").startsWith("+++ ")) {
2952
+ meta.add(i);
2953
+ meta.add(i + 1);
2954
+ i += 1;
2955
+ }
2956
+ }
2957
+ return { meta, proven };
2958
+ }
2959
+ function countTail(lines) {
2960
+ let added = 0;
2961
+ let removed = 0;
2962
+ for (const line of lines) {
2963
+ if (line.startsWith("+++ ") || line.startsWith("--- "))
2964
+ continue;
2965
+ if (line.startsWith("+"))
2966
+ added += 1;
2967
+ else if (line.startsWith("-"))
2968
+ removed += 1;
2969
+ }
2970
+ return { added, removed };
2971
+ }
2972
+ function summarizePatch(patch, maxChars = DIFF_MAX_CHARS, maxLines = DIFF_MAX_LINES) {
2973
+ const totalChars = patch.length;
2974
+ if (totalChars === 0)
2975
+ return;
2976
+ const clipped = patch.slice(0, maxChars);
2977
+ const all = clipped.split(`
2978
+ `);
2979
+ const cutChars = totalChars > maxChars;
2980
+ const cutLines = all.length > maxLines;
2981
+ const kept = all.slice(0, maxLines);
2982
+ const { meta, proven } = scanStructure(kept);
2983
+ if (!proven)
2984
+ return;
2985
+ let added = 0;
2986
+ let removed = 0;
2987
+ const lines = kept.map((line, i) => {
2988
+ if (meta.has(i) || line.startsWith("@@") || FILE_SECTION_START.test(line)) {
2989
+ return { kind: "meta", text: line };
2990
+ }
2991
+ if (line.startsWith("+")) {
2992
+ added += 1;
2993
+ return { kind: "add", text: line.slice(1) };
2994
+ }
2995
+ if (line.startsWith("-")) {
2996
+ removed += 1;
2997
+ return { kind: "del", text: line.slice(1) };
2998
+ }
2999
+ return { kind: "context", text: line };
3000
+ });
3001
+ if (added + removed === 0)
3002
+ return;
3003
+ const tail = countTail(all.slice(maxLines));
3004
+ added += tail.added;
3005
+ removed += tail.removed;
3006
+ const truncated = cutChars ? "chars" : cutLines ? "lines" : undefined;
3007
+ return {
3008
+ lines: truncated === undefined ? lines : [...lines.slice(0, maxLines - 1), TRUNCATED],
3009
+ added,
3010
+ removed,
3011
+ ...truncated === undefined ? {} : { truncated },
3012
+ totalChars
3013
+ };
3014
+ }
3015
+ var DIFF_MAX_CHARS = 8000, DIFF_MAX_LINES = 80, TRUNCATED, HUNK_HEADER, FILE_SECTION_START;
3016
+ var init_diff_view = __esm(() => {
3017
+ TRUNCATED = { kind: "meta", text: "\u2026 diff truncated \u2026" };
3018
+ HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
3019
+ FILE_SECTION_START = /^(?:diff |index |old mode |new mode |new file mode |deleted file mode |similarity index |dissimilarity index |rename |copy |Binary files )/;
2582
3020
  });
2583
3021
 
2584
3022
  // packages/core/src/doctor.ts
@@ -2824,7 +3262,7 @@ var SEQUENCES, KEY_NAMES;
2824
3262
  var init_keys = __esm(() => {
2825
3263
  SEQUENCES = {
2826
3264
  approve: "\r",
2827
- deny: "\x1B[B\r",
3265
+ deny: "\x1B",
2828
3266
  enter: "\r",
2829
3267
  esc: "\x1B",
2830
3268
  up: "\x1B[A",
@@ -3300,24 +3738,7 @@ trusted_hash = "${e.hash}"
3300
3738
  }).join(`
3301
3739
  `);
3302
3740
  }
3303
- function codexPermissionDecision(question, answers, denyMessage) {
3304
- const picked = answers?.find((a) => a.questionIndex === 0)?.selected[0];
3305
- if (picked === undefined)
3306
- return;
3307
- if (picked === question.options[0]?.label) {
3308
- return { hookSpecificOutput: { hookEventName: "PermissionRequest", decision: { behavior: "allow" } } };
3309
- }
3310
- if (picked === question.options[1]?.label) {
3311
- return {
3312
- hookSpecificOutput: {
3313
- hookEventName: "PermissionRequest",
3314
- decision: { behavior: "deny", ...denyMessage !== undefined ? { message: denyMessage } : {} }
3315
- }
3316
- };
3317
- }
3318
- return;
3319
- }
3320
- var OBSERVATION_TIMEOUT_SECONDS = 15, CODEX_PERMISSION_TIMEOUT_SECONDS = 120, CODEX_APPROVAL_HOLD_MS = 110000, CODEX_APPROVAL_POST_TIMEOUT_MS = 115000, SESSION_END_TIMEOUT_SECONDS = 2, OBSERVATION_EVENTS, EVENT_KEY_LABELS;
3741
+ var OBSERVATION_TIMEOUT_SECONDS = 15, CODEX_PERMISSION_TIMEOUT_SECONDS = 120, CODEX_APPROVAL_POST_TIMEOUT_MS = 115000, SESSION_END_TIMEOUT_SECONDS = 2, OBSERVATION_EVENTS, EVENT_KEY_LABELS;
3321
3742
  var init_codex_hooks = __esm(() => {
3322
3743
  OBSERVATION_EVENTS = ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"];
3323
3744
  EVENT_KEY_LABELS = {
@@ -3571,11 +3992,11 @@ var init_prompt_detect = __esm(() => {
3571
3992
  });
3572
3993
 
3573
3994
  // packages/core/src/session-scope.ts
3574
- import path2 from "path";
3995
+ import path3 from "path";
3575
3996
  function comparable(target, resolve) {
3576
- let real = path2.resolve(target);
3997
+ let real = path3.resolve(target);
3577
3998
  try {
3578
- real = path2.resolve(resolve(real));
3999
+ real = path3.resolve(resolve(real));
3579
4000
  } catch {}
3580
4001
  const trimmed = real.replace(/[\\/]+$/, "");
3581
4002
  return process.platform === "win32" ? trimmed.toLowerCase() : trimmed;
@@ -3583,7 +4004,7 @@ function comparable(target, resolve) {
3583
4004
  function pathWithin(parent, child, resolve = asGiven) {
3584
4005
  const from = comparable(parent, resolve);
3585
4006
  const to = comparable(child, resolve);
3586
- return to === from || to.startsWith(from + path2.sep);
4007
+ return to === from || to.startsWith(from + path3.sep);
3587
4008
  }
3588
4009
  function sessionInScope(session, scope, resolve = asGiven) {
3589
4010
  if (pathWithin(scope, session.cwd, resolve))
@@ -3614,6 +4035,9 @@ function sessionsByRecency(live, stored) {
3614
4035
  var asGiven = (target) => target;
3615
4036
  var init_session_scope = () => {};
3616
4037
 
4038
+ // packages/core/src/test-scratch.ts
4039
+ var init_test_scratch = () => {};
4040
+
3617
4041
  // packages/core/src/index.ts
3618
4042
  function isRpcEvent(f) {
3619
4043
  return typeof f.ev === "string";
@@ -3633,7 +4057,9 @@ var init_src = __esm(() => {
3633
4057
  init_config_paths();
3634
4058
  init_setup_wizard();
3635
4059
  init_agent_registry();
4060
+ init_agent_resume();
3636
4061
  init_self_spawn();
4062
+ init_service();
3637
4063
  init_channel_spi();
3638
4064
  init_update();
3639
4065
  init_errors();
@@ -3645,6 +4071,7 @@ var init_src = __esm(() => {
3645
4071
  init_pty_protocol();
3646
4072
  init_claude_hooks();
3647
4073
  init_onboarding();
4074
+ init_diff_view();
3648
4075
  init_doctor();
3649
4076
  init_stall();
3650
4077
  init_codex_stream();
@@ -3660,19 +4087,20 @@ var init_src = __esm(() => {
3660
4087
  init_dotenv_scrub();
3661
4088
  init_prompt_detect();
3662
4089
  init_session_scope();
4090
+ init_test_scratch();
3663
4091
  SUBMIT_DELAY_MS = process.platform === "win32" ? 1500 : 500;
3664
4092
  PASTE_CHUNK_BYTES = 16 * 1024;
3665
4093
  });
3666
4094
 
3667
4095
  // packages/daemon/src/config-store.ts
3668
- import fs5 from "fs";
3669
- import path6 from "path";
3670
- function writeAtomic(target, contents, mode) {
4096
+ import fs7 from "fs";
4097
+ import path8 from "path";
4098
+ function writeAtomic2(target, contents, mode) {
3671
4099
  const tmp = `${target}.tmp`;
3672
- fs5.writeFileSync(tmp, contents, { mode });
3673
- fs5.renameSync(tmp, target);
4100
+ fs7.writeFileSync(tmp, contents, { mode });
4101
+ fs7.renameSync(tmp, target);
3674
4102
  if (process.platform !== "win32")
3675
- fs5.chmodSync(target, mode);
4103
+ fs7.chmodSync(target, mode);
3676
4104
  }
3677
4105
 
3678
4106
  class ConfigStore {
@@ -3688,16 +4116,16 @@ class ConfigStore {
3688
4116
  }
3689
4117
  load() {
3690
4118
  this.loadError = undefined;
3691
- fs5.mkdirSync(this.paths.configDir, { recursive: true, mode: DIR_MODE });
4119
+ fs7.mkdirSync(this.paths.configDir, { recursive: true, mode: DIR_MODE });
3692
4120
  if (process.platform !== "win32") {
3693
4121
  try {
3694
- fs5.chmodSync(this.paths.configDir, DIR_MODE);
4122
+ fs7.chmodSync(this.paths.configDir, DIR_MODE);
3695
4123
  } catch {}
3696
4124
  }
3697
4125
  this.config = structuredClone(DEFAULT_CONFIG);
3698
- if (fs5.existsSync(this.paths.configFile)) {
4126
+ if (fs7.existsSync(this.paths.configFile)) {
3699
4127
  try {
3700
- const parsed = parseConfig(JSON.parse(fs5.readFileSync(this.paths.configFile, "utf8")));
4128
+ const parsed = parseConfig(JSON.parse(fs7.readFileSync(this.paths.configFile, "utf8")));
3701
4129
  if ("errors" in parsed)
3702
4130
  this.loadError = parsed.errors.join("; ");
3703
4131
  else
@@ -3707,10 +4135,10 @@ class ConfigStore {
3707
4135
  }
3708
4136
  }
3709
4137
  this.secrets = {};
3710
- if (fs5.existsSync(this.paths.secretsFile)) {
4138
+ if (fs7.existsSync(this.paths.secretsFile)) {
3711
4139
  this.tightenPermissions(this.paths.secretsFile);
3712
4140
  try {
3713
- const raw = JSON.parse(fs5.readFileSync(this.paths.secretsFile, "utf8"));
4141
+ const raw = JSON.parse(fs7.readFileSync(this.paths.secretsFile, "utf8"));
3714
4142
  if (raw !== null && typeof raw === "object") {
3715
4143
  for (const [key, value] of Object.entries(raw)) {
3716
4144
  if (typeof value === "string")
@@ -3726,8 +4154,8 @@ class ConfigStore {
3726
4154
  if (process.platform === "win32")
3727
4155
  return;
3728
4156
  try {
3729
- if ((fs5.statSync(file).mode & 63) !== 0)
3730
- fs5.chmodSync(file, FILE_MODE);
4157
+ if ((fs7.statSync(file).mode & 63) !== 0)
4158
+ fs7.chmodSync(file, FILE_MODE);
3731
4159
  } catch {}
3732
4160
  }
3733
4161
  get() {
@@ -3735,7 +4163,7 @@ class ConfigStore {
3735
4163
  }
3736
4164
  health() {
3737
4165
  return configHealth({
3738
- fileExists: fs5.existsSync(this.paths.configFile),
4166
+ fileExists: fs7.existsSync(this.paths.configFile),
3739
4167
  parseError: this.loadError,
3740
4168
  config: this.resolved()
3741
4169
  });
@@ -3768,7 +4196,7 @@ class ConfigStore {
3768
4196
  const parsed = parseConfig(candidate);
3769
4197
  if ("errors" in parsed)
3770
4198
  throw new Error(parsed.errors.join("; "));
3771
- writeAtomic(this.paths.configFile, `${JSON.stringify(parsed.config, null, 2)}
4199
+ writeAtomic2(this.paths.configFile, `${JSON.stringify(parsed.config, null, 2)}
3772
4200
  `, FILE_MODE);
3773
4201
  this.config = parsed.config;
3774
4202
  this.loadError = undefined;
@@ -3780,11 +4208,11 @@ class ConfigStore {
3780
4208
  delete this.secrets[dottedPath];
3781
4209
  else
3782
4210
  this.secrets[dottedPath] = value;
3783
- if (!fs5.existsSync(this.paths.configFile)) {
3784
- writeAtomic(this.paths.configFile, `${JSON.stringify(this.config, null, 2)}
4211
+ if (!fs7.existsSync(this.paths.configFile)) {
4212
+ writeAtomic2(this.paths.configFile, `${JSON.stringify(this.config, null, 2)}
3785
4213
  `, FILE_MODE);
3786
4214
  }
3787
- writeAtomic(this.paths.secretsFile, `${JSON.stringify(this.secrets, null, 2)}
4215
+ writeAtomic2(this.paths.secretsFile, `${JSON.stringify(this.secrets, null, 2)}
3788
4216
  `, FILE_MODE);
3789
4217
  }
3790
4218
  hasSecret(dottedPath) {
@@ -3810,7 +4238,7 @@ class ConfigStore {
3810
4238
  fingerprint() {
3811
4239
  const stamp = (file) => {
3812
4240
  try {
3813
- return String(fs5.statSync(file).mtimeMs);
4241
+ return String(fs7.statSync(file).mtimeMs);
3814
4242
  } catch {
3815
4243
  return "-";
3816
4244
  }
@@ -3832,8 +4260,8 @@ function applyPath(config, dottedPath, value) {
3832
4260
  function defaultConfigStorePaths(configDir) {
3833
4261
  return {
3834
4262
  configDir,
3835
- configFile: path6.join(configDir, "config.json"),
3836
- secretsFile: path6.join(configDir, "secrets.json")
4263
+ configFile: path8.join(configDir, "config.json"),
4264
+ secretsFile: path8.join(configDir, "secrets.json")
3837
4265
  };
3838
4266
  }
3839
4267
  var ENV_ALIASES, DIR_MODE = 448, FILE_MODE = 384;
@@ -3852,13 +4280,13 @@ var init_config_store = __esm(() => {
3852
4280
  });
3853
4281
 
3854
4282
  // packages/daemon/src/shell.ts
3855
- import fs6 from "fs";
3856
- import path7 from "path";
4283
+ import fs8 from "fs";
4284
+ import path9 from "path";
3857
4285
  function findExecutable(name, env) {
3858
4286
  const isWindows = env.platform === "win32";
3859
- const join = isWindows ? path7.win32.join : path7.posix.join;
4287
+ const join = isWindows ? path9.win32.join : path9.posix.join;
3860
4288
  const separator = isWindows ? ";" : ":";
3861
- const extensions = isWindows ? path7.win32.extname(name).length > 0 ? [""] : env.pathExt.split(";").filter((e) => e.length > 0) : [""];
4289
+ const extensions = isWindows ? path9.win32.extname(name).length > 0 ? [""] : env.pathExt.split(";").filter((e) => e.length > 0) : [""];
3862
4290
  for (const rawDir of env.pathEnv.split(separator)) {
3863
4291
  const dir = rawDir.trim().replace(/^"|"$/g, "");
3864
4292
  if (dir.length === 0)
@@ -3876,16 +4304,16 @@ function lookupOnPath(name) {
3876
4304
  platform: process.platform,
3877
4305
  pathEnv: process.env["PATH"] ?? "",
3878
4306
  pathExt: process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD",
3879
- exists: (candidate) => fs6.existsSync(candidate)
4307
+ exists: (candidate) => fs8.existsSync(candidate)
3880
4308
  });
3881
4309
  }
3882
4310
  function resolveShell(kind) {
3883
4311
  if (process.platform !== "win32")
3884
4312
  return resolvePosixShell(kind);
3885
4313
  const systemRoot = process.env["SystemRoot"] ?? "C:\\Windows";
3886
- const windowsPowershell = path7.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
4314
+ const windowsPowershell = path9.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
3887
4315
  if (kind === "cmd") {
3888
- return { kind: "cmd", exe: path7.join(systemRoot, "System32", "cmd.exe") };
4316
+ return { kind: "cmd", exe: path9.join(systemRoot, "System32", "cmd.exe") };
3889
4317
  }
3890
4318
  if (kind === "powershell") {
3891
4319
  return { kind: "powershell", exe: windowsPowershell };
@@ -3913,7 +4341,7 @@ function resolvePosixShell(kind) {
3913
4341
  candidates.push({ kind: "bash", exe: login });
3914
4342
  candidates.push({ kind: "bash", exe: "/bin/bash" }, { kind: "bash", exe: "/usr/bin/bash" });
3915
4343
  }
3916
- const found = candidates.find((c) => fs6.existsSync(c.exe));
4344
+ const found = candidates.find((c) => fs8.existsSync(c.exe));
3917
4345
  if (found)
3918
4346
  return found;
3919
4347
  return { kind: "sh", exe: "/bin/sh" };
@@ -3958,24 +4386,24 @@ function ipcEndpoint(platform, user, env) {
3958
4386
  const suffix = `-${shortHash(env.runtimeDir)}`;
3959
4387
  return `\\\\.\\pipe\\axterm-${safeUser}${suffix}`;
3960
4388
  }
3961
- return path7.posix.join(env.runtimeDir, "axterm.sock");
4389
+ return path9.posix.join(env.runtimeDir, "axterm.sock");
3962
4390
  }
3963
4391
  var cachedPwsh;
3964
4392
  var init_shell = () => {};
3965
4393
 
3966
4394
  // packages/daemon/src/paths.ts
3967
- import fs7 from "fs";
3968
- import os3 from "os";
3969
- import path8 from "path";
4395
+ import fs9 from "fs";
4396
+ import os4 from "os";
4397
+ import path10 from "path";
3970
4398
  function ensureDataDirs() {
3971
4399
  for (const dir of [PATHS.configDir, PATHS.stateDir, PATHS.runtimeDir, SCROLLBACK_DIR, LOG_DIR, EXT_DIR]) {
3972
- fs7.mkdirSync(dir, { recursive: true, mode: DIR_MODE2 });
4400
+ fs9.mkdirSync(dir, { recursive: true, mode: DIR_MODE2 });
3973
4401
  }
3974
4402
  if (process.platform === "win32")
3975
4403
  return;
3976
4404
  for (const dir of [PATHS.configDir, PATHS.stateDir, PATHS.runtimeDir]) {
3977
4405
  try {
3978
- fs7.chmodSync(dir, DIR_MODE2);
4406
+ fs9.chmodSync(dir, DIR_MODE2);
3979
4407
  } catch {}
3980
4408
  }
3981
4409
  }
@@ -3983,14 +4411,14 @@ function restrictFile(file) {
3983
4411
  if (process.platform === "win32")
3984
4412
  return;
3985
4413
  try {
3986
- fs7.chmodSync(file, 384);
4414
+ fs9.chmodSync(file, 384);
3987
4415
  } catch {}
3988
4416
  }
3989
4417
  function log(message) {
3990
4418
  const line = `${new Date().toISOString()} ${message}
3991
4419
  `;
3992
4420
  try {
3993
- fs7.appendFileSync(LOG_FILE, line);
4421
+ fs9.appendFileSync(LOG_FILE, line);
3994
4422
  } catch {}
3995
4423
  }
3996
4424
  var PATHS, CONFIG_DIR, STATE_DIR, RUNTIME_DIR, SCROLLBACK_DIR, LOG_DIR, EXT_DIR, DB_PATH, RUNTIME_FILE2, HOOK_ENDPOINT_FILE, DIR_MODE2 = 448, PIPE_PATH, LOG_FILE;
@@ -4007,15 +4435,15 @@ var init_paths = __esm(() => {
4007
4435
  DB_PATH = PATHS.dbFile;
4008
4436
  RUNTIME_FILE2 = PATHS.runtimeFile;
4009
4437
  HOOK_ENDPOINT_FILE = PATHS.hookEndpointFile;
4010
- PIPE_PATH = ipcEndpoint(process.platform, os3.userInfo().username, {
4438
+ PIPE_PATH = ipcEndpoint(process.platform, os4.userInfo().username, {
4011
4439
  runtimeDir: PATHS.runtimeDir
4012
4440
  });
4013
- LOG_FILE = path8.join(LOG_DIR, "daemon.log");
4441
+ LOG_FILE = path10.join(LOG_DIR, "daemon.log");
4014
4442
  });
4015
4443
 
4016
4444
  // packages/daemon/src/store.ts
4017
- import fs8 from "fs";
4018
- import path9 from "path";
4445
+ import fs10 from "fs";
4446
+ import path11 from "path";
4019
4447
  import { Database } from "bun:sqlite";
4020
4448
  function rowToInfo(row) {
4021
4449
  return {
@@ -4046,7 +4474,7 @@ function rowToInfo(row) {
4046
4474
  class Store {
4047
4475
  db;
4048
4476
  constructor(dbPath = DB_PATH) {
4049
- fs8.mkdirSync(path9.dirname(dbPath), { recursive: true });
4477
+ fs10.mkdirSync(path11.dirname(dbPath), { recursive: true });
4050
4478
  this.db = new Database(dbPath, { strict: true });
4051
4479
  this.db.run("PRAGMA journal_mode = WAL");
4052
4480
  this.db.run(`
@@ -4125,8 +4553,10 @@ class Store {
4125
4553
  this.db.prepare(`UPDATE sessions SET state = 'running', exit_code = NULL, exited_at = NULL,
4126
4554
  agent_command = ?, pty_pid = ? WHERE id = ?`).run(JSON.stringify(agentCommand), ptyPid ?? null, id);
4127
4555
  }
4128
- markStaleRunningAsExited() {
4129
- return this.db.prepare(`UPDATE sessions SET state = 'exited', exited_at = ? WHERE state != 'exited'`).run(Date.now()).changes;
4556
+ markStaleRunningAsExited(keep = []) {
4557
+ const holes = keep.map(() => "?").join(",");
4558
+ const sql = keep.length === 0 ? `UPDATE sessions SET state = 'exited', exited_at = ? WHERE state != 'exited'` : `UPDATE sessions SET state = 'exited', exited_at = ? WHERE state != 'exited' AND id NOT IN (${holes})`;
4559
+ return this.db.prepare(sql).run(Date.now(), ...keep).changes;
4130
4560
  }
4131
4561
  listSessions() {
4132
4562
  const rows = this.db.prepare(`SELECT * FROM sessions ORDER BY created_at DESC`).all();
@@ -4212,11 +4642,11 @@ var init_store = __esm(() => {
4212
4642
  });
4213
4643
 
4214
4644
  // packages/daemon/src/config-migrate.ts
4215
- import fs9 from "fs";
4216
- import path10 from "path";
4645
+ import fs11 from "fs";
4646
+ import path12 from "path";
4217
4647
  import { Database as Database2 } from "bun:sqlite";
4218
4648
  function checkpointWal(dbFile) {
4219
- if (!fs9.existsSync(dbFile))
4649
+ if (!fs11.existsSync(dbFile))
4220
4650
  return;
4221
4651
  try {
4222
4652
  const db = new Database2(dbFile);
@@ -4226,32 +4656,32 @@ function checkpointWal(dbFile) {
4226
4656
  }
4227
4657
  function relocate(source, target) {
4228
4658
  try {
4229
- fs9.renameSync(source, target);
4659
+ fs11.renameSync(source, target);
4230
4660
  } catch {
4231
- fs9.cpSync(source, target, { recursive: true });
4661
+ fs11.cpSync(source, target, { recursive: true });
4232
4662
  }
4233
4663
  }
4234
4664
  function moveInto(from, to, notes) {
4235
- checkpointWal(path10.join(from, "axterm.db"));
4236
- fs9.mkdirSync(to, { recursive: true });
4665
+ checkpointWal(path12.join(from, "axterm.db"));
4666
+ fs11.mkdirSync(to, { recursive: true });
4237
4667
  for (const entry of STATE_ENTRIES) {
4238
- const source = path10.join(from, entry);
4239
- const target = path10.join(to, entry);
4240
- if (!fs9.existsSync(source))
4668
+ const source = path12.join(from, entry);
4669
+ const target = path12.join(to, entry);
4670
+ if (!fs11.existsSync(source))
4241
4671
  continue;
4242
- if (!fs9.existsSync(target)) {
4672
+ if (!fs11.existsSync(target)) {
4243
4673
  relocate(source, target);
4244
4674
  notes.push(`moved ${entry}`);
4245
4675
  continue;
4246
4676
  }
4247
- if (!fs9.statSync(source).isDirectory() || !fs9.statSync(target).isDirectory())
4677
+ if (!fs11.statSync(source).isDirectory() || !fs11.statSync(target).isDirectory())
4248
4678
  continue;
4249
4679
  let moved = 0;
4250
- for (const child of fs9.readdirSync(source)) {
4251
- const childTarget = path10.join(target, child);
4252
- if (fs9.existsSync(childTarget))
4680
+ for (const child of fs11.readdirSync(source)) {
4681
+ const childTarget = path12.join(target, child);
4682
+ if (fs11.existsSync(childTarget))
4253
4683
  continue;
4254
- relocate(path10.join(source, child), childTarget);
4684
+ relocate(path12.join(source, child), childTarget);
4255
4685
  moved += 1;
4256
4686
  }
4257
4687
  if (moved > 0)
@@ -4261,29 +4691,29 @@ function moveInto(from, to, notes) {
4261
4691
  function migrateLegacyConfig(input) {
4262
4692
  const notes = [];
4263
4693
  const storePaths = defaultConfigStorePaths(input.configDir);
4264
- if (fs9.existsSync(storePaths.configFile))
4694
+ if (fs11.existsSync(storePaths.configFile))
4265
4695
  return { migrated: false, notes };
4266
- const configSource = input.legacyRoots.find((dir) => fs9.existsSync(path10.join(dir, "config.json")));
4696
+ const configSource = input.legacyRoots.find((dir) => fs11.existsSync(path12.join(dir, "config.json")));
4267
4697
  if (configSource !== undefined) {
4268
- fs9.mkdirSync(input.configDir, { recursive: true });
4269
- fs9.copyFileSync(path10.join(configSource, "config.json"), storePaths.configFile);
4270
- const secrets = path10.join(configSource, "secrets.json");
4271
- if (fs9.existsSync(secrets)) {
4272
- fs9.copyFileSync(secrets, storePaths.secretsFile);
4698
+ fs11.mkdirSync(input.configDir, { recursive: true });
4699
+ fs11.copyFileSync(path12.join(configSource, "config.json"), storePaths.configFile);
4700
+ const secrets = path12.join(configSource, "secrets.json");
4701
+ if (fs11.existsSync(secrets)) {
4702
+ fs11.copyFileSync(secrets, storePaths.secretsFile);
4273
4703
  try {
4274
- fs9.chmodSync(storePaths.secretsFile, 384);
4704
+ fs11.chmodSync(storePaths.secretsFile, 384);
4275
4705
  } catch {}
4276
4706
  }
4277
- fs9.writeFileSync(path10.join(configSource, "MIGRATED.txt"), `axterm now reads its configuration from:
4707
+ fs11.writeFileSync(path12.join(configSource, "MIGRATED.txt"), `axterm now reads its configuration from:
4278
4708
  ${input.configDir}
4279
4709
  ` + `These files are no longer used. secrets.json still holds your bot token \u2014 delete this directory once you are satisfied.
4280
4710
  `);
4281
4711
  notes.push(`adopted configuration from ${configSource}`);
4282
4712
  }
4283
- const legacy = input.legacyRoots.find((dir) => fs9.existsSync(path10.join(dir, "axterm.db")));
4713
+ const legacy = input.legacyRoots.find((dir) => fs11.existsSync(path12.join(dir, "axterm.db")));
4284
4714
  if (legacy !== undefined) {
4285
4715
  moveInto(legacy, input.stateDir, notes);
4286
- fs9.writeFileSync(path10.join(legacy, "MIGRATED.txt"), `axterm now stores state in:
4716
+ fs11.writeFileSync(path12.join(legacy, "MIGRATED.txt"), `axterm now stores state in:
4287
4717
  ${input.stateDir}
4288
4718
  and configuration in:
4289
4719
  ${input.configDir}
@@ -4292,8 +4722,8 @@ and configuration in:
4292
4722
  }
4293
4723
  if (configSource !== undefined)
4294
4724
  return { migrated: true, ...legacy ? { from: legacy } : {}, notes };
4295
- const dbFile = path10.join(input.stateDir, "axterm.db");
4296
- if (!fs9.existsSync(dbFile))
4725
+ const dbFile = path12.join(input.stateDir, "axterm.db");
4726
+ if (!fs11.existsSync(dbFile))
4297
4727
  return { migrated: notes.length > 0, ...legacy ? { from: legacy } : {}, notes };
4298
4728
  const store = new Store(dbFile);
4299
4729
  const machine2 = store.getConfig("axterm_machine");
@@ -5131,13 +5561,13 @@ export default function axtermAgentStatus(pi: PiLike): void {\r
5131
5561
  var init_axterm_agent_status = () => {};
5132
5562
 
5133
5563
  // packages/daemon/src/ext-install.ts
5134
- import fs10 from "fs";
5135
- import path11 from "path";
5564
+ import fs12 from "fs";
5565
+ import path13 from "path";
5136
5566
  function installExtensionAsset(targetDir) {
5137
- fs10.mkdirSync(targetDir, { recursive: true });
5138
- const target = path11.join(targetDir, "axterm-agent-status.mjs");
5567
+ fs12.mkdirSync(targetDir, { recursive: true });
5568
+ const target = path13.join(targetDir, "axterm-agent-status.mjs");
5139
5569
  const js = new Bun.Transpiler({ loader: "ts" }).transformSync(extensionSource);
5140
- fs10.writeFileSync(target, MANAGED_HEADER + js);
5570
+ fs12.writeFileSync(target, MANAGED_HEADER + js);
5141
5571
  return target;
5142
5572
  }
5143
5573
  var extensionSource, MANAGED_HEADER = `// @axterm-managed-omp-extension
@@ -5150,8 +5580,8 @@ var init_ext_install = __esm(() => {
5150
5580
 
5151
5581
  // packages/daemon/src/workspace-local.ts
5152
5582
  import { spawn as spawn2 } from "child_process";
5153
- import fs11 from "fs/promises";
5154
- import path12 from "path";
5583
+ import fs13 from "fs/promises";
5584
+ import path14 from "path";
5155
5585
  function isAbsent(error) {
5156
5586
  const code = error?.code;
5157
5587
  return code === "ENOENT" || code === "ENOTDIR";
@@ -5164,10 +5594,10 @@ class LocalWorkspace {
5164
5594
  this.stagingDir = stagingDir;
5165
5595
  }
5166
5596
  async list(dir) {
5167
- const dirents = await fs11.readdir(dir, { withFileTypes: true });
5597
+ const dirents = await fs13.readdir(dir, { withFileTypes: true });
5168
5598
  const stats = await Promise.all(dirents.map(async (dirent) => {
5169
5599
  try {
5170
- return await fs11.lstat(path12.join(dir, dirent.name));
5600
+ return await fs13.lstat(path14.join(dir, dirent.name));
5171
5601
  } catch (error) {
5172
5602
  if (isAbsent(error))
5173
5603
  return;
@@ -5191,7 +5621,7 @@ class LocalWorkspace {
5191
5621
  }
5192
5622
  async stat(target) {
5193
5623
  try {
5194
- const stat = await fs11.stat(target);
5624
+ const stat = await fs13.stat(target);
5195
5625
  const kind = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "other";
5196
5626
  return { kind, size: stat.size, mtimeMs: stat.mtimeMs };
5197
5627
  } catch (error) {
@@ -5202,8 +5632,8 @@ class LocalWorkspace {
5202
5632
  }
5203
5633
  async read(file, limit) {
5204
5634
  if (limit === undefined)
5205
- return await fs11.readFile(file);
5206
- const handle = await fs11.open(file, "r");
5635
+ return await fs13.readFile(file);
5636
+ const handle = await fs13.open(file, "r");
5207
5637
  try {
5208
5638
  const buffer = Buffer.allocUnsafe(Math.min(limit, (await handle.stat()).size));
5209
5639
  let filled = 0;
@@ -5222,10 +5652,10 @@ class LocalWorkspace {
5222
5652
  return Promise.resolve(file);
5223
5653
  }
5224
5654
  async mkdirp(dir, mode) {
5225
- await fs11.mkdir(dir, mode === undefined ? { recursive: true } : { recursive: true, mode });
5655
+ await fs13.mkdir(dir, mode === undefined ? { recursive: true } : { recursive: true, mode });
5226
5656
  }
5227
5657
  async write(file, bytes, mode) {
5228
- await fs11.writeFile(file, bytes, mode === undefined ? undefined : { mode });
5658
+ await fs13.writeFile(file, bytes, mode === undefined ? undefined : { mode });
5229
5659
  }
5230
5660
  exec(argv, cwd) {
5231
5661
  const [command, ...args] = argv;
@@ -5254,9 +5684,9 @@ var init_workspace_local = () => {};
5254
5684
 
5255
5685
  // packages/daemon/src/hook-host.ts
5256
5686
  import { randomBytes } from "crypto";
5257
- import fs12 from "fs";
5687
+ import fs14 from "fs";
5258
5688
  import http from "http";
5259
- import path13 from "path";
5689
+ import path15 from "path";
5260
5690
  function header(req, name) {
5261
5691
  const value = req.headers[name];
5262
5692
  return Array.isArray(value) ? value[0] : value;
@@ -5270,17 +5700,18 @@ class HookHost {
5270
5700
  }
5271
5701
  sessions = new Map;
5272
5702
  pendingAsks = new Map;
5703
+ refusedHooks = new Set;
5273
5704
  endpointFile;
5274
5705
  server;
5275
5706
  askOpenHandler;
5276
5707
  resolveWorkspace;
5277
5708
  local;
5278
- codexApprovalHoldMs;
5279
- codexAskSeq = 0;
5709
+ approvalHoldMs;
5710
+ approvalSeq = 0;
5280
5711
  constructor(options) {
5281
5712
  this.endpointFile = options.endpointFile;
5282
- this.codexApprovalHoldMs = options.codexApprovalHoldMs ?? CODEX_APPROVAL_HOLD_MS;
5283
- this.resolveWorkspace = options.workspaceFor ?? (() => this.local ??= new LocalWorkspace(path13.join(RUNTIME_DIR, "materialised")));
5713
+ this.approvalHoldMs = options.approvalHoldMs ?? APPROVAL_HOLD_MS;
5714
+ this.resolveWorkspace = options.workspaceFor ?? (() => this.local ??= new LocalWorkspace(path15.join(RUNTIME_DIR, "materialised")));
5284
5715
  }
5285
5716
  start() {
5286
5717
  const server = http.createServer((req, res) => this.handle(req, res));
@@ -5296,16 +5727,19 @@ class HookHost {
5296
5727
  return promise;
5297
5728
  }
5298
5729
  writeEndpointFile(port) {
5299
- fs12.mkdirSync(path13.dirname(this.endpointFile), { recursive: true });
5730
+ fs14.mkdirSync(path15.dirname(this.endpointFile), { recursive: true });
5300
5731
  const tmp = `${this.endpointFile}.tmp`;
5301
- fs12.writeFileSync(tmp, `AXTERM_HOOK_PORT=${port}
5732
+ fs14.writeFileSync(tmp, `AXTERM_HOOK_PORT=${port}
5302
5733
  AXTERM_HOOK_TOKEN=${this.token}
5303
5734
  `);
5304
- fs12.renameSync(tmp, this.endpointFile);
5735
+ fs14.renameSync(tmp, this.endpointFile);
5305
5736
  }
5306
5737
  registerSession(sessionId, launchToken, onEvent) {
5307
5738
  this.sessions.set(sessionId, { launchToken, onEvent });
5308
5739
  }
5740
+ launchMatches(session, presented) {
5741
+ return session.launchToken === undefined || session.launchToken === presented;
5742
+ }
5309
5743
  unregisterSession(sessionId) {
5310
5744
  this.sessions.delete(sessionId);
5311
5745
  for (const [askId, pending] of this.pendingAsks) {
@@ -5367,11 +5801,11 @@ AXTERM_HOOK_TOKEN=${this.token}
5367
5801
  req.on("end", () => {
5368
5802
  const raw = Buffer.concat(chunks).toString("utf8");
5369
5803
  if (isClaude) {
5370
- this.handleAgentHook(req, raw, res, mapClaudeHook, "claude");
5804
+ this.handleDecidingAgentHook(req, raw, res, "claude");
5371
5805
  return;
5372
5806
  }
5373
5807
  if (isCodex) {
5374
- this.handleCodex(req, raw, res);
5808
+ this.handleDecidingAgentHook(req, raw, res, "codex");
5375
5809
  return;
5376
5810
  }
5377
5811
  if (isGemini) {
@@ -5394,7 +5828,7 @@ AXTERM_HOOK_TOKEN=${this.token}
5394
5828
  res.writeHead(404).end();
5395
5829
  return;
5396
5830
  }
5397
- if (body.launchToken !== session.launchToken) {
5831
+ if (!this.launchMatches(session, body.launchToken)) {
5398
5832
  res.writeHead(403).end();
5399
5833
  return;
5400
5834
  }
@@ -5429,9 +5863,11 @@ AXTERM_HOOK_TOKEN=${this.token}
5429
5863
  return;
5430
5864
  }
5431
5865
  res.writeHead(200).end();
5432
- this.dispatchHookPayloads(auth.session, map(input), agent);
5866
+ const named = input;
5867
+ const events = withConversationId(map(input), { sessionId: named.session_id, subagentId: named.agent_id });
5868
+ this.dispatchHookPayloads(auth.session, events, agent);
5433
5869
  }
5434
- handleCodex(req, raw, res) {
5870
+ handleDecidingAgentHook(req, raw, res, agent) {
5435
5871
  const auth = this.agentHookSession(req);
5436
5872
  if (!auth) {
5437
5873
  res.writeHead(200).end();
@@ -5444,20 +5880,27 @@ AXTERM_HOOK_TOKEN=${this.token}
5444
5880
  res.writeHead(200).end();
5445
5881
  return;
5446
5882
  }
5883
+ const dispatch = () => {
5884
+ const events = withConversationId(mapClaudeHook(input), {
5885
+ sessionId: input.session_id,
5886
+ subagentId: input.agent_id
5887
+ });
5888
+ this.dispatchHookPayloads(auth.session, events, agent);
5889
+ };
5447
5890
  if (input.hook_event_name !== "PermissionRequest") {
5448
5891
  res.writeHead(200).end();
5449
- this.dispatchHookPayloads(auth.session, mapClaudeHook(input), "codex");
5892
+ dispatch();
5450
5893
  return;
5451
5894
  }
5452
5895
  const question = buildApprovalQuestion(input.tool_name);
5453
- this.dispatchHookPayloads(auth.session, mapClaudeHook(input), "codex");
5454
- const askId = `codex-${auth.sessionId}-${this.codexAskSeq++}`;
5896
+ dispatch();
5897
+ const askId = `${agent}-${auth.sessionId}-${this.approvalSeq++}`;
5455
5898
  let fence;
5456
5899
  const settle = (body) => {
5457
5900
  clearTimeout(fence);
5458
5901
  if (res.writableEnded)
5459
5902
  return;
5460
- const decision = body.status === "answered" ? codexPermissionDecision(question, body.answers, t("codex.approval.denied")) : undefined;
5903
+ const decision = body.status === "answered" ? hookPermissionDecision(question, body.answers, t("agent.approval.denied")) : undefined;
5461
5904
  if (decision === undefined) {
5462
5905
  res.writeHead(200).end();
5463
5906
  return;
@@ -5478,15 +5921,22 @@ AXTERM_HOOK_TOKEN=${this.token}
5478
5921
  return;
5479
5922
  this.pendingAsks.delete(askId);
5480
5923
  settle({ status: "cancelled" });
5481
- }, this.codexApprovalHoldMs);
5924
+ }, this.approvalHoldMs);
5482
5925
  fence.unref?.();
5483
5926
  }
5484
5927
  agentHookSession(req) {
5485
5928
  const sessionId = header(req, "x-axterm-session");
5486
5929
  const launchToken = header(req, "x-axterm-launch");
5487
5930
  const session = sessionId === undefined ? undefined : this.sessions.get(sessionId);
5488
- if (sessionId === undefined || !session || session.launchToken !== launchToken)
5931
+ if (sessionId === undefined || !session || !this.launchMatches(session, launchToken)) {
5932
+ const why = sessionId === undefined ? "no session header" : !session ? "unknown session" : "launch token mismatch";
5933
+ const key = `${req.url}\x00${sessionId ?? ""}\x00${why}`;
5934
+ if (!this.refusedHooks.has(key)) {
5935
+ this.refusedHooks.add(key);
5936
+ log(`refused hook post to ${req.url}: ${why} (session ${sessionId ?? "-"}) \u2014 reported once per session`);
5937
+ }
5489
5938
  return;
5939
+ }
5490
5940
  return { sessionId, session };
5491
5941
  }
5492
5942
  dispatchHookPayloads(session, payloads, agent) {
@@ -5575,8 +6025,112 @@ var init_hook_host = __esm(() => {
5575
6025
  MAX_BODY_BYTES = 256 * 1024;
5576
6026
  });
5577
6027
 
6028
+ // packages/daemon/src/process-guards.ts
6029
+ class RejectionLogLimiter {
6030
+ detailLimit;
6031
+ windowMs;
6032
+ windowStartedAt;
6033
+ details = 0;
6034
+ noticeEmitted = false;
6035
+ constructor(detailLimit = DEFAULT_REJECTION_DETAILS, windowMs = DEFAULT_REJECTION_WINDOW_MS) {
6036
+ this.detailLimit = detailLimit;
6037
+ this.windowMs = windowMs;
6038
+ if (!Number.isInteger(detailLimit) || detailLimit < 1)
6039
+ throw new RangeError("detailLimit must be a positive integer");
6040
+ if (!Number.isFinite(windowMs) || windowMs <= 0)
6041
+ throw new RangeError("windowMs must be positive");
6042
+ }
6043
+ decide(now) {
6044
+ if (this.windowStartedAt === undefined || now - this.windowStartedAt >= this.windowMs || now < this.windowStartedAt) {
6045
+ this.windowStartedAt = now;
6046
+ this.details = 0;
6047
+ this.noticeEmitted = false;
6048
+ }
6049
+ if (this.details < this.detailLimit) {
6050
+ this.details++;
6051
+ return "detail";
6052
+ }
6053
+ if (!this.noticeEmitted) {
6054
+ this.noticeEmitted = true;
6055
+ return "notice";
6056
+ }
6057
+ return "drop";
6058
+ }
6059
+ }
6060
+ function thrownText(reason) {
6061
+ let text;
6062
+ if (reason instanceof Error) {
6063
+ text = reason.stack ?? reason.message;
6064
+ } else if (typeof reason === "string") {
6065
+ text = reason;
6066
+ } else {
6067
+ try {
6068
+ text = JSON.stringify(reason) ?? String(reason);
6069
+ } catch {
6070
+ text = String(reason);
6071
+ }
6072
+ }
6073
+ if (text.length <= MAX_BREADCRUMB_CHARS)
6074
+ return text;
6075
+ return `${text.slice(0, MAX_BREADCRUMB_CHARS)}\u2026 [truncated]`;
6076
+ }
6077
+
6078
+ class ProcessCrashGuards {
6079
+ options;
6080
+ limiter;
6081
+ now;
6082
+ fatal = false;
6083
+ uninstallCurrent;
6084
+ constructor(options) {
6085
+ this.options = options;
6086
+ this.now = options.now ?? Date.now;
6087
+ this.limiter = new RejectionLogLimiter(options.rejectionDetailsPerWindow, options.rejectionWindowMs);
6088
+ }
6089
+ install(events) {
6090
+ if (this.uninstallCurrent !== undefined)
6091
+ throw new Error("process crash guards are already installed");
6092
+ const uncaught = (reason) => this.fatalExit("uncaughtException", reason);
6093
+ const rejected = (reason) => this.unhandledRejection(reason);
6094
+ events.on("uncaughtException", uncaught);
6095
+ events.on("unhandledRejection", rejected);
6096
+ let installed = true;
6097
+ const uninstall = () => {
6098
+ if (!installed)
6099
+ return;
6100
+ installed = false;
6101
+ events.off("uncaughtException", uncaught);
6102
+ events.off("unhandledRejection", rejected);
6103
+ if (this.uninstallCurrent === uninstall)
6104
+ this.uninstallCurrent = undefined;
6105
+ };
6106
+ this.uninstallCurrent = uninstall;
6107
+ return uninstall;
6108
+ }
6109
+ startupFailed(reason) {
6110
+ this.fatalExit("startupFailure", reason);
6111
+ }
6112
+ unhandledRejection(reason) {
6113
+ if (this.fatal)
6114
+ return;
6115
+ const decision = this.limiter.decide(this.now());
6116
+ if (decision === "detail") {
6117
+ this.options.log(`unhandledRejection: ${thrownText(reason)}`);
6118
+ } else if (decision === "notice") {
6119
+ this.options.log("unhandledRejection: suppressing further unhandled rejections in this rate-limit window");
6120
+ }
6121
+ }
6122
+ fatalExit(origin, reason) {
6123
+ if (this.fatal)
6124
+ return;
6125
+ this.fatal = true;
6126
+ this.options.log(`${origin}: ${thrownText(reason)}`);
6127
+ this.options.exit(1);
6128
+ }
6129
+ }
6130
+ var DEFAULT_REJECTION_DETAILS = 20, DEFAULT_REJECTION_WINDOW_MS = 60000, MAX_BREADCRUMB_CHARS = 16384;
6131
+
5578
6132
  // packages/daemon/src/rpc-server.ts
5579
- import fs13 from "fs";
6133
+ import fs15 from "fs";
5580
6134
  import net2 from "net";
5581
6135
  function writeFrame(socket, frame) {
5582
6136
  if (!socket.destroyed)
@@ -5686,6 +6240,8 @@ function startRpcServer(options) {
5686
6240
  session.write(Buffer.from(p["dataB64"], "base64").toString("utf8"));
5687
6241
  return {};
5688
6242
  }
6243
+ case "session.choose":
6244
+ return { answered: manager.answerScreenChoice(p["id"], p["index"]) };
5689
6245
  case "attach": {
5690
6246
  const session = manager.resolveLive(p["id"]);
5691
6247
  cleanup();
@@ -5812,7 +6368,7 @@ function startRpcServer(options) {
5812
6368
  });
5813
6369
  if (process.platform !== "win32") {
5814
6370
  try {
5815
- fs13.unlinkSync(pipePath);
6371
+ fs15.unlinkSync(pipePath);
5816
6372
  } catch {}
5817
6373
  }
5818
6374
  server.listen(pipePath, () => {
@@ -5827,17 +6383,34 @@ var init_rpc_server = __esm(() => {
5827
6383
  });
5828
6384
 
5829
6385
  // packages/daemon/src/codex-home.ts
5830
- import fs14 from "fs";
5831
- import path14 from "path";
6386
+ import fs16 from "fs";
6387
+ import path16 from "path";
5832
6388
  function codexTrustKeyPath(home) {
5833
6389
  const resolved = (() => {
5834
6390
  try {
5835
- return fs14.realpathSync.native(home);
6391
+ return fs16.realpathSync.native(home);
5836
6392
  } catch {
5837
- return path14.resolve(home);
6393
+ return path16.resolve(home);
5838
6394
  }
5839
6395
  })();
5840
- return path14.join(resolved, "hooks.json");
6396
+ return path16.join(resolved, "hooks.json");
6397
+ }
6398
+ function referenceCredential(source, target, ops) {
6399
+ const symlink = ops?.symlink ?? ((s, t2) => fs16.symlinkSync(s, t2, "file"));
6400
+ const link = ops?.link ?? fs16.linkSync;
6401
+ try {
6402
+ symlink(source, target);
6403
+ return "symlink";
6404
+ } catch {
6405
+ try {
6406
+ link(source, target);
6407
+ return "hardlink";
6408
+ } catch {
6409
+ fs16.copyFileSync(source, target);
6410
+ fs16.chmodSync(target, 384);
6411
+ return "copy";
6412
+ }
6413
+ }
5841
6414
  }
5842
6415
  var init_codex_home = () => {};
5843
6416
 
@@ -5853,15 +6426,36 @@ function stripHostAttribution(env) {
5853
6426
  }
5854
6427
  var ATTRIBUTION_PREFIXES, ATTRIBUTION_KEEP;
5855
6428
  var init_host_env = __esm(() => {
5856
- ATTRIBUTION_PREFIXES = ["ORCA_"];
6429
+ ATTRIBUTION_PREFIXES = ["ORCA_", "AXTERM_"];
5857
6430
  ATTRIBUTION_KEEP = new Set([
5858
6431
  "ORCA_CODEX_HOME",
5859
6432
  "ORCA_OPENCODE_CONFIG_DIR",
5860
6433
  "ORCA_USER_DATA_PATH",
5861
- "ORCA_APP_VERSION"
6434
+ "ORCA_APP_VERSION",
6435
+ "AXTERM_CONFIG_DIR",
6436
+ "AXTERM_STATE_DIR",
6437
+ "AXTERM_RUNTIME_DIR",
6438
+ "AXTERM_LANG",
6439
+ "AXTERM_PTY_HOST",
6440
+ "AXTERM_PTY_HOST_MAIN"
5862
6441
  ]);
5863
6442
  });
5864
6443
 
6444
+ // packages/daemon/src/session-env.ts
6445
+ function sessionEnv(base, identity) {
6446
+ return {
6447
+ ...stripHostAttribution(base),
6448
+ AXTERM_SESSION_ID: identity.sessionId,
6449
+ AXTERM_LAUNCH_TOKEN: identity.launchToken,
6450
+ AXTERM_HOOK_PORT: String(identity.hookPort),
6451
+ AXTERM_HOOK_TOKEN: identity.hookToken,
6452
+ AXTERM_HOOK_ENDPOINT: identity.endpointFile
6453
+ };
6454
+ }
6455
+ var init_session_env = __esm(() => {
6456
+ init_host_env();
6457
+ });
6458
+
5865
6459
  // packages/daemon/src/git-porcelain.ts
5866
6460
  function parseWorktreeList(porcelain) {
5867
6461
  const entries = [];
@@ -5900,8 +6494,8 @@ function slugifyBranch(branch) {
5900
6494
  var MAX_SLUG_LENGTH = 60;
5901
6495
 
5902
6496
  // packages/daemon/src/git.ts
5903
- import fs15 from "fs";
5904
- import path15 from "path";
6497
+ import fs17 from "fs";
6498
+ import path17 from "path";
5905
6499
  async function git(ws, cwd, args) {
5906
6500
  const run = await ws.exec(["git", ...args], cwd);
5907
6501
  return {
@@ -5918,15 +6512,15 @@ async function gitOrThrow(ws, cwd, args) {
5918
6512
  return result.stdout;
5919
6513
  }
5920
6514
  function canonicalPath(target) {
5921
- let resolved = path15.resolve(target);
6515
+ let resolved = path17.resolve(target);
5922
6516
  try {
5923
- resolved = fs15.realpathSync.native(resolved);
6517
+ resolved = fs17.realpathSync.native(resolved);
5924
6518
  } catch {}
5925
6519
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
5926
6520
  }
5927
6521
  async function resolveRepoRoot(ws, cwd) {
5928
6522
  const result = await git(ws, cwd, ["rev-parse", "--show-toplevel"]);
5929
- return result.ok ? path15.resolve(result.stdout) : undefined;
6523
+ return result.ok ? path17.resolve(result.stdout) : undefined;
5930
6524
  }
5931
6525
  async function currentBranch(ws, cwd) {
5932
6526
  const result = await git(ws, cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
@@ -5935,8 +6529,8 @@ async function currentBranch(ws, cwd) {
5935
6529
  async function listWorktrees(ws, repoRoot) {
5936
6530
  return parseWorktreeList(await gitOrThrow(ws, repoRoot, ["worktree", "list", "--porcelain"]));
5937
6531
  }
5938
- function worktreePathFor(repoRoot, branch, root = path15.join(STATE_DIR, "worktrees")) {
5939
- return path15.join(root, slugifyBranch(path15.basename(repoRoot)), slugifyBranch(branch));
6532
+ function worktreePathFor(repoRoot, branch, root = path17.join(STATE_DIR, "worktrees")) {
6533
+ return path17.join(root, slugifyBranch(path17.basename(repoRoot)), slugifyBranch(branch));
5940
6534
  }
5941
6535
  async function createWorktree(ws, options) {
5942
6536
  const { repoRoot, branch, base } = options;
@@ -5944,7 +6538,7 @@ async function createWorktree(ws, options) {
5944
6538
  const existing = (await listWorktrees(ws, repoRoot)).find((w) => canonicalPath(w.path) === canonicalPath(target));
5945
6539
  if (existing)
5946
6540
  return { path: target, branch, createdBranch: false };
5947
- fs15.mkdirSync(path15.dirname(target), { recursive: true });
6541
+ fs17.mkdirSync(path17.dirname(target), { recursive: true });
5948
6542
  const exists = (await git(ws, repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`])).ok;
5949
6543
  const args = exists ? ["worktree", "add", target, branch] : ["worktree", "add", "-b", branch, target, ...base !== undefined ? [base] : []];
5950
6544
  await gitOrThrow(ws, repoRoot, args);
@@ -6005,7 +6599,7 @@ async function diffStat(ws, worktreePath, base) {
6005
6599
  const untracked = await untrackedFiles(ws, worktreePath);
6006
6600
  let newLines = 0;
6007
6601
  for (const file of untracked)
6008
- newLines += await countLines(ws, path15.join(worktreePath, file));
6602
+ newLines += await countLines(ws, path17.join(worktreePath, file));
6009
6603
  return {
6010
6604
  filesChanged: a.filesChanged + b.filesChanged + untracked.length,
6011
6605
  insertions: a.insertions + b.insertions + newLines,
@@ -6028,21 +6622,84 @@ async function changedFiles(ws, worktreePath, base, limit = 20) {
6028
6622
  names.add(file);
6029
6623
  return [...names].slice(0, limit);
6030
6624
  }
6031
- var UNTRACKED_LINE_SCAN_LIMIT;
6625
+ async function diffPatch(ws, worktreePath, base, maxChars = 64 * 1024) {
6626
+ const parts = [];
6627
+ let size = 0;
6628
+ const push = (text) => {
6629
+ if (size >= maxChars)
6630
+ return false;
6631
+ const room = maxChars - size;
6632
+ const piece = text.length > room ? text.slice(0, room) : text;
6633
+ parts.push(piece);
6634
+ size += piece.length;
6635
+ return piece.length === text.length;
6636
+ };
6637
+ for (const args of [
6638
+ ["diff", `${base}...HEAD`, "--", "."],
6639
+ ["diff", "HEAD"]
6640
+ ]) {
6641
+ const out = await git(ws, worktreePath, args);
6642
+ if (out.ok && out.stdout.trim().length > 0 && !push(`${out.stdout.replace(/\s+$/, "")}
6643
+ `)) {
6644
+ return parts.join("");
6645
+ }
6646
+ }
6647
+ for (const file of await untrackedFiles(ws, worktreePath)) {
6648
+ if (size >= maxChars)
6649
+ break;
6650
+ let text;
6651
+ try {
6652
+ const stat = await ws.stat(path17.join(worktreePath, file));
6653
+ if (stat?.kind !== "file" || stat.size > UNTRACKED_LINE_SCAN_LIMIT)
6654
+ continue;
6655
+ text = new TextDecoder().decode(await ws.read(path17.join(worktreePath, file)));
6656
+ } catch {
6657
+ continue;
6658
+ }
6659
+ if (text.slice(0, 8000).includes("\x00")) {
6660
+ if (!push(`diff --git a/${file} b/${file}
6661
+ Binary files /dev/null and b/${file} differ
6662
+ `))
6663
+ break;
6664
+ continue;
6665
+ }
6666
+ const lines = text.split(`
6667
+ `);
6668
+ if (lines.at(-1) === "")
6669
+ lines.pop();
6670
+ const shown = lines.slice(0, UNTRACKED_PATCH_LINES);
6671
+ const body = shown.map((line) => `+${line}`).join(`
6672
+ `);
6673
+ const elided = lines.length > shown.length ? `
6674
+ +\u2026 ${lines.length - shown.length} more lines
6675
+ ` : `
6676
+ `;
6677
+ const header2 = `diff --git a/${file} b/${file}
6678
+ new file mode 100644
6679
+ --- /dev/null
6680
+ +++ b/${file}
6681
+ @@ -0,0 +1,${lines.length} @@
6682
+ `;
6683
+ if (!push(`${header2}${body}${elided}`))
6684
+ break;
6685
+ }
6686
+ return parts.join("");
6687
+ }
6688
+ var UNTRACKED_LINE_SCAN_LIMIT, UNTRACKED_PATCH_LINES = 40;
6032
6689
  var init_git = __esm(() => {
6033
6690
  init_paths();
6034
6691
  UNTRACKED_LINE_SCAN_LIMIT = 1024 * 1024;
6035
6692
  });
6036
6693
 
6037
6694
  // packages/daemon/src/claude-store.ts
6038
- import fs16 from "fs";
6039
- import os4 from "os";
6040
- import path16 from "path";
6695
+ import fs18 from "fs";
6696
+ import os5 from "os";
6697
+ import path18 from "path";
6041
6698
  function claudeConfigDir(env = process.env) {
6042
6699
  const override = env["CLAUDE_CONFIG_DIR"];
6043
6700
  if (override !== undefined && override.length > 0)
6044
6701
  return override;
6045
- return path16.join(os4.homedir(), ".claude");
6702
+ return path18.join(os5.homedir(), ".claude");
6046
6703
  }
6047
6704
  function resumedConversationId(argv) {
6048
6705
  for (let i = 0;i < argv.length; i++) {
@@ -6058,29 +6715,65 @@ function resumedConversationId(argv) {
6058
6715
  return;
6059
6716
  }
6060
6717
  function claudeConversationExists(configDir, conversationId) {
6061
- const projects = path16.join(configDir, "projects");
6718
+ const projects = path18.join(configDir, "projects");
6062
6719
  let entries;
6063
6720
  try {
6064
- entries = fs16.readdirSync(projects);
6721
+ entries = fs18.readdirSync(projects);
6065
6722
  } catch {
6066
6723
  return;
6067
6724
  }
6068
6725
  const file = `${conversationId}.jsonl`;
6069
6726
  for (const entry of entries) {
6070
6727
  try {
6071
- if (fs16.existsSync(path16.join(projects, entry, file)))
6728
+ if (fs18.existsSync(path18.join(projects, entry, file)))
6072
6729
  return true;
6073
6730
  } catch {}
6074
6731
  }
6075
6732
  return false;
6076
6733
  }
6734
+ function claudeConversationCwd(configDir, conversationId) {
6735
+ const projects = path18.join(configDir, "projects");
6736
+ let entries;
6737
+ try {
6738
+ entries = fs18.readdirSync(projects);
6739
+ } catch {
6740
+ return;
6741
+ }
6742
+ for (const entry of entries) {
6743
+ const file = path18.join(projects, entry, `${conversationId}.jsonl`);
6744
+ let head;
6745
+ try {
6746
+ const handle = fs18.openSync(file, "r");
6747
+ try {
6748
+ const buffer = Buffer.alloc(64 * 1024);
6749
+ const read = fs18.readSync(handle, buffer, 0, buffer.byteLength, 0);
6750
+ head = buffer.subarray(0, read).toString("utf8");
6751
+ } finally {
6752
+ fs18.closeSync(handle);
6753
+ }
6754
+ } catch {
6755
+ continue;
6756
+ }
6757
+ for (const line of head.split(/\r?\n/)) {
6758
+ if (line.length === 0)
6759
+ continue;
6760
+ try {
6761
+ const record = JSON.parse(line);
6762
+ const cwd = record.cwd;
6763
+ if (typeof cwd === "string" && cwd.length > 0)
6764
+ return cwd;
6765
+ } catch {}
6766
+ }
6767
+ }
6768
+ return;
6769
+ }
6077
6770
  var CONVERSATION_ID;
6078
6771
  var init_claude_store = __esm(() => {
6079
6772
  CONVERSATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6080
6773
  });
6081
6774
 
6082
6775
  // packages/daemon/src/injector.ts
6083
- import { setTimeout as delay2 } from "timers/promises";
6776
+ import { setTimeout as delay3 } from "timers/promises";
6084
6777
 
6085
6778
  class Injector {
6086
6779
  queues = new Map;
@@ -6095,14 +6788,14 @@ class Injector {
6095
6788
  try {
6096
6789
  const deadline = Date.now() + 5000;
6097
6790
  while (!session.pasteReady && !session.isExited && Date.now() < deadline) {
6098
- await delay2(100);
6791
+ await delay3(100);
6099
6792
  }
6100
6793
  session.write(CTRL_U);
6101
- await delay2(CLEAR_SETTLE_MS);
6794
+ await delay3(CLEAR_SETTLE_MS);
6102
6795
  for (const chunk of iteratePasteChunks(text)) {
6103
6796
  session.write(chunk);
6104
6797
  }
6105
- await delay2(SUBMIT_DELAY_MS);
6798
+ await delay3(SUBMIT_DELAY_MS);
6106
6799
  session.write(SUBMIT);
6107
6800
  } catch (err) {
6108
6801
  session.write(BRACKETED_PASTE_END);
@@ -6114,7 +6807,7 @@ class Injector {
6114
6807
  interrupt(session) {
6115
6808
  return this.enqueue(session.info.id, async () => {
6116
6809
  session.write(ESC);
6117
- await delay2(500);
6810
+ await delay3(500);
6118
6811
  });
6119
6812
  }
6120
6813
  }
@@ -6194,6 +6887,7 @@ function connectPtyHost(options) {
6194
6887
  const decoder = new FrameDecoder;
6195
6888
  const ptys = new Map;
6196
6889
  const pending = new Map;
6890
+ const subscriptions = new Map;
6197
6891
  const dataListeners = [];
6198
6892
  const rawDecoders = new Map;
6199
6893
  let welcomed = false;
@@ -6207,6 +6901,9 @@ function connectPtyHost(options) {
6207
6901
  for (const waiter of pending.values())
6208
6902
  waiter.reject(error);
6209
6903
  pending.clear();
6904
+ for (const waiter of subscriptions.values())
6905
+ waiter.reject(error);
6906
+ subscriptions.clear();
6210
6907
  socket.destroy();
6211
6908
  };
6212
6909
  const timer = setTimeout(() => fail(new Error(`pty host did not greet within ${options.timeoutMs ?? 5000}ms`)), options.timeoutMs ?? 5000);
@@ -6273,6 +6970,15 @@ function connectPtyHost(options) {
6273
6970
  term: spawnOptions.term
6274
6971
  }));
6275
6972
  }),
6973
+ adopt: (session) => new Promise((done, failed) => {
6974
+ const pty = ptys.get(session.id) ?? new RemotePty(session.id, session.pid, send);
6975
+ ptys.set(session.id, pty);
6976
+ subscriptions.set(session.id, {
6977
+ resolve: () => done(pty),
6978
+ reject: failed
6979
+ });
6980
+ send(controlFrame({ t: "subscribe", id: session.id, replay: true }));
6981
+ }),
6276
6982
  onData: (listener) => dataListeners.push(listener),
6277
6983
  close: () => socket.destroy()
6278
6984
  });
@@ -6288,10 +6994,17 @@ function connectPtyHost(options) {
6288
6994
  pending.delete(message.id);
6289
6995
  continue;
6290
6996
  }
6997
+ if (message.t === "subscribed") {
6998
+ subscriptions.get(message.id)?.resolve();
6999
+ subscriptions.delete(message.id);
7000
+ continue;
7001
+ }
6291
7002
  if (message.t === "exit") {
6292
7003
  const pty = ptys.get(message.id);
6293
7004
  ptys.delete(message.id);
6294
7005
  rawDecoders.delete(message.id);
7006
+ subscriptions.get(message.id)?.reject(new Error(`session ${message.id} exited`));
7007
+ subscriptions.delete(message.id);
6295
7008
  pty?.finish(message.code);
6296
7009
  }
6297
7010
  }
@@ -6304,15 +7017,18 @@ var init_pty_host_client = __esm(() => {
6304
7017
 
6305
7018
  // packages/daemon/src/pty-host-launch.ts
6306
7019
  import { spawn as spawn3 } from "child_process";
6307
- import fs17 from "fs";
6308
- import os5 from "os";
6309
- import path17 from "path";
7020
+ import fs19 from "fs";
7021
+ import os6 from "os";
7022
+ import path19 from "path";
6310
7023
  function hostFile(runtimeDir) {
6311
- return path17.join(runtimeDir, "pty-host.json");
7024
+ return path19.join(runtimeDir, "pty-host.json");
7025
+ }
7026
+ function readPtyHostIn(runtimeDir) {
7027
+ return readPtyHostFile(hostFile(runtimeDir));
6312
7028
  }
6313
7029
  function readPtyHostFile(file) {
6314
7030
  try {
6315
- const parsed = JSON.parse(fs17.readFileSync(file, "utf8"));
7031
+ const parsed = JSON.parse(fs19.readFileSync(file, "utf8"));
6316
7032
  if (typeof parsed !== "object" || parsed === null)
6317
7033
  return;
6318
7034
  const { pid, endpoint, token, version: version2 } = parsed;
@@ -6332,8 +7048,8 @@ function isRunning(pid) {
6332
7048
  }
6333
7049
  }
6334
7050
  function hostEndpoint(runtimeDir) {
6335
- const base = ipcEndpoint(process.platform, os5.userInfo().username, { runtimeDir });
6336
- return process.platform === "win32" ? `${base}-pty` : path17.join(runtimeDir, "axterm-pty.sock");
7051
+ const base = ipcEndpoint(process.platform, os6.userInfo().username, { runtimeDir });
7052
+ return process.platform === "win32" ? `${base}-pty` : path19.join(runtimeDir, "axterm-pty.sock");
6337
7053
  }
6338
7054
  async function ensurePtyHost(location) {
6339
7055
  const file = hostFile(location.runtimeDir);
@@ -6344,7 +7060,7 @@ async function ensurePtyHost(location) {
6344
7060
  log(`pty host: handoff file names a dead pid ${existing.pid}, starting a new host`);
6345
7061
  const endpoint = hostEndpoint(location.runtimeDir);
6346
7062
  if (process.platform !== "win32")
6347
- fs17.rmSync(endpoint, { force: true });
7063
+ fs19.rmSync(endpoint, { force: true });
6348
7064
  const token = shortHash(`${location.runtimeDir}:${Date.now()}:${Math.random()}`) + shortHash(String(process.pid));
6349
7065
  const launch = ptyHostLaunch({ execPath: process.execPath, mainScript: location.mainScript ?? Bun.main });
6350
7066
  const child = spawn3(launch.command, launch.args, {
@@ -6373,10 +7089,41 @@ async function ensurePtyHost(location) {
6373
7089
  }
6374
7090
  }
6375
7091
  function writePtyHostFile(file, info) {
6376
- fs17.mkdirSync(path17.dirname(file), { recursive: true, mode: 448 });
6377
- fs17.writeFileSync(file, `${JSON.stringify(info, null, 2)}
6378
- `, { mode: 384 });
6379
- restrictFile(file);
7092
+ const dir = path19.dirname(file);
7093
+ const tmp = `${file}.tmp`;
7094
+ fs19.mkdirSync(dir, { recursive: true, mode: 448 });
7095
+ let handle;
7096
+ try {
7097
+ handle = fs19.openSync(tmp, "w", 384);
7098
+ fs19.writeFileSync(handle, `${JSON.stringify(info, null, 2)}
7099
+ `, "utf8");
7100
+ fs19.fsyncSync(handle);
7101
+ fs19.closeSync(handle);
7102
+ handle = undefined;
7103
+ restrictFile(tmp);
7104
+ fs19.renameSync(tmp, file);
7105
+ restrictFile(file);
7106
+ if (process.platform !== "win32") {
7107
+ try {
7108
+ const directory = fs19.openSync(dir, "r");
7109
+ try {
7110
+ fs19.fsyncSync(directory);
7111
+ } finally {
7112
+ fs19.closeSync(directory);
7113
+ }
7114
+ } catch {}
7115
+ }
7116
+ } catch (err) {
7117
+ if (handle !== undefined) {
7118
+ try {
7119
+ fs19.closeSync(handle);
7120
+ } catch {}
7121
+ }
7122
+ try {
7123
+ fs19.rmSync(tmp, { force: true });
7124
+ } catch {}
7125
+ throw err;
7126
+ }
6380
7127
  }
6381
7128
  var init_pty_host_launch = __esm(() => {
6382
7129
  init_src();
@@ -6385,8 +7132,8 @@ var init_pty_host_launch = __esm(() => {
6385
7132
  });
6386
7133
 
6387
7134
  // packages/daemon/src/pty.ts
6388
- import { spawnSync as spawnSync2 } from "child_process";
6389
- import path18 from "path";
7135
+ import { spawnSync as spawnSync3 } from "child_process";
7136
+ import path20 from "path";
6390
7137
  import { StringDecoder as StringDecoder2 } from "string_decoder";
6391
7138
 
6392
7139
  class Fanout {
@@ -6407,13 +7154,13 @@ class Fanout {
6407
7154
  }
6408
7155
  function killTree(pid, exeName) {
6409
7156
  if (process.platform === "win32") {
6410
- const check = spawnSync2("tasklist", ["/FI", `PID eq ${pid}`, "/NH", "/FO", "CSV"], {
7157
+ const check = spawnSync3("tasklist", ["/FI", `PID eq ${pid}`, "/NH", "/FO", "CSV"], {
6411
7158
  encoding: "utf8",
6412
7159
  windowsHide: true
6413
7160
  });
6414
7161
  const image = check.stdout?.split(",")[0]?.replace(/"/g, "").toLowerCase() ?? "";
6415
7162
  if (image === exeName) {
6416
- spawnSync2("taskkill", ["/PID", String(pid), "/T", "/F"], { encoding: "utf8", windowsHide: true });
7163
+ spawnSync3("taskkill", ["/PID", String(pid), "/T", "/F"], { encoding: "utf8", windowsHide: true });
6417
7164
  }
6418
7165
  return;
6419
7166
  }
@@ -6486,7 +7233,7 @@ function spawnPty(options) {
6486
7233
  }
6487
7234
  }
6488
7235
  });
6489
- handle = new BunPtyHandle(proc, path18.basename(options.exe).toLowerCase());
7236
+ handle = new BunPtyHandle(proc, path20.basename(options.exe).toLowerCase());
6490
7237
  proc.exited.then((code) => handle?.finish(code ?? null));
6491
7238
  return handle;
6492
7239
  }
@@ -6494,7 +7241,7 @@ var init_pty = () => {};
6494
7241
 
6495
7242
  // packages/daemon/src/occupancy.ts
6496
7243
  import { spawn as spawn4 } from "child_process";
6497
- import fs18 from "fs/promises";
7244
+ import fs20 from "fs/promises";
6498
7245
  function shellOccupied(shellPid) {
6499
7246
  return process.platform === "win32" ? occupiedFromSnapshot(shellPid) : occupiedFromPidProbe(shellPid);
6500
7247
  }
@@ -6539,11 +7286,11 @@ async function occupiedFromSnapshot(shellPid) {
6539
7286
  }
6540
7287
  async function childrenViaProc(pid) {
6541
7288
  try {
6542
- const children = await fs18.readFile(`/proc/${pid}/task/${pid}/children`, "utf8");
7289
+ const children = await fs20.readFile(`/proc/${pid}/task/${pid}/children`, "utf8");
6543
7290
  return children.trim().length > 0;
6544
7291
  } catch {
6545
7292
  try {
6546
- await fs18.access(`/proc/${pid}`);
7293
+ await fs20.access(`/proc/${pid}`);
6547
7294
  return;
6548
7295
  } catch {
6549
7296
  return false;
@@ -6607,20 +7354,20 @@ var init_occupancy = __esm(() => {
6607
7354
  });
6608
7355
 
6609
7356
  // packages/daemon/src/agent-store.ts
6610
- import fs19 from "fs";
6611
- import os6 from "os";
6612
- import path19 from "path";
7357
+ import fs21 from "fs";
7358
+ import os7 from "os";
7359
+ import path21 from "path";
6613
7360
  import { Database as Database3 } from "bun:sqlite";
6614
7361
  function readTailLines(file, maxBytes) {
6615
7362
  let fd;
6616
7363
  try {
6617
- fd = fs19.openSync(file, "r");
6618
- const size = fs19.fstatSync(fd).size;
7364
+ fd = fs21.openSync(file, "r");
7365
+ const size = fs21.fstatSync(fd).size;
6619
7366
  if (size === 0)
6620
7367
  return [];
6621
7368
  const length = Math.min(size, maxBytes);
6622
7369
  const buffer = Buffer.allocUnsafe(length);
6623
- fs19.readSync(fd, buffer, 0, length, size - length);
7370
+ fs21.readSync(fd, buffer, 0, length, size - length);
6624
7371
  const text = buffer.toString("utf8");
6625
7372
  const lines = text.split(`
6626
7373
  `);
@@ -6633,46 +7380,46 @@ function readTailLines(file, maxBytes) {
6633
7380
  return [];
6634
7381
  } finally {
6635
7382
  if (fd !== undefined)
6636
- fs19.closeSync(fd);
7383
+ fs21.closeSync(fd);
6637
7384
  }
6638
7385
  }
6639
7386
  function claudeConfigDir2() {
6640
- return process.env["CLAUDE_CONFIG_DIR"] ?? path19.join(os6.homedir(), ".claude");
7387
+ return process.env["CLAUDE_CONFIG_DIR"] ?? path21.join(os7.homedir(), ".claude");
6641
7388
  }
6642
7389
  function codexHome() {
6643
- return path19.join(STATE_DIR, "codex-home");
7390
+ return path21.join(STATE_DIR, "codex-home");
6644
7391
  }
6645
7392
  function ompAgentDb() {
6646
- return path19.join(os6.homedir(), ".omp", "agent", "agent.db");
7393
+ return path21.join(os7.homedir(), ".omp", "agent", "agent.db");
6647
7394
  }
6648
7395
  function findCodexRollout(sessionId) {
6649
- const root = path19.join(codexHome(), "sessions");
7396
+ const root = path21.join(codexHome(), "sessions");
6650
7397
  for (const shard2 of codexRolloutShards(sessionId)) {
6651
- const dir = path19.join(root, ...shard2.split("/"));
7398
+ const dir = path21.join(root, ...shard2.split("/"));
6652
7399
  let entries;
6653
7400
  try {
6654
- entries = fs19.readdirSync(dir);
7401
+ entries = fs21.readdirSync(dir);
6655
7402
  } catch {
6656
7403
  continue;
6657
7404
  }
6658
7405
  const match2 = entries.find((name) => name.endsWith(`-${sessionId}.jsonl`));
6659
7406
  if (match2 !== undefined)
6660
- return path19.join(dir, match2);
7407
+ return path21.join(dir, match2);
6661
7408
  }
6662
7409
  return;
6663
7410
  }
6664
7411
  function newestCodexRollout() {
6665
- const root = path19.join(codexHome(), "sessions");
7412
+ const root = path21.join(codexHome(), "sessions");
6666
7413
  let best;
6667
7414
  const walk = (dir, depth) => {
6668
7415
  let entries;
6669
7416
  try {
6670
- entries = fs19.readdirSync(dir, { withFileTypes: true });
7417
+ entries = fs21.readdirSync(dir, { withFileTypes: true });
6671
7418
  } catch {
6672
7419
  return;
6673
7420
  }
6674
7421
  for (const entry of entries) {
6675
- const full = path19.join(dir, entry.name);
7422
+ const full = path21.join(dir, entry.name);
6676
7423
  if (entry.isDirectory()) {
6677
7424
  if (depth < 3)
6678
7425
  walk(full, depth + 1);
@@ -6681,7 +7428,7 @@ function newestCodexRollout() {
6681
7428
  if (!entry.name.endsWith(".jsonl"))
6682
7429
  continue;
6683
7430
  try {
6684
- const { mtimeMs } = fs19.statSync(full);
7431
+ const { mtimeMs } = fs21.statSync(full);
6685
7432
  if (!best || mtimeMs > best.mtimeMs)
6686
7433
  best = { file: full, mtimeMs };
6687
7434
  } catch {}
@@ -6692,7 +7439,7 @@ function newestCodexRollout() {
6692
7439
  }
6693
7440
  function ompQuotaFacts() {
6694
7441
  const file = ompAgentDb();
6695
- if (!fs19.existsSync(file))
7442
+ if (!fs21.existsSync(file))
6696
7443
  return { agent: "omp", kind: "gauge" };
6697
7444
  let db;
6698
7445
  try {
@@ -6724,7 +7471,7 @@ function claudeQuotaFacts(sessions) {
6724
7471
  for (const session of sessions) {
6725
7472
  if (session.agentSessionId === undefined)
6726
7473
  continue;
6727
- const file = path19.join(dir, claudeTranscriptRelPath(session.cwd, session.agentSessionId));
7474
+ const file = path21.join(dir, claudeTranscriptRelPath(session.cwd, session.agentSessionId));
6728
7475
  const event = parseClaudeLimitEvent(readTailLines(file, QUOTA_TAIL_BYTES));
6729
7476
  if (event && (!newest || event.at > newest.at))
6730
7477
  newest = event;
@@ -6756,11 +7503,11 @@ function collectQuotaFacts(agents, sessions, onPath) {
6756
7503
  function storeQuietForMs(agent, cwd, agentSessionId, nowMs) {
6757
7504
  if (agentSessionId === undefined)
6758
7505
  return;
6759
- const file = agent === "claude" ? path19.join(claudeConfigDir2(), claudeTranscriptRelPath(cwd, agentSessionId)) : agent === "codex" ? findCodexRollout(agentSessionId) : undefined;
7506
+ const file = agent === "claude" ? path21.join(claudeConfigDir2(), claudeTranscriptRelPath(cwd, agentSessionId)) : agent === "codex" ? findCodexRollout(agentSessionId) : undefined;
6760
7507
  if (file === undefined)
6761
7508
  return;
6762
7509
  try {
6763
- return Math.max(0, nowMs - fs19.statSync(file).mtimeMs);
7510
+ return Math.max(0, nowMs - fs21.statSync(file).mtimeMs);
6764
7511
  } catch {
6765
7512
  return;
6766
7513
  }
@@ -6770,15 +7517,15 @@ function readFileRange(file, from, to) {
6770
7517
  return "";
6771
7518
  let fd;
6772
7519
  try {
6773
- fd = fs19.openSync(file, "r");
7520
+ fd = fs21.openSync(file, "r");
6774
7521
  const buffer = Buffer.allocUnsafe(to - from);
6775
- const read = fs19.readSync(fd, buffer, 0, to - from, from);
7522
+ const read = fs21.readSync(fd, buffer, 0, to - from, from);
6776
7523
  return buffer.subarray(0, read).toString("utf8");
6777
7524
  } catch {
6778
7525
  return "";
6779
7526
  } finally {
6780
7527
  if (fd !== undefined)
6781
- fs19.closeSync(fd);
7528
+ fs21.closeSync(fd);
6782
7529
  }
6783
7530
  }
6784
7531
  var QUOTA_TAIL_BYTES;
@@ -11229,15 +11976,55 @@ var require_addon_serialize = __commonJS((exports, module) => {
11229
11976
  })());
11230
11977
  });
11231
11978
 
11979
+ // packages/daemon/src/terminal-mirror.ts
11980
+ class TerminalMirror {
11981
+ scrollbackLines;
11982
+ term;
11983
+ serializeAddon;
11984
+ constructor(cols, rows, scrollbackLines) {
11985
+ this.scrollbackLines = scrollbackLines;
11986
+ this.term = new import_headless.Terminal({ cols, rows, scrollback: scrollbackLines, allowProposedApi: true });
11987
+ this.serializeAddon = new import_addon_serialize.SerializeAddon;
11988
+ this.term.loadAddon(this.serializeAddon);
11989
+ }
11990
+ write(data) {
11991
+ this.term.write(data);
11992
+ }
11993
+ resize(cols, rows) {
11994
+ this.term.resize(cols, rows);
11995
+ }
11996
+ serialize(maxChars = Number.POSITIVE_INFINITY) {
11997
+ const ansi = this.serializeAddon.serialize({ scrollback: this.scrollbackLines });
11998
+ return ansi.length > maxChars ? ansi.slice(-maxChars) : ansi;
11999
+ }
12000
+ text(maxLines) {
12001
+ const buffer = this.term.buffer.active;
12002
+ let last = buffer.length - 1;
12003
+ while (last >= 0 && (buffer.getLine(last)?.translateToString(true).trim() ?? "") === "")
12004
+ last--;
12005
+ const start = Math.max(0, last - maxLines + 1);
12006
+ const lines = [];
12007
+ for (let y = start;y <= last; y++) {
12008
+ lines.push(buffer.getLine(y)?.translateToString(true).trimEnd() ?? "");
12009
+ }
12010
+ return lines.join(`
12011
+ `);
12012
+ }
12013
+ }
12014
+ var import_headless, import_addon_serialize;
12015
+ var init_terminal_mirror = __esm(() => {
12016
+ import_headless = __toESM(require_xterm_headless(), 1);
12017
+ import_addon_serialize = __toESM(require_addon_serialize(), 1);
12018
+ });
12019
+
11232
12020
  // packages/daemon/src/pty-session.ts
11233
- import fs20 from "fs";
11234
- import path20 from "path";
12021
+ import fs22 from "fs";
12022
+ import path22 from "path";
11235
12023
 
11236
12024
  class PtySession {
11237
12025
  info;
11238
12026
  pty;
11239
- term;
11240
- serializeAddon;
12027
+ mirror;
11241
12028
  dataListeners = new Set;
11242
12029
  exitListeners = new Set;
11243
12030
  saveTimer;
@@ -11254,26 +12041,23 @@ class PtySession {
11254
12041
  cols: spawn5.cols,
11255
12042
  rows: spawn5.rows,
11256
12043
  env: spawn5.env,
11257
- term: "xterm-256color"
12044
+ term: "xterm-256color",
12045
+ id: info.id
11258
12046
  });
11259
12047
  return new PtySession(info, spawn5, pty);
11260
12048
  }
12049
+ static adopt(info, pty, cols, rows) {
12050
+ return new PtySession(info, { exe: "", argv: [], cwd: info.cwd, cols, rows, env: {} }, pty);
12051
+ }
11261
12052
  constructor(info, spawn5, pty) {
11262
12053
  this.info = info;
11263
- this.term = new import_headless.Terminal({
11264
- cols: spawn5.cols,
11265
- rows: spawn5.rows,
11266
- scrollback: SCROLLBACK_LINES,
11267
- allowProposedApi: true
11268
- });
11269
- this.serializeAddon = new import_addon_serialize.SerializeAddon;
11270
- this.term.loadAddon(this.serializeAddon);
12054
+ this.mirror = new TerminalMirror(spawn5.cols, spawn5.rows, SCROLLBACK_LINES);
11271
12055
  this.pty = pty;
11272
12056
  info.ptyPid = this.pty.pid;
11273
12057
  this.pty.onData((data) => {
11274
12058
  this.seq += 1;
11275
12059
  this.lastOutputAt = Date.now();
11276
- this.term.write(data);
12060
+ this.mirror.write(data);
11277
12061
  if (!this.pasteReady) {
11278
12062
  const window2 = this.readyScanTail + data;
11279
12063
  if (window2.includes("\x1B[?2004h"))
@@ -11318,27 +12102,16 @@ class PtySession {
11318
12102
  this.info.rows = rows;
11319
12103
  try {
11320
12104
  this.pty.resize(cols, rows);
11321
- this.term.resize(cols, rows);
12105
+ this.mirror.resize(cols, rows);
11322
12106
  } catch (err) {
11323
12107
  log(`resize failed for ${this.info.shortId}: ${String(err)}`);
11324
12108
  }
11325
12109
  }
11326
12110
  screenText(maxLines = 60) {
11327
- const buffer = this.term.buffer.active;
11328
- let last = buffer.length - 1;
11329
- while (last >= 0 && (buffer.getLine(last)?.translateToString(true).trim() ?? "") === "")
11330
- last--;
11331
- const start = Math.max(0, last - maxLines + 1);
11332
- const lines = [];
11333
- for (let y = start;y <= last; y++) {
11334
- lines.push(buffer.getLine(y)?.translateToString(true).trimEnd() ?? "");
11335
- }
11336
- return lines.join(`
11337
- `);
12111
+ return this.mirror.text(maxLines);
11338
12112
  }
11339
12113
  serializeBuffer() {
11340
- const ansi = this.serializeAddon.serialize({ scrollback: SCROLLBACK_LINES });
11341
- return ansi.length > SCROLLBACK_MAX_CHARS ? ansi.slice(-SCROLLBACK_MAX_CHARS) : ansi;
12114
+ return this.mirror.serialize(SCROLLBACK_MAX_CHARS);
11342
12115
  }
11343
12116
  scheduleSave() {
11344
12117
  if (this.exited || this.saveTimer)
@@ -11351,10 +12124,10 @@ class PtySession {
11351
12124
  }
11352
12125
  saveScrollback() {
11353
12126
  try {
11354
- const file = path20.join(SCROLLBACK_DIR, `${this.info.id}.bin`);
12127
+ const file = path22.join(SCROLLBACK_DIR, `${this.info.id}.bin`);
11355
12128
  const tmp = `${file}.tmp`;
11356
- fs20.writeFileSync(tmp, this.serializeBuffer(), "utf8");
11357
- fs20.renameSync(tmp, file);
12129
+ fs22.writeFileSync(tmp, this.serializeBuffer(), "utf8");
12130
+ fs22.renameSync(tmp, file);
11358
12131
  } catch (err) {
11359
12132
  log(`scrollback save failed for ${this.info.shortId}: ${String(err)}`);
11360
12133
  }
@@ -11365,20 +12138,20 @@ class PtySession {
11365
12138
  this.pty.kill();
11366
12139
  }
11367
12140
  }
11368
- var import_headless, import_addon_serialize, SCROLLBACK_LINES = 5000, SAVE_DEBOUNCE_MS = 5000, SCROLLBACK_MAX_CHARS;
12141
+ var SCROLLBACK_LINES = 5000, SAVE_DEBOUNCE_MS = 5000, SCROLLBACK_MAX_CHARS;
11369
12142
  var init_pty_session = __esm(() => {
11370
12143
  init_paths();
11371
12144
  init_pty();
11372
- import_headless = __toESM(require_xterm_headless(), 1);
11373
- import_addon_serialize = __toESM(require_addon_serialize(), 1);
12145
+ init_terminal_mirror();
11374
12146
  SCROLLBACK_MAX_CHARS = 2 * 1024 * 1024;
11375
12147
  });
11376
12148
 
11377
12149
  // packages/daemon/src/session-manager.ts
11378
12150
  import { randomBytes as randomBytes2, randomUUID } from "crypto";
11379
- import fs21 from "fs";
11380
- import os7 from "os";
11381
- import path21 from "path";
12151
+ import { spawn as spawn5 } from "child_process";
12152
+ import fs23 from "fs";
12153
+ import os8 from "os";
12154
+ import path23 from "path";
11382
12155
  function sessionScratchRoot(info) {
11383
12156
  return info.worktreePath ?? info.cwd;
11384
12157
  }
@@ -11420,11 +12193,23 @@ async function sendableNow(runtime, info, probe, now) {
11420
12193
  occupied
11421
12194
  }, now);
11422
12195
  }
12196
+ function pidIsRunning(pid) {
12197
+ try {
12198
+ process.kill(pid, 0);
12199
+ return true;
12200
+ } catch (err) {
12201
+ return err.code === "EPERM";
12202
+ }
12203
+ }
12204
+ function ptyHostEnabled(env) {
12205
+ return env["AXTERM_PTY_HOST"] !== "0";
12206
+ }
11423
12207
 
11424
12208
  class SessionManager {
11425
12209
  store;
11426
12210
  hooks;
11427
12211
  configuredDefaults;
12212
+ ptyHostPolicy;
11428
12213
  live = new Map;
11429
12214
  agentRuntime = new Map;
11430
12215
  injector = new Injector;
@@ -11432,18 +12217,66 @@ class SessionManager {
11432
12217
  eventListeners = new Set;
11433
12218
  rawHookListeners = new Set;
11434
12219
  workspace = new LocalWorkspace(MATERIALISED_DIR);
11435
- constructor(store, hooks, configuredDefaults = () => ({})) {
12220
+ constructor(store, hooks, configuredDefaults = () => ({}), ptyHostPolicy = {
12221
+ enabled: ptyHostEnabled(process.env),
12222
+ ...process.env["AXTERM_PTY_HOST_MAIN"] ? { mainScript: process.env["AXTERM_PTY_HOST_MAIN"] } : {}
12223
+ }) {
11436
12224
  this.store = store;
11437
12225
  this.hooks = hooks;
11438
12226
  this.configuredDefaults = configuredDefaults;
11439
- const stale = store.markStaleRunningAsExited();
11440
- if (stale > 0)
11441
- log(`boot: marked ${stale} stale session(s) as exited`);
12227
+ this.ptyHostPolicy = ptyHostPolicy;
11442
12228
  this.sessionWatch = setInterval(() => this.tickSessions(), SESSION_TICK_MS);
11443
12229
  this.sessionWatch.unref();
11444
12230
  }
12231
+ async recover() {
12232
+ const running = this.store.listSessions().filter((info) => info.state !== "exited");
12233
+ const held = await this.hostSessions();
12234
+ const adoptedIds = [];
12235
+ for (const info of running) {
12236
+ const description = held.get(info.id);
12237
+ if (description === undefined)
12238
+ continue;
12239
+ try {
12240
+ const handle = await this.ptyHost.adopt(description);
12241
+ info.state = "running";
12242
+ delete info.exitCode;
12243
+ delete info.exitedAt;
12244
+ const session = PtySession.adopt(info, handle, description.cols, description.rows);
12245
+ this.wire(session, undefined);
12246
+ this.hostOwned.add(info.id);
12247
+ this.store.markResumed(info.id, info.agentCommand, info.ptyPid);
12248
+ if (agentIdFromCommand(info.agentCommand) === "codex")
12249
+ this.adoptCodexAppServer(info.id);
12250
+ if (agentIdFromCommand(info.agentCommand) === "gemini")
12251
+ this.skipGeminiTelemetryHistory(info.id);
12252
+ adoptedIds.push(info.id);
12253
+ log(`boot: adopted session ${info.shortId} (pty ${description.pid}) from the terminal host`);
12254
+ } catch (err) {
12255
+ log(`boot: could not adopt ${info.shortId}: ${err instanceof Error ? err.message : String(err)}`);
12256
+ }
12257
+ }
12258
+ const sweptExited = this.store.markStaleRunningAsExited(adoptedIds);
12259
+ if (sweptExited > 0)
12260
+ log(`boot: marked ${sweptExited} stale session(s) as exited`);
12261
+ return { adopted: adoptedIds.length, sweptExited };
12262
+ }
12263
+ async hostSessions() {
12264
+ if (!this.ptyHostPolicy.enabled)
12265
+ return new Map;
12266
+ const existing = readPtyHostIn(RUNTIME_DIR);
12267
+ if (existing === undefined)
12268
+ return new Map;
12269
+ try {
12270
+ this.ptyHost ??= await connectPtyHost({ endpoint: existing.endpoint, token: existing.token });
12271
+ return new Map(this.ptyHost.sessions.map((s) => [s.id, s]));
12272
+ } catch (err) {
12273
+ log(`boot: the terminal host named in pty-host.json did not answer: ${err instanceof Error ? err.message : String(err)}`);
12274
+ return new Map;
12275
+ }
12276
+ }
11445
12277
  sessionWatch;
11446
12278
  ptyHost;
12279
+ hostOwned = new Set;
11447
12280
  ptyHostUnavailable = false;
11448
12281
  codexServers = new Map;
11449
12282
  workspaceFor(_sessionId) {
@@ -11517,13 +12350,14 @@ class SessionManager {
11517
12350
  return {
11518
12351
  stat: await diffStat(ws, target, compare),
11519
12352
  files: await changedFiles(ws, target, compare),
12353
+ patch: await diffPatch(ws, target, compare),
11520
12354
  base: compare,
11521
12355
  ...info.branch !== undefined ? { branch: info.branch } : {}
11522
12356
  };
11523
12357
  }
11524
12358
  async create(params) {
11525
- const requestedCwd = params.cwd ? path21.resolve(params.cwd) : os7.homedir();
11526
- if (!fs21.existsSync(requestedCwd)) {
12359
+ const requestedCwd = params.cwd ? path23.resolve(params.cwd) : os8.homedir();
12360
+ if (!fs23.existsSync(requestedCwd)) {
11527
12361
  throw new AxtermError("err.cwd.missing", { cwd: requestedCwd });
11528
12362
  }
11529
12363
  let worktree;
@@ -11567,11 +12401,27 @@ class SessionManager {
11567
12401
  const missing = unresolvableAgent(params.commandOverride, agentCommand, lookupOnPath);
11568
12402
  if (missing !== undefined)
11569
12403
  throw new AxtermError("err.agent.notInstalled", { command: missing });
12404
+ const adopted = agent === "claude" ? resumedConversationId(agentCommand) : undefined;
12405
+ let sessionCwd = cwd;
12406
+ if (adopted !== undefined) {
12407
+ const holder = conversationHeldBy(this.allSessions(), adopted);
12408
+ if (holder !== undefined) {
12409
+ throw new AxtermError("err.claude.conversationBusy", { id: adopted, session: holder.shortId });
12410
+ }
12411
+ if (worktree === undefined) {
12412
+ const recorded = claudeConversationCwd(claudeConfigDir(), adopted);
12413
+ sessionCwd = resolveSessionCwd({
12414
+ requested: cwd,
12415
+ preferConversation: params.preferConversationCwd === true,
12416
+ ...recorded !== undefined && fs23.existsSync(recorded) ? { conversationCwd: recorded } : {}
12417
+ });
12418
+ }
12419
+ }
11570
12420
  const info = {
11571
12421
  id,
11572
12422
  shortId: id.slice(0, 6),
11573
- name: params.name ?? worktree?.branch ?? path21.basename(cwd),
11574
- cwd,
12423
+ name: params.name ?? worktree?.branch ?? path23.basename(sessionCwd),
12424
+ cwd: sessionCwd,
11575
12425
  shell: shell.kind,
11576
12426
  agentCommand,
11577
12427
  state: "running",
@@ -11580,8 +12430,7 @@ class SessionManager {
11580
12430
  createdAt: Date.now(),
11581
12431
  ...worktree !== undefined ? { worktreePath: worktree.path, branch: worktree.branch, repoRoot: worktree.repoRoot } : {}
11582
12432
  };
11583
- const appServerUrl = agent === "codex" ? await this.startCodexAppServer(id, this.ensureCodexHome(), cwd) : undefined;
11584
- await this.launch(info, shell, agentCommand, appServerUrl);
12433
+ await this.launch(info, shell, agentCommand);
11585
12434
  this.store.insertSession(info);
11586
12435
  log(`session ${info.shortId} created shell=${shell.kind} cmd=${agentCommand.join(" ") || "(shell only)"}`);
11587
12436
  this.emit({ t: "session.created", info });
@@ -11595,7 +12444,7 @@ class SessionManager {
11595
12444
  launchToken
11596
12445
  });
11597
12446
  const file = this.claudeSettingsPath(id);
11598
- fs21.writeFileSync(file, JSON.stringify(settings, null, 2), { mode: 384 });
12447
+ fs23.writeFileSync(file, JSON.stringify(settings, null, 2), { mode: 384 });
11599
12448
  }
11600
12449
  writeGeminiSettings(sessionId) {
11601
12450
  try {
@@ -11608,69 +12457,104 @@ class SessionManager {
11608
12457
  });
11609
12458
  const file = this.geminiSettingsPath();
11610
12459
  const tmp = `${file}.tmp`;
11611
- fs21.writeFileSync(tmp, JSON.stringify(settings, null, 2));
11612
- fs21.renameSync(tmp, file);
12460
+ fs23.writeFileSync(tmp, JSON.stringify(settings, null, 2));
12461
+ fs23.renameSync(tmp, file);
11613
12462
  } catch (err) {
11614
12463
  log(`gemini settings write failed; session starts without hooks: ${String(err)}`);
11615
12464
  }
11616
12465
  }
11617
- async startCodexAppServer(id, codexHome2, cwd) {
12466
+ async startCodexAppServer(id, codexHome2, cwd, identity) {
11618
12467
  try {
12468
+ const inherited = this.adoptCodexAppServer(id);
12469
+ if (inherited !== undefined)
12470
+ return inherited;
11619
12471
  const port = await freeLoopbackPort();
11620
12472
  const url = `ws://127.0.0.1:${port}`;
11621
- const proc = Bun.spawn(["codex", "app-server", "--listen", url], {
12473
+ const child = spawn5("codex", ["app-server", "--listen", url], {
11622
12474
  cwd,
11623
- env: { ...stripHostAttribution(process.env), CODEX_HOME: codexHome2 },
11624
- stdout: "ignore",
11625
- stderr: "ignore",
11626
- stdin: "ignore"
12475
+ detached: true,
12476
+ windowsHide: true,
12477
+ stdio: "ignore",
12478
+ env: { ...sessionEnv(process.env, identity), CODEX_HOME: codexHome2 }
11627
12479
  });
12480
+ child.unref();
12481
+ const pid = child.pid;
12482
+ if (pid === undefined)
12483
+ throw new Error("the app-server was not given a pid");
11628
12484
  if (!await waitForPort(port)) {
11629
- proc.kill();
12485
+ this.killCodexAppServer(pid);
11630
12486
  log(`codex app-server did not open ${port}; session ${id.slice(0, 6)} keeps its hooks`);
11631
12487
  return;
11632
12488
  }
11633
- const client = new CodexAppServerClient(url, (payload) => this.ingestHookEvent(id, payload));
11634
- client.connect();
11635
- this.codexServers.set(id, {
11636
- stop: () => {
11637
- client.close();
11638
- try {
11639
- killTree(proc.pid, "codex.exe");
11640
- } catch {}
11641
- try {
11642
- proc.kill();
11643
- } catch {}
11644
- }
11645
- });
11646
- log(`codex app-server on ${url} for session ${id.slice(0, 6)} in ${cwd}`);
12489
+ this.watchCodexAppServer(id, url, pid);
12490
+ fs23.writeFileSync(this.codexAppServerFile(id), JSON.stringify({ pid, url }), { mode: 384 });
12491
+ log(`codex app-server on ${url} (pid ${pid}) for session ${id.slice(0, 6)} in ${cwd}`);
11647
12492
  return url;
11648
12493
  } catch (err) {
11649
12494
  log(`codex app-server unavailable (${String(err)}); session ${id.slice(0, 6)} keeps its hooks`);
11650
12495
  return;
11651
12496
  }
11652
12497
  }
12498
+ codexAppServerFile(id) {
12499
+ return path23.join(RUNTIME_DIR, `codex-app-${id}.json`);
12500
+ }
12501
+ adoptCodexAppServer(id) {
12502
+ const file = this.codexAppServerFile(id);
12503
+ let record;
12504
+ try {
12505
+ record = JSON.parse(fs23.readFileSync(file, "utf8"));
12506
+ } catch {
12507
+ return;
12508
+ }
12509
+ const pid = record.pid;
12510
+ const url = record.url;
12511
+ if (typeof pid !== "number" || typeof url !== "string")
12512
+ return;
12513
+ if (!pidIsRunning(pid)) {
12514
+ fs23.rmSync(file, { force: true });
12515
+ return;
12516
+ }
12517
+ this.watchCodexAppServer(id, url, pid);
12518
+ log(`codex app-server: reconnected to ${url} (pid ${pid}) for session ${id.slice(0, 6)}`);
12519
+ return url;
12520
+ }
12521
+ watchCodexAppServer(id, url, pid) {
12522
+ const client = new CodexAppServerClient(url, (payload) => this.ingestHookEvent(id, payload));
12523
+ client.connect();
12524
+ this.codexServers.set(id, {
12525
+ stop: () => {
12526
+ client.close();
12527
+ this.killCodexAppServer(pid);
12528
+ fs23.rmSync(this.codexAppServerFile(id), { force: true });
12529
+ }
12530
+ });
12531
+ }
12532
+ killCodexAppServer(pid) {
12533
+ try {
12534
+ killTree(pid, "codex.exe");
12535
+ } catch {}
12536
+ }
11653
12537
  stopCodexAppServer(id) {
11654
12538
  this.codexServers.get(id)?.stop();
11655
12539
  this.codexServers.delete(id);
11656
12540
  }
11657
12541
  ensureCodexHome() {
11658
- const home = path21.join(STATE_DIR, "codex-home");
11659
- fs21.mkdirSync(home, { recursive: true, mode: 448 });
12542
+ const home = path23.join(STATE_DIR, "codex-home");
12543
+ fs23.mkdirSync(home, { recursive: true, mode: 448 });
11660
12544
  this.linkCodexAuth(home);
11661
12545
  const bridge = hookLaunch({ execPath: process.execPath, mainScript: Bun.main, agent: "codex" });
11662
12546
  const hooksFile = buildCodexHooks({ command: bridge.command, args: bridge.args });
11663
12547
  const hooksPath = codexTrustKeyPath(home);
11664
12548
  const tmp = `${hooksPath}.tmp`;
11665
- fs21.writeFileSync(tmp, JSON.stringify(hooksFile, null, 2));
11666
- fs21.renameSync(tmp, hooksPath);
12549
+ fs23.writeFileSync(tmp, JSON.stringify(hooksFile, null, 2));
12550
+ fs23.renameSync(tmp, hooksPath);
11667
12551
  const entries = codexHookTrust(hooksPath, hooksFile);
11668
- const configPath = path21.join(home, "config.toml");
11669
- const existing = fs21.existsSync(configPath) ? fs21.readFileSync(configPath, "utf8") : "";
12552
+ const configPath = path23.join(home, "config.toml");
12553
+ const existing = fs23.existsSync(configPath) ? fs23.readFileSync(configPath, "utf8") : "";
11670
12554
  const missing = entries.filter((e) => !existing.includes(`"${e.key.replaceAll("\\", "\\\\")}"`));
11671
12555
  if (missing.length > 0) {
11672
12556
  const block = codexTrustStateToml(missing);
11673
- fs21.appendFileSync(configPath, existing !== "" && !existing.endsWith(`
12557
+ fs23.appendFileSync(configPath, existing !== "" && !existing.endsWith(`
11674
12558
  `) ? `
11675
12559
  ${block}` : block);
11676
12560
  }
@@ -11683,50 +12567,50 @@ ${block}` : block);
11683
12567
  return home;
11684
12568
  }
11685
12569
  linkCodexAuth(home) {
11686
- const target = path21.join(home, "auth.json");
11687
- if (fs21.existsSync(target))
12570
+ const target = path23.join(home, "auth.json");
12571
+ if (fs23.existsSync(target))
11688
12572
  return;
11689
- const operatorHome = process.env["CODEX_HOME"] ?? path21.join(os7.homedir(), ".codex");
11690
- const source = path21.join(operatorHome, "auth.json");
11691
- if (!fs21.existsSync(source))
12573
+ const operatorHome = process.env["CODEX_HOME"] ?? path23.join(os8.homedir(), ".codex");
12574
+ const source = path23.join(operatorHome, "auth.json");
12575
+ if (!fs23.existsSync(source))
11692
12576
  throw new AxtermError("err.codex.noAuth", { authFile: source });
12577
+ let how;
11693
12578
  try {
11694
- fs21.symlinkSync(source, target, "file");
11695
- } catch {
11696
- try {
11697
- fs21.linkSync(source, target);
11698
- } catch (err) {
11699
- throw new AxtermError("err.codex.authLink", { reason: err instanceof Error ? err.message : String(err) });
11700
- }
12579
+ how = referenceCredential(source, target);
12580
+ } catch (err) {
12581
+ throw new AxtermError("err.codex.authLink", { reason: err instanceof Error ? err.message : String(err) });
12582
+ }
12583
+ if (how === "copy") {
12584
+ log(`codex auth: neither a symlink nor a hard link to ${source} was possible (state dir on another volume?) \u2014 ` + `copied instead, so a token refresh will not follow; delete ${target} to re-reference it`);
11701
12585
  }
11702
12586
  }
11703
12587
  doctorReport() {
11704
12588
  const bridge = hookLaunch({ execPath: process.execPath, mainScript: Bun.main, agent: "codex" });
11705
- const home = path21.join(STATE_DIR, "codex-home");
11706
- const operatorHome = process.env["CODEX_HOME"] ?? path21.join(os7.homedir(), ".codex");
11707
- const authSource = path21.join(operatorHome, "auth.json");
12589
+ const home = path23.join(STATE_DIR, "codex-home");
12590
+ const operatorHome = process.env["CODEX_HOME"] ?? path23.join(os8.homedir(), ".codex");
12591
+ const authSource = path23.join(operatorHome, "auth.json");
11708
12592
  const geminiSettings = this.geminiSettingsPath();
11709
- const configPath = path21.join(home, "config.toml");
12593
+ const configPath = path23.join(home, "config.toml");
11710
12594
  const read = (file) => {
11711
12595
  try {
11712
- return fs21.existsSync(file) ? fs21.readFileSync(file, "utf8") : undefined;
12596
+ return fs23.existsSync(file) ? fs23.readFileSync(file, "utf8") : undefined;
11713
12597
  } catch {
11714
12598
  return;
11715
12599
  }
11716
12600
  };
11717
12601
  return [
11718
- ...evaluateBridgeDoctor({ command: bridge.command, exists: fs21.existsSync(bridge.command) }),
12602
+ ...evaluateBridgeDoctor({ command: bridge.command, exists: fs23.existsSync(bridge.command) }),
11719
12603
  ...evaluateOmpDoctor({
11720
12604
  binary: lookupOnPath("omp"),
11721
12605
  extensionPath: this.hooks.extensionPath,
11722
- extensionExists: fs21.existsSync(this.hooks.extensionPath)
12606
+ extensionExists: fs23.existsSync(this.hooks.extensionPath)
11723
12607
  }),
11724
12608
  ...evaluateClaudeDoctor({ binary: lookupOnPath("claude") }),
11725
12609
  ...evaluateCodexDoctor({
11726
12610
  binary: lookupOnPath("codex"),
11727
12611
  authSourcePath: authSource,
11728
- authSourceExists: fs21.existsSync(authSource),
11729
- homeExists: fs21.existsSync(home),
12612
+ authSourceExists: fs23.existsSync(authSource),
12613
+ homeExists: fs23.existsSync(home),
11730
12614
  expected: codexHookTrust(codexTrustKeyPath(home), buildCodexHooks({ command: bridge.command, args: bridge.args })),
11731
12615
  configToml: read(configPath)
11732
12616
  }),
@@ -11825,6 +12709,22 @@ ${block}` : block);
11825
12709
  session.write(digit);
11826
12710
  return true;
11827
12711
  }
12712
+ answerScreenChoice(idOrPrefix, optionIndex) {
12713
+ const session = this.resolveLive(idOrPrefix);
12714
+ const agent = agentIdFromCommand(session.info.agentCommand);
12715
+ const dialog = detectOnboarding(agent, session.screenText(30));
12716
+ const digit = dialog?.options[optionIndex]?.digit;
12717
+ if (digit === undefined) {
12718
+ const keys2 = menuChoiceKeys(agentProfile(agent), optionIndex);
12719
+ if (keys2 === undefined)
12720
+ throw new AxtermError("err.choose.noSafeKey", { agent, index: optionIndex });
12721
+ session.write(keys2);
12722
+ return "menu";
12723
+ }
12724
+ this.pendingPrompt.delete(session.info.id);
12725
+ session.write(digit);
12726
+ return "onboarding";
12727
+ }
11828
12728
  sinceLastCapture(id) {
11829
12729
  const at = this.lastCaptureAt.get(id);
11830
12730
  return at === undefined ? undefined : Date.now() - at;
@@ -11851,13 +12751,13 @@ ${block}` : block);
11851
12751
  }
11852
12752
  writeScreenFile(idOrPrefix, lines) {
11853
12753
  const session = this.resolveLive(idOrPrefix);
11854
- const file = path21.join(path21.dirname(this.hooks.endpointFile), `screen-${session.info.id}.txt`);
11855
- fs21.writeFileSync(file, session.screenText(lines), { mode: 384 });
12754
+ const file = path23.join(path23.dirname(this.hooks.endpointFile), `screen-${session.info.id}.txt`);
12755
+ fs23.writeFileSync(file, session.screenText(lines), { mode: 384 });
11856
12756
  return file;
11857
12757
  }
11858
12758
  removeScreenFile(id) {
11859
12759
  try {
11860
- fs21.rmSync(path21.join(path21.dirname(this.hooks.endpointFile), `screen-${id}.txt`), { force: true });
12760
+ fs23.rmSync(path23.join(path23.dirname(this.hooks.endpointFile), `screen-${id}.txt`), { force: true });
11861
12761
  } catch {}
11862
12762
  }
11863
12763
  async stageInboundFile(idOrPrefix, suggestedName, bytes) {
@@ -11866,20 +12766,20 @@ ${block}` : block);
11866
12766
  return this.stageInRuntimeDir(info, suggestedName, bytes);
11867
12767
  const ws = this.workspaceFor(info.id);
11868
12768
  const dir = sessionScratchRoot(info);
11869
- const file = path21.resolve(planMaterialisedPath(inboundStagingDir(dir), info.id, suggestedName));
12769
+ const file = path23.resolve(planMaterialisedPath(inboundStagingDir(dir), info.id, suggestedName));
11870
12770
  await this.prepareScratchDir(ws, dir, file);
11871
12771
  await ws.write(file, bytes, 384);
11872
- log(`session ${info.shortId} staged ${path21.basename(file)} (${bytes.byteLength} bytes)`);
12772
+ log(`session ${info.shortId} staged ${path23.basename(file)} (${bytes.byteLength} bytes)`);
11873
12773
  return file;
11874
12774
  }
11875
12775
  stagingMode() {
11876
12776
  return this.configuredDefaults().staging ?? "project";
11877
12777
  }
11878
12778
  async stageInRuntimeDir(info, suggestedName, bytes) {
11879
- const file = path21.resolve(planMaterialisedPath(MATERIALISED_DIR, info.id, suggestedName));
11880
- await fs21.promises.mkdir(path21.dirname(file), { recursive: true, mode: 448 });
11881
- await fs21.promises.writeFile(file, bytes, { mode: 384 });
11882
- log(`session ${info.shortId} staged ${path21.basename(file)} (${bytes.byteLength} bytes, runtime dir)`);
12779
+ const file = path23.resolve(planMaterialisedPath(MATERIALISED_DIR, info.id, suggestedName));
12780
+ await fs23.promises.mkdir(path23.dirname(file), { recursive: true, mode: 448 });
12781
+ await fs23.promises.writeFile(file, bytes, { mode: 384 });
12782
+ log(`session ${info.shortId} staged ${path23.basename(file)} (${bytes.byteLength} bytes, runtime dir)`);
11883
12783
  return file;
11884
12784
  }
11885
12785
  async prepareScratchDir(ws, sessionDir, file) {
@@ -11896,47 +12796,47 @@ ${block}` : block);
11896
12796
  throw new AxtermError("err.staging.notIgnored", { dir: scratch, file: SCRATCH_IGNORE_NAME });
11897
12797
  }
11898
12798
  try {
11899
- await ws.mkdirp(path21.dirname(file), 448);
12799
+ await ws.mkdirp(path23.dirname(file), 448);
11900
12800
  } catch (err) {
11901
12801
  throw new AxtermError("err.staging.dir", { dir: scratch, error: err instanceof Error ? err.message : String(err) });
11902
12802
  }
11903
12803
  }
11904
12804
  removeStagedFiles(id, info) {
11905
12805
  try {
11906
- fs21.rmSync(path21.join(MATERIALISED_DIR, id), { recursive: true, force: true });
12806
+ fs23.rmSync(path23.join(MATERIALISED_DIR, id), { recursive: true, force: true });
11907
12807
  } catch {}
11908
12808
  if (info === undefined)
11909
12809
  return;
11910
- const scratch = path21.resolve(scratchDir(sessionScratchRoot(info)));
12810
+ const scratch = path23.resolve(scratchDir(sessionScratchRoot(info)));
11911
12811
  try {
11912
- fs21.rmSync(path21.resolve(inboundStagingDir(sessionScratchRoot(info)), id), { recursive: true, force: true });
11913
- fs21.rmdirSync(path21.join(scratch, "inbound"));
11914
- fs21.rmSync(path21.join(scratch, SCRATCH_IGNORE_NAME), { force: true });
11915
- fs21.rmdirSync(scratch);
12812
+ fs23.rmSync(path23.resolve(inboundStagingDir(sessionScratchRoot(info)), id), { recursive: true, force: true });
12813
+ fs23.rmdirSync(path23.join(scratch, "inbound"));
12814
+ fs23.rmSync(path23.join(scratch, SCRATCH_IGNORE_NAME), { force: true });
12815
+ fs23.rmdirSync(scratch);
11916
12816
  } catch {}
11917
12817
  }
11918
12818
  removeClaudeSettings(id) {
11919
12819
  try {
11920
- fs21.rmSync(this.claudeSettingsPath(id), { force: true });
11921
- fs21.rmSync(this.mcpConfigPath(id), { force: true });
11922
- fs21.rmSync(this.mcpContextPath(id), { force: true });
11923
- fs21.rmSync(this.geminiTelemetryPath(id), { force: true });
12820
+ fs23.rmSync(this.claudeSettingsPath(id), { force: true });
12821
+ fs23.rmSync(this.mcpConfigPath(id), { force: true });
12822
+ fs23.rmSync(this.mcpContextPath(id), { force: true });
12823
+ fs23.rmSync(this.geminiTelemetryPath(id), { force: true });
11924
12824
  this.stopCodexAppServer(id);
11925
12825
  } catch {}
11926
12826
  }
11927
12827
  claudeSettingsPath(id) {
11928
- return path21.join(path21.dirname(this.hooks.endpointFile), `claude-${id}.json`);
12828
+ return path23.join(path23.dirname(this.hooks.endpointFile), `claude-${id}.json`);
11929
12829
  }
11930
12830
  mcpConfigPath(id) {
11931
- return path21.join(path21.dirname(this.hooks.endpointFile), `mcp-${id}.json`);
12831
+ return path23.join(path23.dirname(this.hooks.endpointFile), `mcp-${id}.json`);
11932
12832
  }
11933
12833
  mcpContextPath(id) {
11934
- return path21.join(path21.dirname(this.hooks.endpointFile), `mcp-ctx-${id}.json`);
12834
+ return path23.join(path23.dirname(this.hooks.endpointFile), `mcp-ctx-${id}.json`);
11935
12835
  }
11936
12836
  writeMcpContext(id, launchToken) {
11937
12837
  const file = this.mcpContextPath(id);
11938
12838
  try {
11939
- fs21.writeFileSync(file, JSON.stringify({ port: this.hooks.port, token: this.hooks.host.token, sessionId: id, launchToken }), { mode: 384 });
12839
+ fs23.writeFileSync(file, JSON.stringify({ port: this.hooks.port, token: this.hooks.host.token, sessionId: id, launchToken }), { mode: 384 });
11940
12840
  return file;
11941
12841
  } catch (err) {
11942
12842
  log(`mcp context write failed; session starts without axterm's tools: ${String(err)}`);
@@ -11949,7 +12849,7 @@ ${block}` : block);
11949
12849
  writeMcpConfig(id, contextPath) {
11950
12850
  const file = this.mcpConfigPath(id);
11951
12851
  try {
11952
- fs21.writeFileSync(file, claudeMcpConfig(this.mcpServerLaunch(), contextPath), { mode: 384 });
12852
+ fs23.writeFileSync(file, claudeMcpConfig(this.mcpServerLaunch(), contextPath), { mode: 384 });
11953
12853
  return file;
11954
12854
  } catch (err) {
11955
12855
  log(`mcp config write failed; session starts without axterm's tools: ${String(err)}`);
@@ -11957,27 +12857,29 @@ ${block}` : block);
11957
12857
  }
11958
12858
  }
11959
12859
  geminiTelemetryPath(id) {
11960
- return path21.join(path21.dirname(this.hooks.endpointFile), `gemini-telemetry-${id}.log`);
12860
+ return path23.join(path23.dirname(this.hooks.endpointFile), `gemini-telemetry-${id}.log`);
11961
12861
  }
11962
12862
  geminiSettingsPath() {
11963
- return path21.join(path21.dirname(this.hooks.endpointFile), "gemini-defaults.json");
12863
+ return path23.join(path23.dirname(this.hooks.endpointFile), "gemini-defaults.json");
11964
12864
  }
11965
12865
  async ptySpawner() {
11966
- if (process.env["AXTERM_PTY_HOST"] !== "1" || this.ptyHostUnavailable)
11967
- return spawnPty;
12866
+ if (!this.ptyHostPolicy.enabled || this.ptyHostUnavailable)
12867
+ return { spawn: spawnPty, hostOwned: false };
11968
12868
  try {
11969
12869
  if (this.ptyHost === undefined) {
11970
- const override = process.env["AXTERM_PTY_HOST_MAIN"];
11971
- const info = await ensurePtyHost({ runtimeDir: RUNTIME_DIR, ...override ? { mainScript: override } : {} });
12870
+ const info = await ensurePtyHost({
12871
+ runtimeDir: RUNTIME_DIR,
12872
+ ...this.ptyHostPolicy.mainScript ? { mainScript: this.ptyHostPolicy.mainScript } : {}
12873
+ });
11972
12874
  this.ptyHost = await connectPtyHost({ endpoint: info.endpoint, token: info.token });
11973
12875
  log(`pty host: connected (pid ${info.pid})`);
11974
12876
  }
11975
12877
  const host = this.ptyHost;
11976
- return (options) => host.spawn(options);
12878
+ return { spawn: (options) => host.spawn(options), hostOwned: true };
11977
12879
  } catch (err) {
11978
12880
  this.ptyHostUnavailable = true;
11979
12881
  log(`pty host unavailable, owning ptys in this process: ${err instanceof Error ? err.message : String(err)}`);
11980
- return spawnPty;
12882
+ return { spawn: spawnPty, hostOwned: false };
11981
12883
  }
11982
12884
  }
11983
12885
  requireClaudeConversation(agentCommand) {
@@ -11988,7 +12890,7 @@ ${block}` : block);
11988
12890
  throw new AxtermError("err.claude.noConversation", { id: conversation });
11989
12891
  }
11990
12892
  }
11991
- async launch(info, shell, agentCommand, appServerUrl) {
12893
+ async launch(info, shell, agentCommand) {
11992
12894
  const id = info.id;
11993
12895
  const launchToken = randomBytes2(16).toString("hex");
11994
12896
  const agentId = agentIdFromCommand(info.agentCommand);
@@ -11996,11 +12898,22 @@ ${block}` : block);
11996
12898
  this.writeClaudeSettings(id, launchToken);
11997
12899
  if (agentId !== "omp")
11998
12900
  this.writeMcpContext(id, launchToken);
12901
+ if (agentId === "claude")
12902
+ this.writeMcpConfig(id, this.mcpContextPath(id));
11999
12903
  if (agentId === "gemini")
12000
12904
  this.writeGeminiSettings(id);
12001
12905
  const codexHome2 = agentId === "codex" ? this.ensureCodexHome() : undefined;
12002
12906
  if (agentId === "claude")
12003
12907
  this.requireClaudeConversation(agentCommand);
12908
+ const identity = {
12909
+ sessionId: id,
12910
+ launchToken,
12911
+ hookPort: this.hooks.port,
12912
+ hookToken: this.hooks.host.token,
12913
+ endpointFile: this.hooks.endpointFile
12914
+ };
12915
+ const appServerUrl = codexHome2 === undefined ? undefined : await this.startCodexAppServer(id, codexHome2, info.cwd, identity);
12916
+ const spawner = await this.ptySpawner();
12004
12917
  const session = await PtySession.open(info, {
12005
12918
  exe: shell.exe,
12006
12919
  argv: buildShellArgv(shell, withAppServer(agentCommand, appServerUrl)),
@@ -12008,12 +12921,7 @@ ${block}` : block);
12008
12921
  cols: info.cols,
12009
12922
  rows: info.rows,
12010
12923
  env: {
12011
- ...stripHostAttribution(process.env),
12012
- AXTERM_SESSION_ID: id,
12013
- AXTERM_LAUNCH_TOKEN: launchToken,
12014
- AXTERM_HOOK_PORT: String(this.hooks.port),
12015
- AXTERM_HOOK_TOKEN: this.hooks.host.token,
12016
- AXTERM_HOOK_ENDPOINT: this.hooks.endpointFile,
12924
+ ...sessionEnv(process.env, identity),
12017
12925
  ...agentId === "gemini" ? {
12018
12926
  GEMINI_CLI_SYSTEM_DEFAULTS_PATH: this.geminiSettingsPath(),
12019
12927
  GEMINI_TELEMETRY_ENABLED: "true",
@@ -12024,7 +12932,15 @@ ${block}` : block);
12024
12932
  } : {},
12025
12933
  ...codexHome2 !== undefined ? { CODEX_HOME: codexHome2 } : {}
12026
12934
  }
12027
- }, await this.ptySpawner());
12935
+ }, spawner.spawn);
12936
+ if (spawner.hostOwned)
12937
+ this.hostOwned.add(id);
12938
+ this.wire(session, launchToken);
12939
+ return session;
12940
+ }
12941
+ wire(session, launchToken) {
12942
+ const id = session.info.id;
12943
+ const info = session.info;
12028
12944
  this.agentRuntime.set(id, { agentExited: false, choiceSeq: 0, dialogOpen: false });
12029
12945
  this.hooks.host.registerSession(id, launchToken, (payload) => this.ingestHookEvent(id, payload));
12030
12946
  session.onData(() => {
@@ -12051,11 +12967,11 @@ ${block}` : block);
12051
12967
  this.pendingPrompt.delete(id);
12052
12968
  this.live.delete(id);
12053
12969
  this.agentRuntime.delete(id);
12970
+ this.hostOwned.delete(id);
12054
12971
  log(`session ${info.shortId} exited code=${code}`);
12055
12972
  this.emit({ t: "session.exit", sessionId: id, code });
12056
12973
  });
12057
12974
  this.live.set(id, session);
12058
- return session;
12059
12975
  }
12060
12976
  emitActivity(id, payload) {
12061
12977
  const dropped = payload.dropped;
@@ -12119,7 +13035,7 @@ ${block}` : block);
12119
13035
  this.store.updateAgentState(id, result.entry.state, "hook", now);
12120
13036
  log(`session ${info.shortId} agent ${runtime.entry?.state ?? "-"} -> ${result.entry.state}`);
12121
13037
  this.emit({ t: "agent.status", sessionId: id, state: result.entry.state, toolName: result.entry.toolName });
12122
- if (result.entry.state === "blocked" && agentProfile(agentIdFromCommand(info.agentCommand)).structuredApprovals !== true) {
13038
+ if (offersKeystrokeApproval(agentProfile(agentIdFromCommand(info.agentCommand)), result.entry.state)) {
12123
13039
  runtime.choiceSeq += 1;
12124
13040
  this.emit({
12125
13041
  t: "choice.request",
@@ -12226,38 +13142,14 @@ ${block}` : block);
12226
13142
  allSessions() {
12227
13143
  return sessionsByRecency([...this.live.values()].map((s) => s.info), this.store.listSessions());
12228
13144
  }
12229
- async resumeNamed(stored, agent) {
12230
- const mcpConfigPath = agent === "claude" ? this.writeMcpConfig(stored.id, this.mcpContextPath(stored.id)) : undefined;
12231
- const agentCommand = buildAgentResumeCommand({
12232
- agent,
12233
- sessionId: stored.id,
12234
- extensionPath: this.hooks.extensionPath,
12235
- claudeSettingsPath: this.claudeSettingsPath(stored.id),
12236
- ...mcpConfigPath === undefined ? {} : { mcpConfigPath },
12237
- storedArgv: stored.agentCommand
12238
- });
12239
- const info = { ...stored, agentCommand, state: "running" };
12240
- delete info.exitCode;
12241
- delete info.exitedAt;
12242
- delete info.agentState;
12243
- delete info.agentStateSource;
12244
- await this.launch(info, resolveShell(info.shell), agentCommand);
12245
- this.store.markResumed(info.id, agentCommand, info.ptyPid);
12246
- log(`session ${info.shortId} resumed cmd=${agentCommand.join(" ")}`);
12247
- this.emit({ t: "session.created", info });
12248
- return info;
12249
- }
12250
13145
  async resume(idOrPrefix) {
12251
13146
  const stored = this.store.findByPrefix(idOrPrefix);
12252
13147
  if (!stored)
12253
13148
  throw new AxtermError("err.session.unknown", { id: idOrPrefix });
12254
13149
  if (this.live.has(stored.id))
12255
13150
  throw new AxtermError("err.session.running", { id: stored.shortId });
12256
- const storedAgent = agentIdFromCommand(stored.agentCommand);
12257
- if (storedAgent === "claude" || storedAgent === "gemini")
12258
- return this.resumeNamed(stored, storedAgent);
12259
13151
  const profile = agentProfile(agentIdFromCommand(stored.agentCommand));
12260
- const reference = resumeReference(profile, stored);
13152
+ const reference = resumeReference(profile, { ...stored, ownSessionId: stored.id });
12261
13153
  if (reference === undefined) {
12262
13154
  throw new AxtermError("err.session.noConversation", {
12263
13155
  id: stored.shortId,
@@ -12268,7 +13160,7 @@ ${block}` : block);
12268
13160
  if (agentCommand === undefined) {
12269
13161
  throw new AxtermError("err.session.noResumeSupport", { agent: profile.displayName });
12270
13162
  }
12271
- if (!fs21.existsSync(stored.cwd))
13163
+ if (!fs23.existsSync(stored.cwd))
12272
13164
  throw new AxtermError("err.session.cwdGone", { cwd: stored.cwd });
12273
13165
  const info = { ...stored, agentCommand, state: "running" };
12274
13166
  delete info.exitCode;
@@ -12276,8 +13168,7 @@ ${block}` : block);
12276
13168
  delete info.agentState;
12277
13169
  delete info.agentStateSource;
12278
13170
  const shell = resolveShell(info.shell);
12279
- const resumedAppServer = agentIdFromCommand(agentCommand) === "codex" ? await this.startCodexAppServer(info.id, this.ensureCodexHome(), info.cwd) : undefined;
12280
- await this.launch(info, shell, agentCommand, resumedAppServer);
13171
+ await this.launch(info, shell, agentCommand);
12281
13172
  this.store.markResumed(info.id, agentCommand, info.ptyPid);
12282
13173
  log(`session ${info.shortId} resumed cmd=${agentCommand.join(" ")}`);
12283
13174
  this.emit({ t: "session.created", info });
@@ -12343,7 +13234,7 @@ ${block}` : block);
12343
13234
  removeScrollback(id) {
12344
13235
  for (const file of [`${id}.bin`, `${id}.bin.tmp`]) {
12345
13236
  try {
12346
- fs21.rmSync(path21.join(SCROLLBACK_DIR, file), { force: true });
13237
+ fs23.rmSync(path23.join(SCROLLBACK_DIR, file), { force: true });
12347
13238
  } catch {}
12348
13239
  }
12349
13240
  }
@@ -12351,6 +13242,14 @@ ${block}` : block);
12351
13242
  this.drainGeminiTelemetry();
12352
13243
  this.watchForStalls();
12353
13244
  }
13245
+ skipGeminiTelemetryHistory(id) {
13246
+ const runtime = this.agentRuntime.get(id);
13247
+ if (runtime === undefined)
13248
+ return;
13249
+ try {
13250
+ runtime.telemetryOffset = fs23.statSync(this.geminiTelemetryPath(id)).size;
13251
+ } catch {}
13252
+ }
12354
13253
  drainGeminiTelemetry() {
12355
13254
  for (const [id, session] of this.live) {
12356
13255
  if (agentIdFromCommand(session.info.agentCommand) !== "gemini")
@@ -12361,7 +13260,7 @@ ${block}` : block);
12361
13260
  const file = this.geminiTelemetryPath(id);
12362
13261
  let size = 0;
12363
13262
  try {
12364
- size = fs21.statSync(file).size;
13263
+ size = fs23.statSync(file).size;
12365
13264
  } catch {
12366
13265
  continue;
12367
13266
  }
@@ -12423,10 +13322,17 @@ ${block}` : block);
12423
13322
  clearInterval(this.sessionWatch);
12424
13323
  for (const id of [...this.codexServers.keys()])
12425
13324
  this.stopCodexAppServer(id);
12426
- for (const session of this.live.values()) {
13325
+ let handedOn = 0;
13326
+ for (const [id, session] of this.live) {
12427
13327
  session.saveScrollback();
13328
+ if (this.hostOwned.has(id)) {
13329
+ handedOn += 1;
13330
+ continue;
13331
+ }
12428
13332
  session.kill();
12429
13333
  }
13334
+ if (handedOn > 0)
13335
+ log(`left ${handedOn} session(s) running in the terminal host for the next daemon`);
12430
13336
  this.live.clear();
12431
13337
  this.agentRuntime.clear();
12432
13338
  }
@@ -12435,7 +13341,7 @@ var DEFAULT_COLS = 120, DEFAULT_ROWS = 30, MATERIALISED_DIR, ACTIVITY_ARG_CHARS
12435
13341
  var init_session_manager = __esm(() => {
12436
13342
  init_src();
12437
13343
  init_codex_home();
12438
- init_host_env();
13344
+ init_session_env();
12439
13345
  init_git();
12440
13346
  init_claude_store();
12441
13347
  init_injector();
@@ -12450,7 +13356,7 @@ var init_session_manager = __esm(() => {
12450
13356
  init_pty_session();
12451
13357
  init_shell();
12452
13358
  init_workspace_local();
12453
- MATERIALISED_DIR = path21.join(RUNTIME_DIR, "materialised");
13359
+ MATERIALISED_DIR = path23.join(RUNTIME_DIR, "materialised");
12454
13360
  PROMPT_OPTIONS = {
12455
13361
  hostKey: [{ label: "yes" }, { label: "no" }],
12456
13362
  password: [],
@@ -12481,6 +13387,9 @@ class FetchTelegramApi {
12481
13387
  throw new Error(`telegram file download failed: ${res.status}`);
12482
13388
  return new Uint8Array(await res.arrayBuffer());
12483
13389
  }
13390
+ async deleteMessage(chatId, messageId) {
13391
+ await this.call("deleteMessage", { chat_id: chatId, message_id: messageId });
13392
+ }
12484
13393
  async call(method, params = {}) {
12485
13394
  const attempt = async () => {
12486
13395
  const res = await fetch(`https://api.telegram.org/bot${this.botToken}/${method}`, {
@@ -12599,11 +13508,11 @@ var init_acknowledge = __esm(() => {
12599
13508
  });
12600
13509
 
12601
13510
  // packages/channel-telegram/src/attach.ts
12602
- import fs22 from "fs";
12603
- import path22 from "path";
13511
+ import fs24 from "fs";
13512
+ import path24 from "path";
12604
13513
  function buildAttachmentUpload(filePath, chatId, topicId, caption, limits = TELEGRAM_LIMITS) {
12605
- const name = path22.basename(filePath);
12606
- const bytes = fs22.statSync(filePath).size;
13514
+ const name = path24.basename(filePath);
13515
+ const bytes = fs24.statSync(filePath).size;
12607
13516
  const plan = planAttachment({ name, bytes }, limits);
12608
13517
  if (plan.kind === "rejected")
12609
13518
  throw new Error(plan.reason);
@@ -12613,7 +13522,7 @@ function buildAttachmentUpload(filePath, chatId, topicId, caption, limits = TELE
12613
13522
  form.append("message_thread_id", String(topicId));
12614
13523
  if (caption !== undefined && caption.length > 0)
12615
13524
  form.append("caption", caption);
12616
- form.append(field, new Blob([fs22.readFileSync(filePath)]), name);
13525
+ form.append(field, new Blob([fs24.readFileSync(filePath)]), name);
12617
13526
  return { method: plan.kind === "photo" ? "sendPhoto" : "sendDocument", field, form };
12618
13527
  }
12619
13528
  function isPhotoRejection(error) {
@@ -12621,13 +13530,13 @@ function isPhotoRejection(error) {
12621
13530
  return /IMAGE_PROCESS_FAILED|PHOTO_INVALID_DIMENSIONS|PHOTO_EXT_INVALID|PHOTO_SAVE_FILE_INVALID/i.test(message);
12622
13531
  }
12623
13532
  function asDocumentUpload(filePath, chatId, topicId, caption) {
12624
- const name = path22.basename(filePath);
13533
+ const name = path24.basename(filePath);
12625
13534
  const form = new FormData;
12626
13535
  form.append("chat_id", String(chatId));
12627
13536
  form.append("message_thread_id", String(topicId));
12628
13537
  if (caption !== undefined && caption.length > 0)
12629
13538
  form.append("caption", caption);
12630
- form.append("document", new Blob([fs22.readFileSync(filePath)]), name);
13539
+ form.append("document", new Blob([fs24.readFileSync(filePath)]), name);
12631
13540
  return { method: "sendDocument", field: "document", form };
12632
13541
  }
12633
13542
  var TELEGRAM_LIMITS;
@@ -12697,6 +13606,31 @@ function planScreenDelivery(text, maxMessages) {
12697
13606
  const chunks = preChunks(text);
12698
13607
  return chunks.length <= maxMessages ? { kind: "messages", chunks } : { kind: "file", text };
12699
13608
  }
13609
+ function diffQuote(summary, limit = TELEGRAM_TEXT_LIMIT, reserved = 0) {
13610
+ const OPEN = "<blockquote expandable><pre>";
13611
+ const CLOSE = "</pre></blockquote>";
13612
+ const budget = limit - reserved - OPEN.length - CLOSE.length;
13613
+ const cut = (total) => escapeHtml(`\u2026 truncated, ${total} characters in the full patch`);
13614
+ const rendered = [];
13615
+ let used = 0;
13616
+ let stopped = false;
13617
+ for (const line of summary.lines) {
13618
+ const marker = line.kind === "add" ? "+" : line.kind === "del" ? "-" : "";
13619
+ const escaped = escapeHtml(`${marker}${line.text}`);
13620
+ if (used + escaped.length + 1 + cut(summary.totalChars).length > budget) {
13621
+ stopped = true;
13622
+ break;
13623
+ }
13624
+ rendered.push(escaped);
13625
+ used += escaped.length + 1;
13626
+ }
13627
+ if (rendered.length === 0)
13628
+ return "";
13629
+ if (stopped || summary.truncated !== undefined)
13630
+ rendered.push(cut(summary.totalChars));
13631
+ return `${OPEN}${rendered.join(`
13632
+ `)}${CLOSE}`;
13633
+ }
12700
13634
  function parseScreenLines(args) {
12701
13635
  const raw = args[0];
12702
13636
  if (raw === undefined || !/^-?\d+$/.test(raw))
@@ -13171,7 +14105,7 @@ var init_topic_title = __esm(() => {
13171
14105
  });
13172
14106
 
13173
14107
  // packages/channel-telegram/src/adapter.ts
13174
- import { setTimeout as delay3 } from "timers/promises";
14108
+ import { setTimeout as delay4 } from "timers/promises";
13175
14109
  function readMedia(msg) {
13176
14110
  const photo = msg.photo?.at(-1);
13177
14111
  if (photo) {
@@ -13207,6 +14141,7 @@ class TelegramChannel {
13207
14141
  titleFlushes = new Map;
13208
14142
  flows = new Map;
13209
14143
  outbound = Promise.resolve();
14144
+ username;
13210
14145
  polling = false;
13211
14146
  stopped = false;
13212
14147
  pollAbort;
@@ -13391,7 +14326,16 @@ class TelegramChannel {
13391
14326
  `${d.stat.filesChanged} files +${d.stat.insertions} -${d.stat.deletions}`,
13392
14327
  ...d.files.map((f) => `\u2022 ${f}`)
13393
14328
  ];
13394
- for (const message of htmlLines(lines)) {
14329
+ const messages = htmlLines(lines);
14330
+ const summary = d.patch === undefined ? undefined : summarizePatch(d.patch);
14331
+ const last = messages.at(-1);
14332
+ if (summary !== undefined && last !== undefined) {
14333
+ const quote = diffQuote(summary, TELEGRAM_TEXT_LIMIT, last.length + 1);
14334
+ if (quote !== "")
14335
+ messages[messages.length - 1] = `${last}
14336
+ ${quote}`;
14337
+ }
14338
+ for (const message of messages) {
13395
14339
  await this.sendToSession(sessionId, message);
13396
14340
  }
13397
14341
  }
@@ -13523,6 +14467,26 @@ ${code(prompt.line)}`,
13523
14467
  });
13524
14468
  }
13525
14469
  awaitingSecret = new Map;
14470
+ async captureSecret(sessionId, msg, text) {
14471
+ const seq = this.awaitingSecret.get(sessionId);
14472
+ if (seq === undefined)
14473
+ return false;
14474
+ this.awaitingSecret.delete(sessionId);
14475
+ const accepted = this.control.answerShellPrompt(sessionId, seq, text);
14476
+ let deleted = false;
14477
+ if (this.api.deleteMessage && msg.chat && msg.message_id !== undefined) {
14478
+ try {
14479
+ await this.api.deleteMessage(msg.chat.id, msg.message_id);
14480
+ deleted = true;
14481
+ } catch (err) {
14482
+ this.log(`secret reply message ${msg.message_id} not deleted: ${String(err)}`);
14483
+ }
14484
+ }
14485
+ await this.sendToSession(sessionId, tHtml(accepted ? "tg.prompt.secretSubmitted" : "tg.prompt.secretStale"));
14486
+ if (!deleted)
14487
+ await this.sendToSession(sessionId, tHtml("tg.prompt.secretUndeleted"));
14488
+ return true;
14489
+ }
13526
14490
  async onChoiceRequest(sessionId, seq, questions, askId, onboarding2 = false) {
13527
14491
  const binding = this.control.findBindingBySession(sessionId);
13528
14492
  if (!binding || questions.length === 0)
@@ -13686,6 +14650,8 @@ ${code(prompt.line)}`,
13686
14650
  }
13687
14651
  if (!binding)
13688
14652
  return;
14653
+ if (await this.captureSecret(binding.sessionId, msg, text))
14654
+ return;
13689
14655
  if (await this.captureFreeText(binding.sessionId, text))
13690
14656
  return;
13691
14657
  const chatId = msg.chat.id;
@@ -13792,8 +14758,8 @@ ${code(prompt.line)}`,
13792
14758
  throw new Error("this Telegram client cannot download files");
13793
14759
  const files = [];
13794
14760
  for (const item of batch.items) {
13795
- const path23 = await this.control.stageInboundFile(batch.sessionId, item.name, await download2(item.fileId));
13796
- files.push({ path: path23, kind: item.kind });
14761
+ const path25 = await this.control.stageInboundFile(batch.sessionId, item.name, await download2(item.fileId));
14762
+ files.push({ path: path25, kind: item.kind });
13797
14763
  }
13798
14764
  await this.control.prompt(batch.sessionId, buildAttachmentPrompt(files, batch.caption));
13799
14765
  this.log(`staged ${files.length} inbound file(s) for ${batch.sessionId}`);
@@ -13808,7 +14774,7 @@ ${code(prompt.line)}`,
13808
14774
  async settleInboundForTest() {
13809
14775
  while (this.albums.size > 0 || this.inboundQueues.size > 0) {
13810
14776
  if (this.albums.size > 0)
13811
- await delay3(this.albumDebounceMs + 5);
14777
+ await delay4(this.albumDebounceMs + 5);
13812
14778
  await Promise.all([...this.inboundQueues.values()]);
13813
14779
  }
13814
14780
  }
@@ -13817,10 +14783,22 @@ ${code(prompt.line)}`,
13817
14783
  return;
13818
14784
  await this.api.call("setMessageReaction", { chat_id: chatId, message_id: messageId, reaction: reactionPayload(state) }).catch(() => {});
13819
14785
  }
14786
+ async selfUsername() {
14787
+ if (this.username !== undefined)
14788
+ return this.username;
14789
+ try {
14790
+ const me = await this.api.call("getMe");
14791
+ if (typeof me.username === "string" && me.username.length > 0)
14792
+ this.username = me.username;
14793
+ } catch {}
14794
+ return this.username;
14795
+ }
13820
14796
  async handleCommand(text, binding, msg) {
13821
14797
  const [rawCmd = "", ...args] = text.split(/\s+/);
13822
14798
  const rawTail = text.slice(rawCmd.length);
13823
- const cmd = rawCmd.replace(/@\w+$/, "");
14799
+ const { command: cmd, target } = parseCommandAddress(rawCmd);
14800
+ if (target !== undefined && !commandIsForBot(target, await this.selfUsername()))
14801
+ return;
13824
14802
  if (binding) {
13825
14803
  switch (cmd) {
13826
14804
  case "/esc":
@@ -13916,8 +14894,15 @@ ${code(prompt.line)}`,
13916
14894
  const project = await resolveProject(args[0]);
13917
14895
  if (!project)
13918
14896
  return;
13919
- const name = args.slice(1).join(" ") || undefined;
13920
- await this.control.createSession({ cwd: project.path, ...name !== undefined ? { name } : {} });
14897
+ const tail = args.slice(1);
14898
+ const flagAt = tail.findIndex((token) => token.startsWith("-"));
14899
+ const name = (flagAt < 0 ? tail : tail.slice(0, flagAt)).join(" ") || undefined;
14900
+ const agentArgs = flagAt < 0 ? [] : tail.slice(flagAt);
14901
+ await this.control.createSession({
14902
+ cwd: project.path,
14903
+ ...name !== undefined ? { name } : {},
14904
+ ...agentArgs.length > 0 ? { agentArgs } : {}
14905
+ });
13921
14906
  return;
13922
14907
  }
13923
14908
  case "/wt": {
@@ -13969,13 +14954,13 @@ ${code(prompt.line)}`,
13969
14954
  const project = await resolveProject(args[0]);
13970
14955
  if (!project)
13971
14956
  return;
13972
- const target = args[1];
13973
- if (target === undefined) {
14957
+ const target2 = args[1];
14958
+ if (target2 === undefined) {
13974
14959
  await reply(t("tg.usage.rm"));
13975
14960
  return;
13976
14961
  }
13977
- const result = await this.control.removeWorktree(project.alias, target, args[2] === "force");
13978
- await reply(result.removed ? t("tg.worktree.removed", { target }) : t("tg.worktree.refused", { reason: String(result.reason) }));
14962
+ const result = await this.control.removeWorktree(project.alias, target2, args[2] === "force");
14963
+ await reply(result.removed ? t("tg.worktree.removed", { target: target2 }) : t("tg.worktree.refused", { reason: String(result.reason) }));
13979
14964
  return;
13980
14965
  }
13981
14966
  case "/sessions": {
@@ -14073,8 +15058,12 @@ ${code(prompt.line)}`,
14073
15058
  return;
14074
15059
  }
14075
15060
  if (flow.askId === undefined) {
14076
- const keys2 = agentProfile(agentIdFromCommand(session.agentCommand)).approvalKeys;
14077
- this.control.write(session.id, keys2 !== undefined && decoded.optionIndex <= 1 ? decoded.optionIndex === 0 ? keys2.allow : keys2.deny : buildMenuSelectKeys(decoded.optionIndex));
15061
+ const keys2 = menuChoiceKeys(agentProfile(agentIdFromCommand(session.agentCommand)), decoded.optionIndex);
15062
+ if (keys2 === undefined) {
15063
+ await answer(t("tg.answer.noSafeKey"));
15064
+ return;
15065
+ }
15066
+ this.control.write(session.id, keys2);
14078
15067
  }
14079
15068
  flow.answers.push({ questionIndex: flow.cursor, selected: [label] });
14080
15069
  flow.cursor += 1;
@@ -14127,6 +15116,11 @@ ${code(prompt.line)}`,
14127
15116
  clearTimeout(album.timer);
14128
15117
  this.albums.clear();
14129
15118
  }
15119
+ pollHealth() {
15120
+ return this.pollError;
15121
+ }
15122
+ pollError;
15123
+ pollWarned = false;
14130
15124
  async pollLoop() {
14131
15125
  while (this.polling) {
14132
15126
  try {
@@ -14137,6 +15131,10 @@ ${code(prompt.line)}`,
14137
15131
  });
14138
15132
  if (this.stopped)
14139
15133
  return;
15134
+ if (this.pollError !== undefined)
15135
+ this.log("polling recovered");
15136
+ this.pollError = undefined;
15137
+ this.pollWarned = false;
14140
15138
  for (const update2 of updates) {
14141
15139
  this.updateOffset = Math.max(this.updateOffset, update2.update_id + 1);
14142
15140
  await this.handleUpdate(update2);
@@ -14146,11 +15144,29 @@ ${code(prompt.line)}`,
14146
15144
  } catch (err) {
14147
15145
  if (!this.polling)
14148
15146
  return;
14149
- this.log(`poll failed: ${String(err)}`);
14150
- await delay3(3000);
15147
+ const reason = String(err);
15148
+ this.pollError = reason;
15149
+ this.log(`poll failed: ${reason}`);
15150
+ if (!this.pollWarned) {
15151
+ this.pollWarned = true;
15152
+ this.warnPollFailure(reason);
15153
+ }
15154
+ await delay4(3000);
14151
15155
  }
14152
15156
  }
14153
15157
  }
15158
+ async warnPollFailure(reason) {
15159
+ const conflict = /\b409\b|terminated by other getUpdates/i.test(reason);
15160
+ const text = conflict ? tHtml("tg.poll.conflict", { machine: this.config.machine.name }) : tHtml("tg.poll.failed", { machine: this.config.machine.name, reason: oneLineReason(reason) });
15161
+ try {
15162
+ await this.api.call("sendMessage", { chat_id: this.config.groupId, text, parse_mode: "HTML" });
15163
+ } catch (err) {
15164
+ this.log(`could not report the poll failure: ${String(err)}`);
15165
+ }
15166
+ }
15167
+ }
15168
+ function oneLineReason(reason) {
15169
+ return reason.replace(/\s+/g, " ").trim().slice(0, 200);
14154
15170
  }
14155
15171
  var SCREEN_MESSAGE_BUDGET = 2, AGENT_ERROR_CHARS = 1200, RAW_TYPE_LIMIT = 500, RAW_SCREEN_LINES = 12, HELP_KEYS, helpText = () => HELP_KEYS.map((key) => escapeHtml(t(key))).join(`
14156
15172
  `), ALBUM_DEBOUNCE_MS = 2000, UNKNOWN_SIZE = 1, REFUSAL_KEYS, megabytes2 = (bytes) => `${Math.round(bytes / (1024 * 1024))} MB`;
@@ -14187,13 +15203,13 @@ var init_src2 = __esm(() => {
14187
15203
  });
14188
15204
 
14189
15205
  // packages/daemon/src/machine-identity.ts
14190
- import os8 from "os";
15206
+ import os9 from "os";
14191
15207
  function resolveMachineIdentity(override) {
14192
- const hostname = os8.hostname();
15208
+ const hostname = os9.hostname();
14193
15209
  return {
14194
15210
  name: slugifyMachineName(override && override.trim().length > 0 ? override : hostname),
14195
15211
  hostname,
14196
- addresses: collectAddresses(os8.networkInterfaces())
15212
+ addresses: collectAddresses(os9.networkInterfaces())
14197
15213
  };
14198
15214
  }
14199
15215
  var init_machine_identity = __esm(() => {
@@ -14205,13 +15221,15 @@ class TelegramService {
14205
15221
  config;
14206
15222
  store;
14207
15223
  manager;
15224
+ apiFactory;
14208
15225
  channel;
14209
15226
  unsubscribe;
14210
15227
  runningConfig;
14211
- constructor(config, store, manager) {
15228
+ constructor(config, store, manager, apiFactory = (token) => new FetchTelegramApi(token)) {
14212
15229
  this.config = config;
14213
15230
  this.store = store;
14214
15231
  this.manager = manager;
15232
+ this.apiFactory = apiFactory;
14215
15233
  }
14216
15234
  readSettings() {
14217
15235
  const telegram = this.config.resolved().channels.telegram;
@@ -14260,7 +15278,7 @@ class TelegramService {
14260
15278
  const token = botToken ?? this.readSettings().botToken;
14261
15279
  if (!token)
14262
15280
  throw new AxtermError("err.telegram.noToken");
14263
- const api = new FetchTelegramApi(token);
15281
+ const api = this.apiFactory(token);
14264
15282
  const updates = await api.call("getUpdates", { timeout: 0 });
14265
15283
  return discoverTelegramTargets(updates);
14266
15284
  }
@@ -14275,7 +15293,7 @@ class TelegramService {
14275
15293
  log("telegram: not configured, channel not started");
14276
15294
  return false;
14277
15295
  }
14278
- const api = new FetchTelegramApi(s.botToken);
15296
+ const api = this.apiFactory(s.botToken);
14279
15297
  const config = {
14280
15298
  groupId: s.groupId,
14281
15299
  allowedUserIds: [s.allowUserId],
@@ -14383,6 +15401,9 @@ class TelegramService {
14383
15401
  problems.push(t("doctor.noUser"));
14384
15402
  if (problems.length > 0)
14385
15403
  return { ok: false, problems };
15404
+ const pollError = this.channel?.pollHealth();
15405
+ if (pollError !== undefined)
15406
+ problems.push(t("doctor.telegram.pollFailing", { reason: pollError.replace(/\s+/g, " ").slice(0, 160) }));
14386
15407
  const api = new FetchTelegramApi(s.botToken);
14387
15408
  const report = { ok: true, problems };
14388
15409
  try {
@@ -14423,7 +15444,7 @@ var init_telegram_service = __esm(() => {
14423
15444
  // packages/daemon/src/index.ts
14424
15445
  var exports_src = {};
14425
15446
  import { randomBytes as randomBytes3 } from "crypto";
14426
- import fs23 from "fs";
15447
+ import fs25 from "fs";
14427
15448
  import net5 from "net";
14428
15449
  function isPidAlive(pid) {
14429
15450
  try {
@@ -14435,9 +15456,9 @@ function isPidAlive(pid) {
14435
15456
  }
14436
15457
  async function main() {
14437
15458
  ensureDataDirs();
14438
- if (fs23.existsSync(RUNTIME_FILE2)) {
15459
+ if (fs25.existsSync(RUNTIME_FILE2)) {
14439
15460
  try {
14440
- const existing = JSON.parse(fs23.readFileSync(RUNTIME_FILE2, "utf8"));
15461
+ const existing = JSON.parse(fs25.readFileSync(RUNTIME_FILE2, "utf8"));
14441
15462
  if (existing.pid !== process.pid && isPidAlive(existing.pid)) {
14442
15463
  log(`daemon already running (pid ${existing.pid}); exiting`);
14443
15464
  process.exit(0);
@@ -14460,7 +15481,7 @@ async function main() {
14460
15481
  version: productVersion(),
14461
15482
  startedAt: Date.now()
14462
15483
  };
14463
- fs23.writeFileSync(RUNTIME_FILE2, JSON.stringify(runtime, null, 2), { mode: 384 });
15484
+ fs25.writeFileSync(RUNTIME_FILE2, JSON.stringify(runtime, null, 2), { mode: 384 });
14464
15485
  restrictFile(RUNTIME_FILE2);
14465
15486
  const config = new ConfigStore(defaultConfigStorePaths(CONFIG_DIR));
14466
15487
  if (config.loadError !== undefined)
@@ -14512,12 +15533,15 @@ async function main() {
14512
15533
  hookHost.stop();
14513
15534
  store.close();
14514
15535
  try {
14515
- const current = JSON.parse(fs23.readFileSync(RUNTIME_FILE2, "utf8"));
15536
+ const current = JSON.parse(fs25.readFileSync(RUNTIME_FILE2, "utf8"));
14516
15537
  if (current.pid === process.pid)
14517
- fs23.rmSync(RUNTIME_FILE2);
15538
+ fs25.rmSync(RUNTIME_FILE2);
14518
15539
  } catch {}
14519
15540
  process.exit(0);
14520
15541
  };
15542
+ const recovered = await manager.recover();
15543
+ if (recovered.adopted > 0)
15544
+ log(`boot: ${recovered.adopted} session(s) adopted from the terminal host`);
14521
15545
  startRpcServer({
14522
15546
  pipePath: PIPE_PATH,
14523
15547
  token,
@@ -14530,12 +15554,9 @@ async function main() {
14530
15554
  });
14531
15555
  process.on("SIGINT", () => stop("SIGINT"));
14532
15556
  process.on("SIGTERM", () => stop("SIGTERM"));
14533
- process.on("uncaughtException", (err) => {
14534
- log(`uncaughtException: ${err.stack ?? String(err)}`);
14535
- stop("uncaughtException");
14536
- });
14537
15557
  log(`daemon started pid=${process.pid} config=${CONFIG_DIR} state=${STATE_DIR}`);
14538
15558
  }
15559
+ var crashGuards;
14539
15560
  var init_src3 = __esm(() => {
14540
15561
  init_src();
14541
15562
  init_config_migrate();
@@ -14548,7 +15569,12 @@ var init_src3 = __esm(() => {
14548
15569
  init_telegram_service();
14549
15570
  init_store();
14550
15571
  net5.setDefaultAutoSelectFamilyAttemptTimeout(3000);
14551
- main();
15572
+ crashGuards = new ProcessCrashGuards({
15573
+ log,
15574
+ exit: (code2) => process.exit(code2)
15575
+ });
15576
+ crashGuards.install(process);
15577
+ main().catch((reason) => crashGuards.startupFailed(reason));
14552
15578
  });
14553
15579
 
14554
15580
  // packages/daemon/src/pty-host.ts
@@ -14603,7 +15629,7 @@ function startPtyHost(options) {
14603
15629
  send(socket, {
14604
15630
  t: "welcome",
14605
15631
  v: PTY_PROTOCOL_VERSION,
14606
- sessions: [...sessions.entries()].filter(([, s]) => !s.exited).map(([id, s]) => ({ id, pid: s.handle.pid }))
15632
+ sessions: [...sessions.entries()].filter(([, s]) => !s.exited).map(([id, s]) => ({ id, pid: s.handle.pid, cols: s.cols, rows: s.rows }))
14607
15633
  });
14608
15634
  continue;
14609
15635
  }
@@ -14620,9 +15646,18 @@ function startPtyHost(options) {
14620
15646
  env: message.env,
14621
15647
  term: message.term
14622
15648
  });
14623
- const session = { handle, cols: message.cols, rows: message.rows, exited: false };
15649
+ const session = {
15650
+ handle,
15651
+ cols: message.cols,
15652
+ rows: message.rows,
15653
+ exited: false,
15654
+ mirror: new TerminalMirror(message.cols, message.rows, REPLAY_SCROLLBACK_LINES)
15655
+ };
14624
15656
  sessions.set(message.id, session);
14625
- handle.onData((text) => broadcast(encodeFrame(FrameKind.Data, message.id, new TextEncoder().encode(text))));
15657
+ handle.onData((text) => {
15658
+ session.mirror.write(text);
15659
+ broadcast(encodeFrame(FrameKind.Data, message.id, new TextEncoder().encode(text)));
15660
+ });
14626
15661
  handle.onExit((code2) => {
14627
15662
  session.exited = true;
14628
15663
  for (const client of clients)
@@ -14637,6 +15672,21 @@ function startPtyHost(options) {
14637
15672
  }
14638
15673
  continue;
14639
15674
  }
15675
+ if (message.t === "subscribe") {
15676
+ const session = sessions.get(message.id);
15677
+ if (session === undefined) {
15678
+ send(socket, { t: "error", id: message.id, message: "no such session" });
15679
+ continue;
15680
+ }
15681
+ if (message.replay) {
15682
+ const ansi = session.mirror.serialize(REPLAY_MAX_CHARS);
15683
+ if (ansi.length > 0 && !socket.destroyed) {
15684
+ socket.write(encodeFrame(FrameKind.Data, message.id, new TextEncoder().encode(ansi)));
15685
+ }
15686
+ }
15687
+ send(socket, { t: "subscribed", id: message.id });
15688
+ continue;
15689
+ }
14640
15690
  if (message.t === "resize") {
14641
15691
  const session = sessions.get(message.id);
14642
15692
  if (session === undefined || session.exited)
@@ -14644,6 +15694,7 @@ function startPtyHost(options) {
14644
15694
  session.cols = message.cols;
14645
15695
  session.rows = message.rows;
14646
15696
  session.handle.resize(message.cols, message.rows);
15697
+ session.mirror.resize(message.cols, message.rows);
14647
15698
  continue;
14648
15699
  }
14649
15700
  if (message.t === "kill") {
@@ -14674,10 +15725,13 @@ function startPtyHost(options) {
14674
15725
  });
14675
15726
  });
14676
15727
  }
15728
+ var REPLAY_MAX_CHARS, REPLAY_SCROLLBACK_LINES = 5000;
14677
15729
  var init_pty_host = __esm(() => {
14678
15730
  init_src();
14679
15731
  init_paths();
14680
15732
  init_pty();
15733
+ init_terminal_mirror();
15734
+ REPLAY_MAX_CHARS = 2 * 1024 * 1024;
14681
15735
  });
14682
15736
 
14683
15737
  // packages/daemon/src/pty-host-main.ts
@@ -14730,8 +15784,8 @@ var init_pty_host_main = __esm(() => {
14730
15784
 
14731
15785
  // packages/cli/src/index.ts
14732
15786
  init_src();
14733
- import fs24 from "fs";
14734
- import path23 from "path";
15787
+ import fs26 from "fs";
15788
+ import path25 from "path";
14735
15789
 
14736
15790
  // packages/cli/src/attach.ts
14737
15791
  import fs from "fs";
@@ -15150,11 +16204,11 @@ ${bold(t(state.promptKey))} ${dim("[yes]")} `);
15150
16204
  const finished = advance({ ...state, kind: "review", step: "review" }, "");
15151
16205
  if (!("done" in finished))
15152
16206
  return { saved: false };
15153
- for (const [path3, value] of finished.done.settings) {
15154
- await client.call("config.set", { path: path3, value });
16207
+ for (const [path4, value] of finished.done.settings) {
16208
+ await client.call("config.set", { path: path4, value });
15155
16209
  }
15156
- for (const [path3, value] of finished.done.secrets) {
15157
- await client.call("config.set", { path: path3, value });
16210
+ for (const [path4, value] of finished.done.secrets) {
16211
+ await client.call("config.set", { path: path4, value });
15158
16212
  }
15159
16213
  stdout.write(`
15160
16214
  ${green("\u2713")} ${t("wizard.saved")}
@@ -15192,10 +16246,202 @@ function describeHealth(health) {
15192
16246
  // packages/cli/src/client.ts
15193
16247
  init_src();
15194
16248
  import { spawn } from "child_process";
15195
- import fs2 from "fs";
16249
+ import fs3 from "fs";
15196
16250
  import net from "net";
15197
- import path3 from "path";
16251
+ import path5 from "path";
15198
16252
  import { setTimeout as delay } from "timers/promises";
16253
+
16254
+ // packages/cli/src/service-manager.ts
16255
+ init_src();
16256
+ import { spawnSync } from "child_process";
16257
+ import fs2 from "fs";
16258
+ import os3 from "os";
16259
+ import path4 from "path";
16260
+
16261
+ class ServiceCommandError extends Error {
16262
+ invocation;
16263
+ result;
16264
+ constructor(invocation, result) {
16265
+ super(result.stderr.trim() || result.stdout.trim() || `${invocation.command} exited ${result.status}`);
16266
+ this.invocation = invocation;
16267
+ this.result = result;
16268
+ this.name = "ServiceCommandError";
16269
+ }
16270
+ }
16271
+ var defaultRun = ({ command, args }) => {
16272
+ const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
16273
+ return {
16274
+ status: result.status ?? 1,
16275
+ stdout: result.stdout ?? "",
16276
+ stderr: result.stderr ?? result.error?.message ?? ""
16277
+ };
16278
+ };
16279
+ function writeAtomic(file, contents) {
16280
+ fs2.mkdirSync(path4.dirname(file), { recursive: true, mode: 448 });
16281
+ const tmp = `${file}.tmp`;
16282
+ try {
16283
+ fs2.writeFileSync(tmp, contents, { mode: 384 });
16284
+ fs2.renameSync(tmp, file);
16285
+ if (process.platform !== "win32")
16286
+ fs2.chmodSync(file, 384);
16287
+ } catch (err) {
16288
+ try {
16289
+ fs2.rmSync(tmp, { force: true });
16290
+ } catch {}
16291
+ throw err;
16292
+ }
16293
+ }
16294
+ function utf16Xml(contents) {
16295
+ return Buffer.concat([Buffer.from([255, 254]), Buffer.from(contents, "utf16le")]);
16296
+ }
16297
+ function utf8Script(contents) {
16298
+ return Buffer.concat([Buffer.from([239, 187, 191]), Buffer.from(contents, "utf8")]);
16299
+ }
16300
+
16301
+ class ServiceManager {
16302
+ options;
16303
+ definition;
16304
+ run;
16305
+ uid;
16306
+ constructor(options) {
16307
+ this.options = options;
16308
+ this.run = options.run ?? defaultRun;
16309
+ this.uid = options.uid ?? process.getuid?.();
16310
+ this.definition = serviceDefinition({
16311
+ platform: options.platform,
16312
+ launch: options.launch,
16313
+ homeDir: options.homeDir,
16314
+ stateDir: options.stateDir,
16315
+ ...options.userId ? { userId: options.userId } : {},
16316
+ ...options.environment ? { environment: options.environment } : {}
16317
+ });
16318
+ }
16319
+ install() {
16320
+ fs2.mkdirSync(path4.join(this.options.stateDir, "logs"), { recursive: true, mode: 448 });
16321
+ for (const companion of this.definition.companionFiles ?? []) {
16322
+ writeAtomic(companion.file, this.definition.kind === "task-scheduler" ? utf8Script(companion.contents) : companion.contents);
16323
+ }
16324
+ writeAtomic(this.definition.file, this.definition.kind === "task-scheduler" ? utf16Xml(this.definition.contents) : this.definition.contents);
16325
+ if (this.definition.kind === "systemd") {
16326
+ this.must({ command: "systemctl", args: ["--user", "daemon-reload"] });
16327
+ this.must({ command: "systemctl", args: ["--user", "enable", SYSTEMD_SERVICE] });
16328
+ } else if (this.definition.kind === "launchd") {
16329
+ this.must({ command: "launchctl", args: ["enable", this.launchdTarget()] });
16330
+ } else {
16331
+ this.must({ command: "schtasks.exe", args: ["/Create", "/TN", WINDOWS_TASK, "/XML", this.definition.file, "/F"] });
16332
+ }
16333
+ }
16334
+ start() {
16335
+ if (this.definition.kind === "systemd") {
16336
+ this.must({ command: "systemctl", args: ["--user", "start", SYSTEMD_SERVICE] });
16337
+ } else if (this.definition.kind === "launchd") {
16338
+ const target = this.launchdTarget();
16339
+ const loaded = this.run({ command: "launchctl", args: ["print", target] }).status === 0;
16340
+ this.must(loaded ? { command: "launchctl", args: ["kickstart", target] } : { command: "launchctl", args: ["bootstrap", `gui/${this.requireUid()}`, this.definition.file] });
16341
+ } else {
16342
+ this.must({ command: "schtasks.exe", args: ["/Run", "/TN", WINDOWS_TASK] });
16343
+ }
16344
+ }
16345
+ status() {
16346
+ if (this.definition.kind === "systemd") {
16347
+ return {
16348
+ kind: this.definition.kind,
16349
+ installed: fs2.existsSync(this.definition.file),
16350
+ active: this.run({ command: "systemctl", args: ["--user", "is-active", "--quiet", SYSTEMD_SERVICE] }).status === 0
16351
+ };
16352
+ }
16353
+ if (this.definition.kind === "launchd") {
16354
+ return {
16355
+ kind: this.definition.kind,
16356
+ installed: fs2.existsSync(this.definition.file),
16357
+ active: this.run({ command: "launchctl", args: ["print", this.launchdTarget()] }).status === 0
16358
+ };
16359
+ }
16360
+ const installed = this.run({ command: "schtasks.exe", args: ["/Query", "/TN", WINDOWS_TASK] }).status === 0;
16361
+ const active = installed && this.run({
16362
+ command: "powershell.exe",
16363
+ args: [
16364
+ "-NoProfile",
16365
+ "-NonInteractive",
16366
+ "-Command",
16367
+ `if ((Get-ScheduledTask -TaskName '${WINDOWS_TASK}').State -eq 'Running') { exit 0 } else { exit 1 }`
16368
+ ]
16369
+ }).status === 0;
16370
+ return { kind: this.definition.kind, installed, active };
16371
+ }
16372
+ uninstall() {
16373
+ if (this.definition.kind === "systemd") {
16374
+ this.run({ command: "systemctl", args: ["--user", "disable", SYSTEMD_SERVICE] });
16375
+ fs2.rmSync(this.definition.file, { force: true });
16376
+ this.must({ command: "systemctl", args: ["--user", "daemon-reload"] });
16377
+ } else if (this.definition.kind === "launchd") {
16378
+ this.run({ command: "launchctl", args: ["disable", this.launchdTarget()] });
16379
+ fs2.rmSync(this.definition.file, { force: true });
16380
+ } else {
16381
+ this.run({ command: "schtasks.exe", args: ["/Delete", "/TN", WINDOWS_TASK, "/F"] });
16382
+ fs2.rmSync(this.definition.file, { force: true });
16383
+ }
16384
+ for (const companion of this.definition.companionFiles ?? [])
16385
+ fs2.rmSync(companion.file, { force: true });
16386
+ }
16387
+ must(command) {
16388
+ const result = this.run(command);
16389
+ if (result.status !== 0)
16390
+ throw new ServiceCommandError(command, result);
16391
+ }
16392
+ requireUid() {
16393
+ if (this.uid === undefined)
16394
+ throw new Error("launchd requires the current uid");
16395
+ return this.uid;
16396
+ }
16397
+ launchdTarget() {
16398
+ return `gui/${this.requireUid()}/${LAUNCHD_LABEL}`;
16399
+ }
16400
+ }
16401
+ function windowsUserId(run = defaultRun) {
16402
+ const command = {
16403
+ command: "powershell.exe",
16404
+ args: [
16405
+ "-NoProfile",
16406
+ "-NonInteractive",
16407
+ "-Command",
16408
+ "[System.Security.Principal.WindowsIdentity]::GetCurrent().Name"
16409
+ ]
16410
+ };
16411
+ const result = run(command);
16412
+ const value = result.stdout.trim();
16413
+ if (result.status !== 0 || value === "")
16414
+ throw new ServiceCommandError(command, result);
16415
+ return value;
16416
+ }
16417
+ function startInstalledService(manager) {
16418
+ if (!manager.status().installed)
16419
+ return false;
16420
+ manager.start();
16421
+ return true;
16422
+ }
16423
+ function currentServiceManager(run = defaultRun) {
16424
+ const paths = resolvePaths(currentPathEnv());
16425
+ const environment = {};
16426
+ if (process.env["AXTERM_CONFIG_DIR"] === paths.configDir)
16427
+ environment["AXTERM_CONFIG_DIR"] = paths.configDir;
16428
+ if (process.env["AXTERM_STATE_DIR"] === paths.stateDir)
16429
+ environment["AXTERM_STATE_DIR"] = paths.stateDir;
16430
+ if (process.env["AXTERM_RUNTIME_DIR"] === paths.runtimeDir)
16431
+ environment["AXTERM_RUNTIME_DIR"] = paths.runtimeDir;
16432
+ return new ServiceManager({
16433
+ platform: process.platform,
16434
+ homeDir: os3.homedir(),
16435
+ stateDir: paths.stateDir,
16436
+ launch: daemonLaunch({ execPath: process.execPath, mainScript: Bun.main }),
16437
+ ...process.getuid ? { uid: process.getuid() } : {},
16438
+ ...process.platform === "win32" ? { userId: windowsUserId(run) } : {},
16439
+ ...Object.keys(environment).length > 0 ? { environment } : {},
16440
+ run
16441
+ });
16442
+ }
16443
+
16444
+ // packages/cli/src/client.ts
15199
16445
  var RUNTIME_FILE = resolvePaths(currentPathEnv()).runtimeFile;
15200
16446
 
15201
16447
  class RpcClient {
@@ -15281,7 +16527,7 @@ class RpcClient {
15281
16527
  }
15282
16528
  function readRuntime() {
15283
16529
  try {
15284
- const runtime = JSON.parse(fs2.readFileSync(RUNTIME_FILE, "utf8"));
16530
+ const runtime = JSON.parse(fs3.readFileSync(RUNTIME_FILE, "utf8"));
15285
16531
  process.kill(runtime.pid, 0);
15286
16532
  return runtime;
15287
16533
  } catch {
@@ -15300,11 +16546,14 @@ async function tryConnect() {
15300
16546
  return;
15301
16547
  }
15302
16548
  }
16549
+ function connectDaemonIfRunning() {
16550
+ return tryConnect();
16551
+ }
15303
16552
  function daemonEnv(cwd, env) {
15304
16553
  const files = [];
15305
16554
  for (const name of dotenvCandidates(env["NODE_ENV"])) {
15306
16555
  try {
15307
- files.push(parseDotenv(fs2.readFileSync(path3.join(cwd, name), "utf8")));
16556
+ files.push(parseDotenv(fs3.readFileSync(path5.join(cwd, name), "utf8")));
15308
16557
  } catch {}
15309
16558
  }
15310
16559
  return scrubDotenvKeys(env, files);
@@ -15313,14 +16562,26 @@ async function ensureDaemon() {
15313
16562
  const existing = await tryConnect();
15314
16563
  if (existing)
15315
16564
  return existing;
15316
- const launch = daemonLaunch({ execPath: process.execPath, mainScript: Bun.main });
15317
- const child = spawn(launch.command, launch.args, {
15318
- detached: true,
15319
- stdio: "ignore",
15320
- windowsHide: true,
15321
- env: daemonEnv(process.cwd(), process.env)
15322
- });
15323
- child.unref();
16565
+ let supervised = false;
16566
+ try {
16567
+ supervised = startInstalledService(currentServiceManager());
16568
+ } catch (err) {
16569
+ if (err instanceof ServiceCommandError) {
16570
+ const command = [err.invocation.command, ...err.invocation.args].join(" ");
16571
+ throw new Error(t("cli.service.commandFailed", { command, error: err.message }));
16572
+ }
16573
+ throw err;
16574
+ }
16575
+ if (!supervised) {
16576
+ const launch = daemonLaunch({ execPath: process.execPath, mainScript: Bun.main });
16577
+ const child = spawn(launch.command, launch.args, {
16578
+ detached: true,
16579
+ stdio: "ignore",
16580
+ windowsHide: true,
16581
+ env: daemonEnv(process.cwd(), process.env)
16582
+ });
16583
+ child.unref();
16584
+ }
15324
16585
  const deadline = Date.now() + 15000;
15325
16586
  while (Date.now() < deadline) {
15326
16587
  await delay(300);
@@ -15328,11 +16589,12 @@ async function ensureDaemon() {
15328
16589
  if (client)
15329
16590
  return client;
15330
16591
  }
15331
- throw new Error(t("cli.daemon.notReady", { log: path3.join(resolvePaths(currentPathEnv()).logDir, "daemon.log") }));
16592
+ throw new Error(t("cli.daemon.notReady", { log: path5.join(resolvePaths(currentPathEnv()).logDir, "daemon.log") }));
15332
16593
  }
15333
16594
 
15334
16595
  // packages/cli/src/hook-bridge.ts
15335
16596
  init_src();
16597
+ import fs4 from "fs";
15336
16598
  var POST_TIMEOUT_MS = 1500;
15337
16599
  function isCodexApproval(agent, body) {
15338
16600
  if (agent !== "codex")
@@ -15357,24 +16619,44 @@ async function relayHookEvent(agent) {
15357
16619
  if (body === "")
15358
16620
  return;
15359
16621
  const approval = isCodexApproval(agent, body);
15360
- const res = await fetch(target.url, {
15361
- method: "POST",
15362
- headers: target.headers,
15363
- body,
15364
- signal: AbortSignal.timeout(approval ? CODEX_APPROVAL_POST_TIMEOUT_MS : POST_TIMEOUT_MS)
15365
- });
15366
- if (approval && res.ok) {
16622
+ const timeout = approval ? CODEX_APPROVAL_POST_TIMEOUT_MS : POST_TIMEOUT_MS;
16623
+ const post = (to) => fetch(to.url, { method: "POST", headers: to.headers, body, signal: AbortSignal.timeout(timeout) });
16624
+ let res;
16625
+ try {
16626
+ res = await post(target);
16627
+ } catch {}
16628
+ if (res === undefined || res.status === 401 || res.status === 404) {
16629
+ if (approval && res === undefined)
16630
+ return;
16631
+ const fresh = currentEndpoint();
16632
+ const retarget = fresh === undefined ? undefined : hookPostTarget(process.env, agent, fresh);
16633
+ if (retarget !== undefined && retarget.url !== target.url)
16634
+ res = await post(retarget);
16635
+ else if (res === undefined)
16636
+ return;
16637
+ }
16638
+ if (approval && res !== undefined && res.ok) {
15367
16639
  const decision = await res.text();
15368
16640
  if (decision !== "")
15369
16641
  process.stdout.write(decision);
15370
16642
  }
15371
16643
  } catch {}
15372
16644
  }
16645
+ function currentEndpoint() {
16646
+ const file = process.env["AXTERM_HOOK_ENDPOINT"];
16647
+ if (file === undefined)
16648
+ return;
16649
+ try {
16650
+ return parseHookEndpoint(fs4.readFileSync(file, "utf8"));
16651
+ } catch {
16652
+ return;
16653
+ }
16654
+ }
15373
16655
 
15374
16656
  // packages/cli/src/mcp-server.ts
15375
16657
  init_src();
15376
- import fs3 from "fs";
15377
- import path4 from "path";
16658
+ import fs5 from "fs";
16659
+ import path6 from "path";
15378
16660
  var ASK_TIMEOUT_MS = 10 * 60000;
15379
16661
  var POST_TIMEOUT_MS2 = 5000;
15380
16662
  var TOOLS = [
@@ -15490,7 +16772,7 @@ class McpServer {
15490
16772
  const contextFile = this.io.env[MCP_CONTEXT_ENV];
15491
16773
  if (contextFile !== undefined) {
15492
16774
  try {
15493
- const context = JSON.parse(fs3.readFileSync(contextFile, "utf8"));
16775
+ const context = JSON.parse(fs5.readFileSync(contextFile, "utf8"));
15494
16776
  if (context.port && context.token && context.sessionId) {
15495
16777
  return {
15496
16778
  url: `http://127.0.0.1:${context.port}`,
@@ -15535,7 +16817,7 @@ class McpServer {
15535
16817
  const target = this.target();
15536
16818
  if (!target)
15537
16819
  return { text: "axterm is not reachable from this session", isError: true };
15538
- const resolved = path4.isAbsolute(file) ? file : path4.resolve(this.io.cwd, file);
16820
+ const resolved = path6.isAbsolute(file) ? file : path6.resolve(this.io.cwd, file);
15539
16821
  const caption = typeof args["caption"] === "string" ? args["caption"] : undefined;
15540
16822
  const res = await this.post("/hook/attach", { sessionId: target.sessionId, launchToken: target.launchToken, path: resolved, caption }, POST_TIMEOUT_MS2);
15541
16823
  if (!res)
@@ -15645,15 +16927,15 @@ function runMcpServer() {
15645
16927
 
15646
16928
  // packages/cli/src/update.ts
15647
16929
  init_src();
15648
- import { spawnSync } from "child_process";
16930
+ import { spawnSync as spawnSync2 } from "child_process";
15649
16931
  import { createHash as createHash2 } from "crypto";
15650
- import fs4 from "fs";
15651
- import path5 from "path";
16932
+ import fs6 from "fs";
16933
+ import path7 from "path";
15652
16934
  import { stdin as stdin2, stdout as stdout2 } from "process";
15653
16935
  // packages/cli/package.json
15654
16936
  var package_default = {
15655
16937
  name: "@layorixinc/axterm",
15656
- version: "0.5.0",
16938
+ version: "0.6.1",
15657
16939
  type: "module",
15658
16940
  bin: {
15659
16941
  axterm: "dist/cli.js"
@@ -15727,13 +17009,13 @@ function assetName(platform, arch, isMusl) {
15727
17009
  function usesMusl() {
15728
17010
  if (process.platform !== "linux")
15729
17011
  return false;
15730
- if (fs4.existsSync("/etc/alpine-release"))
17012
+ if (fs6.existsSync("/etc/alpine-release"))
15731
17013
  return true;
15732
- const ldd = spawnSync("ldd", ["--version"], { encoding: "utf8" });
17014
+ const ldd = spawnSync2("ldd", ["--version"], { encoding: "utf8" });
15733
17015
  return `${ldd.stdout ?? ""}${ldd.stderr ?? ""}`.toLowerCase().includes("musl");
15734
17016
  }
15735
17017
  function firstLine(command, args) {
15736
- const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
17018
+ const result = spawnSync2(command, args, { encoding: "utf8", windowsHide: true });
15737
17019
  if (result.status !== 0)
15738
17020
  return;
15739
17021
  return result.stdout?.trim().split(/\r?\n/)[0]?.trim() || undefined;
@@ -15741,7 +17023,7 @@ function firstLine(command, args) {
15741
17023
  function detectOrigin(execPath) {
15742
17024
  const resolved = (() => {
15743
17025
  try {
15744
- return fs4.realpathSync(execPath);
17026
+ return fs6.realpathSync(execPath);
15745
17027
  } catch {
15746
17028
  return execPath;
15747
17029
  }
@@ -15766,7 +17048,7 @@ function npmGlobalBin() {
15766
17048
  const prefix = firstLine("npm", ["prefix", "-g"]);
15767
17049
  if (prefix === undefined)
15768
17050
  return;
15769
- return process.platform === "win32" ? prefix : path5.join(prefix, "bin");
17051
+ return process.platform === "win32" ? prefix : path7.join(prefix, "bin");
15770
17052
  }
15771
17053
  async function download(url, into) {
15772
17054
  const response = await fetch(url, { signal: AbortSignal.timeout(600000) });
@@ -15783,7 +17065,7 @@ function checksumFor(sums, asset) {
15783
17065
  return;
15784
17066
  }
15785
17067
  function sha256(file) {
15786
- return createHash2("sha256").update(fs4.readFileSync(file)).digest("hex");
17068
+ return createHash2("sha256").update(fs6.readFileSync(file)).digest("hex");
15787
17069
  }
15788
17070
  function ask2(question) {
15789
17071
  if (stdin2.isTTY !== true)
@@ -15830,8 +17112,8 @@ async function runUpdate(client, current, options) {
15830
17112
  }
15831
17113
  const asset = assetName(process.platform, process.arch, usesMusl());
15832
17114
  const base = `${RELEASES}/v${latest.version}`;
15833
- const staging = fs4.mkdtempSync(`${process.execPath}.update-`);
15834
- const downloaded = path5.join(staging, asset);
17115
+ const staging = fs6.mkdtempSync(`${process.execPath}.update-`);
17116
+ const downloaded = path7.join(staging, asset);
15835
17117
  try {
15836
17118
  console.log(t("cli.update.downloading", { asset }));
15837
17119
  await download(`${base}/${asset}`, downloaded);
@@ -15844,8 +17126,8 @@ async function runUpdate(client, current, options) {
15844
17126
  return;
15845
17127
  }
15846
17128
  if (process.platform !== "win32")
15847
- fs4.chmodSync(downloaded, 493);
15848
- const reported = spawnSync(downloaded, ["--version"], { encoding: "utf8" }).stdout?.trim();
17129
+ fs6.chmodSync(downloaded, 493);
17130
+ const reported = spawnSync2(downloaded, ["--version"], { encoding: "utf8" }).stdout?.trim();
15849
17131
  if (reported !== latest.version) {
15850
17132
  console.error(t("cli.update.wrongVersion", { got: reported ?? "nothing", want: latest.version }));
15851
17133
  process.exitCode = 1;
@@ -15877,14 +17159,14 @@ async function runUpdate(client, current, options) {
15877
17159
  if (plan.stale > 0)
15878
17160
  console.log(t("cli.update.staleDaemon", { count: plan.stale }));
15879
17161
  for (const id of plan.resume) {
15880
- const result = spawnSync(process.execPath, ["resume", id, "--no-attach", "--no-setup"], { encoding: "utf8" });
17162
+ const result = spawnSync2(process.execPath, ["resume", id, "--no-attach", "--no-setup"], { encoding: "utf8" });
15881
17163
  if (result.status === 0)
15882
17164
  console.log(t("cli.update.restored", { id }));
15883
17165
  else
15884
17166
  console.log(t("cli.update.restoreFailed", { id, reason: (result.stderr ?? "").trim() || "unknown" }));
15885
17167
  }
15886
17168
  } finally {
15887
- fs4.rmSync(staging, { recursive: true, force: true });
17169
+ fs6.rmSync(staging, { recursive: true, force: true });
15888
17170
  }
15889
17171
  }
15890
17172
  function applySwap(target, downloaded) {
@@ -15893,20 +17175,88 @@ function applySwap(target, downloaded) {
15893
17175
  try {
15894
17176
  for (const step of steps) {
15895
17177
  if (step.op === "chmod")
15896
- fs4.chmodSync(step.from, 493);
17178
+ fs6.chmodSync(step.from, 493);
15897
17179
  else if (step.op === "backup") {
15898
- fs4.renameSync(step.from, step.to);
17180
+ fs6.renameSync(step.from, step.to);
15899
17181
  backup = step.to;
15900
17182
  } else
15901
- fs4.renameSync(step.from, step.to);
17183
+ fs6.renameSync(step.from, step.to);
15902
17184
  }
15903
17185
  } catch (err) {
15904
- if (backup !== undefined && !fs4.existsSync(target))
15905
- fs4.renameSync(backup, target);
17186
+ if (backup !== undefined && !fs6.existsSync(target))
17187
+ fs6.renameSync(backup, target);
15906
17188
  throw err;
15907
17189
  }
15908
17190
  }
15909
17191
 
17192
+ // packages/cli/src/service-command.ts
17193
+ init_src();
17194
+ import { setTimeout as delay2 } from "timers/promises";
17195
+ var SERVICE_WAIT_MS = 15000;
17196
+ var SERVICE_POLL_MS = 200;
17197
+ async function waitForDaemon(deps, running) {
17198
+ const sleep = deps.sleep ?? ((ms) => delay2(ms));
17199
+ const now = deps.now ?? Date.now;
17200
+ const deadline = now() + SERVICE_WAIT_MS;
17201
+ for (;; ) {
17202
+ const connected = await deps.connect();
17203
+ if (running && connected !== undefined)
17204
+ return connected;
17205
+ connected?.close();
17206
+ if (!running && connected === undefined)
17207
+ return;
17208
+ if (now() >= deadline)
17209
+ throw new Error(t("cli.service.notReady"));
17210
+ await sleep(SERVICE_POLL_MS);
17211
+ }
17212
+ }
17213
+ function localizedStatus(status) {
17214
+ return t("cli.service.status", {
17215
+ kind: status.kind,
17216
+ installed: t(status.installed ? "cli.service.yes" : "cli.service.no"),
17217
+ active: t(status.active ? "cli.service.yes" : "cli.service.no")
17218
+ });
17219
+ }
17220
+ function localizeCommandError(err) {
17221
+ if (err instanceof ServiceCommandError) {
17222
+ const command = [err.invocation.command, ...err.invocation.args].join(" ");
17223
+ throw new Error(t("cli.service.commandFailed", { command, error: err.message }));
17224
+ }
17225
+ throw err;
17226
+ }
17227
+ async function runServiceCommand(subcommand, deps) {
17228
+ const write = deps.write ?? console.log;
17229
+ try {
17230
+ if (subcommand === "status") {
17231
+ write(localizedStatus(deps.manager.status()));
17232
+ return;
17233
+ }
17234
+ if (subcommand === "uninstall") {
17235
+ deps.manager.uninstall();
17236
+ write(t("cli.service.uninstalled"));
17237
+ return;
17238
+ }
17239
+ if (subcommand !== "install")
17240
+ throw new Error(t("cli.usage.service"));
17241
+ deps.manager.install();
17242
+ const existing = await deps.connect();
17243
+ if (existing !== undefined) {
17244
+ try {
17245
+ await existing.call("daemon.stop");
17246
+ } finally {
17247
+ existing.close();
17248
+ }
17249
+ await waitForDaemon(deps, false);
17250
+ }
17251
+ deps.manager.start();
17252
+ const ready = await waitForDaemon(deps, true);
17253
+ ready?.close();
17254
+ write(t("cli.service.installed", { kind: deps.manager.definition.kind }));
17255
+ } catch (err) {
17256
+ localizeCommandError(err);
17257
+ }
17258
+ }
17259
+
15910
17260
  // packages/cli/src/index.ts
15911
17261
  var VERSION = package_default.version;
15912
17262
  var VALUE_FLAGS = new Set([
@@ -15975,7 +17325,7 @@ function printSessionTable(sessions) {
15975
17325
  console.log(row.map((cell, col) => cell.padEnd(widths[col])).join(" "));
15976
17326
  }
15977
17327
  }
15978
- var realPath = (target) => fs24.realpathSync.native(target);
17328
+ var realPath = (target) => fs26.realpathSync.native(target);
15979
17329
  function scopedHere(sessions) {
15980
17330
  return sessionsInScope(sessions, process.cwd(), realPath);
15981
17331
  }
@@ -15988,7 +17338,7 @@ var help = () => t("cli.help");
15988
17338
  function readDotEnv(file = ".env") {
15989
17339
  const out = {};
15990
17340
  try {
15991
- for (const line of fs24.readFileSync(file, "utf8").split(/\r?\n/)) {
17341
+ for (const line of fs26.readFileSync(file, "utf8").split(/\r?\n/)) {
15992
17342
  const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
15993
17343
  if (m)
15994
17344
  out[m[1]] = m[2].trim();
@@ -15999,7 +17349,7 @@ function readDotEnv(file = ".env") {
15999
17349
  function configuredLocale() {
16000
17350
  try {
16001
17351
  const file = resolvePaths(currentPathEnv()).configFile;
16002
- const raw = JSON.parse(fs24.readFileSync(file, "utf8"));
17352
+ const raw = JSON.parse(fs26.readFileSync(file, "utf8"));
16003
17353
  return typeof raw.locale === "string" ? raw.locale : undefined;
16004
17354
  } catch {
16005
17355
  return;
@@ -16036,6 +17386,13 @@ async function main2() {
16036
17386
  console.log(help());
16037
17387
  return;
16038
17388
  }
17389
+ if (command === "service") {
17390
+ await runServiceCommand(positional[0] ?? "status", {
17391
+ manager: currentServiceManager(),
17392
+ connect: connectDaemonIfRunning
17393
+ });
17394
+ return;
17395
+ }
16039
17396
  const client = await ensureDaemon();
16040
17397
  const health = await client.call("config.health").catch(() => {
16041
17398
  return;
@@ -16055,6 +17412,7 @@ async function main2() {
16055
17412
  const commandOverride = typeof flags.get("cmd") === "string" ? flags.get("cmd").split(/\s+/).filter(Boolean) : undefined;
16056
17413
  const info = await client.call("session.create", {
16057
17414
  cwd: flags.get("cwd") ?? process.cwd(),
17415
+ preferConversationCwd: flags.get("cwd") === undefined,
16058
17416
  name: flags.get("name"),
16059
17417
  shell: flags.get("shell"),
16060
17418
  agent: flags.get("agent"),
@@ -16148,9 +17506,15 @@ async function main2() {
16148
17506
  const [id, indexArg] = positional;
16149
17507
  if (!id || indexArg === undefined)
16150
17508
  throw new Error(t("cli.usage.choose"));
16151
- const keys2 = buildMenuSelectKeys(Number(indexArg));
16152
- await client.call("terminal.write", { id, dataB64: Buffer.from(keys2, "utf8").toString("base64") });
17509
+ const index = Number(indexArg);
17510
+ const answered = await client.call("session.choose", { id, index }).catch((err) => {
17511
+ if (!/unknown method/i.test(String(err)))
17512
+ throw err;
17513
+ throw new Error(t("cli.choose.daemonTooOld"));
17514
+ });
16153
17515
  console.log(t("cli.session.selected", { index: indexArg }));
17516
+ if (answered.answered === "onboarding")
17517
+ console.log(t("cli.session.selectedDialog"));
16154
17518
  break;
16155
17519
  }
16156
17520
  case "setup": {
@@ -16226,8 +17590,8 @@ async function main2() {
16226
17590
  case "project": {
16227
17591
  const sub = positional[0] ?? "list";
16228
17592
  if (sub === "add") {
16229
- const dir = positional[1] ? path23.resolve(positional[1]) : process.cwd();
16230
- const alias = positional[2] ?? path23.basename(dir);
17593
+ const dir = positional[1] ? path25.resolve(positional[1]) : process.cwd();
17594
+ const alias = positional[2] ?? path25.basename(dir);
16231
17595
  const projects = await client.call("project.add", {
16232
17596
  alias,
16233
17597
  path: dir