@linzumi/cli 0.0.37-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 +1413 -214
  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,8 +2547,25 @@ 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
- if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined) {
2568
+ if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined || state.kandanThreadId === undefined || state.codexThreadId === undefined) {
1687
2569
  return;
1688
2570
  }
1689
2571
  const { start: configuredStart, ...watchOptions } = args.options.portForwardWatcher ?? {};
@@ -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
  }
@@ -1715,13 +2599,6 @@ async function bindChannelSession(args, state, payloadContext) {
1715
2599
  }
1716
2600
  } else if (state.codexThreadId !== undefined) {
1717
2601
  await bindCurrentCodexThread(args, state);
1718
- switch (state.rootSeq) {
1719
- case undefined:
1720
- await postBoundThreadAvailability(args, state, payloadContext, codexVersion);
1721
- break;
1722
- default:
1723
- break;
1724
- }
1725
2602
  } else {
1726
2603
  const resolved = await pushOk(args.kandan, args.topic, "session:resolve_thread_session", {
1727
2604
  workspace: session.workspaceSlug,
@@ -1733,22 +2610,9 @@ async function bindChannelSession(args, state, payloadContext) {
1733
2610
  if (state.codexThreadId === undefined) {
1734
2611
  throw new Error("Kandan thread root metadata did not include a Codex thread id");
1735
2612
  }
1736
- await postBoundThreadAvailability(args, state, payloadContext, codexVersion);
2613
+ await bindCurrentCodexThread(args, state);
1737
2614
  }
1738
2615
  }
1739
- async function postBoundThreadAvailability(args, state, payloadContext, codexVersion) {
1740
- if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
1741
- throw new Error("cannot post local Codex availability before thread binding");
1742
- }
1743
- const session = args.options.channelSession;
1744
- await pushOk(args.kandan, args.topic, "session:post_thread_message", {
1745
- workspace: session.workspaceSlug,
1746
- channel: session.channelSlug,
1747
- thread_id: state.kandanThreadId,
1748
- body: availabilityMessage(args.options, codexVersion, state.codexThreadId),
1749
- payload: localRunnerPayload(args.options, args.instanceId, "availability", state.codexThreadId, payloadContext)
1750
- });
1751
- }
1752
2616
  async function handleChannelSessionControl(args, state, payloadContext, control) {
1753
2617
  if (control.type === "update_session_settings") {
1754
2618
  return updateSessionSettings(args, state, control);
@@ -1766,7 +2630,11 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
1766
2630
  return;
1767
2631
  }
1768
2632
  if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
1769
- 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
+ };
1770
2638
  }
1771
2639
  const interrupted = interruptPendingKandanMessages(state.queue, control.throughSeq);
1772
2640
  const activeQueuedSeq = interruptibleQueuedSeq(state.turn);
@@ -1819,7 +2687,11 @@ function updateSessionSettings(args, state, control) {
1819
2687
  return;
1820
2688
  }
1821
2689
  if (state.codexThreadId === undefined) {
1822
- 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
+ };
1823
2695
  }
1824
2696
  state.runtimeSettings = mergeRuntimeSettings(state.runtimeSettings, control);
1825
2697
  publishRuntimeSettings(args, state).catch((error) => {
@@ -1849,16 +2721,27 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
1849
2721
  return;
1850
2722
  }
1851
2723
  if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
1852
- 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
+ };
1853
2729
  }
1854
2730
  const approval = state.pendingApprovalRequests.get(approvalRequestKey(control.requestId, control.sourceSeq));
1855
2731
  if (approval === undefined) {
1856
- 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
+ };
1857
2737
  }
1858
2738
  state.pendingApprovalRequests.delete(approvalRequestKey(control.requestId, control.sourceSeq));
1859
2739
  const codexDecision = control.decision === "approve" ? "accept" : "decline";
1860
2740
  approval.resolve({ decision: codexDecision });
1861
- state.activeProcessingState = { seq: approval.sourceSeq, reason: "streaming response" };
2741
+ state.activeProcessingState = {
2742
+ seq: approval.sourceSeq,
2743
+ reason: "streaming response"
2744
+ };
1862
2745
  await publishMessageState(args, state.kandanThreadId, approval.sourceSeq, {
1863
2746
  status: "processing",
1864
2747
  reason: "streaming response"
@@ -1871,6 +2754,17 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
1871
2754
  });
1872
2755
  return { instanceId: args.instanceId, ok: true };
1873
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
+ }
1874
2768
  async function resolvePendingPortForwardRequest(args, state, payloadContext, control) {
1875
2769
  const request = state.pendingPortForwardRequests.get(control.requestId);
1876
2770
  if (request === undefined) {
@@ -1883,7 +2777,11 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1883
2777
  actor_user_id: control.actorUserId ?? null,
1884
2778
  reason: "sender_not_allowed"
1885
2779
  });
1886
- 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
+ };
1887
2785
  }
1888
2786
  state.pendingPortForwardRequests.delete(control.requestId);
1889
2787
  if (control.decision === "deny") {
@@ -1894,14 +2792,18 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1894
2792
  port: request.port,
1895
2793
  pid: request.pid
1896
2794
  });
2795
+ await drainQueuedPortForwardPrompt(args, state, payloadContext);
1897
2796
  return { instanceId: args.instanceId, ok: true };
1898
2797
  }
1899
2798
  state.approvedForwardPorts.add(request.port);
1900
2799
  state.approvedForwardTargets.set(request.port, approvedTargetFromRequest(request));
2800
+ const processIdentity = guessCanonicalProcessFromCommand(request.command);
1901
2801
  const capabilities = args.options.onForwardPortApproved?.(request.port, {
1902
2802
  kandanThreadId: state.kandanThreadId ?? null,
1903
2803
  codexThreadId: state.codexThreadId ?? null,
1904
- channelSlug: args.options.channelSession.channelSlug ?? null
2804
+ channelSlug: args.options.channelSession.channelSlug ?? null,
2805
+ processName: processIdentity?.appName ?? null,
2806
+ processIconKey: processIdentity?.iconKey ?? null
1905
2807
  });
1906
2808
  await publishForwardPortResolvedEvent(args, request, capabilities);
1907
2809
  await publishMessageStateForPortForwardResult(args, state, request, "processed");
@@ -1911,6 +2813,7 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1911
2813
  port: request.port,
1912
2814
  pid: request.pid
1913
2815
  });
2816
+ await drainQueuedPortForwardPrompt(args, state, payloadContext);
1914
2817
  return { instanceId: args.instanceId, ok: true, port: request.port };
1915
2818
  }
1916
2819
  function portForwardControlSenderAllowed(args, payloadContext, control) {
@@ -1947,37 +2850,39 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1947
2850
  state.approvedForwardTargets.set(review.target.port, review.target);
1948
2851
  return;
1949
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
+ }
1950
2862
  await revokeApprovedForwardPort(args, state, review.revoked, review.reason);
1951
2863
  break;
1952
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
+ }
1953
2874
  break;
1954
2875
  }
1955
2876
  const requestId = `port-forward-${randomUUID()}`;
1956
2877
  const label = portForwardPromptLabel(candidate);
1957
- const body = portForwardPromptBody(candidate, requestId);
1958
- const payload = {
1959
- ...localRunnerPayload(args.options, args.instanceId, "port_forward_request", state.codexThreadId, payloadContext),
1960
- reply_to_seq: state.rootSeq ?? null,
1961
- structured: {
1962
- kind: "local_runner_port_forward_request",
1963
- request_id: requestId,
2878
+ const sourceSeq = portForwardApprovalSourceSeq(state);
2879
+ if (sourceSeq === undefined) {
2880
+ args.log("port_forward.prompt_skipped", {
1964
2881
  port: candidate.port,
1965
2882
  pid: candidate.pid,
1966
- command: candidate.command,
1967
- ...candidate.cwd === undefined ? {} : { cwd: candidate.cwd },
1968
- command_label: label
1969
- }
1970
- };
1971
- const reply = await pushOk(args.kandan, args.topic, "session:post_thread_message", {
1972
- workspace: args.options.channelSession.workspaceSlug,
1973
- channel: args.options.channelSession.channelSlug,
1974
- thread_id: state.kandanThreadId,
1975
- body,
1976
- payload
1977
- });
1978
- const sourceSeq = integerValue(reply.seq);
1979
- if (sourceSeq === undefined) {
1980
- throw new Error("port forward prompt did not return a Kandan message seq");
2883
+ reason: "source_seq_missing"
2884
+ });
2885
+ return;
1981
2886
  }
1982
2887
  const request = pendingRequestFromCandidate({
1983
2888
  requestId,
@@ -1985,6 +2890,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1985
2890
  candidate
1986
2891
  });
1987
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;
1988
2896
  await publishForwardPortRequestedEvent(args, request);
1989
2897
  await publishMessageState(args, state.kandanThreadId, sourceSeq, {
1990
2898
  status: "processing",
@@ -1992,13 +2900,20 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
1992
2900
  approval: {
1993
2901
  requestId,
1994
2902
  kind: "local_runner_port_forward",
1995
- summary: `Open runner port ${candidate.port} from ${label}`,
2903
+ summary: `Make ${processName} on port ${candidate.port} accessible on Linzumi?`,
1996
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 },
1997
2912
  choices: [
1998
2913
  {
1999
2914
  decision: "approve",
2000
- label: "Open preview",
2001
- 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}.`
2002
2917
  },
2003
2918
  {
2004
2919
  decision: "deny",
@@ -2016,6 +2931,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
2016
2931
  pid: candidate.pid
2017
2932
  });
2018
2933
  }
2934
+ function portForwardApprovalSourceSeq(state) {
2935
+ return state.activeProcessingState?.seq ?? state.rootSeq;
2936
+ }
2019
2937
  async function revokeApprovedForwardPort(args, state, target, reason) {
2020
2938
  state.approvedForwardPorts.delete(target.port);
2021
2939
  state.approvedForwardTargets.delete(target.port);
@@ -2035,30 +2953,58 @@ async function revokeApprovedForwardPort(args, state, target, reason) {
2035
2953
  reason
2036
2954
  });
2037
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
+ }
2038
2977
  async function publishPortForwardReadyMessage(args, state, payloadContext, request) {
2039
2978
  if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
2040
2979
  return;
2041
2980
  }
2042
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.`;
2043
2986
  await pushOptional(args.kandan, args.topic, "session:post_thread_message", {
2044
2987
  workspace: args.options.channelSession.workspaceSlug,
2045
2988
  channel: args.options.channelSession.channelSlug,
2046
2989
  thread_id: state.kandanThreadId,
2047
- body: `Runner port ${request.port} is open in Kandan: [Open preview](${path})`,
2990
+ body: `${readySummary} [Open](${path})`,
2048
2991
  payload: {
2049
2992
  ...localRunnerPayload(args.options, args.instanceId, "port_forward_ready", state.codexThreadId, payloadContext),
2050
2993
  ...state.rootSeq === undefined ? {} : { reply_to_seq: state.rootSeq },
2051
2994
  structured: {
2052
2995
  kind: "local_runner_port_forward_ready",
2053
2996
  status: "ready",
2054
- summary: `Runner port ${request.port} is open in Kandan.`,
2997
+ summary: readySummary,
2055
2998
  next_action: "Open HTTP/HTTPS/WebSocket preview",
2056
2999
  source_path: path,
2057
- link_label: "Open preview",
3000
+ link_label: "Open",
2058
3001
  request_id: request.requestId,
2059
3002
  port: request.port,
2060
3003
  pid: request.pid,
2061
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 },
2062
3008
  ...request.cwd === undefined ? {} : { cwd: request.cwd },
2063
3009
  url: path
2064
3010
  }
@@ -2087,10 +3033,15 @@ async function publishForwardPortResolvedEvent(args, request, capabilities) {
2087
3033
  ...capabilities === undefined ? {} : { capabilities }
2088
3034
  }, args.log);
2089
3035
  }
2090
- async function publishMessageStateForPortForwardResult(args, state, request, status) {
3036
+ async function publishMessageStateForPortForwardResult(args, state, request, status, failedReason = "port_forward_denied") {
2091
3037
  if (state.kandanThreadId === undefined) {
2092
3038
  return;
2093
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
+ }
2094
3045
  switch (status) {
2095
3046
  case "processed":
2096
3047
  await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
@@ -2100,7 +3051,7 @@ async function publishMessageStateForPortForwardResult(args, state, request, sta
2100
3051
  case "failed":
2101
3052
  await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
2102
3053
  status: "failed",
2103
- reason: "port_forward_denied"
3054
+ reason: failedReason
2104
3055
  });
2105
3056
  break;
2106
3057
  }
@@ -2171,7 +3122,12 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
2171
3122
  actorUserId: event.actorUserId,
2172
3123
  actorSlug: event.actorSlug
2173
3124
  });
2174
- 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) {
2175
3131
  await publishKandanMessageState(args, event, { status: "processed" });
2176
3132
  } else {
2177
3133
  await publishKandanMessageState(args, event, {
@@ -2201,6 +3157,7 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
2201
3157
  state.kandanThreadId = event.threadId;
2202
3158
  }
2203
3159
  }
3160
+ startPortForwardWatchIfEnabled(args, state, payloadContext);
2204
3161
  if (event.threadId !== state.kandanThreadId) {
2205
3162
  args.log("kandan.message_ignored", {
2206
3163
  seq: event.seq,
@@ -2324,7 +3281,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2324
3281
  if (next === undefined) {
2325
3282
  return;
2326
3283
  }
2327
- state.turn = { status: "starting", queuedSeq: next.seq, interruptAfterStart: false };
3284
+ state.turn = {
3285
+ status: "starting",
3286
+ queuedSeq: next.seq,
3287
+ interruptAfterStart: false
3288
+ };
2328
3289
  state.activeProcessingState = { seq: next.seq, reason: "starting turn" };
2329
3290
  args.log("codex.turn_starting", {
2330
3291
  queued_seq: next.seq,
@@ -2347,10 +3308,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2347
3308
  }
2348
3309
  const started = await args.codex.request("turn/start", {
2349
3310
  threadId: codexThreadId,
2350
- input: await codexInputItemsForQueuedKandanMessage(args, next),
3311
+ input: await codexInputItemsForQueuedKandanMessage(args, next, state.pendingReconnectContextInjection),
2351
3312
  ...codexTurnRuntimeOverrides(runtimeOptionsForSettings(args.options, state.runtimeSettings))
2352
3313
  });
2353
3314
  const turnId = extractTurnIdFromResponse(started);
3315
+ state.pendingReconnectContextInjection = undefined;
2354
3316
  const interruptAfterStart = state.turn.status === "starting" && state.turn.interruptAfterStart;
2355
3317
  state.turn = { status: "active", turnId, queuedSeq: next.seq };
2356
3318
  rememberTurnReplyTarget(state, turnId, next.seq);
@@ -2376,12 +3338,14 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2376
3338
  await stopCodexTyping(args, state);
2377
3339
  const newCodexThreadId = await startCodexThread(args.codex, args.options);
2378
3340
  state.codexThreadId = newCodexThreadId;
3341
+ await bindCurrentCodexThread(args, state);
2379
3342
  args.log("codex.thread_rebound", {
2380
3343
  kandan_thread_id: state.kandanThreadId,
2381
3344
  old_codex_thread_id: oldCodexThreadId ?? null,
2382
3345
  new_codex_thread_id: newCodexThreadId
2383
3346
  });
2384
3347
  await postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId);
3348
+ state.pendingReconnectContextInjection = await fetchReconnectContextInjection(args, state);
2385
3349
  requeuePendingKandanMessageFront(state.queue, next);
2386
3350
  state.turn = { status: "idle" };
2387
3351
  await drainKandanMessageQueue(args, state, payloadContext);
@@ -2407,6 +3371,26 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2407
3371
  });
2408
3372
  }
2409
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
+ }
2410
3394
  async function handleCodexServerRequest(args, state, payloadContext, request) {
2411
3395
  const params = objectValue(request.params) ?? {};
2412
3396
  const turnId = codexNotificationTurnId(params);
@@ -2444,35 +3428,48 @@ function codexApprovalRequestCanSurface(method) {
2444
3428
  return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval";
2445
3429
  }
2446
3430
  async function requestKandanApproval(args, state, request, turnId, payloadContext) {
2447
- const sourceSeq = activeQueuedSeqForTurn(state.turn, turnId);
2448
- if (sourceSeq === undefined || state.kandanThreadId === undefined) {
2449
- const message = `Codex approval request has no active Kandan source message: ${request.method}`;
2450
- await failActiveCodexTurn(args, state, turnId, message, payloadContext);
2451
- throw new Error(message);
2452
- }
2453
- const approval = codexApprovalMessageState(request);
2454
- state.activeProcessingState = { seq: sourceSeq, reason: "awaiting approval", approval };
2455
- await publishMessageState(args, state.kandanThreadId, sourceSeq, {
2456
- status: "processing",
2457
- reason: "awaiting approval",
2458
- approval
2459
- }, undefined, undefined, state.codexThreadId);
2460
- args.log("codex.approval_request_pending", {
2461
- request_id: approval.requestId,
2462
- source_seq: sourceSeq,
2463
- turn_id: turnId,
2464
- method: request.method
2465
- });
2466
- return new Promise((resolve2, reject) => {
2467
- const request2 = {
2468
- requestId: approval.requestId,
2469
- sourceSeq,
2470
- turnId,
2471
- resolve: resolve2,
2472
- 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
2473
3443
  };
2474
- 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;
2475
3466
  });
3467
+ state.approvalPromptChain = approvalResult.then(() => {
3468
+ return;
3469
+ }, () => {
3470
+ return;
3471
+ });
3472
+ return approvalResult;
2476
3473
  }
2477
3474
  function rejectPendingApprovalRequests(state, error) {
2478
3475
  const pendingApprovals = [...state.pendingApprovalRequests.values()];
@@ -2498,7 +3495,11 @@ async function forwardCompletedCodexTurn(args, state, turnId, payloadContext) {
2498
3495
  const completingActiveTurn = completingQueuedSeq !== undefined;
2499
3496
  const completingLocalTuiTurn = isLocalTuiTurn(state, turnId);
2500
3497
  if (completingQueuedSeq !== undefined) {
2501
- state.turn = { status: "completing", turnId, queuedSeq: completingQueuedSeq };
3498
+ state.turn = {
3499
+ status: "completing",
3500
+ turnId,
3501
+ queuedSeq: completingQueuedSeq
3502
+ };
2502
3503
  }
2503
3504
  await waitForPendingTuiInputMirror(state, turnId);
2504
3505
  await waitForStreamingForwardChains(args, state, payloadContext);
@@ -2860,7 +3861,10 @@ async function forwardReasoningDeltaPayload(args, state, delta, payloadContext)
2860
3861
  }
2861
3862
  } else {
2862
3863
  await editCodexStructuredOutput(args, state, existing.seq, nextContent, codexReasoningStructuredMessage(delta.itemKey, nextContent, "streaming"));
2863
- rememberStreamingReasoningOutput(state, { ...existing, content: nextContent });
3864
+ rememberStreamingReasoningOutput(state, {
3865
+ ...existing,
3866
+ content: nextContent
3867
+ });
2864
3868
  }
2865
3869
  args.log("kandan.codex_reasoning_delta_forwarded", {
2866
3870
  item_key: delta.itemKey,
@@ -3417,7 +4421,10 @@ function rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId) {
3417
4421
  if (state.kandanThreadId !== undefined) {
3418
4422
  startCodexTypingHeartbeat(args, state, state.kandanThreadId);
3419
4423
  }
3420
- 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
+ });
3421
4428
  }
3422
4429
  function isLocalTuiTurn(state, turnId) {
3423
4430
  return state.localTuiTurnIds.has(turnId);
@@ -3484,7 +4491,10 @@ function clearPendingStreamFlushTimers(state) {
3484
4491
  clearStreamDeltaFlushTimer(state.fileChangeQueue);
3485
4492
  }
3486
4493
  function rememberTurnReplyTarget(state, turnId, replyToSeq) {
3487
- rememberBoundedCacheValue(state.turnReplyTargets, turnId, { turnId, replyToSeq });
4494
+ rememberBoundedCacheValue(state.turnReplyTargets, turnId, {
4495
+ turnId,
4496
+ replyToSeq
4497
+ });
3488
4498
  }
3489
4499
  function sourceMessageSeqForTurn(state, turnId) {
3490
4500
  return getBoundedCacheValue(state.turnReplyTargets, turnId)?.replyToSeq;
@@ -3493,20 +4503,12 @@ function fileChangePaths(structured) {
3493
4503
  const changes = arrayValue(structured.changes) ?? [];
3494
4504
  return changes.filter(isJsonObject).map((change) => stringValue(change.path) ?? "").filter((path) => path.trim() !== "");
3495
4505
  }
3496
- async function postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId) {
4506
+ async function postCodexThreadReboundMessage(args, state, payloadContext, _oldCodexThreadId, newCodexThreadId) {
3497
4507
  if (state.kandanThreadId === undefined) {
3498
4508
  return;
3499
4509
  }
3500
4510
  const session = args.options.channelSession;
3501
- const body = [
3502
- "Codex reconnected.",
3503
- "",
3504
- "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.",
3505
- "",
3506
- `Previous Codex thread: ${oldCodexThreadId ?? "unknown"}`,
3507
- `New Codex thread: ${newCodexThreadId}`
3508
- ].join(`
3509
- `);
4511
+ const body = "[codex reconnected to new thread]";
3510
4512
  await pushOk(args.kandan, args.topic, "session:post_thread_message", {
3511
4513
  workspace: session.workspaceSlug,
3512
4514
  channel: session.channelSlug,
@@ -3641,10 +4643,19 @@ async function publishMessageState(args, threadId, seq, state, actorSlug, actorU
3641
4643
  approval_request_id: state.approval.requestId,
3642
4644
  approval_kind: state.approval.kind,
3643
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 },
3644
4653
  ...state.approval.reason === undefined ? {} : { approval_reason: state.approval.reason },
3645
4654
  ...state.approval.choices === undefined ? {} : { approval_choices: state.approval.choices },
3646
4655
  ...state.approval.allowedActorSlug === undefined ? {} : { approval_allowed_actor_slug: state.approval.allowedActorSlug },
3647
- ...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
+ }
3648
4659
  } : {},
3649
4660
  ...actorSlug === undefined ? {} : { actor_slug: actorSlug },
3650
4661
  ...actorUserId === undefined ? {} : { actor_user_id: actorUserId }
@@ -3698,11 +4709,15 @@ function clearActiveProcessingState(state, seq) {
3698
4709
  state.activeProcessingState = undefined;
3699
4710
  }
3700
4711
  }
3701
- async function codexInputItemsForQueuedKandanMessage(args, message) {
4712
+ async function codexInputItemsForQueuedKandanMessage(args, message, reconnectContextInjection = undefined) {
3702
4713
  const attachments = await downloadQueuedKandanAttachments(args, message);
3703
4714
  const text = appendDownloadedAttachmentContext(codexInputForQueuedKandanMessage(message), attachments);
3704
4715
  const imageItems = attachments.flatMap((attachment) => attachment.isImage ? [{ type: "localImage", path: attachment.path }] : []);
3705
- return [{ type: "text", text }, ...imageItems];
4716
+ return [
4717
+ ...reconnectContextInjection === undefined ? [] : [reconnectContextInjection],
4718
+ { type: "text", text },
4719
+ ...imageItems
4720
+ ];
3706
4721
  }
3707
4722
  async function downloadQueuedKandanAttachments(args, message) {
3708
4723
  if (message.attachments.length === 0) {
@@ -3835,10 +4850,11 @@ async function uploadedFileIdsForCodexOutput(args, body, structured) {
3835
4850
  throw new Error("Kandan attachment prepare response missing upload_url");
3836
4851
  }
3837
4852
  const bytes = await readFile(file.path);
4853
+ const uploadBody = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
3838
4854
  const response = await fetch(resolveKandanAttachmentUrl(args.options.kandanUrl, uploadUrl), {
3839
4855
  method: uploadMethod,
3840
4856
  headers: { "content-type": file.contentType },
3841
- body: bytes
4857
+ body: uploadBody
3842
4858
  });
3843
4859
  if (!response.ok) {
3844
4860
  throw new Error(`Kandan attachment upload failed for ${file.fileName}: ${response.status} ${response.statusText}`);
@@ -4078,7 +5094,7 @@ function defaultCliAuditLogFile() {
4078
5094
  return override === undefined || override === "" ? join2(homedir(), ".linzumi", "logs", "command-events.jsonl") : override;
4079
5095
  }
4080
5096
  function redactForCliLog(value) {
4081
- return redactValue(value, undefined);
5097
+ return redactObject(value);
4082
5098
  }
4083
5099
  function redactValue(value, key) {
4084
5100
  if (sensitiveKey(key)) {
@@ -4223,8 +5239,10 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
4223
5239
  const child = spawn(codexBin, args, {
4224
5240
  cwd,
4225
5241
  env: process.env,
4226
- stdio: ["ignore", "inherit", "inherit"]
5242
+ stdio: ["ignore", "inherit", "inherit"],
5243
+ detached: true
4227
5244
  });
5245
+ const stop = () => stopCodexAppServerProcess(child);
4228
5246
  writeCliAuditEvent("process.spawned", {
4229
5247
  command: codexBin,
4230
5248
  args,
@@ -4250,33 +5268,57 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
4250
5268
  try {
4251
5269
  await waitForReadyz(url, child);
4252
5270
  } catch (error) {
4253
- child.kill("SIGINT");
5271
+ stop();
4254
5272
  throw error;
4255
5273
  }
4256
- 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;
4257
5310
  }
4258
5311
  function codexAppServerArgs(listenUrl, options = {}) {
4259
- return [
4260
- "app-server",
4261
- ...codexConfigArgs(options),
4262
- "--listen",
4263
- listenUrl
4264
- ];
5312
+ return ["app-server", ...codexConfigArgs(options), "--listen", listenUrl];
4265
5313
  }
4266
5314
  function codexConfigArgs(options) {
4267
5315
  return [
4268
- ...options.model === undefined ? [] : [
4269
- "-c",
4270
- `model=${JSON.stringify(options.model)}`
4271
- ],
5316
+ ...options.model === undefined ? [] : ["-c", `model=${JSON.stringify(options.model)}`],
4272
5317
  ...options.reasoningEffort === undefined ? [] : [
4273
5318
  "-c",
4274
5319
  `model_reasoning_effort=${JSON.stringify(options.reasoningEffort)}`
4275
5320
  ],
4276
- ...options.fast === true ? [
4277
- "-c",
4278
- `service_tier=${JSON.stringify("fast")}`
4279
- ] : []
5321
+ ...options.fast === true ? ["-c", `service_tier=${JSON.stringify("fast")}`] : []
4280
5322
  ];
4281
5323
  }
4282
5324
  async function connectCodexAppServer(websocketUrl, socketFactory = (url) => new WebSocket(url)) {
@@ -4630,7 +5672,9 @@ async function handleForwardHttpRequest(control, allowedPorts) {
4630
5672
  method: control.method,
4631
5673
  headers: requestHeaders(control.headers),
4632
5674
  redirect: "manual",
4633
- ...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
+ }
4634
5678
  };
4635
5679
  const response = await fetchWithHttpsFallback(control.port, control.path, control.queryString, request);
4636
5680
  const upstreamBuffer = Buffer.from(await response.arrayBuffer());
@@ -5100,10 +6144,12 @@ function prepareCodeServerProfile(collaboration, editorRuntime) {
5100
6144
  const userDataDir = mkdtempSync(join4(tmpdir(), "kandan-local-editor-"));
5101
6145
  const extensionsDir = join4(userDataDir, "extensions");
5102
6146
  const collaborationServerDir = join4(userDataDir, "collaboration-server");
6147
+ const tempDir = join4(userDataDir, "tmp");
5103
6148
  const userSettingsDir = join4(userDataDir, "User");
5104
6149
  mkdirSync3(userSettingsDir, { recursive: true });
5105
6150
  mkdirSync3(extensionsDir, { recursive: true });
5106
6151
  mkdirSync3(collaborationServerDir, { recursive: true });
6152
+ mkdirSync3(tempDir, { recursive: true });
5107
6153
  if (editorRuntime !== undefined) {
5108
6154
  installDirectory(editorRuntime.assets.documentStateExtensionDir, join4(extensionsDir, "kandan.document-state-telemetry"));
5109
6155
  }
@@ -5136,6 +6182,12 @@ function prepareCodeServerLaunch(options) {
5136
6182
  "-p",
5137
6183
  codeServerSandboxProfile(options, codeServerExecutable.directory),
5138
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"),
5139
6191
  codeServerExecutable.command,
5140
6192
  ...codeServerArgs(options.port, options.cwd, options.userDataDir, options.extensionsDir)
5141
6193
  ]
@@ -5416,10 +6468,14 @@ function installDirectory(sourceDir, destinationDir) {
5416
6468
  }
5417
6469
  function codeServerEnv(env, cwd, userDataDir, collaboration) {
5418
6470
  const { PORT: _port, ...hostEnv } = env;
6471
+ const tempDir = join4(userDataDir, "tmp");
5419
6472
  const base = {
5420
6473
  ...hostEnv,
5421
6474
  HOME: cwd,
5422
6475
  PWD: cwd,
6476
+ TMPDIR: tempDir,
6477
+ TMP: tempDir,
6478
+ TEMP: tempDir,
5423
6479
  XDG_CACHE_HOME: join4(userDataDir, "xdg-cache"),
5424
6480
  XDG_CONFIG_HOME: join4(userDataDir, "xdg-config"),
5425
6481
  XDG_DATA_HOME: join4(userDataDir, "xdg-data")
@@ -5808,7 +6864,7 @@ async function exchangeCodeForToken(args) {
5808
6864
  };
5809
6865
  }
5810
6866
  function stringBodyField(body, key) {
5811
- const value = key in body ? body[key] : undefined;
6867
+ const value = body[key];
5812
6868
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
5813
6869
  }
5814
6870
  function startCallbackServer(args) {
@@ -5878,7 +6934,9 @@ function isTcpAddress(address) {
5878
6934
  return typeof address === "object" && address !== null && typeof address.port === "number";
5879
6935
  }
5880
6936
  function writeOauthResult(response, args) {
5881
- 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
+ });
5882
6940
  response.end(oauthResultHtml(args));
5883
6941
  }
5884
6942
  function oauthResultHtml(args) {
@@ -6493,7 +7551,7 @@ function assertStartDependencies(status) {
6493
7551
  throw new Error(`Codex is not available at ${status.codex.command}. Install Codex or pass --codex-bin <path>.`);
6494
7552
  }
6495
7553
  if (status.editorRuntime?.status === "unavailable" || status.codeServer?.available === false && status.codeServer.reason !== "not_configured") {
6496
- 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.");
6497
7555
  }
6498
7556
  }
6499
7557
  function dependencyStatusPayload(status) {
@@ -6877,7 +7935,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6877
7935
  forwardPortAttributions.set(port, {
6878
7936
  kandanThreadId: attribution.kandanThreadId ?? null,
6879
7937
  codexThreadId: attribution.codexThreadId ?? null,
6880
- channelSlug: attribution.channelSlug ?? null
7938
+ channelSlug: attribution.channelSlug ?? null,
7939
+ processName: attribution.processName ?? null,
7940
+ processIconKey: attribution.processIconKey ?? null
6881
7941
  });
6882
7942
  };
6883
7943
  const clearForwardPortAttribution = (port) => {
@@ -6889,7 +7949,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6889
7949
  port,
6890
7950
  kandanThreadId: attribution?.kandanThreadId ?? null,
6891
7951
  codexThreadId: attribution?.codexThreadId ?? null,
6892
- channelSlug: attribution?.channelSlug ?? null
7952
+ channelSlug: attribution?.channelSlug ?? null,
7953
+ processName: attribution?.processName ?? null,
7954
+ processIconKey: attribution?.processIconKey ?? null
6893
7955
  };
6894
7956
  });
6895
7957
  const allowedCwds = { value: [...options.allowedCwds] };
@@ -6908,6 +7970,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6908
7970
  codexRemoteTui: true,
6909
7971
  startInstance: allowedCwds.value.length > 0,
6910
7972
  allowedCwds: allowedCwds.value,
7973
+ missingConfiguredAllowedCwds: options.missingConfiguredAllowedCwds ?? [],
6911
7974
  allowedCwdSuggestions: allowedCwdSuggestions(options.cwd, allowedCwds.value),
6912
7975
  portForwarding: liveForwardPorts.size > 0,
6913
7976
  allowedPorts: Array.from(liveForwardPorts).sort((left, right) => left - right),
@@ -6922,7 +7985,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6922
7985
  const joinPayload = () => ({
6923
7986
  clientName: "kandan-local-codex-runner",
6924
7987
  version: "0.0.1",
6925
- workspace: options.channelSession?.workspaceSlug ?? null,
7988
+ workspace: options.channelSession?.workspaceSlug ?? options.workspaceSlug ?? null,
6926
7989
  channel: options.channelSession?.channelSlug ?? null,
6927
7990
  capabilities: capabilitiesPayload()
6928
7991
  });
@@ -6940,7 +8003,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6940
8003
  const started = options.codexUrl === undefined ? await startOwnedCodexAppServer(options) : undefined;
6941
8004
  if (started !== undefined) {
6942
8005
  cleanup.actions.push(() => {
6943
- started.process.kill("SIGINT");
8006
+ started.stop();
6944
8007
  });
6945
8008
  }
6946
8009
  const codexUrl = options.codexUrl ?? started?.url;
@@ -6990,6 +8053,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6990
8053
  cleanup.actions.push(() => codex.close());
6991
8054
  const seq = { value: 0 };
6992
8055
  const codexThreads = options.channelSession === undefined ? await discoverCodexThreads(codex, options.cwd) : [];
8056
+ const discoveredCodexThreads = { value: codexThreads };
6993
8057
  const runnerHost = hostname2();
6994
8058
  const instancePayload = {
6995
8059
  instanceId,
@@ -7038,21 +8102,27 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7038
8102
  kandan.onReconnect(() => channelSession.handleKandanReconnect());
7039
8103
  }
7040
8104
  const dynamicChannelSessions = new Map;
8105
+ kandan.onReconnect(() => rebindDynamicChannelSessionsOnReconnect(dynamicChannelSessions.values()));
7041
8106
  cleanup.actions.push(async () => {
7042
8107
  await Promise.all(Array.from(dynamicChannelSessions.values(), (session) => session.close()));
7043
8108
  dynamicChannelSessions.clear();
7044
8109
  });
7045
- const attachStartedThreadSession = async (control, cwd, codexThreadId) => {
8110
+ const attachThreadSession = async (control, cwd, codexThreadId) => {
7046
8111
  const workspaceSlug = normalizedWorkDescription(control.workspace);
7047
8112
  const channelSlug = normalizedWorkDescription(control.channel);
7048
8113
  const kandanThreadId = normalizedWorkDescription(control.threadId);
7049
- if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined || dynamicChannelSessions.has(kandanThreadId)) {
8114
+ if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined) {
7050
8115
  return;
7051
8116
  }
8117
+ const existingSession = dynamicChannelSessions.get(kandanThreadId);
8118
+ if (existingSession !== undefined) {
8119
+ return existingSession;
8120
+ }
7052
8121
  const listenUser = options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
7053
8122
  if (listenUser === undefined) {
7054
8123
  throw new Error("missing listen user for Commander-started Codex session");
7055
8124
  }
8125
+ const runtimeSettings = startInstanceRuntimeSettings(options, control);
7056
8126
  const session = await attachChannelSession({
7057
8127
  kandan,
7058
8128
  codex,
@@ -7085,10 +8155,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7085
8155
  rootSeq: integerValue(control.rootSeq),
7086
8156
  codexThreadId,
7087
8157
  listenUser,
7088
- model: control.model,
7089
- reasoningEffort: control.reasoningEffort,
7090
- sandbox: control.sandbox,
7091
- approvalPolicy: control.approvalPolicy
8158
+ model: runtimeSettings.model,
8159
+ reasoningEffort: runtimeSettings.reasoningEffort,
8160
+ sandbox: runtimeSettings.sandbox,
8161
+ approvalPolicy: runtimeSettings.approvalPolicy
7092
8162
  }
7093
8163
  },
7094
8164
  log
@@ -7101,10 +8171,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7101
8171
  codexUrl,
7102
8172
  cwd: options.cwd,
7103
8173
  hostname: runnerHost,
7104
- workspace: options.channelSession?.workspaceSlug ?? null,
8174
+ workspace: options.channelSession?.workspaceSlug ?? options.workspaceSlug ?? null,
7105
8175
  channel: options.channelSession?.channelSlug ?? null,
7106
8176
  threadId: channelSession?.currentKandanThreadId() ?? null,
7107
8177
  codexThreadId: channelSession?.currentCodexThreadId() ?? null,
8178
+ codexThreads: discoveredCodexThreads.value,
7108
8179
  model: options.channelSession?.model ?? null,
7109
8180
  reasoningEffort: options.channelSession?.reasoningEffort ?? null,
7110
8181
  fast: options.fast ?? false,
@@ -7115,11 +8186,19 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7115
8186
  message: error instanceof Error ? error.message : String(error)
7116
8187
  });
7117
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
+ });
7118
8197
  const heartbeatInterval = setInterval(() => {
7119
- pushHeartbeat();
8198
+ channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat();
7120
8199
  }, 15000);
7121
8200
  cleanup.actions.push(() => clearInterval(heartbeatInterval));
7122
- kandan.onReconnect(() => pushHeartbeat().then(() => {
8201
+ kandan.onReconnect(() => (channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat()).then(() => {
7123
8202
  return;
7124
8203
  }));
7125
8204
  pushHeartbeat();
@@ -7153,6 +8232,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7153
8232
  });
7154
8233
  });
7155
8234
  }
8235
+ if (channelSession === undefined && notification.method === "thread/started") {
8236
+ refreshDiscoveredCodexThreads();
8237
+ }
7156
8238
  log("codex.notification", {
7157
8239
  method: notification.method,
7158
8240
  metadata
@@ -7264,11 +8346,35 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7264
8346
  pushHeartbeat();
7265
8347
  return;
7266
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
+ }
7267
8373
  resolveSessionControl(channelSession, dynamicChannelSessions, control).then((handled) => {
7268
8374
  if (handled !== undefined) {
7269
8375
  return handled;
7270
8376
  }
7271
- return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control, attachStartedThreadSession);
8377
+ return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control, attachThreadSession);
7272
8378
  }).then((response) => {
7273
8379
  return kandan.push(topic, "codex_response", response);
7274
8380
  }).catch((error) => {
@@ -7333,6 +8439,8 @@ async function discoverCodexThreads(codex, cwd) {
7333
8439
  const data = arrayValue(result?.data);
7334
8440
  return data === undefined ? [] : data.filter(isJsonObject).map((thread) => ({
7335
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) ?? "",
7336
8444
  preview: stringValue(thread.preview) ?? "",
7337
8445
  cwd: stringValue(thread.cwd) ?? "",
7338
8446
  source: stringValue(thread.source) ?? "",
@@ -7408,6 +8516,9 @@ function launchCodexTui(codexBin, codexUrl, cwd, codexThreadId, session, fast) {
7408
8516
  });
7409
8517
  return child;
7410
8518
  }
8519
+ async function rebindDynamicChannelSessionsOnReconnect(sessions) {
8520
+ await Promise.all(Array.from(sessions, (session) => session.handleKandanReconnect()));
8521
+ }
7411
8522
  function forwardedHeaderValue(headers, name) {
7412
8523
  if (!Array.isArray(headers)) {
7413
8524
  return;
@@ -7465,6 +8576,7 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7465
8576
  ensureCodexProjectTrusted(cwd.cwd);
7466
8577
  }
7467
8578
  const developerPrompt = normalizedWorkDescription(control.developerPrompt);
8579
+ const runtimeSettings = startInstanceRuntimeSettings(options, control);
7468
8580
  const response = await codex.request("thread/start", {
7469
8581
  cwd: cwd.cwd,
7470
8582
  serviceName: "kandan-local-runner",
@@ -7473,11 +8585,11 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7473
8585
  cwd: cwd.cwd,
7474
8586
  developerPrompt
7475
8587
  }),
7476
- ...control.model === undefined ? {} : { model: control.model },
7477
- ...control.reasoningEffort === undefined ? {} : { reasoningEffort: control.reasoningEffort },
7478
- ...control.approvalPolicy === undefined ? {} : { approvalPolicy: control.approvalPolicy },
7479
- ...control.sandbox === undefined ? {} : { sandbox: control.sandbox },
7480
- ...control.fast === true ? { serviceTier: "fast" } : {}
8588
+ ...runtimeSettings.model === undefined ? {} : { model: runtimeSettings.model },
8589
+ ...runtimeSettings.reasoningEffort === undefined ? {} : { reasoningEffort: runtimeSettings.reasoningEffort },
8590
+ ...runtimeSettings.approvalPolicy === undefined ? {} : { approvalPolicy: runtimeSettings.approvalPolicy },
8591
+ ...runtimeSettings.sandbox === undefined ? {} : { sandbox: runtimeSettings.sandbox },
8592
+ ...runtimeSettings.fast === true ? { serviceTier: "fast" } : {}
7481
8593
  });
7482
8594
  const codexThreadId = extractStartedThreadId(response);
7483
8595
  const workDescription = normalizedWorkDescription(control.workDescription);
@@ -7487,10 +8599,11 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7487
8599
  const startedThreadSession = codexThreadId !== undefined && onStartedThread !== undefined ? await onStartedThread(control, cwd.cwd, codexThreadId) : undefined;
7488
8600
  if (codexThreadId !== undefined && workDescription !== undefined) {
7489
8601
  const rootSeq = integerValue(control.rootSeq);
7490
- if (startedThreadSession !== undefined && rootSeq !== undefined) {
8602
+ const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
8603
+ if (startedThreadSession !== undefined && sourceSeq !== undefined) {
7491
8604
  const identity = identityFromAccessToken(options.token);
7492
8605
  await startedThreadSession.startThreadMessageTurn({
7493
- seq: rootSeq,
8606
+ seq: sourceSeq,
7494
8607
  body: workDescription,
7495
8608
  actorSlug: identity.actorUsername,
7496
8609
  actorUserId: identity.actorUserId
@@ -7510,6 +8623,52 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7510
8623
  response
7511
8624
  };
7512
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
+ }
7513
8672
  case "start_turn": {
7514
8673
  const response = await codex.request("turn/start", {
7515
8674
  threadId: control.threadId,
@@ -7560,6 +8719,8 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7560
8719
  case "interrupt_queued_messages":
7561
8720
  case "resolve_codex_approval_request":
7562
8721
  case "resolve_port_forward_request":
8722
+ case "update_session_settings":
8723
+ case "set_port_forward_enabled":
7563
8724
  case "forward_http_request":
7564
8725
  case "forward_websocket_open":
7565
8726
  case "forward_websocket_send":
@@ -7649,6 +8810,16 @@ ${developerPrompt}`,
7649
8810
  client_message_id: `codex-start-instructions-${threadId}`
7650
8811
  });
7651
8812
  }
8813
+ function startInstanceRuntimeSettings(options, control) {
8814
+ const session = options.channelSession;
8815
+ return {
8816
+ model: control.model ?? session?.model,
8817
+ reasoningEffort: control.reasoningEffort ?? session?.reasoningEffort,
8818
+ approvalPolicy: control.approvalPolicy ?? session?.approvalPolicy,
8819
+ sandbox: control.sandbox ?? session?.sandbox,
8820
+ fast: control.fast ?? options.fast
8821
+ };
8822
+ }
7652
8823
  async function startOwnedCodexAppServer(options) {
7653
8824
  ensureCodexProjectTrusted(options.cwd);
7654
8825
  return await startCodexAppServer(options.codexBin, options.cwd, {
@@ -7660,6 +8831,9 @@ async function startOwnedCodexAppServer(options) {
7660
8831
  function isUpdateRunnerConfigControl(control) {
7661
8832
  return control.type === "update_runner_config";
7662
8833
  }
8834
+ function isSetPortForwardEnabledControl(control) {
8835
+ return control.type === "set_port_forward_enabled" && Number.isInteger(control.port) && control.port > 0 && control.port <= 65535;
8836
+ }
7663
8837
  function normalizeAllowedCwds(values) {
7664
8838
  return Array.from(new Set(values.flatMap((value) => {
7665
8839
  const normalized = value.trim();
@@ -7810,7 +8984,13 @@ async function acquireAndCacheToken(args) {
7810
8984
  }
7811
8985
 
7812
8986
  // src/localConfig.ts
7813
- 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";
7814
8994
  import { homedir as homedir6 } from "node:os";
7815
8995
  import { dirname as dirname5, resolve as resolve5 } from "node:path";
7816
8996
  function localConfigPath(env = process.env) {
@@ -7830,14 +9010,22 @@ function readLocalConfig(path = localConfigPath()) {
7830
9010
  allowedCwds: uniqueStrings(parsed.allowedCwds)
7831
9011
  };
7832
9012
  }
7833
- function readConfiguredAllowedCwds(path = localConfigPath()) {
7834
- 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) {
7835
9017
  try {
7836
- return realpathSync4(resolve5(expandUserPath(cwd)));
9018
+ const absolutePath = resolve5(expandUserPath(cwd));
9019
+ const realPath = realpathSync4(absolutePath);
9020
+ allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
7837
9021
  } catch (_error) {
7838
- throw new Error(`invalid Linzumi config allowed path: ${cwd} does not exist`);
9022
+ missingCwds.push(cwd);
7839
9023
  }
7840
- });
9024
+ }
9025
+ return {
9026
+ allowedCwds: uniqueStrings(allowedCwds),
9027
+ missingCwds: uniqueStrings(missingCwds)
9028
+ };
7841
9029
  }
7842
9030
  function addAllowedCwd(pathValue, path = localConfigPath()) {
7843
9031
  const normalizedPath = realpathSync4(resolve5(expandUserPath(pathValue)));
@@ -7866,7 +9054,9 @@ function isConfigPayload(value) {
7866
9054
  return typeof value === "object" && value !== null && value.version === 1 && Array.isArray(value.allowedCwds) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
7867
9055
  }
7868
9056
  function uniqueStrings(values) {
7869
- 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
+ ];
7870
9060
  }
7871
9061
  function realpathOrResolved(pathValue) {
7872
9062
  try {
@@ -9382,7 +10572,9 @@ function readProcessIdentity(pid) {
9382
10572
  if (match === null) {
9383
10573
  return { command: output };
9384
10574
  }
9385
- 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 };
9386
10578
  } catch (_error) {
9387
10579
  return;
9388
10580
  }
@@ -9495,7 +10687,7 @@ async function main(args) {
9495
10687
  process.stdout.write(connectGuideText());
9496
10688
  return;
9497
10689
  case "version":
9498
- process.stdout.write(`linzumi 0.0.37-beta
10690
+ process.stdout.write(`linzumi 0.0.39-beta
9499
10691
  `);
9500
10692
  return;
9501
10693
  case "auth":
@@ -9589,12 +10781,17 @@ function runHelloCommand(args) {
9589
10781
  process.stdout.write(helloHelpText());
9590
10782
  return;
9591
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");
9592
10789
  const project = createHelloLinzumiProject({
9593
- rootPath: stringValue3(values, "dir"),
9594
- parentDir: stringValue3(values, "parent-dir") === undefined ? undefined : resolveUserPath(required(values, "parent-dir")),
9595
- name: stringValue3(values, "name"),
9596
- port: tcpPortValue(values, "port"),
9597
- 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 },
9598
10795
  reset: values.get("reset") === true
9599
10796
  });
9600
10797
  if (values.get("json") === true) {
@@ -9875,12 +11072,13 @@ async function parseAgentRunnerArgs(args, deps = {
9875
11072
  }
9876
11073
  const tokenFilePath = stringValue3(values, "agent-token-file") ?? defaultAgentTokenFilePath();
9877
11074
  const tokenFile = readStoredAgentTokenFile(tokenFilePath, deps.readTextFile);
9878
- const channelSlug = requiredStoredAgentChannel(tokenFile.channelId);
9879
- 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);
9880
11077
  const kandanUrl = stringValue3(values, "linzumi-url") ?? agentApiUrlToKandanUrl(tokenFile.apiUrl);
9881
11078
  const requestedCwdValue = cwdArg ?? stringValue3(values, "cwd");
9882
11079
  const requestedCwd = resolveUserPath(requestedCwdValue ?? process.cwd());
9883
- 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]);
9884
11082
  const cwd = allowedCwds[0] ?? requestedCwd;
9885
11083
  const codexBin = stringValue3(values, "codex-bin") ?? "codex";
9886
11084
  const customCodeServerBin = stringValue3(values, "code-server-bin");
@@ -9914,12 +11112,14 @@ async function parseAgentRunnerArgs(args, deps = {
9914
11112
  fast: values.get("fast") === true,
9915
11113
  logFile: stringValue3(values, "log-file"),
9916
11114
  allowedCwds,
11115
+ missingConfiguredAllowedCwds: configuredAllowedCwdState.missingCwds,
9917
11116
  allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
9918
11117
  codeServerBin: editorRuntime.codeServerBin,
9919
11118
  editorRuntime: editorRuntime.runtime,
9920
11119
  socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
9921
11120
  dependencyStatus,
9922
- channelSession: {
11121
+ workspaceSlug: tokenFile.workspaceId,
11122
+ channelSession: channelSlug === undefined || listenUser === undefined ? undefined : {
9923
11123
  workspaceSlug: tokenFile.workspaceId,
9924
11124
  channelSlug,
9925
11125
  kandanThreadId: stringValue3(values, "linzumi-thread-id"),
@@ -9947,12 +11147,6 @@ function rejectAgentRunnerTargetingFlags(values) {
9947
11147
  throw new Error(`linzumi commander uses the claimed human Commander token scope; remove ${unsupportedFlags.map((flag) => `--${flag}`).join(", ")}.`);
9948
11148
  }
9949
11149
  }
9950
- function requiredStoredAgentChannel(channelId) {
9951
- if (channelId !== undefined) {
9952
- return channelId;
9953
- }
9954
- throw new Error("agent token file is missing channelId; rerun linzumi claim before starting a Commander");
9955
- }
9956
11150
  function requiredStoredOwnerUsername(ownerUsername) {
9957
11151
  if (ownerUsername !== undefined) {
9958
11152
  return ownerUsername;
@@ -10009,7 +11203,7 @@ async function parseRunnerArgs(args, deps = {
10009
11203
  process.exit(0);
10010
11204
  }
10011
11205
  if (values.get("version") === true) {
10012
- process.stdout.write(`linzumi 0.0.37-beta
11206
+ process.stdout.write(`linzumi 0.0.39-beta
10013
11207
  `);
10014
11208
  process.exit(0);
10015
11209
  }
@@ -10017,7 +11211,10 @@ async function parseRunnerArgs(args, deps = {
10017
11211
  const kandanUrl = required(values, "linzumi-url");
10018
11212
  const cwd = stringValue3(values, "cwd") ?? process.cwd();
10019
11213
  const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
10020
- 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();
10021
11218
  const codexBin = stringValue3(values, "codex-bin") ?? "codex";
10022
11219
  const customCodeServerBin = stringValue3(values, "code-server-bin");
10023
11220
  const explicitToken = stringValue3(values, "token");
@@ -10057,12 +11254,14 @@ async function parseRunnerArgs(args, deps = {
10057
11254
  launchTui: values.get("launch-tui") === true,
10058
11255
  fast: values.get("fast") === true,
10059
11256
  logFile: stringValue3(values, "log-file"),
10060
- allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...configuredAllowedCwds])),
11257
+ allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...configuredAllowedCwdState.allowedCwds])),
11258
+ missingConfiguredAllowedCwds: configuredAllowedCwdState.missingCwds,
10061
11259
  allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
10062
11260
  codeServerBin: editorRuntime.codeServerBin,
10063
11261
  editorRuntime: editorRuntime.runtime,
10064
11262
  socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
10065
11263
  dependencyStatus,
11264
+ workspaceSlug: channelSession?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
10066
11265
  channelSession
10067
11266
  };
10068
11267
  }