@linzumi/cli 0.0.38-beta → 0.0.39-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 +1389 -183
  3. package/package.json +5 -2
package/dist/index.js CHANGED
@@ -149,19 +149,34 @@ function senderAllowed(listenUser, event, runnerIdentity) {
149
149
  return runnerIdentity.actorUsername?.toLocaleLowerCase() === normalized && runnerIdentity.actorUserId !== undefined && event.actorUserId === runnerIdentity.actorUserId;
150
150
  }
151
151
  function identityFromAccessToken(token) {
152
+ const decoded = decodeAccessTokenPayload(token);
153
+ if (decoded === undefined) {
154
+ return { actorUserId: undefined, actorUsername: undefined };
155
+ }
156
+ const actorUserId = integerValue(decoded.actor_id) ?? integerValue(decoded.sub);
157
+ return {
158
+ actorUserId,
159
+ actorUsername: stringValue(decoded.actor_username)
160
+ };
161
+ }
162
+ function singleWorkspaceScopeFromAccessToken(token) {
163
+ const decoded = decodeAccessTokenPayload(token);
164
+ const workspaceScope = (arrayValue(decoded?.workspace_scope) ?? []).flatMap((value) => {
165
+ const workspace = stringValue(value)?.trim();
166
+ return workspace === undefined || workspace === "" ? [] : [workspace];
167
+ });
168
+ return workspaceScope.length === 1 ? workspaceScope[0] : undefined;
169
+ }
170
+ function decodeAccessTokenPayload(token) {
152
171
  const [, payload] = token.split(".");
153
172
  if (payload === undefined) {
154
- return { actorUserId: undefined, actorUsername: undefined };
173
+ return;
155
174
  }
156
175
  try {
157
176
  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
- };
177
+ return isJsonObject(decoded) ? decoded : undefined;
163
178
  } catch (_error) {
164
- return { actorUserId: undefined, actorUsername: undefined };
179
+ return;
165
180
  }
166
181
  }
167
182
  function detectCodexVersion(codexBin, cwd) {
@@ -801,6 +816,203 @@ function fuseQueuedMessages(selected) {
801
816
  };
802
817
  }
803
818
 
819
+ // src/reconnectContext.ts
820
+ var recentVerbatimMessageCount = 5;
821
+ var reconnectContextModel = "openai/gpt-oss-120b";
822
+ var noiseLocalRunnerEventTypes = new Set([
823
+ "availability",
824
+ "port_forward_requested",
825
+ "port_forward_ready",
826
+ "port_forward_resolved",
827
+ "settings",
828
+ "status",
829
+ "local_runner_config_seeded",
830
+ "local_runner_config_updated"
831
+ ]);
832
+ function parseReconnectContextMessages(value) {
833
+ if (!Array.isArray(value)) {
834
+ return [];
835
+ }
836
+ return value.flatMap(messageFromWirePayload);
837
+ }
838
+ function filterReconnectContextMessages(messages) {
839
+ return messages.filter((message) => {
840
+ const trimmed = message.body.trim();
841
+ if (trimmed === "") {
842
+ return false;
843
+ }
844
+ if (message.actorKind === "system") {
845
+ return false;
846
+ }
847
+ const eventType = localRunnerEventType2(message.payload);
848
+ if (eventType !== undefined && eventType !== "codex_output") {
849
+ return false;
850
+ }
851
+ if (looksLikeLegacyRunnerNoise(message)) {
852
+ return false;
853
+ }
854
+ return true;
855
+ });
856
+ }
857
+ async function buildReconnectContextInjection(messages, summarizer = createConfiguredReconnectContextSummarizer()) {
858
+ const filtered = filterReconnectContextMessages(messages);
859
+ if (filtered.length === 0) {
860
+ throw new Error("durable Linzumi thread history did not include user-visible context");
861
+ }
862
+ const older = filtered.slice(0, Math.max(0, filtered.length - recentVerbatimMessageCount));
863
+ const recent = filtered.slice(-recentVerbatimMessageCount);
864
+ const summary = older.length === 0 ? undefined : await summarizeOlderReconnectContext(older, summarizer);
865
+ const sections = [
866
+ "Reconnected Linzumi thread context",
867
+ "",
868
+ "The local Codex app-server thread was restarted, so this durable Linzumi thread history is being injected before retrying the latest user message.",
869
+ "",
870
+ ...summary === undefined ? [] : ["Summary of earlier messages:", summary, ""],
871
+ "Last five user-visible messages verbatim:",
872
+ recent.map(formatReconnectContextMessage).join(`
873
+
874
+ ---
875
+
876
+ `)
877
+ ];
878
+ return { type: "text", text: sections.join(`
879
+ `) };
880
+ }
881
+ function createConfiguredReconnectContextSummarizer(fetchImpl = globalThis.fetch) {
882
+ return {
883
+ summarize: (messages) => summarizeWithOpenAiCompatibleProvider(fetchImpl, messages)
884
+ };
885
+ }
886
+ async function summarizeOlderReconnectContext(messages, summarizer) {
887
+ const summary = (await summarizer.summarize(messages)).trim();
888
+ if (summary === "") {
889
+ throw new Error("reconnect context summarizer returned an empty summary");
890
+ }
891
+ return summary;
892
+ }
893
+ async function summarizeWithOpenAiCompatibleProvider(fetchImpl, messages) {
894
+ const provider = configuredReconnectContextProvider();
895
+ if (provider === undefined) {
896
+ throw new Error("Reconnect context summarization requires LINZUMI_RECONNECT_CONTEXT_API_KEY, GROQ_API_KEY, or OPENROUTER_API_KEY");
897
+ }
898
+ const model = envString("LINZUMI_RECONNECT_CONTEXT_MODEL") ?? envString("KANDAN_AGENT_PROGRESS_SUMMARY_MODEL") ?? reconnectContextModel;
899
+ const response = await fetchImpl(`${provider.baseUrl.replace(/\/$/, "")}/chat/completions`, {
900
+ method: "POST",
901
+ headers: {
902
+ authorization: `Bearer ${provider.apiKey}`,
903
+ "content-type": "application/json"
904
+ },
905
+ body: JSON.stringify({
906
+ model,
907
+ temperature: 0,
908
+ messages: [
909
+ {
910
+ role: "system",
911
+ 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."
912
+ },
913
+ {
914
+ role: "user",
915
+ content: messages.map(formatReconnectContextMessage).join(`
916
+
917
+ ---
918
+
919
+ `)
920
+ }
921
+ ]
922
+ })
923
+ });
924
+ if (!response.ok) {
925
+ throw new Error(`reconnect context summarization failed: ${response.status} ${response.statusText}`);
926
+ }
927
+ const parsed = await response.json();
928
+ if (!isJsonObject(parsed)) {
929
+ throw new Error("reconnect context summarizer returned non-object JSON");
930
+ }
931
+ const choices = parsed.choices;
932
+ if (!Array.isArray(choices)) {
933
+ throw new Error("reconnect context summarizer response did not include choices");
934
+ }
935
+ const first = choices.find(isJsonObject);
936
+ const message = objectValue(first?.message);
937
+ const content = stringValue(message?.content)?.trim();
938
+ if (content === undefined || content === "") {
939
+ throw new Error("reconnect context summarizer response did not include message content");
940
+ }
941
+ return content;
942
+ }
943
+ function configuredReconnectContextProvider() {
944
+ const linzumiApiKey = envString("LINZUMI_RECONNECT_CONTEXT_API_KEY");
945
+ if (linzumiApiKey !== undefined) {
946
+ return {
947
+ apiKey: linzumiApiKey,
948
+ baseUrl: envString("LINZUMI_RECONNECT_CONTEXT_BASE_URL") ?? "https://api.groq.com/openai/v1"
949
+ };
950
+ }
951
+ const groqApiKey = envString("GROQ_API_KEY");
952
+ if (groqApiKey !== undefined) {
953
+ return {
954
+ apiKey: groqApiKey,
955
+ baseUrl: envString("GROQ_BASE_URL") ?? "https://api.groq.com/openai/v1"
956
+ };
957
+ }
958
+ const openRouterApiKey = envString("OPENROUTER_API_KEY");
959
+ if (openRouterApiKey !== undefined) {
960
+ return {
961
+ apiKey: openRouterApiKey,
962
+ baseUrl: envString("OPENROUTER_BASE_URL") ?? "https://openrouter.ai/api/v1"
963
+ };
964
+ }
965
+ return;
966
+ }
967
+ function messageFromWirePayload(value) {
968
+ if (!isJsonObject(value)) {
969
+ return [];
970
+ }
971
+ const seq = integerValue(value.seq);
972
+ const payload = objectValue(value.payload) ?? {};
973
+ const body = stringValue(value.body) ?? stringValue(payload.body) ?? stringValue(payload.text);
974
+ const type = stringValue(value.type) ?? "thread.message";
975
+ if (seq === undefined || body === undefined) {
976
+ return [];
977
+ }
978
+ const actor = objectValue(value.actor) ?? {};
979
+ return [
980
+ {
981
+ seq,
982
+ type,
983
+ actorKind: stringValue(actor.kind) ?? "unknown",
984
+ actorSlug: stringValue(actor.slug),
985
+ actorUserId: integerValue(value.actor_user_id),
986
+ body,
987
+ payload
988
+ }
989
+ ];
990
+ }
991
+ function formatReconnectContextMessage(message) {
992
+ const sender = message.actorSlug?.trim() === "" || message.actorSlug === undefined ? message.actorKind : message.actorSlug;
993
+ const userId = message.actorUserId === undefined ? "unknown" : message.actorUserId.toString();
994
+ return [
995
+ `Linzumi durable message seq=${message.seq} from ${sender} (kind=${message.actorKind}, user_id=${userId}):`,
996
+ "",
997
+ message.body
998
+ ].join(`
999
+ `);
1000
+ }
1001
+ function localRunnerEventType2(payload) {
1002
+ const metadata = objectValue(payload.metadata);
1003
+ const localRunner = objectValue(metadata?.local_codex_runner);
1004
+ const eventType = stringValue(localRunner?.event_type);
1005
+ return eventType === undefined || eventType.trim() === "" ? undefined : eventType.trim();
1006
+ }
1007
+ function looksLikeLegacyRunnerNoise(message) {
1008
+ const body = message.body.trim();
1009
+ 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);
1010
+ }
1011
+ function envString(name) {
1012
+ const value = process.env[name];
1013
+ return value === undefined || value.trim() === "" ? undefined : value.trim();
1014
+ }
1015
+
804
1016
  // src/localCodexMessageState.ts
805
1017
  function codexApprovalMessageState(request) {
806
1018
  const params = objectValue(request.params) ?? {};
@@ -843,7 +1055,11 @@ function processingReasonForCodexNotification(method, params) {
843
1055
  function processingMessageStateFromActive(state) {
844
1056
  switch (state.reason) {
845
1057
  case "awaiting approval":
846
- return { status: "processing", reason: state.reason, approval: state.approval };
1058
+ return {
1059
+ status: "processing",
1060
+ reason: state.reason,
1061
+ approval: state.approval
1062
+ };
847
1063
  case "starting turn":
848
1064
  case "streaming response":
849
1065
  case "running terminal command":
@@ -1151,14 +1367,17 @@ var defaultIntervalMs = 2000;
1151
1367
  var defaultDebounceMs = 750;
1152
1368
  function startPortForwardWatcher(options) {
1153
1369
  const rootPid = options.rootPid ?? process.pid;
1370
+ const rootCwd = normalizeCwd(options.rootCwd);
1154
1371
  const intervalMs = options.intervalMs ?? defaultIntervalMs;
1155
1372
  const debounceMs = options.debounceMs ?? defaultDebounceMs;
1373
+ const lostDebounceMs = options.lostDebounceMs ?? intervalMs * 2 + debounceMs;
1156
1374
  const scanProcesses = options.scanProcesses ?? readProcessRows;
1157
1375
  const scanListenSockets = options.scanListenSockets ?? readListenSocketRows;
1158
1376
  const scanProcessCwds = options.scanProcessCwds ?? readProcessCwdRows;
1159
1377
  const nowMs = options.nowMs ?? Date.now;
1160
1378
  const candidateStabilityByPort = new Map;
1161
1379
  const emittedByPort = new Map;
1380
+ const missingByPort = new Map;
1162
1381
  const inFlight = { value: false };
1163
1382
  const scan = () => {
1164
1383
  if (inFlight.value) {
@@ -1168,18 +1387,26 @@ function startPortForwardWatcher(options) {
1168
1387
  Promise.resolve().then(async () => {
1169
1388
  const descendants = descendantPidSet(scanProcesses(), rootPid);
1170
1389
  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);
1390
+ const candidatePids = rootCwd === undefined ? sockets.filter((socket) => descendants.has(socket.pid)).map((socket) => socket.pid) : sockets.map((socket) => socket.pid);
1391
+ const candidates = detectedForwardCandidates(sockets, descendants, scanProcessCwds(candidatePids), { rootCwd });
1392
+ const scanTimeMs = nowMs();
1393
+ const stable = stableForwardCandidates(candidateStabilityByPort, candidates, scanTimeMs, debounceMs);
1394
+ const changes = debouncedForwardCandidateChanges(emittedByPort, missingByPort, stable.stableCandidates, scanTimeMs, lostDebounceMs);
1175
1395
  candidateStabilityByPort.clear();
1176
1396
  emittedByPort.clear();
1397
+ missingByPort.clear();
1177
1398
  for (const [port, observed] of stable.nextObservedByPort) {
1178
1399
  candidateStabilityByPort.set(port, observed);
1179
1400
  }
1180
1401
  for (const [port, candidate] of changes.nextObservedByPort) {
1181
1402
  emittedByPort.set(port, candidate);
1182
1403
  }
1404
+ for (const [port, missing] of changes.nextMissingByPort) {
1405
+ missingByPort.set(port, missing);
1406
+ }
1407
+ for (const candidate of changes.lostCandidates) {
1408
+ await options.onCandidateLost?.(candidate);
1409
+ }
1183
1410
  for (const candidate of changes.changedCandidates) {
1184
1411
  await options.onCandidate(candidate);
1185
1412
  }
@@ -1195,9 +1422,11 @@ function startPortForwardWatcher(options) {
1195
1422
  close: () => clearInterval(interval)
1196
1423
  };
1197
1424
  }
1198
- function changedForwardCandidates(previousObservedByPort, candidates) {
1425
+ function debouncedForwardCandidateChanges(previousObservedByPort, previousMissingByPort, candidates, nowMs, lostDebounceMs) {
1199
1426
  const nextObservedByPort = new Map;
1427
+ const nextMissingByPort = new Map;
1200
1428
  const changedCandidates = [];
1429
+ const lostCandidates = [];
1201
1430
  for (const candidate of candidates) {
1202
1431
  nextObservedByPort.set(candidate.port, candidate);
1203
1432
  const previous = previousObservedByPort.get(candidate.port);
@@ -1205,7 +1434,28 @@ function changedForwardCandidates(previousObservedByPort, candidates) {
1205
1434
  changedCandidates.push(candidate);
1206
1435
  }
1207
1436
  }
1208
- return { nextObservedByPort, changedCandidates };
1437
+ for (const previous of previousObservedByPort.values()) {
1438
+ if (nextObservedByPort.has(previous.port)) {
1439
+ continue;
1440
+ }
1441
+ const previousMissing = previousMissingByPort.get(previous.port);
1442
+ const firstMissingAtMs = previousMissing !== undefined && sameForwardCandidate(previousMissing.candidate, previous) ? previousMissing.firstMissingAtMs : nowMs;
1443
+ if (lostDebounceMs <= 0 || nowMs - firstMissingAtMs >= lostDebounceMs) {
1444
+ lostCandidates.push(previous);
1445
+ continue;
1446
+ }
1447
+ nextObservedByPort.set(previous.port, previous);
1448
+ nextMissingByPort.set(previous.port, {
1449
+ candidate: previous,
1450
+ firstMissingAtMs
1451
+ });
1452
+ }
1453
+ return {
1454
+ nextObservedByPort,
1455
+ nextMissingByPort,
1456
+ changedCandidates,
1457
+ lostCandidates
1458
+ };
1209
1459
  }
1210
1460
  function stableForwardCandidates(previousObservedByPort, candidates, nowMs, debounceMs) {
1211
1461
  const nextObservedByPort = new Map;
@@ -1242,8 +1492,17 @@ function descendantPidSet(rows, rootPid) {
1242
1492
  }
1243
1493
  return descendants;
1244
1494
  }
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) => {
1495
+ function detectedForwardCandidates(sockets, descendantPids, processCwds = new Map, options = {}) {
1496
+ const rootCwd = normalizeCwd(options.rootCwd);
1497
+ return sockets.filter((socket) => {
1498
+ if (descendantPids.has(socket.pid)) {
1499
+ return true;
1500
+ }
1501
+ if (rootCwd === undefined) {
1502
+ return false;
1503
+ }
1504
+ return cwdMatchesRoot(processCwds.get(socket.pid), rootCwd);
1505
+ }).filter((socket) => socket.port > 0 && socket.port < 65536).sort((left, right) => left.port - right.port).map((socket) => {
1247
1506
  const cwd = processCwds.get(socket.pid);
1248
1507
  return {
1249
1508
  port: socket.port,
@@ -1253,6 +1512,20 @@ function detectedForwardCandidates(sockets, descendantPids, processCwds = new Ma
1253
1512
  };
1254
1513
  });
1255
1514
  }
1515
+ function normalizeCwd(cwd) {
1516
+ if (cwd === undefined) {
1517
+ return;
1518
+ }
1519
+ const normalized = cwd.trim().replace(/\/+$/, "");
1520
+ return normalized === "" ? undefined : normalized;
1521
+ }
1522
+ function cwdMatchesRoot(candidateCwd, rootCwd) {
1523
+ const normalizedCandidate = normalizeCwd(candidateCwd);
1524
+ if (normalizedCandidate === undefined) {
1525
+ return false;
1526
+ }
1527
+ return normalizedCandidate === rootCwd || normalizedCandidate.startsWith(`${rootCwd}/`);
1528
+ }
1256
1529
  function parseProcessRows(output) {
1257
1530
  return output.split(`
1258
1531
  `).map((line) => line.trim()).filter((line) => line !== "").map((line) => {
@@ -1329,7 +1602,9 @@ function readProcessRows() {
1329
1602
  return parseProcessRows(result.stdout);
1330
1603
  }
1331
1604
  function readListenSocketRows() {
1332
- const result = spawnSync2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPcn"], { encoding: "utf8" });
1605
+ const result = spawnSync2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPcn"], {
1606
+ encoding: "utf8"
1607
+ });
1333
1608
  if (result.error !== undefined) {
1334
1609
  throw result.error;
1335
1610
  }
@@ -1430,22 +1705,6 @@ function approvedTargetFromRequest(request) {
1430
1705
  function portForwardPromptLabel(candidate) {
1431
1706
  return commandLabel(candidate.command);
1432
1707
  }
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
1708
  function portForwardPromptReason(candidate) {
1450
1709
  return [
1451
1710
  `Port ${candidate.port}`,
@@ -1455,15 +1714,16 @@ function portForwardPromptReason(candidate) {
1455
1714
  "preview protocols: HTTP, HTTPS, WebSocket"
1456
1715
  ].join(" / ");
1457
1716
  }
1458
- function portForwardDecisionCommand(decision, requestId) {
1459
- return `/kandan ${decision}-port-forward ${requestId}`;
1460
- }
1461
1717
  function forwardPreviewPath(runnerId, port) {
1462
1718
  return `/local-codex-runners/${encodeURIComponent(runnerId)}/forwards/${port}/preview`;
1463
1719
  }
1464
1720
  function revocationCapabilities(capabilities, port) {
1721
+ const existingAllowedPorts = Array.isArray(capabilities?.allowedPorts) ? capabilities.allowedPorts.filter((value) => typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535) : [];
1722
+ const allowedPorts = existingAllowedPorts.filter((allowedPort) => allowedPort !== port);
1465
1723
  return {
1466
1724
  ...capabilities ?? {},
1725
+ portForwarding: allowedPorts.length > 0,
1726
+ allowedPorts,
1467
1727
  revokedPorts: [port]
1468
1728
  };
1469
1729
  }
@@ -1474,6 +1734,608 @@ function isInternalCodexProcess(candidate) {
1474
1734
  return commandLabel(candidate.command) === "codex";
1475
1735
  }
1476
1736
 
1737
+ // src/processNameCatalog.ts
1738
+ var processCatalogEntries = [
1739
+ { appName: "Bun", iconKey: "bun", names: ["bun", "bunx"] },
1740
+ {
1741
+ appName: "Node.js",
1742
+ iconKey: "nodejs",
1743
+ names: ["node", "nodejs", "node.exe"]
1744
+ },
1745
+ { appName: "npm", iconKey: "npm", names: ["npm", "npm-cli.js", "npx"] },
1746
+ { appName: "pnpm", iconKey: "pnpm", names: ["pnpm", "pnpx"] },
1747
+ { appName: "Yarn", iconKey: "yarn", names: ["yarn", "yarnpkg", "yarn.js"] },
1748
+ { appName: "Corepack", iconKey: "corepack", names: ["corepack"] },
1749
+ { appName: "Deno", iconKey: "deno", names: ["deno"] },
1750
+ { appName: "Vite", iconKey: "vite", names: ["vite", "vite-node"] },
1751
+ { appName: "Vitest", iconKey: "vitest", names: ["vitest"] },
1752
+ { appName: "Next.js", iconKey: "nextjs", names: ["next", "next-server"] },
1753
+ { appName: "Nuxt", iconKey: "nuxt", names: ["nuxt", "nuxi"] },
1754
+ { appName: "Astro", iconKey: "astro", names: ["astro"] },
1755
+ {
1756
+ appName: "SvelteKit",
1757
+ iconKey: "svelte",
1758
+ names: ["svelte-kit", "sveltekit"]
1759
+ },
1760
+ { appName: "Remix", iconKey: "remix", names: ["remix"] },
1761
+ { appName: "Expo", iconKey: "expo", names: ["expo", "expo-cli"] },
1762
+ { appName: "React Native", iconKey: "react", names: ["react-native"] },
1763
+ {
1764
+ appName: "Metro",
1765
+ iconKey: "metro",
1766
+ names: ["metro", "metro-inspector-proxy"]
1767
+ },
1768
+ {
1769
+ appName: "Webpack",
1770
+ iconKey: "webpack",
1771
+ names: ["webpack", "webpack-dev-server", "webpack-cli"]
1772
+ },
1773
+ { appName: "Parcel", iconKey: "parcel", names: ["parcel"] },
1774
+ { appName: "Rollup", iconKey: "rollup", names: ["rollup"] },
1775
+ { appName: "esbuild", iconKey: "esbuild", names: ["esbuild"] },
1776
+ { appName: "SWC", iconKey: "swc", names: ["swc"] },
1777
+ {
1778
+ appName: "TypeScript",
1779
+ iconKey: "typescript",
1780
+ names: ["tsc", "tsserver", "tsx", "ts-node", "typescript-language-server"]
1781
+ },
1782
+ { appName: "ESLint", iconKey: "eslint", names: ["eslint", "eslint_d"] },
1783
+ { appName: "Prettier", iconKey: "prettier", names: ["prettier"] },
1784
+ { appName: "Biome", iconKey: "biome", names: ["biome"] },
1785
+ {
1786
+ appName: "Storybook",
1787
+ iconKey: "storybook",
1788
+ names: ["storybook", "start-storybook"]
1789
+ },
1790
+ { appName: "Playwright", iconKey: "playwright", names: ["playwright"] },
1791
+ { appName: "Cypress", iconKey: "cypress", names: ["cypress"] },
1792
+ { appName: "Jest", iconKey: "jest", names: ["jest"] },
1793
+ {
1794
+ appName: "Python",
1795
+ iconKey: "python",
1796
+ names: [
1797
+ "python",
1798
+ "python2",
1799
+ "python3",
1800
+ "python3.9",
1801
+ "python3.10",
1802
+ "python3.11",
1803
+ "python3.12",
1804
+ "python3.13",
1805
+ "pypy",
1806
+ "pypy3"
1807
+ ]
1808
+ },
1809
+ { appName: "pip", iconKey: "python", names: ["pip", "pip2", "pip3"] },
1810
+ { appName: "pipx", iconKey: "python", names: ["pipx"] },
1811
+ { appName: "uv", iconKey: "python", names: ["uv", "uvx"] },
1812
+ { appName: "Poetry", iconKey: "poetry", names: ["poetry"] },
1813
+ { appName: "Pipenv", iconKey: "python", names: ["pipenv"] },
1814
+ {
1815
+ appName: "Conda",
1816
+ iconKey: "conda",
1817
+ names: ["conda", "mamba", "micromamba"]
1818
+ },
1819
+ {
1820
+ appName: "Django",
1821
+ iconKey: "django",
1822
+ names: ["django-admin", "django-admin.py", "manage.py"]
1823
+ },
1824
+ { appName: "Flask", iconKey: "flask", names: ["flask"] },
1825
+ { appName: "Uvicorn", iconKey: "uvicorn", names: ["uvicorn"] },
1826
+ { appName: "Gunicorn", iconKey: "gunicorn", names: ["gunicorn"] },
1827
+ { appName: "Hypercorn", iconKey: "python", names: ["hypercorn"] },
1828
+ { appName: "Celery", iconKey: "celery", names: ["celery"] },
1829
+ {
1830
+ appName: "Jupyter",
1831
+ iconKey: "jupyter",
1832
+ names: ["jupyter", "jupyter-lab", "jupyter-notebook", "ipython"]
1833
+ },
1834
+ { appName: "Ruby", iconKey: "ruby", names: ["ruby", "erb", "irb"] },
1835
+ { appName: "Bundler", iconKey: "ruby", names: ["bundle", "bundler"] },
1836
+ { appName: "Rails", iconKey: "rails", names: ["rails", "bin/rails"] },
1837
+ { appName: "Rack", iconKey: "ruby", names: ["rackup"] },
1838
+ { appName: "Puma", iconKey: "puma", names: ["puma"] },
1839
+ { appName: "Sidekiq", iconKey: "sidekiq", names: ["sidekiq"] },
1840
+ { appName: "RSpec", iconKey: "ruby", names: ["rspec"] },
1841
+ { appName: "Go", iconKey: "go", names: ["go", "gofmt", "gopls", "air"] },
1842
+ { appName: "Delve", iconKey: "go", names: ["dlv"] },
1843
+ {
1844
+ appName: "Rust",
1845
+ iconKey: "rust",
1846
+ names: ["rustc", "rustup", "cargo", "rust-analyzer"]
1847
+ },
1848
+ {
1849
+ appName: "Java",
1850
+ iconKey: "java",
1851
+ names: ["java", "javac", "jar", "jshell", "jwebserver"]
1852
+ },
1853
+ { appName: "Gradle", iconKey: "gradle", names: ["gradle", "gradlew"] },
1854
+ { appName: "Maven", iconKey: "maven", names: ["mvn", "mvnw"] },
1855
+ {
1856
+ appName: "Spring Boot",
1857
+ iconKey: "spring",
1858
+ names: ["spring", "spring-boot"]
1859
+ },
1860
+ {
1861
+ appName: "Kotlin",
1862
+ iconKey: "kotlin",
1863
+ names: ["kotlin", "kotlinc", "kotlin-language-server"]
1864
+ },
1865
+ {
1866
+ appName: "Scala",
1867
+ iconKey: "scala",
1868
+ names: ["scala", "scalac", "sbt", "metals"]
1869
+ },
1870
+ {
1871
+ appName: "Clojure",
1872
+ iconKey: "clojure",
1873
+ names: ["clojure", "clj", "lein", "boot"]
1874
+ },
1875
+ {
1876
+ appName: "PHP",
1877
+ iconKey: "php",
1878
+ names: ["php", "php-fpm", "php-cgi", "composer"]
1879
+ },
1880
+ { appName: "Laravel", iconKey: "laravel", names: ["artisan", "laravel"] },
1881
+ { appName: "Symfony", iconKey: "symfony", names: ["symfony"] },
1882
+ { appName: "Elixir", iconKey: "elixir", names: ["elixir", "iex", "mix"] },
1883
+ {
1884
+ appName: "Erlang",
1885
+ iconKey: "erlang",
1886
+ names: ["erl", "beam.smp", "beam", "epmd", "rebar3"]
1887
+ },
1888
+ { appName: "Phoenix", iconKey: "phoenix", names: ["phx.server", "phoenix"] },
1889
+ {
1890
+ appName: "PostgreSQL",
1891
+ iconKey: "postgresql",
1892
+ names: ["postgres", "postmaster", "pg_ctl", "psql", "pg_isready"]
1893
+ },
1894
+ {
1895
+ appName: "Redis",
1896
+ iconKey: "redis",
1897
+ names: ["redis-server", "redis-cli", "redis-sentinel"]
1898
+ },
1899
+ {
1900
+ appName: "MySQL",
1901
+ iconKey: "mysql",
1902
+ names: ["mysql", "mysqld", "mysqladmin"]
1903
+ },
1904
+ {
1905
+ appName: "MariaDB",
1906
+ iconKey: "mariadb",
1907
+ names: ["mariadb", "mariadbd", "mariadb-admin"]
1908
+ },
1909
+ { appName: "SQLite", iconKey: "sqlite", names: ["sqlite3", "sqlite"] },
1910
+ {
1911
+ appName: "MongoDB",
1912
+ iconKey: "mongodb",
1913
+ names: ["mongod", "mongosh", "mongo"]
1914
+ },
1915
+ {
1916
+ appName: "Elasticsearch",
1917
+ iconKey: "elasticsearch",
1918
+ names: ["elasticsearch"]
1919
+ },
1920
+ { appName: "OpenSearch", iconKey: "opensearch", names: ["opensearch"] },
1921
+ { appName: "Solr", iconKey: "solr", names: ["solr"] },
1922
+ {
1923
+ appName: "Kafka",
1924
+ iconKey: "kafka",
1925
+ names: [
1926
+ "kafka-server-start",
1927
+ "kafka-console-consumer",
1928
+ "kafka-console-producer"
1929
+ ]
1930
+ },
1931
+ {
1932
+ appName: "ZooKeeper",
1933
+ iconKey: "zookeeper",
1934
+ names: ["zookeeper-server-start", "zkserver"]
1935
+ },
1936
+ {
1937
+ appName: "RabbitMQ",
1938
+ iconKey: "rabbitmq",
1939
+ names: ["rabbitmq-server", "rabbitmqctl"]
1940
+ },
1941
+ { appName: "NATS", iconKey: "nats", names: ["nats-server", "nats"] },
1942
+ {
1943
+ appName: "Docker",
1944
+ iconKey: "docker",
1945
+ names: [
1946
+ "docker",
1947
+ "dockerd",
1948
+ "docker-compose",
1949
+ "docker-proxy",
1950
+ "com.docker.backend"
1951
+ ]
1952
+ },
1953
+ { appName: "Podman", iconKey: "podman", names: ["podman", "podman-compose"] },
1954
+ {
1955
+ appName: "Kubernetes",
1956
+ iconKey: "kubernetes",
1957
+ names: ["kubectl", "kube-apiserver", "kubelet", "minikube", "kind"]
1958
+ },
1959
+ { appName: "Helm", iconKey: "helm", names: ["helm"] },
1960
+ { appName: "Tilt", iconKey: "tilt", names: ["tilt"] },
1961
+ { appName: "nginx", iconKey: "nginx", names: ["nginx"] },
1962
+ { appName: "Caddy", iconKey: "caddy", names: ["caddy"] },
1963
+ {
1964
+ appName: "Apache HTTP Server",
1965
+ iconKey: "apache",
1966
+ names: ["httpd", "apache2", "apachectl"]
1967
+ },
1968
+ { appName: "HAProxy", iconKey: "haproxy", names: ["haproxy"] },
1969
+ { appName: "Traefik", iconKey: "traefik", names: ["traefik"] },
1970
+ { appName: "Envoy", iconKey: "envoy", names: ["envoy"] },
1971
+ {
1972
+ appName: "Tailscale",
1973
+ iconKey: "tailscale",
1974
+ names: ["tailscale", "tailscaled"]
1975
+ },
1976
+ {
1977
+ appName: "Cloudflare Tunnel",
1978
+ iconKey: "cloudflare",
1979
+ names: ["cloudflared"]
1980
+ },
1981
+ { appName: "ngrok", iconKey: "ngrok", names: ["ngrok"] },
1982
+ { appName: "localtunnel", iconKey: "terminal", names: ["lt", "localtunnel"] },
1983
+ { appName: "SSH", iconKey: "ssh", names: ["ssh", "sshd", "scp", "sftp"] },
1984
+ {
1985
+ appName: "Git",
1986
+ iconKey: "git",
1987
+ names: ["git", "git-lfs", "git-upload-pack", "git-receive-pack"]
1988
+ },
1989
+ { appName: "GitHub CLI", iconKey: "github", names: ["gh"] },
1990
+ { appName: "GitLab CLI", iconKey: "gitlab", names: ["glab"] },
1991
+ { appName: "Subversion", iconKey: "subversion", names: ["svn"] },
1992
+ { appName: "Mercurial", iconKey: "mercurial", names: ["hg"] },
1993
+ { appName: "AWS CLI", iconKey: "aws", names: ["aws", "aws-vault", "sam"] },
1994
+ { appName: "Azure CLI", iconKey: "azure", names: ["az"] },
1995
+ {
1996
+ appName: "Google Cloud CLI",
1997
+ iconKey: "google-cloud",
1998
+ names: ["gcloud", "gsutil", "bq"]
1999
+ },
2000
+ { appName: "Terraform", iconKey: "terraform", names: ["terraform"] },
2001
+ { appName: "OpenTofu", iconKey: "opentofu", names: ["tofu"] },
2002
+ {
2003
+ appName: "Ansible",
2004
+ iconKey: "ansible",
2005
+ names: ["ansible", "ansible-playbook"]
2006
+ },
2007
+ { appName: "Pulumi", iconKey: "pulumi", names: ["pulumi"] },
2008
+ { appName: "Nomad", iconKey: "nomad", names: ["nomad"] },
2009
+ { appName: "Vault", iconKey: "vault", names: ["vault"] },
2010
+ { appName: "Consul", iconKey: "consul", names: ["consul"] },
2011
+ { appName: "Make", iconKey: "make", names: ["make", "gmake"] },
2012
+ { appName: "CMake", iconKey: "cmake", names: ["cmake", "ctest", "cpack"] },
2013
+ { appName: "Ninja", iconKey: "ninja", names: ["ninja"] },
2014
+ { appName: "Bazel", iconKey: "bazel", names: ["bazel", "bazelisk"] },
2015
+ { appName: "Buck", iconKey: "buck", names: ["buck", "buck2"] },
2016
+ { appName: "Clang", iconKey: "clang", names: ["clang", "clang++", "clangd"] },
2017
+ { appName: "GCC", iconKey: "gcc", names: ["gcc", "g++", "cc", "c++"] },
2018
+ { appName: "LLDB", iconKey: "terminal", names: ["lldb"] },
2019
+ { appName: "GDB", iconKey: "terminal", names: ["gdb"] },
2020
+ {
2021
+ appName: "Swift",
2022
+ iconKey: "swift",
2023
+ names: ["swift", "swiftc", "sourcekit-lsp"]
2024
+ },
2025
+ {
2026
+ appName: "Xcode",
2027
+ iconKey: "xcode",
2028
+ names: ["xcodebuild", "xcrun", "simctl"]
2029
+ },
2030
+ { appName: "Zig", iconKey: "zig", names: ["zig"] },
2031
+ {
2032
+ appName: "Lua",
2033
+ iconKey: "lua",
2034
+ names: ["lua", "luajit", "lua-language-server"]
2035
+ },
2036
+ { appName: "Perl", iconKey: "perl", names: ["perl"] },
2037
+ {
2038
+ appName: "R",
2039
+ iconKey: "r",
2040
+ names: ["r", "rscript", "rserver", "rsession"]
2041
+ },
2042
+ { appName: "Julia", iconKey: "julia", names: ["julia"] },
2043
+ {
2044
+ appName: "Haskell",
2045
+ iconKey: "haskell",
2046
+ names: ["ghc", "ghci", "cabal", "stack", "haskell-language-server-wrapper"]
2047
+ },
2048
+ {
2049
+ appName: "OCaml",
2050
+ iconKey: "ocaml",
2051
+ names: ["ocaml", "ocamlc", "dune", "opam"]
2052
+ },
2053
+ { appName: "F#", iconKey: "fsharp", names: ["fsharpi", "fsautocomplete"] },
2054
+ {
2055
+ appName: ".NET",
2056
+ iconKey: "dotnet",
2057
+ names: ["dotnet", "csharp-ls", "omnisharp"]
2058
+ },
2059
+ {
2060
+ appName: "PowerShell",
2061
+ iconKey: "powershell",
2062
+ names: ["pwsh", "powershell"]
2063
+ },
2064
+ { appName: "Bash", iconKey: "terminal", names: ["bash"] },
2065
+ { appName: "Zsh", iconKey: "terminal", names: ["zsh"] },
2066
+ { appName: "fish", iconKey: "terminal", names: ["fish"] },
2067
+ { appName: "tmux", iconKey: "terminal", names: ["tmux"] },
2068
+ { appName: "GNU Screen", iconKey: "terminal", names: ["screen"] },
2069
+ { appName: "direnv", iconKey: "terminal", names: ["direnv"] },
2070
+ {
2071
+ appName: "Nix",
2072
+ iconKey: "nix",
2073
+ names: ["nix", "nix-shell", "nix-daemon", "nix-build", "nix develop"]
2074
+ },
2075
+ { appName: "Homebrew", iconKey: "homebrew", names: ["brew"] },
2076
+ { appName: "mise", iconKey: "terminal", names: ["mise"] },
2077
+ { appName: "asdf", iconKey: "terminal", names: ["asdf"] },
2078
+ { appName: "Volta", iconKey: "volta", names: ["volta"] },
2079
+ { appName: "fnm", iconKey: "nodejs", names: ["fnm"] },
2080
+ { appName: "nvm", iconKey: "nodejs", names: ["nvm"] },
2081
+ { appName: "curl", iconKey: "terminal", names: ["curl"] },
2082
+ { appName: "HTTPie", iconKey: "httpie", names: ["http", "https", "httpie"] },
2083
+ { appName: "wget", iconKey: "terminal", names: ["wget"] },
2084
+ { appName: "jq", iconKey: "terminal", names: ["jq"] },
2085
+ { appName: "ripgrep", iconKey: "terminal", names: ["rg", "ripgrep"] },
2086
+ { appName: "fd", iconKey: "terminal", names: ["fd"] },
2087
+ { appName: "fzf", iconKey: "terminal", names: ["fzf"] },
2088
+ { appName: "bat", iconKey: "terminal", names: ["bat"] },
2089
+ { appName: "eza", iconKey: "terminal", names: ["eza", "exa"] },
2090
+ { appName: "htop", iconKey: "terminal", names: ["htop", "top", "btop"] },
2091
+ { appName: "Vim", iconKey: "vim", names: ["vim", "view", "vimdiff"] },
2092
+ { appName: "Neovim", iconKey: "neovim", names: ["nvim", "nvim-qt"] },
2093
+ { appName: "Emacs", iconKey: "emacs", names: ["emacs", "emacsclient"] },
2094
+ {
2095
+ appName: "Visual Studio Code",
2096
+ iconKey: "vscode",
2097
+ names: [
2098
+ "code",
2099
+ "code-insiders",
2100
+ "vscode",
2101
+ "visual studio code",
2102
+ "visual studio code - insiders"
2103
+ ]
2104
+ },
2105
+ { appName: "code-server", iconKey: "vscode", names: ["code-server"] },
2106
+ { appName: "Cursor", iconKey: "cursor", names: ["cursor"] },
2107
+ { appName: "Zed", iconKey: "zed", names: ["zed"] },
2108
+ {
2109
+ appName: "Sublime Text",
2110
+ iconKey: "sublime-text",
2111
+ names: ["subl", "sublime_text", "sublime text"]
2112
+ },
2113
+ { appName: "Atom", iconKey: "atom", names: ["atom"] },
2114
+ {
2115
+ appName: "IntelliJ IDEA",
2116
+ iconKey: "intellij-idea",
2117
+ names: ["idea", "idea.sh", "intellij idea", "idea64.exe"]
2118
+ },
2119
+ {
2120
+ appName: "WebStorm",
2121
+ iconKey: "webstorm",
2122
+ names: ["webstorm", "webstorm.sh"]
2123
+ },
2124
+ { appName: "PyCharm", iconKey: "pycharm", names: ["pycharm", "pycharm.sh"] },
2125
+ {
2126
+ appName: "RubyMine",
2127
+ iconKey: "rubymine",
2128
+ names: ["rubymine", "rubymine.sh"]
2129
+ },
2130
+ { appName: "GoLand", iconKey: "goland", names: ["goland", "goland.sh"] },
2131
+ { appName: "CLion", iconKey: "clion", names: ["clion", "clion.sh"] },
2132
+ {
2133
+ appName: "DataGrip",
2134
+ iconKey: "datagrip",
2135
+ names: ["datagrip", "datagrip.sh"]
2136
+ },
2137
+ { appName: "Rider", iconKey: "rider", names: ["rider", "rider.sh"] },
2138
+ {
2139
+ appName: "Android Studio",
2140
+ iconKey: "android-studio",
2141
+ names: ["studio", "studio.sh", "android studio"]
2142
+ },
2143
+ { appName: "Xcode", iconKey: "xcode", names: ["xcode", "xcode.app"] },
2144
+ { appName: "Postman", iconKey: "postman", names: ["postman"] },
2145
+ { appName: "Insomnia", iconKey: "insomnia", names: ["insomnia"] },
2146
+ { appName: "Hoppscotch", iconKey: "hoppscotch", names: ["hoppscotch"] },
2147
+ { appName: "TablePlus", iconKey: "tableplus", names: ["tableplus"] },
2148
+ { appName: "DBeaver", iconKey: "dbeaver", names: ["dbeaver"] },
2149
+ { appName: "RedisInsight", iconKey: "redis", names: ["redisinsight"] },
2150
+ { appName: "Prisma", iconKey: "prisma", names: ["prisma"] },
2151
+ { appName: "Drizzle Kit", iconKey: "drizzle", names: ["drizzle-kit"] },
2152
+ { appName: "Supabase", iconKey: "supabase", names: ["supabase"] },
2153
+ { appName: "Firebase", iconKey: "firebase", names: ["firebase"] },
2154
+ { appName: "Vercel", iconKey: "vercel", names: ["vercel"] },
2155
+ { appName: "Netlify", iconKey: "netlify", names: ["netlify", "ntl"] },
2156
+ { appName: "Fly.io", iconKey: "fly", names: ["fly", "flyctl"] },
2157
+ { appName: "Heroku", iconKey: "heroku", names: ["heroku"] },
2158
+ { appName: "Stripe CLI", iconKey: "stripe", names: ["stripe"] },
2159
+ { appName: "Twilio CLI", iconKey: "twilio", names: ["twilio"] },
2160
+ { appName: "Mailpit", iconKey: "mail", names: ["mailpit"] },
2161
+ { appName: "MailHog", iconKey: "mail", names: ["mailhog"] },
2162
+ { appName: "OpenSSL", iconKey: "openssl", names: ["openssl"] },
2163
+ { appName: "mkcert", iconKey: "certificate", names: ["mkcert"] },
2164
+ { appName: "mkdocs", iconKey: "markdown", names: ["mkdocs"] },
2165
+ { appName: "Hugo", iconKey: "hugo", names: ["hugo"] },
2166
+ { appName: "Jekyll", iconKey: "jekyll", names: ["jekyll"] },
2167
+ { appName: "Eleventy", iconKey: "eleventy", names: ["eleventy", "11ty"] },
2168
+ { appName: "Docusaurus", iconKey: "docusaurus", names: ["docusaurus"] },
2169
+ { appName: "Trunk", iconKey: "rust", names: ["trunk"] },
2170
+ { appName: "wasm-pack", iconKey: "webassembly", names: ["wasm-pack"] },
2171
+ { appName: "wasmtime", iconKey: "webassembly", names: ["wasmtime"] },
2172
+ {
2173
+ appName: "Language Server",
2174
+ iconKey: "language-server",
2175
+ names: [
2176
+ "lua-language-server",
2177
+ "yaml-language-server",
2178
+ "vscode-json-language-server",
2179
+ "docker-langserver",
2180
+ "bash-language-server",
2181
+ "marksman",
2182
+ "taplo"
2183
+ ]
2184
+ }
2185
+ ];
2186
+ var executableNameLookup = new Map;
2187
+ for (const entry of processCatalogEntries) {
2188
+ for (const name of entry.names) {
2189
+ executableNameLookup.set(normalizeProcessName(name), {
2190
+ appName: entry.appName,
2191
+ iconKey: entry.iconKey
2192
+ });
2193
+ }
2194
+ }
2195
+ var processNameCatalogSize = executableNameLookup.size;
2196
+ function canonicalizeProcessName(processName) {
2197
+ const executableName = executableNameFromPathOrName(processName);
2198
+ if (executableName === undefined) {
2199
+ return;
2200
+ }
2201
+ const directMatch = lookupCanonicalProcess(executableName);
2202
+ if (directMatch !== undefined) {
2203
+ return directMatch;
2204
+ }
2205
+ const pathMatch = lookupCanonicalPathHint(processName);
2206
+ if (pathMatch !== undefined) {
2207
+ return pathMatch;
2208
+ }
2209
+ return;
2210
+ }
2211
+ function guessCanonicalProcessFromCommand(command) {
2212
+ const tokens = shellCommandTokens(command);
2213
+ const executable = executableTokenFromCommandTokens(tokens);
2214
+ if (executable === undefined) {
2215
+ return;
2216
+ }
2217
+ return canonicalizeProcessName(executable);
2218
+ }
2219
+ function lookupCanonicalProcess(processName) {
2220
+ const normalized = normalizeProcessName(processName);
2221
+ const identity = executableNameLookup.get(normalized);
2222
+ if (identity === undefined) {
2223
+ return;
2224
+ }
2225
+ return {
2226
+ ...identity,
2227
+ executableName: processName,
2228
+ matchedName: normalized
2229
+ };
2230
+ }
2231
+ function lookupCanonicalPathHint(processPath) {
2232
+ const normalizedPath = normalizeProcessName(processPath);
2233
+ const pathParts = normalizedPath.split("/").map((part) => trimExecutableExtension(part)).filter((part) => part.length > 0);
2234
+ for (const pathPart of pathParts.reverse()) {
2235
+ const pathHint = executableNameLookup.get(pathPart);
2236
+ if (pathHint !== undefined) {
2237
+ return {
2238
+ ...pathHint,
2239
+ executableName: executableNameFromPathOrName(processPath) ?? processPath,
2240
+ matchedName: pathPart
2241
+ };
2242
+ }
2243
+ }
2244
+ return;
2245
+ }
2246
+ function executableTokenFromCommandTokens(tokens) {
2247
+ const [firstToken, ...remainingTokens] = tokens;
2248
+ switch (firstToken) {
2249
+ case undefined:
2250
+ return;
2251
+ case "env":
2252
+ case "/usr/bin/env":
2253
+ return executableTokenFromCommandTokens(remainingTokens.filter((token) => !isEnvironmentAssignment(token)));
2254
+ case "sudo":
2255
+ case "command":
2256
+ case "exec":
2257
+ case "nohup":
2258
+ return executableTokenFromCommandTokens(remainingTokens.filter((token) => !token.startsWith("-")));
2259
+ default:
2260
+ switch (isEnvironmentAssignment(firstToken)) {
2261
+ case true:
2262
+ return executableTokenFromCommandTokens(remainingTokens);
2263
+ case false:
2264
+ return firstToken;
2265
+ }
2266
+ }
2267
+ }
2268
+ function executableNameFromPathOrName(processName) {
2269
+ const trimmed = processName.trim();
2270
+ if (trimmed.length === 0) {
2271
+ return;
2272
+ }
2273
+ const normalizedSeparators = trimmed.replaceAll("\\", "/");
2274
+ const [lastPathPart] = normalizedSeparators.split("/").filter((part) => part.length > 0).slice(-1);
2275
+ switch (lastPathPart) {
2276
+ case undefined:
2277
+ return;
2278
+ default:
2279
+ return trimExecutableExtension(lastPathPart);
2280
+ }
2281
+ }
2282
+ function trimExecutableExtension(executableName) {
2283
+ const normalized = executableName.toLowerCase();
2284
+ switch (true) {
2285
+ case normalized.endsWith(".app"):
2286
+ return executableName.slice(0, -4);
2287
+ case normalized.endsWith(".exe"):
2288
+ return executableName.slice(0, -4);
2289
+ case normalized.endsWith(".cmd"):
2290
+ case normalized.endsWith(".bat"):
2291
+ return executableName.slice(0, -4);
2292
+ default:
2293
+ return executableName;
2294
+ }
2295
+ }
2296
+ function normalizeProcessName(processName) {
2297
+ return trimExecutableExtension(processName.trim().replaceAll("\\", "/").toLowerCase());
2298
+ }
2299
+ function isEnvironmentAssignment(token) {
2300
+ return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
2301
+ }
2302
+ function shellCommandTokens(command) {
2303
+ const tokens = [];
2304
+ let currentToken = "";
2305
+ let quote = undefined;
2306
+ let escaping = false;
2307
+ for (const character of command.trim()) {
2308
+ switch (true) {
2309
+ case escaping:
2310
+ currentToken = `${currentToken}${character}`;
2311
+ escaping = false;
2312
+ break;
2313
+ case character === "\\":
2314
+ escaping = true;
2315
+ break;
2316
+ case (quote === undefined && (character === '"' || character === "'")):
2317
+ quote = character;
2318
+ break;
2319
+ case quote === character:
2320
+ quote = undefined;
2321
+ break;
2322
+ case (quote === undefined && /\s/.test(character)):
2323
+ if (currentToken.length > 0) {
2324
+ tokens.push(currentToken);
2325
+ currentToken = "";
2326
+ }
2327
+ break;
2328
+ default:
2329
+ currentToken = `${currentToken}${character}`;
2330
+ break;
2331
+ }
2332
+ }
2333
+ if (currentToken.length > 0) {
2334
+ tokens.push(currentToken);
2335
+ }
2336
+ return tokens;
2337
+ }
2338
+
1477
2339
  // src/channelSession.ts
1478
2340
  var codexTypingHeartbeatMs = 5000;
1479
2341
  var defaultStreamFlushIntervalMs = 150;
@@ -1534,7 +2396,7 @@ async function attachChannelSession(args) {
1534
2396
  }
1535
2397
  switch (method) {
1536
2398
  case "turn/started":
1537
- if (turnId !== undefined) {
2399
+ if (threadId !== undefined && turnId !== undefined) {
1538
2400
  rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId);
1539
2401
  }
1540
2402
  break;
@@ -1669,12 +2531,15 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
1669
2531
  forwardedTerminalInputKeys: new Set,
1670
2532
  webSearchProgressOutputs: createBoundedCache(maxForwardedTurnIds),
1671
2533
  pendingApprovalRequests: new Map,
2534
+ approvalPromptChain: Promise.resolve(),
1672
2535
  pendingPortForwardRequests: new Map,
2536
+ queuedPortForwardCandidates: new Map,
1673
2537
  approvedForwardPorts: new Set,
1674
2538
  approvedForwardTargets: new Map,
1675
2539
  dismissedForwardTargets: new Map,
1676
2540
  portForwardWatcher: undefined,
1677
2541
  activeProcessingState: undefined,
2542
+ pendingReconnectContextInjection: undefined,
1678
2543
  terminalInputForwardChain: Promise.resolve(),
1679
2544
  webSearchProgressForwardChain: Promise.resolve(),
1680
2545
  typingHeartbeat: undefined,
@@ -1682,6 +2547,23 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
1682
2547
  runtimeSettings: runtimeSettingsFromOptions(options)
1683
2548
  };
1684
2549
  }
2550
+ async function handleLostPortForwardCandidate(args, state, payloadContext, candidate) {
2551
+ dropLostQueuedPortForwardCandidate(args, state, candidate);
2552
+ await expireLostPendingPortForwardRequest(args, state, payloadContext, candidate);
2553
+ await revokeLostApprovedForwardPort(args, state, candidate);
2554
+ }
2555
+ function dropLostQueuedPortForwardCandidate(args, state, candidate) {
2556
+ const queued = state.queuedPortForwardCandidates.get(candidate.port);
2557
+ if (queued === undefined || !sameForwardCandidate(queued, candidate)) {
2558
+ return;
2559
+ }
2560
+ state.queuedPortForwardCandidates.delete(candidate.port);
2561
+ args.log("port_forward.queued_request_dropped", {
2562
+ port: candidate.port,
2563
+ pid: candidate.pid,
2564
+ reason: "listener_exited"
2565
+ });
2566
+ }
1685
2567
  function startPortForwardWatchIfEnabled(args, state, payloadContext) {
1686
2568
  if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined || state.kandanThreadId === undefined || state.codexThreadId === undefined) {
1687
2569
  return;
@@ -1692,8 +2574,10 @@ function startPortForwardWatchIfEnabled(args, state, payloadContext) {
1692
2574
  state.approvedForwardPorts.add(port);
1693
2575
  }
1694
2576
  state.portForwardWatcher = start({
2577
+ rootCwd: watchOptions.rootCwd ?? args.options.cwd,
1695
2578
  ...watchOptions,
1696
2579
  onCandidate: (candidate) => publishPortForwardPrompt(args, state, payloadContext, candidate),
2580
+ onCandidateLost: (candidate) => handleLostPortForwardCandidate(args, state, payloadContext, candidate),
1697
2581
  onError: (error) => args.log("port_forward.watch_failed", { message: error.message })
1698
2582
  });
1699
2583
  }
@@ -1746,7 +2630,11 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
1746
2630
  return;
1747
2631
  }
1748
2632
  if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
1749
- return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
2633
+ return {
2634
+ instanceId: args.instanceId,
2635
+ ok: false,
2636
+ error: "thread_not_bound"
2637
+ };
1750
2638
  }
1751
2639
  const interrupted = interruptPendingKandanMessages(state.queue, control.throughSeq);
1752
2640
  const activeQueuedSeq = interruptibleQueuedSeq(state.turn);
@@ -1799,7 +2687,11 @@ function updateSessionSettings(args, state, control) {
1799
2687
  return;
1800
2688
  }
1801
2689
  if (state.codexThreadId === undefined) {
1802
- return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
2690
+ return {
2691
+ instanceId: args.instanceId,
2692
+ ok: false,
2693
+ error: "thread_not_bound"
2694
+ };
1803
2695
  }
1804
2696
  state.runtimeSettings = mergeRuntimeSettings(state.runtimeSettings, control);
1805
2697
  publishRuntimeSettings(args, state).catch((error) => {
@@ -1829,16 +2721,27 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
1829
2721
  return;
1830
2722
  }
1831
2723
  if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
1832
- return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
2724
+ return {
2725
+ instanceId: args.instanceId,
2726
+ ok: false,
2727
+ error: "thread_not_bound"
2728
+ };
1833
2729
  }
1834
2730
  const approval = state.pendingApprovalRequests.get(approvalRequestKey(control.requestId, control.sourceSeq));
1835
2731
  if (approval === undefined) {
1836
- return { instanceId: args.instanceId, ok: false, error: "approval_request_not_found" };
2732
+ return {
2733
+ instanceId: args.instanceId,
2734
+ ok: false,
2735
+ error: "approval_request_not_found"
2736
+ };
1837
2737
  }
1838
2738
  state.pendingApprovalRequests.delete(approvalRequestKey(control.requestId, control.sourceSeq));
1839
2739
  const codexDecision = control.decision === "approve" ? "accept" : "decline";
1840
2740
  approval.resolve({ decision: codexDecision });
1841
- state.activeProcessingState = { seq: approval.sourceSeq, reason: "streaming response" };
2741
+ state.activeProcessingState = {
2742
+ seq: approval.sourceSeq,
2743
+ reason: "streaming response"
2744
+ };
1842
2745
  await publishMessageState(args, state.kandanThreadId, approval.sourceSeq, {
1843
2746
  status: "processing",
1844
2747
  reason: "streaming response"
@@ -1851,6 +2754,17 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
1851
2754
  });
1852
2755
  return { instanceId: args.instanceId, ok: true };
1853
2756
  }
2757
+ async function drainQueuedPortForwardPrompt(args, state, payloadContext) {
2758
+ if (state.pendingPortForwardRequests.size > 0 || state.queuedPortForwardCandidates.size === 0) {
2759
+ return;
2760
+ }
2761
+ const next = Array.from(state.queuedPortForwardCandidates.values()).sort((left, right) => left.port - right.port)[0];
2762
+ if (next === undefined) {
2763
+ return;
2764
+ }
2765
+ state.queuedPortForwardCandidates.delete(next.port);
2766
+ await publishPortForwardPrompt(args, state, payloadContext, next);
2767
+ }
1854
2768
  async function resolvePendingPortForwardRequest(args, state, payloadContext, control) {
1855
2769
  const request = state.pendingPortForwardRequests.get(control.requestId);
1856
2770
  if (request === undefined) {
@@ -1863,7 +2777,11 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1863
2777
  actor_user_id: control.actorUserId ?? null,
1864
2778
  reason: "sender_not_allowed"
1865
2779
  });
1866
- return { instanceId: args.instanceId, ok: false, error: "sender_not_allowed" };
2780
+ return {
2781
+ instanceId: args.instanceId,
2782
+ ok: false,
2783
+ error: "sender_not_allowed"
2784
+ };
1867
2785
  }
1868
2786
  state.pendingPortForwardRequests.delete(control.requestId);
1869
2787
  if (control.decision === "deny") {
@@ -1874,14 +2792,18 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1874
2792
  port: request.port,
1875
2793
  pid: request.pid
1876
2794
  });
2795
+ await drainQueuedPortForwardPrompt(args, state, payloadContext);
1877
2796
  return { instanceId: args.instanceId, ok: true };
1878
2797
  }
1879
2798
  state.approvedForwardPorts.add(request.port);
1880
2799
  state.approvedForwardTargets.set(request.port, approvedTargetFromRequest(request));
2800
+ const processIdentity = guessCanonicalProcessFromCommand(request.command);
1881
2801
  const capabilities = args.options.onForwardPortApproved?.(request.port, {
1882
2802
  kandanThreadId: state.kandanThreadId ?? null,
1883
2803
  codexThreadId: state.codexThreadId ?? null,
1884
- channelSlug: args.options.channelSession.channelSlug ?? null
2804
+ channelSlug: args.options.channelSession.channelSlug ?? null,
2805
+ processName: processIdentity?.appName ?? null,
2806
+ processIconKey: processIdentity?.iconKey ?? null
1885
2807
  });
1886
2808
  await publishForwardPortResolvedEvent(args, request, capabilities);
1887
2809
  await publishMessageStateForPortForwardResult(args, state, request, "processed");
@@ -1891,6 +2813,7 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1891
2813
  port: request.port,
1892
2814
  pid: request.pid
1893
2815
  });
2816
+ await drainQueuedPortForwardPrompt(args, state, payloadContext);
1894
2817
  return { instanceId: args.instanceId, ok: true, port: request.port };
1895
2818
  }
1896
2819
  function portForwardControlSenderAllowed(args, payloadContext, control) {
@@ -1927,37 +2850,39 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1927
2850
  state.approvedForwardTargets.set(review.target.port, review.target);
1928
2851
  return;
1929
2852
  case "revoke_and_prompt":
2853
+ if (state.pendingPortForwardRequests.size > 0) {
2854
+ state.queuedPortForwardCandidates.set(candidate.port, candidate);
2855
+ args.log("port_forward.request_queued", {
2856
+ port: candidate.port,
2857
+ pid: candidate.pid,
2858
+ pending_count: state.pendingPortForwardRequests.size
2859
+ });
2860
+ return;
2861
+ }
1930
2862
  await revokeApprovedForwardPort(args, state, review.revoked, review.reason);
1931
2863
  break;
1932
2864
  case "prompt":
2865
+ if (state.pendingPortForwardRequests.size > 0) {
2866
+ state.queuedPortForwardCandidates.set(candidate.port, candidate);
2867
+ args.log("port_forward.request_queued", {
2868
+ port: candidate.port,
2869
+ pid: candidate.pid,
2870
+ pending_count: state.pendingPortForwardRequests.size
2871
+ });
2872
+ return;
2873
+ }
1933
2874
  break;
1934
2875
  }
1935
2876
  const requestId = `port-forward-${randomUUID()}`;
1936
2877
  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,
2878
+ const sourceSeq = portForwardApprovalSourceSeq(state);
2879
+ if (sourceSeq === undefined) {
2880
+ args.log("port_forward.prompt_skipped", {
1944
2881
  port: candidate.port,
1945
2882
  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");
2883
+ reason: "source_seq_missing"
2884
+ });
2885
+ return;
1961
2886
  }
1962
2887
  const request = pendingRequestFromCandidate({
1963
2888
  requestId,
@@ -1965,6 +2890,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1965
2890
  candidate
1966
2891
  });
1967
2892
  state.pendingPortForwardRequests.set(requestId, request);
2893
+ const processIdentity = guessCanonicalProcessFromCommand(candidate.command);
2894
+ const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
2895
+ const processName = processIdentity?.appName ?? label;
1968
2896
  await publishForwardPortRequestedEvent(args, request);
1969
2897
  await publishMessageState(args, state.kandanThreadId, sourceSeq, {
1970
2898
  status: "processing",
@@ -1972,13 +2900,20 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1972
2900
  approval: {
1973
2901
  requestId,
1974
2902
  kind: "local_runner_port_forward",
1975
- summary: `Open runner port ${candidate.port} from ${label}`,
2903
+ summary: `Make ${processName} on port ${candidate.port} accessible on Linzumi?`,
1976
2904
  reason: portForwardPromptReason(candidate),
2905
+ port: candidate.port,
2906
+ pid: candidate.pid,
2907
+ command: candidate.command,
2908
+ ...candidate.cwd === undefined ? {} : { cwd: candidate.cwd },
2909
+ ...processIdentity?.appName === undefined ? {} : { processName },
2910
+ ...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey },
2911
+ ...processIconPath === undefined ? {} : { processIconPath },
1977
2912
  choices: [
1978
2913
  {
1979
2914
  decision: "approve",
1980
- label: "Open preview",
1981
- description: `Allow Kandan to proxy HTTP, HTTPS, and WebSocket traffic for runner port ${candidate.port}.`
2915
+ label: "Enable",
2916
+ description: `Allow Linzumi to proxy HTTP, HTTPS, and WebSocket traffic for runner port ${candidate.port}.`
1982
2917
  },
1983
2918
  {
1984
2919
  decision: "deny",
@@ -1996,6 +2931,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1996
2931
  pid: candidate.pid
1997
2932
  });
1998
2933
  }
2934
+ function portForwardApprovalSourceSeq(state) {
2935
+ return state.activeProcessingState?.seq ?? state.rootSeq;
2936
+ }
1999
2937
  async function revokeApprovedForwardPort(args, state, target, reason) {
2000
2938
  state.approvedForwardPorts.delete(target.port);
2001
2939
  state.approvedForwardTargets.delete(target.port);
@@ -2015,30 +2953,58 @@ async function revokeApprovedForwardPort(args, state, target, reason) {
2015
2953
  reason
2016
2954
  });
2017
2955
  }
2956
+ async function revokeLostApprovedForwardPort(args, state, candidate) {
2957
+ if (!state.approvedForwardPorts.has(candidate.port)) {
2958
+ return;
2959
+ }
2960
+ await revokeApprovedForwardPort(args, state, state.approvedForwardTargets.get(candidate.port) ?? candidate, "listener_exited");
2961
+ }
2962
+ async function expireLostPendingPortForwardRequest(args, state, payloadContext, candidate) {
2963
+ const request = Array.from(state.pendingPortForwardRequests.values()).find((request2) => sameForwardCandidate(approvedTargetFromRequest(request2), candidate));
2964
+ if (request === undefined) {
2965
+ return;
2966
+ }
2967
+ state.pendingPortForwardRequests.delete(request.requestId);
2968
+ await publishMessageStateForPortForwardResult(args, state, request, "failed", "port_forward_listener_exited");
2969
+ args.log("port_forward.pending_request_expired", {
2970
+ request_id: request.requestId,
2971
+ port: request.port,
2972
+ pid: request.pid,
2973
+ reason: "listener_exited"
2974
+ });
2975
+ await drainQueuedPortForwardPrompt(args, state, payloadContext);
2976
+ }
2018
2977
  async function publishPortForwardReadyMessage(args, state, payloadContext, request) {
2019
2978
  if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
2020
2979
  return;
2021
2980
  }
2022
2981
  const path = forwardPreviewPath(args.options.runnerId, request.port);
2982
+ const processIdentity = guessCanonicalProcessFromCommand(request.command);
2983
+ const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
2984
+ const processName = processIdentity?.appName ?? `Port ${request.port}`;
2985
+ const readySummary = `${processName} on port ${request.port} is ready in Linzumi.`;
2023
2986
  await pushOptional(args.kandan, args.topic, "session:post_thread_message", {
2024
2987
  workspace: args.options.channelSession.workspaceSlug,
2025
2988
  channel: args.options.channelSession.channelSlug,
2026
2989
  thread_id: state.kandanThreadId,
2027
- body: `Runner port ${request.port} is open in Kandan: [Open preview](${path})`,
2990
+ body: `${readySummary} [Open](${path})`,
2028
2991
  payload: {
2029
2992
  ...localRunnerPayload(args.options, args.instanceId, "port_forward_ready", state.codexThreadId, payloadContext),
2030
2993
  ...state.rootSeq === undefined ? {} : { reply_to_seq: state.rootSeq },
2031
2994
  structured: {
2032
2995
  kind: "local_runner_port_forward_ready",
2033
2996
  status: "ready",
2034
- summary: `Runner port ${request.port} is open in Kandan.`,
2997
+ summary: readySummary,
2035
2998
  next_action: "Open HTTP/HTTPS/WebSocket preview",
2036
2999
  source_path: path,
2037
- link_label: "Open preview",
3000
+ link_label: "Open",
2038
3001
  request_id: request.requestId,
2039
3002
  port: request.port,
2040
3003
  pid: request.pid,
2041
3004
  command: request.command,
3005
+ ...processIdentity?.appName === undefined ? {} : { process_name: processIdentity.appName },
3006
+ ...processIdentity?.iconKey === undefined ? {} : { process_icon_key: processIdentity.iconKey },
3007
+ ...processIconPath === undefined ? {} : { process_icon_path: processIconPath },
2042
3008
  ...request.cwd === undefined ? {} : { cwd: request.cwd },
2043
3009
  url: path
2044
3010
  }
@@ -2067,10 +3033,15 @@ async function publishForwardPortResolvedEvent(args, request, capabilities) {
2067
3033
  ...capabilities === undefined ? {} : { capabilities }
2068
3034
  }, args.log);
2069
3035
  }
2070
- async function publishMessageStateForPortForwardResult(args, state, request, status) {
3036
+ async function publishMessageStateForPortForwardResult(args, state, request, status, failedReason = "port_forward_denied") {
2071
3037
  if (state.kandanThreadId === undefined) {
2072
3038
  return;
2073
3039
  }
3040
+ const activeProcessingState = state.activeProcessingState;
3041
+ if (activeProcessingState !== undefined && activeProcessingState.seq === request.sourceSeq) {
3042
+ await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
3043
+ return;
3044
+ }
2074
3045
  switch (status) {
2075
3046
  case "processed":
2076
3047
  await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
@@ -2080,7 +3051,7 @@ async function publishMessageStateForPortForwardResult(args, state, request, sta
2080
3051
  case "failed":
2081
3052
  await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
2082
3053
  status: "failed",
2083
- reason: "port_forward_denied"
3054
+ reason: failedReason
2084
3055
  });
2085
3056
  break;
2086
3057
  }
@@ -2151,7 +3122,12 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
2151
3122
  actorUserId: event.actorUserId,
2152
3123
  actorSlug: event.actorSlug
2153
3124
  });
2154
- if (result.ok === true) {
3125
+ if (result === undefined) {
3126
+ await publishKandanMessageState(args, event, {
3127
+ status: "failed",
3128
+ reason: "port_forward_decision_missing"
3129
+ });
3130
+ } else if (result.ok === true) {
2155
3131
  await publishKandanMessageState(args, event, { status: "processed" });
2156
3132
  } else {
2157
3133
  await publishKandanMessageState(args, event, {
@@ -2305,7 +3281,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2305
3281
  if (next === undefined) {
2306
3282
  return;
2307
3283
  }
2308
- state.turn = { status: "starting", queuedSeq: next.seq, interruptAfterStart: false };
3284
+ state.turn = {
3285
+ status: "starting",
3286
+ queuedSeq: next.seq,
3287
+ interruptAfterStart: false
3288
+ };
2309
3289
  state.activeProcessingState = { seq: next.seq, reason: "starting turn" };
2310
3290
  args.log("codex.turn_starting", {
2311
3291
  queued_seq: next.seq,
@@ -2328,10 +3308,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2328
3308
  }
2329
3309
  const started = await args.codex.request("turn/start", {
2330
3310
  threadId: codexThreadId,
2331
- input: await codexInputItemsForQueuedKandanMessage(args, next),
3311
+ input: await codexInputItemsForQueuedKandanMessage(args, next, state.pendingReconnectContextInjection),
2332
3312
  ...codexTurnRuntimeOverrides(runtimeOptionsForSettings(args.options, state.runtimeSettings))
2333
3313
  });
2334
3314
  const turnId = extractTurnIdFromResponse(started);
3315
+ state.pendingReconnectContextInjection = undefined;
2335
3316
  const interruptAfterStart = state.turn.status === "starting" && state.turn.interruptAfterStart;
2336
3317
  state.turn = { status: "active", turnId, queuedSeq: next.seq };
2337
3318
  rememberTurnReplyTarget(state, turnId, next.seq);
@@ -2357,12 +3338,14 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2357
3338
  await stopCodexTyping(args, state);
2358
3339
  const newCodexThreadId = await startCodexThread(args.codex, args.options);
2359
3340
  state.codexThreadId = newCodexThreadId;
3341
+ await bindCurrentCodexThread(args, state);
2360
3342
  args.log("codex.thread_rebound", {
2361
3343
  kandan_thread_id: state.kandanThreadId,
2362
3344
  old_codex_thread_id: oldCodexThreadId ?? null,
2363
3345
  new_codex_thread_id: newCodexThreadId
2364
3346
  });
2365
3347
  await postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId);
3348
+ state.pendingReconnectContextInjection = await fetchReconnectContextInjection(args, state);
2366
3349
  requeuePendingKandanMessageFront(state.queue, next);
2367
3350
  state.turn = { status: "idle" };
2368
3351
  await drainKandanMessageQueue(args, state, payloadContext);
@@ -2388,6 +3371,26 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2388
3371
  });
2389
3372
  }
2390
3373
  }
3374
+ async function fetchReconnectContextInjection(args, state) {
3375
+ if (state.kandanThreadId === undefined) {
3376
+ throw new Error("cannot fetch reconnect context without a Linzumi thread id");
3377
+ }
3378
+ const session = args.options.channelSession;
3379
+ const reply = await pushOk(args.kandan, args.topic, "session:thread_context", {
3380
+ workspace: session.workspaceSlug,
3381
+ channel: session.channelSlug,
3382
+ thread_id: state.kandanThreadId
3383
+ });
3384
+ const messages = parseReconnectContextMessages(reply.messages);
3385
+ const summarizer = args.options.reconnectContextSummarizer ?? createConfiguredReconnectContextSummarizer();
3386
+ const injection = await buildReconnectContextInjection(messages, summarizer);
3387
+ args.log("codex.thread_reconnect_context_prepared", {
3388
+ codex_thread_id: state.codexThreadId ?? null,
3389
+ kandan_thread_id: state.kandanThreadId,
3390
+ context_message_count: messages.length
3391
+ });
3392
+ return injection;
3393
+ }
2391
3394
  async function handleCodexServerRequest(args, state, payloadContext, request) {
2392
3395
  const params = objectValue(request.params) ?? {};
2393
3396
  const turnId = codexNotificationTurnId(params);
@@ -2425,35 +3428,48 @@ function codexApprovalRequestCanSurface(method) {
2425
3428
  return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval";
2426
3429
  }
2427
3430
  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
3431
+ const approvalResult = state.approvalPromptChain.then(async () => {
3432
+ const sourceSeq = activeQueuedSeqForTurn(state.turn, turnId);
3433
+ if (sourceSeq === undefined || state.kandanThreadId === undefined) {
3434
+ const message = `Codex approval request has no active Kandan source message: ${request.method}`;
3435
+ await failActiveCodexTurn(args, state, turnId, message, payloadContext);
3436
+ throw new Error(message);
3437
+ }
3438
+ const approval = codexApprovalMessageState(request);
3439
+ state.activeProcessingState = {
3440
+ seq: sourceSeq,
3441
+ reason: "awaiting approval",
3442
+ approval
2454
3443
  };
2455
- state.pendingApprovalRequests.set(approvalRequestKey(request2.requestId, request2.sourceSeq), request2);
3444
+ const approvalPromise = new Promise((resolve2, reject) => {
3445
+ const pendingRequest = {
3446
+ requestId: approval.requestId,
3447
+ sourceSeq,
3448
+ turnId,
3449
+ resolve: resolve2,
3450
+ reject
3451
+ };
3452
+ state.pendingApprovalRequests.set(approvalRequestKey(pendingRequest.requestId, pendingRequest.sourceSeq), pendingRequest);
3453
+ });
3454
+ await publishMessageState(args, state.kandanThreadId, sourceSeq, {
3455
+ status: "processing",
3456
+ reason: "awaiting approval",
3457
+ approval
3458
+ }, undefined, undefined, state.codexThreadId);
3459
+ args.log("codex.approval_request_pending", {
3460
+ request_id: approval.requestId,
3461
+ source_seq: sourceSeq,
3462
+ turn_id: turnId,
3463
+ method: request.method
3464
+ });
3465
+ return approvalPromise;
3466
+ });
3467
+ state.approvalPromptChain = approvalResult.then(() => {
3468
+ return;
3469
+ }, () => {
3470
+ return;
2456
3471
  });
3472
+ return approvalResult;
2457
3473
  }
2458
3474
  function rejectPendingApprovalRequests(state, error) {
2459
3475
  const pendingApprovals = [...state.pendingApprovalRequests.values()];
@@ -2479,7 +3495,11 @@ async function forwardCompletedCodexTurn(args, state, turnId, payloadContext) {
2479
3495
  const completingActiveTurn = completingQueuedSeq !== undefined;
2480
3496
  const completingLocalTuiTurn = isLocalTuiTurn(state, turnId);
2481
3497
  if (completingQueuedSeq !== undefined) {
2482
- state.turn = { status: "completing", turnId, queuedSeq: completingQueuedSeq };
3498
+ state.turn = {
3499
+ status: "completing",
3500
+ turnId,
3501
+ queuedSeq: completingQueuedSeq
3502
+ };
2483
3503
  }
2484
3504
  await waitForPendingTuiInputMirror(state, turnId);
2485
3505
  await waitForStreamingForwardChains(args, state, payloadContext);
@@ -2841,7 +3861,10 @@ async function forwardReasoningDeltaPayload(args, state, delta, payloadContext)
2841
3861
  }
2842
3862
  } else {
2843
3863
  await editCodexStructuredOutput(args, state, existing.seq, nextContent, codexReasoningStructuredMessage(delta.itemKey, nextContent, "streaming"));
2844
- rememberStreamingReasoningOutput(state, { ...existing, content: nextContent });
3864
+ rememberStreamingReasoningOutput(state, {
3865
+ ...existing,
3866
+ content: nextContent
3867
+ });
2845
3868
  }
2846
3869
  args.log("kandan.codex_reasoning_delta_forwarded", {
2847
3870
  item_key: delta.itemKey,
@@ -3398,7 +4421,10 @@ function rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId) {
3398
4421
  if (state.kandanThreadId !== undefined) {
3399
4422
  startCodexTypingHeartbeat(args, state, state.kandanThreadId);
3400
4423
  }
3401
- args.log("codex.tui_turn_started", { turn_id: turnId, codex_thread_id: threadId });
4424
+ args.log("codex.tui_turn_started", {
4425
+ turn_id: turnId,
4426
+ codex_thread_id: threadId
4427
+ });
3402
4428
  }
3403
4429
  function isLocalTuiTurn(state, turnId) {
3404
4430
  return state.localTuiTurnIds.has(turnId);
@@ -3465,7 +4491,10 @@ function clearPendingStreamFlushTimers(state) {
3465
4491
  clearStreamDeltaFlushTimer(state.fileChangeQueue);
3466
4492
  }
3467
4493
  function rememberTurnReplyTarget(state, turnId, replyToSeq) {
3468
- rememberBoundedCacheValue(state.turnReplyTargets, turnId, { turnId, replyToSeq });
4494
+ rememberBoundedCacheValue(state.turnReplyTargets, turnId, {
4495
+ turnId,
4496
+ replyToSeq
4497
+ });
3469
4498
  }
3470
4499
  function sourceMessageSeqForTurn(state, turnId) {
3471
4500
  return getBoundedCacheValue(state.turnReplyTargets, turnId)?.replyToSeq;
@@ -3474,20 +4503,12 @@ function fileChangePaths(structured) {
3474
4503
  const changes = arrayValue(structured.changes) ?? [];
3475
4504
  return changes.filter(isJsonObject).map((change) => stringValue(change.path) ?? "").filter((path) => path.trim() !== "");
3476
4505
  }
3477
- async function postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId) {
4506
+ async function postCodexThreadReboundMessage(args, state, payloadContext, _oldCodexThreadId, newCodexThreadId) {
3478
4507
  if (state.kandanThreadId === undefined) {
3479
4508
  return;
3480
4509
  }
3481
4510
  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
- `);
4511
+ const body = "[codex reconnected to new thread]";
3491
4512
  await pushOk(args.kandan, args.topic, "session:post_thread_message", {
3492
4513
  workspace: session.workspaceSlug,
3493
4514
  channel: session.channelSlug,
@@ -3622,10 +4643,19 @@ async function publishMessageState(args, threadId, seq, state, actorSlug, actorU
3622
4643
  approval_request_id: state.approval.requestId,
3623
4644
  approval_kind: state.approval.kind,
3624
4645
  approval_summary: state.approval.summary,
4646
+ ...state.approval.port === undefined ? {} : { approval_port: state.approval.port },
4647
+ ...state.approval.pid === undefined ? {} : { approval_pid: state.approval.pid },
4648
+ ...state.approval.command === undefined ? {} : { approval_command: state.approval.command },
4649
+ ...state.approval.cwd === undefined ? {} : { approval_cwd: state.approval.cwd },
4650
+ ...state.approval.processName === undefined ? {} : { approval_process_name: state.approval.processName },
4651
+ ...state.approval.processIconKey === undefined ? {} : { approval_process_icon_key: state.approval.processIconKey },
4652
+ ...state.approval.processIconPath === undefined ? {} : { approval_process_icon_path: state.approval.processIconPath },
3625
4653
  ...state.approval.reason === undefined ? {} : { approval_reason: state.approval.reason },
3626
4654
  ...state.approval.choices === undefined ? {} : { approval_choices: state.approval.choices },
3627
4655
  ...state.approval.allowedActorSlug === undefined ? {} : { approval_allowed_actor_slug: state.approval.allowedActorSlug },
3628
- ...state.approval.allowedActorUserId === undefined ? {} : { approval_allowed_actor_user_id: state.approval.allowedActorUserId }
4656
+ ...state.approval.allowedActorUserId === undefined ? {} : {
4657
+ approval_allowed_actor_user_id: state.approval.allowedActorUserId
4658
+ }
3629
4659
  } : {},
3630
4660
  ...actorSlug === undefined ? {} : { actor_slug: actorSlug },
3631
4661
  ...actorUserId === undefined ? {} : { actor_user_id: actorUserId }
@@ -3679,11 +4709,15 @@ function clearActiveProcessingState(state, seq) {
3679
4709
  state.activeProcessingState = undefined;
3680
4710
  }
3681
4711
  }
3682
- async function codexInputItemsForQueuedKandanMessage(args, message) {
4712
+ async function codexInputItemsForQueuedKandanMessage(args, message, reconnectContextInjection = undefined) {
3683
4713
  const attachments = await downloadQueuedKandanAttachments(args, message);
3684
4714
  const text = appendDownloadedAttachmentContext(codexInputForQueuedKandanMessage(message), attachments);
3685
4715
  const imageItems = attachments.flatMap((attachment) => attachment.isImage ? [{ type: "localImage", path: attachment.path }] : []);
3686
- return [{ type: "text", text }, ...imageItems];
4716
+ return [
4717
+ ...reconnectContextInjection === undefined ? [] : [reconnectContextInjection],
4718
+ { type: "text", text },
4719
+ ...imageItems
4720
+ ];
3687
4721
  }
3688
4722
  async function downloadQueuedKandanAttachments(args, message) {
3689
4723
  if (message.attachments.length === 0) {
@@ -3816,10 +4850,11 @@ async function uploadedFileIdsForCodexOutput(args, body, structured) {
3816
4850
  throw new Error("Kandan attachment prepare response missing upload_url");
3817
4851
  }
3818
4852
  const bytes = await readFile(file.path);
4853
+ const uploadBody = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
3819
4854
  const response = await fetch(resolveKandanAttachmentUrl(args.options.kandanUrl, uploadUrl), {
3820
4855
  method: uploadMethod,
3821
4856
  headers: { "content-type": file.contentType },
3822
- body: bytes
4857
+ body: uploadBody
3823
4858
  });
3824
4859
  if (!response.ok) {
3825
4860
  throw new Error(`Kandan attachment upload failed for ${file.fileName}: ${response.status} ${response.statusText}`);
@@ -4059,7 +5094,7 @@ function defaultCliAuditLogFile() {
4059
5094
  return override === undefined || override === "" ? join2(homedir(), ".linzumi", "logs", "command-events.jsonl") : override;
4060
5095
  }
4061
5096
  function redactForCliLog(value) {
4062
- return redactValue(value, undefined);
5097
+ return redactObject(value);
4063
5098
  }
4064
5099
  function redactValue(value, key) {
4065
5100
  if (sensitiveKey(key)) {
@@ -4204,8 +5239,10 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
4204
5239
  const child = spawn(codexBin, args, {
4205
5240
  cwd,
4206
5241
  env: process.env,
4207
- stdio: ["ignore", "inherit", "inherit"]
5242
+ stdio: ["ignore", "inherit", "inherit"],
5243
+ detached: true
4208
5244
  });
5245
+ const stop = () => stopCodexAppServerProcess(child);
4209
5246
  writeCliAuditEvent("process.spawned", {
4210
5247
  command: codexBin,
4211
5248
  args,
@@ -4231,33 +5268,57 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
4231
5268
  try {
4232
5269
  await waitForReadyz(url, child);
4233
5270
  } catch (error) {
4234
- child.kill("SIGINT");
5271
+ stop();
4235
5272
  throw error;
4236
5273
  }
4237
- return { url, process: child };
5274
+ return { url, process: child, stop };
5275
+ }
5276
+ function stopCodexAppServerProcess(child, killProcess = process.kill) {
5277
+ if (child.pid !== undefined) {
5278
+ try {
5279
+ killProcess(-child.pid, "SIGINT");
5280
+ return;
5281
+ } catch (error) {
5282
+ logProcessGroupSignalFailure(child.pid, error);
5283
+ }
5284
+ }
5285
+ child.kill("SIGINT");
5286
+ }
5287
+ function logProcessGroupSignalFailure(pid, error) {
5288
+ const code = processSignalErrorCode(error);
5289
+ const message = error instanceof Error ? error.message : String(error);
5290
+ const event = code === "EPERM" ? "process.group_signal_denied" : code === "ESRCH" ? "process.group_signal_missing" : "process.group_signal_failed";
5291
+ writeCliAuditEvent(event, {
5292
+ pid,
5293
+ signal: "SIGINT",
5294
+ code,
5295
+ message,
5296
+ fallback: "child_signal",
5297
+ purpose: "codex.app_server"
5298
+ });
5299
+ if (code === "EPERM") {
5300
+ process.stderr.write(`codex app-server process-group SIGINT denied for pid ${pid}; falling back to direct child SIGINT
5301
+ `);
5302
+ }
5303
+ }
5304
+ function processSignalErrorCode(error) {
5305
+ if (error !== null && typeof error === "object" && "code" in error) {
5306
+ const code = error.code;
5307
+ return typeof code === "string" ? code : undefined;
5308
+ }
5309
+ return;
4238
5310
  }
4239
5311
  function codexAppServerArgs(listenUrl, options = {}) {
4240
- return [
4241
- "app-server",
4242
- ...codexConfigArgs(options),
4243
- "--listen",
4244
- listenUrl
4245
- ];
5312
+ return ["app-server", ...codexConfigArgs(options), "--listen", listenUrl];
4246
5313
  }
4247
5314
  function codexConfigArgs(options) {
4248
5315
  return [
4249
- ...options.model === undefined ? [] : [
4250
- "-c",
4251
- `model=${JSON.stringify(options.model)}`
4252
- ],
5316
+ ...options.model === undefined ? [] : ["-c", `model=${JSON.stringify(options.model)}`],
4253
5317
  ...options.reasoningEffort === undefined ? [] : [
4254
5318
  "-c",
4255
5319
  `model_reasoning_effort=${JSON.stringify(options.reasoningEffort)}`
4256
5320
  ],
4257
- ...options.fast === true ? [
4258
- "-c",
4259
- `service_tier=${JSON.stringify("fast")}`
4260
- ] : []
5321
+ ...options.fast === true ? ["-c", `service_tier=${JSON.stringify("fast")}`] : []
4261
5322
  ];
4262
5323
  }
4263
5324
  async function connectCodexAppServer(websocketUrl, socketFactory = (url) => new WebSocket(url)) {
@@ -4611,7 +5672,9 @@ async function handleForwardHttpRequest(control, allowedPorts) {
4611
5672
  method: control.method,
4612
5673
  headers: requestHeaders(control.headers),
4613
5674
  redirect: "manual",
4614
- ...bodyDecision.body === undefined ? {} : { body: bodyDecision.body }
5675
+ ...bodyDecision.body === undefined ? {} : {
5676
+ body: bodyDecision.body.buffer.slice(bodyDecision.body.byteOffset, bodyDecision.body.byteOffset + bodyDecision.body.byteLength)
5677
+ }
4615
5678
  };
4616
5679
  const response = await fetchWithHttpsFallback(control.port, control.path, control.queryString, request);
4617
5680
  const upstreamBuffer = Buffer.from(await response.arrayBuffer());
@@ -5081,10 +6144,12 @@ function prepareCodeServerProfile(collaboration, editorRuntime) {
5081
6144
  const userDataDir = mkdtempSync(join4(tmpdir(), "kandan-local-editor-"));
5082
6145
  const extensionsDir = join4(userDataDir, "extensions");
5083
6146
  const collaborationServerDir = join4(userDataDir, "collaboration-server");
6147
+ const tempDir = join4(userDataDir, "tmp");
5084
6148
  const userSettingsDir = join4(userDataDir, "User");
5085
6149
  mkdirSync3(userSettingsDir, { recursive: true });
5086
6150
  mkdirSync3(extensionsDir, { recursive: true });
5087
6151
  mkdirSync3(collaborationServerDir, { recursive: true });
6152
+ mkdirSync3(tempDir, { recursive: true });
5088
6153
  if (editorRuntime !== undefined) {
5089
6154
  installDirectory(editorRuntime.assets.documentStateExtensionDir, join4(extensionsDir, "kandan.document-state-telemetry"));
5090
6155
  }
@@ -5117,6 +6182,12 @@ function prepareCodeServerLaunch(options) {
5117
6182
  "-p",
5118
6183
  codeServerSandboxProfile(options, codeServerExecutable.directory),
5119
6184
  "--",
6185
+ "/bin/sh",
6186
+ "-c",
6187
+ 'export HOME="$1"; export PWD="$1"; export TMPDIR="$2"; export TMP="$2"; export TEMP="$2"; shift 2; exec "$@"',
6188
+ "kandan-code-server-env",
6189
+ options.cwd,
6190
+ join4(options.userDataDir, "tmp"),
5120
6191
  codeServerExecutable.command,
5121
6192
  ...codeServerArgs(options.port, options.cwd, options.userDataDir, options.extensionsDir)
5122
6193
  ]
@@ -5397,10 +6468,14 @@ function installDirectory(sourceDir, destinationDir) {
5397
6468
  }
5398
6469
  function codeServerEnv(env, cwd, userDataDir, collaboration) {
5399
6470
  const { PORT: _port, ...hostEnv } = env;
6471
+ const tempDir = join4(userDataDir, "tmp");
5400
6472
  const base = {
5401
6473
  ...hostEnv,
5402
6474
  HOME: cwd,
5403
6475
  PWD: cwd,
6476
+ TMPDIR: tempDir,
6477
+ TMP: tempDir,
6478
+ TEMP: tempDir,
5404
6479
  XDG_CACHE_HOME: join4(userDataDir, "xdg-cache"),
5405
6480
  XDG_CONFIG_HOME: join4(userDataDir, "xdg-config"),
5406
6481
  XDG_DATA_HOME: join4(userDataDir, "xdg-data")
@@ -5789,7 +6864,7 @@ async function exchangeCodeForToken(args) {
5789
6864
  };
5790
6865
  }
5791
6866
  function stringBodyField(body, key) {
5792
- const value = key in body ? body[key] : undefined;
6867
+ const value = body[key];
5793
6868
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
5794
6869
  }
5795
6870
  function startCallbackServer(args) {
@@ -5859,7 +6934,9 @@ function isTcpAddress(address) {
5859
6934
  return typeof address === "object" && address !== null && typeof address.port === "number";
5860
6935
  }
5861
6936
  function writeOauthResult(response, args) {
5862
- response.writeHead(args.status, { "content-type": "text/html; charset=utf-8" });
6937
+ response.writeHead(args.status, {
6938
+ "content-type": "text/html; charset=utf-8"
6939
+ });
5863
6940
  response.end(oauthResultHtml(args));
5864
6941
  }
5865
6942
  function oauthResultHtml(args) {
@@ -6474,7 +7551,7 @@ function assertStartDependencies(status) {
6474
7551
  throw new Error(`Codex is not available at ${status.codex.command}. Install Codex or pass --codex-bin <path>.`);
6475
7552
  }
6476
7553
  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.");
7554
+ 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
7555
  }
6479
7556
  }
6480
7557
  function dependencyStatusPayload(status) {
@@ -6858,7 +7935,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6858
7935
  forwardPortAttributions.set(port, {
6859
7936
  kandanThreadId: attribution.kandanThreadId ?? null,
6860
7937
  codexThreadId: attribution.codexThreadId ?? null,
6861
- channelSlug: attribution.channelSlug ?? null
7938
+ channelSlug: attribution.channelSlug ?? null,
7939
+ processName: attribution.processName ?? null,
7940
+ processIconKey: attribution.processIconKey ?? null
6862
7941
  });
6863
7942
  };
6864
7943
  const clearForwardPortAttribution = (port) => {
@@ -6870,7 +7949,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6870
7949
  port,
6871
7950
  kandanThreadId: attribution?.kandanThreadId ?? null,
6872
7951
  codexThreadId: attribution?.codexThreadId ?? null,
6873
- channelSlug: attribution?.channelSlug ?? null
7952
+ channelSlug: attribution?.channelSlug ?? null,
7953
+ processName: attribution?.processName ?? null,
7954
+ processIconKey: attribution?.processIconKey ?? null
6874
7955
  };
6875
7956
  });
6876
7957
  const allowedCwds = { value: [...options.allowedCwds] };
@@ -6889,6 +7970,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6889
7970
  codexRemoteTui: true,
6890
7971
  startInstance: allowedCwds.value.length > 0,
6891
7972
  allowedCwds: allowedCwds.value,
7973
+ missingConfiguredAllowedCwds: options.missingConfiguredAllowedCwds ?? [],
6892
7974
  allowedCwdSuggestions: allowedCwdSuggestions(options.cwd, allowedCwds.value),
6893
7975
  portForwarding: liveForwardPorts.size > 0,
6894
7976
  allowedPorts: Array.from(liveForwardPorts).sort((left, right) => left - right),
@@ -6903,7 +7985,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6903
7985
  const joinPayload = () => ({
6904
7986
  clientName: "kandan-local-codex-runner",
6905
7987
  version: "0.0.1",
6906
- workspace: options.channelSession?.workspaceSlug ?? null,
7988
+ workspace: options.channelSession?.workspaceSlug ?? options.workspaceSlug ?? null,
6907
7989
  channel: options.channelSession?.channelSlug ?? null,
6908
7990
  capabilities: capabilitiesPayload()
6909
7991
  });
@@ -6921,7 +8003,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6921
8003
  const started = options.codexUrl === undefined ? await startOwnedCodexAppServer(options) : undefined;
6922
8004
  if (started !== undefined) {
6923
8005
  cleanup.actions.push(() => {
6924
- started.process.kill("SIGINT");
8006
+ started.stop();
6925
8007
  });
6926
8008
  }
6927
8009
  const codexUrl = options.codexUrl ?? started?.url;
@@ -6971,6 +8053,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6971
8053
  cleanup.actions.push(() => codex.close());
6972
8054
  const seq = { value: 0 };
6973
8055
  const codexThreads = options.channelSession === undefined ? await discoverCodexThreads(codex, options.cwd) : [];
8056
+ const discoveredCodexThreads = { value: codexThreads };
6974
8057
  const runnerHost = hostname2();
6975
8058
  const instancePayload = {
6976
8059
  instanceId,
@@ -7019,17 +8102,22 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7019
8102
  kandan.onReconnect(() => channelSession.handleKandanReconnect());
7020
8103
  }
7021
8104
  const dynamicChannelSessions = new Map;
8105
+ kandan.onReconnect(() => rebindDynamicChannelSessionsOnReconnect(dynamicChannelSessions.values()));
7022
8106
  cleanup.actions.push(async () => {
7023
8107
  await Promise.all(Array.from(dynamicChannelSessions.values(), (session) => session.close()));
7024
8108
  dynamicChannelSessions.clear();
7025
8109
  });
7026
- const attachStartedThreadSession = async (control, cwd, codexThreadId) => {
8110
+ const attachThreadSession = async (control, cwd, codexThreadId) => {
7027
8111
  const workspaceSlug = normalizedWorkDescription(control.workspace);
7028
8112
  const channelSlug = normalizedWorkDescription(control.channel);
7029
8113
  const kandanThreadId = normalizedWorkDescription(control.threadId);
7030
- if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined || dynamicChannelSessions.has(kandanThreadId)) {
8114
+ if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined) {
7031
8115
  return;
7032
8116
  }
8117
+ const existingSession = dynamicChannelSessions.get(kandanThreadId);
8118
+ if (existingSession !== undefined) {
8119
+ return existingSession;
8120
+ }
7033
8121
  const listenUser = options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
7034
8122
  if (listenUser === undefined) {
7035
8123
  throw new Error("missing listen user for Commander-started Codex session");
@@ -7083,10 +8171,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7083
8171
  codexUrl,
7084
8172
  cwd: options.cwd,
7085
8173
  hostname: runnerHost,
7086
- workspace: options.channelSession?.workspaceSlug ?? null,
8174
+ workspace: options.channelSession?.workspaceSlug ?? options.workspaceSlug ?? null,
7087
8175
  channel: options.channelSession?.channelSlug ?? null,
7088
8176
  threadId: channelSession?.currentKandanThreadId() ?? null,
7089
8177
  codexThreadId: channelSession?.currentCodexThreadId() ?? null,
8178
+ codexThreads: discoveredCodexThreads.value,
7090
8179
  model: options.channelSession?.model ?? null,
7091
8180
  reasoningEffort: options.channelSession?.reasoningEffort ?? null,
7092
8181
  fast: options.fast ?? false,
@@ -7097,11 +8186,19 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7097
8186
  message: error instanceof Error ? error.message : String(error)
7098
8187
  });
7099
8188
  });
8189
+ const refreshDiscoveredCodexThreads = () => discoverCodexThreads(codex, options.cwd).then((threads) => {
8190
+ discoveredCodexThreads.value = threads;
8191
+ return kandan.push(topic, "heartbeat", heartbeatPayload());
8192
+ }).catch((error) => {
8193
+ log("kandan.codex_threads_refresh_failed", {
8194
+ message: error instanceof Error ? error.message : String(error)
8195
+ });
8196
+ });
7100
8197
  const heartbeatInterval = setInterval(() => {
7101
- pushHeartbeat();
8198
+ channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat();
7102
8199
  }, 15000);
7103
8200
  cleanup.actions.push(() => clearInterval(heartbeatInterval));
7104
- kandan.onReconnect(() => pushHeartbeat().then(() => {
8201
+ kandan.onReconnect(() => (channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat()).then(() => {
7105
8202
  return;
7106
8203
  }));
7107
8204
  pushHeartbeat();
@@ -7135,6 +8232,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7135
8232
  });
7136
8233
  });
7137
8234
  }
8235
+ if (channelSession === undefined && notification.method === "thread/started") {
8236
+ refreshDiscoveredCodexThreads();
8237
+ }
7138
8238
  log("codex.notification", {
7139
8239
  method: notification.method,
7140
8240
  metadata
@@ -7246,11 +8346,35 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7246
8346
  pushHeartbeat();
7247
8347
  return;
7248
8348
  }
8349
+ if (isSetPortForwardEnabledControl(control)) {
8350
+ switch (control.enabled) {
8351
+ case true:
8352
+ liveForwardPorts.add(control.port);
8353
+ break;
8354
+ case false:
8355
+ liveForwardPorts.delete(control.port);
8356
+ managedForwardPorts.delete(control.port);
8357
+ clearForwardPortAttribution(control.port);
8358
+ kandan.push(topic, "forward_port_revoked", {
8359
+ instanceId,
8360
+ port: control.port,
8361
+ reason: "user_disabled",
8362
+ capabilities: revocationCapabilities(capabilitiesPayload(), control.port)
8363
+ }).catch((error) => {
8364
+ log("kandan.forward_port_revoked_push_failed", {
8365
+ message: error instanceof Error ? error.message : String(error)
8366
+ });
8367
+ });
8368
+ break;
8369
+ }
8370
+ pushHeartbeat();
8371
+ return;
8372
+ }
7249
8373
  resolveSessionControl(channelSession, dynamicChannelSessions, control).then((handled) => {
7250
8374
  if (handled !== undefined) {
7251
8375
  return handled;
7252
8376
  }
7253
- return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control, attachStartedThreadSession);
8377
+ return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control, attachThreadSession);
7254
8378
  }).then((response) => {
7255
8379
  return kandan.push(topic, "codex_response", response);
7256
8380
  }).catch((error) => {
@@ -7315,6 +8439,8 @@ async function discoverCodexThreads(codex, cwd) {
7315
8439
  const data = arrayValue(result?.data);
7316
8440
  return data === undefined ? [] : data.filter(isJsonObject).map((thread) => ({
7317
8441
  id: stringValue(thread.id) ?? "",
8442
+ title: stringValue(thread.title) ?? stringValue(thread.name) ?? stringValue(thread.preview) ?? "",
8443
+ description: stringValue(thread.description) ?? stringValue(thread.summary) ?? stringValue(thread.preview) ?? "",
7318
8444
  preview: stringValue(thread.preview) ?? "",
7319
8445
  cwd: stringValue(thread.cwd) ?? "",
7320
8446
  source: stringValue(thread.source) ?? "",
@@ -7390,6 +8516,9 @@ function launchCodexTui(codexBin, codexUrl, cwd, codexThreadId, session, fast) {
7390
8516
  });
7391
8517
  return child;
7392
8518
  }
8519
+ async function rebindDynamicChannelSessionsOnReconnect(sessions) {
8520
+ await Promise.all(Array.from(sessions, (session) => session.handleKandanReconnect()));
8521
+ }
7393
8522
  function forwardedHeaderValue(headers, name) {
7394
8523
  if (!Array.isArray(headers)) {
7395
8524
  return;
@@ -7470,10 +8599,11 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7470
8599
  const startedThreadSession = codexThreadId !== undefined && onStartedThread !== undefined ? await onStartedThread(control, cwd.cwd, codexThreadId) : undefined;
7471
8600
  if (codexThreadId !== undefined && workDescription !== undefined) {
7472
8601
  const rootSeq = integerValue(control.rootSeq);
7473
- if (startedThreadSession !== undefined && rootSeq !== undefined) {
8602
+ const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
8603
+ if (startedThreadSession !== undefined && sourceSeq !== undefined) {
7474
8604
  const identity = identityFromAccessToken(options.token);
7475
8605
  await startedThreadSession.startThreadMessageTurn({
7476
- seq: rootSeq,
8606
+ seq: sourceSeq,
7477
8607
  body: workDescription,
7478
8608
  actorSlug: identity.actorUsername,
7479
8609
  actorUserId: identity.actorUserId
@@ -7493,6 +8623,52 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7493
8623
  response
7494
8624
  };
7495
8625
  }
8626
+ case "reconnect_thread": {
8627
+ const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
8628
+ if (!cwd.ok) {
8629
+ return {
8630
+ instanceId,
8631
+ controlType: control.type,
8632
+ ok: false,
8633
+ error: cwd.reason
8634
+ };
8635
+ }
8636
+ if (options.codexUrl === undefined) {
8637
+ ensureCodexProjectTrusted(cwd.cwd);
8638
+ }
8639
+ const codexThreadId = normalizedWorkDescription(control.codexThreadId);
8640
+ if (codexThreadId === undefined) {
8641
+ return {
8642
+ instanceId,
8643
+ controlType: control.type,
8644
+ ok: false,
8645
+ error: "missing_codex_thread_id"
8646
+ };
8647
+ }
8648
+ const workDescription = normalizedWorkDescription(control.workDescription);
8649
+ const startedThreadSession = onStartedThread === undefined ? undefined : await onStartedThread(control, cwd.cwd, codexThreadId);
8650
+ if (workDescription !== undefined) {
8651
+ const rootSeq = integerValue(control.rootSeq);
8652
+ const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
8653
+ if (startedThreadSession === undefined || sourceSeq === undefined) {
8654
+ throw new Error("cannot reconnect a Kandan thread without a session");
8655
+ }
8656
+ const identity = identityFromAccessToken(options.token);
8657
+ await startedThreadSession.startThreadMessageTurn({
8658
+ seq: sourceSeq,
8659
+ body: workDescription,
8660
+ actorSlug: identity.actorUsername,
8661
+ actorUserId: identity.actorUserId
8662
+ });
8663
+ }
8664
+ return {
8665
+ instanceId,
8666
+ controlType: control.type,
8667
+ cwd: cwd.cwd,
8668
+ matchedRoot: cwd.matchedRoot,
8669
+ codexThreadId
8670
+ };
8671
+ }
7496
8672
  case "start_turn": {
7497
8673
  const response = await codex.request("turn/start", {
7498
8674
  threadId: control.threadId,
@@ -7543,6 +8719,8 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7543
8719
  case "interrupt_queued_messages":
7544
8720
  case "resolve_codex_approval_request":
7545
8721
  case "resolve_port_forward_request":
8722
+ case "update_session_settings":
8723
+ case "set_port_forward_enabled":
7546
8724
  case "forward_http_request":
7547
8725
  case "forward_websocket_open":
7548
8726
  case "forward_websocket_send":
@@ -7653,6 +8831,9 @@ async function startOwnedCodexAppServer(options) {
7653
8831
  function isUpdateRunnerConfigControl(control) {
7654
8832
  return control.type === "update_runner_config";
7655
8833
  }
8834
+ function isSetPortForwardEnabledControl(control) {
8835
+ return control.type === "set_port_forward_enabled" && Number.isInteger(control.port) && control.port > 0 && control.port <= 65535;
8836
+ }
7656
8837
  function normalizeAllowedCwds(values) {
7657
8838
  return Array.from(new Set(values.flatMap((value) => {
7658
8839
  const normalized = value.trim();
@@ -7803,7 +8984,13 @@ async function acquireAndCacheToken(args) {
7803
8984
  }
7804
8985
 
7805
8986
  // src/localConfig.ts
7806
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync4, realpathSync as realpathSync4, writeFileSync as writeFileSync5 } from "node:fs";
8987
+ import {
8988
+ existsSync as existsSync5,
8989
+ mkdirSync as mkdirSync6,
8990
+ readFileSync as readFileSync4,
8991
+ realpathSync as realpathSync4,
8992
+ writeFileSync as writeFileSync5
8993
+ } from "node:fs";
7807
8994
  import { homedir as homedir6 } from "node:os";
7808
8995
  import { dirname as dirname5, resolve as resolve5 } from "node:path";
7809
8996
  function localConfigPath(env = process.env) {
@@ -7823,14 +9010,22 @@ function readLocalConfig(path = localConfigPath()) {
7823
9010
  allowedCwds: uniqueStrings(parsed.allowedCwds)
7824
9011
  };
7825
9012
  }
7826
- function readConfiguredAllowedCwds(path = localConfigPath()) {
7827
- return readLocalConfig(path).allowedCwds.map((cwd) => {
9013
+ function readConfiguredAllowedCwdState(path = localConfigPath()) {
9014
+ const allowedCwds = [];
9015
+ const missingCwds = [];
9016
+ for (const cwd of readLocalConfig(path).allowedCwds) {
7828
9017
  try {
7829
- return realpathSync4(resolve5(expandUserPath(cwd)));
9018
+ const absolutePath = resolve5(expandUserPath(cwd));
9019
+ const realPath = realpathSync4(absolutePath);
9020
+ allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
7830
9021
  } catch (_error) {
7831
- throw new Error(`invalid Linzumi config allowed path: ${cwd} does not exist`);
9022
+ missingCwds.push(cwd);
7832
9023
  }
7833
- });
9024
+ }
9025
+ return {
9026
+ allowedCwds: uniqueStrings(allowedCwds),
9027
+ missingCwds: uniqueStrings(missingCwds)
9028
+ };
7834
9029
  }
7835
9030
  function addAllowedCwd(pathValue, path = localConfigPath()) {
7836
9031
  const normalizedPath = realpathSync4(resolve5(expandUserPath(pathValue)));
@@ -7859,7 +9054,9 @@ function isConfigPayload(value) {
7859
9054
  return typeof value === "object" && value !== null && value.version === 1 && Array.isArray(value.allowedCwds) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
7860
9055
  }
7861
9056
  function uniqueStrings(values) {
7862
- return [...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))];
9057
+ return [
9058
+ ...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))
9059
+ ];
7863
9060
  }
7864
9061
  function realpathOrResolved(pathValue) {
7865
9062
  try {
@@ -9375,7 +10572,9 @@ function readProcessIdentity(pid) {
9375
10572
  if (match === null) {
9376
10573
  return { command: output };
9377
10574
  }
9378
- return { startedAt: match[1], command: match[2] };
10575
+ const startedAt = match[1];
10576
+ const processCommand = match[2];
10577
+ return startedAt === undefined || processCommand === undefined ? { command: output } : { startedAt, command: processCommand };
9379
10578
  } catch (_error) {
9380
10579
  return;
9381
10580
  }
@@ -9488,7 +10687,7 @@ async function main(args) {
9488
10687
  process.stdout.write(connectGuideText());
9489
10688
  return;
9490
10689
  case "version":
9491
- process.stdout.write(`linzumi 0.0.38-beta
10690
+ process.stdout.write(`linzumi 0.0.39-beta
9492
10691
  `);
9493
10692
  return;
9494
10693
  case "auth":
@@ -9582,12 +10781,17 @@ function runHelloCommand(args) {
9582
10781
  process.stdout.write(helloHelpText());
9583
10782
  return;
9584
10783
  }
10784
+ const rootPath = stringValue3(values, "dir");
10785
+ const parentDir = stringValue3(values, "parent-dir");
10786
+ const name = stringValue3(values, "name");
10787
+ const port = tcpPortValue(values, "port");
10788
+ const host = stringValue3(values, "host");
9585
10789
  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"),
10790
+ ...rootPath === undefined ? {} : { rootPath },
10791
+ ...parentDir === undefined ? {} : { parentDir: resolveUserPath(parentDir) },
10792
+ ...name === undefined ? {} : { name },
10793
+ ...port === undefined ? {} : { port },
10794
+ ...host === undefined ? {} : { host },
9591
10795
  reset: values.get("reset") === true
9592
10796
  });
9593
10797
  if (values.get("json") === true) {
@@ -9868,12 +11072,13 @@ async function parseAgentRunnerArgs(args, deps = {
9868
11072
  }
9869
11073
  const tokenFilePath = stringValue3(values, "agent-token-file") ?? defaultAgentTokenFilePath();
9870
11074
  const tokenFile = readStoredAgentTokenFile(tokenFilePath, deps.readTextFile);
9871
- const channelSlug = requiredStoredAgentChannel(tokenFile.channelId);
9872
- const listenUser = stringValue3(values, "listen-user") ?? requiredStoredOwnerUsername(tokenFile.ownerUsername);
11075
+ const channelSlug = tokenFile.channelId;
11076
+ const listenUser = channelSlug === undefined ? undefined : stringValue3(values, "listen-user") ?? requiredStoredOwnerUsername(tokenFile.ownerUsername);
9873
11077
  const kandanUrl = stringValue3(values, "linzumi-url") ?? agentApiUrlToKandanUrl(tokenFile.apiUrl);
9874
11078
  const requestedCwdValue = cwdArg ?? stringValue3(values, "cwd");
9875
11079
  const requestedCwd = resolveUserPath(requestedCwdValue ?? process.cwd());
9876
- const allowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : requestedCwdValue === undefined ? readConfiguredAllowedCwds() : assertConfiguredAllowedCwds([requestedCwd]);
11080
+ const configuredAllowedCwdState = values.has("allowed-cwd") || requestedCwdValue !== undefined ? { allowedCwds: [], missingCwds: [] } : readConfiguredAllowedCwdState();
11081
+ const allowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : requestedCwdValue === undefined ? configuredAllowedCwdState.allowedCwds : assertConfiguredAllowedCwds([requestedCwd]);
9877
11082
  const cwd = allowedCwds[0] ?? requestedCwd;
9878
11083
  const codexBin = stringValue3(values, "codex-bin") ?? "codex";
9879
11084
  const customCodeServerBin = stringValue3(values, "code-server-bin");
@@ -9907,12 +11112,14 @@ async function parseAgentRunnerArgs(args, deps = {
9907
11112
  fast: values.get("fast") === true,
9908
11113
  logFile: stringValue3(values, "log-file"),
9909
11114
  allowedCwds,
11115
+ missingConfiguredAllowedCwds: configuredAllowedCwdState.missingCwds,
9910
11116
  allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
9911
11117
  codeServerBin: editorRuntime.codeServerBin,
9912
11118
  editorRuntime: editorRuntime.runtime,
9913
11119
  socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
9914
11120
  dependencyStatus,
9915
- channelSession: {
11121
+ workspaceSlug: tokenFile.workspaceId,
11122
+ channelSession: channelSlug === undefined || listenUser === undefined ? undefined : {
9916
11123
  workspaceSlug: tokenFile.workspaceId,
9917
11124
  channelSlug,
9918
11125
  kandanThreadId: stringValue3(values, "linzumi-thread-id"),
@@ -9940,12 +11147,6 @@ function rejectAgentRunnerTargetingFlags(values) {
9940
11147
  throw new Error(`linzumi commander uses the claimed human Commander token scope; remove ${unsupportedFlags.map((flag) => `--${flag}`).join(", ")}.`);
9941
11148
  }
9942
11149
  }
9943
- function requiredStoredAgentChannel(channelId) {
9944
- if (channelId !== undefined) {
9945
- return channelId;
9946
- }
9947
- throw new Error("agent token file is missing channelId; rerun linzumi claim before starting a Commander");
9948
- }
9949
11150
  function requiredStoredOwnerUsername(ownerUsername) {
9950
11151
  if (ownerUsername !== undefined) {
9951
11152
  return ownerUsername;
@@ -10002,7 +11203,7 @@ async function parseRunnerArgs(args, deps = {
10002
11203
  process.exit(0);
10003
11204
  }
10004
11205
  if (values.get("version") === true) {
10005
- process.stdout.write(`linzumi 0.0.38-beta
11206
+ process.stdout.write(`linzumi 0.0.39-beta
10006
11207
  `);
10007
11208
  process.exit(0);
10008
11209
  }
@@ -10010,7 +11211,10 @@ async function parseRunnerArgs(args, deps = {
10010
11211
  const kandanUrl = required(values, "linzumi-url");
10011
11212
  const cwd = stringValue3(values, "cwd") ?? process.cwd();
10012
11213
  const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
10013
- const configuredAllowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : readConfiguredAllowedCwds();
11214
+ const configuredAllowedCwdState = values.has("allowed-cwd") ? {
11215
+ allowedCwds: assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))),
11216
+ missingCwds: []
11217
+ } : readConfiguredAllowedCwdState();
10014
11218
  const codexBin = stringValue3(values, "codex-bin") ?? "codex";
10015
11219
  const customCodeServerBin = stringValue3(values, "code-server-bin");
10016
11220
  const explicitToken = stringValue3(values, "token");
@@ -10050,12 +11254,14 @@ async function parseRunnerArgs(args, deps = {
10050
11254
  launchTui: values.get("launch-tui") === true,
10051
11255
  fast: values.get("fast") === true,
10052
11256
  logFile: stringValue3(values, "log-file"),
10053
- allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...configuredAllowedCwds])),
11257
+ allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...configuredAllowedCwdState.allowedCwds])),
11258
+ missingConfiguredAllowedCwds: configuredAllowedCwdState.missingCwds,
10054
11259
  allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
10055
11260
  codeServerBin: editorRuntime.codeServerBin,
10056
11261
  editorRuntime: editorRuntime.runtime,
10057
11262
  socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
10058
11263
  dependencyStatus,
11264
+ workspaceSlug: channelSession?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
10059
11265
  channelSession
10060
11266
  };
10061
11267
  }