@layorixinc/axterm 0.4.0 → 0.5.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 +1071 -274
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -31,6 +31,19 @@ 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
 
36
49
  // packages/core/src/mcp-config.ts
@@ -69,6 +82,12 @@ function uuidFromSessionId(sessionId) {
69
82
  throw new Error(`session id is not 32 hex characters: ${sessionId}`);
70
83
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
71
84
  }
85
+ function sessionIdFromUuid(ref) {
86
+ if (!ref.includes("-"))
87
+ return;
88
+ const stripped = ref.replace(/-/g, "");
89
+ return /^[0-9a-f]{4,32}$/i.test(stripped) ? stripped.toLowerCase() : undefined;
90
+ }
72
91
  function withAppServer(argv, url) {
73
92
  const out = [];
74
93
  for (let i = 0;i < argv.length; i++) {
@@ -82,6 +101,9 @@ function withAppServer(argv, url) {
82
101
  return out;
83
102
  return [out[0], "--remote", url, ...out.slice(1)];
84
103
  }
104
+ function resumesAConversation(args) {
105
+ return args.some((arg) => CLAUDE_RESUME_FLAGS.has(arg) || arg.startsWith("--resume=") || arg.startsWith("--continue="));
106
+ }
85
107
  function buildAgentCommand(input) {
86
108
  const extra = [...input.extraArgs ?? []];
87
109
  if (input.agent === "claude") {
@@ -92,6 +114,7 @@ function buildAgentCommand(input) {
92
114
  ...input.mcpConfigPath === undefined ? [] : ["--mcp-config", input.mcpConfigPath],
93
115
  "--session-id",
94
116
  uuidFromSessionId(input.sessionId),
117
+ ...resumesAConversation(extra) ? ["--fork-session"] : [],
95
118
  ...extra
96
119
  ];
97
120
  }
@@ -136,21 +159,40 @@ function resolveAgentArgs(input) {
136
159
  });
137
160
  return groups.filter((group, index) => group.flag === undefined || lastIndex.get(group.flag) === index).flatMap((group) => group.tokens);
138
161
  }
162
+ function operatorArgsFrom(storedArgv) {
163
+ if (storedArgv === undefined || storedArgv.length === 0)
164
+ return [];
165
+ const kept = [];
166
+ for (let i = 1;i < storedArgv.length; i++) {
167
+ const token = storedArgv[i];
168
+ const width = AXTERM_OWNED_FLAGS[token];
169
+ if (width !== undefined) {
170
+ i += width - 1;
171
+ continue;
172
+ }
173
+ if (/^--(resume|continue)=/.test(token))
174
+ continue;
175
+ kept.push(token);
176
+ }
177
+ return kept;
178
+ }
139
179
  function buildAgentResumeCommand(input) {
180
+ const operator = input.extraArgs ?? operatorArgsFrom(input.storedArgv);
140
181
  if (input.agent === "claude") {
141
182
  return [
142
183
  "claude",
143
184
  "--settings",
144
185
  input.claudeSettingsPath,
186
+ ...input.mcpConfigPath === undefined ? [] : ["--mcp-config", input.mcpConfigPath],
187
+ ...operator,
145
188
  "--resume",
146
- uuidFromSessionId(input.sessionId),
147
- ...input.extraArgs ?? []
189
+ uuidFromSessionId(input.sessionId)
148
190
  ];
149
191
  }
150
192
  if (input.agent === "gemini") {
151
- return ["gemini", "--resume", input.sessionId, ...input.extraArgs ?? []];
193
+ return ["gemini", ...operator, "--resume", input.sessionId];
152
194
  }
153
- return buildAgentCommand(input);
195
+ return buildAgentCommand({ ...input, extraArgs: operator });
154
196
  }
155
197
  function isAgentId(value) {
156
198
  return AGENT_IDS.includes(value);
@@ -158,8 +200,20 @@ function isAgentId(value) {
158
200
  function supportedAgents() {
159
201
  return AGENT_IDS;
160
202
  }
161
- var AGENT_IDS;
203
+ var CLAUDE_RESUME_FLAGS, AXTERM_OWNED_FLAGS, AGENT_IDS;
162
204
  var init_agent_launch = __esm(() => {
205
+ CLAUDE_RESUME_FLAGS = new Set(["--resume", "-r", "--continue", "-c"]);
206
+ AXTERM_OWNED_FLAGS = {
207
+ "--settings": 2,
208
+ "--mcp-config": 2,
209
+ "--session-id": 2,
210
+ "--extension": 2,
211
+ "--fork-session": 1,
212
+ "--resume": 2,
213
+ "-r": 2,
214
+ "--continue": 1,
215
+ "-c": 1
216
+ };
163
217
  AGENT_IDS = ["omp", "claude", "codex", "gemini"];
164
218
  });
165
219
 
@@ -406,6 +460,9 @@ function isRecord(value) {
406
460
  }
407
461
 
408
462
  // packages/core/src/config-schema.ts
463
+ function isStagingMode(value) {
464
+ return typeof value === "string" && STAGING_MODES.includes(value);
465
+ }
409
466
  function resolveStreamConfig(file) {
410
467
  return {
411
468
  enabled: file?.enabled ?? DEFAULT_STREAM_CONFIG.enabled,
@@ -499,7 +556,7 @@ function parseConfig(raw) {
499
556
  }
500
557
  const defaults = raw["defaults"];
501
558
  if (isRecord(defaults)) {
502
- checkKeys(defaults, ["agent", "shell", "autoApprove", "agentArgs"], "defaults.", errors);
559
+ checkKeys(defaults, ["agent", "shell", "autoApprove", "agentArgs", "staging"], "defaults.", errors);
503
560
  const agent = defaults["agent"];
504
561
  const shell = defaults["shell"];
505
562
  const rawArgs = defaults["agentArgs"];
@@ -518,11 +575,16 @@ function parseConfig(raw) {
518
575
  if (autoApprove !== undefined && typeof autoApprove !== "boolean") {
519
576
  errors.push("defaults.autoApprove must be true or false");
520
577
  }
578
+ const staging = defaults["staging"];
579
+ if (staging !== undefined && !isStagingMode(staging)) {
580
+ errors.push(`defaults.staging must be one of: ${STAGING_MODES.join(", ")}`);
581
+ }
521
582
  config.defaults = {
522
583
  ...typeof agent === "string" ? { agent } : {},
523
584
  ...typeof shell === "string" ? { shell } : {},
524
585
  ...typeof autoApprove === "boolean" ? { autoApprove } : {},
525
- ...Object.keys(agentArgs).length > 0 ? { agentArgs } : {}
586
+ ...Object.keys(agentArgs).length > 0 ? { agentArgs } : {},
587
+ ...isStagingMode(staging) ? { staging } : {}
526
588
  };
527
589
  }
528
590
  const projects = raw["projects"];
@@ -631,6 +693,9 @@ function setConfigValue(config, dottedPath, rawValue) {
631
693
  const shown = Array.isArray(rawValue) ? rawValue.join(" ") : rawValue;
632
694
  return { error: `"${shown}" is not a valid ${type} for ${dottedPath}` };
633
695
  }
696
+ if (dottedPath === "defaults.staging" && !isStagingMode(value)) {
697
+ return { error: `"${String(value)}" is not a valid location \u2014 use one of: ${STAGING_MODES.join(", ")}` };
698
+ }
634
699
  const next = structuredClone(config);
635
700
  const segments = dottedPath.split(".");
636
701
  let cursor = next;
@@ -674,10 +739,11 @@ function resolveEnvRefs(config, env) {
674
739
  }
675
740
  return resolved;
676
741
  }
677
- var CONFIG_VERSION = 1, DEFAULT_CONFIG, DEFAULT_STREAM_CONFIG, DEFAULT_INBOUND_CONFIG, FIELDS, SECRET_SUFFIXES, ENV_REF;
742
+ var STAGING_MODES, CONFIG_VERSION = 1, DEFAULT_CONFIG, DEFAULT_STREAM_CONFIG, DEFAULT_INBOUND_CONFIG, FIELDS, SECRET_SUFFIXES, ENV_REF;
678
743
  var init_config_schema = __esm(() => {
679
744
  init_agent_launch();
680
745
  init_channel_spi();
746
+ STAGING_MODES = ["project", "runtime"];
681
747
  DEFAULT_CONFIG = {
682
748
  version: CONFIG_VERSION,
683
749
  projects: [],
@@ -703,6 +769,7 @@ var init_config_schema = __esm(() => {
703
769
  "defaults.agent": "string",
704
770
  "defaults.shell": "string",
705
771
  "defaults.autoApprove": "boolean",
772
+ "defaults.staging": "string",
706
773
  "channels.telegram.enabled": "boolean",
707
774
  "channels.telegram.groupId": "number",
708
775
  "channels.telegram.allowUserIds": "number[]",
@@ -768,6 +835,7 @@ function resolvePaths(input) {
768
835
  extDir: join(stateDir, "ext"),
769
836
  runtimeFile: join(runtimeDir, "runtime.json"),
770
837
  hookEndpointFile: join(runtimeDir, "hook-endpoint.env"),
838
+ ptyHostFile: join(runtimeDir, "pty-host.json"),
771
839
  legacyRoots: legacyRoots.filter((dir) => dir !== stateDir)
772
840
  };
773
841
  }
@@ -1044,6 +1112,8 @@ usage: /fanout <project> <count|branches> <prompt>`,
1044
1112
  "doctor.bridge.missing": "hook bridge: {command} does not exist \u2014 codex and gemini hooks would fail once per tool call",
1045
1113
  "doctor.agent.installed": "{agent}: installed ({path})",
1046
1114
  "doctor.agent.notInstalled": "{agent}: not installed",
1115
+ "doctor.staging.project": "inbound files: written to <session dir>/.axterm/inbound (hidden from git; set defaults.staging=runtime for a synced folder)",
1116
+ "doctor.staging.runtime": "inbound files: written to axterm's own runtime directory (defaults.staging=runtime)",
1047
1117
  "doctor.omp.extensionOk": "omp extension: deployed ({path})",
1048
1118
  "doctor.omp.extensionMissing": "omp extension: missing at {path} \u2014 no omp session can report status",
1049
1119
  "doctor.gemini.settingsOk": "gemini hooks: defaults file valid ({events} events subscribed)",
@@ -1082,6 +1152,9 @@ usage: /fanout <project> <count|branches> <prompt>`,
1082
1152
  "err.session.dialogOpen": "session {id} is waiting on a dialog ({state}); answer it with the buttons or /esc first",
1083
1153
  "err.agent.notInstalled": "the agent command '{command}' was not found on PATH \u2014 install it, or pass --cmd to run something else",
1084
1154
  "err.agent.unknown": "unknown agent '{agent}' \u2014 supported: {supported}",
1155
+ "err.staging.dir": "could not create the session's staging directory {dir} ({error}) \u2014 fix the permissions, or set defaults.staging to runtime",
1156
+ "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",
1157
+ "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
1158
  "codex.approval.denied": "Denied from the axterm channel",
1086
1159
  "err.codex.noAuth": "codex is not logged in ({authFile} not found) \u2014 run `codex login` once, then start the session again",
1087
1160
  "err.codex.authLink": "could not link codex credentials into axterm's CODEX_HOME ({reason}) \u2014 refusing to start a logged-out agent",
@@ -1340,6 +1413,8 @@ usage: /fanout <project> <count|branches> <prompt>`,
1340
1413
  "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
1414
  "doctor.agent.installed": "{agent}: \uC124\uCE58\uB428 ({path})",
1342
1415
  "doctor.agent.notInstalled": "{agent}: \uBBF8\uC124\uCE58",
1416
+ "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)",
1417
+ "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
1418
  "doctor.omp.extensionOk": "omp \uD655\uC7A5: \uBC30\uD3EC\uB428 ({path})",
1344
1419
  "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
1420
  "doctor.gemini.settingsOk": "gemini \uD6C5: defaults \uD30C\uC77C \uC815\uC0C1 ({events}\uAC1C \uC774\uBCA4\uD2B8 \uAD6C\uB3C5)",
@@ -1378,6 +1453,9 @@ usage: /fanout <project> <count|branches> <prompt>`,
1378
1453
  "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
1454
  "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
1455
  "err.agent.unknown": "\uC54C \uC218 \uC5C6\uB294 \uC5D0\uC774\uC804\uD2B8 '{agent}' \uC785\uB2C8\uB2E4. \uC9C0\uC6D0: {supported}",
1456
+ "err.staging.dir": "\uC138\uC158\uC758 \uC2A4\uD14C\uC774\uC9D5 \uB514\uB809\uD130\uB9AC {dir} \uC744(\uB97C) \uB9CC\uB4E4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4 ({error}). \uAD8C\uD55C\uC744 \uACE0\uCE58\uAC70\uB098 defaults.staging \uC744 runtime \uC73C\uB85C \uC124\uC815\uD558\uC138\uC694",
1457
+ "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",
1458
+ "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
1459
  "codex.approval.denied": "axterm \uCC44\uB110\uC5D0\uC11C \uAC70\uBD80\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
1382
1460
  "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
1461
  "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",
@@ -1763,6 +1841,13 @@ function mcpLaunch(input) {
1763
1841
  function isMcpInvocation(args) {
1764
1842
  return args[0] === MCP_ARGV_FLAG;
1765
1843
  }
1844
+ function ptyHostLaunch(input) {
1845
+ const base = daemonLaunch(input);
1846
+ return { command: base.command, args: [...base.args.slice(0, -1), PTY_HOST_ARGV_FLAG] };
1847
+ }
1848
+ function isPtyHostInvocation(args) {
1849
+ return args[0] === PTY_HOST_ARGV_FLAG;
1850
+ }
1766
1851
  function hookPostTarget(env, agent) {
1767
1852
  const port = Number(env["AXTERM_HOOK_PORT"]);
1768
1853
  const token = env["AXTERM_HOOK_TOKEN"];
@@ -1785,7 +1870,7 @@ function hookPostTarget(env, agent) {
1785
1870
  function isDaemonInvocation(args) {
1786
1871
  return args[0] === DAEMON_ARGV_FLAG;
1787
1872
  }
1788
- var DAEMON_ARGV_FLAG = "__daemon", HOOK_ARGV_FLAG = "__hook", MCP_ARGV_FLAG = "__mcp", RUNTIME_NAMES;
1873
+ var DAEMON_ARGV_FLAG = "__daemon", HOOK_ARGV_FLAG = "__hook", MCP_ARGV_FLAG = "__mcp", PTY_HOST_ARGV_FLAG = "__pty-host", RUNTIME_NAMES;
1789
1874
  var init_self_spawn = __esm(() => {
1790
1875
  RUNTIME_NAMES = new Set(["bun", "bun.exe", "bun-debug", "bun-debug.exe", "node", "node.exe"]);
1791
1876
  });
@@ -2218,7 +2303,91 @@ var init_agent_status = __esm(() => {
2218
2303
  ]);
2219
2304
  });
2220
2305
 
2306
+ // packages/core/src/pty-protocol.ts
2307
+ function encodeFrame(kind, id, payload) {
2308
+ const idBytes = new TextEncoder().encode(id);
2309
+ if (idBytes.length > 255)
2310
+ throw new Error(`session id too long for a frame header: ${idBytes.length} bytes`);
2311
+ const frame = new Uint8Array(HEADER_BYTES + idBytes.length + payload.byteLength);
2312
+ frame[0] = kind;
2313
+ frame[1] = idBytes.length;
2314
+ new DataView(frame.buffer, frame.byteOffset, frame.byteLength).setUint32(2, payload.byteLength, true);
2315
+ frame.set(idBytes, HEADER_BYTES);
2316
+ frame.set(payload, HEADER_BYTES + idBytes.length);
2317
+ return frame;
2318
+ }
2319
+ function isFrameKind(value) {
2320
+ return value === FrameKind.Control || value === FrameKind.Data;
2321
+ }
2322
+
2323
+ class FrameDecoder {
2324
+ buffer = new Uint8Array(0);
2325
+ push(chunk) {
2326
+ this.buffer = concat(this.buffer, chunk);
2327
+ const frames = [];
2328
+ for (;; ) {
2329
+ if (this.buffer.byteLength < HEADER_BYTES)
2330
+ return frames;
2331
+ const kind = this.buffer[0];
2332
+ if (!isFrameKind(kind))
2333
+ throw new Error(`unknown frame kind ${kind}`);
2334
+ const idLength = this.buffer[1];
2335
+ const payloadLength = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength).getUint32(2, true);
2336
+ if (payloadLength > MAX_FRAME_BYTES) {
2337
+ throw new Error(`frame too large: ${payloadLength} bytes declared, ceiling is ${MAX_FRAME_BYTES}`);
2338
+ }
2339
+ const total = HEADER_BYTES + idLength + payloadLength;
2340
+ if (this.buffer.byteLength < total)
2341
+ return frames;
2342
+ const id = new TextDecoder().decode(this.buffer.subarray(HEADER_BYTES, HEADER_BYTES + idLength));
2343
+ const payload = this.buffer.slice(HEADER_BYTES + idLength, total);
2344
+ frames.push({ kind, id, payload });
2345
+ this.buffer = this.buffer.slice(total);
2346
+ }
2347
+ }
2348
+ }
2349
+ function concat(left, right) {
2350
+ if (left.byteLength === 0)
2351
+ return right.slice();
2352
+ const joined = new Uint8Array(left.byteLength + right.byteLength);
2353
+ joined.set(left, 0);
2354
+ joined.set(right, left.byteLength);
2355
+ return joined;
2356
+ }
2357
+ function parseControl(payload) {
2358
+ let parsed;
2359
+ try {
2360
+ parsed = JSON.parse(new TextDecoder().decode(payload));
2361
+ } catch {
2362
+ return;
2363
+ }
2364
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
2365
+ return;
2366
+ const type = parsed.t;
2367
+ if (typeof type !== "string" || !KNOWN_TYPES.has(type))
2368
+ return;
2369
+ return parsed;
2370
+ }
2371
+ function controlFrame(message) {
2372
+ return encodeFrame(FrameKind.Control, "", new TextEncoder().encode(JSON.stringify(message)));
2373
+ }
2374
+ var PTY_PROTOCOL_VERSION = 1, FrameKind, MAX_FRAME_BYTES, HEADER_BYTES = 6, KNOWN_TYPES;
2375
+ var init_pty_protocol = __esm(() => {
2376
+ FrameKind = {
2377
+ Control: 1,
2378
+ Data: 2
2379
+ };
2380
+ MAX_FRAME_BYTES = 8 * 1024 * 1024;
2381
+ KNOWN_TYPES = new Set(["hello", "spawn", "resize", "kill", "welcome", "spawned", "exit", "error"]);
2382
+ });
2383
+
2221
2384
  // packages/core/src/workspace.ts
2385
+ function scratchDir(sessionDir) {
2386
+ return `${sessionDir.replace(/[/\\]+$/, "")}/${SCRATCH_DIR_NAME}`;
2387
+ }
2388
+ function inboundStagingDir(sessionDir) {
2389
+ return `${scratchDir(sessionDir)}/inbound`;
2390
+ }
2222
2391
  function planMaterialisedPath(stagingDir, sessionId, remotePath) {
2223
2392
  const parts = remotePath.split(/[/\\]/);
2224
2393
  let base = "";
@@ -2239,7 +2408,8 @@ function planMaterialisedPath(stagingDir, sessionId, remotePath) {
2239
2408
  }
2240
2409
  return `${stagingDir}/${sessionId}/${name}`;
2241
2410
  }
2242
- var MAX_NAME = 120;
2411
+ var SCRATCH_DIR_NAME = ".axterm", SCRATCH_IGNORE_NAME = ".gitignore", SCRATCH_IGNORE_BODY = `*
2412
+ `, MAX_NAME = 120;
2243
2413
 
2244
2414
  // packages/core/src/claude-hooks.ts
2245
2415
  function resultText(response) {
@@ -2430,6 +2600,15 @@ function evaluateVersionDrift(input) {
2430
2600
  }
2431
2601
  ];
2432
2602
  }
2603
+ function evaluateStagingDoctor(facts) {
2604
+ return [
2605
+ {
2606
+ id: "staging.mode",
2607
+ status: "info",
2608
+ key: facts.mode === "runtime" ? "doctor.staging.runtime" : "doctor.staging.project"
2609
+ }
2610
+ ];
2611
+ }
2433
2612
  function binaryCheck(agent, binary) {
2434
2613
  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
2614
  }
@@ -3463,6 +3642,7 @@ var init_src = __esm(() => {
3463
3642
  init_choice();
3464
3643
  init_agent_status();
3465
3644
  init_activity();
3645
+ init_pty_protocol();
3466
3646
  init_claude_hooks();
3467
3647
  init_onboarding();
3468
3648
  init_doctor();
@@ -3961,7 +4141,11 @@ class Store {
3961
4141
  return existed;
3962
4142
  }
3963
4143
  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}%`);
4144
+ const alternate = sessionIdFromUuid(idOrPrefix) ?? idOrPrefix;
4145
+ const rows = this.db.prepare(`SELECT * FROM sessions
4146
+ WHERE id = ? OR short_id = ? OR id LIKE ?
4147
+ OR id = ? OR short_id = ? OR id LIKE ?
4148
+ ORDER BY created_at DESC`).all(idOrPrefix, idOrPrefix, `${idOrPrefix}%`, alternate, alternate, `${alternate}%`);
3965
4149
  const row = rows[0];
3966
4150
  return row ? rowToInfo(row) : undefined;
3967
4151
  }
@@ -4023,6 +4207,7 @@ class Store {
4023
4207
  }
4024
4208
  }
4025
4209
  var init_store = __esm(() => {
4210
+ init_src();
4026
4211
  init_paths();
4027
4212
  });
4028
4213
 
@@ -5036,6 +5221,12 @@ class LocalWorkspace {
5036
5221
  materialise(file) {
5037
5222
  return Promise.resolve(file);
5038
5223
  }
5224
+ async mkdirp(dir, mode) {
5225
+ await fs11.mkdir(dir, mode === undefined ? { recursive: true } : { recursive: true, mode });
5226
+ }
5227
+ async write(file, bytes, mode) {
5228
+ await fs11.writeFile(file, bytes, mode === undefined ? undefined : { mode });
5229
+ }
5039
5230
  exec(argv, cwd) {
5040
5231
  const [command, ...args] = argv;
5041
5232
  if (command === undefined)
@@ -5782,6 +5973,14 @@ async function untrackedFiles(ws, worktreePath) {
5782
5973
  return [];
5783
5974
  return result.stdout.split(/\r?\n/).filter((line) => line.trim().length > 0);
5784
5975
  }
5976
+ async function ignoreState(ws, cwd, file) {
5977
+ const run = await ws.exec(["git", "check-ignore", "-q", file], cwd);
5978
+ if (run.code === 0)
5979
+ return "ignored";
5980
+ if (run.code === 1)
5981
+ return "visible";
5982
+ return "unknown";
5983
+ }
5785
5984
  async function countLines(ws, filePath) {
5786
5985
  try {
5787
5986
  const stat = await ws.stat(filePath);
@@ -5835,6 +6034,51 @@ var init_git = __esm(() => {
5835
6034
  UNTRACKED_LINE_SCAN_LIMIT = 1024 * 1024;
5836
6035
  });
5837
6036
 
6037
+ // packages/daemon/src/claude-store.ts
6038
+ import fs16 from "fs";
6039
+ import os4 from "os";
6040
+ import path16 from "path";
6041
+ function claudeConfigDir(env = process.env) {
6042
+ const override = env["CLAUDE_CONFIG_DIR"];
6043
+ if (override !== undefined && override.length > 0)
6044
+ return override;
6045
+ return path16.join(os4.homedir(), ".claude");
6046
+ }
6047
+ function resumedConversationId(argv) {
6048
+ for (let i = 0;i < argv.length; i++) {
6049
+ const token = argv[i];
6050
+ if (token === "--resume" || token === "-r") {
6051
+ const value = argv[i + 1];
6052
+ return value !== undefined && CONVERSATION_ID.test(value) ? value : undefined;
6053
+ }
6054
+ const inline = /^--resume=(.+)$/.exec(token);
6055
+ if (inline)
6056
+ return CONVERSATION_ID.test(inline[1]) ? inline[1] : undefined;
6057
+ }
6058
+ return;
6059
+ }
6060
+ function claudeConversationExists(configDir, conversationId) {
6061
+ const projects = path16.join(configDir, "projects");
6062
+ let entries;
6063
+ try {
6064
+ entries = fs16.readdirSync(projects);
6065
+ } catch {
6066
+ return;
6067
+ }
6068
+ const file = `${conversationId}.jsonl`;
6069
+ for (const entry of entries) {
6070
+ try {
6071
+ if (fs16.existsSync(path16.join(projects, entry, file)))
6072
+ return true;
6073
+ } catch {}
6074
+ }
6075
+ return false;
6076
+ }
6077
+ var CONVERSATION_ID;
6078
+ var init_claude_store = __esm(() => {
6079
+ CONVERSATION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6080
+ });
6081
+
5838
6082
  // packages/daemon/src/injector.ts
5839
6083
  import { setTimeout as delay2 } from "timers/promises";
5840
6084
 
@@ -5880,9 +6124,377 @@ var init_injector = __esm(() => {
5880
6124
  init_paths();
5881
6125
  });
5882
6126
 
5883
- // packages/daemon/src/occupancy.ts
6127
+ // packages/daemon/src/pty-host-client.ts
6128
+ import net3 from "net";
6129
+ import { StringDecoder } from "string_decoder";
6130
+
6131
+ class RemotePty {
6132
+ id;
6133
+ pid;
6134
+ send;
6135
+ dataListeners = [];
6136
+ exitListeners = [];
6137
+ decoder = new StringDecoder("utf8");
6138
+ buffered = "";
6139
+ exitCode;
6140
+ exited = false;
6141
+ constructor(id, pid, send) {
6142
+ this.id = id;
6143
+ this.pid = pid;
6144
+ this.send = send;
6145
+ }
6146
+ emit(text) {
6147
+ if (text.length === 0)
6148
+ return;
6149
+ if (this.dataListeners.length === 0) {
6150
+ this.buffered += text;
6151
+ return;
6152
+ }
6153
+ for (const listener of this.dataListeners)
6154
+ listener(text);
6155
+ }
6156
+ push(payload) {
6157
+ this.emit(this.decoder.write(Buffer.from(payload)));
6158
+ }
6159
+ finish(code) {
6160
+ this.emit(this.decoder.end());
6161
+ this.exited = true;
6162
+ this.exitCode = code;
6163
+ for (const listener of this.exitListeners)
6164
+ listener(code);
6165
+ }
6166
+ onData(listener) {
6167
+ this.dataListeners.push(listener);
6168
+ if (this.buffered.length > 0) {
6169
+ const held = this.buffered;
6170
+ this.buffered = "";
6171
+ listener(held);
6172
+ }
6173
+ }
6174
+ onExit(listener) {
6175
+ this.exitListeners.push(listener);
6176
+ if (this.exited)
6177
+ listener(this.exitCode ?? null);
6178
+ }
6179
+ write(data) {
6180
+ this.send(encodeFrame(FrameKind.Data, this.id, new TextEncoder().encode(data)));
6181
+ }
6182
+ resize(cols, rows) {
6183
+ this.send(controlFrame({ t: "resize", id: this.id, cols, rows }));
6184
+ }
6185
+ kill() {
6186
+ if (this.exited)
6187
+ return;
6188
+ this.send(controlFrame({ t: "kill", id: this.id }));
6189
+ }
6190
+ }
6191
+ function connectPtyHost(options) {
6192
+ return new Promise((resolve, reject) => {
6193
+ const socket = net3.createConnection(options.endpoint);
6194
+ const decoder = new FrameDecoder;
6195
+ const ptys = new Map;
6196
+ const pending = new Map;
6197
+ const dataListeners = [];
6198
+ const rawDecoders = new Map;
6199
+ let welcomed = false;
6200
+ const send = (frame) => {
6201
+ if (!socket.destroyed)
6202
+ socket.write(frame);
6203
+ };
6204
+ const fail = (error) => {
6205
+ if (!welcomed)
6206
+ reject(error);
6207
+ for (const waiter of pending.values())
6208
+ waiter.reject(error);
6209
+ pending.clear();
6210
+ socket.destroy();
6211
+ };
6212
+ const timer = setTimeout(() => fail(new Error(`pty host did not greet within ${options.timeoutMs ?? 5000}ms`)), options.timeoutMs ?? 5000);
6213
+ timer.unref?.();
6214
+ socket.on("error", (err) => fail(err instanceof Error ? err : new Error(String(err))));
6215
+ socket.on("close", () => {
6216
+ clearTimeout(timer);
6217
+ if (!welcomed)
6218
+ reject(new Error("pty host refused the connection"));
6219
+ });
6220
+ socket.on("connect", () => send(controlFrame({ t: "hello", v: PTY_PROTOCOL_VERSION, token: options.token })));
6221
+ socket.on("data", (chunk) => {
6222
+ let frames;
6223
+ try {
6224
+ frames = decoder.push(new Uint8Array(chunk));
6225
+ } catch (err) {
6226
+ fail(err instanceof Error ? err : new Error(String(err)));
6227
+ return;
6228
+ }
6229
+ for (const frame of frames) {
6230
+ if (frame.kind === FrameKind.Data) {
6231
+ ptys.get(frame.id)?.push(frame.payload);
6232
+ if (dataListeners.length > 0) {
6233
+ let raw = rawDecoders.get(frame.id);
6234
+ if (raw === undefined) {
6235
+ raw = new StringDecoder("utf8");
6236
+ rawDecoders.set(frame.id, raw);
6237
+ }
6238
+ const text = raw.write(Buffer.from(frame.payload));
6239
+ if (text.length > 0)
6240
+ for (const listener of dataListeners)
6241
+ listener(frame.id, text);
6242
+ }
6243
+ continue;
6244
+ }
6245
+ const message = parseControl(frame.payload);
6246
+ if (message === undefined)
6247
+ continue;
6248
+ if (message.t === "welcome") {
6249
+ clearTimeout(timer);
6250
+ welcomed = true;
6251
+ resolve({
6252
+ protocolVersion: message.v,
6253
+ sessions: message.sessions,
6254
+ spawn: (spawnOptions) => new Promise((done, failed) => {
6255
+ const id = spawnOptions.id ?? crypto.randomUUID().replace(/-/g, "");
6256
+ pending.set(id, {
6257
+ resolve: (pid) => {
6258
+ const pty = new RemotePty(id, pid, send);
6259
+ ptys.set(id, pty);
6260
+ done(pty);
6261
+ },
6262
+ reject: failed
6263
+ });
6264
+ send(controlFrame({
6265
+ t: "spawn",
6266
+ id,
6267
+ exe: spawnOptions.exe,
6268
+ argv: spawnOptions.argv,
6269
+ cwd: spawnOptions.cwd,
6270
+ env: Object.fromEntries(Object.entries(spawnOptions.env).filter((entry) => entry[1] !== undefined)),
6271
+ cols: spawnOptions.cols,
6272
+ rows: spawnOptions.rows,
6273
+ term: spawnOptions.term
6274
+ }));
6275
+ }),
6276
+ onData: (listener) => dataListeners.push(listener),
6277
+ close: () => socket.destroy()
6278
+ });
6279
+ continue;
6280
+ }
6281
+ if (message.t === "spawned") {
6282
+ pending.get(message.id)?.resolve(message.pid);
6283
+ pending.delete(message.id);
6284
+ continue;
6285
+ }
6286
+ if (message.t === "error") {
6287
+ pending.get(message.id)?.reject(new Error(message.message));
6288
+ pending.delete(message.id);
6289
+ continue;
6290
+ }
6291
+ if (message.t === "exit") {
6292
+ const pty = ptys.get(message.id);
6293
+ ptys.delete(message.id);
6294
+ rawDecoders.delete(message.id);
6295
+ pty?.finish(message.code);
6296
+ }
6297
+ }
6298
+ });
6299
+ });
6300
+ }
6301
+ var init_pty_host_client = __esm(() => {
6302
+ init_src();
6303
+ });
6304
+
6305
+ // packages/daemon/src/pty-host-launch.ts
5884
6306
  import { spawn as spawn3 } from "child_process";
5885
- import fs16 from "fs/promises";
6307
+ import fs17 from "fs";
6308
+ import os5 from "os";
6309
+ import path17 from "path";
6310
+ function hostFile(runtimeDir) {
6311
+ return path17.join(runtimeDir, "pty-host.json");
6312
+ }
6313
+ function readPtyHostFile(file) {
6314
+ try {
6315
+ const parsed = JSON.parse(fs17.readFileSync(file, "utf8"));
6316
+ if (typeof parsed !== "object" || parsed === null)
6317
+ return;
6318
+ const { pid, endpoint, token, version: version2 } = parsed;
6319
+ if (typeof pid !== "number" || typeof endpoint !== "string" || typeof token !== "string")
6320
+ return;
6321
+ return { pid, endpoint, token, version: typeof version2 === "number" ? version2 : PTY_PROTOCOL_VERSION };
6322
+ } catch {
6323
+ return;
6324
+ }
6325
+ }
6326
+ function isRunning(pid) {
6327
+ try {
6328
+ process.kill(pid, 0);
6329
+ return true;
6330
+ } catch (err) {
6331
+ return err.code === "EPERM";
6332
+ }
6333
+ }
6334
+ function hostEndpoint(runtimeDir) {
6335
+ const base = ipcEndpoint(process.platform, os5.userInfo().username, { runtimeDir });
6336
+ return process.platform === "win32" ? `${base}-pty` : path17.join(runtimeDir, "axterm-pty.sock");
6337
+ }
6338
+ async function ensurePtyHost(location) {
6339
+ const file = hostFile(location.runtimeDir);
6340
+ const existing = readPtyHostFile(file);
6341
+ if (existing !== undefined && isRunning(existing.pid))
6342
+ return existing;
6343
+ if (existing !== undefined)
6344
+ log(`pty host: handoff file names a dead pid ${existing.pid}, starting a new host`);
6345
+ const endpoint = hostEndpoint(location.runtimeDir);
6346
+ if (process.platform !== "win32")
6347
+ fs17.rmSync(endpoint, { force: true });
6348
+ const token = shortHash(`${location.runtimeDir}:${Date.now()}:${Math.random()}`) + shortHash(String(process.pid));
6349
+ const launch = ptyHostLaunch({ execPath: process.execPath, mainScript: location.mainScript ?? Bun.main });
6350
+ const child = spawn3(launch.command, launch.args, {
6351
+ detached: true,
6352
+ stdio: "ignore",
6353
+ windowsHide: true,
6354
+ env: {
6355
+ ...process.env,
6356
+ ...location.env,
6357
+ AXTERM_PTY_HOST_ENDPOINT: endpoint,
6358
+ AXTERM_PTY_HOST_TOKEN: token,
6359
+ AXTERM_PTY_HOST_FILE: file
6360
+ }
6361
+ });
6362
+ child.unref();
6363
+ if (child.pid === undefined)
6364
+ throw new Error("could not start the terminal host");
6365
+ const deadline = Date.now() + 15000;
6366
+ for (;; ) {
6367
+ const info = readPtyHostFile(file);
6368
+ if (info !== undefined && info.endpoint === endpoint && isRunning(info.pid))
6369
+ return info;
6370
+ if (Date.now() > deadline)
6371
+ throw new Error(`the terminal host did not become ready within 15s (${endpoint})`);
6372
+ await new Promise((resolve) => setTimeout(resolve, 100));
6373
+ }
6374
+ }
6375
+ function writePtyHostFile(file, info) {
6376
+ fs17.mkdirSync(path17.dirname(file), { recursive: true, mode: 448 });
6377
+ fs17.writeFileSync(file, `${JSON.stringify(info, null, 2)}
6378
+ `, { mode: 384 });
6379
+ restrictFile(file);
6380
+ }
6381
+ var init_pty_host_launch = __esm(() => {
6382
+ init_src();
6383
+ init_paths();
6384
+ init_shell();
6385
+ });
6386
+
6387
+ // packages/daemon/src/pty.ts
6388
+ import { spawnSync as spawnSync2 } from "child_process";
6389
+ import path18 from "path";
6390
+ import { StringDecoder as StringDecoder2 } from "string_decoder";
6391
+
6392
+ class Fanout {
6393
+ listeners = new Set;
6394
+ backlog = [];
6395
+ add(listener) {
6396
+ this.listeners.add(listener);
6397
+ for (const value of this.backlog)
6398
+ listener(value);
6399
+ }
6400
+ emit(value) {
6401
+ if (this.listeners.size === 0)
6402
+ this.backlog.push(value);
6403
+ else
6404
+ for (const listener of this.listeners)
6405
+ listener(value);
6406
+ }
6407
+ }
6408
+ function killTree(pid, exeName) {
6409
+ if (process.platform === "win32") {
6410
+ const check = spawnSync2("tasklist", ["/FI", `PID eq ${pid}`, "/NH", "/FO", "CSV"], {
6411
+ encoding: "utf8",
6412
+ windowsHide: true
6413
+ });
6414
+ const image = check.stdout?.split(",")[0]?.replace(/"/g, "").toLowerCase() ?? "";
6415
+ if (image === exeName) {
6416
+ spawnSync2("taskkill", ["/PID", String(pid), "/T", "/F"], { encoding: "utf8", windowsHide: true });
6417
+ }
6418
+ return;
6419
+ }
6420
+ try {
6421
+ process.kill(-pid, "SIGKILL");
6422
+ } catch {}
6423
+ }
6424
+
6425
+ class BunPtyHandle {
6426
+ proc;
6427
+ exeName;
6428
+ data = new Fanout;
6429
+ exit = new Fanout;
6430
+ decoder = new StringDecoder2("utf8");
6431
+ exited = false;
6432
+ constructor(proc, exeName) {
6433
+ this.proc = proc;
6434
+ this.exeName = exeName;
6435
+ }
6436
+ get pid() {
6437
+ return this.proc.pid;
6438
+ }
6439
+ push(bytes) {
6440
+ const text = this.decoder.write(Buffer.from(bytes));
6441
+ if (text.length > 0)
6442
+ this.data.emit(text);
6443
+ }
6444
+ finish(code) {
6445
+ this.exited = true;
6446
+ const tail = this.decoder.end();
6447
+ if (tail.length > 0)
6448
+ this.data.emit(tail);
6449
+ try {
6450
+ this.proc.terminal.close();
6451
+ } catch {}
6452
+ this.exit.emit(code);
6453
+ }
6454
+ onData(listener) {
6455
+ this.data.add(listener);
6456
+ }
6457
+ onExit(listener) {
6458
+ this.exit.add(listener);
6459
+ }
6460
+ write(data) {
6461
+ this.proc.terminal.write(data);
6462
+ }
6463
+ resize(cols, rows) {
6464
+ this.proc.terminal.resize(cols, rows);
6465
+ }
6466
+ kill() {
6467
+ if (this.exited)
6468
+ return;
6469
+ killTree(this.proc.pid, this.exeName);
6470
+ try {
6471
+ this.proc.kill();
6472
+ } catch {}
6473
+ }
6474
+ }
6475
+ function spawnPty(options) {
6476
+ let handle;
6477
+ const proc = Bun.spawn([options.exe, ...options.argv], {
6478
+ cwd: options.cwd,
6479
+ env: { ...options.env, TERM: options.term },
6480
+ terminal: {
6481
+ cols: options.cols,
6482
+ rows: options.rows,
6483
+ name: options.term,
6484
+ data(_terminal, bytes) {
6485
+ handle?.push(bytes);
6486
+ }
6487
+ }
6488
+ });
6489
+ handle = new BunPtyHandle(proc, path18.basename(options.exe).toLowerCase());
6490
+ proc.exited.then((code) => handle?.finish(code ?? null));
6491
+ return handle;
6492
+ }
6493
+ var init_pty = () => {};
6494
+
6495
+ // packages/daemon/src/occupancy.ts
6496
+ import { spawn as spawn4 } from "child_process";
6497
+ import fs18 from "fs/promises";
5886
6498
  function shellOccupied(shellPid) {
5887
6499
  return process.platform === "win32" ? occupiedFromSnapshot(shellPid) : occupiedFromPidProbe(shellPid);
5888
6500
  }
@@ -5927,11 +6539,11 @@ async function occupiedFromSnapshot(shellPid) {
5927
6539
  }
5928
6540
  async function childrenViaProc(pid) {
5929
6541
  try {
5930
- const children = await fs16.readFile(`/proc/${pid}/task/${pid}/children`, "utf8");
6542
+ const children = await fs18.readFile(`/proc/${pid}/task/${pid}/children`, "utf8");
5931
6543
  return children.trim().length > 0;
5932
6544
  } catch {
5933
6545
  try {
5934
- await fs16.access(`/proc/${pid}`);
6546
+ await fs18.access(`/proc/${pid}`);
5935
6547
  return;
5936
6548
  } catch {
5937
6549
  return false;
@@ -5978,7 +6590,7 @@ async function processTable() {
5978
6590
  }
5979
6591
  function capture(exe, argv) {
5980
6592
  const { promise, resolve } = Promise.withResolvers();
5981
- const child = spawn3(exe, argv, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
6593
+ const child = spawn4(exe, argv, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
5982
6594
  let stdout3 = "";
5983
6595
  child.stdout.setEncoding("utf8");
5984
6596
  child.stdout.on("data", (chunk) => {
@@ -5995,20 +6607,20 @@ var init_occupancy = __esm(() => {
5995
6607
  });
5996
6608
 
5997
6609
  // packages/daemon/src/agent-store.ts
5998
- import fs17 from "fs";
5999
- import os4 from "os";
6000
- import path16 from "path";
6610
+ import fs19 from "fs";
6611
+ import os6 from "os";
6612
+ import path19 from "path";
6001
6613
  import { Database as Database3 } from "bun:sqlite";
6002
6614
  function readTailLines(file, maxBytes) {
6003
6615
  let fd;
6004
6616
  try {
6005
- fd = fs17.openSync(file, "r");
6006
- const size = fs17.fstatSync(fd).size;
6617
+ fd = fs19.openSync(file, "r");
6618
+ const size = fs19.fstatSync(fd).size;
6007
6619
  if (size === 0)
6008
6620
  return [];
6009
6621
  const length = Math.min(size, maxBytes);
6010
6622
  const buffer = Buffer.allocUnsafe(length);
6011
- fs17.readSync(fd, buffer, 0, length, size - length);
6623
+ fs19.readSync(fd, buffer, 0, length, size - length);
6012
6624
  const text = buffer.toString("utf8");
6013
6625
  const lines = text.split(`
6014
6626
  `);
@@ -6021,46 +6633,46 @@ function readTailLines(file, maxBytes) {
6021
6633
  return [];
6022
6634
  } finally {
6023
6635
  if (fd !== undefined)
6024
- fs17.closeSync(fd);
6636
+ fs19.closeSync(fd);
6025
6637
  }
6026
6638
  }
6027
- function claudeConfigDir() {
6028
- return process.env["CLAUDE_CONFIG_DIR"] ?? path16.join(os4.homedir(), ".claude");
6639
+ function claudeConfigDir2() {
6640
+ return process.env["CLAUDE_CONFIG_DIR"] ?? path19.join(os6.homedir(), ".claude");
6029
6641
  }
6030
6642
  function codexHome() {
6031
- return path16.join(STATE_DIR, "codex-home");
6643
+ return path19.join(STATE_DIR, "codex-home");
6032
6644
  }
6033
6645
  function ompAgentDb() {
6034
- return path16.join(os4.homedir(), ".omp", "agent", "agent.db");
6646
+ return path19.join(os6.homedir(), ".omp", "agent", "agent.db");
6035
6647
  }
6036
6648
  function findCodexRollout(sessionId) {
6037
- const root = path16.join(codexHome(), "sessions");
6649
+ const root = path19.join(codexHome(), "sessions");
6038
6650
  for (const shard2 of codexRolloutShards(sessionId)) {
6039
- const dir = path16.join(root, ...shard2.split("/"));
6651
+ const dir = path19.join(root, ...shard2.split("/"));
6040
6652
  let entries;
6041
6653
  try {
6042
- entries = fs17.readdirSync(dir);
6654
+ entries = fs19.readdirSync(dir);
6043
6655
  } catch {
6044
6656
  continue;
6045
6657
  }
6046
6658
  const match2 = entries.find((name) => name.endsWith(`-${sessionId}.jsonl`));
6047
6659
  if (match2 !== undefined)
6048
- return path16.join(dir, match2);
6660
+ return path19.join(dir, match2);
6049
6661
  }
6050
6662
  return;
6051
6663
  }
6052
6664
  function newestCodexRollout() {
6053
- const root = path16.join(codexHome(), "sessions");
6665
+ const root = path19.join(codexHome(), "sessions");
6054
6666
  let best;
6055
6667
  const walk = (dir, depth) => {
6056
6668
  let entries;
6057
6669
  try {
6058
- entries = fs17.readdirSync(dir, { withFileTypes: true });
6670
+ entries = fs19.readdirSync(dir, { withFileTypes: true });
6059
6671
  } catch {
6060
6672
  return;
6061
6673
  }
6062
6674
  for (const entry of entries) {
6063
- const full = path16.join(dir, entry.name);
6675
+ const full = path19.join(dir, entry.name);
6064
6676
  if (entry.isDirectory()) {
6065
6677
  if (depth < 3)
6066
6678
  walk(full, depth + 1);
@@ -6069,7 +6681,7 @@ function newestCodexRollout() {
6069
6681
  if (!entry.name.endsWith(".jsonl"))
6070
6682
  continue;
6071
6683
  try {
6072
- const { mtimeMs } = fs17.statSync(full);
6684
+ const { mtimeMs } = fs19.statSync(full);
6073
6685
  if (!best || mtimeMs > best.mtimeMs)
6074
6686
  best = { file: full, mtimeMs };
6075
6687
  } catch {}
@@ -6080,7 +6692,7 @@ function newestCodexRollout() {
6080
6692
  }
6081
6693
  function ompQuotaFacts() {
6082
6694
  const file = ompAgentDb();
6083
- if (!fs17.existsSync(file))
6695
+ if (!fs19.existsSync(file))
6084
6696
  return { agent: "omp", kind: "gauge" };
6085
6697
  let db;
6086
6698
  try {
@@ -6107,12 +6719,12 @@ function codexQuotaFacts() {
6107
6719
  return reading ? { agent: "codex", kind: "gauge", reading } : { agent: "codex", kind: "gauge" };
6108
6720
  }
6109
6721
  function claudeQuotaFacts(sessions) {
6110
- const dir = claudeConfigDir();
6722
+ const dir = claudeConfigDir2();
6111
6723
  let newest;
6112
6724
  for (const session of sessions) {
6113
6725
  if (session.agentSessionId === undefined)
6114
6726
  continue;
6115
- const file = path16.join(dir, claudeTranscriptRelPath(session.cwd, session.agentSessionId));
6727
+ const file = path19.join(dir, claudeTranscriptRelPath(session.cwd, session.agentSessionId));
6116
6728
  const event = parseClaudeLimitEvent(readTailLines(file, QUOTA_TAIL_BYTES));
6117
6729
  if (event && (!newest || event.at > newest.at))
6118
6730
  newest = event;
@@ -6144,11 +6756,11 @@ function collectQuotaFacts(agents, sessions, onPath) {
6144
6756
  function storeQuietForMs(agent, cwd, agentSessionId, nowMs) {
6145
6757
  if (agentSessionId === undefined)
6146
6758
  return;
6147
- const file = agent === "claude" ? path16.join(claudeConfigDir(), claudeTranscriptRelPath(cwd, agentSessionId)) : agent === "codex" ? findCodexRollout(agentSessionId) : undefined;
6759
+ const file = agent === "claude" ? path19.join(claudeConfigDir2(), claudeTranscriptRelPath(cwd, agentSessionId)) : agent === "codex" ? findCodexRollout(agentSessionId) : undefined;
6148
6760
  if (file === undefined)
6149
6761
  return;
6150
6762
  try {
6151
- return Math.max(0, nowMs - fs17.statSync(file).mtimeMs);
6763
+ return Math.max(0, nowMs - fs19.statSync(file).mtimeMs);
6152
6764
  } catch {
6153
6765
  return;
6154
6766
  }
@@ -6158,15 +6770,15 @@ function readFileRange(file, from, to) {
6158
6770
  return "";
6159
6771
  let fd;
6160
6772
  try {
6161
- fd = fs17.openSync(file, "r");
6773
+ fd = fs19.openSync(file, "r");
6162
6774
  const buffer = Buffer.allocUnsafe(to - from);
6163
- const read = fs17.readSync(fd, buffer, 0, to - from, from);
6775
+ const read = fs19.readSync(fd, buffer, 0, to - from, from);
6164
6776
  return buffer.subarray(0, read).toString("utf8");
6165
6777
  } catch {
6166
6778
  return "";
6167
6779
  } finally {
6168
6780
  if (fd !== undefined)
6169
- fs17.closeSync(fd);
6781
+ fs19.closeSync(fd);
6170
6782
  }
6171
6783
  }
6172
6784
  var QUOTA_TAIL_BYTES;
@@ -6177,10 +6789,10 @@ var init_agent_store = __esm(() => {
6177
6789
  });
6178
6790
 
6179
6791
  // packages/daemon/src/codex-app-server.ts
6180
- import net3 from "net";
6792
+ import net4 from "net";
6181
6793
  async function freeLoopbackPort() {
6182
6794
  const { promise, resolve, reject } = Promise.withResolvers();
6183
- const probe = net3.createServer();
6795
+ const probe = net4.createServer();
6184
6796
  probe.once("error", reject);
6185
6797
  probe.listen({ host: "127.0.0.1", port: 0 }, () => {
6186
6798
  const address = probe.address();
@@ -6193,7 +6805,7 @@ async function waitForPort(port, timeoutMs = READY_TIMEOUT_MS) {
6193
6805
  const deadline = Date.now() + timeoutMs;
6194
6806
  for (;; ) {
6195
6807
  const open = await new Promise((resolve) => {
6196
- const socket = net3.connect({ host: "127.0.0.1", port });
6808
+ const socket = net4.connect({ host: "127.0.0.1", port });
6197
6809
  const done = (value) => {
6198
6810
  socket.destroy();
6199
6811
  resolve(value);
@@ -6288,114 +6900,6 @@ var init_codex_app_server = __esm(() => {
6288
6900
  init_paths();
6289
6901
  });
6290
6902
 
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
6903
  // node_modules/.bun/@xterm+headless@5.5.0/node_modules/@xterm/headless/lib-headless/xterm-headless.js
6400
6904
  var require_xterm_headless = __commonJS((exports) => {
6401
6905
  (() => {
@@ -10726,8 +11230,8 @@ var require_addon_serialize = __commonJS((exports, module) => {
10726
11230
  });
10727
11231
 
10728
11232
  // packages/daemon/src/pty-session.ts
10729
- import fs18 from "fs";
10730
- import path18 from "path";
11233
+ import fs20 from "fs";
11234
+ import path20 from "path";
10731
11235
 
10732
11236
  class PtySession {
10733
11237
  info;
@@ -10742,25 +11246,29 @@ class PtySession {
10742
11246
  pasteReady = false;
10743
11247
  readyScanTail = "";
10744
11248
  lastOutputAt = Date.now();
10745
- constructor(info, spawn4) {
11249
+ static async open(info, spawn5, spawner = spawnPty) {
11250
+ const pty = await spawner({
11251
+ exe: spawn5.exe,
11252
+ argv: spawn5.argv,
11253
+ cwd: spawn5.cwd,
11254
+ cols: spawn5.cols,
11255
+ rows: spawn5.rows,
11256
+ env: spawn5.env,
11257
+ term: "xterm-256color"
11258
+ });
11259
+ return new PtySession(info, spawn5, pty);
11260
+ }
11261
+ constructor(info, spawn5, pty) {
10746
11262
  this.info = info;
10747
11263
  this.term = new import_headless.Terminal({
10748
- cols: spawn4.cols,
10749
- rows: spawn4.rows,
11264
+ cols: spawn5.cols,
11265
+ rows: spawn5.rows,
10750
11266
  scrollback: SCROLLBACK_LINES,
10751
11267
  allowProposedApi: true
10752
11268
  });
10753
11269
  this.serializeAddon = new import_addon_serialize.SerializeAddon;
10754
11270
  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"
10763
- });
11271
+ this.pty = pty;
10764
11272
  info.ptyPid = this.pty.pid;
10765
11273
  this.pty.onData((data) => {
10766
11274
  this.seq += 1;
@@ -10843,10 +11351,10 @@ class PtySession {
10843
11351
  }
10844
11352
  saveScrollback() {
10845
11353
  try {
10846
- const file = path18.join(SCROLLBACK_DIR, `${this.info.id}.bin`);
11354
+ const file = path20.join(SCROLLBACK_DIR, `${this.info.id}.bin`);
10847
11355
  const tmp = `${file}.tmp`;
10848
- fs18.writeFileSync(tmp, this.serializeBuffer(), "utf8");
10849
- fs18.renameSync(tmp, file);
11356
+ fs20.writeFileSync(tmp, this.serializeBuffer(), "utf8");
11357
+ fs20.renameSync(tmp, file);
10850
11358
  } catch (err) {
10851
11359
  log(`scrollback save failed for ${this.info.shortId}: ${String(err)}`);
10852
11360
  }
@@ -10868,9 +11376,12 @@ var init_pty_session = __esm(() => {
10868
11376
 
10869
11377
  // packages/daemon/src/session-manager.ts
10870
11378
  import { randomBytes as randomBytes2, randomUUID } from "crypto";
10871
- import fs19 from "fs";
10872
- import os5 from "os";
10873
- import path19 from "path";
11379
+ import fs21 from "fs";
11380
+ import os7 from "os";
11381
+ import path21 from "path";
11382
+ function sessionScratchRoot(info) {
11383
+ return info.worktreePath ?? info.cwd;
11384
+ }
10874
11385
  function unresolvableAgent(commandOverride, agentCommand, lookup) {
10875
11386
  if (commandOverride !== undefined)
10876
11387
  return;
@@ -10932,6 +11443,8 @@ class SessionManager {
10932
11443
  this.sessionWatch.unref();
10933
11444
  }
10934
11445
  sessionWatch;
11446
+ ptyHost;
11447
+ ptyHostUnavailable = false;
10935
11448
  codexServers = new Map;
10936
11449
  workspaceFor(_sessionId) {
10937
11450
  return this.workspace;
@@ -11009,8 +11522,8 @@ class SessionManager {
11009
11522
  };
11010
11523
  }
11011
11524
  async create(params) {
11012
- const requestedCwd = params.cwd ? path19.resolve(params.cwd) : os5.homedir();
11013
- if (!fs19.existsSync(requestedCwd)) {
11525
+ const requestedCwd = params.cwd ? path21.resolve(params.cwd) : os7.homedir();
11526
+ if (!fs21.existsSync(requestedCwd)) {
11014
11527
  throw new AxtermError("err.cwd.missing", { cwd: requestedCwd });
11015
11528
  }
11016
11529
  let worktree;
@@ -11057,7 +11570,7 @@ class SessionManager {
11057
11570
  const info = {
11058
11571
  id,
11059
11572
  shortId: id.slice(0, 6),
11060
- name: params.name ?? worktree?.branch ?? path19.basename(cwd),
11573
+ name: params.name ?? worktree?.branch ?? path21.basename(cwd),
11061
11574
  cwd,
11062
11575
  shell: shell.kind,
11063
11576
  agentCommand,
@@ -11068,7 +11581,7 @@ class SessionManager {
11068
11581
  ...worktree !== undefined ? { worktreePath: worktree.path, branch: worktree.branch, repoRoot: worktree.repoRoot } : {}
11069
11582
  };
11070
11583
  const appServerUrl = agent === "codex" ? await this.startCodexAppServer(id, this.ensureCodexHome(), cwd) : undefined;
11071
- this.launch(info, shell, agentCommand, appServerUrl);
11584
+ await this.launch(info, shell, agentCommand, appServerUrl);
11072
11585
  this.store.insertSession(info);
11073
11586
  log(`session ${info.shortId} created shell=${shell.kind} cmd=${agentCommand.join(" ") || "(shell only)"}`);
11074
11587
  this.emit({ t: "session.created", info });
@@ -11082,7 +11595,7 @@ class SessionManager {
11082
11595
  launchToken
11083
11596
  });
11084
11597
  const file = this.claudeSettingsPath(id);
11085
- fs19.writeFileSync(file, JSON.stringify(settings, null, 2), { mode: 384 });
11598
+ fs21.writeFileSync(file, JSON.stringify(settings, null, 2), { mode: 384 });
11086
11599
  }
11087
11600
  writeGeminiSettings(sessionId) {
11088
11601
  try {
@@ -11095,8 +11608,8 @@ class SessionManager {
11095
11608
  });
11096
11609
  const file = this.geminiSettingsPath();
11097
11610
  const tmp = `${file}.tmp`;
11098
- fs19.writeFileSync(tmp, JSON.stringify(settings, null, 2));
11099
- fs19.renameSync(tmp, file);
11611
+ fs21.writeFileSync(tmp, JSON.stringify(settings, null, 2));
11612
+ fs21.renameSync(tmp, file);
11100
11613
  } catch (err) {
11101
11614
  log(`gemini settings write failed; session starts without hooks: ${String(err)}`);
11102
11615
  }
@@ -11142,22 +11655,22 @@ class SessionManager {
11142
11655
  this.codexServers.delete(id);
11143
11656
  }
11144
11657
  ensureCodexHome() {
11145
- const home = path19.join(STATE_DIR, "codex-home");
11146
- fs19.mkdirSync(home, { recursive: true, mode: 448 });
11658
+ const home = path21.join(STATE_DIR, "codex-home");
11659
+ fs21.mkdirSync(home, { recursive: true, mode: 448 });
11147
11660
  this.linkCodexAuth(home);
11148
11661
  const bridge = hookLaunch({ execPath: process.execPath, mainScript: Bun.main, agent: "codex" });
11149
11662
  const hooksFile = buildCodexHooks({ command: bridge.command, args: bridge.args });
11150
11663
  const hooksPath = codexTrustKeyPath(home);
11151
11664
  const tmp = `${hooksPath}.tmp`;
11152
- fs19.writeFileSync(tmp, JSON.stringify(hooksFile, null, 2));
11153
- fs19.renameSync(tmp, hooksPath);
11665
+ fs21.writeFileSync(tmp, JSON.stringify(hooksFile, null, 2));
11666
+ fs21.renameSync(tmp, hooksPath);
11154
11667
  const entries = codexHookTrust(hooksPath, hooksFile);
11155
- const configPath = path19.join(home, "config.toml");
11156
- const existing = fs19.existsSync(configPath) ? fs19.readFileSync(configPath, "utf8") : "";
11668
+ const configPath = path21.join(home, "config.toml");
11669
+ const existing = fs21.existsSync(configPath) ? fs21.readFileSync(configPath, "utf8") : "";
11157
11670
  const missing = entries.filter((e) => !existing.includes(`"${e.key.replaceAll("\\", "\\\\")}"`));
11158
11671
  if (missing.length > 0) {
11159
11672
  const block = codexTrustStateToml(missing);
11160
- fs19.appendFileSync(configPath, existing !== "" && !existing.endsWith(`
11673
+ fs21.appendFileSync(configPath, existing !== "" && !existing.endsWith(`
11161
11674
  `) ? `
11162
11675
  ${block}` : block);
11163
11676
  }
@@ -11170,18 +11683,18 @@ ${block}` : block);
11170
11683
  return home;
11171
11684
  }
11172
11685
  linkCodexAuth(home) {
11173
- const target = path19.join(home, "auth.json");
11174
- if (fs19.existsSync(target))
11686
+ const target = path21.join(home, "auth.json");
11687
+ if (fs21.existsSync(target))
11175
11688
  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))
11689
+ const operatorHome = process.env["CODEX_HOME"] ?? path21.join(os7.homedir(), ".codex");
11690
+ const source = path21.join(operatorHome, "auth.json");
11691
+ if (!fs21.existsSync(source))
11179
11692
  throw new AxtermError("err.codex.noAuth", { authFile: source });
11180
11693
  try {
11181
- fs19.symlinkSync(source, target, "file");
11694
+ fs21.symlinkSync(source, target, "file");
11182
11695
  } catch {
11183
11696
  try {
11184
- fs19.linkSync(source, target);
11697
+ fs21.linkSync(source, target);
11185
11698
  } catch (err) {
11186
11699
  throw new AxtermError("err.codex.authLink", { reason: err instanceof Error ? err.message : String(err) });
11187
11700
  }
@@ -11189,35 +11702,36 @@ ${block}` : block);
11189
11702
  }
11190
11703
  doctorReport() {
11191
11704
  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");
11705
+ const home = path21.join(STATE_DIR, "codex-home");
11706
+ const operatorHome = process.env["CODEX_HOME"] ?? path21.join(os7.homedir(), ".codex");
11707
+ const authSource = path21.join(operatorHome, "auth.json");
11195
11708
  const geminiSettings = this.geminiSettingsPath();
11196
- const configPath = path19.join(home, "config.toml");
11709
+ const configPath = path21.join(home, "config.toml");
11197
11710
  const read = (file) => {
11198
11711
  try {
11199
- return fs19.existsSync(file) ? fs19.readFileSync(file, "utf8") : undefined;
11712
+ return fs21.existsSync(file) ? fs21.readFileSync(file, "utf8") : undefined;
11200
11713
  } catch {
11201
11714
  return;
11202
11715
  }
11203
11716
  };
11204
11717
  return [
11205
- ...evaluateBridgeDoctor({ command: bridge.command, exists: fs19.existsSync(bridge.command) }),
11718
+ ...evaluateBridgeDoctor({ command: bridge.command, exists: fs21.existsSync(bridge.command) }),
11206
11719
  ...evaluateOmpDoctor({
11207
11720
  binary: lookupOnPath("omp"),
11208
11721
  extensionPath: this.hooks.extensionPath,
11209
- extensionExists: fs19.existsSync(this.hooks.extensionPath)
11722
+ extensionExists: fs21.existsSync(this.hooks.extensionPath)
11210
11723
  }),
11211
11724
  ...evaluateClaudeDoctor({ binary: lookupOnPath("claude") }),
11212
11725
  ...evaluateCodexDoctor({
11213
11726
  binary: lookupOnPath("codex"),
11214
11727
  authSourcePath: authSource,
11215
- authSourceExists: fs19.existsSync(authSource),
11216
- homeExists: fs19.existsSync(home),
11728
+ authSourceExists: fs21.existsSync(authSource),
11729
+ homeExists: fs21.existsSync(home),
11217
11730
  expected: codexHookTrust(codexTrustKeyPath(home), buildCodexHooks({ command: bridge.command, args: bridge.args })),
11218
11731
  configToml: read(configPath)
11219
11732
  }),
11220
- ...evaluateGeminiDoctor({ binary: lookupOnPath("gemini"), settingsJson: read(geminiSettings) })
11733
+ ...evaluateGeminiDoctor({ binary: lookupOnPath("gemini"), settingsJson: read(geminiSettings) }),
11734
+ ...evaluateStagingDoctor({ mode: this.stagingMode() })
11221
11735
  ];
11222
11736
  }
11223
11737
  quotaReport() {
@@ -11337,50 +11851,92 @@ ${block}` : block);
11337
11851
  }
11338
11852
  writeScreenFile(idOrPrefix, lines) {
11339
11853
  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 });
11854
+ const file = path21.join(path21.dirname(this.hooks.endpointFile), `screen-${session.info.id}.txt`);
11855
+ fs21.writeFileSync(file, session.screenText(lines), { mode: 384 });
11342
11856
  return file;
11343
11857
  }
11344
11858
  removeScreenFile(id) {
11345
11859
  try {
11346
- fs19.rmSync(path19.join(path19.dirname(this.hooks.endpointFile), `screen-${id}.txt`), { force: true });
11860
+ fs21.rmSync(path21.join(path21.dirname(this.hooks.endpointFile), `screen-${id}.txt`), { force: true });
11347
11861
  } catch {}
11348
11862
  }
11349
11863
  async stageInboundFile(idOrPrefix, suggestedName, bytes) {
11350
11864
  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)`);
11865
+ if (this.stagingMode() === "runtime")
11866
+ return this.stageInRuntimeDir(info, suggestedName, bytes);
11867
+ const ws = this.workspaceFor(info.id);
11868
+ const dir = sessionScratchRoot(info);
11869
+ const file = path21.resolve(planMaterialisedPath(inboundStagingDir(dir), info.id, suggestedName));
11870
+ await this.prepareScratchDir(ws, dir, file);
11871
+ await ws.write(file, bytes, 384);
11872
+ log(`session ${info.shortId} staged ${path21.basename(file)} (${bytes.byteLength} bytes)`);
11873
+ return file;
11874
+ }
11875
+ stagingMode() {
11876
+ return this.configuredDefaults().staging ?? "project";
11877
+ }
11878
+ async stageInRuntimeDir(info, suggestedName, bytes) {
11879
+ const file = path21.resolve(planMaterialisedPath(MATERIALISED_DIR, info.id, suggestedName));
11880
+ await fs21.promises.mkdir(path21.dirname(file), { recursive: true, mode: 448 });
11881
+ await fs21.promises.writeFile(file, bytes, { mode: 384 });
11882
+ log(`session ${info.shortId} staged ${path21.basename(file)} (${bytes.byteLength} bytes, runtime dir)`);
11355
11883
  return file;
11356
11884
  }
11357
- removeStagedFiles(id) {
11885
+ async prepareScratchDir(ws, sessionDir, file) {
11886
+ const scratch = scratchDir(sessionDir);
11887
+ const ignore = `${scratch}/${SCRATCH_IGNORE_NAME}`;
11358
11888
  try {
11359
- fs19.rmSync(path19.join(MATERIALISED_DIR, id), { recursive: true, force: true });
11889
+ await ws.mkdirp(scratch, 448);
11890
+ if (await ws.stat(ignore) === undefined)
11891
+ await ws.write(ignore, new TextEncoder().encode(SCRATCH_IGNORE_BODY));
11892
+ } catch (err) {
11893
+ throw new AxtermError("err.staging.dir", { dir: scratch, error: err instanceof Error ? err.message : String(err) });
11894
+ }
11895
+ if (await ignoreState(ws, sessionDir, file) === "visible") {
11896
+ throw new AxtermError("err.staging.notIgnored", { dir: scratch, file: SCRATCH_IGNORE_NAME });
11897
+ }
11898
+ try {
11899
+ await ws.mkdirp(path21.dirname(file), 448);
11900
+ } catch (err) {
11901
+ throw new AxtermError("err.staging.dir", { dir: scratch, error: err instanceof Error ? err.message : String(err) });
11902
+ }
11903
+ }
11904
+ removeStagedFiles(id, info) {
11905
+ try {
11906
+ fs21.rmSync(path21.join(MATERIALISED_DIR, id), { recursive: true, force: true });
11907
+ } catch {}
11908
+ if (info === undefined)
11909
+ return;
11910
+ const scratch = path21.resolve(scratchDir(sessionScratchRoot(info)));
11911
+ try {
11912
+ fs21.rmSync(path21.resolve(inboundStagingDir(sessionScratchRoot(info)), id), { recursive: true, force: true });
11913
+ fs21.rmdirSync(path21.join(scratch, "inbound"));
11914
+ fs21.rmSync(path21.join(scratch, SCRATCH_IGNORE_NAME), { force: true });
11915
+ fs21.rmdirSync(scratch);
11360
11916
  } catch {}
11361
11917
  }
11362
11918
  removeClaudeSettings(id) {
11363
11919
  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 });
11920
+ fs21.rmSync(this.claudeSettingsPath(id), { force: true });
11921
+ fs21.rmSync(this.mcpConfigPath(id), { force: true });
11922
+ fs21.rmSync(this.mcpContextPath(id), { force: true });
11923
+ fs21.rmSync(this.geminiTelemetryPath(id), { force: true });
11368
11924
  this.stopCodexAppServer(id);
11369
11925
  } catch {}
11370
11926
  }
11371
11927
  claudeSettingsPath(id) {
11372
- return path19.join(path19.dirname(this.hooks.endpointFile), `claude-${id}.json`);
11928
+ return path21.join(path21.dirname(this.hooks.endpointFile), `claude-${id}.json`);
11373
11929
  }
11374
11930
  mcpConfigPath(id) {
11375
- return path19.join(path19.dirname(this.hooks.endpointFile), `mcp-${id}.json`);
11931
+ return path21.join(path21.dirname(this.hooks.endpointFile), `mcp-${id}.json`);
11376
11932
  }
11377
11933
  mcpContextPath(id) {
11378
- return path19.join(path19.dirname(this.hooks.endpointFile), `mcp-ctx-${id}.json`);
11934
+ return path21.join(path21.dirname(this.hooks.endpointFile), `mcp-ctx-${id}.json`);
11379
11935
  }
11380
11936
  writeMcpContext(id, launchToken) {
11381
11937
  const file = this.mcpContextPath(id);
11382
11938
  try {
11383
- fs19.writeFileSync(file, JSON.stringify({ port: this.hooks.port, token: this.hooks.host.token, sessionId: id, launchToken }), { mode: 384 });
11939
+ fs21.writeFileSync(file, JSON.stringify({ port: this.hooks.port, token: this.hooks.host.token, sessionId: id, launchToken }), { mode: 384 });
11384
11940
  return file;
11385
11941
  } catch (err) {
11386
11942
  log(`mcp context write failed; session starts without axterm's tools: ${String(err)}`);
@@ -11393,7 +11949,7 @@ ${block}` : block);
11393
11949
  writeMcpConfig(id, contextPath) {
11394
11950
  const file = this.mcpConfigPath(id);
11395
11951
  try {
11396
- fs19.writeFileSync(file, claudeMcpConfig(this.mcpServerLaunch(), contextPath), { mode: 384 });
11952
+ fs21.writeFileSync(file, claudeMcpConfig(this.mcpServerLaunch(), contextPath), { mode: 384 });
11397
11953
  return file;
11398
11954
  } catch (err) {
11399
11955
  log(`mcp config write failed; session starts without axterm's tools: ${String(err)}`);
@@ -11401,12 +11957,38 @@ ${block}` : block);
11401
11957
  }
11402
11958
  }
11403
11959
  geminiTelemetryPath(id) {
11404
- return path19.join(path19.dirname(this.hooks.endpointFile), `gemini-telemetry-${id}.log`);
11960
+ return path21.join(path21.dirname(this.hooks.endpointFile), `gemini-telemetry-${id}.log`);
11405
11961
  }
11406
11962
  geminiSettingsPath() {
11407
- return path19.join(path19.dirname(this.hooks.endpointFile), "gemini-defaults.json");
11963
+ return path21.join(path21.dirname(this.hooks.endpointFile), "gemini-defaults.json");
11964
+ }
11965
+ async ptySpawner() {
11966
+ if (process.env["AXTERM_PTY_HOST"] !== "1" || this.ptyHostUnavailable)
11967
+ return spawnPty;
11968
+ try {
11969
+ if (this.ptyHost === undefined) {
11970
+ const override = process.env["AXTERM_PTY_HOST_MAIN"];
11971
+ const info = await ensurePtyHost({ runtimeDir: RUNTIME_DIR, ...override ? { mainScript: override } : {} });
11972
+ this.ptyHost = await connectPtyHost({ endpoint: info.endpoint, token: info.token });
11973
+ log(`pty host: connected (pid ${info.pid})`);
11974
+ }
11975
+ const host = this.ptyHost;
11976
+ return (options) => host.spawn(options);
11977
+ } catch (err) {
11978
+ this.ptyHostUnavailable = true;
11979
+ log(`pty host unavailable, owning ptys in this process: ${err instanceof Error ? err.message : String(err)}`);
11980
+ return spawnPty;
11981
+ }
11408
11982
  }
11409
- launch(info, shell, agentCommand, appServerUrl) {
11983
+ requireClaudeConversation(agentCommand) {
11984
+ const conversation = resumedConversationId(agentCommand);
11985
+ if (conversation === undefined)
11986
+ return;
11987
+ if (claudeConversationExists(claudeConfigDir(), conversation) === false) {
11988
+ throw new AxtermError("err.claude.noConversation", { id: conversation });
11989
+ }
11990
+ }
11991
+ async launch(info, shell, agentCommand, appServerUrl) {
11410
11992
  const id = info.id;
11411
11993
  const launchToken = randomBytes2(16).toString("hex");
11412
11994
  const agentId = agentIdFromCommand(info.agentCommand);
@@ -11417,7 +11999,9 @@ ${block}` : block);
11417
11999
  if (agentId === "gemini")
11418
12000
  this.writeGeminiSettings(id);
11419
12001
  const codexHome2 = agentId === "codex" ? this.ensureCodexHome() : undefined;
11420
- const session = new PtySession(info, {
12002
+ if (agentId === "claude")
12003
+ this.requireClaudeConversation(agentCommand);
12004
+ const session = await PtySession.open(info, {
11421
12005
  exe: shell.exe,
11422
12006
  argv: buildShellArgv(shell, withAppServer(agentCommand, appServerUrl)),
11423
12007
  cwd: info.cwd,
@@ -11440,7 +12024,7 @@ ${block}` : block);
11440
12024
  } : {},
11441
12025
  ...codexHome2 !== undefined ? { CODEX_HOME: codexHome2 } : {}
11442
12026
  }
11443
- });
12027
+ }, await this.ptySpawner());
11444
12028
  this.agentRuntime.set(id, { agentExited: false, choiceSeq: 0, dialogOpen: false });
11445
12029
  this.hooks.host.registerSession(id, launchToken, (payload) => this.ingestHookEvent(id, payload));
11446
12030
  session.onData(() => {
@@ -11456,7 +12040,7 @@ ${block}` : block);
11456
12040
  this.hooks.host.unregisterSession(id);
11457
12041
  this.removeClaudeSettings(id);
11458
12042
  this.removeScreenFile(id);
11459
- this.removeStagedFiles(id);
12043
+ this.removeStagedFiles(id, info);
11460
12044
  clearTimeout(this.idleTimers.get(id));
11461
12045
  this.idleTimers.delete(id);
11462
12046
  this.sawOutput.delete(id);
@@ -11615,7 +12199,8 @@ ${block}` : block);
11615
12199
  const direct = this.live.get(idOrPrefix);
11616
12200
  if (direct)
11617
12201
  return direct;
11618
- const matches = [...this.live.values()].filter((s) => s.info.id.startsWith(idOrPrefix) || s.info.shortId === idOrPrefix || s.info.name === idOrPrefix);
12202
+ const alternate = sessionIdFromUuid(idOrPrefix);
12203
+ 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
12204
  if (matches.length === 1)
11620
12205
  return matches[0];
11621
12206
  if (matches.length > 1)
@@ -11641,21 +12226,22 @@ ${block}` : block);
11641
12226
  allSessions() {
11642
12227
  return sessionsByRecency([...this.live.values()].map((s) => s.info), this.store.listSessions());
11643
12228
  }
11644
- resumeNamed(stored, agent) {
11645
- if (!fs19.existsSync(stored.cwd))
11646
- throw new AxtermError("err.session.cwdGone", { cwd: stored.cwd });
12229
+ async resumeNamed(stored, agent) {
12230
+ const mcpConfigPath = agent === "claude" ? this.writeMcpConfig(stored.id, this.mcpContextPath(stored.id)) : undefined;
11647
12231
  const agentCommand = buildAgentResumeCommand({
11648
12232
  agent,
11649
12233
  sessionId: stored.id,
11650
12234
  extensionPath: this.hooks.extensionPath,
11651
- claudeSettingsPath: this.claudeSettingsPath(stored.id)
12235
+ claudeSettingsPath: this.claudeSettingsPath(stored.id),
12236
+ ...mcpConfigPath === undefined ? {} : { mcpConfigPath },
12237
+ storedArgv: stored.agentCommand
11652
12238
  });
11653
12239
  const info = { ...stored, agentCommand, state: "running" };
11654
12240
  delete info.exitCode;
11655
12241
  delete info.exitedAt;
11656
12242
  delete info.agentState;
11657
12243
  delete info.agentStateSource;
11658
- this.launch(info, resolveShell(info.shell), agentCommand);
12244
+ await this.launch(info, resolveShell(info.shell), agentCommand);
11659
12245
  this.store.markResumed(info.id, agentCommand, info.ptyPid);
11660
12246
  log(`session ${info.shortId} resumed cmd=${agentCommand.join(" ")}`);
11661
12247
  this.emit({ t: "session.created", info });
@@ -11682,7 +12268,7 @@ ${block}` : block);
11682
12268
  if (agentCommand === undefined) {
11683
12269
  throw new AxtermError("err.session.noResumeSupport", { agent: profile.displayName });
11684
12270
  }
11685
- if (!fs19.existsSync(stored.cwd))
12271
+ if (!fs21.existsSync(stored.cwd))
11686
12272
  throw new AxtermError("err.session.cwdGone", { cwd: stored.cwd });
11687
12273
  const info = { ...stored, agentCommand, state: "running" };
11688
12274
  delete info.exitCode;
@@ -11691,7 +12277,7 @@ ${block}` : block);
11691
12277
  delete info.agentStateSource;
11692
12278
  const shell = resolveShell(info.shell);
11693
12279
  const resumedAppServer = agentIdFromCommand(agentCommand) === "codex" ? await this.startCodexAppServer(info.id, this.ensureCodexHome(), info.cwd) : undefined;
11694
- this.launch(info, shell, agentCommand, resumedAppServer);
12280
+ await this.launch(info, shell, agentCommand, resumedAppServer);
11695
12281
  this.store.markResumed(info.id, agentCommand, info.ptyPid);
11696
12282
  log(`session ${info.shortId} resumed cmd=${agentCommand.join(" ")}`);
11697
12283
  this.emit({ t: "session.created", info });
@@ -11732,7 +12318,8 @@ ${block}` : block);
11732
12318
  if (err instanceof AxtermError && err.key === "err.session.ambiguous")
11733
12319
  throw err;
11734
12320
  }
11735
- const id = live?.info.id ?? this.store.findByPrefix(idOrPrefix)?.id;
12321
+ const info = live?.info ?? this.store.findByPrefix(idOrPrefix);
12322
+ const id = info?.id;
11736
12323
  if (id === undefined)
11737
12324
  throw new AxtermError("err.session.missing", { id: idOrPrefix });
11738
12325
  if (live !== undefined && !live.isExited) {
@@ -11747,7 +12334,7 @@ ${block}` : block);
11747
12334
  await promise;
11748
12335
  }
11749
12336
  this.removeScreenFile(id);
11750
- this.removeStagedFiles(id);
12337
+ this.removeStagedFiles(id, info);
11751
12338
  this.removeClaudeSettings(id);
11752
12339
  this.removeScrollback(id);
11753
12340
  this.store.deleteSession(id);
@@ -11756,7 +12343,7 @@ ${block}` : block);
11756
12343
  removeScrollback(id) {
11757
12344
  for (const file of [`${id}.bin`, `${id}.bin.tmp`]) {
11758
12345
  try {
11759
- fs19.rmSync(path19.join(SCROLLBACK_DIR, file), { force: true });
12346
+ fs21.rmSync(path21.join(SCROLLBACK_DIR, file), { force: true });
11760
12347
  } catch {}
11761
12348
  }
11762
12349
  }
@@ -11774,7 +12361,7 @@ ${block}` : block);
11774
12361
  const file = this.geminiTelemetryPath(id);
11775
12362
  let size = 0;
11776
12363
  try {
11777
- size = fs19.statSync(file).size;
12364
+ size = fs21.statSync(file).size;
11778
12365
  } catch {
11779
12366
  continue;
11780
12367
  }
@@ -11850,7 +12437,11 @@ var init_session_manager = __esm(() => {
11850
12437
  init_codex_home();
11851
12438
  init_host_env();
11852
12439
  init_git();
12440
+ init_claude_store();
11853
12441
  init_injector();
12442
+ init_pty_host_client();
12443
+ init_pty_host_launch();
12444
+ init_pty();
11854
12445
  init_occupancy();
11855
12446
  init_agent_store();
11856
12447
  init_codex_app_server();
@@ -11859,7 +12450,7 @@ var init_session_manager = __esm(() => {
11859
12450
  init_pty_session();
11860
12451
  init_shell();
11861
12452
  init_workspace_local();
11862
- MATERIALISED_DIR = path19.join(RUNTIME_DIR, "materialised");
12453
+ MATERIALISED_DIR = path21.join(RUNTIME_DIR, "materialised");
11863
12454
  PROMPT_OPTIONS = {
11864
12455
  hostKey: [{ label: "yes" }, { label: "no" }],
11865
12456
  password: [],
@@ -12008,11 +12599,11 @@ var init_acknowledge = __esm(() => {
12008
12599
  });
12009
12600
 
12010
12601
  // packages/channel-telegram/src/attach.ts
12011
- import fs20 from "fs";
12012
- import path20 from "path";
12602
+ import fs22 from "fs";
12603
+ import path22 from "path";
12013
12604
  function buildAttachmentUpload(filePath, chatId, topicId, caption, limits = TELEGRAM_LIMITS) {
12014
- const name = path20.basename(filePath);
12015
- const bytes = fs20.statSync(filePath).size;
12605
+ const name = path22.basename(filePath);
12606
+ const bytes = fs22.statSync(filePath).size;
12016
12607
  const plan = planAttachment({ name, bytes }, limits);
12017
12608
  if (plan.kind === "rejected")
12018
12609
  throw new Error(plan.reason);
@@ -12022,7 +12613,7 @@ function buildAttachmentUpload(filePath, chatId, topicId, caption, limits = TELE
12022
12613
  form.append("message_thread_id", String(topicId));
12023
12614
  if (caption !== undefined && caption.length > 0)
12024
12615
  form.append("caption", caption);
12025
- form.append(field, new Blob([fs20.readFileSync(filePath)]), name);
12616
+ form.append(field, new Blob([fs22.readFileSync(filePath)]), name);
12026
12617
  return { method: plan.kind === "photo" ? "sendPhoto" : "sendDocument", field, form };
12027
12618
  }
12028
12619
  function isPhotoRejection(error) {
@@ -12030,13 +12621,13 @@ function isPhotoRejection(error) {
12030
12621
  return /IMAGE_PROCESS_FAILED|PHOTO_INVALID_DIMENSIONS|PHOTO_EXT_INVALID|PHOTO_SAVE_FILE_INVALID/i.test(message);
12031
12622
  }
12032
12623
  function asDocumentUpload(filePath, chatId, topicId, caption) {
12033
- const name = path20.basename(filePath);
12624
+ const name = path22.basename(filePath);
12034
12625
  const form = new FormData;
12035
12626
  form.append("chat_id", String(chatId));
12036
12627
  form.append("message_thread_id", String(topicId));
12037
12628
  if (caption !== undefined && caption.length > 0)
12038
12629
  form.append("caption", caption);
12039
- form.append("document", new Blob([fs20.readFileSync(filePath)]), name);
12630
+ form.append("document", new Blob([fs22.readFileSync(filePath)]), name);
12040
12631
  return { method: "sendDocument", field: "document", form };
12041
12632
  }
12042
12633
  var TELEGRAM_LIMITS;
@@ -12622,6 +13213,7 @@ class TelegramChannel {
12622
13213
  updateOffset = 0;
12623
13214
  albums = new Map;
12624
13215
  inboundQueues = new Map;
13216
+ refusedAlbums = new Map;
12625
13217
  albumDebounceMs;
12626
13218
  inboundLimits;
12627
13219
  inboundEnabled;
@@ -13076,6 +13668,10 @@ ${code(prompt.line)}`,
13076
13668
  if (!msg.from || !this.config.allowedUserIds.includes(msg.from.id))
13077
13669
  return;
13078
13670
  const binding = msg.message_thread_id ? this.control.findBindingByTopic(msg.chat.id, msg.message_thread_id) : undefined;
13671
+ if (msg.message_thread_id !== undefined && binding === undefined) {
13672
+ this.log(`ignored update in topic ${msg.message_thread_id}: not this machine's`);
13673
+ return;
13674
+ }
13079
13675
  const media = readMedia(msg);
13080
13676
  if (media) {
13081
13677
  await this.handleMedia(msg, binding, media);
@@ -13108,28 +13704,47 @@ ${code(prompt.line)}`,
13108
13704
  return;
13109
13705
  }
13110
13706
  if (!binding) {
13111
- await this.refuseMedia(msg, tHtml("tg.inbound.noSession"));
13707
+ await this.refuseMedia(msg, tHtml("tg.inbound.noSession"), "no session bound to the topic");
13112
13708
  return;
13113
13709
  }
13114
13710
  const plan = planInboundAttachment(media.candidate, this.inboundLimits);
13115
13711
  if (!plan.ok) {
13116
- await this.refuseMedia(msg, tHtml(REFUSAL_KEYS[plan.reason], { limit: megabytes2(this.inboundLimits.maxBytes) }));
13712
+ const text = tHtml(REFUSAL_KEYS[plan.reason], { limit: megabytes2(this.inboundLimits.maxBytes) });
13713
+ await this.refuseMedia(msg, text, plan.reason);
13117
13714
  return;
13118
13715
  }
13119
13716
  await this.react(msg.chat?.id, msg.message_id, "received");
13120
13717
  this.bufferMedia(msg, binding.sessionId, { fileId: media.fileId, name: plan.name, kind: plan.kind, messageId: msg.message_id });
13121
13718
  }
13122
- async refuseMedia(msg, text) {
13719
+ async refuseMedia(msg, text, reason) {
13123
13720
  const chatId = msg.chat?.id;
13721
+ const where = `chat ${chatId ?? "?"} topic ${msg.message_thread_id ?? "general"}`;
13722
+ const album = msg.media_group_id !== undefined ? ` album ${msg.media_group_id}` : "";
13723
+ this.log(`inbound refused (${reason}) in ${where}${album}, message ${msg.message_id ?? "?"}`);
13124
13724
  await this.react(chatId, msg.message_id, "failed");
13125
13725
  if (chatId === undefined)
13126
13726
  return;
13727
+ if (this.albumAlreadyRefused(msg, reason))
13728
+ return;
13729
+ const attributed = msg.message_thread_id === undefined ? `[${escapeHtml(this.config.machine.name)}] ${text}` : text;
13127
13730
  await sendFormatted(this.api, {
13128
13731
  chat_id: chatId,
13129
13732
  ...msg.message_thread_id !== undefined ? { message_thread_id: msg.message_thread_id } : {},
13130
- text
13733
+ text: attributed
13131
13734
  }).catch((err) => this.log(`inbound refusal failed: ${String(err)}`));
13132
13735
  }
13736
+ albumAlreadyRefused(msg, reason) {
13737
+ const group = msg.media_group_id;
13738
+ if (group === undefined)
13739
+ return false;
13740
+ const key = `${group}\x00${reason}`;
13741
+ const pending = this.refusedAlbums.get(key);
13742
+ clearTimeout(pending);
13743
+ const timer = setTimeout(() => this.refusedAlbums.delete(key), this.albumDebounceMs);
13744
+ timer.unref?.();
13745
+ this.refusedAlbums.set(key, timer);
13746
+ return pending !== undefined;
13747
+ }
13133
13748
  bufferMedia(msg, sessionId, item) {
13134
13749
  const group = msg.media_group_id;
13135
13750
  const caption = msg.caption?.trim();
@@ -13177,8 +13792,8 @@ ${code(prompt.line)}`,
13177
13792
  throw new Error("this Telegram client cannot download files");
13178
13793
  const files = [];
13179
13794
  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 });
13795
+ const path23 = await this.control.stageInboundFile(batch.sessionId, item.name, await download2(item.fileId));
13796
+ files.push({ path: path23, kind: item.kind });
13182
13797
  }
13183
13798
  await this.control.prompt(batch.sessionId, buildAttachmentPrompt(files, batch.caption));
13184
13799
  this.log(`staged ${files.length} inbound file(s) for ${batch.sessionId}`);
@@ -13572,13 +14187,13 @@ var init_src2 = __esm(() => {
13572
14187
  });
13573
14188
 
13574
14189
  // packages/daemon/src/machine-identity.ts
13575
- import os6 from "os";
14190
+ import os8 from "os";
13576
14191
  function resolveMachineIdentity(override) {
13577
- const hostname = os6.hostname();
14192
+ const hostname = os8.hostname();
13578
14193
  return {
13579
14194
  name: slugifyMachineName(override && override.trim().length > 0 ? override : hostname),
13580
14195
  hostname,
13581
- addresses: collectAddresses(os6.networkInterfaces())
14196
+ addresses: collectAddresses(os8.networkInterfaces())
13582
14197
  };
13583
14198
  }
13584
14199
  var init_machine_identity = __esm(() => {
@@ -13808,8 +14423,8 @@ var init_telegram_service = __esm(() => {
13808
14423
  // packages/daemon/src/index.ts
13809
14424
  var exports_src = {};
13810
14425
  import { randomBytes as randomBytes3 } from "crypto";
13811
- import fs21 from "fs";
13812
- import net4 from "net";
14426
+ import fs23 from "fs";
14427
+ import net5 from "net";
13813
14428
  function isPidAlive(pid) {
13814
14429
  try {
13815
14430
  process.kill(pid, 0);
@@ -13820,9 +14435,9 @@ function isPidAlive(pid) {
13820
14435
  }
13821
14436
  async function main() {
13822
14437
  ensureDataDirs();
13823
- if (fs21.existsSync(RUNTIME_FILE2)) {
14438
+ if (fs23.existsSync(RUNTIME_FILE2)) {
13824
14439
  try {
13825
- const existing = JSON.parse(fs21.readFileSync(RUNTIME_FILE2, "utf8"));
14440
+ const existing = JSON.parse(fs23.readFileSync(RUNTIME_FILE2, "utf8"));
13826
14441
  if (existing.pid !== process.pid && isPidAlive(existing.pid)) {
13827
14442
  log(`daemon already running (pid ${existing.pid}); exiting`);
13828
14443
  process.exit(0);
@@ -13845,7 +14460,7 @@ async function main() {
13845
14460
  version: productVersion(),
13846
14461
  startedAt: Date.now()
13847
14462
  };
13848
- fs21.writeFileSync(RUNTIME_FILE2, JSON.stringify(runtime, null, 2), { mode: 384 });
14463
+ fs23.writeFileSync(RUNTIME_FILE2, JSON.stringify(runtime, null, 2), { mode: 384 });
13849
14464
  restrictFile(RUNTIME_FILE2);
13850
14465
  const config = new ConfigStore(defaultConfigStorePaths(CONFIG_DIR));
13851
14466
  if (config.loadError !== undefined)
@@ -13897,9 +14512,9 @@ async function main() {
13897
14512
  hookHost.stop();
13898
14513
  store.close();
13899
14514
  try {
13900
- const current = JSON.parse(fs21.readFileSync(RUNTIME_FILE2, "utf8"));
14515
+ const current = JSON.parse(fs23.readFileSync(RUNTIME_FILE2, "utf8"));
13901
14516
  if (current.pid === process.pid)
13902
- fs21.rmSync(RUNTIME_FILE2);
14517
+ fs23.rmSync(RUNTIME_FILE2);
13903
14518
  } catch {}
13904
14519
  process.exit(0);
13905
14520
  };
@@ -13932,14 +14547,191 @@ var init_src3 = __esm(() => {
13932
14547
  init_session_manager();
13933
14548
  init_telegram_service();
13934
14549
  init_store();
13935
- net4.setDefaultAutoSelectFamilyAttemptTimeout(3000);
14550
+ net5.setDefaultAutoSelectFamilyAttemptTimeout(3000);
13936
14551
  main();
13937
14552
  });
13938
14553
 
14554
+ // packages/daemon/src/pty-host.ts
14555
+ import net6 from "net";
14556
+ function startPtyHost(options) {
14557
+ const sessions = new Map;
14558
+ const clients = new Set;
14559
+ const send = (socket, reply) => {
14560
+ if (!socket.destroyed)
14561
+ socket.write(controlFrame(reply));
14562
+ };
14563
+ const broadcast = (frame) => {
14564
+ for (const client of clients)
14565
+ if (!client.destroyed)
14566
+ client.write(frame);
14567
+ };
14568
+ const server = net6.createServer((socket) => {
14569
+ const decoder = new FrameDecoder;
14570
+ let greeted = false;
14571
+ const drop = (reason) => {
14572
+ log(`pty host: dropping client (${reason})`);
14573
+ socket.destroy();
14574
+ clients.delete(socket);
14575
+ };
14576
+ socket.on("close", () => clients.delete(socket));
14577
+ socket.on("error", () => clients.delete(socket));
14578
+ socket.on("data", (chunk) => {
14579
+ let frames;
14580
+ try {
14581
+ frames = decoder.push(new Uint8Array(chunk));
14582
+ } catch (err) {
14583
+ drop(err instanceof Error ? err.message : String(err));
14584
+ return;
14585
+ }
14586
+ for (const frame of frames) {
14587
+ if (frame.kind === FrameKind.Data) {
14588
+ if (!greeted)
14589
+ return drop("data before hello");
14590
+ const session = sessions.get(frame.id);
14591
+ if (session && !session.exited)
14592
+ session.handle.write(new TextDecoder().decode(frame.payload));
14593
+ continue;
14594
+ }
14595
+ const message = parseControl(frame.payload);
14596
+ if (message === undefined)
14597
+ continue;
14598
+ if (message.t === "hello") {
14599
+ if (message.token !== options.token)
14600
+ return drop("bad token");
14601
+ greeted = true;
14602
+ clients.add(socket);
14603
+ send(socket, {
14604
+ t: "welcome",
14605
+ v: PTY_PROTOCOL_VERSION,
14606
+ sessions: [...sessions.entries()].filter(([, s]) => !s.exited).map(([id, s]) => ({ id, pid: s.handle.pid }))
14607
+ });
14608
+ continue;
14609
+ }
14610
+ if (!greeted)
14611
+ return drop("control before hello");
14612
+ if (message.t === "spawn") {
14613
+ try {
14614
+ const handle = spawnPty({
14615
+ exe: message.exe,
14616
+ argv: message.argv,
14617
+ cwd: message.cwd,
14618
+ cols: message.cols,
14619
+ rows: message.rows,
14620
+ env: message.env,
14621
+ term: message.term
14622
+ });
14623
+ const session = { handle, cols: message.cols, rows: message.rows, exited: false };
14624
+ sessions.set(message.id, session);
14625
+ handle.onData((text) => broadcast(encodeFrame(FrameKind.Data, message.id, new TextEncoder().encode(text))));
14626
+ handle.onExit((code2) => {
14627
+ session.exited = true;
14628
+ for (const client of clients)
14629
+ send(client, { t: "exit", id: message.id, code: code2 });
14630
+ sessions.delete(message.id);
14631
+ if (sessions.size === 0)
14632
+ options.onIdle?.();
14633
+ });
14634
+ send(socket, { t: "spawned", id: message.id, pid: handle.pid });
14635
+ } catch (err) {
14636
+ send(socket, { t: "error", id: message.id, message: err instanceof Error ? err.message : String(err) });
14637
+ }
14638
+ continue;
14639
+ }
14640
+ if (message.t === "resize") {
14641
+ const session = sessions.get(message.id);
14642
+ if (session === undefined || session.exited)
14643
+ continue;
14644
+ session.cols = message.cols;
14645
+ session.rows = message.rows;
14646
+ session.handle.resize(message.cols, message.rows);
14647
+ continue;
14648
+ }
14649
+ if (message.t === "kill") {
14650
+ sessions.get(message.id)?.handle.kill();
14651
+ }
14652
+ }
14653
+ });
14654
+ });
14655
+ return new Promise((resolve, reject) => {
14656
+ server.once("error", reject);
14657
+ server.listen(options.endpoint, () => {
14658
+ server.removeListener("error", reject);
14659
+ log(`pty host: listening on ${options.endpoint}`);
14660
+ resolve({
14661
+ endpoint: options.endpoint,
14662
+ get sessionCount() {
14663
+ return sessions.size;
14664
+ },
14665
+ close: async () => {
14666
+ for (const client of clients)
14667
+ client.destroy();
14668
+ clients.clear();
14669
+ for (const session of sessions.values())
14670
+ session.handle.kill();
14671
+ await new Promise((done) => server.close(() => done()));
14672
+ }
14673
+ });
14674
+ });
14675
+ });
14676
+ }
14677
+ var init_pty_host = __esm(() => {
14678
+ init_src();
14679
+ init_paths();
14680
+ init_pty();
14681
+ });
14682
+
14683
+ // packages/daemon/src/pty-host-main.ts
14684
+ var exports_pty_host_main = {};
14685
+ __export(exports_pty_host_main, {
14686
+ runPtyHostProcess: () => runPtyHostProcess,
14687
+ PTY_HOST_IDLE_MS: () => PTY_HOST_IDLE_MS
14688
+ });
14689
+ async function runPtyHostProcess() {
14690
+ const endpoint = process.env["AXTERM_PTY_HOST_ENDPOINT"];
14691
+ const token = process.env["AXTERM_PTY_HOST_TOKEN"];
14692
+ const file = process.env["AXTERM_PTY_HOST_FILE"];
14693
+ if (!endpoint || !token || !file) {
14694
+ console.error("axterm __pty-host: AXTERM_PTY_HOST_ENDPOINT, _TOKEN and _FILE are required");
14695
+ process.exitCode = 2;
14696
+ return;
14697
+ }
14698
+ let idleTimer;
14699
+ const server = await startPtyHost({
14700
+ endpoint,
14701
+ token,
14702
+ onIdle: () => armIdleExit()
14703
+ });
14704
+ function armIdleExit() {
14705
+ clearTimeout(idleTimer);
14706
+ idleTimer = setTimeout(() => {
14707
+ if (server.sessionCount > 0)
14708
+ return armIdleExit();
14709
+ log("pty host: idle with no sessions, exiting");
14710
+ server.close().then(() => process.exit(0));
14711
+ }, PTY_HOST_IDLE_MS);
14712
+ idleTimer.unref?.();
14713
+ }
14714
+ writePtyHostFile(file, { pid: process.pid, endpoint, token, version: PTY_PROTOCOL_VERSION });
14715
+ log(`pty host: ready on ${endpoint} (pid ${process.pid})`);
14716
+ armIdleExit();
14717
+ for (const signal of ["SIGINT", "SIGTERM"]) {
14718
+ process.on(signal, () => {
14719
+ server.close().then(() => process.exit(0));
14720
+ });
14721
+ }
14722
+ }
14723
+ var PTY_HOST_IDLE_MS = 60000;
14724
+ var init_pty_host_main = __esm(() => {
14725
+ init_src();
14726
+ init_paths();
14727
+ init_pty_host_launch();
14728
+ init_pty_host();
14729
+ });
14730
+
13939
14731
  // packages/cli/src/index.ts
13940
14732
  init_src();
13941
- import fs22 from "fs";
13942
- import path21 from "path";
14733
+ import fs24 from "fs";
14734
+ import path23 from "path";
13943
14735
 
13944
14736
  // packages/cli/src/attach.ts
13945
14737
  import fs from "fs";
@@ -14861,7 +15653,7 @@ import { stdin as stdin2, stdout as stdout2 } from "process";
14861
15653
  // packages/cli/package.json
14862
15654
  var package_default = {
14863
15655
  name: "@layorixinc/axterm",
14864
- version: "0.4.0",
15656
+ version: "0.5.0",
14865
15657
  type: "module",
14866
15658
  bin: {
14867
15659
  axterm: "dist/cli.js"
@@ -15183,7 +15975,7 @@ function printSessionTable(sessions) {
15183
15975
  console.log(row.map((cell, col) => cell.padEnd(widths[col])).join(" "));
15184
15976
  }
15185
15977
  }
15186
- var realPath = (target) => fs22.realpathSync.native(target);
15978
+ var realPath = (target) => fs24.realpathSync.native(target);
15187
15979
  function scopedHere(sessions) {
15188
15980
  return sessionsInScope(sessions, process.cwd(), realPath);
15189
15981
  }
@@ -15196,7 +15988,7 @@ var help = () => t("cli.help");
15196
15988
  function readDotEnv(file = ".env") {
15197
15989
  const out = {};
15198
15990
  try {
15199
- for (const line of fs22.readFileSync(file, "utf8").split(/\r?\n/)) {
15991
+ for (const line of fs24.readFileSync(file, "utf8").split(/\r?\n/)) {
15200
15992
  const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
15201
15993
  if (m)
15202
15994
  out[m[1]] = m[2].trim();
@@ -15207,7 +15999,7 @@ function readDotEnv(file = ".env") {
15207
15999
  function configuredLocale() {
15208
16000
  try {
15209
16001
  const file = resolvePaths(currentPathEnv()).configFile;
15210
- const raw = JSON.parse(fs22.readFileSync(file, "utf8"));
16002
+ const raw = JSON.parse(fs24.readFileSync(file, "utf8"));
15211
16003
  return typeof raw.locale === "string" ? raw.locale : undefined;
15212
16004
  } catch {
15213
16005
  return;
@@ -15228,6 +16020,11 @@ async function main2() {
15228
16020
  runMcpServer();
15229
16021
  return;
15230
16022
  }
16023
+ if (isPtyHostInvocation(argv)) {
16024
+ const { runPtyHostProcess: runPtyHostProcess2 } = await Promise.resolve().then(() => (init_pty_host_main(), exports_pty_host_main));
16025
+ await runPtyHostProcess2();
16026
+ return;
16027
+ }
15231
16028
  const [command, ...rest] = argv;
15232
16029
  setLocale(resolveLocale({ configured: configuredLocale(), env: process.env }));
15233
16030
  const { flags, positional, passthrough } = parseArgs(rest);
@@ -15429,8 +16226,8 @@ async function main2() {
15429
16226
  case "project": {
15430
16227
  const sub = positional[0] ?? "list";
15431
16228
  if (sub === "add") {
15432
- const dir = positional[1] ? path21.resolve(positional[1]) : process.cwd();
15433
- const alias = positional[2] ?? path21.basename(dir);
16229
+ const dir = positional[1] ? path23.resolve(positional[1]) : process.cwd();
16230
+ const alias = positional[2] ?? path23.basename(dir);
15434
16231
  const projects = await client.call("project.add", {
15435
16232
  alias,
15436
16233
  path: dir