@linzumi/cli 0.0.58-beta → 0.0.60-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 +660 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -62,7 +62,7 @@ Install the CLI or run it with `npx`:
62
62
  ```bash
63
63
  npm install -g @linzumi/cli@latest
64
64
  npx -y @linzumi/cli@latest signup
65
- npx -y @linzumi/cli@0.0.58-beta --version
65
+ npx -y @linzumi/cli@0.0.60-beta --version
66
66
  linzumi --version
67
67
  ```
68
68
 
package/dist/index.js CHANGED
@@ -20847,7 +20847,7 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
20847
20847
  request.port,
20848
20848
  approvedTargetFromRequest(request)
20849
20849
  );
20850
- await publishForwardPortResolvedEvent(args, request, {
20850
+ await publishForwardPortResolvedEvent(args, state, request, {
20851
20851
  decision: "deny"
20852
20852
  });
20853
20853
  await publishMessageStateForPortForwardResult(
@@ -20874,7 +20874,7 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
20874
20874
  async function expirePendingPortForwardRequest(args, state, request, reason) {
20875
20875
  const failedReason = reason === "listener_exited" ? "port_forward_listener_exited" : "port_forward_listener_changed";
20876
20876
  state.pendingPortForwardRequests.delete(request.requestId);
20877
- await publishForwardPortResolvedEvent(args, request, {
20877
+ await publishForwardPortResolvedEvent(args, state, request, {
20878
20878
  decision: "expired",
20879
20879
  reason
20880
20880
  });
@@ -20906,7 +20906,7 @@ async function approvePortForwardRequest(args, state, request, options) {
20906
20906
  processName: processIdentity?.appName ?? null,
20907
20907
  processIconKey: processIdentity?.iconKey ?? null
20908
20908
  });
20909
- await publishForwardPortResolvedEvent(args, request, {
20909
+ await publishForwardPortResolvedEvent(args, state, request, {
20910
20910
  decision: "approve",
20911
20911
  capabilities
20912
20912
  });
@@ -21224,7 +21224,7 @@ async function publishForwardPortRequestedEvent(args, state, request, processIde
21224
21224
  args.log
21225
21225
  );
21226
21226
  }
21227
- async function publishForwardPortResolvedEvent(args, request, result) {
21227
+ async function publishForwardPortResolvedEvent(args, state, request, result) {
21228
21228
  await pushOptional(
21229
21229
  args.kandan,
21230
21230
  args.topic,
@@ -21235,6 +21235,9 @@ async function publishForwardPortResolvedEvent(args, request, result) {
21235
21235
  port: request.port,
21236
21236
  pid: request.pid,
21237
21237
  command: request.command,
21238
+ codexThreadId: state.codexThreadId ?? null,
21239
+ kandanThreadId: state.kandanThreadId ?? null,
21240
+ channelSlug: args.options.channelSession.channelSlug ?? null,
21238
21241
  ...request.cwd === void 0 ? {} : { cwd: request.cwd },
21239
21242
  sourceSeq: request.sourceSeq,
21240
21243
  decision: result.decision,
@@ -24718,6 +24721,7 @@ function linzumiMcpServerConfig(options) {
24718
24721
  name: "linzumi",
24719
24722
  command: options.command ?? "linzumi",
24720
24723
  args: [
24724
+ ...options.argsPrefix ?? [],
24721
24725
  "mcp",
24722
24726
  "server",
24723
24727
  "--api-url",
@@ -24738,6 +24742,23 @@ function codexMcpConfigArgs(config2) {
24738
24742
  `mcp_servers.${config2.name}.args=${tomlStringArray(config2.args)}`
24739
24743
  ];
24740
24744
  }
24745
+ function linzumiMcpCommandForProcess(processExecPath, scriptPath) {
24746
+ const trimmedScriptPath = scriptPath?.trim() ?? "";
24747
+ switch (trimmedScriptPath === "") {
24748
+ case true:
24749
+ return { command: "linzumi", argsPrefix: [] };
24750
+ case false:
24751
+ switch (trimmedScriptPath.endsWith(".ts") || trimmedScriptPath.endsWith(".js")) {
24752
+ case true:
24753
+ return {
24754
+ command: processExecPath,
24755
+ argsPrefix: [trimmedScriptPath]
24756
+ };
24757
+ case false:
24758
+ return { command: trimmedScriptPath, argsPrefix: [] };
24759
+ }
24760
+ }
24761
+ }
24741
24762
  function claudeCodeMcpConfigJson(config2) {
24742
24763
  return `${JSON.stringify(
24743
24764
  {
@@ -28273,7 +28294,7 @@ function realpathOrResolved(pathValue) {
28273
28294
  }
28274
28295
 
28275
28296
  // src/version.ts
28276
- var linzumiCliVersion = "0.0.58-beta";
28297
+ var linzumiCliVersion = "0.0.60-beta";
28277
28298
  var linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
28278
28299
 
28279
28300
  // src/runnerLock.ts
@@ -28759,6 +28780,7 @@ function normalizeLocalRunnerCache(value) {
28759
28780
  }
28760
28781
 
28761
28782
  // src/runner.ts
28783
+ var THREAD_RUNNER_READY_TIMEOUT_MS = 3e4;
28762
28784
  async function runLocalCodexRunner(options) {
28763
28785
  const log = makeRunnerLogger(options);
28764
28786
  const cleanup = {
@@ -28796,6 +28818,29 @@ async function runLocalCodexRunner(options) {
28796
28818
  throw error51;
28797
28819
  }
28798
28820
  }
28821
+ async function runThreadCodexWorker(options) {
28822
+ if (options.threadProcess?.role !== "thread") {
28823
+ throw new Error("thread Codex worker requires thread process options");
28824
+ }
28825
+ if (process.send === void 0) {
28826
+ throw new Error("thread Codex worker requires an IPC channel");
28827
+ }
28828
+ const log = makeRunnerLogger(options);
28829
+ const started = await startOwnedCodexAppServer(options, {
28830
+ linzumiMcp: false
28831
+ });
28832
+ const stop = once(() => {
28833
+ started.stop();
28834
+ log.close();
28835
+ });
28836
+ process.send({
28837
+ type: "linzumi_thread_codex_worker_ready",
28838
+ kandanThreadId: options.threadProcess.kandanThreadId,
28839
+ codexUrl: started.url
28840
+ });
28841
+ await waitForThreadCodexWorkerStop(started.process);
28842
+ stop();
28843
+ }
28799
28844
  async function openLocalCodexRunner(options, log, cleanup, close) {
28800
28845
  const agentProviders = availableRunnerAgentProviders(options);
28801
28846
  const allowedForwardPorts = options.allowedForwardPorts ?? [];
@@ -28908,7 +28953,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
28908
28953
  );
28909
28954
  cleanup.actions.push(() => kandan.close());
28910
28955
  const topic = `local_runner:${options.runnerId}`;
28911
- const clientId = options.machineId ?? options.runnerId;
28956
+ const clientId = options.clientId ?? options.machineId ?? options.runnerId;
28912
28957
  const runnerHost = hostname2();
28913
28958
  const joinPayload = () => ({
28914
28959
  clientName: "kandan-local-codex-runner",
@@ -29690,6 +29735,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29690
29735
  launchTui: options.launchTui,
29691
29736
  enablePortForwardWatch: true,
29692
29737
  initialForwardPorts: allowedForwardPorts,
29738
+ portForwardWatcher: channelSessionPortForwardWatcherOptions({
29739
+ rootPid: started?.process.pid,
29740
+ start: options.portForwardWatcher
29741
+ }),
29693
29742
  suppressedForwardPorts,
29694
29743
  onForwardPortApproved: (port, attribution) => {
29695
29744
  liveForwardPorts.add(port);
@@ -29742,7 +29791,16 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29742
29791
  )
29743
29792
  );
29744
29793
  });
29745
- const attachThreadSession = async (control, cwd, codexThreadId) => {
29794
+ const threadRunnerProcesses = /* @__PURE__ */ new Map();
29795
+ cleanup.actions.push(async () => {
29796
+ await Promise.all(
29797
+ Array.from(threadRunnerProcesses.values(), closeThreadRunnerEntry)
29798
+ );
29799
+ threadRunnerProcesses.clear();
29800
+ });
29801
+ const attachThreadSessionWithCodex = async (args) => {
29802
+ const { control, cwd, codexThreadId, sessionCodex } = args;
29803
+ const portForwardWatcherRootPid = args.portForwardWatcherRootPid;
29746
29804
  const workspaceSlug = optionalThreadControlField(control, "workspace");
29747
29805
  const channelSlug = optionalThreadControlField(control, "channel");
29748
29806
  const kandanThreadId = optionalThreadControlField(control, "threadId");
@@ -29762,7 +29820,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29762
29820
  const runtimeSettings = startInstanceRuntimeSettings(options, control);
29763
29821
  const session = await attachChannelSession({
29764
29822
  kandan,
29765
- codex,
29823
+ codex: sessionCodex,
29766
29824
  topic,
29767
29825
  instanceId,
29768
29826
  options: {
@@ -29776,6 +29834,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29776
29834
  launchTui: false,
29777
29835
  enablePortForwardWatch: true,
29778
29836
  initialForwardPorts: allowedForwardPorts,
29837
+ portForwardWatcher: channelSessionPortForwardWatcherOptions({
29838
+ rootPid: portForwardWatcherRootPid,
29839
+ start: options.portForwardWatcher
29840
+ }),
29779
29841
  suppressedForwardPorts,
29780
29842
  onForwardPortApproved: (port, attribution) => {
29781
29843
  liveForwardPorts.add(port);
@@ -29810,6 +29872,151 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29810
29872
  dynamicChannelSessions.set(kandanThreadId, session);
29811
29873
  return session;
29812
29874
  };
29875
+ const attachThreadSession = async (control, cwd, codexThreadId) => {
29876
+ return await attachThreadSessionWithCodex({
29877
+ control,
29878
+ cwd,
29879
+ codexThreadId,
29880
+ sessionCodex: codex,
29881
+ portForwardWatcherRootPid: started?.process.pid
29882
+ });
29883
+ };
29884
+ const startThreadRunnerProcess = async (control, cwd) => {
29885
+ if (options.threadProcess?.role === "thread") {
29886
+ return void 0;
29887
+ }
29888
+ const agentProviderResult = startInstanceAgentProvider(control);
29889
+ if (agentProviderResult.type !== "ok" || agentProviderResult.provider !== "codex") {
29890
+ return void 0;
29891
+ }
29892
+ const workspaceSlug = optionalThreadControlField(control, "workspace");
29893
+ const channelSlug = optionalThreadControlField(control, "channel");
29894
+ const kandanThreadId = optionalThreadControlField(control, "threadId");
29895
+ if (workspaceSlug === void 0 || channelSlug === void 0 || kandanThreadId === void 0) {
29896
+ return void 0;
29897
+ }
29898
+ const existing = threadRunnerProcesses.get(kandanThreadId);
29899
+ if (existing !== void 0) {
29900
+ return {
29901
+ instanceId,
29902
+ controlType: control.type,
29903
+ ok: true,
29904
+ delegated: true,
29905
+ threadProcess: "already_started",
29906
+ kandanThreadId,
29907
+ cwd
29908
+ };
29909
+ }
29910
+ const spawnThreadRunner = options.spawnThreadRunner ?? spawnLocalThreadRunnerProcess;
29911
+ const readyTimeoutMs = threadRunnerReadyTimeoutMs(options);
29912
+ const startingEntry = {
29913
+ kind: "starting",
29914
+ promise: withThreadRunnerReadyTimeout(
29915
+ spawnThreadRunner(
29916
+ threadRunnerOptions({
29917
+ options,
29918
+ control,
29919
+ cwd,
29920
+ workspaceSlug,
29921
+ channelSlug,
29922
+ kandanThreadId
29923
+ })
29924
+ ),
29925
+ {
29926
+ kandanThreadId,
29927
+ timeoutMs: readyTimeoutMs
29928
+ }
29929
+ )
29930
+ };
29931
+ threadRunnerProcesses.set(kandanThreadId, startingEntry);
29932
+ let handle;
29933
+ try {
29934
+ handle = await startingEntry.promise;
29935
+ } catch (error51) {
29936
+ if (threadRunnerProcesses.get(kandanThreadId) === startingEntry) {
29937
+ threadRunnerProcesses.delete(kandanThreadId);
29938
+ }
29939
+ throw error51;
29940
+ }
29941
+ try {
29942
+ const threadCodex = await connectCodexAppServer(handle.codexUrl);
29943
+ const closeThreadCodex = once(() => threadCodex.close());
29944
+ handle.onExit?.(closeThreadCodex);
29945
+ const runtimeHandle = {
29946
+ ...handle,
29947
+ close: async () => {
29948
+ closeThreadCodex();
29949
+ await handle.close();
29950
+ }
29951
+ };
29952
+ const runningEntry = {
29953
+ kind: "running",
29954
+ handle: runtimeHandle
29955
+ };
29956
+ if (threadRunnerProcesses.get(kandanThreadId) === startingEntry) {
29957
+ threadRunnerProcesses.set(kandanThreadId, runningEntry);
29958
+ }
29959
+ handle.onExit?.(() => {
29960
+ if (threadRunnerProcesses.get(kandanThreadId) === runningEntry) {
29961
+ threadRunnerProcesses.delete(kandanThreadId);
29962
+ }
29963
+ });
29964
+ log("runner.thread_process_started", {
29965
+ kandanThreadId,
29966
+ cwd
29967
+ });
29968
+ const onStartedThread = (startedControl, startedCwd, codexThreadId) => attachThreadSessionWithCodex({
29969
+ control: startedControl,
29970
+ cwd: startedCwd,
29971
+ codexThreadId,
29972
+ sessionCodex: threadCodex,
29973
+ portForwardWatcherRootPid: handle.processPid
29974
+ });
29975
+ switch (control.type) {
29976
+ case "start_instance": {
29977
+ const result = await startCodexProviderInstance({
29978
+ codex: threadCodex,
29979
+ kandan,
29980
+ topic,
29981
+ control,
29982
+ options,
29983
+ instanceId,
29984
+ cwd,
29985
+ matchedRoot: cwd,
29986
+ onStartedThread,
29987
+ setStartupStage: () => void 0,
29988
+ setStartedCodexThreadId: () => void 0
29989
+ });
29990
+ return result.controlResponse;
29991
+ }
29992
+ case "reconnect_thread":
29993
+ return await applyControl(
29994
+ threadCodex,
29995
+ kandan,
29996
+ topic,
29997
+ instanceId,
29998
+ options,
29999
+ agentProviders,
30000
+ allowedCwds.value,
30001
+ activeClaudeCodeSessions,
30002
+ pendingClaudeCodeApprovals,
30003
+ ensureClaudeCodeForwardPortSession,
30004
+ disposeClaudeCodeForwardPortSession,
30005
+ control,
30006
+ log,
30007
+ onStartedThread,
30008
+ void 0
30009
+ );
30010
+ }
30011
+ } catch (error51) {
30012
+ const entry = threadRunnerProcesses.get(kandanThreadId);
30013
+ if (entry === startingEntry || entry?.kind === "running") {
30014
+ threadRunnerProcesses.delete(kandanThreadId);
30015
+ }
30016
+ await (entry?.kind === "running" ? entry.handle.close() : handle.close()).catch(() => void 0);
30017
+ throw error51;
30018
+ }
30019
+ };
29813
30020
  const heartbeatPayload = () => ({
29814
30021
  instanceId,
29815
30022
  clientId,
@@ -29890,6 +30097,15 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29890
30097
  });
29891
30098
  const handleControl = (control) => {
29892
30099
  log("kandan.control", { control });
30100
+ if (!threadProcessOwnsControl(options, control)) {
30101
+ log("kandan.control_ignored", {
30102
+ reason: "thread_process_mismatch",
30103
+ controlType: control.type,
30104
+ controlThreadId: controlThreadId(control) ?? null,
30105
+ threadProcessId: options.threadProcess?.kandanThreadId ?? null
30106
+ });
30107
+ return;
30108
+ }
29893
30109
  if (control.type === "replace_runner") {
29894
30110
  log("runner.replaced", {
29895
30111
  runnerId: options.runnerId,
@@ -30099,7 +30315,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
30099
30315
  disposeClaudeCodeForwardPortSession,
30100
30316
  control,
30101
30317
  log,
30102
- attachThreadSession
30318
+ attachThreadSession,
30319
+ shouldUseThreadProcesses(options) ? startThreadRunnerProcess : void 0
30103
30320
  );
30104
30321
  }).then((response) => {
30105
30322
  return kandan.push(topic, "codex_response", response);
@@ -30129,6 +30346,60 @@ function controlTargetsInstance(control, instanceId) {
30129
30346
  function controlInstanceId(control) {
30130
30347
  return "instanceId" in control ? control.instanceId : void 0;
30131
30348
  }
30349
+ function shouldUseThreadProcesses(options) {
30350
+ if (options.threadProcess?.role === "thread") {
30351
+ return false;
30352
+ }
30353
+ return options.spawnThreadRunner !== void 0 || options.codexUrl === void 0;
30354
+ }
30355
+ function threadProcessOwnsControl(options, control) {
30356
+ const threadProcess = options.threadProcess;
30357
+ const threadId = controlThreadId(control);
30358
+ if (threadProcess?.role !== "thread") {
30359
+ if (shouldUseThreadProcesses(options) && threadId !== void 0 && threadChildOnlyControl(control)) {
30360
+ return false;
30361
+ }
30362
+ return true;
30363
+ }
30364
+ if (threadId !== void 0) {
30365
+ return threadId === threadProcess.kandanThreadId;
30366
+ }
30367
+ if ("instanceId" in control && control.instanceId !== void 0 && control.instanceId !== "") {
30368
+ return true;
30369
+ }
30370
+ switch (control.type) {
30371
+ case "replace_runner":
30372
+ return true;
30373
+ default:
30374
+ return false;
30375
+ }
30376
+ }
30377
+ function threadChildOnlyControl(control) {
30378
+ switch (control.type) {
30379
+ case "forward_http_request":
30380
+ case "forward_tcp_open":
30381
+ case "forward_tcp_send":
30382
+ case "forward_tcp_close":
30383
+ return true;
30384
+ default:
30385
+ return false;
30386
+ }
30387
+ }
30388
+ function controlThreadId(control) {
30389
+ switch (control.type) {
30390
+ case "start_instance":
30391
+ case "reconnect_thread":
30392
+ case "forward_http_request":
30393
+ case "forward_tcp_open":
30394
+ case "forward_tcp_send":
30395
+ case "forward_tcp_close": {
30396
+ const threadId = stringValue(control.threadId)?.trim();
30397
+ return threadId === void 0 || threadId === "" ? void 0 : threadId;
30398
+ }
30399
+ default:
30400
+ return void 0;
30401
+ }
30402
+ }
30132
30403
  async function resolveSessionControl(channelSession, dynamicChannelSessions, control) {
30133
30404
  const primaryHandled = await (channelSession?.handleControl(control) ?? Promise.resolve(void 0));
30134
30405
  if (primaryHandled !== void 0) {
@@ -30349,7 +30620,7 @@ async function resumeCodexThreadForReconnect(codex, codexThreadId, resumeOverrid
30349
30620
  );
30350
30621
  }
30351
30622
  }
30352
- async function applyControl(codex, kandan, topic, instanceId, options, agentProviders, allowedCwds, activeClaudeCodeSessions, pendingClaudeCodeApprovals, ensureClaudeCodeForwardPortSession, disposeClaudeCodeForwardPortSession, control, log, onStartedThread) {
30623
+ async function applyControl(codex, kandan, topic, instanceId, options, agentProviders, allowedCwds, activeClaudeCodeSessions, pendingClaudeCodeApprovals, ensureClaudeCodeForwardPortSession, disposeClaudeCodeForwardPortSession, control, log, onStartedThread, onThreadProcessStart) {
30353
30624
  switch (control.type) {
30354
30625
  case "start_instance": {
30355
30626
  const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
@@ -30427,6 +30698,10 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
30427
30698
  });
30428
30699
  }
30429
30700
  try {
30701
+ const delegated = await onThreadProcessStart?.(control, cwd.cwd);
30702
+ if (delegated !== void 0) {
30703
+ return delegated;
30704
+ }
30430
30705
  if (agentProvider === "claude-code") {
30431
30706
  startupStage = "starting_claude_code_session";
30432
30707
  const result2 = await startClaudeCodeProviderInstance({
@@ -30564,6 +30839,10 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
30564
30839
  const workDescription = normalizedWorkDescription(
30565
30840
  control.workDescription
30566
30841
  );
30842
+ const delegated = await onThreadProcessStart?.(control, cwd.cwd);
30843
+ if (delegated !== void 0) {
30844
+ return delegated;
30845
+ }
30567
30846
  if (agentProvider === "claude-code") {
30568
30847
  const activeSession = activeClaudeCodeSessions.get(codexThreadId);
30569
30848
  if (activeSession !== void 0 && workDescription !== void 0) {
@@ -32371,25 +32650,330 @@ function startInstanceRuntimeSettings(options, control) {
32371
32650
  allowPortForwardingByDefault: control.allowPortForwardingByDefault ?? defaults.allowPortForwardingByDefault
32372
32651
  };
32373
32652
  }
32374
- async function startOwnedCodexAppServer(options) {
32653
+ function channelSessionPortForwardWatcherOptions(args) {
32654
+ if (args.rootPid === void 0 && args.start === void 0) {
32655
+ return void 0;
32656
+ }
32657
+ return {
32658
+ ...args.rootPid === void 0 ? {} : { rootPid: args.rootPid },
32659
+ ...args.start === void 0 ? {} : { start: args.start }
32660
+ };
32661
+ }
32662
+ function threadRunnerOptions(args) {
32663
+ const runtimeSettings = startInstanceRuntimeSettings(
32664
+ args.options,
32665
+ args.control
32666
+ );
32667
+ return {
32668
+ ...args.options,
32669
+ clientId: void 0,
32670
+ machineId: void 0,
32671
+ runnerLockConfigPath: void 0,
32672
+ workspaceSlug: args.workspaceSlug,
32673
+ cwd: args.cwd,
32674
+ codexUrl: void 0,
32675
+ launchTui: false,
32676
+ allowedCwds: Array.from(/* @__PURE__ */ new Set([args.cwd, ...args.options.allowedCwds])),
32677
+ codeServerBin: args.options.editorRuntime?.mode === "server_managed" ? void 0 : args.options.codeServerBin,
32678
+ editorRuntime: void 0,
32679
+ threadProcess: {
32680
+ role: "thread",
32681
+ kandanThreadId: args.kandanThreadId
32682
+ },
32683
+ spawnThreadRunner: void 0,
32684
+ threadRunnerReadyTimeoutMs: args.options.threadRunnerReadyTimeoutMs,
32685
+ runtimeDefaults: {
32686
+ model: runtimeSettings.model,
32687
+ reasoningEffort: runtimeSettings.reasoningEffort,
32688
+ sandbox: runtimeSettings.sandbox,
32689
+ approvalPolicy: runtimeSettings.approvalPolicy,
32690
+ allowPortForwardingByDefault: runtimeSettings.allowPortForwardingByDefault
32691
+ },
32692
+ channelSession: void 0
32693
+ };
32694
+ }
32695
+ async function closeThreadRunnerEntry(entry) {
32696
+ switch (entry.kind) {
32697
+ case "running":
32698
+ await entry.handle.close();
32699
+ return;
32700
+ case "starting":
32701
+ try {
32702
+ const handle = await entry.promise;
32703
+ await handle.close();
32704
+ } catch (_error) {
32705
+ return;
32706
+ }
32707
+ return;
32708
+ }
32709
+ }
32710
+ async function spawnLocalThreadRunnerProcess(options) {
32711
+ const scriptPath = process.argv[1];
32712
+ const kandanThreadId = options.threadProcess?.kandanThreadId ?? "";
32713
+ if (scriptPath === void 0 || scriptPath.trim() === "") {
32714
+ throw new Error(
32715
+ "cannot fork thread runner without current CLI script path"
32716
+ );
32717
+ }
32718
+ const args = ["run", ...threadRunnerCliArgs(options)];
32719
+ const env = {
32720
+ ...process.env,
32721
+ LINZUMI_THREAD_RUNNER_ROLE: "thread",
32722
+ LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID: kandanThreadId
32723
+ };
32724
+ writeCliAuditEvent("process.spawn", {
32725
+ command: process.execPath,
32726
+ args: [scriptPath, ...redactedThreadRunnerCliArgs(args)],
32727
+ cwd: options.cwd,
32728
+ purpose: "linzumi.thread_runner"
32729
+ });
32730
+ const child = spawn6(process.execPath, [scriptPath, ...args], {
32731
+ cwd: options.cwd,
32732
+ env,
32733
+ stdio: ["inherit", "inherit", "inherit", "ipc"]
32734
+ });
32735
+ writeCliAuditEvent("process.spawned", {
32736
+ command: process.execPath,
32737
+ args: [scriptPath, ...redactedThreadRunnerCliArgs(args)],
32738
+ cwd: options.cwd,
32739
+ pid: child.pid,
32740
+ purpose: "linzumi.thread_runner"
32741
+ });
32742
+ const exitListeners = /* @__PURE__ */ new Set();
32743
+ const exitState = { exited: false };
32744
+ child.once("exit", (code, signal) => {
32745
+ writeCliAuditEvent("process.exit", {
32746
+ command: process.execPath,
32747
+ args: [scriptPath, ...redactedThreadRunnerCliArgs(args)],
32748
+ cwd: options.cwd,
32749
+ pid: child.pid,
32750
+ code,
32751
+ signal,
32752
+ purpose: "linzumi.thread_runner"
32753
+ });
32754
+ exitState.exited = true;
32755
+ for (const listener of exitListeners) {
32756
+ listener();
32757
+ }
32758
+ exitListeners.clear();
32759
+ });
32760
+ const ready2 = await new Promise((resolve11, reject) => {
32761
+ let settled = false;
32762
+ let timeout;
32763
+ const cleanupReadyListeners = () => {
32764
+ if (timeout !== void 0) {
32765
+ clearTimeout(timeout);
32766
+ }
32767
+ child.off("message", onMessage);
32768
+ child.off("exit", onExitBeforeReady);
32769
+ };
32770
+ const settle = (action) => {
32771
+ if (settled) {
32772
+ return;
32773
+ }
32774
+ settled = true;
32775
+ cleanupReadyListeners();
32776
+ action();
32777
+ };
32778
+ const onMessage = (message) => {
32779
+ const parsed = (() => {
32780
+ try {
32781
+ return threadRunnerReadyMessage(message, kandanThreadId);
32782
+ } catch (error51) {
32783
+ settle(
32784
+ () => reject(error51 instanceof Error ? error51 : new Error(String(error51)))
32785
+ );
32786
+ return void 0;
32787
+ }
32788
+ })();
32789
+ if (parsed === void 0) {
32790
+ return;
32791
+ }
32792
+ settle(() => resolve11(parsed));
32793
+ };
32794
+ const onExitBeforeReady = (code, signal) => {
32795
+ settle(
32796
+ () => reject(
32797
+ new Error(
32798
+ `thread Codex worker exited before ready: code=${code ?? "null"} signal=${signal ?? "null"}`
32799
+ )
32800
+ )
32801
+ );
32802
+ };
32803
+ const onReadyTimeout = () => {
32804
+ settle(() => {
32805
+ child.kill("SIGINT");
32806
+ reject(threadRunnerReadyTimeoutError(kandanThreadId, readyTimeoutMs));
32807
+ });
32808
+ };
32809
+ child.on("message", onMessage);
32810
+ child.once("exit", onExitBeforeReady);
32811
+ const readyTimeoutMs = threadRunnerReadyTimeoutMs(options);
32812
+ timeout = setTimeout(onReadyTimeout, readyTimeoutMs);
32813
+ });
32814
+ return {
32815
+ kandanThreadId: ready2.kandanThreadId,
32816
+ codexUrl: ready2.codexUrl,
32817
+ processPid: child.pid,
32818
+ onExit: (listener) => {
32819
+ if (exitState.exited) {
32820
+ queueMicrotask(listener);
32821
+ return;
32822
+ }
32823
+ exitListeners.add(listener);
32824
+ },
32825
+ close: () => new Promise((resolveClose) => {
32826
+ if (child.exitCode !== null || child.signalCode !== null) {
32827
+ resolveClose();
32828
+ return;
32829
+ }
32830
+ child.once("exit", () => resolveClose());
32831
+ child.kill("SIGINT");
32832
+ })
32833
+ };
32834
+ }
32835
+ function threadRunnerReadyMessage(message, expectedKandanThreadId) {
32836
+ if (!isJsonObject(message)) {
32837
+ return void 0;
32838
+ }
32839
+ if (message.type !== "linzumi_thread_codex_worker_ready") {
32840
+ return void 0;
32841
+ }
32842
+ const kandanThreadId = stringValue(message.kandanThreadId);
32843
+ const codexUrl = stringValue(message.codexUrl);
32844
+ if (kandanThreadId !== expectedKandanThreadId || codexUrl === void 0) {
32845
+ throw new Error("thread Codex worker reported invalid ready payload");
32846
+ }
32847
+ return { kandanThreadId, codexUrl };
32848
+ }
32849
+ function waitForThreadCodexWorkerStop(codexProcess) {
32850
+ return new Promise((resolve11) => {
32851
+ const stop = once(() => {
32852
+ process.off("disconnect", stop);
32853
+ process.off("SIGINT", stop);
32854
+ process.off("SIGTERM", stop);
32855
+ codexProcess.off("exit", stop);
32856
+ resolve11();
32857
+ });
32858
+ process.once("disconnect", stop);
32859
+ process.once("SIGINT", stop);
32860
+ process.once("SIGTERM", stop);
32861
+ codexProcess.once("exit", stop);
32862
+ });
32863
+ }
32864
+ function withThreadRunnerReadyTimeout(promise2, args) {
32865
+ return new Promise((resolve11, reject) => {
32866
+ let settled = false;
32867
+ const timeout = setTimeout(() => {
32868
+ if (settled) {
32869
+ return;
32870
+ }
32871
+ settled = true;
32872
+ reject(
32873
+ threadRunnerReadyTimeoutError(args.kandanThreadId, args.timeoutMs)
32874
+ );
32875
+ }, args.timeoutMs);
32876
+ promise2.then(
32877
+ (handle) => {
32878
+ if (settled) {
32879
+ return;
32880
+ }
32881
+ settled = true;
32882
+ clearTimeout(timeout);
32883
+ resolve11(handle);
32884
+ },
32885
+ (error51) => {
32886
+ if (settled) {
32887
+ return;
32888
+ }
32889
+ settled = true;
32890
+ clearTimeout(timeout);
32891
+ reject(error51);
32892
+ }
32893
+ );
32894
+ });
32895
+ }
32896
+ function threadRunnerReadyTimeoutError(kandanThreadId, timeoutMs) {
32897
+ return new Error(
32898
+ `thread Codex worker did not report ready within ${timeoutMs}ms for Kandan thread ${kandanThreadId}`
32899
+ );
32900
+ }
32901
+ function threadRunnerReadyTimeoutMs(options) {
32902
+ const timeoutMs = options.threadRunnerReadyTimeoutMs ?? THREAD_RUNNER_READY_TIMEOUT_MS;
32903
+ if (Number.isFinite(timeoutMs) && timeoutMs >= 0) {
32904
+ return timeoutMs;
32905
+ }
32906
+ throw new Error("thread runner ready timeout must be a non-negative number");
32907
+ }
32908
+ function threadRunnerCliArgs(options) {
32909
+ return [
32910
+ "--api-url",
32911
+ options.kandanUrl,
32912
+ "--cwd",
32913
+ options.cwd,
32914
+ "--codex-bin",
32915
+ options.codexBin,
32916
+ ...optionalCliValue("--workspace", options.workspaceSlug),
32917
+ ...optionalCliValue("--log-file", options.logFile),
32918
+ ...options.fast === true ? ["--fast"] : [],
32919
+ ...optionalCliValue("--allowed-cwd", options.allowedCwds.join(",")),
32920
+ ...optionalCliValue(
32921
+ "--forward-port",
32922
+ (options.allowedForwardPorts ?? []).join(",")
32923
+ ),
32924
+ ...optionalCliValue("--code-server-bin", options.codeServerBin),
32925
+ ...optionalCliValue("--model", options.runtimeDefaults?.model),
32926
+ ...optionalCliValue(
32927
+ "--reasoning-effort",
32928
+ options.runtimeDefaults?.reasoningEffort
32929
+ ),
32930
+ ...optionalCliValue("--sandbox", options.runtimeDefaults?.sandbox),
32931
+ ...optionalCliValue(
32932
+ "--approval-policy",
32933
+ options.runtimeDefaults?.approvalPolicy
32934
+ ),
32935
+ ...options.runtimeDefaults?.allowPortForwardingByDefault === true ? ["--allow-port-forwarding-by-default"] : []
32936
+ ];
32937
+ }
32938
+ function redactedThreadRunnerCliArgs(args) {
32939
+ const redacted = [];
32940
+ for (let index = 0; index < args.length; index += 1) {
32941
+ const arg = args[index];
32942
+ if (arg === "--token") {
32943
+ redacted.push(arg, "<redacted>");
32944
+ index += 1;
32945
+ continue;
32946
+ }
32947
+ if (arg !== void 0) {
32948
+ redacted.push(arg);
32949
+ }
32950
+ }
32951
+ return redacted;
32952
+ }
32953
+ function optionalCliValue(flag, value) {
32954
+ return value === void 0 || value === "" ? [] : [flag, value];
32955
+ }
32956
+ async function startOwnedCodexAppServer(options, args = { linzumiMcp: true }) {
32375
32957
  ensureCodexProjectTrusted(options.cwd);
32376
32958
  const defaults = runnerRuntimeDefaults(options);
32377
- const mcpAuth = writeEphemeralMcpAuthFile(options);
32959
+ const mcpAuth = args.linzumiMcp === true ? writeEphemeralMcpAuthFile(options) : void 0;
32960
+ const mcpCommand = currentLinzumiCommand();
32378
32961
  try {
32379
32962
  const started = await startCodexAppServer(options.codexBin, options.cwd, {
32380
32963
  model: defaults.model,
32381
32964
  reasoningEffort: defaults.reasoningEffort,
32382
32965
  fast: options.fast,
32383
- mcpServers: [
32966
+ mcpServers: mcpAuth === void 0 ? [] : [
32384
32967
  linzumiMcpServerConfig({
32385
- command: currentLinzumiCommand(),
32968
+ command: mcpCommand.command,
32969
+ argsPrefix: mcpCommand.argsPrefix,
32386
32970
  kandanUrl: options.kandanUrl,
32387
32971
  authFilePath: mcpAuth.path,
32388
32972
  ownerUsername: mcpOwnerUsername(options)
32389
32973
  })
32390
32974
  ]
32391
32975
  });
32392
- const cleanup = once(mcpAuth.cleanup);
32976
+ const cleanup = once(() => mcpAuth?.cleanup());
32393
32977
  started.process.once("exit", cleanup);
32394
32978
  return {
32395
32979
  ...started,
@@ -32399,7 +32983,7 @@ async function startOwnedCodexAppServer(options) {
32399
32983
  }
32400
32984
  };
32401
32985
  } catch (error51) {
32402
- mcpAuth.cleanup();
32986
+ mcpAuth?.cleanup();
32403
32987
  throw error51;
32404
32988
  }
32405
32989
  }
@@ -32435,8 +33019,7 @@ function once(action) {
32435
33019
  };
32436
33020
  }
32437
33021
  function currentLinzumiCommand() {
32438
- const scriptPath = process.argv[1];
32439
- return scriptPath === void 0 || scriptPath.trim() === "" ? "linzumi" : scriptPath;
33022
+ return linzumiMcpCommandForProcess(process.execPath, process.argv[1]);
32440
33023
  }
32441
33024
  function runnerWorkspaceSlug(options) {
32442
33025
  return options.channelSession?.workspaceSlug ?? options.workspaceSlug;
@@ -62791,6 +63374,10 @@ async function main(args) {
62791
63374
  return;
62792
63375
  }
62793
63376
  case "run": {
63377
+ if (process.env.LINZUMI_THREAD_RUNNER_ROLE === "thread") {
63378
+ await runThreadCodexWorker(parseThreadCodexWorkerArgs(parsed.args));
63379
+ return;
63380
+ }
62794
63381
  const options = await parseRunnerArgs(parsed.args);
62795
63382
  await runLocalCodexRunner(withLocalMachineId(options));
62796
63383
  return;
@@ -63421,6 +64008,12 @@ async function parseRunnerArgs(args, deps = {
63421
64008
  ) : [...localConfiguredAllowedCwds.allowedCwds];
63422
64009
  const codexBin = stringValue5(values, "codex-bin") ?? "codex";
63423
64010
  const customCodeServerBin = stringValue5(values, "code-server-bin");
64011
+ const initialDependencyStatus = await deps.buildDependencyStatus({
64012
+ cwd,
64013
+ codexBin,
64014
+ codeServerBin: customCodeServerBin
64015
+ });
64016
+ assertStartDependencies(initialDependencyStatus);
63424
64017
  const explicitToken = stringValue5(values, "token");
63425
64018
  const token = await deps.resolveToken({
63426
64019
  kandanUrl,
@@ -63657,11 +64250,60 @@ function parseChannelPath(channel) {
63657
64250
  };
63658
64251
  }
63659
64252
  function withLocalMachineId(options) {
64253
+ if (options.threadProcess?.role === "thread") {
64254
+ return options;
64255
+ }
63660
64256
  return {
63661
64257
  ...options,
63662
64258
  machineId: localConfigScopeKey(options.kandanUrl) === localConfigScopeKey(defaultLinzumiWebSocketUrl) ? ensureLocalMachineId() : ensureLocalMachineIdForLinzumiUrl(options.kandanUrl)
63663
64259
  };
63664
64260
  }
64261
+ function parseThreadCodexWorkerArgs(args) {
64262
+ const values = strictFlagValues2(args);
64263
+ const kandanUrl = kandanUrlValue(values) ?? defaultLinzumiWebSocketUrl;
64264
+ const cwd = stringValue5(values, "cwd") ?? process.cwd();
64265
+ const configuredAllowedCwds2 = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(
64266
+ parseAllowedCwdList(stringValue5(values, "allowed-cwd"))
64267
+ ) : assertConfiguredAllowedCwds([cwd]);
64268
+ const kandanThreadId = requiredThreadRunnerKandanThreadId();
64269
+ return {
64270
+ kandanUrl,
64271
+ token: "",
64272
+ runnerId: `thread-codex-worker:${kandanThreadId}`,
64273
+ machineId: void 0,
64274
+ runnerLockConfigPath: void 0,
64275
+ workspaceSlug: stringValue5(values, "workspace"),
64276
+ cwd,
64277
+ codexBin: stringValue5(values, "codex-bin") ?? "codex",
64278
+ codexUrl: void 0,
64279
+ launchTui: false,
64280
+ fast: values.get("fast") === true ? true : void 0,
64281
+ logFile: stringValue5(values, "log-file"),
64282
+ allowedCwds: configuredAllowedCwds2.allowedCwds,
64283
+ missingAllowedCwds: configuredAllowedCwds2.missingAllowedCwds,
64284
+ allowedForwardPorts: parseAllowedPortList(
64285
+ stringValue5(values, "forward-port")
64286
+ ),
64287
+ codeServerBin: void 0,
64288
+ editorRuntime: void 0,
64289
+ socketFactory: void 0,
64290
+ runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
64291
+ threadProcess: {
64292
+ role: "thread",
64293
+ kandanThreadId
64294
+ },
64295
+ channelSession: void 0
64296
+ };
64297
+ }
64298
+ function requiredThreadRunnerKandanThreadId() {
64299
+ const kandanThreadId = process.env.LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID;
64300
+ if (kandanThreadId === void 0 || kandanThreadId.trim() === "") {
64301
+ throw new Error(
64302
+ "thread runner is missing LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID"
64303
+ );
64304
+ }
64305
+ return kandanThreadId;
64306
+ }
63665
64307
  function required3(values, key) {
63666
64308
  const value = stringValue5(values, key);
63667
64309
  if (value === void 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.58-beta",
3
+ "version": "0.0.60-beta",
4
4
  "description": "Linzumi CLI — point a Codex agent at the real code on your laptop, with your team watching and steering from shared threads.",
5
5
  "type": "module",
6
6
  "bin": {