@layorixinc/axterm 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1875 -681
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -31,8 +31,169 @@ var __toESM = (mod, isNodeMode, target) => {
31
31
  return to;
32
32
  };
33
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
+ var __returnValue = (v) => v;
35
+ function __exportSetter(name, newValue) {
36
+ this[name] = __returnValue.bind(null, newValue);
37
+ }
38
+ var __export = (target, all) => {
39
+ for (var name in all)
40
+ __defProp(target, name, {
41
+ get: all[name],
42
+ enumerable: true,
43
+ configurable: true,
44
+ set: __exportSetter.bind(all, name)
45
+ });
46
+ };
34
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
35
48
 
49
+ // packages/core/src/agent-registry.ts
50
+ function agentProfile(id) {
51
+ const known = PROFILES[id];
52
+ if (known)
53
+ return known;
54
+ return {
55
+ id,
56
+ displayName: id,
57
+ title: {
58
+ workingLabel: id,
59
+ permissionLabel: `${id} - action required`,
60
+ idleLabel: `${id} ready`
61
+ },
62
+ hostHookSource: id
63
+ };
64
+ }
65
+ function renderAgentTitle(profile, state, frame) {
66
+ if (state === undefined)
67
+ return;
68
+ const { title } = profile;
69
+ if (state === "working") {
70
+ if (title.synthesizeWorking === false)
71
+ return;
72
+ const spinner = SPINNER_FRAMES[(frame % SPINNER_FRAMES.length + SPINNER_FRAMES.length) % SPINNER_FRAMES.length];
73
+ return `${spinner} ${title.workingLabel}`;
74
+ }
75
+ if (title.synthesizeTerminal === false)
76
+ return;
77
+ return state === "done" ? title.idleLabel : title.permissionLabel;
78
+ }
79
+ function oscTitle(title) {
80
+ const safe = title.replace(/[\x00-\x1f\x7f]/g, "").slice(0, MAX_TITLE_LENGTH);
81
+ return `\x1B]0;${safe}\x07`;
82
+ }
83
+ var SPINNER_FRAMES, MENU_APPROVE = "\r", PROFILES, REGISTERED_AGENTS, MAX_TITLE_LENGTH = 200;
84
+ var init_agent_registry = __esm(() => {
85
+ SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
86
+ PROFILES = {
87
+ omp: {
88
+ id: "omp",
89
+ displayName: "OMP",
90
+ title: { workingLabel: "OMP", permissionLabel: "OMP - action required", idleLabel: "OMP ready" },
91
+ hostHookSource: "omp",
92
+ approvalBypassArgs: ["--approval-mode", "yolo"],
93
+ quota: "gauge",
94
+ resume: { argv: ["--resume", "{ref}"], ref: "sessionFile" }
95
+ },
96
+ pi: {
97
+ id: "pi",
98
+ displayName: "Pi",
99
+ title: { workingLabel: "Pi", permissionLabel: "Pi - action required", idleLabel: "Pi ready" },
100
+ hostHookSource: "pi",
101
+ resume: { argv: ["--session", "{ref}"], ref: "sessionId" }
102
+ },
103
+ codex: {
104
+ id: "codex",
105
+ displayName: "Codex",
106
+ title: {
107
+ workingLabel: "Codex",
108
+ permissionLabel: "Codex - action required",
109
+ idleLabel: "Codex ready",
110
+ synthesizeWorking: false
111
+ },
112
+ hostHookSource: "codex",
113
+ quota: "gauge",
114
+ approvalBypassArgs: ["--dangerously-bypass-approvals-and-sandbox"],
115
+ structuredApprovals: true,
116
+ resume: { argv: ["resume", "{ref}"], ref: "sessionId", form: "subcommand" }
117
+ },
118
+ opencode: {
119
+ id: "opencode",
120
+ displayName: "OpenCode",
121
+ title: {
122
+ workingLabel: "OpenCode",
123
+ permissionLabel: "OpenCode - action required",
124
+ idleLabel: "OpenCode ready",
125
+ synthesizeTerminal: false
126
+ },
127
+ hostHookSource: "opencode",
128
+ resume: { argv: ["--session", "{ref}"], ref: "sessionId" }
129
+ },
130
+ cursor: {
131
+ id: "cursor",
132
+ displayName: "Cursor Agent",
133
+ title: {
134
+ workingLabel: "Cursor Agent",
135
+ permissionLabel: "Cursor - action required",
136
+ idleLabel: "Cursor ready"
137
+ },
138
+ hostHookSource: "cursor",
139
+ resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
140
+ },
141
+ droid: {
142
+ id: "droid",
143
+ displayName: "Droid",
144
+ title: { workingLabel: "Droid", permissionLabel: "Droid - action required", idleLabel: "Droid ready" },
145
+ hostHookSource: "droid",
146
+ resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
147
+ },
148
+ claude: {
149
+ id: "claude",
150
+ displayName: "Claude Code",
151
+ title: {
152
+ workingLabel: "Claude",
153
+ permissionLabel: "Claude - action required",
154
+ idleLabel: "Claude ready",
155
+ synthesizeWorking: false,
156
+ synthesizeTerminal: false
157
+ },
158
+ hostHookSource: "claude",
159
+ quota: "event",
160
+ approvalBypassArgs: ["--permission-mode", "bypassPermissions"],
161
+ resume: { argv: ["--resume", "{ref}"], ref: "sessionId", selfNamed: "uuid" }
162
+ },
163
+ gemini: {
164
+ id: "gemini",
165
+ displayName: "Gemini CLI",
166
+ title: {
167
+ workingLabel: "Gemini",
168
+ permissionLabel: "Gemini - action required",
169
+ idleLabel: "Gemini ready",
170
+ synthesizeWorking: false,
171
+ synthesizeTerminal: false
172
+ },
173
+ hostHookSource: "gemini",
174
+ approvalBypassArgs: ["--approval-mode", "yolo"],
175
+ hookAck: "{}",
176
+ approvalKeys: { allow: MENU_APPROVE, deny: "\x1B" },
177
+ resume: { argv: ["--resume", "{ref}"], ref: "sessionId", selfNamed: "bare" }
178
+ }
179
+ };
180
+ REGISTERED_AGENTS = Object.keys(PROFILES);
181
+ });
182
+
183
+ // packages/core/src/session-id.ts
184
+ function uuidFromSessionId(sessionId) {
185
+ const hex = sessionId.replace(/-/g, "");
186
+ if (hex.length !== 32)
187
+ throw new Error(`session id is not 32 hex characters: ${sessionId}`);
188
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
189
+ }
190
+ function sessionIdFromUuid(ref) {
191
+ if (!ref.includes("-"))
192
+ return;
193
+ const stripped = ref.replace(/-/g, "");
194
+ return /^[0-9a-f]{4,32}$/i.test(stripped) ? stripped.toLowerCase() : undefined;
195
+ }
196
+
36
197
  // packages/core/src/mcp-config.ts
37
198
  function mcpServerEntry(launch, contextPath) {
38
199
  return {
@@ -63,12 +224,6 @@ function codexMcpArgs(launch, contextPath) {
63
224
  var MCP_SERVER_NAME = "axterm", MCP_CONTEXT_ENV = "AXTERM_MCP_CONTEXT";
64
225
 
65
226
  // packages/core/src/agent-launch.ts
66
- function uuidFromSessionId(sessionId) {
67
- const hex = sessionId.replace(/-/g, "");
68
- if (hex.length !== 32)
69
- throw new Error(`session id is not 32 hex characters: ${sessionId}`);
70
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
71
- }
72
227
  function withAppServer(argv, url) {
73
228
  const out = [];
74
229
  for (let i = 0;i < argv.length; i++) {
@@ -82,6 +237,15 @@ function withAppServer(argv, url) {
82
237
  return out;
83
238
  return [out[0], "--remote", url, ...out.slice(1)];
84
239
  }
240
+ function resumesAConversation(args) {
241
+ return args.some((arg) => RESUME_FLAGS.has(arg) || arg.startsWith("--resume=") || arg.startsWith("--continue="));
242
+ }
243
+ function namingFlag(agent, sessionId, extra) {
244
+ const named = agentProfile(agent).resume?.selfNamed;
245
+ if (named === undefined || resumesAConversation(extra))
246
+ return [];
247
+ return ["--session-id", named === "uuid" ? uuidFromSessionId(sessionId) : sessionId];
248
+ }
85
249
  function buildAgentCommand(input) {
86
250
  const extra = [...input.extraArgs ?? []];
87
251
  if (input.agent === "claude") {
@@ -90,16 +254,14 @@ function buildAgentCommand(input) {
90
254
  "--settings",
91
255
  input.claudeSettingsPath,
92
256
  ...input.mcpConfigPath === undefined ? [] : ["--mcp-config", input.mcpConfigPath],
93
- "--session-id",
94
- uuidFromSessionId(input.sessionId),
257
+ ...namingFlag("claude", input.sessionId, extra),
95
258
  ...extra
96
259
  ];
97
260
  }
98
261
  if (input.agent === "gemini") {
99
262
  return [
100
263
  "gemini",
101
- "--session-id",
102
- input.sessionId,
264
+ ...namingFlag("gemini", input.sessionId, extra),
103
265
  ...extra
104
266
  ];
105
267
  }
@@ -136,30 +298,16 @@ function resolveAgentArgs(input) {
136
298
  });
137
299
  return groups.filter((group, index) => group.flag === undefined || lastIndex.get(group.flag) === index).flatMap((group) => group.tokens);
138
300
  }
139
- function buildAgentResumeCommand(input) {
140
- if (input.agent === "claude") {
141
- return [
142
- "claude",
143
- "--settings",
144
- input.claudeSettingsPath,
145
- "--resume",
146
- uuidFromSessionId(input.sessionId),
147
- ...input.extraArgs ?? []
148
- ];
149
- }
150
- if (input.agent === "gemini") {
151
- return ["gemini", "--resume", input.sessionId, ...input.extraArgs ?? []];
152
- }
153
- return buildAgentCommand(input);
154
- }
155
301
  function isAgentId(value) {
156
302
  return AGENT_IDS.includes(value);
157
303
  }
158
304
  function supportedAgents() {
159
305
  return AGENT_IDS;
160
306
  }
161
- var AGENT_IDS;
307
+ var RESUME_FLAGS, AGENT_IDS;
162
308
  var init_agent_launch = __esm(() => {
309
+ init_agent_registry();
310
+ RESUME_FLAGS = new Set(["--resume", "-r", "--continue", "-c"]);
163
311
  AGENT_IDS = ["omp", "claude", "codex", "gemini"];
164
312
  });
165
313
 
@@ -406,6 +554,9 @@ function isRecord(value) {
406
554
  }
407
555
 
408
556
  // packages/core/src/config-schema.ts
557
+ function isStagingMode(value) {
558
+ return typeof value === "string" && STAGING_MODES.includes(value);
559
+ }
409
560
  function resolveStreamConfig(file) {
410
561
  return {
411
562
  enabled: file?.enabled ?? DEFAULT_STREAM_CONFIG.enabled,
@@ -499,7 +650,7 @@ function parseConfig(raw) {
499
650
  }
500
651
  const defaults = raw["defaults"];
501
652
  if (isRecord(defaults)) {
502
- checkKeys(defaults, ["agent", "shell", "autoApprove", "agentArgs"], "defaults.", errors);
653
+ checkKeys(defaults, ["agent", "shell", "autoApprove", "agentArgs", "staging"], "defaults.", errors);
503
654
  const agent = defaults["agent"];
504
655
  const shell = defaults["shell"];
505
656
  const rawArgs = defaults["agentArgs"];
@@ -518,11 +669,16 @@ function parseConfig(raw) {
518
669
  if (autoApprove !== undefined && typeof autoApprove !== "boolean") {
519
670
  errors.push("defaults.autoApprove must be true or false");
520
671
  }
672
+ const staging = defaults["staging"];
673
+ if (staging !== undefined && !isStagingMode(staging)) {
674
+ errors.push(`defaults.staging must be one of: ${STAGING_MODES.join(", ")}`);
675
+ }
521
676
  config.defaults = {
522
677
  ...typeof agent === "string" ? { agent } : {},
523
678
  ...typeof shell === "string" ? { shell } : {},
524
679
  ...typeof autoApprove === "boolean" ? { autoApprove } : {},
525
- ...Object.keys(agentArgs).length > 0 ? { agentArgs } : {}
680
+ ...Object.keys(agentArgs).length > 0 ? { agentArgs } : {},
681
+ ...isStagingMode(staging) ? { staging } : {}
526
682
  };
527
683
  }
528
684
  const projects = raw["projects"];
@@ -631,6 +787,9 @@ function setConfigValue(config, dottedPath, rawValue) {
631
787
  const shown = Array.isArray(rawValue) ? rawValue.join(" ") : rawValue;
632
788
  return { error: `"${shown}" is not a valid ${type} for ${dottedPath}` };
633
789
  }
790
+ if (dottedPath === "defaults.staging" && !isStagingMode(value)) {
791
+ return { error: `"${String(value)}" is not a valid location \u2014 use one of: ${STAGING_MODES.join(", ")}` };
792
+ }
634
793
  const next = structuredClone(config);
635
794
  const segments = dottedPath.split(".");
636
795
  let cursor = next;
@@ -674,10 +833,11 @@ function resolveEnvRefs(config, env) {
674
833
  }
675
834
  return resolved;
676
835
  }
677
- var CONFIG_VERSION = 1, DEFAULT_CONFIG, DEFAULT_STREAM_CONFIG, DEFAULT_INBOUND_CONFIG, FIELDS, SECRET_SUFFIXES, ENV_REF;
836
+ var STAGING_MODES, CONFIG_VERSION = 1, DEFAULT_CONFIG, DEFAULT_STREAM_CONFIG, DEFAULT_INBOUND_CONFIG, FIELDS, SECRET_SUFFIXES, ENV_REF;
678
837
  var init_config_schema = __esm(() => {
679
838
  init_agent_launch();
680
839
  init_channel_spi();
840
+ STAGING_MODES = ["project", "runtime"];
681
841
  DEFAULT_CONFIG = {
682
842
  version: CONFIG_VERSION,
683
843
  projects: [],
@@ -703,6 +863,7 @@ var init_config_schema = __esm(() => {
703
863
  "defaults.agent": "string",
704
864
  "defaults.shell": "string",
705
865
  "defaults.autoApprove": "boolean",
866
+ "defaults.staging": "string",
706
867
  "channels.telegram.enabled": "boolean",
707
868
  "channels.telegram.groupId": "number",
708
869
  "channels.telegram.allowUserIds": "number[]",
@@ -768,6 +929,7 @@ function resolvePaths(input) {
768
929
  extDir: join(stateDir, "ext"),
769
930
  runtimeFile: join(runtimeDir, "runtime.json"),
770
931
  hookEndpointFile: join(runtimeDir, "hook-endpoint.env"),
932
+ ptyHostFile: join(runtimeDir, "pty-host.json"),
771
933
  legacyRoots: legacyRoots.filter((dir) => dir !== stateDir)
772
934
  };
773
935
  }
@@ -876,6 +1038,7 @@ locale: set AXTERM_LANG=en|ko, or \`axterm config set locale ko\`
876
1038
  "cli.session.sent": "sent",
877
1039
  "cli.session.sentKey": "sent {key}",
878
1040
  "cli.session.selected": "selected option {index}",
1041
+ "cli.session.selectedDialog": "answered the agent's first-run dialog",
879
1042
  "cli.resume.done": "resumed session {id} ({command})",
880
1043
  "cli.update.current": "axterm {version} is the latest version",
881
1044
  "cli.update.available": "axterm {latest} is available (you have {current})",
@@ -945,7 +1108,7 @@ locale: set AXTERM_LANG=en|ko, or \`axterm config set locale ko\`
945
1108
  "err.telegram.noToken": "no bot token is configured \u2014 run `axterm setup` or pass --token",
946
1109
  "tg.help.general.header": "axterm commands (General topic):",
947
1110
  "tg.help.general.projects": "/projects \u2014 registered projects",
948
- "tg.help.general.new": "/new <project> [name] \u2014 new omp session",
1111
+ "tg.help.general.new": "/new <project> [name] [-- agent flags] \u2014 new session (e.g. --resume <conversation>)",
949
1112
  "tg.help.general.wt": "/wt <project> <branch> \u2014 worktree session",
950
1113
  "tg.help.general.fanout": "/fanout <project> <count|branches> <prompt> \u2014 run in parallel",
951
1114
  "tg.help.general.worktrees": "/worktrees <project> \xB7 /rm <project> <branch> [force]",
@@ -967,6 +1130,9 @@ locale: set AXTERM_LANG=en|ko, or \`axterm config set locale ko\`
967
1130
  "tg.prompt.confirm": "the session is waiting on a confirmation:",
968
1131
  "tg.prompt.secretAsk": "the session is asking for a secret. Reply with it and the message will be deleted:",
969
1132
  "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",
1133
+ "tg.prompt.secretSubmitted": "the secret reached the session",
1134
+ "tg.prompt.secretStale": "that prompt is no longer waiting, so nothing was typed into the session",
1135
+ "tg.prompt.secretUndeleted": "\u26A0\uFE0F Telegram refused to delete that message \u2014 the secret is still in the chat; delete it yourself",
970
1136
  "tg.session.screenFile": "terminal screen",
971
1137
  "tg.session.none": "no sessions",
972
1138
  "tg.prompt.failed": "\u26A0\uFE0F Delivery failed: {error}",
@@ -1044,6 +1210,8 @@ usage: /fanout <project> <count|branches> <prompt>`,
1044
1210
  "doctor.bridge.missing": "hook bridge: {command} does not exist \u2014 codex and gemini hooks would fail once per tool call",
1045
1211
  "doctor.agent.installed": "{agent}: installed ({path})",
1046
1212
  "doctor.agent.notInstalled": "{agent}: not installed",
1213
+ "doctor.staging.project": "inbound files: written to <session dir>/.axterm/inbound (hidden from git; set defaults.staging=runtime for a synced folder)",
1214
+ "doctor.staging.runtime": "inbound files: written to axterm's own runtime directory (defaults.staging=runtime)",
1047
1215
  "doctor.omp.extensionOk": "omp extension: deployed ({path})",
1048
1216
  "doctor.omp.extensionMissing": "omp extension: missing at {path} \u2014 no omp session can report status",
1049
1217
  "doctor.gemini.settingsOk": "gemini hooks: defaults file valid ({events} events subscribed)",
@@ -1082,6 +1250,10 @@ usage: /fanout <project> <count|branches> <prompt>`,
1082
1250
  "err.session.dialogOpen": "session {id} is waiting on a dialog ({state}); answer it with the buttons or /esc first",
1083
1251
  "err.agent.notInstalled": "the agent command '{command}' was not found on PATH \u2014 install it, or pass --cmd to run something else",
1084
1252
  "err.agent.unknown": "unknown agent '{agent}' \u2014 supported: {supported}",
1253
+ "err.staging.dir": "could not create the session's staging directory {dir} ({error}) \u2014 fix the permissions, or set defaults.staging to runtime",
1254
+ "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",
1255
+ "err.claude.conversationBusy": "session {session} is already in conversation {id} \u2014 two sessions writing one transcript would corrupt it; kill that one first",
1256
+ "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",
1085
1257
  "codex.approval.denied": "Denied from the axterm channel",
1086
1258
  "err.codex.noAuth": "codex is not logged in ({authFile} not found) \u2014 run `codex login` once, then start the session again",
1087
1259
  "err.codex.authLink": "could not link codex credentials into axterm's CODEX_HOME ({reason}) \u2014 refusing to start a logged-out agent",
@@ -1172,6 +1344,7 @@ usage: /fanout <project> <count|branches> <prompt>`,
1172
1344
  "cli.session.sent": "\uC804\uB2EC\uB428",
1173
1345
  "cli.session.sentKey": "{key} \uC804\uB2EC\uB428",
1174
1346
  "cli.session.selected": "{index}\uBC88 \uD56D\uBAA9 \uC120\uD0DD\uB428",
1347
+ "cli.session.selectedDialog": "\uC5D0\uC774\uC804\uD2B8\uC758 \uCCAB \uC2E4\uD589 \uB300\uD654\uC0C1\uC790\uC5D0 \uC751\uB2F5\uD588\uC2B5\uB2C8\uB2E4",
1175
1348
  "cli.resume.done": "\uC138\uC158 {id} \uC7AC\uAC1C\uB428 ({command})",
1176
1349
  "cli.update.current": "axterm {version} \uC774 \uCD5C\uC2E0 \uBC84\uC804\uC785\uB2C8\uB2E4",
1177
1350
  "cli.update.available": "axterm {latest} \uC744(\uB97C) \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4 (\uD604\uC7AC {current})",
@@ -1234,7 +1407,7 @@ usage: /fanout <project> <count|branches> <prompt>`,
1234
1407
  "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.",
1235
1408
  "tg.help.general.header": "axterm \uBA85\uB839 (General \uD1A0\uD53D):",
1236
1409
  "tg.help.general.projects": "/projects \u2014 \uB4F1\uB85D\uB41C \uD504\uB85C\uC81D\uD2B8",
1237
- "tg.help.general.new": "/new <\uD504\uB85C\uC81D\uD2B8> [\uC774\uB984] \u2014 \uC0C8 omp \uC138\uC158",
1410
+ "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>)",
1238
1411
  "tg.help.general.wt": "/wt <\uD504\uB85C\uC81D\uD2B8> <\uBE0C\uB79C\uCE58> \u2014 worktree \uC138\uC158",
1239
1412
  "tg.help.general.fanout": "/fanout <\uD504\uB85C\uC81D\uD2B8> <\uAC1C\uC218|\uBE0C\uB79C\uCE58\uB4E4> <\uD504\uB86C\uD504\uD2B8> \u2014 \uBCD1\uB82C \uC2E4\uD589",
1240
1413
  "tg.help.general.worktrees": "/worktrees <\uD504\uB85C\uC81D\uD2B8> \xB7 /rm <\uD504\uB85C\uC81D\uD2B8> <\uBE0C\uB79C\uCE58> [force]",
@@ -1263,6 +1436,9 @@ usage: /fanout <project> <count|branches> <prompt>`,
1263
1436
  "tg.prompt.confirm": "\uC138\uC158\uC774 \uD655\uC778\uC744 \uAE30\uB2E4\uB9AC\uACE0 \uC788\uC2B5\uB2C8\uB2E4:",
1264
1437
  "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:",
1265
1438
  "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",
1439
+ "tg.prompt.secretSubmitted": "\uBE44\uBC00\uAC12\uC774 \uC138\uC158\uC5D0 \uC804\uB2EC\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
1440
+ "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",
1441
+ "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",
1266
1442
  "tg.session.screenFile": "\uD130\uBBF8\uB110 \uD654\uBA74",
1267
1443
  "tg.session.none": "\uC138\uC158 \uC5C6\uC74C",
1268
1444
  "tg.prompt.failed": "\u26A0\uFE0F \uC804\uB2EC \uC2E4\uD328: {error}",
@@ -1340,6 +1516,8 @@ usage: /fanout <project> <count|branches> <prompt>`,
1340
1516
  "doctor.bridge.missing": "\uD6C5 \uBE0C\uB9AC\uC9C0: {command} \uC5C6\uC74C \u2014 codex\xB7gemini \uD6C5\uC774 \uD234 \uD638\uCD9C\uB9C8\uB2E4 \uC2E4\uD328\uD558\uAC8C \uB429\uB2C8\uB2E4",
1341
1517
  "doctor.agent.installed": "{agent}: \uC124\uCE58\uB428 ({path})",
1342
1518
  "doctor.agent.notInstalled": "{agent}: \uBBF8\uC124\uCE58",
1519
+ "doctor.staging.project": "\uC778\uBC14\uC6B4\uB4DC \uD30C\uC77C: <\uC138\uC158 \uB514\uB809\uD130\uB9AC>/.axterm/inbound \uC5D0 \uC501\uB2C8\uB2E4 (git\uC5D0\uC11C \uAC10\uCDB0\uC9D0; \uB3D9\uAE30\uD654 \uD3F4\uB354\uB77C\uBA74 defaults.staging=runtime)",
1520
+ "doctor.staging.runtime": "\uC778\uBC14\uC6B4\uB4DC \uD30C\uC77C: axterm \uC790\uCCB4 \uB7F0\uD0C0\uC784 \uB514\uB809\uD130\uB9AC\uC5D0 \uC501\uB2C8\uB2E4 (defaults.staging=runtime)",
1343
1521
  "doctor.omp.extensionOk": "omp \uD655\uC7A5: \uBC30\uD3EC\uB428 ({path})",
1344
1522
  "doctor.omp.extensionMissing": "omp \uD655\uC7A5: {path} \uC5C6\uC74C \u2014 \uC5B4\uB5A4 omp \uC138\uC158\uB3C4 \uC0C1\uD0DC\uB97C \uBCF4\uACE0\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
1345
1523
  "doctor.gemini.settingsOk": "gemini \uD6C5: defaults \uD30C\uC77C \uC815\uC0C1 ({events}\uAC1C \uC774\uBCA4\uD2B8 \uAD6C\uB3C5)",
@@ -1378,6 +1556,10 @@ usage: /fanout <project> <count|branches> <prompt>`,
1378
1556
  "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",
1379
1557
  "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",
1380
1558
  "err.agent.unknown": "\uC54C \uC218 \uC5C6\uB294 \uC5D0\uC774\uC804\uD2B8 '{agent}' \uC785\uB2C8\uB2E4. \uC9C0\uC6D0: {supported}",
1559
+ "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",
1560
+ "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",
1561
+ "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",
1562
+ "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",
1381
1563
  "codex.approval.denied": "axterm \uCC44\uB110\uC5D0\uC11C \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
1382
1564
  "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",
1383
1565
  "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",
@@ -1567,144 +1749,16 @@ var init_setup_wizard = __esm(() => {
1567
1749
  wizardConfig = new WeakMap;
1568
1750
  });
1569
1751
 
1570
- // packages/core/src/agent-registry.ts
1571
- function agentProfile(id) {
1572
- const known = PROFILES[id];
1573
- if (known)
1574
- return known;
1575
- return {
1576
- id,
1577
- displayName: id,
1578
- title: {
1579
- workingLabel: id,
1580
- permissionLabel: `${id} - action required`,
1581
- idleLabel: `${id} ready`
1582
- },
1583
- hostHookSource: id
1584
- };
1585
- }
1586
- function renderAgentTitle(profile, state, frame) {
1587
- if (state === undefined)
1588
- return;
1589
- const { title } = profile;
1590
- if (state === "working") {
1591
- if (title.synthesizeWorking === false)
1592
- return;
1593
- const spinner = SPINNER_FRAMES[(frame % SPINNER_FRAMES.length + SPINNER_FRAMES.length) % SPINNER_FRAMES.length];
1594
- return `${spinner} ${title.workingLabel}`;
1595
- }
1596
- if (title.synthesizeTerminal === false)
1597
- return;
1598
- return state === "done" ? title.idleLabel : title.permissionLabel;
1599
- }
1600
- function oscTitle(title) {
1601
- const safe = title.replace(/[\x00-\x1f\x7f]/g, "").slice(0, MAX_TITLE_LENGTH);
1602
- return `\x1B]0;${safe}\x07`;
1603
- }
1604
- var SPINNER_FRAMES, MENU_APPROVE = "\r", PROFILES, REGISTERED_AGENTS, MAX_TITLE_LENGTH = 200;
1605
- var init_agent_registry = __esm(() => {
1606
- SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1607
- PROFILES = {
1608
- omp: {
1609
- id: "omp",
1610
- displayName: "OMP",
1611
- title: { workingLabel: "OMP", permissionLabel: "OMP - action required", idleLabel: "OMP ready" },
1612
- hostHookSource: "omp",
1613
- approvalBypassArgs: ["--approval-mode", "yolo"],
1614
- quota: "gauge",
1615
- resume: { argv: ["--resume", "{ref}"], ref: "sessionFile" }
1616
- },
1617
- pi: {
1618
- id: "pi",
1619
- displayName: "Pi",
1620
- title: { workingLabel: "Pi", permissionLabel: "Pi - action required", idleLabel: "Pi ready" },
1621
- hostHookSource: "pi",
1622
- resume: { argv: ["--session", "{ref}"], ref: "sessionId" }
1623
- },
1624
- codex: {
1625
- id: "codex",
1626
- displayName: "Codex",
1627
- title: {
1628
- workingLabel: "Codex",
1629
- permissionLabel: "Codex - action required",
1630
- idleLabel: "Codex ready",
1631
- synthesizeWorking: false
1632
- },
1633
- hostHookSource: "codex",
1634
- quota: "gauge",
1635
- approvalBypassArgs: ["--dangerously-bypass-approvals-and-sandbox"],
1636
- structuredApprovals: true,
1637
- resume: { argv: ["resume", "{ref}"], ref: "sessionId", form: "subcommand" }
1638
- },
1639
- opencode: {
1640
- id: "opencode",
1641
- displayName: "OpenCode",
1642
- title: {
1643
- workingLabel: "OpenCode",
1644
- permissionLabel: "OpenCode - action required",
1645
- idleLabel: "OpenCode ready",
1646
- synthesizeTerminal: false
1647
- },
1648
- hostHookSource: "opencode",
1649
- resume: { argv: ["--session", "{ref}"], ref: "sessionId" }
1650
- },
1651
- cursor: {
1652
- id: "cursor",
1653
- displayName: "Cursor Agent",
1654
- title: {
1655
- workingLabel: "Cursor Agent",
1656
- permissionLabel: "Cursor - action required",
1657
- idleLabel: "Cursor ready"
1658
- },
1659
- hostHookSource: "cursor",
1660
- resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
1661
- },
1662
- droid: {
1663
- id: "droid",
1664
- displayName: "Droid",
1665
- title: { workingLabel: "Droid", permissionLabel: "Droid - action required", idleLabel: "Droid ready" },
1666
- hostHookSource: "droid",
1667
- resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
1668
- },
1669
- claude: {
1670
- id: "claude",
1671
- displayName: "Claude Code",
1672
- title: {
1673
- workingLabel: "Claude",
1674
- permissionLabel: "Claude - action required",
1675
- idleLabel: "Claude ready",
1676
- synthesizeWorking: false,
1677
- synthesizeTerminal: false
1678
- },
1679
- hostHookSource: "claude",
1680
- quota: "event",
1681
- approvalBypassArgs: ["--permission-mode", "bypassPermissions"],
1682
- resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
1683
- },
1684
- gemini: {
1685
- id: "gemini",
1686
- displayName: "Gemini CLI",
1687
- title: {
1688
- workingLabel: "Gemini",
1689
- permissionLabel: "Gemini - action required",
1690
- idleLabel: "Gemini ready",
1691
- synthesizeWorking: false,
1692
- synthesizeTerminal: false
1693
- },
1694
- hostHookSource: "gemini",
1695
- approvalBypassArgs: ["--approval-mode", "yolo"],
1696
- hookAck: "{}",
1697
- approvalKeys: { allow: MENU_APPROVE, deny: "\x1B" },
1698
- resume: { argv: ["--resume", "{ref}"], ref: "sessionId" }
1699
- }
1700
- };
1701
- REGISTERED_AGENTS = Object.keys(PROFILES);
1702
- });
1703
-
1704
1752
  // packages/core/src/agent-resume.ts
1705
1753
  function resumeReference(profile, source) {
1706
- const value = profile.resume?.ref === "sessionId" ? source.agentSessionId : source.agentSessionFile;
1707
- return value !== undefined && value.length > 0 ? value : undefined;
1754
+ const reported = profile.resume?.ref === "sessionId" ? source.agentSessionId : source.agentSessionFile;
1755
+ if (reported !== undefined && reported.length > 0)
1756
+ return reported;
1757
+ const named = profile.resume?.selfNamed;
1758
+ const own = source.ownSessionId;
1759
+ if (named === undefined || own === undefined || own.length === 0)
1760
+ return;
1761
+ return named === "uuid" ? uuidFromSessionId(own) : own;
1708
1762
  }
1709
1763
  function agentIdFromCommand(command) {
1710
1764
  const head = command[0] ?? "";
@@ -1731,17 +1785,23 @@ function withoutExistingResume(command, fragment) {
1731
1785
  }
1732
1786
  return command;
1733
1787
  }
1788
+ function withoutNamingFlag(command) {
1789
+ const at = command.indexOf("--session-id");
1790
+ return at < 0 ? command : [...command.slice(0, at), ...command.slice(at + 2)];
1791
+ }
1734
1792
  function buildResumeCommand(command, profile, reference) {
1735
1793
  const resume = profile.resume;
1736
1794
  const binary = command[0];
1737
1795
  if (!resume || binary === undefined || reference.length === 0)
1738
1796
  return;
1739
1797
  const template = resume.argv;
1740
- const base = withoutExistingResume(command, template);
1798
+ const stripped = withoutExistingResume(command, template);
1799
+ const base = resume.selfNamed === undefined ? stripped : withoutNamingFlag(stripped);
1741
1800
  const fragment = template.map((token) => token.replaceAll(REF, reference));
1742
1801
  return resume.form === "subcommand" ? [base[0], ...fragment, ...base.slice(1)] : [...base, ...fragment];
1743
1802
  }
1744
1803
  var REF = "{ref}";
1804
+ var init_agent_resume = () => {};
1745
1805
 
1746
1806
  // packages/core/src/self-spawn.ts
1747
1807
  function daemonLaunch(input) {
@@ -1763,9 +1823,16 @@ function mcpLaunch(input) {
1763
1823
  function isMcpInvocation(args) {
1764
1824
  return args[0] === MCP_ARGV_FLAG;
1765
1825
  }
1766
- function hookPostTarget(env, agent) {
1767
- const port = Number(env["AXTERM_HOOK_PORT"]);
1768
- const token = env["AXTERM_HOOK_TOKEN"];
1826
+ function ptyHostLaunch(input) {
1827
+ const base = daemonLaunch(input);
1828
+ return { command: base.command, args: [...base.args.slice(0, -1), PTY_HOST_ARGV_FLAG] };
1829
+ }
1830
+ function isPtyHostInvocation(args) {
1831
+ return args[0] === PTY_HOST_ARGV_FLAG;
1832
+ }
1833
+ function hookPostTarget(env, agent, override) {
1834
+ const port = override?.port ?? Number(env["AXTERM_HOOK_PORT"]);
1835
+ const token = override?.token ?? env["AXTERM_HOOK_TOKEN"];
1769
1836
  const sessionId = env["AXTERM_SESSION_ID"];
1770
1837
  const launchToken = env["AXTERM_LAUNCH_TOKEN"];
1771
1838
  if (!Number.isSafeInteger(port) || port <= 0)
@@ -1782,10 +1849,17 @@ function hookPostTarget(env, agent) {
1782
1849
  }
1783
1850
  };
1784
1851
  }
1852
+ function parseHookEndpoint(text) {
1853
+ const port = Number(/^AXTERM_HOOK_PORT=(\d+)$/m.exec(text)?.[1]);
1854
+ const token = /^AXTERM_HOOK_TOKEN=(\S+)$/m.exec(text)?.[1];
1855
+ if (!Number.isSafeInteger(port) || port <= 0 || !token)
1856
+ return;
1857
+ return { port, token };
1858
+ }
1785
1859
  function isDaemonInvocation(args) {
1786
1860
  return args[0] === DAEMON_ARGV_FLAG;
1787
1861
  }
1788
- var DAEMON_ARGV_FLAG = "__daemon", HOOK_ARGV_FLAG = "__hook", MCP_ARGV_FLAG = "__mcp", RUNTIME_NAMES;
1862
+ var DAEMON_ARGV_FLAG = "__daemon", HOOK_ARGV_FLAG = "__hook", MCP_ARGV_FLAG = "__mcp", PTY_HOST_ARGV_FLAG = "__pty-host", RUNTIME_NAMES;
1789
1863
  var init_self_spawn = __esm(() => {
1790
1864
  RUNTIME_NAMES = new Set(["bun", "bun.exe", "bun-debug", "bun-debug.exe", "node", "node.exe"]);
1791
1865
  });
@@ -1907,6 +1981,7 @@ function planUpdateRestart(input) {
1907
1981
  var SEMVER;
1908
1982
  var init_update = __esm(() => {
1909
1983
  init_agent_registry();
1984
+ init_agent_resume();
1910
1985
  SEMVER = /^\d+\.\d+\.\d+/;
1911
1986
  });
1912
1987
 
@@ -2107,6 +2182,30 @@ function buildMenuSelectKeys(index, normalizePresses = MENU_NORMALIZE_PRESSES) {
2107
2182
  }
2108
2183
  var MENU_UP = "\x1B[A", MENU_DOWN = "\x1B[B", MENU_SELECT = "\r", MENU_NORMALIZE_PRESSES = 24;
2109
2184
 
2185
+ // packages/core/src/adopt.ts
2186
+ function resolveSessionCwd(input) {
2187
+ if (!input.preferConversation)
2188
+ return input.requested;
2189
+ const own = input.conversationCwd;
2190
+ return own !== undefined && own.length > 0 ? own : input.requested;
2191
+ }
2192
+ function conversationHeldBy(sessions, conversationId) {
2193
+ if (conversationId.length === 0)
2194
+ return;
2195
+ return sessions.find((session) => {
2196
+ if (session.state !== "running")
2197
+ return false;
2198
+ if (session.agentSessionId === conversationId)
2199
+ return true;
2200
+ const argv = session.agentCommand;
2201
+ return argv.some((token, index) => {
2202
+ if (token === "--resume" || token === "-r" || token === "--session-id")
2203
+ return argv[index + 1] === conversationId;
2204
+ return token === `--resume=${conversationId}`;
2205
+ });
2206
+ });
2207
+ }
2208
+
2110
2209
  // packages/core/src/agent-status.ts
2111
2210
  function next(prev, state, now) {
2112
2211
  return {
@@ -2116,17 +2215,29 @@ function next(prev, state, now) {
2116
2215
  stateStartedAt: prev?.state === state ? prev.stateStartedAt : now
2117
2216
  };
2118
2217
  }
2218
+ function withConversationId(events, identity) {
2219
+ const id = identity.sessionId;
2220
+ const subagent = identity.subagentId;
2221
+ if (typeof id !== "string" || id === "" || typeof subagent === "string" && subagent !== "") {
2222
+ return [...events];
2223
+ }
2224
+ return events.map((event) => ({ session_id: id, ...event }));
2225
+ }
2119
2226
  function reduceAgentState(prev, payload, now) {
2227
+ const result = reduceState(prev, payload, now);
2228
+ const identity = {
2229
+ ...payload.session_id !== undefined ? { agentSessionId: payload.session_id } : {},
2230
+ ...payload.session_file !== undefined ? { agentSessionFile: payload.session_file } : {},
2231
+ ...payload.session_name !== undefined ? { sessionName: payload.session_name } : {}
2232
+ };
2233
+ if (Object.keys(identity).length === 0)
2234
+ return result;
2235
+ return { ...result, meta: { ...identity, ...result.meta } };
2236
+ }
2237
+ function reduceState(prev, payload, now) {
2120
2238
  switch (payload.event) {
2121
2239
  case "session_start":
2122
- return {
2123
- entry: prev,
2124
- meta: {
2125
- agentSessionId: payload.session_id,
2126
- agentSessionFile: payload.session_file,
2127
- sessionName: payload.session_name
2128
- }
2129
- };
2240
+ return { entry: prev };
2130
2241
  case "before_agent_start": {
2131
2242
  const entry = next(prev, prev?.state ?? "working", now);
2132
2243
  entry.prompt = payload.prompt ?? "";
@@ -2218,7 +2329,102 @@ var init_agent_status = __esm(() => {
2218
2329
  ]);
2219
2330
  });
2220
2331
 
2332
+ // packages/core/src/pty-protocol.ts
2333
+ function encodeFrame(kind, id, payload) {
2334
+ const idBytes = new TextEncoder().encode(id);
2335
+ if (idBytes.length > 255)
2336
+ throw new Error(`session id too long for a frame header: ${idBytes.length} bytes`);
2337
+ const frame = new Uint8Array(HEADER_BYTES + idBytes.length + payload.byteLength);
2338
+ frame[0] = kind;
2339
+ frame[1] = idBytes.length;
2340
+ new DataView(frame.buffer, frame.byteOffset, frame.byteLength).setUint32(2, payload.byteLength, true);
2341
+ frame.set(idBytes, HEADER_BYTES);
2342
+ frame.set(payload, HEADER_BYTES + idBytes.length);
2343
+ return frame;
2344
+ }
2345
+ function isFrameKind(value) {
2346
+ return value === FrameKind.Control || value === FrameKind.Data;
2347
+ }
2348
+
2349
+ class FrameDecoder {
2350
+ buffer = new Uint8Array(0);
2351
+ push(chunk) {
2352
+ this.buffer = concat(this.buffer, chunk);
2353
+ const frames = [];
2354
+ for (;; ) {
2355
+ if (this.buffer.byteLength < HEADER_BYTES)
2356
+ return frames;
2357
+ const kind = this.buffer[0];
2358
+ if (!isFrameKind(kind))
2359
+ throw new Error(`unknown frame kind ${kind}`);
2360
+ const idLength = this.buffer[1];
2361
+ const payloadLength = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength).getUint32(2, true);
2362
+ if (payloadLength > MAX_FRAME_BYTES) {
2363
+ throw new Error(`frame too large: ${payloadLength} bytes declared, ceiling is ${MAX_FRAME_BYTES}`);
2364
+ }
2365
+ const total = HEADER_BYTES + idLength + payloadLength;
2366
+ if (this.buffer.byteLength < total)
2367
+ return frames;
2368
+ const id = new TextDecoder().decode(this.buffer.subarray(HEADER_BYTES, HEADER_BYTES + idLength));
2369
+ const payload = this.buffer.slice(HEADER_BYTES + idLength, total);
2370
+ frames.push({ kind, id, payload });
2371
+ this.buffer = this.buffer.slice(total);
2372
+ }
2373
+ }
2374
+ }
2375
+ function concat(left, right) {
2376
+ if (left.byteLength === 0)
2377
+ return right.slice();
2378
+ const joined = new Uint8Array(left.byteLength + right.byteLength);
2379
+ joined.set(left, 0);
2380
+ joined.set(right, left.byteLength);
2381
+ return joined;
2382
+ }
2383
+ function parseControl(payload) {
2384
+ let parsed;
2385
+ try {
2386
+ parsed = JSON.parse(new TextDecoder().decode(payload));
2387
+ } catch {
2388
+ return;
2389
+ }
2390
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
2391
+ return;
2392
+ const type = parsed.t;
2393
+ if (typeof type !== "string" || !KNOWN_TYPES.has(type))
2394
+ return;
2395
+ return parsed;
2396
+ }
2397
+ function controlFrame(message) {
2398
+ return encodeFrame(FrameKind.Control, "", new TextEncoder().encode(JSON.stringify(message)));
2399
+ }
2400
+ var PTY_PROTOCOL_VERSION = 1, FrameKind, MAX_FRAME_BYTES, HEADER_BYTES = 6, KNOWN_TYPES;
2401
+ var init_pty_protocol = __esm(() => {
2402
+ FrameKind = {
2403
+ Control: 1,
2404
+ Data: 2
2405
+ };
2406
+ MAX_FRAME_BYTES = 8 * 1024 * 1024;
2407
+ KNOWN_TYPES = new Set([
2408
+ "hello",
2409
+ "spawn",
2410
+ "subscribe",
2411
+ "resize",
2412
+ "kill",
2413
+ "welcome",
2414
+ "spawned",
2415
+ "subscribed",
2416
+ "exit",
2417
+ "error"
2418
+ ]);
2419
+ });
2420
+
2221
2421
  // packages/core/src/workspace.ts
2422
+ function scratchDir(sessionDir) {
2423
+ return `${sessionDir.replace(/[/\\]+$/, "")}/${SCRATCH_DIR_NAME}`;
2424
+ }
2425
+ function inboundStagingDir(sessionDir) {
2426
+ return `${scratchDir(sessionDir)}/inbound`;
2427
+ }
2222
2428
  function planMaterialisedPath(stagingDir, sessionId, remotePath) {
2223
2429
  const parts = remotePath.split(/[/\\]/);
2224
2430
  let base = "";
@@ -2239,7 +2445,8 @@ function planMaterialisedPath(stagingDir, sessionId, remotePath) {
2239
2445
  }
2240
2446
  return `${stagingDir}/${sessionId}/${name}`;
2241
2447
  }
2242
- var MAX_NAME = 120;
2448
+ var SCRATCH_DIR_NAME = ".axterm", SCRATCH_IGNORE_NAME = ".gitignore", SCRATCH_IGNORE_BODY = `*
2449
+ `, MAX_NAME = 120;
2243
2450
 
2244
2451
  // packages/core/src/claude-hooks.ts
2245
2452
  function resultText(response) {
@@ -2403,6 +2610,8 @@ var init_onboarding = __esm(() => {
2403
2610
  { kind: "authMethod", question: "How would you like to authenticate for this project?" }
2404
2611
  ],
2405
2612
  claude: [
2613
+ { kind: "folderTrust", question: "Quick safety check: Is this a project you created or one you trust?" },
2614
+ { kind: "autoMode", question: "Set up auto mode for your environment?" },
2406
2615
  { kind: "apiKey", question: "Do you want to use this API key?" },
2407
2616
  { kind: "loginMethod", question: "Select login method:" }
2408
2617
  ],
@@ -2430,6 +2639,15 @@ function evaluateVersionDrift(input) {
2430
2639
  }
2431
2640
  ];
2432
2641
  }
2642
+ function evaluateStagingDoctor(facts) {
2643
+ return [
2644
+ {
2645
+ id: "staging.mode",
2646
+ status: "info",
2647
+ key: facts.mode === "runtime" ? "doctor.staging.runtime" : "doctor.staging.project"
2648
+ }
2649
+ ];
2650
+ }
2433
2651
  function binaryCheck(agent, binary) {
2434
2652
  return binary === undefined ? { id: `${agent}.binary`, status: "info", key: "doctor.agent.notInstalled", params: { agent } } : { id: `${agent}.binary`, status: "ok", key: "doctor.agent.installed", params: { agent, path: binary } };
2435
2653
  }
@@ -3435,6 +3653,9 @@ function sessionsByRecency(live, stored) {
3435
3653
  var asGiven = (target) => target;
3436
3654
  var init_session_scope = () => {};
3437
3655
 
3656
+ // packages/core/src/test-scratch.ts
3657
+ var init_test_scratch = () => {};
3658
+
3438
3659
  // packages/core/src/index.ts
3439
3660
  function isRpcEvent(f) {
3440
3661
  return typeof f.ev === "string";
@@ -3454,6 +3675,7 @@ var init_src = __esm(() => {
3454
3675
  init_config_paths();
3455
3676
  init_setup_wizard();
3456
3677
  init_agent_registry();
3678
+ init_agent_resume();
3457
3679
  init_self_spawn();
3458
3680
  init_channel_spi();
3459
3681
  init_update();
@@ -3463,6 +3685,7 @@ var init_src = __esm(() => {
3463
3685
  init_choice();
3464
3686
  init_agent_status();
3465
3687
  init_activity();
3688
+ init_pty_protocol();
3466
3689
  init_claude_hooks();
3467
3690
  init_onboarding();
3468
3691
  init_doctor();
@@ -3480,19 +3703,20 @@ var init_src = __esm(() => {
3480
3703
  init_dotenv_scrub();
3481
3704
  init_prompt_detect();
3482
3705
  init_session_scope();
3706
+ init_test_scratch();
3483
3707
  SUBMIT_DELAY_MS = process.platform === "win32" ? 1500 : 500;
3484
3708
  PASTE_CHUNK_BYTES = 16 * 1024;
3485
3709
  });
3486
3710
 
3487
3711
  // packages/daemon/src/config-store.ts
3488
- import fs5 from "fs";
3712
+ import fs6 from "fs";
3489
3713
  import path6 from "path";
3490
3714
  function writeAtomic(target, contents, mode) {
3491
3715
  const tmp = `${target}.tmp`;
3492
- fs5.writeFileSync(tmp, contents, { mode });
3493
- fs5.renameSync(tmp, target);
3716
+ fs6.writeFileSync(tmp, contents, { mode });
3717
+ fs6.renameSync(tmp, target);
3494
3718
  if (process.platform !== "win32")
3495
- fs5.chmodSync(target, mode);
3719
+ fs6.chmodSync(target, mode);
3496
3720
  }
3497
3721
 
3498
3722
  class ConfigStore {
@@ -3508,16 +3732,16 @@ class ConfigStore {
3508
3732
  }
3509
3733
  load() {
3510
3734
  this.loadError = undefined;
3511
- fs5.mkdirSync(this.paths.configDir, { recursive: true, mode: DIR_MODE });
3735
+ fs6.mkdirSync(this.paths.configDir, { recursive: true, mode: DIR_MODE });
3512
3736
  if (process.platform !== "win32") {
3513
3737
  try {
3514
- fs5.chmodSync(this.paths.configDir, DIR_MODE);
3738
+ fs6.chmodSync(this.paths.configDir, DIR_MODE);
3515
3739
  } catch {}
3516
3740
  }
3517
3741
  this.config = structuredClone(DEFAULT_CONFIG);
3518
- if (fs5.existsSync(this.paths.configFile)) {
3742
+ if (fs6.existsSync(this.paths.configFile)) {
3519
3743
  try {
3520
- const parsed = parseConfig(JSON.parse(fs5.readFileSync(this.paths.configFile, "utf8")));
3744
+ const parsed = parseConfig(JSON.parse(fs6.readFileSync(this.paths.configFile, "utf8")));
3521
3745
  if ("errors" in parsed)
3522
3746
  this.loadError = parsed.errors.join("; ");
3523
3747
  else
@@ -3527,10 +3751,10 @@ class ConfigStore {
3527
3751
  }
3528
3752
  }
3529
3753
  this.secrets = {};
3530
- if (fs5.existsSync(this.paths.secretsFile)) {
3754
+ if (fs6.existsSync(this.paths.secretsFile)) {
3531
3755
  this.tightenPermissions(this.paths.secretsFile);
3532
3756
  try {
3533
- const raw = JSON.parse(fs5.readFileSync(this.paths.secretsFile, "utf8"));
3757
+ const raw = JSON.parse(fs6.readFileSync(this.paths.secretsFile, "utf8"));
3534
3758
  if (raw !== null && typeof raw === "object") {
3535
3759
  for (const [key, value] of Object.entries(raw)) {
3536
3760
  if (typeof value === "string")
@@ -3546,8 +3770,8 @@ class ConfigStore {
3546
3770
  if (process.platform === "win32")
3547
3771
  return;
3548
3772
  try {
3549
- if ((fs5.statSync(file).mode & 63) !== 0)
3550
- fs5.chmodSync(file, FILE_MODE);
3773
+ if ((fs6.statSync(file).mode & 63) !== 0)
3774
+ fs6.chmodSync(file, FILE_MODE);
3551
3775
  } catch {}
3552
3776
  }
3553
3777
  get() {
@@ -3555,7 +3779,7 @@ class ConfigStore {
3555
3779
  }
3556
3780
  health() {
3557
3781
  return configHealth({
3558
- fileExists: fs5.existsSync(this.paths.configFile),
3782
+ fileExists: fs6.existsSync(this.paths.configFile),
3559
3783
  parseError: this.loadError,
3560
3784
  config: this.resolved()
3561
3785
  });
@@ -3600,7 +3824,7 @@ class ConfigStore {
3600
3824
  delete this.secrets[dottedPath];
3601
3825
  else
3602
3826
  this.secrets[dottedPath] = value;
3603
- if (!fs5.existsSync(this.paths.configFile)) {
3827
+ if (!fs6.existsSync(this.paths.configFile)) {
3604
3828
  writeAtomic(this.paths.configFile, `${JSON.stringify(this.config, null, 2)}
3605
3829
  `, FILE_MODE);
3606
3830
  }
@@ -3630,7 +3854,7 @@ class ConfigStore {
3630
3854
  fingerprint() {
3631
3855
  const stamp = (file) => {
3632
3856
  try {
3633
- return String(fs5.statSync(file).mtimeMs);
3857
+ return String(fs6.statSync(file).mtimeMs);
3634
3858
  } catch {
3635
3859
  return "-";
3636
3860
  }
@@ -3672,7 +3896,7 @@ var init_config_store = __esm(() => {
3672
3896
  });
3673
3897
 
3674
3898
  // packages/daemon/src/shell.ts
3675
- import fs6 from "fs";
3899
+ import fs7 from "fs";
3676
3900
  import path7 from "path";
3677
3901
  function findExecutable(name, env) {
3678
3902
  const isWindows = env.platform === "win32";
@@ -3696,7 +3920,7 @@ function lookupOnPath(name) {
3696
3920
  platform: process.platform,
3697
3921
  pathEnv: process.env["PATH"] ?? "",
3698
3922
  pathExt: process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD",
3699
- exists: (candidate) => fs6.existsSync(candidate)
3923
+ exists: (candidate) => fs7.existsSync(candidate)
3700
3924
  });
3701
3925
  }
3702
3926
  function resolveShell(kind) {
@@ -3733,7 +3957,7 @@ function resolvePosixShell(kind) {
3733
3957
  candidates.push({ kind: "bash", exe: login });
3734
3958
  candidates.push({ kind: "bash", exe: "/bin/bash" }, { kind: "bash", exe: "/usr/bin/bash" });
3735
3959
  }
3736
- const found = candidates.find((c) => fs6.existsSync(c.exe));
3960
+ const found = candidates.find((c) => fs7.existsSync(c.exe));
3737
3961
  if (found)
3738
3962
  return found;
3739
3963
  return { kind: "sh", exe: "/bin/sh" };
@@ -3784,18 +4008,18 @@ var cachedPwsh;
3784
4008
  var init_shell = () => {};
3785
4009
 
3786
4010
  // packages/daemon/src/paths.ts
3787
- import fs7 from "fs";
4011
+ import fs8 from "fs";
3788
4012
  import os3 from "os";
3789
4013
  import path8 from "path";
3790
4014
  function ensureDataDirs() {
3791
4015
  for (const dir of [PATHS.configDir, PATHS.stateDir, PATHS.runtimeDir, SCROLLBACK_DIR, LOG_DIR, EXT_DIR]) {
3792
- fs7.mkdirSync(dir, { recursive: true, mode: DIR_MODE2 });
4016
+ fs8.mkdirSync(dir, { recursive: true, mode: DIR_MODE2 });
3793
4017
  }
3794
4018
  if (process.platform === "win32")
3795
4019
  return;
3796
4020
  for (const dir of [PATHS.configDir, PATHS.stateDir, PATHS.runtimeDir]) {
3797
4021
  try {
3798
- fs7.chmodSync(dir, DIR_MODE2);
4022
+ fs8.chmodSync(dir, DIR_MODE2);
3799
4023
  } catch {}
3800
4024
  }
3801
4025
  }
@@ -3803,14 +4027,14 @@ function restrictFile(file) {
3803
4027
  if (process.platform === "win32")
3804
4028
  return;
3805
4029
  try {
3806
- fs7.chmodSync(file, 384);
4030
+ fs8.chmodSync(file, 384);
3807
4031
  } catch {}
3808
4032
  }
3809
4033
  function log(message) {
3810
4034
  const line = `${new Date().toISOString()} ${message}
3811
4035
  `;
3812
4036
  try {
3813
- fs7.appendFileSync(LOG_FILE, line);
4037
+ fs8.appendFileSync(LOG_FILE, line);
3814
4038
  } catch {}
3815
4039
  }
3816
4040
  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;
@@ -3834,7 +4058,7 @@ var init_paths = __esm(() => {
3834
4058
  });
3835
4059
 
3836
4060
  // packages/daemon/src/store.ts
3837
- import fs8 from "fs";
4061
+ import fs9 from "fs";
3838
4062
  import path9 from "path";
3839
4063
  import { Database } from "bun:sqlite";
3840
4064
  function rowToInfo(row) {
@@ -3866,7 +4090,7 @@ function rowToInfo(row) {
3866
4090
  class Store {
3867
4091
  db;
3868
4092
  constructor(dbPath = DB_PATH) {
3869
- fs8.mkdirSync(path9.dirname(dbPath), { recursive: true });
4093
+ fs9.mkdirSync(path9.dirname(dbPath), { recursive: true });
3870
4094
  this.db = new Database(dbPath, { strict: true });
3871
4095
  this.db.run("PRAGMA journal_mode = WAL");
3872
4096
  this.db.run(`
@@ -3945,8 +4169,10 @@ class Store {
3945
4169
  this.db.prepare(`UPDATE sessions SET state = 'running', exit_code = NULL, exited_at = NULL,
3946
4170
  agent_command = ?, pty_pid = ? WHERE id = ?`).run(JSON.stringify(agentCommand), ptyPid ?? null, id);
3947
4171
  }
3948
- markStaleRunningAsExited() {
3949
- return this.db.prepare(`UPDATE sessions SET state = 'exited', exited_at = ? WHERE state != 'exited'`).run(Date.now()).changes;
4172
+ markStaleRunningAsExited(keep = []) {
4173
+ const holes = keep.map(() => "?").join(",");
4174
+ 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})`;
4175
+ return this.db.prepare(sql).run(Date.now(), ...keep).changes;
3950
4176
  }
3951
4177
  listSessions() {
3952
4178
  const rows = this.db.prepare(`SELECT * FROM sessions ORDER BY created_at DESC`).all();
@@ -3961,7 +4187,11 @@ class Store {
3961
4187
  return existed;
3962
4188
  }
3963
4189
  findByPrefix(idOrPrefix) {
3964
- const rows = this.db.prepare(`SELECT * FROM sessions WHERE id = ? OR short_id = ? OR id LIKE ? ORDER BY created_at DESC`).all(idOrPrefix, idOrPrefix, `${idOrPrefix}%`);
4190
+ const alternate = sessionIdFromUuid(idOrPrefix) ?? idOrPrefix;
4191
+ const rows = this.db.prepare(`SELECT * FROM sessions
4192
+ WHERE id = ? OR short_id = ? OR id LIKE ?
4193
+ OR id = ? OR short_id = ? OR id LIKE ?
4194
+ ORDER BY created_at DESC`).all(idOrPrefix, idOrPrefix, `${idOrPrefix}%`, alternate, alternate, `${alternate}%`);
3965
4195
  const row = rows[0];
3966
4196
  return row ? rowToInfo(row) : undefined;
3967
4197
  }
@@ -4023,15 +4253,16 @@ class Store {
4023
4253
  }
4024
4254
  }
4025
4255
  var init_store = __esm(() => {
4256
+ init_src();
4026
4257
  init_paths();
4027
4258
  });
4028
4259
 
4029
4260
  // packages/daemon/src/config-migrate.ts
4030
- import fs9 from "fs";
4261
+ import fs10 from "fs";
4031
4262
  import path10 from "path";
4032
4263
  import { Database as Database2 } from "bun:sqlite";
4033
4264
  function checkpointWal(dbFile) {
4034
- if (!fs9.existsSync(dbFile))
4265
+ if (!fs10.existsSync(dbFile))
4035
4266
  return;
4036
4267
  try {
4037
4268
  const db = new Database2(dbFile);
@@ -4041,30 +4272,30 @@ function checkpointWal(dbFile) {
4041
4272
  }
4042
4273
  function relocate(source, target) {
4043
4274
  try {
4044
- fs9.renameSync(source, target);
4275
+ fs10.renameSync(source, target);
4045
4276
  } catch {
4046
- fs9.cpSync(source, target, { recursive: true });
4277
+ fs10.cpSync(source, target, { recursive: true });
4047
4278
  }
4048
4279
  }
4049
4280
  function moveInto(from, to, notes) {
4050
4281
  checkpointWal(path10.join(from, "axterm.db"));
4051
- fs9.mkdirSync(to, { recursive: true });
4282
+ fs10.mkdirSync(to, { recursive: true });
4052
4283
  for (const entry of STATE_ENTRIES) {
4053
4284
  const source = path10.join(from, entry);
4054
4285
  const target = path10.join(to, entry);
4055
- if (!fs9.existsSync(source))
4286
+ if (!fs10.existsSync(source))
4056
4287
  continue;
4057
- if (!fs9.existsSync(target)) {
4288
+ if (!fs10.existsSync(target)) {
4058
4289
  relocate(source, target);
4059
4290
  notes.push(`moved ${entry}`);
4060
4291
  continue;
4061
4292
  }
4062
- if (!fs9.statSync(source).isDirectory() || !fs9.statSync(target).isDirectory())
4293
+ if (!fs10.statSync(source).isDirectory() || !fs10.statSync(target).isDirectory())
4063
4294
  continue;
4064
4295
  let moved = 0;
4065
- for (const child of fs9.readdirSync(source)) {
4296
+ for (const child of fs10.readdirSync(source)) {
4066
4297
  const childTarget = path10.join(target, child);
4067
- if (fs9.existsSync(childTarget))
4298
+ if (fs10.existsSync(childTarget))
4068
4299
  continue;
4069
4300
  relocate(path10.join(source, child), childTarget);
4070
4301
  moved += 1;
@@ -4076,29 +4307,29 @@ function moveInto(from, to, notes) {
4076
4307
  function migrateLegacyConfig(input) {
4077
4308
  const notes = [];
4078
4309
  const storePaths = defaultConfigStorePaths(input.configDir);
4079
- if (fs9.existsSync(storePaths.configFile))
4310
+ if (fs10.existsSync(storePaths.configFile))
4080
4311
  return { migrated: false, notes };
4081
- const configSource = input.legacyRoots.find((dir) => fs9.existsSync(path10.join(dir, "config.json")));
4312
+ const configSource = input.legacyRoots.find((dir) => fs10.existsSync(path10.join(dir, "config.json")));
4082
4313
  if (configSource !== undefined) {
4083
- fs9.mkdirSync(input.configDir, { recursive: true });
4084
- fs9.copyFileSync(path10.join(configSource, "config.json"), storePaths.configFile);
4314
+ fs10.mkdirSync(input.configDir, { recursive: true });
4315
+ fs10.copyFileSync(path10.join(configSource, "config.json"), storePaths.configFile);
4085
4316
  const secrets = path10.join(configSource, "secrets.json");
4086
- if (fs9.existsSync(secrets)) {
4087
- fs9.copyFileSync(secrets, storePaths.secretsFile);
4317
+ if (fs10.existsSync(secrets)) {
4318
+ fs10.copyFileSync(secrets, storePaths.secretsFile);
4088
4319
  try {
4089
- fs9.chmodSync(storePaths.secretsFile, 384);
4320
+ fs10.chmodSync(storePaths.secretsFile, 384);
4090
4321
  } catch {}
4091
4322
  }
4092
- fs9.writeFileSync(path10.join(configSource, "MIGRATED.txt"), `axterm now reads its configuration from:
4323
+ fs10.writeFileSync(path10.join(configSource, "MIGRATED.txt"), `axterm now reads its configuration from:
4093
4324
  ${input.configDir}
4094
4325
  ` + `These files are no longer used. secrets.json still holds your bot token \u2014 delete this directory once you are satisfied.
4095
4326
  `);
4096
4327
  notes.push(`adopted configuration from ${configSource}`);
4097
4328
  }
4098
- const legacy = input.legacyRoots.find((dir) => fs9.existsSync(path10.join(dir, "axterm.db")));
4329
+ const legacy = input.legacyRoots.find((dir) => fs10.existsSync(path10.join(dir, "axterm.db")));
4099
4330
  if (legacy !== undefined) {
4100
4331
  moveInto(legacy, input.stateDir, notes);
4101
- fs9.writeFileSync(path10.join(legacy, "MIGRATED.txt"), `axterm now stores state in:
4332
+ fs10.writeFileSync(path10.join(legacy, "MIGRATED.txt"), `axterm now stores state in:
4102
4333
  ${input.stateDir}
4103
4334
  and configuration in:
4104
4335
  ${input.configDir}
@@ -4108,7 +4339,7 @@ and configuration in:
4108
4339
  if (configSource !== undefined)
4109
4340
  return { migrated: true, ...legacy ? { from: legacy } : {}, notes };
4110
4341
  const dbFile = path10.join(input.stateDir, "axterm.db");
4111
- if (!fs9.existsSync(dbFile))
4342
+ if (!fs10.existsSync(dbFile))
4112
4343
  return { migrated: notes.length > 0, ...legacy ? { from: legacy } : {}, notes };
4113
4344
  const store = new Store(dbFile);
4114
4345
  const machine2 = store.getConfig("axterm_machine");
@@ -4946,13 +5177,13 @@ export default function axtermAgentStatus(pi: PiLike): void {\r
4946
5177
  var init_axterm_agent_status = () => {};
4947
5178
 
4948
5179
  // packages/daemon/src/ext-install.ts
4949
- import fs10 from "fs";
5180
+ import fs11 from "fs";
4950
5181
  import path11 from "path";
4951
5182
  function installExtensionAsset(targetDir) {
4952
- fs10.mkdirSync(targetDir, { recursive: true });
5183
+ fs11.mkdirSync(targetDir, { recursive: true });
4953
5184
  const target = path11.join(targetDir, "axterm-agent-status.mjs");
4954
5185
  const js = new Bun.Transpiler({ loader: "ts" }).transformSync(extensionSource);
4955
- fs10.writeFileSync(target, MANAGED_HEADER + js);
5186
+ fs11.writeFileSync(target, MANAGED_HEADER + js);
4956
5187
  return target;
4957
5188
  }
4958
5189
  var extensionSource, MANAGED_HEADER = `// @axterm-managed-omp-extension
@@ -4965,7 +5196,7 @@ var init_ext_install = __esm(() => {
4965
5196
 
4966
5197
  // packages/daemon/src/workspace-local.ts
4967
5198
  import { spawn as spawn2 } from "child_process";
4968
- import fs11 from "fs/promises";
5199
+ import fs12 from "fs/promises";
4969
5200
  import path12 from "path";
4970
5201
  function isAbsent(error) {
4971
5202
  const code = error?.code;
@@ -4979,10 +5210,10 @@ class LocalWorkspace {
4979
5210
  this.stagingDir = stagingDir;
4980
5211
  }
4981
5212
  async list(dir) {
4982
- const dirents = await fs11.readdir(dir, { withFileTypes: true });
5213
+ const dirents = await fs12.readdir(dir, { withFileTypes: true });
4983
5214
  const stats = await Promise.all(dirents.map(async (dirent) => {
4984
5215
  try {
4985
- return await fs11.lstat(path12.join(dir, dirent.name));
5216
+ return await fs12.lstat(path12.join(dir, dirent.name));
4986
5217
  } catch (error) {
4987
5218
  if (isAbsent(error))
4988
5219
  return;
@@ -5006,7 +5237,7 @@ class LocalWorkspace {
5006
5237
  }
5007
5238
  async stat(target) {
5008
5239
  try {
5009
- const stat = await fs11.stat(target);
5240
+ const stat = await fs12.stat(target);
5010
5241
  const kind = stat.isFile() ? "file" : stat.isDirectory() ? "dir" : "other";
5011
5242
  return { kind, size: stat.size, mtimeMs: stat.mtimeMs };
5012
5243
  } catch (error) {
@@ -5017,8 +5248,8 @@ class LocalWorkspace {
5017
5248
  }
5018
5249
  async read(file, limit) {
5019
5250
  if (limit === undefined)
5020
- return await fs11.readFile(file);
5021
- const handle = await fs11.open(file, "r");
5251
+ return await fs12.readFile(file);
5252
+ const handle = await fs12.open(file, "r");
5022
5253
  try {
5023
5254
  const buffer = Buffer.allocUnsafe(Math.min(limit, (await handle.stat()).size));
5024
5255
  let filled = 0;
@@ -5036,6 +5267,12 @@ class LocalWorkspace {
5036
5267
  materialise(file) {
5037
5268
  return Promise.resolve(file);
5038
5269
  }
5270
+ async mkdirp(dir, mode) {
5271
+ await fs12.mkdir(dir, mode === undefined ? { recursive: true } : { recursive: true, mode });
5272
+ }
5273
+ async write(file, bytes, mode) {
5274
+ await fs12.writeFile(file, bytes, mode === undefined ? undefined : { mode });
5275
+ }
5039
5276
  exec(argv, cwd) {
5040
5277
  const [command, ...args] = argv;
5041
5278
  if (command === undefined)
@@ -5063,7 +5300,7 @@ var init_workspace_local = () => {};
5063
5300
 
5064
5301
  // packages/daemon/src/hook-host.ts
5065
5302
  import { randomBytes } from "crypto";
5066
- import fs12 from "fs";
5303
+ import fs13 from "fs";
5067
5304
  import http from "http";
5068
5305
  import path13 from "path";
5069
5306
  function header(req, name) {
@@ -5079,6 +5316,7 @@ class HookHost {
5079
5316
  }
5080
5317
  sessions = new Map;
5081
5318
  pendingAsks = new Map;
5319
+ refusedHooks = new Set;
5082
5320
  endpointFile;
5083
5321
  server;
5084
5322
  askOpenHandler;
@@ -5105,16 +5343,19 @@ class HookHost {
5105
5343
  return promise;
5106
5344
  }
5107
5345
  writeEndpointFile(port) {
5108
- fs12.mkdirSync(path13.dirname(this.endpointFile), { recursive: true });
5346
+ fs13.mkdirSync(path13.dirname(this.endpointFile), { recursive: true });
5109
5347
  const tmp = `${this.endpointFile}.tmp`;
5110
- fs12.writeFileSync(tmp, `AXTERM_HOOK_PORT=${port}
5348
+ fs13.writeFileSync(tmp, `AXTERM_HOOK_PORT=${port}
5111
5349
  AXTERM_HOOK_TOKEN=${this.token}
5112
5350
  `);
5113
- fs12.renameSync(tmp, this.endpointFile);
5351
+ fs13.renameSync(tmp, this.endpointFile);
5114
5352
  }
5115
5353
  registerSession(sessionId, launchToken, onEvent) {
5116
5354
  this.sessions.set(sessionId, { launchToken, onEvent });
5117
5355
  }
5356
+ launchMatches(session, presented) {
5357
+ return session.launchToken === undefined || session.launchToken === presented;
5358
+ }
5118
5359
  unregisterSession(sessionId) {
5119
5360
  this.sessions.delete(sessionId);
5120
5361
  for (const [askId, pending] of this.pendingAsks) {
@@ -5203,7 +5444,7 @@ AXTERM_HOOK_TOKEN=${this.token}
5203
5444
  res.writeHead(404).end();
5204
5445
  return;
5205
5446
  }
5206
- if (body.launchToken !== session.launchToken) {
5447
+ if (!this.launchMatches(session, body.launchToken)) {
5207
5448
  res.writeHead(403).end();
5208
5449
  return;
5209
5450
  }
@@ -5238,7 +5479,9 @@ AXTERM_HOOK_TOKEN=${this.token}
5238
5479
  return;
5239
5480
  }
5240
5481
  res.writeHead(200).end();
5241
- this.dispatchHookPayloads(auth.session, map(input), agent);
5482
+ const named = input;
5483
+ const events = withConversationId(map(input), { sessionId: named.session_id, subagentId: named.agent_id });
5484
+ this.dispatchHookPayloads(auth.session, events, agent);
5242
5485
  }
5243
5486
  handleCodex(req, raw, res) {
5244
5487
  const auth = this.agentHookSession(req);
@@ -5294,8 +5537,15 @@ AXTERM_HOOK_TOKEN=${this.token}
5294
5537
  const sessionId = header(req, "x-axterm-session");
5295
5538
  const launchToken = header(req, "x-axterm-launch");
5296
5539
  const session = sessionId === undefined ? undefined : this.sessions.get(sessionId);
5297
- if (sessionId === undefined || !session || session.launchToken !== launchToken)
5540
+ if (sessionId === undefined || !session || !this.launchMatches(session, launchToken)) {
5541
+ const why = sessionId === undefined ? "no session header" : !session ? "unknown session" : "launch token mismatch";
5542
+ const key = `${req.url}\x00${sessionId ?? ""}\x00${why}`;
5543
+ if (!this.refusedHooks.has(key)) {
5544
+ this.refusedHooks.add(key);
5545
+ log(`refused hook post to ${req.url}: ${why} (session ${sessionId ?? "-"}) \u2014 reported once per session`);
5546
+ }
5298
5547
  return;
5548
+ }
5299
5549
  return { sessionId, session };
5300
5550
  }
5301
5551
  dispatchHookPayloads(session, payloads, agent) {
@@ -5385,7 +5635,7 @@ var init_hook_host = __esm(() => {
5385
5635
  });
5386
5636
 
5387
5637
  // packages/daemon/src/rpc-server.ts
5388
- import fs13 from "fs";
5638
+ import fs14 from "fs";
5389
5639
  import net2 from "net";
5390
5640
  function writeFrame(socket, frame) {
5391
5641
  if (!socket.destroyed)
@@ -5495,6 +5745,8 @@ function startRpcServer(options) {
5495
5745
  session.write(Buffer.from(p["dataB64"], "base64").toString("utf8"));
5496
5746
  return {};
5497
5747
  }
5748
+ case "session.choose":
5749
+ return { answered: manager.answerScreenChoice(p["id"], p["index"]) };
5498
5750
  case "attach": {
5499
5751
  const session = manager.resolveLive(p["id"]);
5500
5752
  cleanup();
@@ -5621,7 +5873,7 @@ function startRpcServer(options) {
5621
5873
  });
5622
5874
  if (process.platform !== "win32") {
5623
5875
  try {
5624
- fs13.unlinkSync(pipePath);
5876
+ fs14.unlinkSync(pipePath);
5625
5877
  } catch {}
5626
5878
  }
5627
5879
  server.listen(pipePath, () => {
@@ -5636,18 +5888,35 @@ var init_rpc_server = __esm(() => {
5636
5888
  });
5637
5889
 
5638
5890
  // packages/daemon/src/codex-home.ts
5639
- import fs14 from "fs";
5891
+ import fs15 from "fs";
5640
5892
  import path14 from "path";
5641
5893
  function codexTrustKeyPath(home) {
5642
5894
  const resolved = (() => {
5643
5895
  try {
5644
- return fs14.realpathSync.native(home);
5896
+ return fs15.realpathSync.native(home);
5645
5897
  } catch {
5646
5898
  return path14.resolve(home);
5647
5899
  }
5648
5900
  })();
5649
5901
  return path14.join(resolved, "hooks.json");
5650
5902
  }
5903
+ function referenceCredential(source, target, ops) {
5904
+ const symlink = ops?.symlink ?? ((s, t2) => fs15.symlinkSync(s, t2, "file"));
5905
+ const link = ops?.link ?? fs15.linkSync;
5906
+ try {
5907
+ symlink(source, target);
5908
+ return "symlink";
5909
+ } catch {
5910
+ try {
5911
+ link(source, target);
5912
+ return "hardlink";
5913
+ } catch {
5914
+ fs15.copyFileSync(source, target);
5915
+ fs15.chmodSync(target, 384);
5916
+ return "copy";
5917
+ }
5918
+ }
5919
+ }
5651
5920
  var init_codex_home = () => {};
5652
5921
 
5653
5922
  // packages/daemon/src/host-env.ts
@@ -5662,15 +5931,36 @@ function stripHostAttribution(env) {
5662
5931
  }
5663
5932
  var ATTRIBUTION_PREFIXES, ATTRIBUTION_KEEP;
5664
5933
  var init_host_env = __esm(() => {
5665
- ATTRIBUTION_PREFIXES = ["ORCA_"];
5934
+ ATTRIBUTION_PREFIXES = ["ORCA_", "AXTERM_"];
5666
5935
  ATTRIBUTION_KEEP = new Set([
5667
5936
  "ORCA_CODEX_HOME",
5668
5937
  "ORCA_OPENCODE_CONFIG_DIR",
5669
5938
  "ORCA_USER_DATA_PATH",
5670
- "ORCA_APP_VERSION"
5939
+ "ORCA_APP_VERSION",
5940
+ "AXTERM_CONFIG_DIR",
5941
+ "AXTERM_STATE_DIR",
5942
+ "AXTERM_RUNTIME_DIR",
5943
+ "AXTERM_LANG",
5944
+ "AXTERM_PTY_HOST",
5945
+ "AXTERM_PTY_HOST_MAIN"
5671
5946
  ]);
5672
5947
  });
5673
5948
 
5949
+ // packages/daemon/src/session-env.ts
5950
+ function sessionEnv(base, identity) {
5951
+ return {
5952
+ ...stripHostAttribution(base),
5953
+ AXTERM_SESSION_ID: identity.sessionId,
5954
+ AXTERM_LAUNCH_TOKEN: identity.launchToken,
5955
+ AXTERM_HOOK_PORT: String(identity.hookPort),
5956
+ AXTERM_HOOK_TOKEN: identity.hookToken,
5957
+ AXTERM_HOOK_ENDPOINT: identity.endpointFile
5958
+ };
5959
+ }
5960
+ var init_session_env = __esm(() => {
5961
+ init_host_env();
5962
+ });
5963
+
5674
5964
  // packages/daemon/src/git-porcelain.ts
5675
5965
  function parseWorktreeList(porcelain) {
5676
5966
  const entries = [];
@@ -5709,7 +5999,7 @@ function slugifyBranch(branch) {
5709
5999
  var MAX_SLUG_LENGTH = 60;
5710
6000
 
5711
6001
  // packages/daemon/src/git.ts
5712
- import fs15 from "fs";
6002
+ import fs16 from "fs";
5713
6003
  import path15 from "path";
5714
6004
  async function git(ws, cwd, args) {
5715
6005
  const run = await ws.exec(["git", ...args], cwd);
@@ -5729,7 +6019,7 @@ async function gitOrThrow(ws, cwd, args) {
5729
6019
  function canonicalPath(target) {
5730
6020
  let resolved = path15.resolve(target);
5731
6021
  try {
5732
- resolved = fs15.realpathSync.native(resolved);
6022
+ resolved = fs16.realpathSync.native(resolved);
5733
6023
  } catch {}
5734
6024
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
5735
6025
  }
@@ -5753,7 +6043,7 @@ async function createWorktree(ws, options) {
5753
6043
  const existing = (await listWorktrees(ws, repoRoot)).find((w) => canonicalPath(w.path) === canonicalPath(target));
5754
6044
  if (existing)
5755
6045
  return { path: target, branch, createdBranch: false };
5756
- fs15.mkdirSync(path15.dirname(target), { recursive: true });
6046
+ fs16.mkdirSync(path15.dirname(target), { recursive: true });
5757
6047
  const exists = (await git(ws, repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`])).ok;
5758
6048
  const args = exists ? ["worktree", "add", target, branch] : ["worktree", "add", "-b", branch, target, ...base !== undefined ? [base] : []];
5759
6049
  await gitOrThrow(ws, repoRoot, args);
@@ -5782,6 +6072,14 @@ async function untrackedFiles(ws, worktreePath) {
5782
6072
  return [];
5783
6073
  return result.stdout.split(/\r?\n/).filter((line) => line.trim().length > 0);
5784
6074
  }
6075
+ async function ignoreState(ws, cwd, file) {
6076
+ const run = await ws.exec(["git", "check-ignore", "-q", file], cwd);
6077
+ if (run.code === 0)
6078
+ return "ignored";
6079
+ if (run.code === 1)
6080
+ return "visible";
6081
+ return "unknown";
6082
+ }
5785
6083
  async function countLines(ws, filePath) {
5786
6084
  try {
5787
6085
  const stat = await ws.stat(filePath);
@@ -5825,64 +6123,536 @@ async function changedFiles(ws, worktreePath, base, limit = 20) {
5825
6123
  names.add(line.trim());
5826
6124
  }
5827
6125
  }
5828
- for (const file of await untrackedFiles(ws, worktreePath))
5829
- names.add(file);
5830
- return [...names].slice(0, limit);
6126
+ for (const file of await untrackedFiles(ws, worktreePath))
6127
+ names.add(file);
6128
+ return [...names].slice(0, limit);
6129
+ }
6130
+ var UNTRACKED_LINE_SCAN_LIMIT;
6131
+ var init_git = __esm(() => {
6132
+ init_paths();
6133
+ UNTRACKED_LINE_SCAN_LIMIT = 1024 * 1024;
6134
+ });
6135
+
6136
+ // packages/daemon/src/claude-store.ts
6137
+ import fs17 from "fs";
6138
+ import os4 from "os";
6139
+ import path16 from "path";
6140
+ function claudeConfigDir(env = process.env) {
6141
+ const override = env["CLAUDE_CONFIG_DIR"];
6142
+ if (override !== undefined && override.length > 0)
6143
+ return override;
6144
+ return path16.join(os4.homedir(), ".claude");
6145
+ }
6146
+ function resumedConversationId(argv) {
6147
+ for (let i = 0;i < argv.length; i++) {
6148
+ const token = argv[i];
6149
+ if (token === "--resume" || token === "-r") {
6150
+ const value = argv[i + 1];
6151
+ return value !== undefined && CONVERSATION_ID.test(value) ? value : undefined;
6152
+ }
6153
+ const inline = /^--resume=(.+)$/.exec(token);
6154
+ if (inline)
6155
+ return CONVERSATION_ID.test(inline[1]) ? inline[1] : undefined;
6156
+ }
6157
+ return;
6158
+ }
6159
+ function claudeConversationExists(configDir, conversationId) {
6160
+ const projects = path16.join(configDir, "projects");
6161
+ let entries;
6162
+ try {
6163
+ entries = fs17.readdirSync(projects);
6164
+ } catch {
6165
+ return;
6166
+ }
6167
+ const file = `${conversationId}.jsonl`;
6168
+ for (const entry of entries) {
6169
+ try {
6170
+ if (fs17.existsSync(path16.join(projects, entry, file)))
6171
+ return true;
6172
+ } catch {}
6173
+ }
6174
+ return false;
6175
+ }
6176
+ function claudeConversationCwd(configDir, conversationId) {
6177
+ const projects = path16.join(configDir, "projects");
6178
+ let entries;
6179
+ try {
6180
+ entries = fs17.readdirSync(projects);
6181
+ } catch {
6182
+ return;
6183
+ }
6184
+ for (const entry of entries) {
6185
+ const file = path16.join(projects, entry, `${conversationId}.jsonl`);
6186
+ let head;
6187
+ try {
6188
+ const handle = fs17.openSync(file, "r");
6189
+ try {
6190
+ const buffer = Buffer.alloc(64 * 1024);
6191
+ const read = fs17.readSync(handle, buffer, 0, buffer.byteLength, 0);
6192
+ head = buffer.subarray(0, read).toString("utf8");
6193
+ } finally {
6194
+ fs17.closeSync(handle);
6195
+ }
6196
+ } catch {
6197
+ continue;
6198
+ }
6199
+ for (const line of head.split(/\r?\n/)) {
6200
+ if (line.length === 0)
6201
+ continue;
6202
+ try {
6203
+ const record = JSON.parse(line);
6204
+ const cwd = record.cwd;
6205
+ if (typeof cwd === "string" && cwd.length > 0)
6206
+ return cwd;
6207
+ } catch {}
6208
+ }
6209
+ }
6210
+ return;
6211
+ }
6212
+ var CONVERSATION_ID;
6213
+ var init_claude_store = __esm(() => {
6214
+ CONVERSATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6215
+ });
6216
+
6217
+ // packages/daemon/src/injector.ts
6218
+ import { setTimeout as delay2 } from "timers/promises";
6219
+
6220
+ class Injector {
6221
+ queues = new Map;
6222
+ enqueue(sessionId, task) {
6223
+ const previous = this.queues.get(sessionId) ?? Promise.resolve();
6224
+ const next2 = previous.then(task, task);
6225
+ this.queues.set(sessionId, next2);
6226
+ return next2;
6227
+ }
6228
+ prompt(session, text) {
6229
+ return this.enqueue(session.info.id, async () => {
6230
+ try {
6231
+ const deadline = Date.now() + 5000;
6232
+ while (!session.pasteReady && !session.isExited && Date.now() < deadline) {
6233
+ await delay2(100);
6234
+ }
6235
+ session.write(CTRL_U);
6236
+ await delay2(CLEAR_SETTLE_MS);
6237
+ for (const chunk of iteratePasteChunks(text)) {
6238
+ session.write(chunk);
6239
+ }
6240
+ await delay2(SUBMIT_DELAY_MS);
6241
+ session.write(SUBMIT);
6242
+ } catch (err) {
6243
+ session.write(BRACKETED_PASTE_END);
6244
+ log(`prompt injection failed for ${session.info.shortId}: ${String(err)}`);
6245
+ throw err;
6246
+ }
6247
+ });
6248
+ }
6249
+ interrupt(session) {
6250
+ return this.enqueue(session.info.id, async () => {
6251
+ session.write(ESC);
6252
+ await delay2(500);
6253
+ });
6254
+ }
6255
+ }
6256
+ var CLEAR_SETTLE_MS = 150;
6257
+ var init_injector = __esm(() => {
6258
+ init_src();
6259
+ init_paths();
6260
+ });
6261
+
6262
+ // packages/daemon/src/pty-host-client.ts
6263
+ import net3 from "net";
6264
+ import { StringDecoder } from "string_decoder";
6265
+
6266
+ class RemotePty {
6267
+ id;
6268
+ pid;
6269
+ send;
6270
+ dataListeners = [];
6271
+ exitListeners = [];
6272
+ decoder = new StringDecoder("utf8");
6273
+ buffered = "";
6274
+ exitCode;
6275
+ exited = false;
6276
+ constructor(id, pid, send) {
6277
+ this.id = id;
6278
+ this.pid = pid;
6279
+ this.send = send;
6280
+ }
6281
+ emit(text) {
6282
+ if (text.length === 0)
6283
+ return;
6284
+ if (this.dataListeners.length === 0) {
6285
+ this.buffered += text;
6286
+ return;
6287
+ }
6288
+ for (const listener of this.dataListeners)
6289
+ listener(text);
6290
+ }
6291
+ push(payload) {
6292
+ this.emit(this.decoder.write(Buffer.from(payload)));
6293
+ }
6294
+ finish(code) {
6295
+ this.emit(this.decoder.end());
6296
+ this.exited = true;
6297
+ this.exitCode = code;
6298
+ for (const listener of this.exitListeners)
6299
+ listener(code);
6300
+ }
6301
+ onData(listener) {
6302
+ this.dataListeners.push(listener);
6303
+ if (this.buffered.length > 0) {
6304
+ const held = this.buffered;
6305
+ this.buffered = "";
6306
+ listener(held);
6307
+ }
6308
+ }
6309
+ onExit(listener) {
6310
+ this.exitListeners.push(listener);
6311
+ if (this.exited)
6312
+ listener(this.exitCode ?? null);
6313
+ }
6314
+ write(data) {
6315
+ this.send(encodeFrame(FrameKind.Data, this.id, new TextEncoder().encode(data)));
6316
+ }
6317
+ resize(cols, rows) {
6318
+ this.send(controlFrame({ t: "resize", id: this.id, cols, rows }));
6319
+ }
6320
+ kill() {
6321
+ if (this.exited)
6322
+ return;
6323
+ this.send(controlFrame({ t: "kill", id: this.id }));
6324
+ }
6325
+ }
6326
+ function connectPtyHost(options) {
6327
+ return new Promise((resolve, reject) => {
6328
+ const socket = net3.createConnection(options.endpoint);
6329
+ const decoder = new FrameDecoder;
6330
+ const ptys = new Map;
6331
+ const pending = new Map;
6332
+ const subscriptions = new Map;
6333
+ const dataListeners = [];
6334
+ const rawDecoders = new Map;
6335
+ let welcomed = false;
6336
+ const send = (frame) => {
6337
+ if (!socket.destroyed)
6338
+ socket.write(frame);
6339
+ };
6340
+ const fail = (error) => {
6341
+ if (!welcomed)
6342
+ reject(error);
6343
+ for (const waiter of pending.values())
6344
+ waiter.reject(error);
6345
+ pending.clear();
6346
+ for (const waiter of subscriptions.values())
6347
+ waiter.reject(error);
6348
+ subscriptions.clear();
6349
+ socket.destroy();
6350
+ };
6351
+ const timer = setTimeout(() => fail(new Error(`pty host did not greet within ${options.timeoutMs ?? 5000}ms`)), options.timeoutMs ?? 5000);
6352
+ timer.unref?.();
6353
+ socket.on("error", (err) => fail(err instanceof Error ? err : new Error(String(err))));
6354
+ socket.on("close", () => {
6355
+ clearTimeout(timer);
6356
+ if (!welcomed)
6357
+ reject(new Error("pty host refused the connection"));
6358
+ });
6359
+ socket.on("connect", () => send(controlFrame({ t: "hello", v: PTY_PROTOCOL_VERSION, token: options.token })));
6360
+ socket.on("data", (chunk) => {
6361
+ let frames;
6362
+ try {
6363
+ frames = decoder.push(new Uint8Array(chunk));
6364
+ } catch (err) {
6365
+ fail(err instanceof Error ? err : new Error(String(err)));
6366
+ return;
6367
+ }
6368
+ for (const frame of frames) {
6369
+ if (frame.kind === FrameKind.Data) {
6370
+ ptys.get(frame.id)?.push(frame.payload);
6371
+ if (dataListeners.length > 0) {
6372
+ let raw = rawDecoders.get(frame.id);
6373
+ if (raw === undefined) {
6374
+ raw = new StringDecoder("utf8");
6375
+ rawDecoders.set(frame.id, raw);
6376
+ }
6377
+ const text = raw.write(Buffer.from(frame.payload));
6378
+ if (text.length > 0)
6379
+ for (const listener of dataListeners)
6380
+ listener(frame.id, text);
6381
+ }
6382
+ continue;
6383
+ }
6384
+ const message = parseControl(frame.payload);
6385
+ if (message === undefined)
6386
+ continue;
6387
+ if (message.t === "welcome") {
6388
+ clearTimeout(timer);
6389
+ welcomed = true;
6390
+ resolve({
6391
+ protocolVersion: message.v,
6392
+ sessions: message.sessions,
6393
+ spawn: (spawnOptions) => new Promise((done, failed) => {
6394
+ const id = spawnOptions.id ?? crypto.randomUUID().replace(/-/g, "");
6395
+ pending.set(id, {
6396
+ resolve: (pid) => {
6397
+ const pty = new RemotePty(id, pid, send);
6398
+ ptys.set(id, pty);
6399
+ done(pty);
6400
+ },
6401
+ reject: failed
6402
+ });
6403
+ send(controlFrame({
6404
+ t: "spawn",
6405
+ id,
6406
+ exe: spawnOptions.exe,
6407
+ argv: spawnOptions.argv,
6408
+ cwd: spawnOptions.cwd,
6409
+ env: Object.fromEntries(Object.entries(spawnOptions.env).filter((entry) => entry[1] !== undefined)),
6410
+ cols: spawnOptions.cols,
6411
+ rows: spawnOptions.rows,
6412
+ term: spawnOptions.term
6413
+ }));
6414
+ }),
6415
+ adopt: (session) => new Promise((done, failed) => {
6416
+ const pty = ptys.get(session.id) ?? new RemotePty(session.id, session.pid, send);
6417
+ ptys.set(session.id, pty);
6418
+ subscriptions.set(session.id, {
6419
+ resolve: () => done(pty),
6420
+ reject: failed
6421
+ });
6422
+ send(controlFrame({ t: "subscribe", id: session.id, replay: true }));
6423
+ }),
6424
+ onData: (listener) => dataListeners.push(listener),
6425
+ close: () => socket.destroy()
6426
+ });
6427
+ continue;
6428
+ }
6429
+ if (message.t === "spawned") {
6430
+ pending.get(message.id)?.resolve(message.pid);
6431
+ pending.delete(message.id);
6432
+ continue;
6433
+ }
6434
+ if (message.t === "error") {
6435
+ pending.get(message.id)?.reject(new Error(message.message));
6436
+ pending.delete(message.id);
6437
+ continue;
6438
+ }
6439
+ if (message.t === "subscribed") {
6440
+ subscriptions.get(message.id)?.resolve();
6441
+ subscriptions.delete(message.id);
6442
+ continue;
6443
+ }
6444
+ if (message.t === "exit") {
6445
+ const pty = ptys.get(message.id);
6446
+ ptys.delete(message.id);
6447
+ rawDecoders.delete(message.id);
6448
+ subscriptions.get(message.id)?.reject(new Error(`session ${message.id} exited`));
6449
+ subscriptions.delete(message.id);
6450
+ pty?.finish(message.code);
6451
+ }
6452
+ }
6453
+ });
6454
+ });
6455
+ }
6456
+ var init_pty_host_client = __esm(() => {
6457
+ init_src();
6458
+ });
6459
+
6460
+ // packages/daemon/src/pty-host-launch.ts
6461
+ import { spawn as spawn3 } from "child_process";
6462
+ import fs18 from "fs";
6463
+ import os5 from "os";
6464
+ import path17 from "path";
6465
+ function hostFile(runtimeDir) {
6466
+ return path17.join(runtimeDir, "pty-host.json");
6467
+ }
6468
+ function readPtyHostIn(runtimeDir) {
6469
+ return readPtyHostFile(hostFile(runtimeDir));
6470
+ }
6471
+ function readPtyHostFile(file) {
6472
+ try {
6473
+ const parsed = JSON.parse(fs18.readFileSync(file, "utf8"));
6474
+ if (typeof parsed !== "object" || parsed === null)
6475
+ return;
6476
+ const { pid, endpoint, token, version: version2 } = parsed;
6477
+ if (typeof pid !== "number" || typeof endpoint !== "string" || typeof token !== "string")
6478
+ return;
6479
+ return { pid, endpoint, token, version: typeof version2 === "number" ? version2 : PTY_PROTOCOL_VERSION };
6480
+ } catch {
6481
+ return;
6482
+ }
6483
+ }
6484
+ function isRunning(pid) {
6485
+ try {
6486
+ process.kill(pid, 0);
6487
+ return true;
6488
+ } catch (err) {
6489
+ return err.code === "EPERM";
6490
+ }
6491
+ }
6492
+ function hostEndpoint(runtimeDir) {
6493
+ const base = ipcEndpoint(process.platform, os5.userInfo().username, { runtimeDir });
6494
+ return process.platform === "win32" ? `${base}-pty` : path17.join(runtimeDir, "axterm-pty.sock");
6495
+ }
6496
+ async function ensurePtyHost(location) {
6497
+ const file = hostFile(location.runtimeDir);
6498
+ const existing = readPtyHostFile(file);
6499
+ if (existing !== undefined && isRunning(existing.pid))
6500
+ return existing;
6501
+ if (existing !== undefined)
6502
+ log(`pty host: handoff file names a dead pid ${existing.pid}, starting a new host`);
6503
+ const endpoint = hostEndpoint(location.runtimeDir);
6504
+ if (process.platform !== "win32")
6505
+ fs18.rmSync(endpoint, { force: true });
6506
+ const token = shortHash(`${location.runtimeDir}:${Date.now()}:${Math.random()}`) + shortHash(String(process.pid));
6507
+ const launch = ptyHostLaunch({ execPath: process.execPath, mainScript: location.mainScript ?? Bun.main });
6508
+ const child = spawn3(launch.command, launch.args, {
6509
+ detached: true,
6510
+ stdio: "ignore",
6511
+ windowsHide: true,
6512
+ env: {
6513
+ ...process.env,
6514
+ ...location.env,
6515
+ AXTERM_PTY_HOST_ENDPOINT: endpoint,
6516
+ AXTERM_PTY_HOST_TOKEN: token,
6517
+ AXTERM_PTY_HOST_FILE: file
6518
+ }
6519
+ });
6520
+ child.unref();
6521
+ if (child.pid === undefined)
6522
+ throw new Error("could not start the terminal host");
6523
+ const deadline = Date.now() + 15000;
6524
+ for (;; ) {
6525
+ const info = readPtyHostFile(file);
6526
+ if (info !== undefined && info.endpoint === endpoint && isRunning(info.pid))
6527
+ return info;
6528
+ if (Date.now() > deadline)
6529
+ throw new Error(`the terminal host did not become ready within 15s (${endpoint})`);
6530
+ await new Promise((resolve) => setTimeout(resolve, 100));
6531
+ }
6532
+ }
6533
+ function writePtyHostFile(file, info) {
6534
+ fs18.mkdirSync(path17.dirname(file), { recursive: true, mode: 448 });
6535
+ fs18.writeFileSync(file, `${JSON.stringify(info, null, 2)}
6536
+ `, { mode: 384 });
6537
+ restrictFile(file);
6538
+ }
6539
+ var init_pty_host_launch = __esm(() => {
6540
+ init_src();
6541
+ init_paths();
6542
+ init_shell();
6543
+ });
6544
+
6545
+ // packages/daemon/src/pty.ts
6546
+ import { spawnSync as spawnSync2 } from "child_process";
6547
+ import path18 from "path";
6548
+ import { StringDecoder as StringDecoder2 } from "string_decoder";
6549
+
6550
+ class Fanout {
6551
+ listeners = new Set;
6552
+ backlog = [];
6553
+ add(listener) {
6554
+ this.listeners.add(listener);
6555
+ for (const value of this.backlog)
6556
+ listener(value);
6557
+ }
6558
+ emit(value) {
6559
+ if (this.listeners.size === 0)
6560
+ this.backlog.push(value);
6561
+ else
6562
+ for (const listener of this.listeners)
6563
+ listener(value);
6564
+ }
6565
+ }
6566
+ function killTree(pid, exeName) {
6567
+ if (process.platform === "win32") {
6568
+ const check = spawnSync2("tasklist", ["/FI", `PID eq ${pid}`, "/NH", "/FO", "CSV"], {
6569
+ encoding: "utf8",
6570
+ windowsHide: true
6571
+ });
6572
+ const image = check.stdout?.split(",")[0]?.replace(/"/g, "").toLowerCase() ?? "";
6573
+ if (image === exeName) {
6574
+ spawnSync2("taskkill", ["/PID", String(pid), "/T", "/F"], { encoding: "utf8", windowsHide: true });
6575
+ }
6576
+ return;
6577
+ }
6578
+ try {
6579
+ process.kill(-pid, "SIGKILL");
6580
+ } catch {}
5831
6581
  }
5832
- var UNTRACKED_LINE_SCAN_LIMIT;
5833
- var init_git = __esm(() => {
5834
- init_paths();
5835
- UNTRACKED_LINE_SCAN_LIMIT = 1024 * 1024;
5836
- });
5837
6582
 
5838
- // packages/daemon/src/injector.ts
5839
- import { setTimeout as delay2 } from "timers/promises";
5840
-
5841
- class Injector {
5842
- queues = new Map;
5843
- enqueue(sessionId, task) {
5844
- const previous = this.queues.get(sessionId) ?? Promise.resolve();
5845
- const next2 = previous.then(task, task);
5846
- this.queues.set(sessionId, next2);
5847
- return next2;
6583
+ class BunPtyHandle {
6584
+ proc;
6585
+ exeName;
6586
+ data = new Fanout;
6587
+ exit = new Fanout;
6588
+ decoder = new StringDecoder2("utf8");
6589
+ exited = false;
6590
+ constructor(proc, exeName) {
6591
+ this.proc = proc;
6592
+ this.exeName = exeName;
5848
6593
  }
5849
- prompt(session, text) {
5850
- return this.enqueue(session.info.id, async () => {
5851
- try {
5852
- const deadline = Date.now() + 5000;
5853
- while (!session.pasteReady && !session.isExited && Date.now() < deadline) {
5854
- await delay2(100);
5855
- }
5856
- session.write(CTRL_U);
5857
- await delay2(CLEAR_SETTLE_MS);
5858
- for (const chunk of iteratePasteChunks(text)) {
5859
- session.write(chunk);
5860
- }
5861
- await delay2(SUBMIT_DELAY_MS);
5862
- session.write(SUBMIT);
5863
- } catch (err) {
5864
- session.write(BRACKETED_PASTE_END);
5865
- log(`prompt injection failed for ${session.info.shortId}: ${String(err)}`);
5866
- throw err;
5867
- }
5868
- });
6594
+ get pid() {
6595
+ return this.proc.pid;
5869
6596
  }
5870
- interrupt(session) {
5871
- return this.enqueue(session.info.id, async () => {
5872
- session.write(ESC);
5873
- await delay2(500);
5874
- });
6597
+ push(bytes) {
6598
+ const text = this.decoder.write(Buffer.from(bytes));
6599
+ if (text.length > 0)
6600
+ this.data.emit(text);
6601
+ }
6602
+ finish(code) {
6603
+ this.exited = true;
6604
+ const tail = this.decoder.end();
6605
+ if (tail.length > 0)
6606
+ this.data.emit(tail);
6607
+ try {
6608
+ this.proc.terminal.close();
6609
+ } catch {}
6610
+ this.exit.emit(code);
6611
+ }
6612
+ onData(listener) {
6613
+ this.data.add(listener);
6614
+ }
6615
+ onExit(listener) {
6616
+ this.exit.add(listener);
6617
+ }
6618
+ write(data) {
6619
+ this.proc.terminal.write(data);
6620
+ }
6621
+ resize(cols, rows) {
6622
+ this.proc.terminal.resize(cols, rows);
6623
+ }
6624
+ kill() {
6625
+ if (this.exited)
6626
+ return;
6627
+ killTree(this.proc.pid, this.exeName);
6628
+ try {
6629
+ this.proc.kill();
6630
+ } catch {}
5875
6631
  }
5876
6632
  }
5877
- var CLEAR_SETTLE_MS = 150;
5878
- var init_injector = __esm(() => {
5879
- init_src();
5880
- init_paths();
5881
- });
6633
+ function spawnPty(options) {
6634
+ let handle;
6635
+ const proc = Bun.spawn([options.exe, ...options.argv], {
6636
+ cwd: options.cwd,
6637
+ env: { ...options.env, TERM: options.term },
6638
+ terminal: {
6639
+ cols: options.cols,
6640
+ rows: options.rows,
6641
+ name: options.term,
6642
+ data(_terminal, bytes) {
6643
+ handle?.push(bytes);
6644
+ }
6645
+ }
6646
+ });
6647
+ handle = new BunPtyHandle(proc, path18.basename(options.exe).toLowerCase());
6648
+ proc.exited.then((code) => handle?.finish(code ?? null));
6649
+ return handle;
6650
+ }
6651
+ var init_pty = () => {};
5882
6652
 
5883
6653
  // packages/daemon/src/occupancy.ts
5884
- import { spawn as spawn3 } from "child_process";
5885
- import fs16 from "fs/promises";
6654
+ import { spawn as spawn4 } from "child_process";
6655
+ import fs19 from "fs/promises";
5886
6656
  function shellOccupied(shellPid) {
5887
6657
  return process.platform === "win32" ? occupiedFromSnapshot(shellPid) : occupiedFromPidProbe(shellPid);
5888
6658
  }
@@ -5927,11 +6697,11 @@ async function occupiedFromSnapshot(shellPid) {
5927
6697
  }
5928
6698
  async function childrenViaProc(pid) {
5929
6699
  try {
5930
- const children = await fs16.readFile(`/proc/${pid}/task/${pid}/children`, "utf8");
6700
+ const children = await fs19.readFile(`/proc/${pid}/task/${pid}/children`, "utf8");
5931
6701
  return children.trim().length > 0;
5932
6702
  } catch {
5933
6703
  try {
5934
- await fs16.access(`/proc/${pid}`);
6704
+ await fs19.access(`/proc/${pid}`);
5935
6705
  return;
5936
6706
  } catch {
5937
6707
  return false;
@@ -5978,7 +6748,7 @@ async function processTable() {
5978
6748
  }
5979
6749
  function capture(exe, argv) {
5980
6750
  const { promise, resolve } = Promise.withResolvers();
5981
- const child = spawn3(exe, argv, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
6751
+ const child = spawn4(exe, argv, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
5982
6752
  let stdout3 = "";
5983
6753
  child.stdout.setEncoding("utf8");
5984
6754
  child.stdout.on("data", (chunk) => {
@@ -5995,20 +6765,20 @@ var init_occupancy = __esm(() => {
5995
6765
  });
5996
6766
 
5997
6767
  // packages/daemon/src/agent-store.ts
5998
- import fs17 from "fs";
5999
- import os4 from "os";
6000
- import path16 from "path";
6768
+ import fs20 from "fs";
6769
+ import os6 from "os";
6770
+ import path19 from "path";
6001
6771
  import { Database as Database3 } from "bun:sqlite";
6002
6772
  function readTailLines(file, maxBytes) {
6003
6773
  let fd;
6004
6774
  try {
6005
- fd = fs17.openSync(file, "r");
6006
- const size = fs17.fstatSync(fd).size;
6775
+ fd = fs20.openSync(file, "r");
6776
+ const size = fs20.fstatSync(fd).size;
6007
6777
  if (size === 0)
6008
6778
  return [];
6009
6779
  const length = Math.min(size, maxBytes);
6010
6780
  const buffer = Buffer.allocUnsafe(length);
6011
- fs17.readSync(fd, buffer, 0, length, size - length);
6781
+ fs20.readSync(fd, buffer, 0, length, size - length);
6012
6782
  const text = buffer.toString("utf8");
6013
6783
  const lines = text.split(`
6014
6784
  `);
@@ -6021,46 +6791,46 @@ function readTailLines(file, maxBytes) {
6021
6791
  return [];
6022
6792
  } finally {
6023
6793
  if (fd !== undefined)
6024
- fs17.closeSync(fd);
6794
+ fs20.closeSync(fd);
6025
6795
  }
6026
6796
  }
6027
- function claudeConfigDir() {
6028
- return process.env["CLAUDE_CONFIG_DIR"] ?? path16.join(os4.homedir(), ".claude");
6797
+ function claudeConfigDir2() {
6798
+ return process.env["CLAUDE_CONFIG_DIR"] ?? path19.join(os6.homedir(), ".claude");
6029
6799
  }
6030
6800
  function codexHome() {
6031
- return path16.join(STATE_DIR, "codex-home");
6801
+ return path19.join(STATE_DIR, "codex-home");
6032
6802
  }
6033
6803
  function ompAgentDb() {
6034
- return path16.join(os4.homedir(), ".omp", "agent", "agent.db");
6804
+ return path19.join(os6.homedir(), ".omp", "agent", "agent.db");
6035
6805
  }
6036
6806
  function findCodexRollout(sessionId) {
6037
- const root = path16.join(codexHome(), "sessions");
6807
+ const root = path19.join(codexHome(), "sessions");
6038
6808
  for (const shard2 of codexRolloutShards(sessionId)) {
6039
- const dir = path16.join(root, ...shard2.split("/"));
6809
+ const dir = path19.join(root, ...shard2.split("/"));
6040
6810
  let entries;
6041
6811
  try {
6042
- entries = fs17.readdirSync(dir);
6812
+ entries = fs20.readdirSync(dir);
6043
6813
  } catch {
6044
6814
  continue;
6045
6815
  }
6046
6816
  const match2 = entries.find((name) => name.endsWith(`-${sessionId}.jsonl`));
6047
6817
  if (match2 !== undefined)
6048
- return path16.join(dir, match2);
6818
+ return path19.join(dir, match2);
6049
6819
  }
6050
6820
  return;
6051
6821
  }
6052
6822
  function newestCodexRollout() {
6053
- const root = path16.join(codexHome(), "sessions");
6823
+ const root = path19.join(codexHome(), "sessions");
6054
6824
  let best;
6055
6825
  const walk = (dir, depth) => {
6056
6826
  let entries;
6057
6827
  try {
6058
- entries = fs17.readdirSync(dir, { withFileTypes: true });
6828
+ entries = fs20.readdirSync(dir, { withFileTypes: true });
6059
6829
  } catch {
6060
6830
  return;
6061
6831
  }
6062
6832
  for (const entry of entries) {
6063
- const full = path16.join(dir, entry.name);
6833
+ const full = path19.join(dir, entry.name);
6064
6834
  if (entry.isDirectory()) {
6065
6835
  if (depth < 3)
6066
6836
  walk(full, depth + 1);
@@ -6069,7 +6839,7 @@ function newestCodexRollout() {
6069
6839
  if (!entry.name.endsWith(".jsonl"))
6070
6840
  continue;
6071
6841
  try {
6072
- const { mtimeMs } = fs17.statSync(full);
6842
+ const { mtimeMs } = fs20.statSync(full);
6073
6843
  if (!best || mtimeMs > best.mtimeMs)
6074
6844
  best = { file: full, mtimeMs };
6075
6845
  } catch {}
@@ -6080,7 +6850,7 @@ function newestCodexRollout() {
6080
6850
  }
6081
6851
  function ompQuotaFacts() {
6082
6852
  const file = ompAgentDb();
6083
- if (!fs17.existsSync(file))
6853
+ if (!fs20.existsSync(file))
6084
6854
  return { agent: "omp", kind: "gauge" };
6085
6855
  let db;
6086
6856
  try {
@@ -6107,12 +6877,12 @@ function codexQuotaFacts() {
6107
6877
  return reading ? { agent: "codex", kind: "gauge", reading } : { agent: "codex", kind: "gauge" };
6108
6878
  }
6109
6879
  function claudeQuotaFacts(sessions) {
6110
- const dir = claudeConfigDir();
6880
+ const dir = claudeConfigDir2();
6111
6881
  let newest;
6112
6882
  for (const session of sessions) {
6113
6883
  if (session.agentSessionId === undefined)
6114
6884
  continue;
6115
- const file = path16.join(dir, claudeTranscriptRelPath(session.cwd, session.agentSessionId));
6885
+ const file = path19.join(dir, claudeTranscriptRelPath(session.cwd, session.agentSessionId));
6116
6886
  const event = parseClaudeLimitEvent(readTailLines(file, QUOTA_TAIL_BYTES));
6117
6887
  if (event && (!newest || event.at > newest.at))
6118
6888
  newest = event;
@@ -6144,11 +6914,11 @@ function collectQuotaFacts(agents, sessions, onPath) {
6144
6914
  function storeQuietForMs(agent, cwd, agentSessionId, nowMs) {
6145
6915
  if (agentSessionId === undefined)
6146
6916
  return;
6147
- const file = agent === "claude" ? path16.join(claudeConfigDir(), claudeTranscriptRelPath(cwd, agentSessionId)) : agent === "codex" ? findCodexRollout(agentSessionId) : undefined;
6917
+ const file = agent === "claude" ? path19.join(claudeConfigDir2(), claudeTranscriptRelPath(cwd, agentSessionId)) : agent === "codex" ? findCodexRollout(agentSessionId) : undefined;
6148
6918
  if (file === undefined)
6149
6919
  return;
6150
6920
  try {
6151
- return Math.max(0, nowMs - fs17.statSync(file).mtimeMs);
6921
+ return Math.max(0, nowMs - fs20.statSync(file).mtimeMs);
6152
6922
  } catch {
6153
6923
  return;
6154
6924
  }
@@ -6158,15 +6928,15 @@ function readFileRange(file, from, to) {
6158
6928
  return "";
6159
6929
  let fd;
6160
6930
  try {
6161
- fd = fs17.openSync(file, "r");
6931
+ fd = fs20.openSync(file, "r");
6162
6932
  const buffer = Buffer.allocUnsafe(to - from);
6163
- const read = fs17.readSync(fd, buffer, 0, to - from, from);
6933
+ const read = fs20.readSync(fd, buffer, 0, to - from, from);
6164
6934
  return buffer.subarray(0, read).toString("utf8");
6165
6935
  } catch {
6166
6936
  return "";
6167
6937
  } finally {
6168
6938
  if (fd !== undefined)
6169
- fs17.closeSync(fd);
6939
+ fs20.closeSync(fd);
6170
6940
  }
6171
6941
  }
6172
6942
  var QUOTA_TAIL_BYTES;
@@ -6177,10 +6947,10 @@ var init_agent_store = __esm(() => {
6177
6947
  });
6178
6948
 
6179
6949
  // packages/daemon/src/codex-app-server.ts
6180
- import net3 from "net";
6950
+ import net4 from "net";
6181
6951
  async function freeLoopbackPort() {
6182
6952
  const { promise, resolve, reject } = Promise.withResolvers();
6183
- const probe = net3.createServer();
6953
+ const probe = net4.createServer();
6184
6954
  probe.once("error", reject);
6185
6955
  probe.listen({ host: "127.0.0.1", port: 0 }, () => {
6186
6956
  const address = probe.address();
@@ -6193,7 +6963,7 @@ async function waitForPort(port, timeoutMs = READY_TIMEOUT_MS) {
6193
6963
  const deadline = Date.now() + timeoutMs;
6194
6964
  for (;; ) {
6195
6965
  const open = await new Promise((resolve) => {
6196
- const socket = net3.connect({ host: "127.0.0.1", port });
6966
+ const socket = net4.connect({ host: "127.0.0.1", port });
6197
6967
  const done = (value) => {
6198
6968
  socket.destroy();
6199
6969
  resolve(value);
@@ -6288,114 +7058,6 @@ var init_codex_app_server = __esm(() => {
6288
7058
  init_paths();
6289
7059
  });
6290
7060
 
6291
- // packages/daemon/src/pty.ts
6292
- import { spawnSync as spawnSync2 } from "child_process";
6293
- import path17 from "path";
6294
- import { StringDecoder } from "string_decoder";
6295
-
6296
- class Fanout {
6297
- listeners = new Set;
6298
- backlog = [];
6299
- add(listener) {
6300
- this.listeners.add(listener);
6301
- for (const value of this.backlog)
6302
- listener(value);
6303
- }
6304
- emit(value) {
6305
- if (this.listeners.size === 0)
6306
- this.backlog.push(value);
6307
- else
6308
- for (const listener of this.listeners)
6309
- listener(value);
6310
- }
6311
- }
6312
- function killTree(pid, exeName) {
6313
- if (process.platform === "win32") {
6314
- const check = spawnSync2("tasklist", ["/FI", `PID eq ${pid}`, "/NH", "/FO", "CSV"], {
6315
- encoding: "utf8",
6316
- windowsHide: true
6317
- });
6318
- const image = check.stdout?.split(",")[0]?.replace(/"/g, "").toLowerCase() ?? "";
6319
- if (image === exeName) {
6320
- spawnSync2("taskkill", ["/PID", String(pid), "/T", "/F"], { encoding: "utf8", windowsHide: true });
6321
- }
6322
- return;
6323
- }
6324
- try {
6325
- process.kill(-pid, "SIGKILL");
6326
- } catch {}
6327
- }
6328
-
6329
- class BunPtyHandle {
6330
- proc;
6331
- exeName;
6332
- data = new Fanout;
6333
- exit = new Fanout;
6334
- decoder = new StringDecoder("utf8");
6335
- exited = false;
6336
- constructor(proc, exeName) {
6337
- this.proc = proc;
6338
- this.exeName = exeName;
6339
- }
6340
- get pid() {
6341
- return this.proc.pid;
6342
- }
6343
- push(bytes) {
6344
- const text = this.decoder.write(Buffer.from(bytes));
6345
- if (text.length > 0)
6346
- this.data.emit(text);
6347
- }
6348
- finish(code) {
6349
- this.exited = true;
6350
- const tail = this.decoder.end();
6351
- if (tail.length > 0)
6352
- this.data.emit(tail);
6353
- try {
6354
- this.proc.terminal.close();
6355
- } catch {}
6356
- this.exit.emit(code);
6357
- }
6358
- onData(listener) {
6359
- this.data.add(listener);
6360
- }
6361
- onExit(listener) {
6362
- this.exit.add(listener);
6363
- }
6364
- write(data) {
6365
- this.proc.terminal.write(data);
6366
- }
6367
- resize(cols, rows) {
6368
- this.proc.terminal.resize(cols, rows);
6369
- }
6370
- kill() {
6371
- if (this.exited)
6372
- return;
6373
- killTree(this.proc.pid, this.exeName);
6374
- try {
6375
- this.proc.kill();
6376
- } catch {}
6377
- }
6378
- }
6379
- function spawnPty(options) {
6380
- let handle;
6381
- const proc = Bun.spawn([options.exe, ...options.argv], {
6382
- cwd: options.cwd,
6383
- env: { ...options.env, TERM: options.term },
6384
- terminal: {
6385
- cols: options.cols,
6386
- rows: options.rows,
6387
- name: options.term,
6388
- data(_terminal, bytes) {
6389
- handle?.push(bytes);
6390
- }
6391
- }
6392
- });
6393
- handle = new BunPtyHandle(proc, path17.basename(options.exe).toLowerCase());
6394
- proc.exited.then((code) => handle?.finish(code ?? null));
6395
- return handle;
6396
- }
6397
- var init_pty = () => {};
6398
-
6399
7061
  // node_modules/.bun/@xterm+headless@5.5.0/node_modules/@xterm/headless/lib-headless/xterm-headless.js
6400
7062
  var require_xterm_headless = __commonJS((exports) => {
6401
7063
  (() => {
@@ -10725,15 +11387,55 @@ var require_addon_serialize = __commonJS((exports, module) => {
10725
11387
  })());
10726
11388
  });
10727
11389
 
11390
+ // packages/daemon/src/terminal-mirror.ts
11391
+ class TerminalMirror {
11392
+ scrollbackLines;
11393
+ term;
11394
+ serializeAddon;
11395
+ constructor(cols, rows, scrollbackLines) {
11396
+ this.scrollbackLines = scrollbackLines;
11397
+ this.term = new import_headless.Terminal({ cols, rows, scrollback: scrollbackLines, allowProposedApi: true });
11398
+ this.serializeAddon = new import_addon_serialize.SerializeAddon;
11399
+ this.term.loadAddon(this.serializeAddon);
11400
+ }
11401
+ write(data) {
11402
+ this.term.write(data);
11403
+ }
11404
+ resize(cols, rows) {
11405
+ this.term.resize(cols, rows);
11406
+ }
11407
+ serialize(maxChars = Number.POSITIVE_INFINITY) {
11408
+ const ansi = this.serializeAddon.serialize({ scrollback: this.scrollbackLines });
11409
+ return ansi.length > maxChars ? ansi.slice(-maxChars) : ansi;
11410
+ }
11411
+ text(maxLines) {
11412
+ const buffer = this.term.buffer.active;
11413
+ let last = buffer.length - 1;
11414
+ while (last >= 0 && (buffer.getLine(last)?.translateToString(true).trim() ?? "") === "")
11415
+ last--;
11416
+ const start = Math.max(0, last - maxLines + 1);
11417
+ const lines = [];
11418
+ for (let y = start;y <= last; y++) {
11419
+ lines.push(buffer.getLine(y)?.translateToString(true).trimEnd() ?? "");
11420
+ }
11421
+ return lines.join(`
11422
+ `);
11423
+ }
11424
+ }
11425
+ var import_headless, import_addon_serialize;
11426
+ var init_terminal_mirror = __esm(() => {
11427
+ import_headless = __toESM(require_xterm_headless(), 1);
11428
+ import_addon_serialize = __toESM(require_addon_serialize(), 1);
11429
+ });
11430
+
10728
11431
  // packages/daemon/src/pty-session.ts
10729
- import fs18 from "fs";
10730
- import path18 from "path";
11432
+ import fs21 from "fs";
11433
+ import path20 from "path";
10731
11434
 
10732
11435
  class PtySession {
10733
11436
  info;
10734
11437
  pty;
10735
- term;
10736
- serializeAddon;
11438
+ mirror;
10737
11439
  dataListeners = new Set;
10738
11440
  exitListeners = new Set;
10739
11441
  saveTimer;
@@ -10742,30 +11444,31 @@ class PtySession {
10742
11444
  pasteReady = false;
10743
11445
  readyScanTail = "";
10744
11446
  lastOutputAt = Date.now();
10745
- constructor(info, spawn4) {
10746
- this.info = info;
10747
- this.term = new import_headless.Terminal({
10748
- cols: spawn4.cols,
10749
- rows: spawn4.rows,
10750
- scrollback: SCROLLBACK_LINES,
10751
- allowProposedApi: true
10752
- });
10753
- this.serializeAddon = new import_addon_serialize.SerializeAddon;
10754
- this.term.loadAddon(this.serializeAddon);
10755
- this.pty = spawnPty({
10756
- exe: spawn4.exe,
10757
- argv: spawn4.argv,
10758
- cwd: spawn4.cwd,
10759
- cols: spawn4.cols,
10760
- rows: spawn4.rows,
10761
- env: spawn4.env,
10762
- term: "xterm-256color"
11447
+ static async open(info, spawn5, spawner = spawnPty) {
11448
+ const pty = await spawner({
11449
+ exe: spawn5.exe,
11450
+ argv: spawn5.argv,
11451
+ cwd: spawn5.cwd,
11452
+ cols: spawn5.cols,
11453
+ rows: spawn5.rows,
11454
+ env: spawn5.env,
11455
+ term: "xterm-256color",
11456
+ id: info.id
10763
11457
  });
11458
+ return new PtySession(info, spawn5, pty);
11459
+ }
11460
+ static adopt(info, pty, cols, rows) {
11461
+ return new PtySession(info, { exe: "", argv: [], cwd: info.cwd, cols, rows, env: {} }, pty);
11462
+ }
11463
+ constructor(info, spawn5, pty) {
11464
+ this.info = info;
11465
+ this.mirror = new TerminalMirror(spawn5.cols, spawn5.rows, SCROLLBACK_LINES);
11466
+ this.pty = pty;
10764
11467
  info.ptyPid = this.pty.pid;
10765
11468
  this.pty.onData((data) => {
10766
11469
  this.seq += 1;
10767
11470
  this.lastOutputAt = Date.now();
10768
- this.term.write(data);
11471
+ this.mirror.write(data);
10769
11472
  if (!this.pasteReady) {
10770
11473
  const window2 = this.readyScanTail + data;
10771
11474
  if (window2.includes("\x1B[?2004h"))
@@ -10810,27 +11513,16 @@ class PtySession {
10810
11513
  this.info.rows = rows;
10811
11514
  try {
10812
11515
  this.pty.resize(cols, rows);
10813
- this.term.resize(cols, rows);
11516
+ this.mirror.resize(cols, rows);
10814
11517
  } catch (err) {
10815
11518
  log(`resize failed for ${this.info.shortId}: ${String(err)}`);
10816
11519
  }
10817
11520
  }
10818
11521
  screenText(maxLines = 60) {
10819
- const buffer = this.term.buffer.active;
10820
- let last = buffer.length - 1;
10821
- while (last >= 0 && (buffer.getLine(last)?.translateToString(true).trim() ?? "") === "")
10822
- last--;
10823
- const start = Math.max(0, last - maxLines + 1);
10824
- const lines = [];
10825
- for (let y = start;y <= last; y++) {
10826
- lines.push(buffer.getLine(y)?.translateToString(true).trimEnd() ?? "");
10827
- }
10828
- return lines.join(`
10829
- `);
11522
+ return this.mirror.text(maxLines);
10830
11523
  }
10831
11524
  serializeBuffer() {
10832
- const ansi = this.serializeAddon.serialize({ scrollback: SCROLLBACK_LINES });
10833
- return ansi.length > SCROLLBACK_MAX_CHARS ? ansi.slice(-SCROLLBACK_MAX_CHARS) : ansi;
11525
+ return this.mirror.serialize(SCROLLBACK_MAX_CHARS);
10834
11526
  }
10835
11527
  scheduleSave() {
10836
11528
  if (this.exited || this.saveTimer)
@@ -10843,10 +11535,10 @@ class PtySession {
10843
11535
  }
10844
11536
  saveScrollback() {
10845
11537
  try {
10846
- const file = path18.join(SCROLLBACK_DIR, `${this.info.id}.bin`);
11538
+ const file = path20.join(SCROLLBACK_DIR, `${this.info.id}.bin`);
10847
11539
  const tmp = `${file}.tmp`;
10848
- fs18.writeFileSync(tmp, this.serializeBuffer(), "utf8");
10849
- fs18.renameSync(tmp, file);
11540
+ fs21.writeFileSync(tmp, this.serializeBuffer(), "utf8");
11541
+ fs21.renameSync(tmp, file);
10850
11542
  } catch (err) {
10851
11543
  log(`scrollback save failed for ${this.info.shortId}: ${String(err)}`);
10852
11544
  }
@@ -10857,20 +11549,23 @@ class PtySession {
10857
11549
  this.pty.kill();
10858
11550
  }
10859
11551
  }
10860
- var import_headless, import_addon_serialize, SCROLLBACK_LINES = 5000, SAVE_DEBOUNCE_MS = 5000, SCROLLBACK_MAX_CHARS;
11552
+ var SCROLLBACK_LINES = 5000, SAVE_DEBOUNCE_MS = 5000, SCROLLBACK_MAX_CHARS;
10861
11553
  var init_pty_session = __esm(() => {
10862
11554
  init_paths();
10863
11555
  init_pty();
10864
- import_headless = __toESM(require_xterm_headless(), 1);
10865
- import_addon_serialize = __toESM(require_addon_serialize(), 1);
11556
+ init_terminal_mirror();
10866
11557
  SCROLLBACK_MAX_CHARS = 2 * 1024 * 1024;
10867
11558
  });
10868
11559
 
10869
11560
  // packages/daemon/src/session-manager.ts
10870
11561
  import { randomBytes as randomBytes2, randomUUID } from "crypto";
10871
- import fs19 from "fs";
10872
- import os5 from "os";
10873
- import path19 from "path";
11562
+ import { spawn as spawn5 } from "child_process";
11563
+ import fs22 from "fs";
11564
+ import os7 from "os";
11565
+ import path21 from "path";
11566
+ function sessionScratchRoot(info) {
11567
+ return info.worktreePath ?? info.cwd;
11568
+ }
10874
11569
  function unresolvableAgent(commandOverride, agentCommand, lookup) {
10875
11570
  if (commandOverride !== undefined)
10876
11571
  return;
@@ -10909,6 +11604,14 @@ async function sendableNow(runtime, info, probe, now) {
10909
11604
  occupied
10910
11605
  }, now);
10911
11606
  }
11607
+ function pidIsRunning(pid) {
11608
+ try {
11609
+ process.kill(pid, 0);
11610
+ return true;
11611
+ } catch (err) {
11612
+ return err.code === "EPERM";
11613
+ }
11614
+ }
10912
11615
 
10913
11616
  class SessionManager {
10914
11617
  store;
@@ -10925,13 +11628,59 @@ class SessionManager {
10925
11628
  this.store = store;
10926
11629
  this.hooks = hooks;
10927
11630
  this.configuredDefaults = configuredDefaults;
10928
- const stale = store.markStaleRunningAsExited();
10929
- if (stale > 0)
10930
- log(`boot: marked ${stale} stale session(s) as exited`);
10931
11631
  this.sessionWatch = setInterval(() => this.tickSessions(), SESSION_TICK_MS);
10932
11632
  this.sessionWatch.unref();
10933
11633
  }
11634
+ async recover() {
11635
+ const running = this.store.listSessions().filter((info) => info.state !== "exited");
11636
+ const held = await this.hostSessions();
11637
+ const adoptedIds = [];
11638
+ for (const info of running) {
11639
+ const description = held.get(info.id);
11640
+ if (description === undefined)
11641
+ continue;
11642
+ try {
11643
+ const handle = await this.ptyHost.adopt(description);
11644
+ info.state = "running";
11645
+ delete info.exitCode;
11646
+ delete info.exitedAt;
11647
+ const session = PtySession.adopt(info, handle, description.cols, description.rows);
11648
+ this.wire(session, undefined);
11649
+ this.hostOwned.add(info.id);
11650
+ this.store.markResumed(info.id, info.agentCommand, info.ptyPid);
11651
+ if (agentIdFromCommand(info.agentCommand) === "codex")
11652
+ this.adoptCodexAppServer(info.id);
11653
+ if (agentIdFromCommand(info.agentCommand) === "gemini")
11654
+ this.skipGeminiTelemetryHistory(info.id);
11655
+ adoptedIds.push(info.id);
11656
+ log(`boot: adopted session ${info.shortId} (pty ${description.pid}) from the terminal host`);
11657
+ } catch (err) {
11658
+ log(`boot: could not adopt ${info.shortId}: ${err instanceof Error ? err.message : String(err)}`);
11659
+ }
11660
+ }
11661
+ const sweptExited = this.store.markStaleRunningAsExited(adoptedIds);
11662
+ if (sweptExited > 0)
11663
+ log(`boot: marked ${sweptExited} stale session(s) as exited`);
11664
+ return { adopted: adoptedIds.length, sweptExited };
11665
+ }
11666
+ async hostSessions() {
11667
+ if (process.env["AXTERM_PTY_HOST"] !== "1")
11668
+ return new Map;
11669
+ const existing = readPtyHostIn(RUNTIME_DIR);
11670
+ if (existing === undefined)
11671
+ return new Map;
11672
+ try {
11673
+ this.ptyHost ??= await connectPtyHost({ endpoint: existing.endpoint, token: existing.token });
11674
+ return new Map(this.ptyHost.sessions.map((s) => [s.id, s]));
11675
+ } catch (err) {
11676
+ log(`boot: the terminal host named in pty-host.json did not answer: ${err instanceof Error ? err.message : String(err)}`);
11677
+ return new Map;
11678
+ }
11679
+ }
10934
11680
  sessionWatch;
11681
+ ptyHost;
11682
+ hostOwned = new Set;
11683
+ ptyHostUnavailable = false;
10935
11684
  codexServers = new Map;
10936
11685
  workspaceFor(_sessionId) {
10937
11686
  return this.workspace;
@@ -11009,8 +11758,8 @@ class SessionManager {
11009
11758
  };
11010
11759
  }
11011
11760
  async create(params) {
11012
- const requestedCwd = params.cwd ? path19.resolve(params.cwd) : os5.homedir();
11013
- if (!fs19.existsSync(requestedCwd)) {
11761
+ const requestedCwd = params.cwd ? path21.resolve(params.cwd) : os7.homedir();
11762
+ if (!fs22.existsSync(requestedCwd)) {
11014
11763
  throw new AxtermError("err.cwd.missing", { cwd: requestedCwd });
11015
11764
  }
11016
11765
  let worktree;
@@ -11054,11 +11803,27 @@ class SessionManager {
11054
11803
  const missing = unresolvableAgent(params.commandOverride, agentCommand, lookupOnPath);
11055
11804
  if (missing !== undefined)
11056
11805
  throw new AxtermError("err.agent.notInstalled", { command: missing });
11806
+ const adopted = agent === "claude" ? resumedConversationId(agentCommand) : undefined;
11807
+ let sessionCwd = cwd;
11808
+ if (adopted !== undefined) {
11809
+ const holder = conversationHeldBy(this.allSessions(), adopted);
11810
+ if (holder !== undefined) {
11811
+ throw new AxtermError("err.claude.conversationBusy", { id: adopted, session: holder.shortId });
11812
+ }
11813
+ if (worktree === undefined) {
11814
+ const recorded = claudeConversationCwd(claudeConfigDir(), adopted);
11815
+ sessionCwd = resolveSessionCwd({
11816
+ requested: cwd,
11817
+ preferConversation: params.preferConversationCwd === true,
11818
+ ...recorded !== undefined && fs22.existsSync(recorded) ? { conversationCwd: recorded } : {}
11819
+ });
11820
+ }
11821
+ }
11057
11822
  const info = {
11058
11823
  id,
11059
11824
  shortId: id.slice(0, 6),
11060
- name: params.name ?? worktree?.branch ?? path19.basename(cwd),
11061
- cwd,
11825
+ name: params.name ?? worktree?.branch ?? path21.basename(sessionCwd),
11826
+ cwd: sessionCwd,
11062
11827
  shell: shell.kind,
11063
11828
  agentCommand,
11064
11829
  state: "running",
@@ -11067,8 +11832,7 @@ class SessionManager {
11067
11832
  createdAt: Date.now(),
11068
11833
  ...worktree !== undefined ? { worktreePath: worktree.path, branch: worktree.branch, repoRoot: worktree.repoRoot } : {}
11069
11834
  };
11070
- const appServerUrl = agent === "codex" ? await this.startCodexAppServer(id, this.ensureCodexHome(), cwd) : undefined;
11071
- this.launch(info, shell, agentCommand, appServerUrl);
11835
+ await this.launch(info, shell, agentCommand);
11072
11836
  this.store.insertSession(info);
11073
11837
  log(`session ${info.shortId} created shell=${shell.kind} cmd=${agentCommand.join(" ") || "(shell only)"}`);
11074
11838
  this.emit({ t: "session.created", info });
@@ -11082,7 +11846,7 @@ class SessionManager {
11082
11846
  launchToken
11083
11847
  });
11084
11848
  const file = this.claudeSettingsPath(id);
11085
- fs19.writeFileSync(file, JSON.stringify(settings, null, 2), { mode: 384 });
11849
+ fs22.writeFileSync(file, JSON.stringify(settings, null, 2), { mode: 384 });
11086
11850
  }
11087
11851
  writeGeminiSettings(sessionId) {
11088
11852
  try {
@@ -11095,69 +11859,104 @@ class SessionManager {
11095
11859
  });
11096
11860
  const file = this.geminiSettingsPath();
11097
11861
  const tmp = `${file}.tmp`;
11098
- fs19.writeFileSync(tmp, JSON.stringify(settings, null, 2));
11099
- fs19.renameSync(tmp, file);
11862
+ fs22.writeFileSync(tmp, JSON.stringify(settings, null, 2));
11863
+ fs22.renameSync(tmp, file);
11100
11864
  } catch (err) {
11101
11865
  log(`gemini settings write failed; session starts without hooks: ${String(err)}`);
11102
11866
  }
11103
11867
  }
11104
- async startCodexAppServer(id, codexHome2, cwd) {
11868
+ async startCodexAppServer(id, codexHome2, cwd, identity) {
11105
11869
  try {
11870
+ const inherited = this.adoptCodexAppServer(id);
11871
+ if (inherited !== undefined)
11872
+ return inherited;
11106
11873
  const port = await freeLoopbackPort();
11107
11874
  const url = `ws://127.0.0.1:${port}`;
11108
- const proc = Bun.spawn(["codex", "app-server", "--listen", url], {
11875
+ const child = spawn5("codex", ["app-server", "--listen", url], {
11109
11876
  cwd,
11110
- env: { ...stripHostAttribution(process.env), CODEX_HOME: codexHome2 },
11111
- stdout: "ignore",
11112
- stderr: "ignore",
11113
- stdin: "ignore"
11877
+ detached: true,
11878
+ windowsHide: true,
11879
+ stdio: "ignore",
11880
+ env: { ...sessionEnv(process.env, identity), CODEX_HOME: codexHome2 }
11114
11881
  });
11882
+ child.unref();
11883
+ const pid = child.pid;
11884
+ if (pid === undefined)
11885
+ throw new Error("the app-server was not given a pid");
11115
11886
  if (!await waitForPort(port)) {
11116
- proc.kill();
11887
+ this.killCodexAppServer(pid);
11117
11888
  log(`codex app-server did not open ${port}; session ${id.slice(0, 6)} keeps its hooks`);
11118
11889
  return;
11119
11890
  }
11120
- const client = new CodexAppServerClient(url, (payload) => this.ingestHookEvent(id, payload));
11121
- client.connect();
11122
- this.codexServers.set(id, {
11123
- stop: () => {
11124
- client.close();
11125
- try {
11126
- killTree(proc.pid, "codex.exe");
11127
- } catch {}
11128
- try {
11129
- proc.kill();
11130
- } catch {}
11131
- }
11132
- });
11133
- log(`codex app-server on ${url} for session ${id.slice(0, 6)} in ${cwd}`);
11891
+ this.watchCodexAppServer(id, url, pid);
11892
+ fs22.writeFileSync(this.codexAppServerFile(id), JSON.stringify({ pid, url }), { mode: 384 });
11893
+ log(`codex app-server on ${url} (pid ${pid}) for session ${id.slice(0, 6)} in ${cwd}`);
11134
11894
  return url;
11135
11895
  } catch (err) {
11136
11896
  log(`codex app-server unavailable (${String(err)}); session ${id.slice(0, 6)} keeps its hooks`);
11137
11897
  return;
11138
11898
  }
11139
11899
  }
11900
+ codexAppServerFile(id) {
11901
+ return path21.join(RUNTIME_DIR, `codex-app-${id}.json`);
11902
+ }
11903
+ adoptCodexAppServer(id) {
11904
+ const file = this.codexAppServerFile(id);
11905
+ let record;
11906
+ try {
11907
+ record = JSON.parse(fs22.readFileSync(file, "utf8"));
11908
+ } catch {
11909
+ return;
11910
+ }
11911
+ const pid = record.pid;
11912
+ const url = record.url;
11913
+ if (typeof pid !== "number" || typeof url !== "string")
11914
+ return;
11915
+ if (!pidIsRunning(pid)) {
11916
+ fs22.rmSync(file, { force: true });
11917
+ return;
11918
+ }
11919
+ this.watchCodexAppServer(id, url, pid);
11920
+ log(`codex app-server: reconnected to ${url} (pid ${pid}) for session ${id.slice(0, 6)}`);
11921
+ return url;
11922
+ }
11923
+ watchCodexAppServer(id, url, pid) {
11924
+ const client = new CodexAppServerClient(url, (payload) => this.ingestHookEvent(id, payload));
11925
+ client.connect();
11926
+ this.codexServers.set(id, {
11927
+ stop: () => {
11928
+ client.close();
11929
+ this.killCodexAppServer(pid);
11930
+ fs22.rmSync(this.codexAppServerFile(id), { force: true });
11931
+ }
11932
+ });
11933
+ }
11934
+ killCodexAppServer(pid) {
11935
+ try {
11936
+ killTree(pid, "codex.exe");
11937
+ } catch {}
11938
+ }
11140
11939
  stopCodexAppServer(id) {
11141
11940
  this.codexServers.get(id)?.stop();
11142
11941
  this.codexServers.delete(id);
11143
11942
  }
11144
11943
  ensureCodexHome() {
11145
- const home = path19.join(STATE_DIR, "codex-home");
11146
- fs19.mkdirSync(home, { recursive: true, mode: 448 });
11944
+ const home = path21.join(STATE_DIR, "codex-home");
11945
+ fs22.mkdirSync(home, { recursive: true, mode: 448 });
11147
11946
  this.linkCodexAuth(home);
11148
11947
  const bridge = hookLaunch({ execPath: process.execPath, mainScript: Bun.main, agent: "codex" });
11149
11948
  const hooksFile = buildCodexHooks({ command: bridge.command, args: bridge.args });
11150
11949
  const hooksPath = codexTrustKeyPath(home);
11151
11950
  const tmp = `${hooksPath}.tmp`;
11152
- fs19.writeFileSync(tmp, JSON.stringify(hooksFile, null, 2));
11153
- fs19.renameSync(tmp, hooksPath);
11951
+ fs22.writeFileSync(tmp, JSON.stringify(hooksFile, null, 2));
11952
+ fs22.renameSync(tmp, hooksPath);
11154
11953
  const entries = codexHookTrust(hooksPath, hooksFile);
11155
- const configPath = path19.join(home, "config.toml");
11156
- const existing = fs19.existsSync(configPath) ? fs19.readFileSync(configPath, "utf8") : "";
11954
+ const configPath = path21.join(home, "config.toml");
11955
+ const existing = fs22.existsSync(configPath) ? fs22.readFileSync(configPath, "utf8") : "";
11157
11956
  const missing = entries.filter((e) => !existing.includes(`"${e.key.replaceAll("\\", "\\\\")}"`));
11158
11957
  if (missing.length > 0) {
11159
11958
  const block = codexTrustStateToml(missing);
11160
- fs19.appendFileSync(configPath, existing !== "" && !existing.endsWith(`
11959
+ fs22.appendFileSync(configPath, existing !== "" && !existing.endsWith(`
11161
11960
  `) ? `
11162
11961
  ${block}` : block);
11163
11962
  }
@@ -11170,54 +11969,55 @@ ${block}` : block);
11170
11969
  return home;
11171
11970
  }
11172
11971
  linkCodexAuth(home) {
11173
- const target = path19.join(home, "auth.json");
11174
- if (fs19.existsSync(target))
11972
+ const target = path21.join(home, "auth.json");
11973
+ if (fs22.existsSync(target))
11175
11974
  return;
11176
- const operatorHome = process.env["CODEX_HOME"] ?? path19.join(os5.homedir(), ".codex");
11177
- const source = path19.join(operatorHome, "auth.json");
11178
- if (!fs19.existsSync(source))
11975
+ const operatorHome = process.env["CODEX_HOME"] ?? path21.join(os7.homedir(), ".codex");
11976
+ const source = path21.join(operatorHome, "auth.json");
11977
+ if (!fs22.existsSync(source))
11179
11978
  throw new AxtermError("err.codex.noAuth", { authFile: source });
11979
+ let how;
11180
11980
  try {
11181
- fs19.symlinkSync(source, target, "file");
11182
- } catch {
11183
- try {
11184
- fs19.linkSync(source, target);
11185
- } catch (err) {
11186
- throw new AxtermError("err.codex.authLink", { reason: err instanceof Error ? err.message : String(err) });
11187
- }
11981
+ how = referenceCredential(source, target);
11982
+ } catch (err) {
11983
+ throw new AxtermError("err.codex.authLink", { reason: err instanceof Error ? err.message : String(err) });
11984
+ }
11985
+ if (how === "copy") {
11986
+ 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`);
11188
11987
  }
11189
11988
  }
11190
11989
  doctorReport() {
11191
11990
  const bridge = hookLaunch({ execPath: process.execPath, mainScript: Bun.main, agent: "codex" });
11192
- const home = path19.join(STATE_DIR, "codex-home");
11193
- const operatorHome = process.env["CODEX_HOME"] ?? path19.join(os5.homedir(), ".codex");
11194
- const authSource = path19.join(operatorHome, "auth.json");
11991
+ const home = path21.join(STATE_DIR, "codex-home");
11992
+ const operatorHome = process.env["CODEX_HOME"] ?? path21.join(os7.homedir(), ".codex");
11993
+ const authSource = path21.join(operatorHome, "auth.json");
11195
11994
  const geminiSettings = this.geminiSettingsPath();
11196
- const configPath = path19.join(home, "config.toml");
11995
+ const configPath = path21.join(home, "config.toml");
11197
11996
  const read = (file) => {
11198
11997
  try {
11199
- return fs19.existsSync(file) ? fs19.readFileSync(file, "utf8") : undefined;
11998
+ return fs22.existsSync(file) ? fs22.readFileSync(file, "utf8") : undefined;
11200
11999
  } catch {
11201
12000
  return;
11202
12001
  }
11203
12002
  };
11204
12003
  return [
11205
- ...evaluateBridgeDoctor({ command: bridge.command, exists: fs19.existsSync(bridge.command) }),
12004
+ ...evaluateBridgeDoctor({ command: bridge.command, exists: fs22.existsSync(bridge.command) }),
11206
12005
  ...evaluateOmpDoctor({
11207
12006
  binary: lookupOnPath("omp"),
11208
12007
  extensionPath: this.hooks.extensionPath,
11209
- extensionExists: fs19.existsSync(this.hooks.extensionPath)
12008
+ extensionExists: fs22.existsSync(this.hooks.extensionPath)
11210
12009
  }),
11211
12010
  ...evaluateClaudeDoctor({ binary: lookupOnPath("claude") }),
11212
12011
  ...evaluateCodexDoctor({
11213
12012
  binary: lookupOnPath("codex"),
11214
12013
  authSourcePath: authSource,
11215
- authSourceExists: fs19.existsSync(authSource),
11216
- homeExists: fs19.existsSync(home),
12014
+ authSourceExists: fs22.existsSync(authSource),
12015
+ homeExists: fs22.existsSync(home),
11217
12016
  expected: codexHookTrust(codexTrustKeyPath(home), buildCodexHooks({ command: bridge.command, args: bridge.args })),
11218
12017
  configToml: read(configPath)
11219
12018
  }),
11220
- ...evaluateGeminiDoctor({ binary: lookupOnPath("gemini"), settingsJson: read(geminiSettings) })
12019
+ ...evaluateGeminiDoctor({ binary: lookupOnPath("gemini"), settingsJson: read(geminiSettings) }),
12020
+ ...evaluateStagingDoctor({ mode: this.stagingMode() })
11221
12021
  ];
11222
12022
  }
11223
12023
  quotaReport() {
@@ -11311,6 +12111,18 @@ ${block}` : block);
11311
12111
  session.write(digit);
11312
12112
  return true;
11313
12113
  }
12114
+ answerScreenChoice(idOrPrefix, optionIndex) {
12115
+ const session = this.resolveLive(idOrPrefix);
12116
+ const dialog = detectOnboarding(agentIdFromCommand(session.info.agentCommand), session.screenText(30));
12117
+ const digit = dialog?.options[optionIndex]?.digit;
12118
+ if (digit === undefined) {
12119
+ session.write(buildMenuSelectKeys(optionIndex));
12120
+ return "menu";
12121
+ }
12122
+ this.pendingPrompt.delete(session.info.id);
12123
+ session.write(digit);
12124
+ return "onboarding";
12125
+ }
11314
12126
  sinceLastCapture(id) {
11315
12127
  const at = this.lastCaptureAt.get(id);
11316
12128
  return at === undefined ? undefined : Date.now() - at;
@@ -11337,50 +12149,92 @@ ${block}` : block);
11337
12149
  }
11338
12150
  writeScreenFile(idOrPrefix, lines) {
11339
12151
  const session = this.resolveLive(idOrPrefix);
11340
- const file = path19.join(path19.dirname(this.hooks.endpointFile), `screen-${session.info.id}.txt`);
11341
- fs19.writeFileSync(file, session.screenText(lines), { mode: 384 });
12152
+ const file = path21.join(path21.dirname(this.hooks.endpointFile), `screen-${session.info.id}.txt`);
12153
+ fs22.writeFileSync(file, session.screenText(lines), { mode: 384 });
11342
12154
  return file;
11343
12155
  }
11344
12156
  removeScreenFile(id) {
11345
12157
  try {
11346
- fs19.rmSync(path19.join(path19.dirname(this.hooks.endpointFile), `screen-${id}.txt`), { force: true });
12158
+ fs22.rmSync(path21.join(path21.dirname(this.hooks.endpointFile), `screen-${id}.txt`), { force: true });
11347
12159
  } catch {}
11348
12160
  }
11349
12161
  async stageInboundFile(idOrPrefix, suggestedName, bytes) {
11350
12162
  const info = this.resolveLive(idOrPrefix).info;
11351
- const file = path19.resolve(planMaterialisedPath(MATERIALISED_DIR, info.id, suggestedName));
11352
- await fs19.promises.mkdir(path19.dirname(file), { recursive: true, mode: 448 });
11353
- await fs19.promises.writeFile(file, bytes, { mode: 384 });
11354
- log(`session ${info.shortId} staged ${path19.basename(file)} (${bytes.byteLength} bytes)`);
12163
+ if (this.stagingMode() === "runtime")
12164
+ return this.stageInRuntimeDir(info, suggestedName, bytes);
12165
+ const ws = this.workspaceFor(info.id);
12166
+ const dir = sessionScratchRoot(info);
12167
+ const file = path21.resolve(planMaterialisedPath(inboundStagingDir(dir), info.id, suggestedName));
12168
+ await this.prepareScratchDir(ws, dir, file);
12169
+ await ws.write(file, bytes, 384);
12170
+ log(`session ${info.shortId} staged ${path21.basename(file)} (${bytes.byteLength} bytes)`);
12171
+ return file;
12172
+ }
12173
+ stagingMode() {
12174
+ return this.configuredDefaults().staging ?? "project";
12175
+ }
12176
+ async stageInRuntimeDir(info, suggestedName, bytes) {
12177
+ const file = path21.resolve(planMaterialisedPath(MATERIALISED_DIR, info.id, suggestedName));
12178
+ await fs22.promises.mkdir(path21.dirname(file), { recursive: true, mode: 448 });
12179
+ await fs22.promises.writeFile(file, bytes, { mode: 384 });
12180
+ log(`session ${info.shortId} staged ${path21.basename(file)} (${bytes.byteLength} bytes, runtime dir)`);
11355
12181
  return file;
11356
12182
  }
11357
- removeStagedFiles(id) {
12183
+ async prepareScratchDir(ws, sessionDir, file) {
12184
+ const scratch = scratchDir(sessionDir);
12185
+ const ignore = `${scratch}/${SCRATCH_IGNORE_NAME}`;
11358
12186
  try {
11359
- fs19.rmSync(path19.join(MATERIALISED_DIR, id), { recursive: true, force: true });
12187
+ await ws.mkdirp(scratch, 448);
12188
+ if (await ws.stat(ignore) === undefined)
12189
+ await ws.write(ignore, new TextEncoder().encode(SCRATCH_IGNORE_BODY));
12190
+ } catch (err) {
12191
+ throw new AxtermError("err.staging.dir", { dir: scratch, error: err instanceof Error ? err.message : String(err) });
12192
+ }
12193
+ if (await ignoreState(ws, sessionDir, file) === "visible") {
12194
+ throw new AxtermError("err.staging.notIgnored", { dir: scratch, file: SCRATCH_IGNORE_NAME });
12195
+ }
12196
+ try {
12197
+ await ws.mkdirp(path21.dirname(file), 448);
12198
+ } catch (err) {
12199
+ throw new AxtermError("err.staging.dir", { dir: scratch, error: err instanceof Error ? err.message : String(err) });
12200
+ }
12201
+ }
12202
+ removeStagedFiles(id, info) {
12203
+ try {
12204
+ fs22.rmSync(path21.join(MATERIALISED_DIR, id), { recursive: true, force: true });
12205
+ } catch {}
12206
+ if (info === undefined)
12207
+ return;
12208
+ const scratch = path21.resolve(scratchDir(sessionScratchRoot(info)));
12209
+ try {
12210
+ fs22.rmSync(path21.resolve(inboundStagingDir(sessionScratchRoot(info)), id), { recursive: true, force: true });
12211
+ fs22.rmdirSync(path21.join(scratch, "inbound"));
12212
+ fs22.rmSync(path21.join(scratch, SCRATCH_IGNORE_NAME), { force: true });
12213
+ fs22.rmdirSync(scratch);
11360
12214
  } catch {}
11361
12215
  }
11362
12216
  removeClaudeSettings(id) {
11363
12217
  try {
11364
- fs19.rmSync(this.claudeSettingsPath(id), { force: true });
11365
- fs19.rmSync(this.mcpConfigPath(id), { force: true });
11366
- fs19.rmSync(this.mcpContextPath(id), { force: true });
11367
- fs19.rmSync(this.geminiTelemetryPath(id), { force: true });
12218
+ fs22.rmSync(this.claudeSettingsPath(id), { force: true });
12219
+ fs22.rmSync(this.mcpConfigPath(id), { force: true });
12220
+ fs22.rmSync(this.mcpContextPath(id), { force: true });
12221
+ fs22.rmSync(this.geminiTelemetryPath(id), { force: true });
11368
12222
  this.stopCodexAppServer(id);
11369
12223
  } catch {}
11370
12224
  }
11371
12225
  claudeSettingsPath(id) {
11372
- return path19.join(path19.dirname(this.hooks.endpointFile), `claude-${id}.json`);
12226
+ return path21.join(path21.dirname(this.hooks.endpointFile), `claude-${id}.json`);
11373
12227
  }
11374
12228
  mcpConfigPath(id) {
11375
- return path19.join(path19.dirname(this.hooks.endpointFile), `mcp-${id}.json`);
12229
+ return path21.join(path21.dirname(this.hooks.endpointFile), `mcp-${id}.json`);
11376
12230
  }
11377
12231
  mcpContextPath(id) {
11378
- return path19.join(path19.dirname(this.hooks.endpointFile), `mcp-ctx-${id}.json`);
12232
+ return path21.join(path21.dirname(this.hooks.endpointFile), `mcp-ctx-${id}.json`);
11379
12233
  }
11380
12234
  writeMcpContext(id, launchToken) {
11381
12235
  const file = this.mcpContextPath(id);
11382
12236
  try {
11383
- fs19.writeFileSync(file, JSON.stringify({ port: this.hooks.port, token: this.hooks.host.token, sessionId: id, launchToken }), { mode: 384 });
12237
+ fs22.writeFileSync(file, JSON.stringify({ port: this.hooks.port, token: this.hooks.host.token, sessionId: id, launchToken }), { mode: 384 });
11384
12238
  return file;
11385
12239
  } catch (err) {
11386
12240
  log(`mcp context write failed; session starts without axterm's tools: ${String(err)}`);
@@ -11393,7 +12247,7 @@ ${block}` : block);
11393
12247
  writeMcpConfig(id, contextPath) {
11394
12248
  const file = this.mcpConfigPath(id);
11395
12249
  try {
11396
- fs19.writeFileSync(file, claudeMcpConfig(this.mcpServerLaunch(), contextPath), { mode: 384 });
12250
+ fs22.writeFileSync(file, claudeMcpConfig(this.mcpServerLaunch(), contextPath), { mode: 384 });
11397
12251
  return file;
11398
12252
  } catch (err) {
11399
12253
  log(`mcp config write failed; session starts without axterm's tools: ${String(err)}`);
@@ -11401,12 +12255,38 @@ ${block}` : block);
11401
12255
  }
11402
12256
  }
11403
12257
  geminiTelemetryPath(id) {
11404
- return path19.join(path19.dirname(this.hooks.endpointFile), `gemini-telemetry-${id}.log`);
12258
+ return path21.join(path21.dirname(this.hooks.endpointFile), `gemini-telemetry-${id}.log`);
11405
12259
  }
11406
12260
  geminiSettingsPath() {
11407
- return path19.join(path19.dirname(this.hooks.endpointFile), "gemini-defaults.json");
12261
+ return path21.join(path21.dirname(this.hooks.endpointFile), "gemini-defaults.json");
12262
+ }
12263
+ async ptySpawner() {
12264
+ if (process.env["AXTERM_PTY_HOST"] !== "1" || this.ptyHostUnavailable)
12265
+ return { spawn: spawnPty, hostOwned: false };
12266
+ try {
12267
+ if (this.ptyHost === undefined) {
12268
+ const override = process.env["AXTERM_PTY_HOST_MAIN"];
12269
+ const info = await ensurePtyHost({ runtimeDir: RUNTIME_DIR, ...override ? { mainScript: override } : {} });
12270
+ this.ptyHost = await connectPtyHost({ endpoint: info.endpoint, token: info.token });
12271
+ log(`pty host: connected (pid ${info.pid})`);
12272
+ }
12273
+ const host = this.ptyHost;
12274
+ return { spawn: (options) => host.spawn(options), hostOwned: true };
12275
+ } catch (err) {
12276
+ this.ptyHostUnavailable = true;
12277
+ log(`pty host unavailable, owning ptys in this process: ${err instanceof Error ? err.message : String(err)}`);
12278
+ return { spawn: spawnPty, hostOwned: false };
12279
+ }
11408
12280
  }
11409
- launch(info, shell, agentCommand, appServerUrl) {
12281
+ requireClaudeConversation(agentCommand) {
12282
+ const conversation = resumedConversationId(agentCommand);
12283
+ if (conversation === undefined)
12284
+ return;
12285
+ if (claudeConversationExists(claudeConfigDir(), conversation) === false) {
12286
+ throw new AxtermError("err.claude.noConversation", { id: conversation });
12287
+ }
12288
+ }
12289
+ async launch(info, shell, agentCommand) {
11410
12290
  const id = info.id;
11411
12291
  const launchToken = randomBytes2(16).toString("hex");
11412
12292
  const agentId = agentIdFromCommand(info.agentCommand);
@@ -11414,22 +12294,30 @@ ${block}` : block);
11414
12294
  this.writeClaudeSettings(id, launchToken);
11415
12295
  if (agentId !== "omp")
11416
12296
  this.writeMcpContext(id, launchToken);
12297
+ if (agentId === "claude")
12298
+ this.writeMcpConfig(id, this.mcpContextPath(id));
11417
12299
  if (agentId === "gemini")
11418
12300
  this.writeGeminiSettings(id);
11419
12301
  const codexHome2 = agentId === "codex" ? this.ensureCodexHome() : undefined;
11420
- const session = new PtySession(info, {
12302
+ if (agentId === "claude")
12303
+ this.requireClaudeConversation(agentCommand);
12304
+ const identity = {
12305
+ sessionId: id,
12306
+ launchToken,
12307
+ hookPort: this.hooks.port,
12308
+ hookToken: this.hooks.host.token,
12309
+ endpointFile: this.hooks.endpointFile
12310
+ };
12311
+ const appServerUrl = codexHome2 === undefined ? undefined : await this.startCodexAppServer(id, codexHome2, info.cwd, identity);
12312
+ const spawner = await this.ptySpawner();
12313
+ const session = await PtySession.open(info, {
11421
12314
  exe: shell.exe,
11422
12315
  argv: buildShellArgv(shell, withAppServer(agentCommand, appServerUrl)),
11423
12316
  cwd: info.cwd,
11424
12317
  cols: info.cols,
11425
12318
  rows: info.rows,
11426
12319
  env: {
11427
- ...stripHostAttribution(process.env),
11428
- AXTERM_SESSION_ID: id,
11429
- AXTERM_LAUNCH_TOKEN: launchToken,
11430
- AXTERM_HOOK_PORT: String(this.hooks.port),
11431
- AXTERM_HOOK_TOKEN: this.hooks.host.token,
11432
- AXTERM_HOOK_ENDPOINT: this.hooks.endpointFile,
12320
+ ...sessionEnv(process.env, identity),
11433
12321
  ...agentId === "gemini" ? {
11434
12322
  GEMINI_CLI_SYSTEM_DEFAULTS_PATH: this.geminiSettingsPath(),
11435
12323
  GEMINI_TELEMETRY_ENABLED: "true",
@@ -11440,7 +12328,15 @@ ${block}` : block);
11440
12328
  } : {},
11441
12329
  ...codexHome2 !== undefined ? { CODEX_HOME: codexHome2 } : {}
11442
12330
  }
11443
- });
12331
+ }, spawner.spawn);
12332
+ if (spawner.hostOwned)
12333
+ this.hostOwned.add(id);
12334
+ this.wire(session, launchToken);
12335
+ return session;
12336
+ }
12337
+ wire(session, launchToken) {
12338
+ const id = session.info.id;
12339
+ const info = session.info;
11444
12340
  this.agentRuntime.set(id, { agentExited: false, choiceSeq: 0, dialogOpen: false });
11445
12341
  this.hooks.host.registerSession(id, launchToken, (payload) => this.ingestHookEvent(id, payload));
11446
12342
  session.onData(() => {
@@ -11456,7 +12352,7 @@ ${block}` : block);
11456
12352
  this.hooks.host.unregisterSession(id);
11457
12353
  this.removeClaudeSettings(id);
11458
12354
  this.removeScreenFile(id);
11459
- this.removeStagedFiles(id);
12355
+ this.removeStagedFiles(id, info);
11460
12356
  clearTimeout(this.idleTimers.get(id));
11461
12357
  this.idleTimers.delete(id);
11462
12358
  this.sawOutput.delete(id);
@@ -11467,11 +12363,11 @@ ${block}` : block);
11467
12363
  this.pendingPrompt.delete(id);
11468
12364
  this.live.delete(id);
11469
12365
  this.agentRuntime.delete(id);
12366
+ this.hostOwned.delete(id);
11470
12367
  log(`session ${info.shortId} exited code=${code}`);
11471
12368
  this.emit({ t: "session.exit", sessionId: id, code });
11472
12369
  });
11473
12370
  this.live.set(id, session);
11474
- return session;
11475
12371
  }
11476
12372
  emitActivity(id, payload) {
11477
12373
  const dropped = payload.dropped;
@@ -11615,7 +12511,8 @@ ${block}` : block);
11615
12511
  const direct = this.live.get(idOrPrefix);
11616
12512
  if (direct)
11617
12513
  return direct;
11618
- const matches = [...this.live.values()].filter((s) => s.info.id.startsWith(idOrPrefix) || s.info.shortId === idOrPrefix || s.info.name === idOrPrefix);
12514
+ const alternate = sessionIdFromUuid(idOrPrefix);
12515
+ const matches = [...this.live.values()].filter((s) => s.info.id.startsWith(idOrPrefix) || s.info.shortId === idOrPrefix || s.info.name === idOrPrefix || alternate !== undefined && s.info.id.startsWith(alternate));
11619
12516
  if (matches.length === 1)
11620
12517
  return matches[0];
11621
12518
  if (matches.length > 1)
@@ -11641,37 +12538,14 @@ ${block}` : block);
11641
12538
  allSessions() {
11642
12539
  return sessionsByRecency([...this.live.values()].map((s) => s.info), this.store.listSessions());
11643
12540
  }
11644
- resumeNamed(stored, agent) {
11645
- if (!fs19.existsSync(stored.cwd))
11646
- throw new AxtermError("err.session.cwdGone", { cwd: stored.cwd });
11647
- const agentCommand = buildAgentResumeCommand({
11648
- agent,
11649
- sessionId: stored.id,
11650
- extensionPath: this.hooks.extensionPath,
11651
- claudeSettingsPath: this.claudeSettingsPath(stored.id)
11652
- });
11653
- const info = { ...stored, agentCommand, state: "running" };
11654
- delete info.exitCode;
11655
- delete info.exitedAt;
11656
- delete info.agentState;
11657
- delete info.agentStateSource;
11658
- this.launch(info, resolveShell(info.shell), agentCommand);
11659
- this.store.markResumed(info.id, agentCommand, info.ptyPid);
11660
- log(`session ${info.shortId} resumed cmd=${agentCommand.join(" ")}`);
11661
- this.emit({ t: "session.created", info });
11662
- return info;
11663
- }
11664
12541
  async resume(idOrPrefix) {
11665
12542
  const stored = this.store.findByPrefix(idOrPrefix);
11666
12543
  if (!stored)
11667
12544
  throw new AxtermError("err.session.unknown", { id: idOrPrefix });
11668
12545
  if (this.live.has(stored.id))
11669
12546
  throw new AxtermError("err.session.running", { id: stored.shortId });
11670
- const storedAgent = agentIdFromCommand(stored.agentCommand);
11671
- if (storedAgent === "claude" || storedAgent === "gemini")
11672
- return this.resumeNamed(stored, storedAgent);
11673
12547
  const profile = agentProfile(agentIdFromCommand(stored.agentCommand));
11674
- const reference = resumeReference(profile, stored);
12548
+ const reference = resumeReference(profile, { ...stored, ownSessionId: stored.id });
11675
12549
  if (reference === undefined) {
11676
12550
  throw new AxtermError("err.session.noConversation", {
11677
12551
  id: stored.shortId,
@@ -11682,7 +12556,7 @@ ${block}` : block);
11682
12556
  if (agentCommand === undefined) {
11683
12557
  throw new AxtermError("err.session.noResumeSupport", { agent: profile.displayName });
11684
12558
  }
11685
- if (!fs19.existsSync(stored.cwd))
12559
+ if (!fs22.existsSync(stored.cwd))
11686
12560
  throw new AxtermError("err.session.cwdGone", { cwd: stored.cwd });
11687
12561
  const info = { ...stored, agentCommand, state: "running" };
11688
12562
  delete info.exitCode;
@@ -11690,8 +12564,7 @@ ${block}` : block);
11690
12564
  delete info.agentState;
11691
12565
  delete info.agentStateSource;
11692
12566
  const shell = resolveShell(info.shell);
11693
- const resumedAppServer = agentIdFromCommand(agentCommand) === "codex" ? await this.startCodexAppServer(info.id, this.ensureCodexHome(), info.cwd) : undefined;
11694
- this.launch(info, shell, agentCommand, resumedAppServer);
12567
+ await this.launch(info, shell, agentCommand);
11695
12568
  this.store.markResumed(info.id, agentCommand, info.ptyPid);
11696
12569
  log(`session ${info.shortId} resumed cmd=${agentCommand.join(" ")}`);
11697
12570
  this.emit({ t: "session.created", info });
@@ -11732,7 +12605,8 @@ ${block}` : block);
11732
12605
  if (err instanceof AxtermError && err.key === "err.session.ambiguous")
11733
12606
  throw err;
11734
12607
  }
11735
- const id = live?.info.id ?? this.store.findByPrefix(idOrPrefix)?.id;
12608
+ const info = live?.info ?? this.store.findByPrefix(idOrPrefix);
12609
+ const id = info?.id;
11736
12610
  if (id === undefined)
11737
12611
  throw new AxtermError("err.session.missing", { id: idOrPrefix });
11738
12612
  if (live !== undefined && !live.isExited) {
@@ -11747,7 +12621,7 @@ ${block}` : block);
11747
12621
  await promise;
11748
12622
  }
11749
12623
  this.removeScreenFile(id);
11750
- this.removeStagedFiles(id);
12624
+ this.removeStagedFiles(id, info);
11751
12625
  this.removeClaudeSettings(id);
11752
12626
  this.removeScrollback(id);
11753
12627
  this.store.deleteSession(id);
@@ -11756,7 +12630,7 @@ ${block}` : block);
11756
12630
  removeScrollback(id) {
11757
12631
  for (const file of [`${id}.bin`, `${id}.bin.tmp`]) {
11758
12632
  try {
11759
- fs19.rmSync(path19.join(SCROLLBACK_DIR, file), { force: true });
12633
+ fs22.rmSync(path21.join(SCROLLBACK_DIR, file), { force: true });
11760
12634
  } catch {}
11761
12635
  }
11762
12636
  }
@@ -11764,6 +12638,14 @@ ${block}` : block);
11764
12638
  this.drainGeminiTelemetry();
11765
12639
  this.watchForStalls();
11766
12640
  }
12641
+ skipGeminiTelemetryHistory(id) {
12642
+ const runtime = this.agentRuntime.get(id);
12643
+ if (runtime === undefined)
12644
+ return;
12645
+ try {
12646
+ runtime.telemetryOffset = fs22.statSync(this.geminiTelemetryPath(id)).size;
12647
+ } catch {}
12648
+ }
11767
12649
  drainGeminiTelemetry() {
11768
12650
  for (const [id, session] of this.live) {
11769
12651
  if (agentIdFromCommand(session.info.agentCommand) !== "gemini")
@@ -11774,7 +12656,7 @@ ${block}` : block);
11774
12656
  const file = this.geminiTelemetryPath(id);
11775
12657
  let size = 0;
11776
12658
  try {
11777
- size = fs19.statSync(file).size;
12659
+ size = fs22.statSync(file).size;
11778
12660
  } catch {
11779
12661
  continue;
11780
12662
  }
@@ -11836,10 +12718,17 @@ ${block}` : block);
11836
12718
  clearInterval(this.sessionWatch);
11837
12719
  for (const id of [...this.codexServers.keys()])
11838
12720
  this.stopCodexAppServer(id);
11839
- for (const session of this.live.values()) {
12721
+ let handedOn = 0;
12722
+ for (const [id, session] of this.live) {
11840
12723
  session.saveScrollback();
12724
+ if (this.hostOwned.has(id)) {
12725
+ handedOn += 1;
12726
+ continue;
12727
+ }
11841
12728
  session.kill();
11842
12729
  }
12730
+ if (handedOn > 0)
12731
+ log(`left ${handedOn} session(s) running in the terminal host for the next daemon`);
11843
12732
  this.live.clear();
11844
12733
  this.agentRuntime.clear();
11845
12734
  }
@@ -11848,9 +12737,13 @@ var DEFAULT_COLS = 120, DEFAULT_ROWS = 30, MATERIALISED_DIR, ACTIVITY_ARG_CHARS
11848
12737
  var init_session_manager = __esm(() => {
11849
12738
  init_src();
11850
12739
  init_codex_home();
11851
- init_host_env();
12740
+ init_session_env();
11852
12741
  init_git();
12742
+ init_claude_store();
11853
12743
  init_injector();
12744
+ init_pty_host_client();
12745
+ init_pty_host_launch();
12746
+ init_pty();
11854
12747
  init_occupancy();
11855
12748
  init_agent_store();
11856
12749
  init_codex_app_server();
@@ -11859,7 +12752,7 @@ var init_session_manager = __esm(() => {
11859
12752
  init_pty_session();
11860
12753
  init_shell();
11861
12754
  init_workspace_local();
11862
- MATERIALISED_DIR = path19.join(RUNTIME_DIR, "materialised");
12755
+ MATERIALISED_DIR = path21.join(RUNTIME_DIR, "materialised");
11863
12756
  PROMPT_OPTIONS = {
11864
12757
  hostKey: [{ label: "yes" }, { label: "no" }],
11865
12758
  password: [],
@@ -11890,6 +12783,9 @@ class FetchTelegramApi {
11890
12783
  throw new Error(`telegram file download failed: ${res.status}`);
11891
12784
  return new Uint8Array(await res.arrayBuffer());
11892
12785
  }
12786
+ async deleteMessage(chatId, messageId) {
12787
+ await this.call("deleteMessage", { chat_id: chatId, message_id: messageId });
12788
+ }
11893
12789
  async call(method, params = {}) {
11894
12790
  const attempt = async () => {
11895
12791
  const res = await fetch(`https://api.telegram.org/bot${this.botToken}/${method}`, {
@@ -12008,11 +12904,11 @@ var init_acknowledge = __esm(() => {
12008
12904
  });
12009
12905
 
12010
12906
  // packages/channel-telegram/src/attach.ts
12011
- import fs20 from "fs";
12012
- import path20 from "path";
12907
+ import fs23 from "fs";
12908
+ import path22 from "path";
12013
12909
  function buildAttachmentUpload(filePath, chatId, topicId, caption, limits = TELEGRAM_LIMITS) {
12014
- const name = path20.basename(filePath);
12015
- const bytes = fs20.statSync(filePath).size;
12910
+ const name = path22.basename(filePath);
12911
+ const bytes = fs23.statSync(filePath).size;
12016
12912
  const plan = planAttachment({ name, bytes }, limits);
12017
12913
  if (plan.kind === "rejected")
12018
12914
  throw new Error(plan.reason);
@@ -12022,7 +12918,7 @@ function buildAttachmentUpload(filePath, chatId, topicId, caption, limits = TELE
12022
12918
  form.append("message_thread_id", String(topicId));
12023
12919
  if (caption !== undefined && caption.length > 0)
12024
12920
  form.append("caption", caption);
12025
- form.append(field, new Blob([fs20.readFileSync(filePath)]), name);
12921
+ form.append(field, new Blob([fs23.readFileSync(filePath)]), name);
12026
12922
  return { method: plan.kind === "photo" ? "sendPhoto" : "sendDocument", field, form };
12027
12923
  }
12028
12924
  function isPhotoRejection(error) {
@@ -12030,13 +12926,13 @@ function isPhotoRejection(error) {
12030
12926
  return /IMAGE_PROCESS_FAILED|PHOTO_INVALID_DIMENSIONS|PHOTO_EXT_INVALID|PHOTO_SAVE_FILE_INVALID/i.test(message);
12031
12927
  }
12032
12928
  function asDocumentUpload(filePath, chatId, topicId, caption) {
12033
- const name = path20.basename(filePath);
12929
+ const name = path22.basename(filePath);
12034
12930
  const form = new FormData;
12035
12931
  form.append("chat_id", String(chatId));
12036
12932
  form.append("message_thread_id", String(topicId));
12037
12933
  if (caption !== undefined && caption.length > 0)
12038
12934
  form.append("caption", caption);
12039
- form.append("document", new Blob([fs20.readFileSync(filePath)]), name);
12935
+ form.append("document", new Blob([fs23.readFileSync(filePath)]), name);
12040
12936
  return { method: "sendDocument", field: "document", form };
12041
12937
  }
12042
12938
  var TELEGRAM_LIMITS;
@@ -12622,6 +13518,7 @@ class TelegramChannel {
12622
13518
  updateOffset = 0;
12623
13519
  albums = new Map;
12624
13520
  inboundQueues = new Map;
13521
+ refusedAlbums = new Map;
12625
13522
  albumDebounceMs;
12626
13523
  inboundLimits;
12627
13524
  inboundEnabled;
@@ -12931,6 +13828,26 @@ ${code(prompt.line)}`,
12931
13828
  });
12932
13829
  }
12933
13830
  awaitingSecret = new Map;
13831
+ async captureSecret(sessionId, msg, text) {
13832
+ const seq = this.awaitingSecret.get(sessionId);
13833
+ if (seq === undefined)
13834
+ return false;
13835
+ this.awaitingSecret.delete(sessionId);
13836
+ const accepted = this.control.answerShellPrompt(sessionId, seq, text);
13837
+ let deleted = false;
13838
+ if (this.api.deleteMessage && msg.chat && msg.message_id !== undefined) {
13839
+ try {
13840
+ await this.api.deleteMessage(msg.chat.id, msg.message_id);
13841
+ deleted = true;
13842
+ } catch (err) {
13843
+ this.log(`secret reply message ${msg.message_id} not deleted: ${String(err)}`);
13844
+ }
13845
+ }
13846
+ await this.sendToSession(sessionId, tHtml(accepted ? "tg.prompt.secretSubmitted" : "tg.prompt.secretStale"));
13847
+ if (!deleted)
13848
+ await this.sendToSession(sessionId, tHtml("tg.prompt.secretUndeleted"));
13849
+ return true;
13850
+ }
12934
13851
  async onChoiceRequest(sessionId, seq, questions, askId, onboarding2 = false) {
12935
13852
  const binding = this.control.findBindingBySession(sessionId);
12936
13853
  if (!binding || questions.length === 0)
@@ -13076,6 +13993,10 @@ ${code(prompt.line)}`,
13076
13993
  if (!msg.from || !this.config.allowedUserIds.includes(msg.from.id))
13077
13994
  return;
13078
13995
  const binding = msg.message_thread_id ? this.control.findBindingByTopic(msg.chat.id, msg.message_thread_id) : undefined;
13996
+ if (msg.message_thread_id !== undefined && binding === undefined) {
13997
+ this.log(`ignored update in topic ${msg.message_thread_id}: not this machine's`);
13998
+ return;
13999
+ }
13079
14000
  const media = readMedia(msg);
13080
14001
  if (media) {
13081
14002
  await this.handleMedia(msg, binding, media);
@@ -13090,6 +14011,8 @@ ${code(prompt.line)}`,
13090
14011
  }
13091
14012
  if (!binding)
13092
14013
  return;
14014
+ if (await this.captureSecret(binding.sessionId, msg, text))
14015
+ return;
13093
14016
  if (await this.captureFreeText(binding.sessionId, text))
13094
14017
  return;
13095
14018
  const chatId = msg.chat.id;
@@ -13108,28 +14031,47 @@ ${code(prompt.line)}`,
13108
14031
  return;
13109
14032
  }
13110
14033
  if (!binding) {
13111
- await this.refuseMedia(msg, tHtml("tg.inbound.noSession"));
14034
+ await this.refuseMedia(msg, tHtml("tg.inbound.noSession"), "no session bound to the topic");
13112
14035
  return;
13113
14036
  }
13114
14037
  const plan = planInboundAttachment(media.candidate, this.inboundLimits);
13115
14038
  if (!plan.ok) {
13116
- await this.refuseMedia(msg, tHtml(REFUSAL_KEYS[plan.reason], { limit: megabytes2(this.inboundLimits.maxBytes) }));
14039
+ const text = tHtml(REFUSAL_KEYS[plan.reason], { limit: megabytes2(this.inboundLimits.maxBytes) });
14040
+ await this.refuseMedia(msg, text, plan.reason);
13117
14041
  return;
13118
14042
  }
13119
14043
  await this.react(msg.chat?.id, msg.message_id, "received");
13120
14044
  this.bufferMedia(msg, binding.sessionId, { fileId: media.fileId, name: plan.name, kind: plan.kind, messageId: msg.message_id });
13121
14045
  }
13122
- async refuseMedia(msg, text) {
14046
+ async refuseMedia(msg, text, reason) {
13123
14047
  const chatId = msg.chat?.id;
14048
+ const where = `chat ${chatId ?? "?"} topic ${msg.message_thread_id ?? "general"}`;
14049
+ const album = msg.media_group_id !== undefined ? ` album ${msg.media_group_id}` : "";
14050
+ this.log(`inbound refused (${reason}) in ${where}${album}, message ${msg.message_id ?? "?"}`);
13124
14051
  await this.react(chatId, msg.message_id, "failed");
13125
14052
  if (chatId === undefined)
13126
14053
  return;
14054
+ if (this.albumAlreadyRefused(msg, reason))
14055
+ return;
14056
+ const attributed = msg.message_thread_id === undefined ? `[${escapeHtml(this.config.machine.name)}] ${text}` : text;
13127
14057
  await sendFormatted(this.api, {
13128
14058
  chat_id: chatId,
13129
14059
  ...msg.message_thread_id !== undefined ? { message_thread_id: msg.message_thread_id } : {},
13130
- text
14060
+ text: attributed
13131
14061
  }).catch((err) => this.log(`inbound refusal failed: ${String(err)}`));
13132
14062
  }
14063
+ albumAlreadyRefused(msg, reason) {
14064
+ const group = msg.media_group_id;
14065
+ if (group === undefined)
14066
+ return false;
14067
+ const key = `${group}\x00${reason}`;
14068
+ const pending = this.refusedAlbums.get(key);
14069
+ clearTimeout(pending);
14070
+ const timer = setTimeout(() => this.refusedAlbums.delete(key), this.albumDebounceMs);
14071
+ timer.unref?.();
14072
+ this.refusedAlbums.set(key, timer);
14073
+ return pending !== undefined;
14074
+ }
13133
14075
  bufferMedia(msg, sessionId, item) {
13134
14076
  const group = msg.media_group_id;
13135
14077
  const caption = msg.caption?.trim();
@@ -13177,8 +14119,8 @@ ${code(prompt.line)}`,
13177
14119
  throw new Error("this Telegram client cannot download files");
13178
14120
  const files = [];
13179
14121
  for (const item of batch.items) {
13180
- const path21 = await this.control.stageInboundFile(batch.sessionId, item.name, await download2(item.fileId));
13181
- files.push({ path: path21, kind: item.kind });
14122
+ const path23 = await this.control.stageInboundFile(batch.sessionId, item.name, await download2(item.fileId));
14123
+ files.push({ path: path23, kind: item.kind });
13182
14124
  }
13183
14125
  await this.control.prompt(batch.sessionId, buildAttachmentPrompt(files, batch.caption));
13184
14126
  this.log(`staged ${files.length} inbound file(s) for ${batch.sessionId}`);
@@ -13301,8 +14243,15 @@ ${code(prompt.line)}`,
13301
14243
  const project = await resolveProject(args[0]);
13302
14244
  if (!project)
13303
14245
  return;
13304
- const name = args.slice(1).join(" ") || undefined;
13305
- await this.control.createSession({ cwd: project.path, ...name !== undefined ? { name } : {} });
14246
+ const tail = args.slice(1);
14247
+ const flagAt = tail.findIndex((token) => token.startsWith("-"));
14248
+ const name = (flagAt < 0 ? tail : tail.slice(0, flagAt)).join(" ") || undefined;
14249
+ const agentArgs = flagAt < 0 ? [] : tail.slice(flagAt);
14250
+ await this.control.createSession({
14251
+ cwd: project.path,
14252
+ ...name !== undefined ? { name } : {},
14253
+ ...agentArgs.length > 0 ? { agentArgs } : {}
14254
+ });
13306
14255
  return;
13307
14256
  }
13308
14257
  case "/wt": {
@@ -13572,13 +14521,13 @@ var init_src2 = __esm(() => {
13572
14521
  });
13573
14522
 
13574
14523
  // packages/daemon/src/machine-identity.ts
13575
- import os6 from "os";
14524
+ import os8 from "os";
13576
14525
  function resolveMachineIdentity(override) {
13577
- const hostname = os6.hostname();
14526
+ const hostname = os8.hostname();
13578
14527
  return {
13579
14528
  name: slugifyMachineName(override && override.trim().length > 0 ? override : hostname),
13580
14529
  hostname,
13581
- addresses: collectAddresses(os6.networkInterfaces())
14530
+ addresses: collectAddresses(os8.networkInterfaces())
13582
14531
  };
13583
14532
  }
13584
14533
  var init_machine_identity = __esm(() => {
@@ -13590,13 +14539,15 @@ class TelegramService {
13590
14539
  config;
13591
14540
  store;
13592
14541
  manager;
14542
+ apiFactory;
13593
14543
  channel;
13594
14544
  unsubscribe;
13595
14545
  runningConfig;
13596
- constructor(config, store, manager) {
14546
+ constructor(config, store, manager, apiFactory = (token) => new FetchTelegramApi(token)) {
13597
14547
  this.config = config;
13598
14548
  this.store = store;
13599
14549
  this.manager = manager;
14550
+ this.apiFactory = apiFactory;
13600
14551
  }
13601
14552
  readSettings() {
13602
14553
  const telegram = this.config.resolved().channels.telegram;
@@ -13645,7 +14596,7 @@ class TelegramService {
13645
14596
  const token = botToken ?? this.readSettings().botToken;
13646
14597
  if (!token)
13647
14598
  throw new AxtermError("err.telegram.noToken");
13648
- const api = new FetchTelegramApi(token);
14599
+ const api = this.apiFactory(token);
13649
14600
  const updates = await api.call("getUpdates", { timeout: 0 });
13650
14601
  return discoverTelegramTargets(updates);
13651
14602
  }
@@ -13660,7 +14611,7 @@ class TelegramService {
13660
14611
  log("telegram: not configured, channel not started");
13661
14612
  return false;
13662
14613
  }
13663
- const api = new FetchTelegramApi(s.botToken);
14614
+ const api = this.apiFactory(s.botToken);
13664
14615
  const config = {
13665
14616
  groupId: s.groupId,
13666
14617
  allowedUserIds: [s.allowUserId],
@@ -13808,8 +14759,8 @@ var init_telegram_service = __esm(() => {
13808
14759
  // packages/daemon/src/index.ts
13809
14760
  var exports_src = {};
13810
14761
  import { randomBytes as randomBytes3 } from "crypto";
13811
- import fs21 from "fs";
13812
- import net4 from "net";
14762
+ import fs24 from "fs";
14763
+ import net5 from "net";
13813
14764
  function isPidAlive(pid) {
13814
14765
  try {
13815
14766
  process.kill(pid, 0);
@@ -13820,9 +14771,9 @@ function isPidAlive(pid) {
13820
14771
  }
13821
14772
  async function main() {
13822
14773
  ensureDataDirs();
13823
- if (fs21.existsSync(RUNTIME_FILE2)) {
14774
+ if (fs24.existsSync(RUNTIME_FILE2)) {
13824
14775
  try {
13825
- const existing = JSON.parse(fs21.readFileSync(RUNTIME_FILE2, "utf8"));
14776
+ const existing = JSON.parse(fs24.readFileSync(RUNTIME_FILE2, "utf8"));
13826
14777
  if (existing.pid !== process.pid && isPidAlive(existing.pid)) {
13827
14778
  log(`daemon already running (pid ${existing.pid}); exiting`);
13828
14779
  process.exit(0);
@@ -13845,7 +14796,7 @@ async function main() {
13845
14796
  version: productVersion(),
13846
14797
  startedAt: Date.now()
13847
14798
  };
13848
- fs21.writeFileSync(RUNTIME_FILE2, JSON.stringify(runtime, null, 2), { mode: 384 });
14799
+ fs24.writeFileSync(RUNTIME_FILE2, JSON.stringify(runtime, null, 2), { mode: 384 });
13849
14800
  restrictFile(RUNTIME_FILE2);
13850
14801
  const config = new ConfigStore(defaultConfigStorePaths(CONFIG_DIR));
13851
14802
  if (config.loadError !== undefined)
@@ -13897,12 +14848,15 @@ async function main() {
13897
14848
  hookHost.stop();
13898
14849
  store.close();
13899
14850
  try {
13900
- const current = JSON.parse(fs21.readFileSync(RUNTIME_FILE2, "utf8"));
14851
+ const current = JSON.parse(fs24.readFileSync(RUNTIME_FILE2, "utf8"));
13901
14852
  if (current.pid === process.pid)
13902
- fs21.rmSync(RUNTIME_FILE2);
14853
+ fs24.rmSync(RUNTIME_FILE2);
13903
14854
  } catch {}
13904
14855
  process.exit(0);
13905
14856
  };
14857
+ const recovered = await manager.recover();
14858
+ if (recovered.adopted > 0)
14859
+ log(`boot: ${recovered.adopted} session(s) adopted from the terminal host`);
13906
14860
  startRpcServer({
13907
14861
  pipePath: PIPE_PATH,
13908
14862
  token,
@@ -13932,14 +14886,219 @@ var init_src3 = __esm(() => {
13932
14886
  init_session_manager();
13933
14887
  init_telegram_service();
13934
14888
  init_store();
13935
- net4.setDefaultAutoSelectFamilyAttemptTimeout(3000);
14889
+ net5.setDefaultAutoSelectFamilyAttemptTimeout(3000);
13936
14890
  main();
13937
14891
  });
13938
14892
 
14893
+ // packages/daemon/src/pty-host.ts
14894
+ import net6 from "net";
14895
+ function startPtyHost(options) {
14896
+ const sessions = new Map;
14897
+ const clients = new Set;
14898
+ const send = (socket, reply) => {
14899
+ if (!socket.destroyed)
14900
+ socket.write(controlFrame(reply));
14901
+ };
14902
+ const broadcast = (frame) => {
14903
+ for (const client of clients)
14904
+ if (!client.destroyed)
14905
+ client.write(frame);
14906
+ };
14907
+ const server = net6.createServer((socket) => {
14908
+ const decoder = new FrameDecoder;
14909
+ let greeted = false;
14910
+ const drop = (reason) => {
14911
+ log(`pty host: dropping client (${reason})`);
14912
+ socket.destroy();
14913
+ clients.delete(socket);
14914
+ };
14915
+ socket.on("close", () => clients.delete(socket));
14916
+ socket.on("error", () => clients.delete(socket));
14917
+ socket.on("data", (chunk) => {
14918
+ let frames;
14919
+ try {
14920
+ frames = decoder.push(new Uint8Array(chunk));
14921
+ } catch (err) {
14922
+ drop(err instanceof Error ? err.message : String(err));
14923
+ return;
14924
+ }
14925
+ for (const frame of frames) {
14926
+ if (frame.kind === FrameKind.Data) {
14927
+ if (!greeted)
14928
+ return drop("data before hello");
14929
+ const session = sessions.get(frame.id);
14930
+ if (session && !session.exited)
14931
+ session.handle.write(new TextDecoder().decode(frame.payload));
14932
+ continue;
14933
+ }
14934
+ const message = parseControl(frame.payload);
14935
+ if (message === undefined)
14936
+ continue;
14937
+ if (message.t === "hello") {
14938
+ if (message.token !== options.token)
14939
+ return drop("bad token");
14940
+ greeted = true;
14941
+ clients.add(socket);
14942
+ send(socket, {
14943
+ t: "welcome",
14944
+ v: PTY_PROTOCOL_VERSION,
14945
+ sessions: [...sessions.entries()].filter(([, s]) => !s.exited).map(([id, s]) => ({ id, pid: s.handle.pid, cols: s.cols, rows: s.rows }))
14946
+ });
14947
+ continue;
14948
+ }
14949
+ if (!greeted)
14950
+ return drop("control before hello");
14951
+ if (message.t === "spawn") {
14952
+ try {
14953
+ const handle = spawnPty({
14954
+ exe: message.exe,
14955
+ argv: message.argv,
14956
+ cwd: message.cwd,
14957
+ cols: message.cols,
14958
+ rows: message.rows,
14959
+ env: message.env,
14960
+ term: message.term
14961
+ });
14962
+ const session = {
14963
+ handle,
14964
+ cols: message.cols,
14965
+ rows: message.rows,
14966
+ exited: false,
14967
+ mirror: new TerminalMirror(message.cols, message.rows, REPLAY_SCROLLBACK_LINES)
14968
+ };
14969
+ sessions.set(message.id, session);
14970
+ handle.onData((text) => {
14971
+ session.mirror.write(text);
14972
+ broadcast(encodeFrame(FrameKind.Data, message.id, new TextEncoder().encode(text)));
14973
+ });
14974
+ handle.onExit((code2) => {
14975
+ session.exited = true;
14976
+ for (const client of clients)
14977
+ send(client, { t: "exit", id: message.id, code: code2 });
14978
+ sessions.delete(message.id);
14979
+ if (sessions.size === 0)
14980
+ options.onIdle?.();
14981
+ });
14982
+ send(socket, { t: "spawned", id: message.id, pid: handle.pid });
14983
+ } catch (err) {
14984
+ send(socket, { t: "error", id: message.id, message: err instanceof Error ? err.message : String(err) });
14985
+ }
14986
+ continue;
14987
+ }
14988
+ if (message.t === "subscribe") {
14989
+ const session = sessions.get(message.id);
14990
+ if (session === undefined) {
14991
+ send(socket, { t: "error", id: message.id, message: "no such session" });
14992
+ continue;
14993
+ }
14994
+ if (message.replay) {
14995
+ const ansi = session.mirror.serialize(REPLAY_MAX_CHARS);
14996
+ if (ansi.length > 0 && !socket.destroyed) {
14997
+ socket.write(encodeFrame(FrameKind.Data, message.id, new TextEncoder().encode(ansi)));
14998
+ }
14999
+ }
15000
+ send(socket, { t: "subscribed", id: message.id });
15001
+ continue;
15002
+ }
15003
+ if (message.t === "resize") {
15004
+ const session = sessions.get(message.id);
15005
+ if (session === undefined || session.exited)
15006
+ continue;
15007
+ session.cols = message.cols;
15008
+ session.rows = message.rows;
15009
+ session.handle.resize(message.cols, message.rows);
15010
+ session.mirror.resize(message.cols, message.rows);
15011
+ continue;
15012
+ }
15013
+ if (message.t === "kill") {
15014
+ sessions.get(message.id)?.handle.kill();
15015
+ }
15016
+ }
15017
+ });
15018
+ });
15019
+ return new Promise((resolve, reject) => {
15020
+ server.once("error", reject);
15021
+ server.listen(options.endpoint, () => {
15022
+ server.removeListener("error", reject);
15023
+ log(`pty host: listening on ${options.endpoint}`);
15024
+ resolve({
15025
+ endpoint: options.endpoint,
15026
+ get sessionCount() {
15027
+ return sessions.size;
15028
+ },
15029
+ close: async () => {
15030
+ for (const client of clients)
15031
+ client.destroy();
15032
+ clients.clear();
15033
+ for (const session of sessions.values())
15034
+ session.handle.kill();
15035
+ await new Promise((done) => server.close(() => done()));
15036
+ }
15037
+ });
15038
+ });
15039
+ });
15040
+ }
15041
+ var REPLAY_MAX_CHARS, REPLAY_SCROLLBACK_LINES = 5000;
15042
+ var init_pty_host = __esm(() => {
15043
+ init_src();
15044
+ init_paths();
15045
+ init_pty();
15046
+ init_terminal_mirror();
15047
+ REPLAY_MAX_CHARS = 2 * 1024 * 1024;
15048
+ });
15049
+
15050
+ // packages/daemon/src/pty-host-main.ts
15051
+ var exports_pty_host_main = {};
15052
+ __export(exports_pty_host_main, {
15053
+ runPtyHostProcess: () => runPtyHostProcess,
15054
+ PTY_HOST_IDLE_MS: () => PTY_HOST_IDLE_MS
15055
+ });
15056
+ async function runPtyHostProcess() {
15057
+ const endpoint = process.env["AXTERM_PTY_HOST_ENDPOINT"];
15058
+ const token = process.env["AXTERM_PTY_HOST_TOKEN"];
15059
+ const file = process.env["AXTERM_PTY_HOST_FILE"];
15060
+ if (!endpoint || !token || !file) {
15061
+ console.error("axterm __pty-host: AXTERM_PTY_HOST_ENDPOINT, _TOKEN and _FILE are required");
15062
+ process.exitCode = 2;
15063
+ return;
15064
+ }
15065
+ let idleTimer;
15066
+ const server = await startPtyHost({
15067
+ endpoint,
15068
+ token,
15069
+ onIdle: () => armIdleExit()
15070
+ });
15071
+ function armIdleExit() {
15072
+ clearTimeout(idleTimer);
15073
+ idleTimer = setTimeout(() => {
15074
+ if (server.sessionCount > 0)
15075
+ return armIdleExit();
15076
+ log("pty host: idle with no sessions, exiting");
15077
+ server.close().then(() => process.exit(0));
15078
+ }, PTY_HOST_IDLE_MS);
15079
+ idleTimer.unref?.();
15080
+ }
15081
+ writePtyHostFile(file, { pid: process.pid, endpoint, token, version: PTY_PROTOCOL_VERSION });
15082
+ log(`pty host: ready on ${endpoint} (pid ${process.pid})`);
15083
+ armIdleExit();
15084
+ for (const signal of ["SIGINT", "SIGTERM"]) {
15085
+ process.on(signal, () => {
15086
+ server.close().then(() => process.exit(0));
15087
+ });
15088
+ }
15089
+ }
15090
+ var PTY_HOST_IDLE_MS = 60000;
15091
+ var init_pty_host_main = __esm(() => {
15092
+ init_src();
15093
+ init_paths();
15094
+ init_pty_host_launch();
15095
+ init_pty_host();
15096
+ });
15097
+
13939
15098
  // packages/cli/src/index.ts
13940
15099
  init_src();
13941
- import fs22 from "fs";
13942
- import path21 from "path";
15100
+ import fs25 from "fs";
15101
+ import path23 from "path";
13943
15102
 
13944
15103
  // packages/cli/src/attach.ts
13945
15104
  import fs from "fs";
@@ -14541,6 +15700,7 @@ async function ensureDaemon() {
14541
15700
 
14542
15701
  // packages/cli/src/hook-bridge.ts
14543
15702
  init_src();
15703
+ import fs3 from "fs";
14544
15704
  var POST_TIMEOUT_MS = 1500;
14545
15705
  function isCodexApproval(agent, body) {
14546
15706
  if (agent !== "codex")
@@ -14565,23 +15725,43 @@ async function relayHookEvent(agent) {
14565
15725
  if (body === "")
14566
15726
  return;
14567
15727
  const approval = isCodexApproval(agent, body);
14568
- const res = await fetch(target.url, {
14569
- method: "POST",
14570
- headers: target.headers,
14571
- body,
14572
- signal: AbortSignal.timeout(approval ? CODEX_APPROVAL_POST_TIMEOUT_MS : POST_TIMEOUT_MS)
14573
- });
14574
- if (approval && res.ok) {
15728
+ const timeout = approval ? CODEX_APPROVAL_POST_TIMEOUT_MS : POST_TIMEOUT_MS;
15729
+ const post = (to) => fetch(to.url, { method: "POST", headers: to.headers, body, signal: AbortSignal.timeout(timeout) });
15730
+ let res;
15731
+ try {
15732
+ res = await post(target);
15733
+ } catch {}
15734
+ if (res === undefined || res.status === 401 || res.status === 404) {
15735
+ if (approval && res === undefined)
15736
+ return;
15737
+ const fresh = currentEndpoint();
15738
+ const retarget = fresh === undefined ? undefined : hookPostTarget(process.env, agent, fresh);
15739
+ if (retarget !== undefined && retarget.url !== target.url)
15740
+ res = await post(retarget);
15741
+ else if (res === undefined)
15742
+ return;
15743
+ }
15744
+ if (approval && res !== undefined && res.ok) {
14575
15745
  const decision = await res.text();
14576
15746
  if (decision !== "")
14577
15747
  process.stdout.write(decision);
14578
15748
  }
14579
15749
  } catch {}
14580
15750
  }
15751
+ function currentEndpoint() {
15752
+ const file = process.env["AXTERM_HOOK_ENDPOINT"];
15753
+ if (file === undefined)
15754
+ return;
15755
+ try {
15756
+ return parseHookEndpoint(fs3.readFileSync(file, "utf8"));
15757
+ } catch {
15758
+ return;
15759
+ }
15760
+ }
14581
15761
 
14582
15762
  // packages/cli/src/mcp-server.ts
14583
15763
  init_src();
14584
- import fs3 from "fs";
15764
+ import fs4 from "fs";
14585
15765
  import path4 from "path";
14586
15766
  var ASK_TIMEOUT_MS = 10 * 60000;
14587
15767
  var POST_TIMEOUT_MS2 = 5000;
@@ -14698,7 +15878,7 @@ class McpServer {
14698
15878
  const contextFile = this.io.env[MCP_CONTEXT_ENV];
14699
15879
  if (contextFile !== undefined) {
14700
15880
  try {
14701
- const context = JSON.parse(fs3.readFileSync(contextFile, "utf8"));
15881
+ const context = JSON.parse(fs4.readFileSync(contextFile, "utf8"));
14702
15882
  if (context.port && context.token && context.sessionId) {
14703
15883
  return {
14704
15884
  url: `http://127.0.0.1:${context.port}`,
@@ -14855,13 +16035,13 @@ function runMcpServer() {
14855
16035
  init_src();
14856
16036
  import { spawnSync } from "child_process";
14857
16037
  import { createHash as createHash2 } from "crypto";
14858
- import fs4 from "fs";
16038
+ import fs5 from "fs";
14859
16039
  import path5 from "path";
14860
16040
  import { stdin as stdin2, stdout as stdout2 } from "process";
14861
16041
  // packages/cli/package.json
14862
16042
  var package_default = {
14863
16043
  name: "@layorixinc/axterm",
14864
- version: "0.4.0",
16044
+ version: "0.6.0",
14865
16045
  type: "module",
14866
16046
  bin: {
14867
16047
  axterm: "dist/cli.js"
@@ -14935,7 +16115,7 @@ function assetName(platform, arch, isMusl) {
14935
16115
  function usesMusl() {
14936
16116
  if (process.platform !== "linux")
14937
16117
  return false;
14938
- if (fs4.existsSync("/etc/alpine-release"))
16118
+ if (fs5.existsSync("/etc/alpine-release"))
14939
16119
  return true;
14940
16120
  const ldd = spawnSync("ldd", ["--version"], { encoding: "utf8" });
14941
16121
  return `${ldd.stdout ?? ""}${ldd.stderr ?? ""}`.toLowerCase().includes("musl");
@@ -14949,7 +16129,7 @@ function firstLine(command, args) {
14949
16129
  function detectOrigin(execPath) {
14950
16130
  const resolved = (() => {
14951
16131
  try {
14952
- return fs4.realpathSync(execPath);
16132
+ return fs5.realpathSync(execPath);
14953
16133
  } catch {
14954
16134
  return execPath;
14955
16135
  }
@@ -14991,7 +16171,7 @@ function checksumFor(sums, asset) {
14991
16171
  return;
14992
16172
  }
14993
16173
  function sha256(file) {
14994
- return createHash2("sha256").update(fs4.readFileSync(file)).digest("hex");
16174
+ return createHash2("sha256").update(fs5.readFileSync(file)).digest("hex");
14995
16175
  }
14996
16176
  function ask2(question) {
14997
16177
  if (stdin2.isTTY !== true)
@@ -15038,7 +16218,7 @@ async function runUpdate(client, current, options) {
15038
16218
  }
15039
16219
  const asset = assetName(process.platform, process.arch, usesMusl());
15040
16220
  const base = `${RELEASES}/v${latest.version}`;
15041
- const staging = fs4.mkdtempSync(`${process.execPath}.update-`);
16221
+ const staging = fs5.mkdtempSync(`${process.execPath}.update-`);
15042
16222
  const downloaded = path5.join(staging, asset);
15043
16223
  try {
15044
16224
  console.log(t("cli.update.downloading", { asset }));
@@ -15052,7 +16232,7 @@ async function runUpdate(client, current, options) {
15052
16232
  return;
15053
16233
  }
15054
16234
  if (process.platform !== "win32")
15055
- fs4.chmodSync(downloaded, 493);
16235
+ fs5.chmodSync(downloaded, 493);
15056
16236
  const reported = spawnSync(downloaded, ["--version"], { encoding: "utf8" }).stdout?.trim();
15057
16237
  if (reported !== latest.version) {
15058
16238
  console.error(t("cli.update.wrongVersion", { got: reported ?? "nothing", want: latest.version }));
@@ -15092,7 +16272,7 @@ async function runUpdate(client, current, options) {
15092
16272
  console.log(t("cli.update.restoreFailed", { id, reason: (result.stderr ?? "").trim() || "unknown" }));
15093
16273
  }
15094
16274
  } finally {
15095
- fs4.rmSync(staging, { recursive: true, force: true });
16275
+ fs5.rmSync(staging, { recursive: true, force: true });
15096
16276
  }
15097
16277
  }
15098
16278
  function applySwap(target, downloaded) {
@@ -15101,16 +16281,16 @@ function applySwap(target, downloaded) {
15101
16281
  try {
15102
16282
  for (const step of steps) {
15103
16283
  if (step.op === "chmod")
15104
- fs4.chmodSync(step.from, 493);
16284
+ fs5.chmodSync(step.from, 493);
15105
16285
  else if (step.op === "backup") {
15106
- fs4.renameSync(step.from, step.to);
16286
+ fs5.renameSync(step.from, step.to);
15107
16287
  backup = step.to;
15108
16288
  } else
15109
- fs4.renameSync(step.from, step.to);
16289
+ fs5.renameSync(step.from, step.to);
15110
16290
  }
15111
16291
  } catch (err) {
15112
- if (backup !== undefined && !fs4.existsSync(target))
15113
- fs4.renameSync(backup, target);
16292
+ if (backup !== undefined && !fs5.existsSync(target))
16293
+ fs5.renameSync(backup, target);
15114
16294
  throw err;
15115
16295
  }
15116
16296
  }
@@ -15183,7 +16363,7 @@ function printSessionTable(sessions) {
15183
16363
  console.log(row.map((cell, col) => cell.padEnd(widths[col])).join(" "));
15184
16364
  }
15185
16365
  }
15186
- var realPath = (target) => fs22.realpathSync.native(target);
16366
+ var realPath = (target) => fs25.realpathSync.native(target);
15187
16367
  function scopedHere(sessions) {
15188
16368
  return sessionsInScope(sessions, process.cwd(), realPath);
15189
16369
  }
@@ -15196,7 +16376,7 @@ var help = () => t("cli.help");
15196
16376
  function readDotEnv(file = ".env") {
15197
16377
  const out = {};
15198
16378
  try {
15199
- for (const line of fs22.readFileSync(file, "utf8").split(/\r?\n/)) {
16379
+ for (const line of fs25.readFileSync(file, "utf8").split(/\r?\n/)) {
15200
16380
  const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
15201
16381
  if (m)
15202
16382
  out[m[1]] = m[2].trim();
@@ -15207,7 +16387,7 @@ function readDotEnv(file = ".env") {
15207
16387
  function configuredLocale() {
15208
16388
  try {
15209
16389
  const file = resolvePaths(currentPathEnv()).configFile;
15210
- const raw = JSON.parse(fs22.readFileSync(file, "utf8"));
16390
+ const raw = JSON.parse(fs25.readFileSync(file, "utf8"));
15211
16391
  return typeof raw.locale === "string" ? raw.locale : undefined;
15212
16392
  } catch {
15213
16393
  return;
@@ -15228,6 +16408,11 @@ async function main2() {
15228
16408
  runMcpServer();
15229
16409
  return;
15230
16410
  }
16411
+ if (isPtyHostInvocation(argv)) {
16412
+ const { runPtyHostProcess: runPtyHostProcess2 } = await Promise.resolve().then(() => (init_pty_host_main(), exports_pty_host_main));
16413
+ await runPtyHostProcess2();
16414
+ return;
16415
+ }
15231
16416
  const [command, ...rest] = argv;
15232
16417
  setLocale(resolveLocale({ configured: configuredLocale(), env: process.env }));
15233
16418
  const { flags, positional, passthrough } = parseArgs(rest);
@@ -15258,6 +16443,7 @@ async function main2() {
15258
16443
  const commandOverride = typeof flags.get("cmd") === "string" ? flags.get("cmd").split(/\s+/).filter(Boolean) : undefined;
15259
16444
  const info = await client.call("session.create", {
15260
16445
  cwd: flags.get("cwd") ?? process.cwd(),
16446
+ preferConversationCwd: flags.get("cwd") === undefined,
15261
16447
  name: flags.get("name"),
15262
16448
  shell: flags.get("shell"),
15263
16449
  agent: flags.get("agent"),
@@ -15351,9 +16537,17 @@ async function main2() {
15351
16537
  const [id, indexArg] = positional;
15352
16538
  if (!id || indexArg === undefined)
15353
16539
  throw new Error(t("cli.usage.choose"));
15354
- const keys2 = buildMenuSelectKeys(Number(indexArg));
15355
- await client.call("terminal.write", { id, dataB64: Buffer.from(keys2, "utf8").toString("base64") });
16540
+ const index = Number(indexArg);
16541
+ const answered = await client.call("session.choose", { id, index }).catch(async (err) => {
16542
+ if (!/unknown method/i.test(String(err)))
16543
+ throw err;
16544
+ const keys2 = buildMenuSelectKeys(index);
16545
+ await client.call("terminal.write", { id, dataB64: Buffer.from(keys2, "utf8").toString("base64") });
16546
+ return { answered: "menu" };
16547
+ });
15356
16548
  console.log(t("cli.session.selected", { index: indexArg }));
16549
+ if (answered.answered === "onboarding")
16550
+ console.log(t("cli.session.selectedDialog"));
15357
16551
  break;
15358
16552
  }
15359
16553
  case "setup": {
@@ -15429,8 +16623,8 @@ async function main2() {
15429
16623
  case "project": {
15430
16624
  const sub = positional[0] ?? "list";
15431
16625
  if (sub === "add") {
15432
- const dir = positional[1] ? path21.resolve(positional[1]) : process.cwd();
15433
- const alias = positional[2] ?? path21.basename(dir);
16626
+ const dir = positional[1] ? path23.resolve(positional[1]) : process.cwd();
16627
+ const alias = positional[2] ?? path23.basename(dir);
15434
16628
  const projects = await client.call("project.add", {
15435
16629
  alias,
15436
16630
  path: dir