@linzumi/cli 0.0.38-beta → 0.0.40-beta

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +1695 -278
  3. package/package.json +5 -2
package/dist/index.js CHANGED
@@ -1,15 +1,16 @@
1
1
  // src/index.ts
2
2
  import { randomUUID as randomUUID3 } from "node:crypto";
3
- import { existsSync as existsSync10, readFileSync as readFileSync9, realpathSync as realpathSync5 } from "node:fs";
3
+ import { existsSync as existsSync10, readFileSync as readFileSync9, realpathSync as realpathSync6 } from "node:fs";
4
4
  import { homedir as homedir9 } from "node:os";
5
- import { resolve as resolve8 } from "node:path";
5
+ import { resolve as resolve9 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7
7
 
8
8
  // src/runner.ts
9
9
  import { spawn as spawn6 } from "node:child_process";
10
10
  import { randomUUID as randomUUID2 } from "node:crypto";
11
+ import { realpathSync as realpathSync4 } from "node:fs";
11
12
  import { hostname as hostname2 } from "node:os";
12
- import { join as join6 } from "node:path";
13
+ import { join as join6, resolve as resolve5 } from "node:path";
13
14
 
14
15
  // src/channelSessionSupport.ts
15
16
  import { spawnSync } from "node:child_process";
@@ -149,19 +150,34 @@ function senderAllowed(listenUser, event, runnerIdentity) {
149
150
  return runnerIdentity.actorUsername?.toLocaleLowerCase() === normalized && runnerIdentity.actorUserId !== undefined && event.actorUserId === runnerIdentity.actorUserId;
150
151
  }
151
152
  function identityFromAccessToken(token) {
153
+ const decoded = decodeAccessTokenPayload(token);
154
+ if (decoded === undefined) {
155
+ return { actorUserId: undefined, actorUsername: undefined };
156
+ }
157
+ const actorUserId = integerValue(decoded.actor_id) ?? integerValue(decoded.sub);
158
+ return {
159
+ actorUserId,
160
+ actorUsername: stringValue(decoded.actor_username)
161
+ };
162
+ }
163
+ function singleWorkspaceScopeFromAccessToken(token) {
164
+ const decoded = decodeAccessTokenPayload(token);
165
+ const workspaceScope = (arrayValue(decoded?.workspace_scope) ?? []).flatMap((value) => {
166
+ const workspace = stringValue(value)?.trim();
167
+ return workspace === undefined || workspace === "" ? [] : [workspace];
168
+ });
169
+ return workspaceScope.length === 1 ? workspaceScope[0] : undefined;
170
+ }
171
+ function decodeAccessTokenPayload(token) {
152
172
  const [, payload] = token.split(".");
153
173
  if (payload === undefined) {
154
- return { actorUserId: undefined, actorUsername: undefined };
174
+ return;
155
175
  }
156
176
  try {
157
177
  const decoded = JSON.parse(Buffer.from(base64UrlToBase64(payload), "base64").toString("utf8"));
158
- const actorUserId = integerValue(decoded.actor_id) ?? integerValue(decoded.sub);
159
- return {
160
- actorUserId,
161
- actorUsername: stringValue(decoded.actor_username)
162
- };
178
+ return isJsonObject(decoded) ? decoded : undefined;
163
179
  } catch (_error) {
164
- return { actorUserId: undefined, actorUsername: undefined };
180
+ return;
165
181
  }
166
182
  }
167
183
  function detectCodexVersion(codexBin, cwd) {
@@ -801,6 +817,203 @@ function fuseQueuedMessages(selected) {
801
817
  };
802
818
  }
803
819
 
820
+ // src/reconnectContext.ts
821
+ var recentVerbatimMessageCount = 5;
822
+ var reconnectContextModel = "openai/gpt-oss-120b";
823
+ var noiseLocalRunnerEventTypes = new Set([
824
+ "availability",
825
+ "port_forward_requested",
826
+ "port_forward_ready",
827
+ "port_forward_resolved",
828
+ "settings",
829
+ "status",
830
+ "local_runner_config_seeded",
831
+ "local_runner_config_updated"
832
+ ]);
833
+ function parseReconnectContextMessages(value) {
834
+ if (!Array.isArray(value)) {
835
+ return [];
836
+ }
837
+ return value.flatMap(messageFromWirePayload);
838
+ }
839
+ function filterReconnectContextMessages(messages) {
840
+ return messages.filter((message) => {
841
+ const trimmed = message.body.trim();
842
+ if (trimmed === "") {
843
+ return false;
844
+ }
845
+ if (message.actorKind === "system") {
846
+ return false;
847
+ }
848
+ const eventType = localRunnerEventType2(message.payload);
849
+ if (eventType !== undefined && eventType !== "codex_output") {
850
+ return false;
851
+ }
852
+ if (looksLikeLegacyRunnerNoise(message)) {
853
+ return false;
854
+ }
855
+ return true;
856
+ });
857
+ }
858
+ async function buildReconnectContextInjection(messages, summarizer = createConfiguredReconnectContextSummarizer()) {
859
+ const filtered = filterReconnectContextMessages(messages);
860
+ if (filtered.length === 0) {
861
+ throw new Error("durable Linzumi thread history did not include user-visible context");
862
+ }
863
+ const older = filtered.slice(0, Math.max(0, filtered.length - recentVerbatimMessageCount));
864
+ const recent = filtered.slice(-recentVerbatimMessageCount);
865
+ const summary = older.length === 0 ? undefined : await summarizeOlderReconnectContext(older, summarizer);
866
+ const sections = [
867
+ "Reconnected Linzumi thread context",
868
+ "",
869
+ "The local Codex app-server thread was restarted, so this durable Linzumi thread history is being injected before retrying the latest user message.",
870
+ "",
871
+ ...summary === undefined ? [] : ["Summary of earlier messages:", summary, ""],
872
+ "Last five user-visible messages verbatim:",
873
+ recent.map(formatReconnectContextMessage).join(`
874
+
875
+ ---
876
+
877
+ `)
878
+ ];
879
+ return { type: "text", text: sections.join(`
880
+ `) };
881
+ }
882
+ function createConfiguredReconnectContextSummarizer(fetchImpl = globalThis.fetch) {
883
+ return {
884
+ summarize: (messages) => summarizeWithOpenAiCompatibleProvider(fetchImpl, messages)
885
+ };
886
+ }
887
+ async function summarizeOlderReconnectContext(messages, summarizer) {
888
+ const summary = (await summarizer.summarize(messages)).trim();
889
+ if (summary === "") {
890
+ throw new Error("reconnect context summarizer returned an empty summary");
891
+ }
892
+ return summary;
893
+ }
894
+ async function summarizeWithOpenAiCompatibleProvider(fetchImpl, messages) {
895
+ const provider = configuredReconnectContextProvider();
896
+ if (provider === undefined) {
897
+ throw new Error("Reconnect context summarization requires LINZUMI_RECONNECT_CONTEXT_API_KEY, GROQ_API_KEY, or OPENROUTER_API_KEY");
898
+ }
899
+ const model = envString("LINZUMI_RECONNECT_CONTEXT_MODEL") ?? envString("KANDAN_AGENT_PROGRESS_SUMMARY_MODEL") ?? reconnectContextModel;
900
+ const response = await fetchImpl(`${provider.baseUrl.replace(/\/$/, "")}/chat/completions`, {
901
+ method: "POST",
902
+ headers: {
903
+ authorization: `Bearer ${provider.apiKey}`,
904
+ "content-type": "application/json"
905
+ },
906
+ body: JSON.stringify({
907
+ model,
908
+ temperature: 0,
909
+ messages: [
910
+ {
911
+ role: "system",
912
+ content: "Summarize durable Linzumi thread history for a restarted local Codex coding agent. Preserve concrete user requests, constraints, file/module names, and decisions. Do not mention local runner availability, reconnect status, or port-forward noise."
913
+ },
914
+ {
915
+ role: "user",
916
+ content: messages.map(formatReconnectContextMessage).join(`
917
+
918
+ ---
919
+
920
+ `)
921
+ }
922
+ ]
923
+ })
924
+ });
925
+ if (!response.ok) {
926
+ throw new Error(`reconnect context summarization failed: ${response.status} ${response.statusText}`);
927
+ }
928
+ const parsed = await response.json();
929
+ if (!isJsonObject(parsed)) {
930
+ throw new Error("reconnect context summarizer returned non-object JSON");
931
+ }
932
+ const choices = parsed.choices;
933
+ if (!Array.isArray(choices)) {
934
+ throw new Error("reconnect context summarizer response did not include choices");
935
+ }
936
+ const first = choices.find(isJsonObject);
937
+ const message = objectValue(first?.message);
938
+ const content = stringValue(message?.content)?.trim();
939
+ if (content === undefined || content === "") {
940
+ throw new Error("reconnect context summarizer response did not include message content");
941
+ }
942
+ return content;
943
+ }
944
+ function configuredReconnectContextProvider() {
945
+ const linzumiApiKey = envString("LINZUMI_RECONNECT_CONTEXT_API_KEY");
946
+ if (linzumiApiKey !== undefined) {
947
+ return {
948
+ apiKey: linzumiApiKey,
949
+ baseUrl: envString("LINZUMI_RECONNECT_CONTEXT_BASE_URL") ?? "https://api.groq.com/openai/v1"
950
+ };
951
+ }
952
+ const groqApiKey = envString("GROQ_API_KEY");
953
+ if (groqApiKey !== undefined) {
954
+ return {
955
+ apiKey: groqApiKey,
956
+ baseUrl: envString("GROQ_BASE_URL") ?? "https://api.groq.com/openai/v1"
957
+ };
958
+ }
959
+ const openRouterApiKey = envString("OPENROUTER_API_KEY");
960
+ if (openRouterApiKey !== undefined) {
961
+ return {
962
+ apiKey: openRouterApiKey,
963
+ baseUrl: envString("OPENROUTER_BASE_URL") ?? "https://openrouter.ai/api/v1"
964
+ };
965
+ }
966
+ return;
967
+ }
968
+ function messageFromWirePayload(value) {
969
+ if (!isJsonObject(value)) {
970
+ return [];
971
+ }
972
+ const seq = integerValue(value.seq);
973
+ const payload = objectValue(value.payload) ?? {};
974
+ const body = stringValue(value.body) ?? stringValue(payload.body) ?? stringValue(payload.text);
975
+ const type = stringValue(value.type) ?? "thread.message";
976
+ if (seq === undefined || body === undefined) {
977
+ return [];
978
+ }
979
+ const actor = objectValue(value.actor) ?? {};
980
+ return [
981
+ {
982
+ seq,
983
+ type,
984
+ actorKind: stringValue(actor.kind) ?? "unknown",
985
+ actorSlug: stringValue(actor.slug),
986
+ actorUserId: integerValue(value.actor_user_id),
987
+ body,
988
+ payload
989
+ }
990
+ ];
991
+ }
992
+ function formatReconnectContextMessage(message) {
993
+ const sender = message.actorSlug?.trim() === "" || message.actorSlug === undefined ? message.actorKind : message.actorSlug;
994
+ const userId = message.actorUserId === undefined ? "unknown" : message.actorUserId.toString();
995
+ return [
996
+ `Linzumi durable message seq=${message.seq} from ${sender} (kind=${message.actorKind}, user_id=${userId}):`,
997
+ "",
998
+ message.body
999
+ ].join(`
1000
+ `);
1001
+ }
1002
+ function localRunnerEventType2(payload) {
1003
+ const metadata = objectValue(payload.metadata);
1004
+ const localRunner = objectValue(metadata?.local_codex_runner);
1005
+ const eventType = stringValue(localRunner?.event_type);
1006
+ return eventType === undefined || eventType.trim() === "" ? undefined : eventType.trim();
1007
+ }
1008
+ function looksLikeLegacyRunnerNoise(message) {
1009
+ const body = message.body.trim();
1010
+ return body.startsWith("Codex reconnected.") || body.startsWith("[codex reconnected") || body.startsWith("Codex is ready.") || /^Runner port \d+ is open in (Kandan|Linzumi):/.test(body) || /^.+ on port \d+ is ready in Linzumi\. \[Open\]\(/.test(body);
1011
+ }
1012
+ function envString(name) {
1013
+ const value = process.env[name];
1014
+ return value === undefined || value.trim() === "" ? undefined : value.trim();
1015
+ }
1016
+
804
1017
  // src/localCodexMessageState.ts
805
1018
  function codexApprovalMessageState(request) {
806
1019
  const params = objectValue(request.params) ?? {};
@@ -843,7 +1056,11 @@ function processingReasonForCodexNotification(method, params) {
843
1056
  function processingMessageStateFromActive(state) {
844
1057
  switch (state.reason) {
845
1058
  case "awaiting approval":
846
- return { status: "processing", reason: state.reason, approval: state.approval };
1059
+ return {
1060
+ status: "processing",
1061
+ reason: state.reason,
1062
+ approval: state.approval
1063
+ };
847
1064
  case "starting turn":
848
1065
  case "streaming response":
849
1066
  case "running terminal command":
@@ -1151,14 +1368,17 @@ var defaultIntervalMs = 2000;
1151
1368
  var defaultDebounceMs = 750;
1152
1369
  function startPortForwardWatcher(options) {
1153
1370
  const rootPid = options.rootPid ?? process.pid;
1371
+ const rootCwd = normalizeCwd(options.rootCwd);
1154
1372
  const intervalMs = options.intervalMs ?? defaultIntervalMs;
1155
1373
  const debounceMs = options.debounceMs ?? defaultDebounceMs;
1374
+ const lostDebounceMs = options.lostDebounceMs ?? intervalMs * 2 + debounceMs;
1156
1375
  const scanProcesses = options.scanProcesses ?? readProcessRows;
1157
1376
  const scanListenSockets = options.scanListenSockets ?? readListenSocketRows;
1158
1377
  const scanProcessCwds = options.scanProcessCwds ?? readProcessCwdRows;
1159
1378
  const nowMs = options.nowMs ?? Date.now;
1160
1379
  const candidateStabilityByPort = new Map;
1161
1380
  const emittedByPort = new Map;
1381
+ const missingByPort = new Map;
1162
1382
  const inFlight = { value: false };
1163
1383
  const scan = () => {
1164
1384
  if (inFlight.value) {
@@ -1168,18 +1388,26 @@ function startPortForwardWatcher(options) {
1168
1388
  Promise.resolve().then(async () => {
1169
1389
  const descendants = descendantPidSet(scanProcesses(), rootPid);
1170
1390
  const sockets = scanListenSockets();
1171
- const candidatePids = sockets.filter((socket) => descendants.has(socket.pid)).map((socket) => socket.pid);
1172
- const candidates = detectedForwardCandidates(sockets, descendants, scanProcessCwds(candidatePids));
1173
- const stable = stableForwardCandidates(candidateStabilityByPort, candidates, nowMs(), debounceMs);
1174
- const changes = changedForwardCandidates(emittedByPort, stable.stableCandidates);
1391
+ const candidatePids = rootCwd === undefined ? sockets.filter((socket) => descendants.has(socket.pid)).map((socket) => socket.pid) : sockets.map((socket) => socket.pid);
1392
+ const candidates = detectedForwardCandidates(sockets, descendants, scanProcessCwds(candidatePids), { rootCwd });
1393
+ const scanTimeMs = nowMs();
1394
+ const stable = stableForwardCandidates(candidateStabilityByPort, candidates, scanTimeMs, debounceMs);
1395
+ const changes = debouncedForwardCandidateChanges(emittedByPort, missingByPort, stable.stableCandidates, scanTimeMs, lostDebounceMs);
1175
1396
  candidateStabilityByPort.clear();
1176
1397
  emittedByPort.clear();
1398
+ missingByPort.clear();
1177
1399
  for (const [port, observed] of stable.nextObservedByPort) {
1178
1400
  candidateStabilityByPort.set(port, observed);
1179
1401
  }
1180
1402
  for (const [port, candidate] of changes.nextObservedByPort) {
1181
1403
  emittedByPort.set(port, candidate);
1182
1404
  }
1405
+ for (const [port, missing] of changes.nextMissingByPort) {
1406
+ missingByPort.set(port, missing);
1407
+ }
1408
+ for (const candidate of changes.lostCandidates) {
1409
+ await options.onCandidateLost?.(candidate);
1410
+ }
1183
1411
  for (const candidate of changes.changedCandidates) {
1184
1412
  await options.onCandidate(candidate);
1185
1413
  }
@@ -1195,9 +1423,11 @@ function startPortForwardWatcher(options) {
1195
1423
  close: () => clearInterval(interval)
1196
1424
  };
1197
1425
  }
1198
- function changedForwardCandidates(previousObservedByPort, candidates) {
1426
+ function debouncedForwardCandidateChanges(previousObservedByPort, previousMissingByPort, candidates, nowMs, lostDebounceMs) {
1199
1427
  const nextObservedByPort = new Map;
1428
+ const nextMissingByPort = new Map;
1200
1429
  const changedCandidates = [];
1430
+ const lostCandidates = [];
1201
1431
  for (const candidate of candidates) {
1202
1432
  nextObservedByPort.set(candidate.port, candidate);
1203
1433
  const previous = previousObservedByPort.get(candidate.port);
@@ -1205,7 +1435,28 @@ function changedForwardCandidates(previousObservedByPort, candidates) {
1205
1435
  changedCandidates.push(candidate);
1206
1436
  }
1207
1437
  }
1208
- return { nextObservedByPort, changedCandidates };
1438
+ for (const previous of previousObservedByPort.values()) {
1439
+ if (nextObservedByPort.has(previous.port)) {
1440
+ continue;
1441
+ }
1442
+ const previousMissing = previousMissingByPort.get(previous.port);
1443
+ const firstMissingAtMs = previousMissing !== undefined && sameForwardCandidate(previousMissing.candidate, previous) ? previousMissing.firstMissingAtMs : nowMs;
1444
+ if (lostDebounceMs <= 0 || nowMs - firstMissingAtMs >= lostDebounceMs) {
1445
+ lostCandidates.push(previous);
1446
+ continue;
1447
+ }
1448
+ nextObservedByPort.set(previous.port, previous);
1449
+ nextMissingByPort.set(previous.port, {
1450
+ candidate: previous,
1451
+ firstMissingAtMs
1452
+ });
1453
+ }
1454
+ return {
1455
+ nextObservedByPort,
1456
+ nextMissingByPort,
1457
+ changedCandidates,
1458
+ lostCandidates
1459
+ };
1209
1460
  }
1210
1461
  function stableForwardCandidates(previousObservedByPort, candidates, nowMs, debounceMs) {
1211
1462
  const nextObservedByPort = new Map;
@@ -1242,8 +1493,17 @@ function descendantPidSet(rows, rootPid) {
1242
1493
  }
1243
1494
  return descendants;
1244
1495
  }
1245
- function detectedForwardCandidates(sockets, descendantPids, processCwds = new Map) {
1246
- return sockets.filter((socket) => descendantPids.has(socket.pid)).filter((socket) => socket.port > 0 && socket.port < 65536).sort((left, right) => left.port - right.port).map((socket) => {
1496
+ function detectedForwardCandidates(sockets, descendantPids, processCwds = new Map, options = {}) {
1497
+ const rootCwd = normalizeCwd(options.rootCwd);
1498
+ return sockets.filter((socket) => {
1499
+ if (descendantPids.has(socket.pid)) {
1500
+ return true;
1501
+ }
1502
+ if (rootCwd === undefined) {
1503
+ return false;
1504
+ }
1505
+ return cwdMatchesRoot(processCwds.get(socket.pid), rootCwd);
1506
+ }).filter((socket) => socket.port > 0 && socket.port < 65536).sort((left, right) => left.port - right.port).map((socket) => {
1247
1507
  const cwd = processCwds.get(socket.pid);
1248
1508
  return {
1249
1509
  port: socket.port,
@@ -1253,6 +1513,20 @@ function detectedForwardCandidates(sockets, descendantPids, processCwds = new Ma
1253
1513
  };
1254
1514
  });
1255
1515
  }
1516
+ function normalizeCwd(cwd) {
1517
+ if (cwd === undefined) {
1518
+ return;
1519
+ }
1520
+ const normalized = cwd.trim().replace(/\/+$/, "");
1521
+ return normalized === "" ? undefined : normalized;
1522
+ }
1523
+ function cwdMatchesRoot(candidateCwd, rootCwd) {
1524
+ const normalizedCandidate = normalizeCwd(candidateCwd);
1525
+ if (normalizedCandidate === undefined) {
1526
+ return false;
1527
+ }
1528
+ return normalizedCandidate === rootCwd || normalizedCandidate.startsWith(`${rootCwd}/`);
1529
+ }
1256
1530
  function parseProcessRows(output) {
1257
1531
  return output.split(`
1258
1532
  `).map((line) => line.trim()).filter((line) => line !== "").map((line) => {
@@ -1329,7 +1603,9 @@ function readProcessRows() {
1329
1603
  return parseProcessRows(result.stdout);
1330
1604
  }
1331
1605
  function readListenSocketRows() {
1332
- const result = spawnSync2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPcn"], { encoding: "utf8" });
1606
+ const result = spawnSync2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPcn"], {
1607
+ encoding: "utf8"
1608
+ });
1333
1609
  if (result.error !== undefined) {
1334
1610
  throw result.error;
1335
1611
  }
@@ -1430,22 +1706,6 @@ function approvedTargetFromRequest(request) {
1430
1706
  function portForwardPromptLabel(candidate) {
1431
1707
  return commandLabel(candidate.command);
1432
1708
  }
1433
- function portForwardPromptBody(candidate, requestId) {
1434
- const label = portForwardPromptLabel(candidate);
1435
- const cwdLine = candidate.cwd === undefined ? undefined : `Working directory: ${candidate.cwd}`;
1436
- return [
1437
- `Detected ${label} listening from a descendant process.`,
1438
- `Port: ${candidate.port}`,
1439
- `PID: ${candidate.pid}`,
1440
- `Command: ${candidate.command}`,
1441
- ...cwdLine === undefined ? [] : [cwdLine],
1442
- "Kandan can open this as an authenticated HTTP, HTTPS, or WebSocket preview. It does not expose arbitrary TCP or UDP protocols.",
1443
- "Open this preview in Kandan?",
1444
- "",
1445
- `Fallback commands: ${portForwardDecisionCommand("approve", requestId)} or ${portForwardDecisionCommand("deny", requestId)}`
1446
- ].join(`
1447
- `);
1448
- }
1449
1709
  function portForwardPromptReason(candidate) {
1450
1710
  return [
1451
1711
  `Port ${candidate.port}`,
@@ -1455,15 +1715,16 @@ function portForwardPromptReason(candidate) {
1455
1715
  "preview protocols: HTTP, HTTPS, WebSocket"
1456
1716
  ].join(" / ");
1457
1717
  }
1458
- function portForwardDecisionCommand(decision, requestId) {
1459
- return `/kandan ${decision}-port-forward ${requestId}`;
1460
- }
1461
1718
  function forwardPreviewPath(runnerId, port) {
1462
1719
  return `/local-codex-runners/${encodeURIComponent(runnerId)}/forwards/${port}/preview`;
1463
1720
  }
1464
1721
  function revocationCapabilities(capabilities, port) {
1722
+ const existingAllowedPorts = Array.isArray(capabilities?.allowedPorts) ? capabilities.allowedPorts.filter((value) => typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535) : [];
1723
+ const allowedPorts = existingAllowedPorts.filter((allowedPort) => allowedPort !== port);
1465
1724
  return {
1466
1725
  ...capabilities ?? {},
1726
+ portForwarding: allowedPorts.length > 0,
1727
+ allowedPorts,
1467
1728
  revokedPorts: [port]
1468
1729
  };
1469
1730
  }
@@ -1474,6 +1735,608 @@ function isInternalCodexProcess(candidate) {
1474
1735
  return commandLabel(candidate.command) === "codex";
1475
1736
  }
1476
1737
 
1738
+ // src/processNameCatalog.ts
1739
+ var processCatalogEntries = [
1740
+ { appName: "Bun", iconKey: "bun", names: ["bun", "bunx"] },
1741
+ {
1742
+ appName: "Node.js",
1743
+ iconKey: "nodejs",
1744
+ names: ["node", "nodejs", "node.exe"]
1745
+ },
1746
+ { appName: "npm", iconKey: "npm", names: ["npm", "npm-cli.js", "npx"] },
1747
+ { appName: "pnpm", iconKey: "pnpm", names: ["pnpm", "pnpx"] },
1748
+ { appName: "Yarn", iconKey: "yarn", names: ["yarn", "yarnpkg", "yarn.js"] },
1749
+ { appName: "Corepack", iconKey: "corepack", names: ["corepack"] },
1750
+ { appName: "Deno", iconKey: "deno", names: ["deno"] },
1751
+ { appName: "Vite", iconKey: "vite", names: ["vite", "vite-node"] },
1752
+ { appName: "Vitest", iconKey: "vitest", names: ["vitest"] },
1753
+ { appName: "Next.js", iconKey: "nextjs", names: ["next", "next-server"] },
1754
+ { appName: "Nuxt", iconKey: "nuxt", names: ["nuxt", "nuxi"] },
1755
+ { appName: "Astro", iconKey: "astro", names: ["astro"] },
1756
+ {
1757
+ appName: "SvelteKit",
1758
+ iconKey: "svelte",
1759
+ names: ["svelte-kit", "sveltekit"]
1760
+ },
1761
+ { appName: "Remix", iconKey: "remix", names: ["remix"] },
1762
+ { appName: "Expo", iconKey: "expo", names: ["expo", "expo-cli"] },
1763
+ { appName: "React Native", iconKey: "react", names: ["react-native"] },
1764
+ {
1765
+ appName: "Metro",
1766
+ iconKey: "metro",
1767
+ names: ["metro", "metro-inspector-proxy"]
1768
+ },
1769
+ {
1770
+ appName: "Webpack",
1771
+ iconKey: "webpack",
1772
+ names: ["webpack", "webpack-dev-server", "webpack-cli"]
1773
+ },
1774
+ { appName: "Parcel", iconKey: "parcel", names: ["parcel"] },
1775
+ { appName: "Rollup", iconKey: "rollup", names: ["rollup"] },
1776
+ { appName: "esbuild", iconKey: "esbuild", names: ["esbuild"] },
1777
+ { appName: "SWC", iconKey: "swc", names: ["swc"] },
1778
+ {
1779
+ appName: "TypeScript",
1780
+ iconKey: "typescript",
1781
+ names: ["tsc", "tsserver", "tsx", "ts-node", "typescript-language-server"]
1782
+ },
1783
+ { appName: "ESLint", iconKey: "eslint", names: ["eslint", "eslint_d"] },
1784
+ { appName: "Prettier", iconKey: "prettier", names: ["prettier"] },
1785
+ { appName: "Biome", iconKey: "biome", names: ["biome"] },
1786
+ {
1787
+ appName: "Storybook",
1788
+ iconKey: "storybook",
1789
+ names: ["storybook", "start-storybook"]
1790
+ },
1791
+ { appName: "Playwright", iconKey: "playwright", names: ["playwright"] },
1792
+ { appName: "Cypress", iconKey: "cypress", names: ["cypress"] },
1793
+ { appName: "Jest", iconKey: "jest", names: ["jest"] },
1794
+ {
1795
+ appName: "Python",
1796
+ iconKey: "python",
1797
+ names: [
1798
+ "python",
1799
+ "python2",
1800
+ "python3",
1801
+ "python3.9",
1802
+ "python3.10",
1803
+ "python3.11",
1804
+ "python3.12",
1805
+ "python3.13",
1806
+ "pypy",
1807
+ "pypy3"
1808
+ ]
1809
+ },
1810
+ { appName: "pip", iconKey: "python", names: ["pip", "pip2", "pip3"] },
1811
+ { appName: "pipx", iconKey: "python", names: ["pipx"] },
1812
+ { appName: "uv", iconKey: "python", names: ["uv", "uvx"] },
1813
+ { appName: "Poetry", iconKey: "poetry", names: ["poetry"] },
1814
+ { appName: "Pipenv", iconKey: "python", names: ["pipenv"] },
1815
+ {
1816
+ appName: "Conda",
1817
+ iconKey: "conda",
1818
+ names: ["conda", "mamba", "micromamba"]
1819
+ },
1820
+ {
1821
+ appName: "Django",
1822
+ iconKey: "django",
1823
+ names: ["django-admin", "django-admin.py", "manage.py"]
1824
+ },
1825
+ { appName: "Flask", iconKey: "flask", names: ["flask"] },
1826
+ { appName: "Uvicorn", iconKey: "uvicorn", names: ["uvicorn"] },
1827
+ { appName: "Gunicorn", iconKey: "gunicorn", names: ["gunicorn"] },
1828
+ { appName: "Hypercorn", iconKey: "python", names: ["hypercorn"] },
1829
+ { appName: "Celery", iconKey: "celery", names: ["celery"] },
1830
+ {
1831
+ appName: "Jupyter",
1832
+ iconKey: "jupyter",
1833
+ names: ["jupyter", "jupyter-lab", "jupyter-notebook", "ipython"]
1834
+ },
1835
+ { appName: "Ruby", iconKey: "ruby", names: ["ruby", "erb", "irb"] },
1836
+ { appName: "Bundler", iconKey: "ruby", names: ["bundle", "bundler"] },
1837
+ { appName: "Rails", iconKey: "rails", names: ["rails", "bin/rails"] },
1838
+ { appName: "Rack", iconKey: "ruby", names: ["rackup"] },
1839
+ { appName: "Puma", iconKey: "puma", names: ["puma"] },
1840
+ { appName: "Sidekiq", iconKey: "sidekiq", names: ["sidekiq"] },
1841
+ { appName: "RSpec", iconKey: "ruby", names: ["rspec"] },
1842
+ { appName: "Go", iconKey: "go", names: ["go", "gofmt", "gopls", "air"] },
1843
+ { appName: "Delve", iconKey: "go", names: ["dlv"] },
1844
+ {
1845
+ appName: "Rust",
1846
+ iconKey: "rust",
1847
+ names: ["rustc", "rustup", "cargo", "rust-analyzer"]
1848
+ },
1849
+ {
1850
+ appName: "Java",
1851
+ iconKey: "java",
1852
+ names: ["java", "javac", "jar", "jshell", "jwebserver"]
1853
+ },
1854
+ { appName: "Gradle", iconKey: "gradle", names: ["gradle", "gradlew"] },
1855
+ { appName: "Maven", iconKey: "maven", names: ["mvn", "mvnw"] },
1856
+ {
1857
+ appName: "Spring Boot",
1858
+ iconKey: "spring",
1859
+ names: ["spring", "spring-boot"]
1860
+ },
1861
+ {
1862
+ appName: "Kotlin",
1863
+ iconKey: "kotlin",
1864
+ names: ["kotlin", "kotlinc", "kotlin-language-server"]
1865
+ },
1866
+ {
1867
+ appName: "Scala",
1868
+ iconKey: "scala",
1869
+ names: ["scala", "scalac", "sbt", "metals"]
1870
+ },
1871
+ {
1872
+ appName: "Clojure",
1873
+ iconKey: "clojure",
1874
+ names: ["clojure", "clj", "lein", "boot"]
1875
+ },
1876
+ {
1877
+ appName: "PHP",
1878
+ iconKey: "php",
1879
+ names: ["php", "php-fpm", "php-cgi", "composer"]
1880
+ },
1881
+ { appName: "Laravel", iconKey: "laravel", names: ["artisan", "laravel"] },
1882
+ { appName: "Symfony", iconKey: "symfony", names: ["symfony"] },
1883
+ { appName: "Elixir", iconKey: "elixir", names: ["elixir", "iex", "mix"] },
1884
+ {
1885
+ appName: "Erlang",
1886
+ iconKey: "erlang",
1887
+ names: ["erl", "beam.smp", "beam", "epmd", "rebar3"]
1888
+ },
1889
+ { appName: "Phoenix", iconKey: "phoenix", names: ["phx.server", "phoenix"] },
1890
+ {
1891
+ appName: "PostgreSQL",
1892
+ iconKey: "postgresql",
1893
+ names: ["postgres", "postmaster", "pg_ctl", "psql", "pg_isready"]
1894
+ },
1895
+ {
1896
+ appName: "Redis",
1897
+ iconKey: "redis",
1898
+ names: ["redis-server", "redis-cli", "redis-sentinel"]
1899
+ },
1900
+ {
1901
+ appName: "MySQL",
1902
+ iconKey: "mysql",
1903
+ names: ["mysql", "mysqld", "mysqladmin"]
1904
+ },
1905
+ {
1906
+ appName: "MariaDB",
1907
+ iconKey: "mariadb",
1908
+ names: ["mariadb", "mariadbd", "mariadb-admin"]
1909
+ },
1910
+ { appName: "SQLite", iconKey: "sqlite", names: ["sqlite3", "sqlite"] },
1911
+ {
1912
+ appName: "MongoDB",
1913
+ iconKey: "mongodb",
1914
+ names: ["mongod", "mongosh", "mongo"]
1915
+ },
1916
+ {
1917
+ appName: "Elasticsearch",
1918
+ iconKey: "elasticsearch",
1919
+ names: ["elasticsearch"]
1920
+ },
1921
+ { appName: "OpenSearch", iconKey: "opensearch", names: ["opensearch"] },
1922
+ { appName: "Solr", iconKey: "solr", names: ["solr"] },
1923
+ {
1924
+ appName: "Kafka",
1925
+ iconKey: "kafka",
1926
+ names: [
1927
+ "kafka-server-start",
1928
+ "kafka-console-consumer",
1929
+ "kafka-console-producer"
1930
+ ]
1931
+ },
1932
+ {
1933
+ appName: "ZooKeeper",
1934
+ iconKey: "zookeeper",
1935
+ names: ["zookeeper-server-start", "zkserver"]
1936
+ },
1937
+ {
1938
+ appName: "RabbitMQ",
1939
+ iconKey: "rabbitmq",
1940
+ names: ["rabbitmq-server", "rabbitmqctl"]
1941
+ },
1942
+ { appName: "NATS", iconKey: "nats", names: ["nats-server", "nats"] },
1943
+ {
1944
+ appName: "Docker",
1945
+ iconKey: "docker",
1946
+ names: [
1947
+ "docker",
1948
+ "dockerd",
1949
+ "docker-compose",
1950
+ "docker-proxy",
1951
+ "com.docker.backend"
1952
+ ]
1953
+ },
1954
+ { appName: "Podman", iconKey: "podman", names: ["podman", "podman-compose"] },
1955
+ {
1956
+ appName: "Kubernetes",
1957
+ iconKey: "kubernetes",
1958
+ names: ["kubectl", "kube-apiserver", "kubelet", "minikube", "kind"]
1959
+ },
1960
+ { appName: "Helm", iconKey: "helm", names: ["helm"] },
1961
+ { appName: "Tilt", iconKey: "tilt", names: ["tilt"] },
1962
+ { appName: "nginx", iconKey: "nginx", names: ["nginx"] },
1963
+ { appName: "Caddy", iconKey: "caddy", names: ["caddy"] },
1964
+ {
1965
+ appName: "Apache HTTP Server",
1966
+ iconKey: "apache",
1967
+ names: ["httpd", "apache2", "apachectl"]
1968
+ },
1969
+ { appName: "HAProxy", iconKey: "haproxy", names: ["haproxy"] },
1970
+ { appName: "Traefik", iconKey: "traefik", names: ["traefik"] },
1971
+ { appName: "Envoy", iconKey: "envoy", names: ["envoy"] },
1972
+ {
1973
+ appName: "Tailscale",
1974
+ iconKey: "tailscale",
1975
+ names: ["tailscale", "tailscaled"]
1976
+ },
1977
+ {
1978
+ appName: "Cloudflare Tunnel",
1979
+ iconKey: "cloudflare",
1980
+ names: ["cloudflared"]
1981
+ },
1982
+ { appName: "ngrok", iconKey: "ngrok", names: ["ngrok"] },
1983
+ { appName: "localtunnel", iconKey: "terminal", names: ["lt", "localtunnel"] },
1984
+ { appName: "SSH", iconKey: "ssh", names: ["ssh", "sshd", "scp", "sftp"] },
1985
+ {
1986
+ appName: "Git",
1987
+ iconKey: "git",
1988
+ names: ["git", "git-lfs", "git-upload-pack", "git-receive-pack"]
1989
+ },
1990
+ { appName: "GitHub CLI", iconKey: "github", names: ["gh"] },
1991
+ { appName: "GitLab CLI", iconKey: "gitlab", names: ["glab"] },
1992
+ { appName: "Subversion", iconKey: "subversion", names: ["svn"] },
1993
+ { appName: "Mercurial", iconKey: "mercurial", names: ["hg"] },
1994
+ { appName: "AWS CLI", iconKey: "aws", names: ["aws", "aws-vault", "sam"] },
1995
+ { appName: "Azure CLI", iconKey: "azure", names: ["az"] },
1996
+ {
1997
+ appName: "Google Cloud CLI",
1998
+ iconKey: "google-cloud",
1999
+ names: ["gcloud", "gsutil", "bq"]
2000
+ },
2001
+ { appName: "Terraform", iconKey: "terraform", names: ["terraform"] },
2002
+ { appName: "OpenTofu", iconKey: "opentofu", names: ["tofu"] },
2003
+ {
2004
+ appName: "Ansible",
2005
+ iconKey: "ansible",
2006
+ names: ["ansible", "ansible-playbook"]
2007
+ },
2008
+ { appName: "Pulumi", iconKey: "pulumi", names: ["pulumi"] },
2009
+ { appName: "Nomad", iconKey: "nomad", names: ["nomad"] },
2010
+ { appName: "Vault", iconKey: "vault", names: ["vault"] },
2011
+ { appName: "Consul", iconKey: "consul", names: ["consul"] },
2012
+ { appName: "Make", iconKey: "make", names: ["make", "gmake"] },
2013
+ { appName: "CMake", iconKey: "cmake", names: ["cmake", "ctest", "cpack"] },
2014
+ { appName: "Ninja", iconKey: "ninja", names: ["ninja"] },
2015
+ { appName: "Bazel", iconKey: "bazel", names: ["bazel", "bazelisk"] },
2016
+ { appName: "Buck", iconKey: "buck", names: ["buck", "buck2"] },
2017
+ { appName: "Clang", iconKey: "clang", names: ["clang", "clang++", "clangd"] },
2018
+ { appName: "GCC", iconKey: "gcc", names: ["gcc", "g++", "cc", "c++"] },
2019
+ { appName: "LLDB", iconKey: "terminal", names: ["lldb"] },
2020
+ { appName: "GDB", iconKey: "terminal", names: ["gdb"] },
2021
+ {
2022
+ appName: "Swift",
2023
+ iconKey: "swift",
2024
+ names: ["swift", "swiftc", "sourcekit-lsp"]
2025
+ },
2026
+ {
2027
+ appName: "Xcode",
2028
+ iconKey: "xcode",
2029
+ names: ["xcodebuild", "xcrun", "simctl"]
2030
+ },
2031
+ { appName: "Zig", iconKey: "zig", names: ["zig"] },
2032
+ {
2033
+ appName: "Lua",
2034
+ iconKey: "lua",
2035
+ names: ["lua", "luajit", "lua-language-server"]
2036
+ },
2037
+ { appName: "Perl", iconKey: "perl", names: ["perl"] },
2038
+ {
2039
+ appName: "R",
2040
+ iconKey: "r",
2041
+ names: ["r", "rscript", "rserver", "rsession"]
2042
+ },
2043
+ { appName: "Julia", iconKey: "julia", names: ["julia"] },
2044
+ {
2045
+ appName: "Haskell",
2046
+ iconKey: "haskell",
2047
+ names: ["ghc", "ghci", "cabal", "stack", "haskell-language-server-wrapper"]
2048
+ },
2049
+ {
2050
+ appName: "OCaml",
2051
+ iconKey: "ocaml",
2052
+ names: ["ocaml", "ocamlc", "dune", "opam"]
2053
+ },
2054
+ { appName: "F#", iconKey: "fsharp", names: ["fsharpi", "fsautocomplete"] },
2055
+ {
2056
+ appName: ".NET",
2057
+ iconKey: "dotnet",
2058
+ names: ["dotnet", "csharp-ls", "omnisharp"]
2059
+ },
2060
+ {
2061
+ appName: "PowerShell",
2062
+ iconKey: "powershell",
2063
+ names: ["pwsh", "powershell"]
2064
+ },
2065
+ { appName: "Bash", iconKey: "terminal", names: ["bash"] },
2066
+ { appName: "Zsh", iconKey: "terminal", names: ["zsh"] },
2067
+ { appName: "fish", iconKey: "terminal", names: ["fish"] },
2068
+ { appName: "tmux", iconKey: "terminal", names: ["tmux"] },
2069
+ { appName: "GNU Screen", iconKey: "terminal", names: ["screen"] },
2070
+ { appName: "direnv", iconKey: "terminal", names: ["direnv"] },
2071
+ {
2072
+ appName: "Nix",
2073
+ iconKey: "nix",
2074
+ names: ["nix", "nix-shell", "nix-daemon", "nix-build", "nix develop"]
2075
+ },
2076
+ { appName: "Homebrew", iconKey: "homebrew", names: ["brew"] },
2077
+ { appName: "mise", iconKey: "terminal", names: ["mise"] },
2078
+ { appName: "asdf", iconKey: "terminal", names: ["asdf"] },
2079
+ { appName: "Volta", iconKey: "volta", names: ["volta"] },
2080
+ { appName: "fnm", iconKey: "nodejs", names: ["fnm"] },
2081
+ { appName: "nvm", iconKey: "nodejs", names: ["nvm"] },
2082
+ { appName: "curl", iconKey: "terminal", names: ["curl"] },
2083
+ { appName: "HTTPie", iconKey: "httpie", names: ["http", "https", "httpie"] },
2084
+ { appName: "wget", iconKey: "terminal", names: ["wget"] },
2085
+ { appName: "jq", iconKey: "terminal", names: ["jq"] },
2086
+ { appName: "ripgrep", iconKey: "terminal", names: ["rg", "ripgrep"] },
2087
+ { appName: "fd", iconKey: "terminal", names: ["fd"] },
2088
+ { appName: "fzf", iconKey: "terminal", names: ["fzf"] },
2089
+ { appName: "bat", iconKey: "terminal", names: ["bat"] },
2090
+ { appName: "eza", iconKey: "terminal", names: ["eza", "exa"] },
2091
+ { appName: "htop", iconKey: "terminal", names: ["htop", "top", "btop"] },
2092
+ { appName: "Vim", iconKey: "vim", names: ["vim", "view", "vimdiff"] },
2093
+ { appName: "Neovim", iconKey: "neovim", names: ["nvim", "nvim-qt"] },
2094
+ { appName: "Emacs", iconKey: "emacs", names: ["emacs", "emacsclient"] },
2095
+ {
2096
+ appName: "Visual Studio Code",
2097
+ iconKey: "vscode",
2098
+ names: [
2099
+ "code",
2100
+ "code-insiders",
2101
+ "vscode",
2102
+ "visual studio code",
2103
+ "visual studio code - insiders"
2104
+ ]
2105
+ },
2106
+ { appName: "code-server", iconKey: "vscode", names: ["code-server"] },
2107
+ { appName: "Cursor", iconKey: "cursor", names: ["cursor"] },
2108
+ { appName: "Zed", iconKey: "zed", names: ["zed"] },
2109
+ {
2110
+ appName: "Sublime Text",
2111
+ iconKey: "sublime-text",
2112
+ names: ["subl", "sublime_text", "sublime text"]
2113
+ },
2114
+ { appName: "Atom", iconKey: "atom", names: ["atom"] },
2115
+ {
2116
+ appName: "IntelliJ IDEA",
2117
+ iconKey: "intellij-idea",
2118
+ names: ["idea", "idea.sh", "intellij idea", "idea64.exe"]
2119
+ },
2120
+ {
2121
+ appName: "WebStorm",
2122
+ iconKey: "webstorm",
2123
+ names: ["webstorm", "webstorm.sh"]
2124
+ },
2125
+ { appName: "PyCharm", iconKey: "pycharm", names: ["pycharm", "pycharm.sh"] },
2126
+ {
2127
+ appName: "RubyMine",
2128
+ iconKey: "rubymine",
2129
+ names: ["rubymine", "rubymine.sh"]
2130
+ },
2131
+ { appName: "GoLand", iconKey: "goland", names: ["goland", "goland.sh"] },
2132
+ { appName: "CLion", iconKey: "clion", names: ["clion", "clion.sh"] },
2133
+ {
2134
+ appName: "DataGrip",
2135
+ iconKey: "datagrip",
2136
+ names: ["datagrip", "datagrip.sh"]
2137
+ },
2138
+ { appName: "Rider", iconKey: "rider", names: ["rider", "rider.sh"] },
2139
+ {
2140
+ appName: "Android Studio",
2141
+ iconKey: "android-studio",
2142
+ names: ["studio", "studio.sh", "android studio"]
2143
+ },
2144
+ { appName: "Xcode", iconKey: "xcode", names: ["xcode", "xcode.app"] },
2145
+ { appName: "Postman", iconKey: "postman", names: ["postman"] },
2146
+ { appName: "Insomnia", iconKey: "insomnia", names: ["insomnia"] },
2147
+ { appName: "Hoppscotch", iconKey: "hoppscotch", names: ["hoppscotch"] },
2148
+ { appName: "TablePlus", iconKey: "tableplus", names: ["tableplus"] },
2149
+ { appName: "DBeaver", iconKey: "dbeaver", names: ["dbeaver"] },
2150
+ { appName: "RedisInsight", iconKey: "redis", names: ["redisinsight"] },
2151
+ { appName: "Prisma", iconKey: "prisma", names: ["prisma"] },
2152
+ { appName: "Drizzle Kit", iconKey: "drizzle", names: ["drizzle-kit"] },
2153
+ { appName: "Supabase", iconKey: "supabase", names: ["supabase"] },
2154
+ { appName: "Firebase", iconKey: "firebase", names: ["firebase"] },
2155
+ { appName: "Vercel", iconKey: "vercel", names: ["vercel"] },
2156
+ { appName: "Netlify", iconKey: "netlify", names: ["netlify", "ntl"] },
2157
+ { appName: "Fly.io", iconKey: "fly", names: ["fly", "flyctl"] },
2158
+ { appName: "Heroku", iconKey: "heroku", names: ["heroku"] },
2159
+ { appName: "Stripe CLI", iconKey: "stripe", names: ["stripe"] },
2160
+ { appName: "Twilio CLI", iconKey: "twilio", names: ["twilio"] },
2161
+ { appName: "Mailpit", iconKey: "mail", names: ["mailpit"] },
2162
+ { appName: "MailHog", iconKey: "mail", names: ["mailhog"] },
2163
+ { appName: "OpenSSL", iconKey: "openssl", names: ["openssl"] },
2164
+ { appName: "mkcert", iconKey: "certificate", names: ["mkcert"] },
2165
+ { appName: "mkdocs", iconKey: "markdown", names: ["mkdocs"] },
2166
+ { appName: "Hugo", iconKey: "hugo", names: ["hugo"] },
2167
+ { appName: "Jekyll", iconKey: "jekyll", names: ["jekyll"] },
2168
+ { appName: "Eleventy", iconKey: "eleventy", names: ["eleventy", "11ty"] },
2169
+ { appName: "Docusaurus", iconKey: "docusaurus", names: ["docusaurus"] },
2170
+ { appName: "Trunk", iconKey: "rust", names: ["trunk"] },
2171
+ { appName: "wasm-pack", iconKey: "webassembly", names: ["wasm-pack"] },
2172
+ { appName: "wasmtime", iconKey: "webassembly", names: ["wasmtime"] },
2173
+ {
2174
+ appName: "Language Server",
2175
+ iconKey: "language-server",
2176
+ names: [
2177
+ "lua-language-server",
2178
+ "yaml-language-server",
2179
+ "vscode-json-language-server",
2180
+ "docker-langserver",
2181
+ "bash-language-server",
2182
+ "marksman",
2183
+ "taplo"
2184
+ ]
2185
+ }
2186
+ ];
2187
+ var executableNameLookup = new Map;
2188
+ for (const entry of processCatalogEntries) {
2189
+ for (const name of entry.names) {
2190
+ executableNameLookup.set(normalizeProcessName(name), {
2191
+ appName: entry.appName,
2192
+ iconKey: entry.iconKey
2193
+ });
2194
+ }
2195
+ }
2196
+ var processNameCatalogSize = executableNameLookup.size;
2197
+ function canonicalizeProcessName(processName) {
2198
+ const executableName = executableNameFromPathOrName(processName);
2199
+ if (executableName === undefined) {
2200
+ return;
2201
+ }
2202
+ const directMatch = lookupCanonicalProcess(executableName);
2203
+ if (directMatch !== undefined) {
2204
+ return directMatch;
2205
+ }
2206
+ const pathMatch = lookupCanonicalPathHint(processName);
2207
+ if (pathMatch !== undefined) {
2208
+ return pathMatch;
2209
+ }
2210
+ return;
2211
+ }
2212
+ function guessCanonicalProcessFromCommand(command) {
2213
+ const tokens = shellCommandTokens(command);
2214
+ const executable = executableTokenFromCommandTokens(tokens);
2215
+ if (executable === undefined) {
2216
+ return;
2217
+ }
2218
+ return canonicalizeProcessName(executable);
2219
+ }
2220
+ function lookupCanonicalProcess(processName) {
2221
+ const normalized = normalizeProcessName(processName);
2222
+ const identity = executableNameLookup.get(normalized);
2223
+ if (identity === undefined) {
2224
+ return;
2225
+ }
2226
+ return {
2227
+ ...identity,
2228
+ executableName: processName,
2229
+ matchedName: normalized
2230
+ };
2231
+ }
2232
+ function lookupCanonicalPathHint(processPath) {
2233
+ const normalizedPath = normalizeProcessName(processPath);
2234
+ const pathParts = normalizedPath.split("/").map((part) => trimExecutableExtension(part)).filter((part) => part.length > 0);
2235
+ for (const pathPart of pathParts.reverse()) {
2236
+ const pathHint = executableNameLookup.get(pathPart);
2237
+ if (pathHint !== undefined) {
2238
+ return {
2239
+ ...pathHint,
2240
+ executableName: executableNameFromPathOrName(processPath) ?? processPath,
2241
+ matchedName: pathPart
2242
+ };
2243
+ }
2244
+ }
2245
+ return;
2246
+ }
2247
+ function executableTokenFromCommandTokens(tokens) {
2248
+ const [firstToken, ...remainingTokens] = tokens;
2249
+ switch (firstToken) {
2250
+ case undefined:
2251
+ return;
2252
+ case "env":
2253
+ case "/usr/bin/env":
2254
+ return executableTokenFromCommandTokens(remainingTokens.filter((token) => !isEnvironmentAssignment(token)));
2255
+ case "sudo":
2256
+ case "command":
2257
+ case "exec":
2258
+ case "nohup":
2259
+ return executableTokenFromCommandTokens(remainingTokens.filter((token) => !token.startsWith("-")));
2260
+ default:
2261
+ switch (isEnvironmentAssignment(firstToken)) {
2262
+ case true:
2263
+ return executableTokenFromCommandTokens(remainingTokens);
2264
+ case false:
2265
+ return firstToken;
2266
+ }
2267
+ }
2268
+ }
2269
+ function executableNameFromPathOrName(processName) {
2270
+ const trimmed = processName.trim();
2271
+ if (trimmed.length === 0) {
2272
+ return;
2273
+ }
2274
+ const normalizedSeparators = trimmed.replaceAll("\\", "/");
2275
+ const [lastPathPart] = normalizedSeparators.split("/").filter((part) => part.length > 0).slice(-1);
2276
+ switch (lastPathPart) {
2277
+ case undefined:
2278
+ return;
2279
+ default:
2280
+ return trimExecutableExtension(lastPathPart);
2281
+ }
2282
+ }
2283
+ function trimExecutableExtension(executableName) {
2284
+ const normalized = executableName.toLowerCase();
2285
+ switch (true) {
2286
+ case normalized.endsWith(".app"):
2287
+ return executableName.slice(0, -4);
2288
+ case normalized.endsWith(".exe"):
2289
+ return executableName.slice(0, -4);
2290
+ case normalized.endsWith(".cmd"):
2291
+ case normalized.endsWith(".bat"):
2292
+ return executableName.slice(0, -4);
2293
+ default:
2294
+ return executableName;
2295
+ }
2296
+ }
2297
+ function normalizeProcessName(processName) {
2298
+ return trimExecutableExtension(processName.trim().replaceAll("\\", "/").toLowerCase());
2299
+ }
2300
+ function isEnvironmentAssignment(token) {
2301
+ return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
2302
+ }
2303
+ function shellCommandTokens(command) {
2304
+ const tokens = [];
2305
+ let currentToken = "";
2306
+ let quote = undefined;
2307
+ let escaping = false;
2308
+ for (const character of command.trim()) {
2309
+ switch (true) {
2310
+ case escaping:
2311
+ currentToken = `${currentToken}${character}`;
2312
+ escaping = false;
2313
+ break;
2314
+ case character === "\\":
2315
+ escaping = true;
2316
+ break;
2317
+ case (quote === undefined && (character === '"' || character === "'")):
2318
+ quote = character;
2319
+ break;
2320
+ case quote === character:
2321
+ quote = undefined;
2322
+ break;
2323
+ case (quote === undefined && /\s/.test(character)):
2324
+ if (currentToken.length > 0) {
2325
+ tokens.push(currentToken);
2326
+ currentToken = "";
2327
+ }
2328
+ break;
2329
+ default:
2330
+ currentToken = `${currentToken}${character}`;
2331
+ break;
2332
+ }
2333
+ }
2334
+ if (currentToken.length > 0) {
2335
+ tokens.push(currentToken);
2336
+ }
2337
+ return tokens;
2338
+ }
2339
+
1477
2340
  // src/channelSession.ts
1478
2341
  var codexTypingHeartbeatMs = 5000;
1479
2342
  var defaultStreamFlushIntervalMs = 150;
@@ -1534,7 +2397,7 @@ async function attachChannelSession(args) {
1534
2397
  }
1535
2398
  switch (method) {
1536
2399
  case "turn/started":
1537
- if (turnId !== undefined) {
2400
+ if (threadId !== undefined && turnId !== undefined) {
1538
2401
  rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId);
1539
2402
  }
1540
2403
  break;
@@ -1669,19 +2532,42 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
1669
2532
  forwardedTerminalInputKeys: new Set,
1670
2533
  webSearchProgressOutputs: createBoundedCache(maxForwardedTurnIds),
1671
2534
  pendingApprovalRequests: new Map,
2535
+ approvalPromptChain: Promise.resolve(),
1672
2536
  pendingPortForwardRequests: new Map,
2537
+ queuedPortForwardCandidates: new Map,
1673
2538
  approvedForwardPorts: new Set,
1674
2539
  approvedForwardTargets: new Map,
1675
2540
  dismissedForwardTargets: new Map,
1676
2541
  portForwardWatcher: undefined,
1677
2542
  activeProcessingState: undefined,
2543
+ pendingReconnectContextInjection: undefined,
1678
2544
  terminalInputForwardChain: Promise.resolve(),
1679
2545
  webSearchProgressForwardChain: Promise.resolve(),
1680
2546
  typingHeartbeat: undefined,
1681
2547
  typingHeartbeatInFlight: false,
1682
- runtimeSettings: runtimeSettingsFromOptions(options)
2548
+ runtimeSettings: runtimeSettingsFromOptions(options),
2549
+ pendingRuntimeSettingsResume: false,
2550
+ runtimeSettingsGeneration: 0,
2551
+ runtimeSettingsResumeChain: Promise.resolve()
1683
2552
  };
1684
2553
  }
2554
+ async function handleLostPortForwardCandidate(args, state, payloadContext, candidate) {
2555
+ dropLostQueuedPortForwardCandidate(args, state, candidate);
2556
+ await expireLostPendingPortForwardRequest(args, state, payloadContext, candidate);
2557
+ await revokeLostApprovedForwardPort(args, state, candidate);
2558
+ }
2559
+ function dropLostQueuedPortForwardCandidate(args, state, candidate) {
2560
+ const queued = state.queuedPortForwardCandidates.get(candidate.port);
2561
+ if (queued === undefined || !sameForwardCandidate(queued, candidate)) {
2562
+ return;
2563
+ }
2564
+ state.queuedPortForwardCandidates.delete(candidate.port);
2565
+ args.log("port_forward.queued_request_dropped", {
2566
+ port: candidate.port,
2567
+ pid: candidate.pid,
2568
+ reason: "listener_exited"
2569
+ });
2570
+ }
1685
2571
  function startPortForwardWatchIfEnabled(args, state, payloadContext) {
1686
2572
  if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined || state.kandanThreadId === undefined || state.codexThreadId === undefined) {
1687
2573
  return;
@@ -1692,8 +2578,10 @@ function startPortForwardWatchIfEnabled(args, state, payloadContext) {
1692
2578
  state.approvedForwardPorts.add(port);
1693
2579
  }
1694
2580
  state.portForwardWatcher = start({
2581
+ rootCwd: watchOptions.rootCwd ?? args.options.cwd,
1695
2582
  ...watchOptions,
1696
2583
  onCandidate: (candidate) => publishPortForwardPrompt(args, state, payloadContext, candidate),
2584
+ onCandidateLost: (candidate) => handleLostPortForwardCandidate(args, state, payloadContext, candidate),
1697
2585
  onError: (error) => args.log("port_forward.watch_failed", { message: error.message })
1698
2586
  });
1699
2587
  }
@@ -1746,7 +2634,11 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
1746
2634
  return;
1747
2635
  }
1748
2636
  if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
1749
- return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
2637
+ return {
2638
+ instanceId: args.instanceId,
2639
+ ok: false,
2640
+ error: "thread_not_bound"
2641
+ };
1750
2642
  }
1751
2643
  const interrupted = interruptPendingKandanMessages(state.queue, control.throughSeq);
1752
2644
  const activeQueuedSeq = interruptibleQueuedSeq(state.turn);
@@ -1794,14 +2686,20 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
1794
2686
  interruptedQueuedMessages: interrupted.ok ? interrupted.selectedCount : 0
1795
2687
  };
1796
2688
  }
1797
- function updateSessionSettings(args, state, control) {
2689
+ async function updateSessionSettings(args, state, control) {
1798
2690
  if (state.codexThreadId !== control.threadId) {
1799
2691
  return;
1800
2692
  }
1801
2693
  if (state.codexThreadId === undefined) {
1802
- return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
2694
+ return {
2695
+ instanceId: args.instanceId,
2696
+ ok: false,
2697
+ error: "thread_not_bound"
2698
+ };
1803
2699
  }
1804
2700
  state.runtimeSettings = mergeRuntimeSettings(state.runtimeSettings, control);
2701
+ state.pendingRuntimeSettingsResume = true;
2702
+ state.runtimeSettingsGeneration += 1;
1805
2703
  publishRuntimeSettings(args, state).catch((error) => {
1806
2704
  args.log("kandan.session_settings_publish_failed", {
1807
2705
  message: error instanceof Error ? error.message : String(error)
@@ -1814,6 +2712,7 @@ function updateSessionSettings(args, state, control) {
1814
2712
  sandbox: state.runtimeSettings.sandbox ?? null,
1815
2713
  fast: state.runtimeSettings.fast ?? null
1816
2714
  });
2715
+ await tryResumeCodexThreadForPendingRuntimeSettings(args, state);
1817
2716
  return {
1818
2717
  instanceId: args.instanceId,
1819
2718
  ok: true,
@@ -1824,21 +2723,97 @@ function updateSessionSettings(args, state, control) {
1824
2723
  fast: state.runtimeSettings.fast ?? null
1825
2724
  };
1826
2725
  }
2726
+ async function resumeCodexThreadForPendingRuntimeSettings(args, state) {
2727
+ const requestedGeneration = state.runtimeSettingsGeneration;
2728
+ const previousResume = state.runtimeSettingsResumeChain.catch(() => {
2729
+ return;
2730
+ });
2731
+ const resume = previousResume.then(() => performCodexThreadResumeForPendingRuntimeSettings(args, state, requestedGeneration));
2732
+ state.runtimeSettingsResumeChain = resume.then(() => {
2733
+ return;
2734
+ }, () => {
2735
+ return;
2736
+ });
2737
+ return await resume;
2738
+ }
2739
+ async function performCodexThreadResumeForPendingRuntimeSettings(args, state, requestedGeneration) {
2740
+ if (state.pendingRuntimeSettingsResume !== true || state.closed || state.turn.status !== "idle" || localTuiTurnIsActive(state)) {
2741
+ return true;
2742
+ }
2743
+ const codexThreadId = state.codexThreadId;
2744
+ if (codexThreadId === undefined) {
2745
+ return true;
2746
+ }
2747
+ if (state.runtimeSettingsGeneration !== requestedGeneration) {
2748
+ return false;
2749
+ }
2750
+ const resumeGeneration = requestedGeneration;
2751
+ const resumeSettings = state.runtimeSettings;
2752
+ const runtimeOptions = runtimeOptionsForSettings(args.options, resumeSettings);
2753
+ const resumeParams = {
2754
+ threadId: codexThreadId,
2755
+ ...codexThreadRuntimeOverrides(runtimeOptions)
2756
+ };
2757
+ args.log("codex.session_settings_resume_requested", {
2758
+ codex_thread_id: codexThreadId,
2759
+ model: resumeSettings.model ?? null,
2760
+ reasoning_effort: resumeSettings.reasoningEffort ?? null,
2761
+ approval_policy: resumeSettings.approvalPolicy ?? null,
2762
+ sandbox: resumeSettings.sandbox ?? null,
2763
+ fast: resumeSettings.fast ?? null,
2764
+ generation: resumeGeneration
2765
+ });
2766
+ const resumed = await args.codex.request("thread/resume", resumeParams);
2767
+ if ("error" in resumed) {
2768
+ throw new Error(`failed to resume Codex thread with updated settings: ${resumed.error.message}`);
2769
+ }
2770
+ if (state.pendingRuntimeSettingsResume === true && state.runtimeSettingsGeneration === resumeGeneration) {
2771
+ state.pendingRuntimeSettingsResume = false;
2772
+ }
2773
+ args.log("codex.session_settings_resume_completed", {
2774
+ codex_thread_id: codexThreadId,
2775
+ generation: resumeGeneration,
2776
+ current_generation: state.runtimeSettingsGeneration,
2777
+ pending: state.pendingRuntimeSettingsResume
2778
+ });
2779
+ return state.pendingRuntimeSettingsResume !== true;
2780
+ }
2781
+ async function tryResumeCodexThreadForPendingRuntimeSettings(args, state) {
2782
+ try {
2783
+ return await resumeCodexThreadForPendingRuntimeSettings(args, state);
2784
+ } catch (error) {
2785
+ args.log("codex.session_settings_resume_failed", {
2786
+ message: error instanceof Error ? error.message : String(error)
2787
+ });
2788
+ return false;
2789
+ }
2790
+ }
1827
2791
  async function resolvePendingCodexApprovalRequest(args, state, control) {
1828
2792
  if (state.codexThreadId !== control.threadId) {
1829
2793
  return;
1830
2794
  }
1831
2795
  if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
1832
- return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
2796
+ return {
2797
+ instanceId: args.instanceId,
2798
+ ok: false,
2799
+ error: "thread_not_bound"
2800
+ };
1833
2801
  }
1834
2802
  const approval = state.pendingApprovalRequests.get(approvalRequestKey(control.requestId, control.sourceSeq));
1835
2803
  if (approval === undefined) {
1836
- return { instanceId: args.instanceId, ok: false, error: "approval_request_not_found" };
2804
+ return {
2805
+ instanceId: args.instanceId,
2806
+ ok: false,
2807
+ error: "approval_request_not_found"
2808
+ };
1837
2809
  }
1838
2810
  state.pendingApprovalRequests.delete(approvalRequestKey(control.requestId, control.sourceSeq));
1839
2811
  const codexDecision = control.decision === "approve" ? "accept" : "decline";
1840
2812
  approval.resolve({ decision: codexDecision });
1841
- state.activeProcessingState = { seq: approval.sourceSeq, reason: "streaming response" };
2813
+ state.activeProcessingState = {
2814
+ seq: approval.sourceSeq,
2815
+ reason: "streaming response"
2816
+ };
1842
2817
  await publishMessageState(args, state.kandanThreadId, approval.sourceSeq, {
1843
2818
  status: "processing",
1844
2819
  reason: "streaming response"
@@ -1851,6 +2826,17 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
1851
2826
  });
1852
2827
  return { instanceId: args.instanceId, ok: true };
1853
2828
  }
2829
+ async function drainQueuedPortForwardPrompt(args, state, payloadContext) {
2830
+ if (state.pendingPortForwardRequests.size > 0 || state.queuedPortForwardCandidates.size === 0) {
2831
+ return;
2832
+ }
2833
+ const next = Array.from(state.queuedPortForwardCandidates.values()).sort((left, right) => left.port - right.port)[0];
2834
+ if (next === undefined) {
2835
+ return;
2836
+ }
2837
+ state.queuedPortForwardCandidates.delete(next.port);
2838
+ await publishPortForwardPrompt(args, state, payloadContext, next);
2839
+ }
1854
2840
  async function resolvePendingPortForwardRequest(args, state, payloadContext, control) {
1855
2841
  const request = state.pendingPortForwardRequests.get(control.requestId);
1856
2842
  if (request === undefined) {
@@ -1863,7 +2849,11 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1863
2849
  actor_user_id: control.actorUserId ?? null,
1864
2850
  reason: "sender_not_allowed"
1865
2851
  });
1866
- return { instanceId: args.instanceId, ok: false, error: "sender_not_allowed" };
2852
+ return {
2853
+ instanceId: args.instanceId,
2854
+ ok: false,
2855
+ error: "sender_not_allowed"
2856
+ };
1867
2857
  }
1868
2858
  state.pendingPortForwardRequests.delete(control.requestId);
1869
2859
  if (control.decision === "deny") {
@@ -1874,14 +2864,18 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1874
2864
  port: request.port,
1875
2865
  pid: request.pid
1876
2866
  });
2867
+ await drainQueuedPortForwardPrompt(args, state, payloadContext);
1877
2868
  return { instanceId: args.instanceId, ok: true };
1878
2869
  }
1879
2870
  state.approvedForwardPorts.add(request.port);
1880
2871
  state.approvedForwardTargets.set(request.port, approvedTargetFromRequest(request));
2872
+ const processIdentity = guessCanonicalProcessFromCommand(request.command);
1881
2873
  const capabilities = args.options.onForwardPortApproved?.(request.port, {
1882
2874
  kandanThreadId: state.kandanThreadId ?? null,
1883
2875
  codexThreadId: state.codexThreadId ?? null,
1884
- channelSlug: args.options.channelSession.channelSlug ?? null
2876
+ channelSlug: args.options.channelSession.channelSlug ?? null,
2877
+ processName: processIdentity?.appName ?? null,
2878
+ processIconKey: processIdentity?.iconKey ?? null
1885
2879
  });
1886
2880
  await publishForwardPortResolvedEvent(args, request, capabilities);
1887
2881
  await publishMessageStateForPortForwardResult(args, state, request, "processed");
@@ -1891,6 +2885,7 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1891
2885
  port: request.port,
1892
2886
  pid: request.pid
1893
2887
  });
2888
+ await drainQueuedPortForwardPrompt(args, state, payloadContext);
1894
2889
  return { instanceId: args.instanceId, ok: true, port: request.port };
1895
2890
  }
1896
2891
  function portForwardControlSenderAllowed(args, payloadContext, control) {
@@ -1927,37 +2922,39 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1927
2922
  state.approvedForwardTargets.set(review.target.port, review.target);
1928
2923
  return;
1929
2924
  case "revoke_and_prompt":
2925
+ if (state.pendingPortForwardRequests.size > 0) {
2926
+ state.queuedPortForwardCandidates.set(candidate.port, candidate);
2927
+ args.log("port_forward.request_queued", {
2928
+ port: candidate.port,
2929
+ pid: candidate.pid,
2930
+ pending_count: state.pendingPortForwardRequests.size
2931
+ });
2932
+ return;
2933
+ }
1930
2934
  await revokeApprovedForwardPort(args, state, review.revoked, review.reason);
1931
2935
  break;
1932
2936
  case "prompt":
2937
+ if (state.pendingPortForwardRequests.size > 0) {
2938
+ state.queuedPortForwardCandidates.set(candidate.port, candidate);
2939
+ args.log("port_forward.request_queued", {
2940
+ port: candidate.port,
2941
+ pid: candidate.pid,
2942
+ pending_count: state.pendingPortForwardRequests.size
2943
+ });
2944
+ return;
2945
+ }
1933
2946
  break;
1934
2947
  }
1935
2948
  const requestId = `port-forward-${randomUUID()}`;
1936
2949
  const label = portForwardPromptLabel(candidate);
1937
- const body = portForwardPromptBody(candidate, requestId);
1938
- const payload = {
1939
- ...localRunnerPayload(args.options, args.instanceId, "port_forward_request", state.codexThreadId, payloadContext),
1940
- reply_to_seq: state.rootSeq ?? null,
1941
- structured: {
1942
- kind: "local_runner_port_forward_request",
1943
- request_id: requestId,
2950
+ const sourceSeq = portForwardApprovalSourceSeq(state);
2951
+ if (sourceSeq === undefined) {
2952
+ args.log("port_forward.prompt_skipped", {
1944
2953
  port: candidate.port,
1945
2954
  pid: candidate.pid,
1946
- command: candidate.command,
1947
- ...candidate.cwd === undefined ? {} : { cwd: candidate.cwd },
1948
- command_label: label
1949
- }
1950
- };
1951
- const reply = await pushOk(args.kandan, args.topic, "session:post_thread_message", {
1952
- workspace: args.options.channelSession.workspaceSlug,
1953
- channel: args.options.channelSession.channelSlug,
1954
- thread_id: state.kandanThreadId,
1955
- body,
1956
- payload
1957
- });
1958
- const sourceSeq = integerValue(reply.seq);
1959
- if (sourceSeq === undefined) {
1960
- throw new Error("port forward prompt did not return a Kandan message seq");
2955
+ reason: "source_seq_missing"
2956
+ });
2957
+ return;
1961
2958
  }
1962
2959
  const request = pendingRequestFromCandidate({
1963
2960
  requestId,
@@ -1965,6 +2962,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1965
2962
  candidate
1966
2963
  });
1967
2964
  state.pendingPortForwardRequests.set(requestId, request);
2965
+ const processIdentity = guessCanonicalProcessFromCommand(candidate.command);
2966
+ const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
2967
+ const processName = processIdentity?.appName ?? label;
1968
2968
  await publishForwardPortRequestedEvent(args, request);
1969
2969
  await publishMessageState(args, state.kandanThreadId, sourceSeq, {
1970
2970
  status: "processing",
@@ -1972,13 +2972,20 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1972
2972
  approval: {
1973
2973
  requestId,
1974
2974
  kind: "local_runner_port_forward",
1975
- summary: `Open runner port ${candidate.port} from ${label}`,
2975
+ summary: `Make ${processName} on port ${candidate.port} accessible on Linzumi?`,
1976
2976
  reason: portForwardPromptReason(candidate),
2977
+ port: candidate.port,
2978
+ pid: candidate.pid,
2979
+ command: candidate.command,
2980
+ ...candidate.cwd === undefined ? {} : { cwd: candidate.cwd },
2981
+ ...processIdentity?.appName === undefined ? {} : { processName },
2982
+ ...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey },
2983
+ ...processIconPath === undefined ? {} : { processIconPath },
1977
2984
  choices: [
1978
2985
  {
1979
2986
  decision: "approve",
1980
- label: "Open preview",
1981
- description: `Allow Kandan to proxy HTTP, HTTPS, and WebSocket traffic for runner port ${candidate.port}.`
2987
+ label: "Enable",
2988
+ description: `Allow Linzumi to proxy HTTP, HTTPS, and WebSocket traffic for runner port ${candidate.port}.`
1982
2989
  },
1983
2990
  {
1984
2991
  decision: "deny",
@@ -1996,6 +3003,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1996
3003
  pid: candidate.pid
1997
3004
  });
1998
3005
  }
3006
+ function portForwardApprovalSourceSeq(state) {
3007
+ return state.activeProcessingState?.seq ?? state.rootSeq;
3008
+ }
1999
3009
  async function revokeApprovedForwardPort(args, state, target, reason) {
2000
3010
  state.approvedForwardPorts.delete(target.port);
2001
3011
  state.approvedForwardTargets.delete(target.port);
@@ -2015,30 +3025,58 @@ async function revokeApprovedForwardPort(args, state, target, reason) {
2015
3025
  reason
2016
3026
  });
2017
3027
  }
3028
+ async function revokeLostApprovedForwardPort(args, state, candidate) {
3029
+ if (!state.approvedForwardPorts.has(candidate.port)) {
3030
+ return;
3031
+ }
3032
+ await revokeApprovedForwardPort(args, state, state.approvedForwardTargets.get(candidate.port) ?? candidate, "listener_exited");
3033
+ }
3034
+ async function expireLostPendingPortForwardRequest(args, state, payloadContext, candidate) {
3035
+ const request = Array.from(state.pendingPortForwardRequests.values()).find((request2) => sameForwardCandidate(approvedTargetFromRequest(request2), candidate));
3036
+ if (request === undefined) {
3037
+ return;
3038
+ }
3039
+ state.pendingPortForwardRequests.delete(request.requestId);
3040
+ await publishMessageStateForPortForwardResult(args, state, request, "failed", "port_forward_listener_exited");
3041
+ args.log("port_forward.pending_request_expired", {
3042
+ request_id: request.requestId,
3043
+ port: request.port,
3044
+ pid: request.pid,
3045
+ reason: "listener_exited"
3046
+ });
3047
+ await drainQueuedPortForwardPrompt(args, state, payloadContext);
3048
+ }
2018
3049
  async function publishPortForwardReadyMessage(args, state, payloadContext, request) {
2019
3050
  if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
2020
3051
  return;
2021
3052
  }
2022
3053
  const path = forwardPreviewPath(args.options.runnerId, request.port);
3054
+ const processIdentity = guessCanonicalProcessFromCommand(request.command);
3055
+ const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
3056
+ const processName = processIdentity?.appName ?? `Port ${request.port}`;
3057
+ const readySummary = `${processName} on port ${request.port} is ready in Linzumi.`;
2023
3058
  await pushOptional(args.kandan, args.topic, "session:post_thread_message", {
2024
3059
  workspace: args.options.channelSession.workspaceSlug,
2025
3060
  channel: args.options.channelSession.channelSlug,
2026
3061
  thread_id: state.kandanThreadId,
2027
- body: `Runner port ${request.port} is open in Kandan: [Open preview](${path})`,
3062
+ body: `${readySummary} [Open](${path})`,
2028
3063
  payload: {
2029
3064
  ...localRunnerPayload(args.options, args.instanceId, "port_forward_ready", state.codexThreadId, payloadContext),
2030
3065
  ...state.rootSeq === undefined ? {} : { reply_to_seq: state.rootSeq },
2031
3066
  structured: {
2032
3067
  kind: "local_runner_port_forward_ready",
2033
3068
  status: "ready",
2034
- summary: `Runner port ${request.port} is open in Kandan.`,
3069
+ summary: readySummary,
2035
3070
  next_action: "Open HTTP/HTTPS/WebSocket preview",
2036
3071
  source_path: path,
2037
- link_label: "Open preview",
3072
+ link_label: "Open",
2038
3073
  request_id: request.requestId,
2039
3074
  port: request.port,
2040
3075
  pid: request.pid,
2041
3076
  command: request.command,
3077
+ ...processIdentity?.appName === undefined ? {} : { process_name: processIdentity.appName },
3078
+ ...processIdentity?.iconKey === undefined ? {} : { process_icon_key: processIdentity.iconKey },
3079
+ ...processIconPath === undefined ? {} : { process_icon_path: processIconPath },
2042
3080
  ...request.cwd === undefined ? {} : { cwd: request.cwd },
2043
3081
  url: path
2044
3082
  }
@@ -2067,10 +3105,15 @@ async function publishForwardPortResolvedEvent(args, request, capabilities) {
2067
3105
  ...capabilities === undefined ? {} : { capabilities }
2068
3106
  }, args.log);
2069
3107
  }
2070
- async function publishMessageStateForPortForwardResult(args, state, request, status) {
3108
+ async function publishMessageStateForPortForwardResult(args, state, request, status, failedReason = "port_forward_denied") {
2071
3109
  if (state.kandanThreadId === undefined) {
2072
3110
  return;
2073
3111
  }
3112
+ const activeProcessingState = state.activeProcessingState;
3113
+ if (activeProcessingState !== undefined && activeProcessingState.seq === request.sourceSeq) {
3114
+ await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
3115
+ return;
3116
+ }
2074
3117
  switch (status) {
2075
3118
  case "processed":
2076
3119
  await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
@@ -2080,7 +3123,7 @@ async function publishMessageStateForPortForwardResult(args, state, request, sta
2080
3123
  case "failed":
2081
3124
  await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
2082
3125
  status: "failed",
2083
- reason: "port_forward_denied"
3126
+ reason: failedReason
2084
3127
  });
2085
3128
  break;
2086
3129
  }
@@ -2151,7 +3194,12 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
2151
3194
  actorUserId: event.actorUserId,
2152
3195
  actorSlug: event.actorSlug
2153
3196
  });
2154
- if (result.ok === true) {
3197
+ if (result === undefined) {
3198
+ await publishKandanMessageState(args, event, {
3199
+ status: "failed",
3200
+ reason: "port_forward_decision_missing"
3201
+ });
3202
+ } else if (result.ok === true) {
2155
3203
  await publishKandanMessageState(args, event, { status: "processed" });
2156
3204
  } else {
2157
3205
  await publishKandanMessageState(args, event, {
@@ -2301,11 +3349,19 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2301
3349
  if (state.closed || state.turn.status !== "idle" || localTuiTurnIsActive(state)) {
2302
3350
  return;
2303
3351
  }
3352
+ const resumed = await tryResumeCodexThreadForPendingRuntimeSettings(args, state);
3353
+ if (!resumed) {
3354
+ return;
3355
+ }
2304
3356
  const next = dequeuePendingKandanMessage(state.queue);
2305
3357
  if (next === undefined) {
2306
3358
  return;
2307
3359
  }
2308
- state.turn = { status: "starting", queuedSeq: next.seq, interruptAfterStart: false };
3360
+ state.turn = {
3361
+ status: "starting",
3362
+ queuedSeq: next.seq,
3363
+ interruptAfterStart: false
3364
+ };
2309
3365
  state.activeProcessingState = { seq: next.seq, reason: "starting turn" };
2310
3366
  args.log("codex.turn_starting", {
2311
3367
  queued_seq: next.seq,
@@ -2328,10 +3384,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2328
3384
  }
2329
3385
  const started = await args.codex.request("turn/start", {
2330
3386
  threadId: codexThreadId,
2331
- input: await codexInputItemsForQueuedKandanMessage(args, next),
3387
+ input: await codexInputItemsForQueuedKandanMessage(args, next, state.pendingReconnectContextInjection),
2332
3388
  ...codexTurnRuntimeOverrides(runtimeOptionsForSettings(args.options, state.runtimeSettings))
2333
3389
  });
2334
3390
  const turnId = extractTurnIdFromResponse(started);
3391
+ state.pendingReconnectContextInjection = undefined;
2335
3392
  const interruptAfterStart = state.turn.status === "starting" && state.turn.interruptAfterStart;
2336
3393
  state.turn = { status: "active", turnId, queuedSeq: next.seq };
2337
3394
  rememberTurnReplyTarget(state, turnId, next.seq);
@@ -2357,12 +3414,14 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2357
3414
  await stopCodexTyping(args, state);
2358
3415
  const newCodexThreadId = await startCodexThread(args.codex, args.options);
2359
3416
  state.codexThreadId = newCodexThreadId;
3417
+ await bindCurrentCodexThread(args, state);
2360
3418
  args.log("codex.thread_rebound", {
2361
3419
  kandan_thread_id: state.kandanThreadId,
2362
3420
  old_codex_thread_id: oldCodexThreadId ?? null,
2363
3421
  new_codex_thread_id: newCodexThreadId
2364
3422
  });
2365
3423
  await postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId);
3424
+ state.pendingReconnectContextInjection = await fetchReconnectContextInjection(args, state);
2366
3425
  requeuePendingKandanMessageFront(state.queue, next);
2367
3426
  state.turn = { status: "idle" };
2368
3427
  await drainKandanMessageQueue(args, state, payloadContext);
@@ -2388,6 +3447,26 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2388
3447
  });
2389
3448
  }
2390
3449
  }
3450
+ async function fetchReconnectContextInjection(args, state) {
3451
+ if (state.kandanThreadId === undefined) {
3452
+ throw new Error("cannot fetch reconnect context without a Linzumi thread id");
3453
+ }
3454
+ const session = args.options.channelSession;
3455
+ const reply = await pushOk(args.kandan, args.topic, "session:thread_context", {
3456
+ workspace: session.workspaceSlug,
3457
+ channel: session.channelSlug,
3458
+ thread_id: state.kandanThreadId
3459
+ });
3460
+ const messages = parseReconnectContextMessages(reply.messages);
3461
+ const summarizer = args.options.reconnectContextSummarizer ?? createConfiguredReconnectContextSummarizer();
3462
+ const injection = await buildReconnectContextInjection(messages, summarizer);
3463
+ args.log("codex.thread_reconnect_context_prepared", {
3464
+ codex_thread_id: state.codexThreadId ?? null,
3465
+ kandan_thread_id: state.kandanThreadId,
3466
+ context_message_count: messages.length
3467
+ });
3468
+ return injection;
3469
+ }
2391
3470
  async function handleCodexServerRequest(args, state, payloadContext, request) {
2392
3471
  const params = objectValue(request.params) ?? {};
2393
3472
  const turnId = codexNotificationTurnId(params);
@@ -2425,35 +3504,48 @@ function codexApprovalRequestCanSurface(method) {
2425
3504
  return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval";
2426
3505
  }
2427
3506
  async function requestKandanApproval(args, state, request, turnId, payloadContext) {
2428
- const sourceSeq = activeQueuedSeqForTurn(state.turn, turnId);
2429
- if (sourceSeq === undefined || state.kandanThreadId === undefined) {
2430
- const message = `Codex approval request has no active Kandan source message: ${request.method}`;
2431
- await failActiveCodexTurn(args, state, turnId, message, payloadContext);
2432
- throw new Error(message);
2433
- }
2434
- const approval = codexApprovalMessageState(request);
2435
- state.activeProcessingState = { seq: sourceSeq, reason: "awaiting approval", approval };
2436
- await publishMessageState(args, state.kandanThreadId, sourceSeq, {
2437
- status: "processing",
2438
- reason: "awaiting approval",
2439
- approval
2440
- }, undefined, undefined, state.codexThreadId);
2441
- args.log("codex.approval_request_pending", {
2442
- request_id: approval.requestId,
2443
- source_seq: sourceSeq,
2444
- turn_id: turnId,
2445
- method: request.method
2446
- });
2447
- return new Promise((resolve2, reject) => {
2448
- const request2 = {
2449
- requestId: approval.requestId,
2450
- sourceSeq,
2451
- turnId,
2452
- resolve: resolve2,
2453
- reject
3507
+ const approvalResult = state.approvalPromptChain.then(async () => {
3508
+ const sourceSeq = activeQueuedSeqForTurn(state.turn, turnId);
3509
+ if (sourceSeq === undefined || state.kandanThreadId === undefined) {
3510
+ const message = `Codex approval request has no active Kandan source message: ${request.method}`;
3511
+ await failActiveCodexTurn(args, state, turnId, message, payloadContext);
3512
+ throw new Error(message);
3513
+ }
3514
+ const approval = codexApprovalMessageState(request);
3515
+ state.activeProcessingState = {
3516
+ seq: sourceSeq,
3517
+ reason: "awaiting approval",
3518
+ approval
2454
3519
  };
2455
- state.pendingApprovalRequests.set(approvalRequestKey(request2.requestId, request2.sourceSeq), request2);
3520
+ const approvalPromise = new Promise((resolve2, reject) => {
3521
+ const pendingRequest = {
3522
+ requestId: approval.requestId,
3523
+ sourceSeq,
3524
+ turnId,
3525
+ resolve: resolve2,
3526
+ reject
3527
+ };
3528
+ state.pendingApprovalRequests.set(approvalRequestKey(pendingRequest.requestId, pendingRequest.sourceSeq), pendingRequest);
3529
+ });
3530
+ await publishMessageState(args, state.kandanThreadId, sourceSeq, {
3531
+ status: "processing",
3532
+ reason: "awaiting approval",
3533
+ approval
3534
+ }, undefined, undefined, state.codexThreadId);
3535
+ args.log("codex.approval_request_pending", {
3536
+ request_id: approval.requestId,
3537
+ source_seq: sourceSeq,
3538
+ turn_id: turnId,
3539
+ method: request.method
3540
+ });
3541
+ return approvalPromise;
3542
+ });
3543
+ state.approvalPromptChain = approvalResult.then(() => {
3544
+ return;
3545
+ }, () => {
3546
+ return;
2456
3547
  });
3548
+ return approvalResult;
2457
3549
  }
2458
3550
  function rejectPendingApprovalRequests(state, error) {
2459
3551
  const pendingApprovals = [...state.pendingApprovalRequests.values()];
@@ -2479,7 +3571,11 @@ async function forwardCompletedCodexTurn(args, state, turnId, payloadContext) {
2479
3571
  const completingActiveTurn = completingQueuedSeq !== undefined;
2480
3572
  const completingLocalTuiTurn = isLocalTuiTurn(state, turnId);
2481
3573
  if (completingQueuedSeq !== undefined) {
2482
- state.turn = { status: "completing", turnId, queuedSeq: completingQueuedSeq };
3574
+ state.turn = {
3575
+ status: "completing",
3576
+ turnId,
3577
+ queuedSeq: completingQueuedSeq
3578
+ };
2483
3579
  }
2484
3580
  await waitForPendingTuiInputMirror(state, turnId);
2485
3581
  await waitForStreamingForwardChains(args, state, payloadContext);
@@ -2582,11 +3678,17 @@ async function forwardCompletedCodexTurn(args, state, turnId, payloadContext) {
2582
3678
  if (completingActiveTurn && state.turn.status === "completing" && state.turn.turnId === turnId) {
2583
3679
  state.turn = { status: "idle" };
2584
3680
  await stopCodexTyping(args, state);
2585
- await drainKandanMessageQueue(args, state, payloadContext);
3681
+ const resumed = await tryResumeCodexThreadForPendingRuntimeSettings(args, state);
3682
+ if (resumed) {
3683
+ await drainKandanMessageQueue(args, state, payloadContext);
3684
+ }
2586
3685
  }
2587
3686
  if (completingLocalTuiTurn && !completingActiveTurn) {
2588
3687
  await stopCodexTyping(args, state);
2589
- await drainKandanMessageQueue(args, state, payloadContext);
3688
+ const resumed = await tryResumeCodexThreadForPendingRuntimeSettings(args, state);
3689
+ if (resumed) {
3690
+ await drainKandanMessageQueue(args, state, payloadContext);
3691
+ }
2590
3692
  }
2591
3693
  }
2592
3694
  }
@@ -2841,7 +3943,10 @@ async function forwardReasoningDeltaPayload(args, state, delta, payloadContext)
2841
3943
  }
2842
3944
  } else {
2843
3945
  await editCodexStructuredOutput(args, state, existing.seq, nextContent, codexReasoningStructuredMessage(delta.itemKey, nextContent, "streaming"));
2844
- rememberStreamingReasoningOutput(state, { ...existing, content: nextContent });
3946
+ rememberStreamingReasoningOutput(state, {
3947
+ ...existing,
3948
+ content: nextContent
3949
+ });
2845
3950
  }
2846
3951
  args.log("kandan.codex_reasoning_delta_forwarded", {
2847
3952
  item_key: delta.itemKey,
@@ -3398,7 +4503,10 @@ function rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId) {
3398
4503
  if (state.kandanThreadId !== undefined) {
3399
4504
  startCodexTypingHeartbeat(args, state, state.kandanThreadId);
3400
4505
  }
3401
- args.log("codex.tui_turn_started", { turn_id: turnId, codex_thread_id: threadId });
4506
+ args.log("codex.tui_turn_started", {
4507
+ turn_id: turnId,
4508
+ codex_thread_id: threadId
4509
+ });
3402
4510
  }
3403
4511
  function isLocalTuiTurn(state, turnId) {
3404
4512
  return state.localTuiTurnIds.has(turnId);
@@ -3465,7 +4573,10 @@ function clearPendingStreamFlushTimers(state) {
3465
4573
  clearStreamDeltaFlushTimer(state.fileChangeQueue);
3466
4574
  }
3467
4575
  function rememberTurnReplyTarget(state, turnId, replyToSeq) {
3468
- rememberBoundedCacheValue(state.turnReplyTargets, turnId, { turnId, replyToSeq });
4576
+ rememberBoundedCacheValue(state.turnReplyTargets, turnId, {
4577
+ turnId,
4578
+ replyToSeq
4579
+ });
3469
4580
  }
3470
4581
  function sourceMessageSeqForTurn(state, turnId) {
3471
4582
  return getBoundedCacheValue(state.turnReplyTargets, turnId)?.replyToSeq;
@@ -3474,20 +4585,12 @@ function fileChangePaths(structured) {
3474
4585
  const changes = arrayValue(structured.changes) ?? [];
3475
4586
  return changes.filter(isJsonObject).map((change) => stringValue(change.path) ?? "").filter((path) => path.trim() !== "");
3476
4587
  }
3477
- async function postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId) {
4588
+ async function postCodexThreadReboundMessage(args, state, payloadContext, _oldCodexThreadId, newCodexThreadId) {
3478
4589
  if (state.kandanThreadId === undefined) {
3479
4590
  return;
3480
4591
  }
3481
4592
  const session = args.options.channelSession;
3482
- const body = [
3483
- "Codex reconnected.",
3484
- "",
3485
- "The previous local Codex app-server thread was not available in this process, so this runner started a new local Codex thread for this Kandan thread.",
3486
- "",
3487
- `Previous Codex thread: ${oldCodexThreadId ?? "unknown"}`,
3488
- `New Codex thread: ${newCodexThreadId}`
3489
- ].join(`
3490
- `);
4593
+ const body = "[codex reconnected to new thread]";
3491
4594
  await pushOk(args.kandan, args.topic, "session:post_thread_message", {
3492
4595
  workspace: session.workspaceSlug,
3493
4596
  channel: session.channelSlug,
@@ -3622,10 +4725,19 @@ async function publishMessageState(args, threadId, seq, state, actorSlug, actorU
3622
4725
  approval_request_id: state.approval.requestId,
3623
4726
  approval_kind: state.approval.kind,
3624
4727
  approval_summary: state.approval.summary,
4728
+ ...state.approval.port === undefined ? {} : { approval_port: state.approval.port },
4729
+ ...state.approval.pid === undefined ? {} : { approval_pid: state.approval.pid },
4730
+ ...state.approval.command === undefined ? {} : { approval_command: state.approval.command },
4731
+ ...state.approval.cwd === undefined ? {} : { approval_cwd: state.approval.cwd },
4732
+ ...state.approval.processName === undefined ? {} : { approval_process_name: state.approval.processName },
4733
+ ...state.approval.processIconKey === undefined ? {} : { approval_process_icon_key: state.approval.processIconKey },
4734
+ ...state.approval.processIconPath === undefined ? {} : { approval_process_icon_path: state.approval.processIconPath },
3625
4735
  ...state.approval.reason === undefined ? {} : { approval_reason: state.approval.reason },
3626
4736
  ...state.approval.choices === undefined ? {} : { approval_choices: state.approval.choices },
3627
4737
  ...state.approval.allowedActorSlug === undefined ? {} : { approval_allowed_actor_slug: state.approval.allowedActorSlug },
3628
- ...state.approval.allowedActorUserId === undefined ? {} : { approval_allowed_actor_user_id: state.approval.allowedActorUserId }
4738
+ ...state.approval.allowedActorUserId === undefined ? {} : {
4739
+ approval_allowed_actor_user_id: state.approval.allowedActorUserId
4740
+ }
3629
4741
  } : {},
3630
4742
  ...actorSlug === undefined ? {} : { actor_slug: actorSlug },
3631
4743
  ...actorUserId === undefined ? {} : { actor_user_id: actorUserId }
@@ -3679,11 +4791,15 @@ function clearActiveProcessingState(state, seq) {
3679
4791
  state.activeProcessingState = undefined;
3680
4792
  }
3681
4793
  }
3682
- async function codexInputItemsForQueuedKandanMessage(args, message) {
4794
+ async function codexInputItemsForQueuedKandanMessage(args, message, reconnectContextInjection = undefined) {
3683
4795
  const attachments = await downloadQueuedKandanAttachments(args, message);
3684
4796
  const text = appendDownloadedAttachmentContext(codexInputForQueuedKandanMessage(message), attachments);
3685
4797
  const imageItems = attachments.flatMap((attachment) => attachment.isImage ? [{ type: "localImage", path: attachment.path }] : []);
3686
- return [{ type: "text", text }, ...imageItems];
4798
+ return [
4799
+ ...reconnectContextInjection === undefined ? [] : [reconnectContextInjection],
4800
+ { type: "text", text },
4801
+ ...imageItems
4802
+ ];
3687
4803
  }
3688
4804
  async function downloadQueuedKandanAttachments(args, message) {
3689
4805
  if (message.attachments.length === 0) {
@@ -3816,10 +4932,11 @@ async function uploadedFileIdsForCodexOutput(args, body, structured) {
3816
4932
  throw new Error("Kandan attachment prepare response missing upload_url");
3817
4933
  }
3818
4934
  const bytes = await readFile(file.path);
4935
+ const uploadBody = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
3819
4936
  const response = await fetch(resolveKandanAttachmentUrl(args.options.kandanUrl, uploadUrl), {
3820
4937
  method: uploadMethod,
3821
4938
  headers: { "content-type": file.contentType },
3822
- body: bytes
4939
+ body: uploadBody
3823
4940
  });
3824
4941
  if (!response.ok) {
3825
4942
  throw new Error(`Kandan attachment upload failed for ${file.fileName}: ${response.status} ${response.statusText}`);
@@ -4059,7 +5176,7 @@ function defaultCliAuditLogFile() {
4059
5176
  return override === undefined || override === "" ? join2(homedir(), ".linzumi", "logs", "command-events.jsonl") : override;
4060
5177
  }
4061
5178
  function redactForCliLog(value) {
4062
- return redactValue(value, undefined);
5179
+ return redactObject(value);
4063
5180
  }
4064
5181
  function redactValue(value, key) {
4065
5182
  if (sensitiveKey(key)) {
@@ -4204,8 +5321,10 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
4204
5321
  const child = spawn(codexBin, args, {
4205
5322
  cwd,
4206
5323
  env: process.env,
4207
- stdio: ["ignore", "inherit", "inherit"]
5324
+ stdio: ["ignore", "inherit", "inherit"],
5325
+ detached: true
4208
5326
  });
5327
+ const stop = () => stopCodexAppServerProcess(child);
4209
5328
  writeCliAuditEvent("process.spawned", {
4210
5329
  command: codexBin,
4211
5330
  args,
@@ -4231,33 +5350,57 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
4231
5350
  try {
4232
5351
  await waitForReadyz(url, child);
4233
5352
  } catch (error) {
4234
- child.kill("SIGINT");
5353
+ stop();
4235
5354
  throw error;
4236
5355
  }
4237
- return { url, process: child };
5356
+ return { url, process: child, stop };
5357
+ }
5358
+ function stopCodexAppServerProcess(child, killProcess = process.kill) {
5359
+ if (child.pid !== undefined) {
5360
+ try {
5361
+ killProcess(-child.pid, "SIGINT");
5362
+ return;
5363
+ } catch (error) {
5364
+ logProcessGroupSignalFailure(child.pid, error);
5365
+ }
5366
+ }
5367
+ child.kill("SIGINT");
5368
+ }
5369
+ function logProcessGroupSignalFailure(pid, error) {
5370
+ const code = processSignalErrorCode(error);
5371
+ const message = error instanceof Error ? error.message : String(error);
5372
+ const event = code === "EPERM" ? "process.group_signal_denied" : code === "ESRCH" ? "process.group_signal_missing" : "process.group_signal_failed";
5373
+ writeCliAuditEvent(event, {
5374
+ pid,
5375
+ signal: "SIGINT",
5376
+ code,
5377
+ message,
5378
+ fallback: "child_signal",
5379
+ purpose: "codex.app_server"
5380
+ });
5381
+ if (code === "EPERM") {
5382
+ process.stderr.write(`codex app-server process-group SIGINT denied for pid ${pid}; falling back to direct child SIGINT
5383
+ `);
5384
+ }
5385
+ }
5386
+ function processSignalErrorCode(error) {
5387
+ if (error !== null && typeof error === "object" && "code" in error) {
5388
+ const code = error.code;
5389
+ return typeof code === "string" ? code : undefined;
5390
+ }
5391
+ return;
4238
5392
  }
4239
5393
  function codexAppServerArgs(listenUrl, options = {}) {
4240
- return [
4241
- "app-server",
4242
- ...codexConfigArgs(options),
4243
- "--listen",
4244
- listenUrl
4245
- ];
5394
+ return ["app-server", ...codexConfigArgs(options), "--listen", listenUrl];
4246
5395
  }
4247
5396
  function codexConfigArgs(options) {
4248
5397
  return [
4249
- ...options.model === undefined ? [] : [
4250
- "-c",
4251
- `model=${JSON.stringify(options.model)}`
4252
- ],
5398
+ ...options.model === undefined ? [] : ["-c", `model=${JSON.stringify(options.model)}`],
4253
5399
  ...options.reasoningEffort === undefined ? [] : [
4254
5400
  "-c",
4255
5401
  `model_reasoning_effort=${JSON.stringify(options.reasoningEffort)}`
4256
5402
  ],
4257
- ...options.fast === true ? [
4258
- "-c",
4259
- `service_tier=${JSON.stringify("fast")}`
4260
- ] : []
5403
+ ...options.fast === true ? ["-c", `service_tier=${JSON.stringify("fast")}`] : []
4261
5404
  ];
4262
5405
  }
4263
5406
  async function connectCodexAppServer(websocketUrl, socketFactory = (url) => new WebSocket(url)) {
@@ -4611,7 +5754,9 @@ async function handleForwardHttpRequest(control, allowedPorts) {
4611
5754
  method: control.method,
4612
5755
  headers: requestHeaders(control.headers),
4613
5756
  redirect: "manual",
4614
- ...bodyDecision.body === undefined ? {} : { body: bodyDecision.body }
5757
+ ...bodyDecision.body === undefined ? {} : {
5758
+ body: bodyDecision.body.buffer.slice(bodyDecision.body.byteOffset, bodyDecision.body.byteOffset + bodyDecision.body.byteLength)
5759
+ }
4615
5760
  };
4616
5761
  const response = await fetchWithHttpsFallback(control.port, control.path, control.queryString, request);
4617
5762
  const upstreamBuffer = Buffer.from(await response.arrayBuffer());
@@ -5081,10 +6226,12 @@ function prepareCodeServerProfile(collaboration, editorRuntime) {
5081
6226
  const userDataDir = mkdtempSync(join4(tmpdir(), "kandan-local-editor-"));
5082
6227
  const extensionsDir = join4(userDataDir, "extensions");
5083
6228
  const collaborationServerDir = join4(userDataDir, "collaboration-server");
6229
+ const tempDir = join4(userDataDir, "tmp");
5084
6230
  const userSettingsDir = join4(userDataDir, "User");
5085
6231
  mkdirSync3(userSettingsDir, { recursive: true });
5086
6232
  mkdirSync3(extensionsDir, { recursive: true });
5087
6233
  mkdirSync3(collaborationServerDir, { recursive: true });
6234
+ mkdirSync3(tempDir, { recursive: true });
5088
6235
  if (editorRuntime !== undefined) {
5089
6236
  installDirectory(editorRuntime.assets.documentStateExtensionDir, join4(extensionsDir, "kandan.document-state-telemetry"));
5090
6237
  }
@@ -5117,6 +6264,12 @@ function prepareCodeServerLaunch(options) {
5117
6264
  "-p",
5118
6265
  codeServerSandboxProfile(options, codeServerExecutable.directory),
5119
6266
  "--",
6267
+ "/bin/sh",
6268
+ "-c",
6269
+ 'export HOME="$1"; export PWD="$1"; export TMPDIR="$2"; export TMP="$2"; export TEMP="$2"; shift 2; exec "$@"',
6270
+ "kandan-code-server-env",
6271
+ options.cwd,
6272
+ join4(options.userDataDir, "tmp"),
5120
6273
  codeServerExecutable.command,
5121
6274
  ...codeServerArgs(options.port, options.cwd, options.userDataDir, options.extensionsDir)
5122
6275
  ]
@@ -5200,7 +6353,6 @@ function codeServerSandboxProfile(options, codeServerBinDir) {
5200
6353
  "/etc",
5201
6354
  "/private/etc",
5202
6355
  "/opt/homebrew",
5203
- "/dev",
5204
6356
  ...options.codeServerRuntimeRoot === undefined ? [] : sandboxPathAliases(options.codeServerRuntimeRoot),
5205
6357
  codeServerBinDir
5206
6358
  ]);
@@ -5223,6 +6375,7 @@ function codeServerSandboxProfile(options, codeServerBinDir) {
5223
6375
  "(allow file-map-executable)",
5224
6376
  '(allow file-read* (literal "/") (literal "/private") (literal "/private/var"))',
5225
6377
  `(allow file-read* ${readOnlyRoots.map(sandboxSubpath).join(" ")})`,
6378
+ '(allow file-read* file-write* (subpath "/dev"))',
5226
6379
  `(allow file-read* file-write* ${readWriteRoots.map(sandboxSubpath).join(" ")})`
5227
6380
  ].join(`
5228
6381
  `);
@@ -5397,10 +6550,14 @@ function installDirectory(sourceDir, destinationDir) {
5397
6550
  }
5398
6551
  function codeServerEnv(env, cwd, userDataDir, collaboration) {
5399
6552
  const { PORT: _port, ...hostEnv } = env;
6553
+ const tempDir = join4(userDataDir, "tmp");
5400
6554
  const base = {
5401
6555
  ...hostEnv,
5402
6556
  HOME: cwd,
5403
6557
  PWD: cwd,
6558
+ TMPDIR: tempDir,
6559
+ TMP: tempDir,
6560
+ TEMP: tempDir,
5404
6561
  XDG_CACHE_HOME: join4(userDataDir, "xdg-cache"),
5405
6562
  XDG_CONFIG_HOME: join4(userDataDir, "xdg-config"),
5406
6563
  XDG_DATA_HOME: join4(userDataDir, "xdg-data")
@@ -5789,7 +6946,7 @@ async function exchangeCodeForToken(args) {
5789
6946
  };
5790
6947
  }
5791
6948
  function stringBodyField(body, key) {
5792
- const value = key in body ? body[key] : undefined;
6949
+ const value = body[key];
5793
6950
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
5794
6951
  }
5795
6952
  function startCallbackServer(args) {
@@ -5859,7 +7016,9 @@ function isTcpAddress(address) {
5859
7016
  return typeof address === "object" && address !== null && typeof address.port === "number";
5860
7017
  }
5861
7018
  function writeOauthResult(response, args) {
5862
- response.writeHead(args.status, { "content-type": "text/html; charset=utf-8" });
7019
+ response.writeHead(args.status, {
7020
+ "content-type": "text/html; charset=utf-8"
7021
+ });
5863
7022
  response.end(oauthResultHtml(args));
5864
7023
  }
5865
7024
  function oauthResultHtml(args) {
@@ -6474,7 +7633,7 @@ function assertStartDependencies(status) {
6474
7633
  throw new Error(`Codex is not available at ${status.codex.command}. Install Codex or pass --codex-bin <path>.`);
6475
7634
  }
6476
7635
  if (status.editorRuntime?.status === "unavailable" || status.codeServer?.available === false && status.codeServer.reason !== "not_configured") {
6477
- throw new Error("The Kandan editor runtime is not available. Reconnect when the runtime update finishes or pass --code-server-bin <path> for a custom development runtime.");
7636
+ throw new Error("The Linzumi editor runtime is not available. Reconnect when the runtime update finishes or pass --code-server-bin <path> for a custom development runtime.");
6478
7637
  }
6479
7638
  }
6480
7639
  function dependencyStatusPayload(status) {
@@ -6858,7 +8017,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6858
8017
  forwardPortAttributions.set(port, {
6859
8018
  kandanThreadId: attribution.kandanThreadId ?? null,
6860
8019
  codexThreadId: attribution.codexThreadId ?? null,
6861
- channelSlug: attribution.channelSlug ?? null
8020
+ channelSlug: attribution.channelSlug ?? null,
8021
+ processName: attribution.processName ?? null,
8022
+ processIconKey: attribution.processIconKey ?? null
6862
8023
  });
6863
8024
  };
6864
8025
  const clearForwardPortAttribution = (port) => {
@@ -6870,10 +8031,15 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6870
8031
  port,
6871
8032
  kandanThreadId: attribution?.kandanThreadId ?? null,
6872
8033
  codexThreadId: attribution?.codexThreadId ?? null,
6873
- channelSlug: attribution?.channelSlug ?? null
8034
+ channelSlug: attribution?.channelSlug ?? null,
8035
+ processName: attribution?.processName ?? null,
8036
+ processIconKey: attribution?.processIconKey ?? null
6874
8037
  };
6875
8038
  });
6876
8039
  const allowedCwds = { value: [...options.allowedCwds] };
8040
+ const missingAllowedCwds = {
8041
+ value: normalizeAllowedCwds(options.missingAllowedCwds ?? [])
8042
+ };
6877
8043
  const localEditorState = {
6878
8044
  value: { status: "disabled" }
6879
8045
  };
@@ -6889,6 +8055,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6889
8055
  codexRemoteTui: true,
6890
8056
  startInstance: allowedCwds.value.length > 0,
6891
8057
  allowedCwds: allowedCwds.value,
8058
+ missingAllowedCwds: missingAllowedCwds.value,
6892
8059
  allowedCwdSuggestions: allowedCwdSuggestions(options.cwd, allowedCwds.value),
6893
8060
  portForwarding: liveForwardPorts.size > 0,
6894
8061
  allowedPorts: Array.from(liveForwardPorts).sort((left, right) => left - right),
@@ -6903,7 +8070,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6903
8070
  const joinPayload = () => ({
6904
8071
  clientName: "kandan-local-codex-runner",
6905
8072
  version: "0.0.1",
6906
- workspace: options.channelSession?.workspaceSlug ?? null,
8073
+ workspace: runnerWorkspaceSlug(options) ?? null,
6907
8074
  channel: options.channelSession?.channelSlug ?? null,
6908
8075
  capabilities: capabilitiesPayload()
6909
8076
  });
@@ -6921,7 +8088,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6921
8088
  const started = options.codexUrl === undefined ? await startOwnedCodexAppServer(options) : undefined;
6922
8089
  if (started !== undefined) {
6923
8090
  cleanup.actions.push(() => {
6924
- started.process.kill("SIGINT");
8091
+ started.stop();
6925
8092
  });
6926
8093
  }
6927
8094
  const codexUrl = options.codexUrl ?? started?.url;
@@ -6971,7 +8138,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6971
8138
  cleanup.actions.push(() => codex.close());
6972
8139
  const seq = { value: 0 };
6973
8140
  const codexThreads = options.channelSession === undefined ? await discoverCodexThreads(codex, options.cwd) : [];
8141
+ const discoveredCodexThreads = { value: codexThreads };
6974
8142
  const runnerHost = hostname2();
8143
+ const runtimeDefaults = runnerRuntimeDefaults(options);
6975
8144
  const instancePayload = {
6976
8145
  instanceId,
6977
8146
  codexUrl,
@@ -6979,8 +8148,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6979
8148
  cwd: options.cwd,
6980
8149
  hostname: runnerHost,
6981
8150
  codexThreads,
6982
- model: options.channelSession?.model ?? null,
6983
- reasoningEffort: options.channelSession?.reasoningEffort ?? null,
8151
+ workspace: runnerWorkspaceSlug(options) ?? null,
8152
+ channel: options.channelSession?.channelSlug ?? null,
8153
+ model: runtimeDefaults.model ?? null,
8154
+ reasoningEffort: runtimeDefaults.reasoningEffort ?? null,
6984
8155
  fast: options.fast ?? false
6985
8156
  };
6986
8157
  await kandan.push(topic, "instance_started", instancePayload);
@@ -7019,17 +8190,22 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7019
8190
  kandan.onReconnect(() => channelSession.handleKandanReconnect());
7020
8191
  }
7021
8192
  const dynamicChannelSessions = new Map;
8193
+ kandan.onReconnect(() => rebindDynamicChannelSessionsOnReconnect(dynamicChannelSessions.values()));
7022
8194
  cleanup.actions.push(async () => {
7023
8195
  await Promise.all(Array.from(dynamicChannelSessions.values(), (session) => session.close()));
7024
8196
  dynamicChannelSessions.clear();
7025
8197
  });
7026
- const attachStartedThreadSession = async (control, cwd, codexThreadId) => {
7027
- const workspaceSlug = normalizedWorkDescription(control.workspace);
7028
- const channelSlug = normalizedWorkDescription(control.channel);
7029
- const kandanThreadId = normalizedWorkDescription(control.threadId);
7030
- if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined || dynamicChannelSessions.has(kandanThreadId)) {
8198
+ const attachThreadSession = async (control, cwd, codexThreadId) => {
8199
+ const workspaceSlug = optionalThreadControlField(control, "workspace");
8200
+ const channelSlug = optionalThreadControlField(control, "channel");
8201
+ const kandanThreadId = optionalThreadControlField(control, "threadId");
8202
+ if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined) {
7031
8203
  return;
7032
8204
  }
8205
+ const existingSession = dynamicChannelSessions.get(kandanThreadId);
8206
+ if (existingSession !== undefined) {
8207
+ return existingSession;
8208
+ }
7033
8209
  const listenUser = options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
7034
8210
  if (listenUser === undefined) {
7035
8211
  throw new Error("missing listen user for Commander-started Codex session");
@@ -7083,12 +8259,13 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7083
8259
  codexUrl,
7084
8260
  cwd: options.cwd,
7085
8261
  hostname: runnerHost,
7086
- workspace: options.channelSession?.workspaceSlug ?? null,
8262
+ workspace: runnerWorkspaceSlug(options) ?? null,
7087
8263
  channel: options.channelSession?.channelSlug ?? null,
7088
8264
  threadId: channelSession?.currentKandanThreadId() ?? null,
7089
8265
  codexThreadId: channelSession?.currentCodexThreadId() ?? null,
7090
- model: options.channelSession?.model ?? null,
7091
- reasoningEffort: options.channelSession?.reasoningEffort ?? null,
8266
+ codexThreads: discoveredCodexThreads.value,
8267
+ model: runtimeDefaults.model ?? null,
8268
+ reasoningEffort: runtimeDefaults.reasoningEffort ?? null,
7092
8269
  fast: options.fast ?? false,
7093
8270
  capabilities: capabilitiesPayload()
7094
8271
  });
@@ -7097,11 +8274,19 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7097
8274
  message: error instanceof Error ? error.message : String(error)
7098
8275
  });
7099
8276
  });
8277
+ const refreshDiscoveredCodexThreads = () => discoverCodexThreads(codex, options.cwd).then((threads) => {
8278
+ discoveredCodexThreads.value = threads;
8279
+ return kandan.push(topic, "heartbeat", heartbeatPayload());
8280
+ }).catch((error) => {
8281
+ log("kandan.codex_threads_refresh_failed", {
8282
+ message: error instanceof Error ? error.message : String(error)
8283
+ });
8284
+ });
7100
8285
  const heartbeatInterval = setInterval(() => {
7101
- pushHeartbeat();
8286
+ channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat();
7102
8287
  }, 15000);
7103
8288
  cleanup.actions.push(() => clearInterval(heartbeatInterval));
7104
- kandan.onReconnect(() => pushHeartbeat().then(() => {
8289
+ kandan.onReconnect(() => (channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat()).then(() => {
7105
8290
  return;
7106
8291
  }));
7107
8292
  pushHeartbeat();
@@ -7135,6 +8320,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7135
8320
  });
7136
8321
  });
7137
8322
  }
8323
+ if (channelSession === undefined && notification.method === "thread/started") {
8324
+ refreshDiscoveredCodexThreads();
8325
+ }
7138
8326
  log("codex.notification", {
7139
8327
  method: notification.method,
7140
8328
  metadata
@@ -7242,7 +8430,33 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7242
8430
  return;
7243
8431
  }
7244
8432
  if (isUpdateRunnerConfigControl(control)) {
7245
- allowedCwds.value = normalizeAllowedCwds(control.allowedCwds);
8433
+ const updatedAllowedCwds = configuredAllowedCwds(control.allowedCwds);
8434
+ allowedCwds.value = updatedAllowedCwds.allowedCwds;
8435
+ missingAllowedCwds.value = updatedAllowedCwds.missingAllowedCwds;
8436
+ pushHeartbeat();
8437
+ return;
8438
+ }
8439
+ if (isSetPortForwardEnabledControl(control)) {
8440
+ switch (control.enabled) {
8441
+ case true:
8442
+ liveForwardPorts.add(control.port);
8443
+ break;
8444
+ case false:
8445
+ liveForwardPorts.delete(control.port);
8446
+ managedForwardPorts.delete(control.port);
8447
+ clearForwardPortAttribution(control.port);
8448
+ kandan.push(topic, "forward_port_revoked", {
8449
+ instanceId,
8450
+ port: control.port,
8451
+ reason: "user_disabled",
8452
+ capabilities: revocationCapabilities(capabilitiesPayload(), control.port)
8453
+ }).catch((error) => {
8454
+ log("kandan.forward_port_revoked_push_failed", {
8455
+ message: error instanceof Error ? error.message : String(error)
8456
+ });
8457
+ });
8458
+ break;
8459
+ }
7246
8460
  pushHeartbeat();
7247
8461
  return;
7248
8462
  }
@@ -7250,7 +8464,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7250
8464
  if (handled !== undefined) {
7251
8465
  return handled;
7252
8466
  }
7253
- return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control, attachStartedThreadSession);
8467
+ return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control, log, attachThreadSession);
7254
8468
  }).then((response) => {
7255
8469
  return kandan.push(topic, "codex_response", response);
7256
8470
  }).catch((error) => {
@@ -7315,6 +8529,8 @@ async function discoverCodexThreads(codex, cwd) {
7315
8529
  const data = arrayValue(result?.data);
7316
8530
  return data === undefined ? [] : data.filter(isJsonObject).map((thread) => ({
7317
8531
  id: stringValue(thread.id) ?? "",
8532
+ title: stringValue(thread.title) ?? stringValue(thread.name) ?? stringValue(thread.preview) ?? "",
8533
+ description: stringValue(thread.description) ?? stringValue(thread.summary) ?? stringValue(thread.preview) ?? "",
7318
8534
  preview: stringValue(thread.preview) ?? "",
7319
8535
  cwd: stringValue(thread.cwd) ?? "",
7320
8536
  source: stringValue(thread.source) ?? "",
@@ -7324,7 +8540,7 @@ async function discoverCodexThreads(codex, cwd) {
7324
8540
  }
7325
8541
  function extractStartedThreadId(response) {
7326
8542
  if ("error" in response) {
7327
- return;
8543
+ throw new Error(`thread/start failed: ${response.error.message}`);
7328
8544
  }
7329
8545
  return stringValue(objectValue(objectValue(response.result)?.thread)?.id);
7330
8546
  }
@@ -7390,6 +8606,9 @@ function launchCodexTui(codexBin, codexUrl, cwd, codexThreadId, session, fast) {
7390
8606
  });
7391
8607
  return child;
7392
8608
  }
8609
+ async function rebindDynamicChannelSessionsOnReconnect(sessions) {
8610
+ await Promise.all(Array.from(sessions, (session) => session.handleKandanReconnect()));
8611
+ }
7393
8612
  function forwardedHeaderValue(headers, name) {
7394
8613
  if (!Array.isArray(headers)) {
7395
8614
  return;
@@ -7431,9 +8650,85 @@ async function prepareCodexThreadForTuiResume(codex, codexThreadId) {
7431
8650
  throw new Error(`failed to verify Codex TUI resume: ${verified.error.message}`);
7432
8651
  }
7433
8652
  }
7434
- async function applyControl(codex, kandan, topic, instanceId, options, allowedCwds, control, onStartedThread) {
8653
+ async function applyControl(codex, kandan, topic, instanceId, options, allowedCwds, control, log, onStartedThread) {
7435
8654
  switch (control.type) {
7436
8655
  case "start_instance": {
8656
+ const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
8657
+ if (!cwd.ok) {
8658
+ return {
8659
+ instanceId,
8660
+ controlType: control.type,
8661
+ ok: false,
8662
+ error: cwd.reason
8663
+ };
8664
+ }
8665
+ const processingStatePayload = startInstanceMessageStatePayload(control, "processing", "starting Codex session");
8666
+ if (processingStatePayload !== undefined) {
8667
+ kandan.push(topic, "message_state", processingStatePayload).catch((error) => {
8668
+ log("kandan.start_instance_processing_state_push_failed", {
8669
+ message: error instanceof Error ? error.message : String(error)
8670
+ });
8671
+ });
8672
+ }
8673
+ try {
8674
+ if (options.codexUrl === undefined) {
8675
+ ensureCodexProjectTrusted(cwd.cwd);
8676
+ }
8677
+ const developerPrompt = normalizedWorkDescription(control.developerPrompt);
8678
+ const runtimeSettings = startInstanceRuntimeSettings(options, control);
8679
+ const response = await codex.request("thread/start", {
8680
+ cwd: cwd.cwd,
8681
+ serviceName: "kandan-local-runner",
8682
+ personality: "pragmatic",
8683
+ developerInstructions: commanderDeveloperInstructions({
8684
+ cwd: cwd.cwd,
8685
+ developerPrompt
8686
+ }),
8687
+ ...runtimeSettings.model === undefined ? {} : { model: runtimeSettings.model },
8688
+ ...runtimeSettings.reasoningEffort === undefined ? {} : { reasoningEffort: runtimeSettings.reasoningEffort },
8689
+ ...runtimeSettings.approvalPolicy === undefined ? {} : { approvalPolicy: runtimeSettings.approvalPolicy },
8690
+ ...runtimeSettings.sandbox === undefined ? {} : { sandbox: runtimeSettings.sandbox },
8691
+ ...runtimeSettings.fast === true ? { serviceTier: "fast" } : {}
8692
+ });
8693
+ const codexThreadId = extractStartedThreadId(response);
8694
+ const workDescription = normalizedWorkDescription(control.workDescription);
8695
+ if (codexThreadId !== undefined && developerPrompt !== undefined) {
8696
+ await postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId);
8697
+ }
8698
+ const startedThreadSession = codexThreadId !== undefined && onStartedThread !== undefined ? await onStartedThread(control, cwd.cwd, codexThreadId) : undefined;
8699
+ if (codexThreadId !== undefined && workDescription !== undefined) {
8700
+ const rootSeq = integerValue(control.rootSeq);
8701
+ const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
8702
+ if (startedThreadSession !== undefined && sourceSeq !== undefined) {
8703
+ const identity = identityFromAccessToken(options.token);
8704
+ await startedThreadSession.startThreadMessageTurn({
8705
+ seq: sourceSeq,
8706
+ body: workDescription,
8707
+ actorSlug: identity.actorUsername,
8708
+ actorUserId: identity.actorUserId
8709
+ });
8710
+ } else {
8711
+ await codex.request("turn/start", {
8712
+ threadId: codexThreadId,
8713
+ input: [{ type: "text", text: workDescription }]
8714
+ });
8715
+ }
8716
+ }
8717
+ return {
8718
+ instanceId,
8719
+ controlType: control.type,
8720
+ cwd: cwd.cwd,
8721
+ matchedRoot: cwd.matchedRoot,
8722
+ response
8723
+ };
8724
+ } catch (error) {
8725
+ try {
8726
+ await publishStartInstanceMessageState(kandan, topic, control, "failed", "failed to start Codex session");
8727
+ } catch (_publishError) {}
8728
+ throw error;
8729
+ }
8730
+ }
8731
+ case "reconnect_thread": {
7437
8732
  const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
7438
8733
  if (!cwd.ok) {
7439
8734
  return {
@@ -7446,51 +8741,37 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7446
8741
  if (options.codexUrl === undefined) {
7447
8742
  ensureCodexProjectTrusted(cwd.cwd);
7448
8743
  }
7449
- const developerPrompt = normalizedWorkDescription(control.developerPrompt);
7450
- const runtimeSettings = startInstanceRuntimeSettings(options, control);
7451
- const response = await codex.request("thread/start", {
7452
- cwd: cwd.cwd,
7453
- serviceName: "kandan-local-runner",
7454
- personality: "pragmatic",
7455
- developerInstructions: commanderDeveloperInstructions({
7456
- cwd: cwd.cwd,
7457
- developerPrompt
7458
- }),
7459
- ...runtimeSettings.model === undefined ? {} : { model: runtimeSettings.model },
7460
- ...runtimeSettings.reasoningEffort === undefined ? {} : { reasoningEffort: runtimeSettings.reasoningEffort },
7461
- ...runtimeSettings.approvalPolicy === undefined ? {} : { approvalPolicy: runtimeSettings.approvalPolicy },
7462
- ...runtimeSettings.sandbox === undefined ? {} : { sandbox: runtimeSettings.sandbox },
7463
- ...runtimeSettings.fast === true ? { serviceTier: "fast" } : {}
7464
- });
7465
- const codexThreadId = extractStartedThreadId(response);
7466
- const workDescription = normalizedWorkDescription(control.workDescription);
7467
- if (codexThreadId !== undefined && developerPrompt !== undefined) {
7468
- await postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId);
8744
+ const codexThreadId = normalizedWorkDescription(control.codexThreadId);
8745
+ if (codexThreadId === undefined) {
8746
+ return {
8747
+ instanceId,
8748
+ controlType: control.type,
8749
+ ok: false,
8750
+ error: "missing_codex_thread_id"
8751
+ };
7469
8752
  }
7470
- const startedThreadSession = codexThreadId !== undefined && onStartedThread !== undefined ? await onStartedThread(control, cwd.cwd, codexThreadId) : undefined;
7471
- if (codexThreadId !== undefined && workDescription !== undefined) {
8753
+ const workDescription = normalizedWorkDescription(control.workDescription);
8754
+ const startedThreadSession = onStartedThread === undefined ? undefined : await onStartedThread(control, cwd.cwd, codexThreadId);
8755
+ if (workDescription !== undefined) {
7472
8756
  const rootSeq = integerValue(control.rootSeq);
7473
- if (startedThreadSession !== undefined && rootSeq !== undefined) {
7474
- const identity = identityFromAccessToken(options.token);
7475
- await startedThreadSession.startThreadMessageTurn({
7476
- seq: rootSeq,
7477
- body: workDescription,
7478
- actorSlug: identity.actorUsername,
7479
- actorUserId: identity.actorUserId
7480
- });
7481
- } else {
7482
- await codex.request("turn/start", {
7483
- threadId: codexThreadId,
7484
- input: [{ type: "text", text: workDescription }]
7485
- });
8757
+ const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
8758
+ if (startedThreadSession === undefined || sourceSeq === undefined) {
8759
+ throw new Error("cannot reconnect a Kandan thread without a session");
7486
8760
  }
8761
+ const identity = identityFromAccessToken(options.token);
8762
+ await startedThreadSession.startThreadMessageTurn({
8763
+ seq: sourceSeq,
8764
+ body: workDescription,
8765
+ actorSlug: identity.actorUsername,
8766
+ actorUserId: identity.actorUserId
8767
+ });
7487
8768
  }
7488
8769
  return {
7489
8770
  instanceId,
7490
8771
  controlType: control.type,
7491
8772
  cwd: cwd.cwd,
7492
8773
  matchedRoot: cwd.matchedRoot,
7493
- response
8774
+ codexThreadId
7494
8775
  };
7495
8776
  }
7496
8777
  case "start_turn": {
@@ -7543,6 +8824,8 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7543
8824
  case "interrupt_queued_messages":
7544
8825
  case "resolve_codex_approval_request":
7545
8826
  case "resolve_port_forward_request":
8827
+ case "update_session_settings":
8828
+ case "set_port_forward_enabled":
7546
8829
  case "forward_http_request":
7547
8830
  case "forward_websocket_open":
7548
8831
  case "forward_websocket_send":
@@ -7608,9 +8891,9 @@ secure tunnel, and keep the Linzumi thread truthful.
7608
8891
  </task_reminder>`;
7609
8892
  }
7610
8893
  async function postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId) {
7611
- const workspace = normalizedWorkDescription(control.workspace);
7612
- const channel = normalizedWorkDescription(control.channel);
7613
- const threadId = normalizedWorkDescription(control.threadId);
8894
+ const workspace = optionalThreadControlField(control, "workspace");
8895
+ const channel = optionalThreadControlField(control, "channel");
8896
+ const threadId = optionalThreadControlField(control, "threadId");
7614
8897
  if (workspace === undefined || channel === undefined || threadId === undefined) {
7615
8898
  return;
7616
8899
  }
@@ -7632,33 +8915,116 @@ ${developerPrompt}`,
7632
8915
  client_message_id: `codex-start-instructions-${threadId}`
7633
8916
  });
7634
8917
  }
8918
+ async function publishStartInstanceMessageState(kandan, topic, control, status, reason) {
8919
+ const payload = startInstanceMessageStatePayload(control, status, reason);
8920
+ if (payload === undefined) {
8921
+ return;
8922
+ }
8923
+ await kandan.push(topic, "message_state", payload);
8924
+ }
8925
+ function startInstanceMessageStatePayload(control, status, reason) {
8926
+ const rootSeq = integerValue(control.rootSeq);
8927
+ const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
8928
+ if (sourceSeq === undefined) {
8929
+ return;
8930
+ }
8931
+ const workspace = requiredStartInstanceControlField(control, "workspace");
8932
+ const channel = requiredStartInstanceControlField(control, "channel");
8933
+ const threadId = requiredStartInstanceControlField(control, "threadId");
8934
+ return {
8935
+ workspace,
8936
+ channel,
8937
+ thread_id: threadId,
8938
+ seq: sourceSeq,
8939
+ status,
8940
+ reason
8941
+ };
8942
+ }
8943
+ function requiredStartInstanceControlField(control, field) {
8944
+ return requiredThreadControlField(control, field);
8945
+ }
8946
+ function requiredThreadControlField(control, field) {
8947
+ const value = optionalThreadControlField(control, field);
8948
+ if (value === undefined) {
8949
+ throw new Error(`${control.type} control missing ${field}`);
8950
+ }
8951
+ return value;
8952
+ }
8953
+ function optionalThreadControlField(control, field) {
8954
+ const value = stringValue(control[field])?.trim();
8955
+ if (value === undefined || value === "") {
8956
+ return;
8957
+ }
8958
+ return value;
8959
+ }
7635
8960
  function startInstanceRuntimeSettings(options, control) {
7636
- const session = options.channelSession;
8961
+ const defaults = runnerRuntimeDefaults(options);
7637
8962
  return {
7638
- model: control.model ?? session?.model,
7639
- reasoningEffort: control.reasoningEffort ?? session?.reasoningEffort,
7640
- approvalPolicy: control.approvalPolicy ?? session?.approvalPolicy,
7641
- sandbox: control.sandbox ?? session?.sandbox,
8963
+ model: control.model ?? defaults.model,
8964
+ reasoningEffort: control.reasoningEffort ?? defaults.reasoningEffort,
8965
+ approvalPolicy: control.approvalPolicy ?? defaults.approvalPolicy,
8966
+ sandbox: control.sandbox ?? defaults.sandbox,
7642
8967
  fast: control.fast ?? options.fast
7643
8968
  };
7644
8969
  }
7645
8970
  async function startOwnedCodexAppServer(options) {
7646
8971
  ensureCodexProjectTrusted(options.cwd);
8972
+ const defaults = runnerRuntimeDefaults(options);
7647
8973
  return await startCodexAppServer(options.codexBin, options.cwd, {
7648
- model: options.channelSession?.model,
7649
- reasoningEffort: options.channelSession?.reasoningEffort,
8974
+ model: defaults.model,
8975
+ reasoningEffort: defaults.reasoningEffort,
7650
8976
  fast: options.fast
7651
8977
  });
7652
8978
  }
8979
+ function runnerWorkspaceSlug(options) {
8980
+ return options.channelSession?.workspaceSlug ?? options.workspaceSlug;
8981
+ }
8982
+ function runnerRuntimeDefaults(options) {
8983
+ const session = options.channelSession;
8984
+ const defaults = options.runtimeDefaults;
8985
+ return {
8986
+ model: defaults?.model ?? session?.model,
8987
+ reasoningEffort: defaults?.reasoningEffort ?? session?.reasoningEffort,
8988
+ approvalPolicy: defaults?.approvalPolicy ?? session?.approvalPolicy,
8989
+ sandbox: defaults?.sandbox ?? session?.sandbox
8990
+ };
8991
+ }
7653
8992
  function isUpdateRunnerConfigControl(control) {
7654
8993
  return control.type === "update_runner_config";
7655
8994
  }
8995
+ function isSetPortForwardEnabledControl(control) {
8996
+ return control.type === "set_port_forward_enabled" && Number.isInteger(control.port) && control.port > 0 && control.port <= 65535;
8997
+ }
7656
8998
  function normalizeAllowedCwds(values) {
7657
8999
  return Array.from(new Set(values.flatMap((value) => {
7658
9000
  const normalized = value.trim();
7659
9001
  return normalized === "" ? [] : [normalized];
7660
9002
  })));
7661
9003
  }
9004
+ function configuredAllowedCwds(values) {
9005
+ const allowedCwds = [];
9006
+ const missingAllowedCwds = [];
9007
+ for (const value of normalizeAllowedCwds(values)) {
9008
+ const absolutePath = resolve5(expandUserPath(value));
9009
+ try {
9010
+ const realPath = realpathSync4(absolutePath);
9011
+ allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
9012
+ } catch (error) {
9013
+ if (isMissingAllowedCwdError(error)) {
9014
+ missingAllowedCwds.push(absolutePath);
9015
+ continue;
9016
+ }
9017
+ throw error;
9018
+ }
9019
+ }
9020
+ return {
9021
+ allowedCwds: normalizeAllowedCwds(allowedCwds),
9022
+ missingAllowedCwds: normalizeAllowedCwds(missingAllowedCwds)
9023
+ };
9024
+ }
9025
+ function isMissingAllowedCwdError(error) {
9026
+ return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "ELOOP" || error.code === "EIO");
9027
+ }
7662
9028
  function allowedCwdSuggestions(cwd, allowedCwds) {
7663
9029
  return normalizeAllowedCwds([cwd, ...allowedCwds]);
7664
9030
  }
@@ -7803,12 +9169,18 @@ async function acquireAndCacheToken(args) {
7803
9169
  }
7804
9170
 
7805
9171
  // src/localConfig.ts
7806
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync4, realpathSync as realpathSync4, writeFileSync as writeFileSync5 } from "node:fs";
9172
+ import {
9173
+ existsSync as existsSync5,
9174
+ mkdirSync as mkdirSync6,
9175
+ readFileSync as readFileSync4,
9176
+ realpathSync as realpathSync5,
9177
+ writeFileSync as writeFileSync5
9178
+ } from "node:fs";
7807
9179
  import { homedir as homedir6 } from "node:os";
7808
- import { dirname as dirname5, resolve as resolve5 } from "node:path";
9180
+ import { dirname as dirname5, resolve as resolve6 } from "node:path";
7809
9181
  function localConfigPath(env = process.env) {
7810
9182
  const override = env.LINZUMI_CONFIG_FILE;
7811
- return override !== undefined && override.trim() !== "" ? resolve5(expandUserPath(override)) : resolve5(homedir6(), ".linzumi", "config.json");
9183
+ return override !== undefined && override.trim() !== "" ? resolve6(expandUserPath(override)) : resolve6(homedir6(), ".linzumi", "config.json");
7812
9184
  }
7813
9185
  function readLocalConfig(path = localConfigPath()) {
7814
9186
  if (!existsSync5(path)) {
@@ -7823,24 +9195,36 @@ function readLocalConfig(path = localConfigPath()) {
7823
9195
  allowedCwds: uniqueStrings(parsed.allowedCwds)
7824
9196
  };
7825
9197
  }
7826
- function readConfiguredAllowedCwds(path = localConfigPath()) {
7827
- return readLocalConfig(path).allowedCwds.map((cwd) => {
9198
+ function readConfiguredAllowedCwdDetails(path = localConfigPath()) {
9199
+ const allowedCwds = [];
9200
+ const missingAllowedCwds = [];
9201
+ for (const cwd of readLocalConfig(path).allowedCwds) {
9202
+ const absolutePath = resolve6(expandUserPath(cwd));
7828
9203
  try {
7829
- return realpathSync4(resolve5(expandUserPath(cwd)));
7830
- } catch (_error) {
7831
- throw new Error(`invalid Linzumi config allowed path: ${cwd} does not exist`);
9204
+ const realPath = realpathSync5(absolutePath);
9205
+ allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
9206
+ } catch (error) {
9207
+ if (isMissingPathError(error)) {
9208
+ missingAllowedCwds.push(absolutePath);
9209
+ continue;
9210
+ }
9211
+ throw error;
7832
9212
  }
7833
- });
9213
+ }
9214
+ return {
9215
+ allowedCwds: uniqueStrings(allowedCwds),
9216
+ missingAllowedCwds: uniqueStrings(missingAllowedCwds)
9217
+ };
7834
9218
  }
7835
9219
  function addAllowedCwd(pathValue, path = localConfigPath()) {
7836
- const normalizedPath = realpathSync4(resolve5(expandUserPath(pathValue)));
9220
+ const normalizedPath = realpathSync5(resolve6(expandUserPath(pathValue)));
7837
9221
  const config = readLocalConfig(path);
7838
9222
  const allowedCwds = uniqueStrings([...config.allowedCwds, normalizedPath]);
7839
9223
  writeLocalConfig({ version: 1, allowedCwds }, path);
7840
9224
  return allowedCwds;
7841
9225
  }
7842
9226
  function removeAllowedCwd(pathValue, path = localConfigPath()) {
7843
- const requestedPath = resolve5(expandUserPath(pathValue));
9227
+ const requestedPath = resolve6(expandUserPath(pathValue));
7844
9228
  const normalizedRequest = realpathOrResolved(requestedPath);
7845
9229
  const config = readLocalConfig(path);
7846
9230
  const allowedCwds = config.allowedCwds.filter((cwd) => {
@@ -7859,13 +9243,18 @@ function isConfigPayload(value) {
7859
9243
  return typeof value === "object" && value !== null && value.version === 1 && Array.isArray(value.allowedCwds) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
7860
9244
  }
7861
9245
  function uniqueStrings(values) {
7862
- return [...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))];
9246
+ return [
9247
+ ...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))
9248
+ ];
9249
+ }
9250
+ function isMissingPathError(error) {
9251
+ return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "ELOOP" || error.code === "EIO");
7863
9252
  }
7864
9253
  function realpathOrResolved(pathValue) {
7865
9254
  try {
7866
- return realpathSync4(resolve5(expandUserPath(pathValue)));
9255
+ return realpathSync5(resolve6(expandUserPath(pathValue)));
7867
9256
  } catch (_error) {
7868
- return resolve5(expandUserPath(pathValue));
9257
+ return resolve6(expandUserPath(pathValue));
7869
9258
  }
7870
9259
  }
7871
9260
 
@@ -8263,7 +9652,7 @@ async function runClaim(command, deps) {
8263
9652
  workspaceName: stringValue(response.workspace_name),
8264
9653
  channelId: stringValue(response.channel_id),
8265
9654
  ownerUsername: stringValue(response.owner_username),
8266
- channelUrl: requiredString(response, "channel_url"),
9655
+ channelUrl: stringValue(response.channel_url),
8267
9656
  loginUrl: requiredString(response, "login_url"),
8268
9657
  supportChannelId: requiredString(response, "support_channel_id"),
8269
9658
  supportChannelUrl: requiredString(response, "support_channel_url"),
@@ -8282,8 +9671,10 @@ async function runClaim(command, deps) {
8282
9671
  deps.stdout.write(`login_url: ${tokenFile.loginUrl}
8283
9672
  `);
8284
9673
  writeHumanLoginUrlWarning(deps);
8285
- deps.stdout.write(`channel_url: ${tokenFile.channelUrl}
9674
+ if (tokenFile.channelUrl !== undefined) {
9675
+ deps.stdout.write(`channel_url: ${tokenFile.channelUrl}
8286
9676
  `);
9677
+ }
8287
9678
  deps.stdout.write(`support_channel_id: ${tokenFile.supportChannelId}
8288
9679
  `);
8289
9680
  deps.stdout.write(`support_channel_url: ${tokenFile.supportChannelUrl}
@@ -8552,7 +9943,7 @@ function readStoredAgentTokenFile(path, readTextFile = readOptionalTextFile) {
8552
9943
  workspaceName: stringValue(parsed.workspaceName),
8553
9944
  channelId: stringValue(parsed.channelId),
8554
9945
  ownerUsername: stringValue(parsed.ownerUsername),
8555
- channelUrl: requiredString(parsed, "channelUrl"),
9946
+ channelUrl: stringValue(parsed.channelUrl),
8556
9947
  loginUrl: requiredString(parsed, "loginUrl"),
8557
9948
  supportChannelId: requiredString(parsed, "supportChannelId"),
8558
9949
  supportChannelUrl: stringValue(parsed.supportChannelUrl),
@@ -8606,7 +9997,7 @@ Launch target:
8606
9997
 
8607
9998
  // src/helloLinzumiProject.ts
8608
9999
  import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
8609
- import { dirname as dirname7, join as join9, resolve as resolve6 } from "node:path";
10000
+ import { dirname as dirname7, join as join9, resolve as resolve7 } from "node:path";
8610
10001
  import { fileURLToPath } from "node:url";
8611
10002
  var defaultHelloLinzumiProjectDir = "/tmp/hello_linzumi";
8612
10003
  var defaultHelloLinzumiProjectName = "hello_linzumi";
@@ -8640,10 +10031,10 @@ function resolveHelloProjectRoot(options) {
8640
10031
  throw new Error("linzumi init-hello-linzumi-demo-app accepts either --dir or --parent-dir/--name, not both");
8641
10032
  }
8642
10033
  if (options.rootPath !== undefined) {
8643
- return resolve6(options.rootPath);
10034
+ return resolve7(options.rootPath);
8644
10035
  }
8645
10036
  const name = normalizeProjectName(options.name ?? defaultHelloLinzumiProjectName);
8646
- return resolve6(options.parentDir ?? defaultHelloLinzumiParentDir, name);
10037
+ return resolve7(options.parentDir ?? defaultHelloLinzumiParentDir, name);
8647
10038
  }
8648
10039
  function normalizeProjectName(value) {
8649
10040
  const name = value.trim();
@@ -9178,7 +10569,7 @@ import {
9178
10569
  writeFileSync as writeFileSync8
9179
10570
  } from "node:fs";
9180
10571
  import { homedir as homedir8 } from "node:os";
9181
- import { dirname as dirname8, join as join10, resolve as resolve7 } from "node:path";
10572
+ import { dirname as dirname8, join as join10, resolve as resolve8 } from "node:path";
9182
10573
  import { execFileSync, spawn as spawn7 } from "node:child_process";
9183
10574
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9184
10575
  var connectedMarker = "Runner connected:";
@@ -9194,7 +10585,7 @@ function defaultCommanderLogFile(runnerId, statusDir = commanderStatusDir()) {
9194
10585
  function startCommanderDaemon(options) {
9195
10586
  const statusDir = options.statusDir ?? commanderStatusDir();
9196
10587
  const statusFile = commanderStatusFile(options.runnerId, statusDir);
9197
- const logFile = resolve7(options.logFile ?? defaultCommanderLogFile(options.runnerId, statusDir));
10588
+ const logFile = resolve8(options.logFile ?? defaultCommanderLogFile(options.runnerId, statusDir));
9198
10589
  const entrypoint = options.entrypoint ?? currentEntrypoint();
9199
10590
  const nodeBin = options.nodeBin ?? process.execPath;
9200
10591
  const command = [
@@ -9375,7 +10766,9 @@ function readProcessIdentity(pid) {
9375
10766
  if (match === null) {
9376
10767
  return { command: output };
9377
10768
  }
9378
- return { startedAt: match[1], command: match[2] };
10769
+ const startedAt = match[1];
10770
+ const processCommand = match[2];
10771
+ return startedAt === undefined || processCommand === undefined ? { command: output } : { startedAt, command: processCommand };
9379
10772
  } catch (_error) {
9380
10773
  return;
9381
10774
  }
@@ -9389,7 +10782,7 @@ function safeRunnerId(runnerId) {
9389
10782
  }
9390
10783
  async function waitForFileChangeOrTimeout(path, deadline, now, ready2 = () => false) {
9391
10784
  const remaining = Math.max(0, deadline - now());
9392
- await new Promise((resolve8) => {
10785
+ await new Promise((resolve9) => {
9393
10786
  let resolved = false;
9394
10787
  let watcher;
9395
10788
  const finish = () => {
@@ -9399,7 +10792,7 @@ async function waitForFileChangeOrTimeout(path, deadline, now, ready2 = () => fa
9399
10792
  resolved = true;
9400
10793
  watcher?.close();
9401
10794
  clearTimeout(timer);
9402
- resolve8();
10795
+ resolve9();
9403
10796
  };
9404
10797
  const timer = setTimeout(finish, remaining);
9405
10798
  try {
@@ -9479,7 +10872,7 @@ function isMainModule() {
9479
10872
  if (scriptPath === undefined) {
9480
10873
  return false;
9481
10874
  }
9482
- return fileURLToPath3(import.meta.url) === resolve8(scriptPath);
10875
+ return fileURLToPath3(import.meta.url) === resolve9(scriptPath);
9483
10876
  }
9484
10877
  async function main(args) {
9485
10878
  const parsed = parseCommand(args);
@@ -9488,7 +10881,7 @@ async function main(args) {
9488
10881
  process.stdout.write(connectGuideText());
9489
10882
  return;
9490
10883
  case "version":
9491
- process.stdout.write(`linzumi 0.0.38-beta
10884
+ process.stdout.write(`linzumi 0.0.40-beta
9492
10885
  `);
9493
10886
  return;
9494
10887
  case "auth":
@@ -9582,12 +10975,17 @@ function runHelloCommand(args) {
9582
10975
  process.stdout.write(helloHelpText());
9583
10976
  return;
9584
10977
  }
10978
+ const rootPath = stringValue3(values, "dir");
10979
+ const parentDir = stringValue3(values, "parent-dir");
10980
+ const name = stringValue3(values, "name");
10981
+ const port = tcpPortValue(values, "port");
10982
+ const host = stringValue3(values, "host");
9585
10983
  const project = createHelloLinzumiProject({
9586
- rootPath: stringValue3(values, "dir"),
9587
- parentDir: stringValue3(values, "parent-dir") === undefined ? undefined : resolveUserPath(required(values, "parent-dir")),
9588
- name: stringValue3(values, "name"),
9589
- port: tcpPortValue(values, "port"),
9590
- host: stringValue3(values, "host"),
10984
+ ...rootPath === undefined ? {} : { rootPath },
10985
+ ...parentDir === undefined ? {} : { parentDir: resolveUserPath(parentDir) },
10986
+ ...name === undefined ? {} : { name },
10987
+ ...port === undefined ? {} : { port },
10988
+ ...host === undefined ? {} : { host },
9591
10989
  reset: values.get("reset") === true
9592
10990
  });
9593
10991
  if (values.get("json") === true) {
@@ -9636,7 +11034,7 @@ function runPathsCommand(args) {
9636
11034
  if (pathValue === undefined || pathValue.trim() === "") {
9637
11035
  throw new Error("missing path for linzumi paths add");
9638
11036
  }
9639
- const trustedPath = realpathSync5(resolve8(expandUserPath(pathValue)));
11037
+ const trustedPath = realpathSync6(resolve9(expandUserPath(pathValue)));
9640
11038
  addAllowedCwd(pathValue);
9641
11039
  process.stdout.write(`Trusted ${trustedPath}
9642
11040
  `);
@@ -9826,6 +11224,7 @@ async function parseStartRunnerArgs(args, deps = {
9826
11224
  kandanUrl,
9827
11225
  token: targetToken,
9828
11226
  runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID3()}`,
11227
+ workspaceSlug: target.workspaceSlug,
9829
11228
  cwd,
9830
11229
  codexBin,
9831
11230
  codexUrl: stringValue3(values, "codex-url"),
@@ -9838,6 +11237,7 @@ async function parseStartRunnerArgs(args, deps = {
9838
11237
  editorRuntime: editorRuntime.runtime,
9839
11238
  socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
9840
11239
  dependencyStatus,
11240
+ runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
9841
11241
  channelSession: {
9842
11242
  workspaceSlug: target.workspaceSlug,
9843
11243
  channelSlug: target.channelSlug,
@@ -9868,12 +11268,24 @@ async function parseAgentRunnerArgs(args, deps = {
9868
11268
  }
9869
11269
  const tokenFilePath = stringValue3(values, "agent-token-file") ?? defaultAgentTokenFilePath();
9870
11270
  const tokenFile = readStoredAgentTokenFile(tokenFilePath, deps.readTextFile);
9871
- const channelSlug = requiredStoredAgentChannel(tokenFile.channelId);
9872
- const listenUser = stringValue3(values, "listen-user") ?? requiredStoredOwnerUsername(tokenFile.ownerUsername);
11271
+ const channelSlug = tokenFile.channelId;
11272
+ rejectWorkspaceCommanderThreadFlags(values, channelSlug);
11273
+ const channelSession = channelSlug === undefined ? undefined : {
11274
+ workspaceSlug: tokenFile.workspaceId,
11275
+ channelSlug,
11276
+ kandanThreadId: stringValue3(values, "linzumi-thread-id"),
11277
+ listenUser: stringValue3(values, "listen-user") ?? requiredStoredOwnerUsername(tokenFile.ownerUsername),
11278
+ model: stringValue3(values, "model"),
11279
+ reasoningEffort: stringValue3(values, "reasoning-effort"),
11280
+ sandbox: stringValue3(values, "sandbox"),
11281
+ approvalPolicy: stringValue3(values, "approval-policy"),
11282
+ streamFlushMs: positiveIntegerValue(values, "stream-flush-ms")
11283
+ };
9873
11284
  const kandanUrl = stringValue3(values, "linzumi-url") ?? agentApiUrlToKandanUrl(tokenFile.apiUrl);
9874
11285
  const requestedCwdValue = cwdArg ?? stringValue3(values, "cwd");
9875
11286
  const requestedCwd = resolveUserPath(requestedCwdValue ?? process.cwd());
9876
- const allowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : requestedCwdValue === undefined ? readConfiguredAllowedCwds() : assertConfiguredAllowedCwds([requestedCwd]);
11287
+ const configuredAllowedCwds2 = requestedCwdValue === undefined && !values.has("allowed-cwd") ? readConfiguredAllowedCwdDetails() : { allowedCwds: [], missingAllowedCwds: [] };
11288
+ const allowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : requestedCwdValue === undefined ? configuredAllowedCwds2.allowedCwds.length > 0 ? [...configuredAllowedCwds2.allowedCwds] : assertConfiguredAllowedCwds([requestedCwd]) : assertConfiguredAllowedCwds([requestedCwd]);
9877
11289
  const cwd = allowedCwds[0] ?? requestedCwd;
9878
11290
  const codexBin = stringValue3(values, "codex-bin") ?? "codex";
9879
11291
  const customCodeServerBin = stringValue3(values, "code-server-bin");
@@ -9900,6 +11312,7 @@ async function parseAgentRunnerArgs(args, deps = {
9900
11312
  kandanUrl,
9901
11313
  token: tokenFile.commanderToken,
9902
11314
  runnerId: stringValue3(values, "runner-id") ?? `agent-runner-${randomUUID3()}`,
11315
+ workspaceSlug: tokenFile.workspaceId,
9903
11316
  cwd,
9904
11317
  codexBin,
9905
11318
  codexUrl: stringValue3(values, "codex-url"),
@@ -9907,22 +11320,14 @@ async function parseAgentRunnerArgs(args, deps = {
9907
11320
  fast: values.get("fast") === true,
9908
11321
  logFile: stringValue3(values, "log-file"),
9909
11322
  allowedCwds,
11323
+ missingAllowedCwds: configuredAllowedCwds2.missingAllowedCwds,
9910
11324
  allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
9911
11325
  codeServerBin: editorRuntime.codeServerBin,
9912
11326
  editorRuntime: editorRuntime.runtime,
9913
11327
  socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
9914
11328
  dependencyStatus,
9915
- channelSession: {
9916
- workspaceSlug: tokenFile.workspaceId,
9917
- channelSlug,
9918
- kandanThreadId: stringValue3(values, "linzumi-thread-id"),
9919
- listenUser,
9920
- model: stringValue3(values, "model"),
9921
- reasoningEffort: stringValue3(values, "reasoning-effort"),
9922
- sandbox: stringValue3(values, "sandbox"),
9923
- approvalPolicy: stringValue3(values, "approval-policy"),
9924
- streamFlushMs: positiveIntegerValue(values, "stream-flush-ms")
9925
- }
11329
+ runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
11330
+ channelSession
9926
11331
  };
9927
11332
  }
9928
11333
  function readAgentTokenTextFile(path) {
@@ -9940,11 +11345,11 @@ function rejectAgentRunnerTargetingFlags(values) {
9940
11345
  throw new Error(`linzumi commander uses the claimed human Commander token scope; remove ${unsupportedFlags.map((flag) => `--${flag}`).join(", ")}.`);
9941
11346
  }
9942
11347
  }
9943
- function requiredStoredAgentChannel(channelId) {
9944
- if (channelId !== undefined) {
9945
- return channelId;
11348
+ function rejectWorkspaceCommanderThreadFlags(values, channelSlug) {
11349
+ if (channelSlug !== undefined || !values.has("linzumi-thread-id")) {
11350
+ return;
9946
11351
  }
9947
- throw new Error("agent token file is missing channelId; rerun linzumi claim before starting a Commander");
11352
+ throw new Error("linzumi commander cannot bind --linzumi-thread-id because the agent token file has no channelId");
9948
11353
  }
9949
11354
  function requiredStoredOwnerUsername(ownerUsername) {
9950
11355
  if (ownerUsername !== undefined) {
@@ -10002,7 +11407,7 @@ async function parseRunnerArgs(args, deps = {
10002
11407
  process.exit(0);
10003
11408
  }
10004
11409
  if (values.get("version") === true) {
10005
- process.stdout.write(`linzumi 0.0.38-beta
11410
+ process.stdout.write(`linzumi 0.0.40-beta
10006
11411
  `);
10007
11412
  process.exit(0);
10008
11413
  }
@@ -10010,7 +11415,8 @@ async function parseRunnerArgs(args, deps = {
10010
11415
  const kandanUrl = required(values, "linzumi-url");
10011
11416
  const cwd = stringValue3(values, "cwd") ?? process.cwd();
10012
11417
  const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
10013
- const configuredAllowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : readConfiguredAllowedCwds();
11418
+ const localConfiguredAllowedCwds = values.has("allowed-cwd") ? { allowedCwds: [], missingAllowedCwds: [] } : readConfiguredAllowedCwdDetails();
11419
+ const configuredAllowedCwds2 = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : [...localConfiguredAllowedCwds.allowedCwds];
10014
11420
  const codexBin = stringValue3(values, "codex-bin") ?? "codex";
10015
11421
  const customCodeServerBin = stringValue3(values, "code-server-bin");
10016
11422
  const explicitToken = stringValue3(values, "token");
@@ -10050,15 +11456,26 @@ async function parseRunnerArgs(args, deps = {
10050
11456
  launchTui: values.get("launch-tui") === true,
10051
11457
  fast: values.get("fast") === true,
10052
11458
  logFile: stringValue3(values, "log-file"),
10053
- allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...configuredAllowedCwds])),
11459
+ allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...configuredAllowedCwds2])),
11460
+ missingAllowedCwds: localConfiguredAllowedCwds.missingAllowedCwds,
10054
11461
  allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
10055
11462
  codeServerBin: editorRuntime.codeServerBin,
10056
11463
  editorRuntime: editorRuntime.runtime,
10057
11464
  socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
10058
11465
  dependencyStatus,
11466
+ workspaceSlug: channelSession?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
11467
+ runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
10059
11468
  channelSession
10060
11469
  };
10061
11470
  }
11471
+ function runnerRuntimeDefaultsFromValues(values) {
11472
+ return {
11473
+ model: stringValue3(values, "model"),
11474
+ reasoningEffort: stringValue3(values, "reasoning-effort"),
11475
+ approvalPolicy: stringValue3(values, "approval-policy"),
11476
+ sandbox: stringValue3(values, "sandbox")
11477
+ };
11478
+ }
10062
11479
  function strictFlagValues(args, definitions = flagDefinitions) {
10063
11480
  const values = new Map;
10064
11481
  for (let index = 0;index < args.length; index += 1) {
@@ -10153,9 +11570,9 @@ function resolveUserPath(pathValue) {
10153
11570
  return homedir9();
10154
11571
  }
10155
11572
  if (pathValue.startsWith("~/")) {
10156
- return resolve8(homedir9(), pathValue.slice(2));
11573
+ return resolve9(homedir9(), pathValue.slice(2));
10157
11574
  }
10158
- return resolve8(pathValue);
11575
+ return resolve9(pathValue);
10159
11576
  }
10160
11577
  function parseChannelSession(values, token, target) {
10161
11578
  if (target === undefined) {
@@ -10439,10 +11856,10 @@ Usage:
10439
11856
 
10440
11857
  What it does:
10441
11858
  Starts this computer as the claimed human's scoped Linzumi Commander. The command
10442
- reads ~/.linzumi/agent-token.json, uses its workspace/channel scope, reads
10443
- trusted folders from ~/.linzumi/config.json when no folder is passed, and
10444
- listens only to the owning human recorded during claim unless --listen-user is
10445
- passed.
11859
+ reads ~/.linzumi/agent-token.json, connects to its workspace even when no
11860
+ channel is assigned, reads trusted folders from ~/.linzumi/config.json when no
11861
+ folder is passed, and uses the token's channel scope for channel-bound startup
11862
+ when one is present.
10446
11863
 
10447
11864
  Options:
10448
11865
  --agent-token-file <path> Agent token cache, default ~/.linzumi/agent-token.json