@linzumi/cli 0.0.59-beta → 0.0.61-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 +679 -110
  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.59-beta --version
65
+ npx -y @linzumi/cli@0.0.61-beta --version
66
66
  linzumi --version
67
67
  ```
68
68
 
package/dist/index.js CHANGED
@@ -28294,7 +28294,7 @@ function realpathOrResolved(pathValue) {
28294
28294
  }
28295
28295
 
28296
28296
  // src/version.ts
28297
- var linzumiCliVersion = "0.0.59-beta";
28297
+ var linzumiCliVersion = "0.0.61-beta";
28298
28298
  var linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
28299
28299
 
28300
28300
  // src/runnerLock.ts
@@ -28779,7 +28779,335 @@ function normalizeLocalRunnerCache(value) {
28779
28779
  );
28780
28780
  }
28781
28781
 
28782
+ // src/threadCodexWorkerIpc.ts
28783
+ function bindThreadCodexWorkerIpc(codex) {
28784
+ const pendingServerRequests = /* @__PURE__ */ new Map();
28785
+ const state = { nextServerRequestId: 1 };
28786
+ const send = (message) => {
28787
+ if (process.send === void 0) {
28788
+ throw new Error("thread Codex worker IPC channel closed");
28789
+ }
28790
+ process.send(message);
28791
+ };
28792
+ process.on("message", (message) => {
28793
+ const parsed = threadCodexWorkerParentMessage(message);
28794
+ if (parsed === void 0) {
28795
+ return;
28796
+ }
28797
+ switch (parsed.type) {
28798
+ case "linzumi_thread_codex_worker_request":
28799
+ void codex.request(parsed.method, parsed.params).then(
28800
+ (response) => send({
28801
+ type: "linzumi_thread_codex_worker_response",
28802
+ id: parsed.id,
28803
+ response
28804
+ })
28805
+ ).catch(
28806
+ (error51) => send({
28807
+ type: "linzumi_thread_codex_worker_response",
28808
+ id: parsed.id,
28809
+ error: error51 instanceof Error ? error51.message : String(error51)
28810
+ })
28811
+ );
28812
+ return;
28813
+ case "linzumi_thread_codex_worker_notify":
28814
+ codex.notify(parsed.method, parsed.params);
28815
+ return;
28816
+ case "linzumi_thread_codex_worker_server_request_response": {
28817
+ const pending = pendingServerRequests.get(parsed.id);
28818
+ if (pending === void 0) {
28819
+ return;
28820
+ }
28821
+ pendingServerRequests.delete(parsed.id);
28822
+ if (parsed.error !== void 0) {
28823
+ pending.reject(new Error(parsed.error));
28824
+ return;
28825
+ }
28826
+ pending.resolve(parsed.result);
28827
+ return;
28828
+ }
28829
+ }
28830
+ });
28831
+ codex.onNotification((notification) => {
28832
+ send({
28833
+ type: "linzumi_thread_codex_worker_notification",
28834
+ notification
28835
+ });
28836
+ });
28837
+ codex.onRequest(
28838
+ (request) => new Promise((resolve11, reject) => {
28839
+ const id = state.nextServerRequestId;
28840
+ state.nextServerRequestId += 1;
28841
+ pendingServerRequests.set(id, { resolve: resolve11, reject });
28842
+ send({
28843
+ type: "linzumi_thread_codex_worker_server_request",
28844
+ id,
28845
+ request
28846
+ });
28847
+ })
28848
+ );
28849
+ process.once("disconnect", () => {
28850
+ for (const pending of pendingServerRequests.values()) {
28851
+ pending.reject(new Error("thread Codex worker IPC channel closed"));
28852
+ }
28853
+ pendingServerRequests.clear();
28854
+ });
28855
+ }
28856
+ function connectThreadCodexWorkerIpc(child) {
28857
+ const pendingRequests = /* @__PURE__ */ new Map();
28858
+ const notificationCallbacks = /* @__PURE__ */ new Set();
28859
+ const requestCallbacks = /* @__PURE__ */ new Set();
28860
+ const pendingNotifications = [];
28861
+ const state = { nextRequestId: 1, closed: false };
28862
+ const rejectPendingRequests = (message) => {
28863
+ const error51 = new Error(message);
28864
+ for (const pending of pendingRequests.values()) {
28865
+ pending.reject(error51);
28866
+ }
28867
+ pendingRequests.clear();
28868
+ };
28869
+ const send = (message) => {
28870
+ if (state.closed) {
28871
+ throw new Error("thread Codex worker IPC client is closed");
28872
+ }
28873
+ if (child.send === void 0 || child.connected === false) {
28874
+ throw new Error("thread Codex worker IPC channel closed");
28875
+ }
28876
+ child.send(message);
28877
+ };
28878
+ const onMessage = (message) => {
28879
+ const parsed = threadCodexWorkerChildMessage(message);
28880
+ if (parsed === void 0) {
28881
+ return;
28882
+ }
28883
+ switch (parsed.type) {
28884
+ case "linzumi_thread_codex_worker_response": {
28885
+ const pending = pendingRequests.get(parsed.id);
28886
+ if (pending === void 0) {
28887
+ return;
28888
+ }
28889
+ pendingRequests.delete(parsed.id);
28890
+ if (parsed.error !== void 0) {
28891
+ pending.reject(new Error(parsed.error));
28892
+ return;
28893
+ }
28894
+ if (parsed.response === void 0) {
28895
+ pending.reject(
28896
+ new Error("thread Codex worker response missing body")
28897
+ );
28898
+ return;
28899
+ }
28900
+ pending.resolve(parsed.response);
28901
+ return;
28902
+ }
28903
+ case "linzumi_thread_codex_worker_notification":
28904
+ if (notificationCallbacks.size === 0) {
28905
+ pendingNotifications.push(parsed.notification);
28906
+ } else {
28907
+ for (const callback of notificationCallbacks) {
28908
+ callback(parsed.notification);
28909
+ }
28910
+ }
28911
+ return;
28912
+ case "linzumi_thread_codex_worker_server_request":
28913
+ void respondToThreadCodexWorkerServerRequest(
28914
+ send,
28915
+ parsed.id,
28916
+ parsed.request,
28917
+ requestCallbacks
28918
+ );
28919
+ return;
28920
+ }
28921
+ };
28922
+ const onClosed = () => {
28923
+ state.closed = true;
28924
+ rejectPendingRequests("thread Codex worker IPC channel closed");
28925
+ };
28926
+ child.on("message", onMessage);
28927
+ child.once("exit", onClosed);
28928
+ child.once("disconnect", onClosed);
28929
+ return {
28930
+ request: (method, params) => new Promise((resolve11, reject) => {
28931
+ const id = state.nextRequestId;
28932
+ state.nextRequestId += 1;
28933
+ pendingRequests.set(id, { resolve: resolve11, reject });
28934
+ try {
28935
+ send({
28936
+ type: "linzumi_thread_codex_worker_request",
28937
+ id,
28938
+ method,
28939
+ params
28940
+ });
28941
+ } catch (error51) {
28942
+ pendingRequests.delete(id);
28943
+ reject(error51 instanceof Error ? error51 : new Error(String(error51)));
28944
+ }
28945
+ }),
28946
+ notify: (method, params) => {
28947
+ send({
28948
+ type: "linzumi_thread_codex_worker_notify",
28949
+ method,
28950
+ params
28951
+ });
28952
+ },
28953
+ onNotification: (callback) => {
28954
+ notificationCallbacks.add(callback);
28955
+ pendingNotifications.splice(0).forEach((notification) => {
28956
+ callback(notification);
28957
+ });
28958
+ },
28959
+ onRequest: (callback) => {
28960
+ requestCallbacks.add(callback);
28961
+ },
28962
+ close: () => {
28963
+ state.closed = true;
28964
+ child.off("message", onMessage);
28965
+ child.off("exit", onClosed);
28966
+ child.off("disconnect", onClosed);
28967
+ rejectPendingRequests("thread Codex worker IPC client closed");
28968
+ }
28969
+ };
28970
+ }
28971
+ async function respondToThreadCodexWorkerServerRequest(send, id, request, callbacks) {
28972
+ try {
28973
+ for (const callback of callbacks) {
28974
+ const result = await callback(request);
28975
+ if (result !== void 0) {
28976
+ send({
28977
+ type: "linzumi_thread_codex_worker_server_request_response",
28978
+ id,
28979
+ result
28980
+ });
28981
+ return;
28982
+ }
28983
+ }
28984
+ send({
28985
+ type: "linzumi_thread_codex_worker_server_request_response",
28986
+ id,
28987
+ error: `unhandled Codex app-server request: ${request.method}`
28988
+ });
28989
+ } catch (error51) {
28990
+ send({
28991
+ type: "linzumi_thread_codex_worker_server_request_response",
28992
+ id,
28993
+ error: error51 instanceof Error ? error51.message : String(error51)
28994
+ });
28995
+ }
28996
+ }
28997
+ function threadCodexWorkerParentMessage(message) {
28998
+ if (!isJsonObject(message)) {
28999
+ return void 0;
29000
+ }
29001
+ switch (message.type) {
29002
+ case "linzumi_thread_codex_worker_request": {
29003
+ const id = integerValue(message.id);
29004
+ const method = stringValue(message.method);
29005
+ if (id === void 0 || method === void 0) {
29006
+ return void 0;
29007
+ }
29008
+ return {
29009
+ type: "linzumi_thread_codex_worker_request",
29010
+ id,
29011
+ method,
29012
+ params: objectValue(message.params)
29013
+ };
29014
+ }
29015
+ case "linzumi_thread_codex_worker_notify": {
29016
+ const method = stringValue(message.method);
29017
+ if (method === void 0) {
29018
+ return void 0;
29019
+ }
29020
+ return {
29021
+ type: "linzumi_thread_codex_worker_notify",
29022
+ method,
29023
+ params: objectValue(message.params)
29024
+ };
29025
+ }
29026
+ case "linzumi_thread_codex_worker_server_request_response": {
29027
+ const id = integerValue(message.id);
29028
+ if (id === void 0) {
29029
+ return void 0;
29030
+ }
29031
+ return {
29032
+ type: "linzumi_thread_codex_worker_server_request_response",
29033
+ id,
29034
+ result: message.result,
29035
+ error: stringValue(message.error)
29036
+ };
29037
+ }
29038
+ default:
29039
+ return void 0;
29040
+ }
29041
+ }
29042
+ function threadCodexWorkerChildMessage(message) {
29043
+ if (!isJsonObject(message)) {
29044
+ return void 0;
29045
+ }
29046
+ switch (message.type) {
29047
+ case "linzumi_thread_codex_worker_response": {
29048
+ const id = integerValue(message.id);
29049
+ if (id === void 0) {
29050
+ return void 0;
29051
+ }
29052
+ return {
29053
+ type: "linzumi_thread_codex_worker_response",
29054
+ id,
29055
+ response: jsonRpcResponseValue(message.response),
29056
+ error: stringValue(message.error)
29057
+ };
29058
+ }
29059
+ case "linzumi_thread_codex_worker_notification": {
29060
+ const notification = jsonRpcNotificationValue(message.notification);
29061
+ if (notification === void 0) {
29062
+ return void 0;
29063
+ }
29064
+ return {
29065
+ type: "linzumi_thread_codex_worker_notification",
29066
+ notification
29067
+ };
29068
+ }
29069
+ case "linzumi_thread_codex_worker_server_request": {
29070
+ const id = integerValue(message.id);
29071
+ const request = jsonRpcRequestValue(message.request);
29072
+ if (id === void 0 || request === void 0) {
29073
+ return void 0;
29074
+ }
29075
+ return {
29076
+ type: "linzumi_thread_codex_worker_server_request",
29077
+ id,
29078
+ request
29079
+ };
29080
+ }
29081
+ case "linzumi_thread_codex_worker_ready":
29082
+ return void 0;
29083
+ default:
29084
+ return void 0;
29085
+ }
29086
+ }
29087
+ function jsonRpcResponseValue(value) {
29088
+ if (!isJsonObject(value) || !("id" in value)) {
29089
+ return void 0;
29090
+ }
29091
+ if ("result" in value || "error" in value) {
29092
+ return value;
29093
+ }
29094
+ return void 0;
29095
+ }
29096
+ function jsonRpcNotificationValue(value) {
29097
+ if (!isJsonObject(value) || "id" in value || typeof value.method !== "string") {
29098
+ return void 0;
29099
+ }
29100
+ return value;
29101
+ }
29102
+ function jsonRpcRequestValue(value) {
29103
+ if (!isJsonObject(value) || !("id" in value) || typeof value.method !== "string") {
29104
+ return void 0;
29105
+ }
29106
+ return value;
29107
+ }
29108
+
28782
29109
  // src/runner.ts
29110
+ var THREAD_RUNNER_READY_TIMEOUT_MS = 3e4;
28783
29111
  async function runLocalCodexRunner(options) {
28784
29112
  const log = makeRunnerLogger(options);
28785
29113
  const cleanup = {
@@ -28817,6 +29145,31 @@ async function runLocalCodexRunner(options) {
28817
29145
  throw error51;
28818
29146
  }
28819
29147
  }
29148
+ async function runThreadCodexWorker(options) {
29149
+ if (options.threadProcess?.role !== "thread") {
29150
+ throw new Error("thread Codex worker requires thread process options");
29151
+ }
29152
+ if (process.send === void 0) {
29153
+ throw new Error("thread Codex worker requires an IPC channel");
29154
+ }
29155
+ const log = makeRunnerLogger(options);
29156
+ const started = await startOwnedCodexAppServer(options, {
29157
+ linzumiMcp: false
29158
+ });
29159
+ const codex = await connectCodexAppServer(started.url);
29160
+ const stop = once(() => {
29161
+ codex.close();
29162
+ started.stop();
29163
+ log.close();
29164
+ });
29165
+ bindThreadCodexWorkerIpc(codex);
29166
+ process.send({
29167
+ type: "linzumi_thread_codex_worker_ready",
29168
+ kandanThreadId: options.threadProcess.kandanThreadId
29169
+ });
29170
+ await waitForThreadCodexWorkerStop(started.process);
29171
+ stop();
29172
+ }
28820
29173
  async function openLocalCodexRunner(options, log, cleanup, close) {
28821
29174
  const agentProviders = availableRunnerAgentProviders(options);
28822
29175
  const allowedForwardPorts = options.allowedForwardPorts ?? [];
@@ -29711,7 +30064,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29711
30064
  launchTui: options.launchTui,
29712
30065
  enablePortForwardWatch: true,
29713
30066
  initialForwardPorts: allowedForwardPorts,
29714
- portForwardWatcher: started?.process.pid === void 0 ? void 0 : { rootPid: started.process.pid },
30067
+ portForwardWatcher: channelSessionPortForwardWatcherOptions({
30068
+ rootPid: started?.process.pid,
30069
+ start: options.portForwardWatcher
30070
+ }),
29715
30071
  suppressedForwardPorts,
29716
30072
  onForwardPortApproved: (port, attribution) => {
29717
30073
  liveForwardPorts.add(port);
@@ -29771,7 +30127,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29771
30127
  );
29772
30128
  threadRunnerProcesses.clear();
29773
30129
  });
29774
- const attachThreadSession = async (control, cwd, codexThreadId) => {
30130
+ const attachThreadSessionWithCodex = async (args) => {
30131
+ const { control, cwd, codexThreadId, sessionCodex } = args;
30132
+ const portForwardWatcherRootPid = args.portForwardWatcherRootPid;
29775
30133
  const workspaceSlug = optionalThreadControlField(control, "workspace");
29776
30134
  const channelSlug = optionalThreadControlField(control, "channel");
29777
30135
  const kandanThreadId = optionalThreadControlField(control, "threadId");
@@ -29791,7 +30149,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29791
30149
  const runtimeSettings = startInstanceRuntimeSettings(options, control);
29792
30150
  const session = await attachChannelSession({
29793
30151
  kandan,
29794
- codex,
30152
+ codex: sessionCodex,
29795
30153
  topic,
29796
30154
  instanceId,
29797
30155
  options: {
@@ -29805,7 +30163,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29805
30163
  launchTui: false,
29806
30164
  enablePortForwardWatch: true,
29807
30165
  initialForwardPorts: allowedForwardPorts,
29808
- portForwardWatcher: started?.process.pid === void 0 ? void 0 : { rootPid: started.process.pid },
30166
+ portForwardWatcher: channelSessionPortForwardWatcherOptions({
30167
+ rootPid: portForwardWatcherRootPid,
30168
+ start: options.portForwardWatcher
30169
+ }),
29809
30170
  suppressedForwardPorts,
29810
30171
  onForwardPortApproved: (port, attribution) => {
29811
30172
  liveForwardPorts.add(port);
@@ -29838,12 +30199,37 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29838
30199
  log
29839
30200
  });
29840
30201
  dynamicChannelSessions.set(kandanThreadId, session);
30202
+ if (sessionCodex !== codex) {
30203
+ sessionCodex.onNotification((notification) => {
30204
+ seq.value += 1;
30205
+ const params = notification.params ?? {};
30206
+ const metadata = extractCodexIds(params);
30207
+ log("codex.notification", {
30208
+ method: notification.method,
30209
+ metadata
30210
+ });
30211
+ session.handleCodexNotification(notification.method, params);
30212
+ });
30213
+ }
29841
30214
  return session;
29842
30215
  };
30216
+ const attachThreadSession = async (control, cwd, codexThreadId) => {
30217
+ return await attachThreadSessionWithCodex({
30218
+ control,
30219
+ cwd,
30220
+ codexThreadId,
30221
+ sessionCodex: codex,
30222
+ portForwardWatcherRootPid: started?.process.pid
30223
+ });
30224
+ };
29843
30225
  const startThreadRunnerProcess = async (control, cwd) => {
29844
30226
  if (options.threadProcess?.role === "thread") {
29845
30227
  return void 0;
29846
30228
  }
30229
+ const agentProviderResult = startInstanceAgentProvider(control);
30230
+ if (agentProviderResult.type !== "ok" || agentProviderResult.provider !== "codex") {
30231
+ return void 0;
30232
+ }
29847
30233
  const workspaceSlug = optionalThreadControlField(control, "workspace");
29848
30234
  const channelSlug = optionalThreadControlField(control, "channel");
29849
30235
  const kandanThreadId = optionalThreadControlField(control, "threadId");
@@ -29863,17 +30249,24 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29863
30249
  };
29864
30250
  }
29865
30251
  const spawnThreadRunner = options.spawnThreadRunner ?? spawnLocalThreadRunnerProcess;
30252
+ const readyTimeoutMs = threadRunnerReadyTimeoutMs(options);
29866
30253
  const startingEntry = {
29867
30254
  kind: "starting",
29868
- promise: spawnThreadRunner(
29869
- threadRunnerOptions({
29870
- options,
29871
- control,
29872
- cwd,
29873
- workspaceSlug,
29874
- channelSlug,
29875
- kandanThreadId
29876
- })
30255
+ promise: withThreadRunnerReadyTimeout(
30256
+ spawnThreadRunner(
30257
+ threadRunnerOptions({
30258
+ options,
30259
+ control,
30260
+ cwd,
30261
+ workspaceSlug,
30262
+ channelSlug,
30263
+ kandanThreadId
30264
+ })
30265
+ ),
30266
+ {
30267
+ kandanThreadId,
30268
+ timeoutMs: readyTimeoutMs
30269
+ }
29877
30270
  )
29878
30271
  };
29879
30272
  threadRunnerProcesses.set(kandanThreadId, startingEntry);
@@ -29886,31 +30279,84 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29886
30279
  }
29887
30280
  throw error51;
29888
30281
  }
29889
- const runningEntry = {
29890
- kind: "running",
29891
- handle
29892
- };
29893
- if (threadRunnerProcesses.get(kandanThreadId) === startingEntry) {
29894
- threadRunnerProcesses.set(kandanThreadId, runningEntry);
29895
- }
29896
- handle.onExit?.(() => {
29897
- if (threadRunnerProcesses.get(kandanThreadId) === runningEntry) {
30282
+ try {
30283
+ const threadCodex = handle.codex;
30284
+ const closeThreadCodex = once(() => threadCodex.close());
30285
+ handle.onExit?.(closeThreadCodex);
30286
+ const runtimeHandle = {
30287
+ ...handle,
30288
+ close: async () => {
30289
+ closeThreadCodex();
30290
+ await handle.close();
30291
+ }
30292
+ };
30293
+ const runningEntry = {
30294
+ kind: "running",
30295
+ handle: runtimeHandle
30296
+ };
30297
+ if (threadRunnerProcesses.get(kandanThreadId) === startingEntry) {
30298
+ threadRunnerProcesses.set(kandanThreadId, runningEntry);
30299
+ }
30300
+ handle.onExit?.(() => {
30301
+ if (threadRunnerProcesses.get(kandanThreadId) === runningEntry) {
30302
+ threadRunnerProcesses.delete(kandanThreadId);
30303
+ }
30304
+ });
30305
+ log("runner.thread_process_started", {
30306
+ kandanThreadId,
30307
+ cwd
30308
+ });
30309
+ const onStartedThread = (startedControl, startedCwd, codexThreadId) => attachThreadSessionWithCodex({
30310
+ control: startedControl,
30311
+ cwd: startedCwd,
30312
+ codexThreadId,
30313
+ sessionCodex: threadCodex,
30314
+ portForwardWatcherRootPid: handle.processPid
30315
+ });
30316
+ switch (control.type) {
30317
+ case "start_instance": {
30318
+ const result = await startCodexProviderInstance({
30319
+ codex: threadCodex,
30320
+ kandan,
30321
+ topic,
30322
+ control,
30323
+ options,
30324
+ instanceId,
30325
+ cwd,
30326
+ matchedRoot: cwd,
30327
+ onStartedThread,
30328
+ setStartupStage: () => void 0,
30329
+ setStartedCodexThreadId: () => void 0
30330
+ });
30331
+ return result.controlResponse;
30332
+ }
30333
+ case "reconnect_thread":
30334
+ return await applyControl(
30335
+ threadCodex,
30336
+ kandan,
30337
+ topic,
30338
+ instanceId,
30339
+ options,
30340
+ agentProviders,
30341
+ allowedCwds.value,
30342
+ activeClaudeCodeSessions,
30343
+ pendingClaudeCodeApprovals,
30344
+ ensureClaudeCodeForwardPortSession,
30345
+ disposeClaudeCodeForwardPortSession,
30346
+ control,
30347
+ log,
30348
+ onStartedThread,
30349
+ void 0
30350
+ );
30351
+ }
30352
+ } catch (error51) {
30353
+ const entry = threadRunnerProcesses.get(kandanThreadId);
30354
+ if (entry === startingEntry || entry?.kind === "running") {
29898
30355
  threadRunnerProcesses.delete(kandanThreadId);
29899
30356
  }
29900
- });
29901
- log("runner.thread_process_started", {
29902
- kandanThreadId,
29903
- cwd
29904
- });
29905
- return {
29906
- instanceId,
29907
- controlType: control.type,
29908
- ok: true,
29909
- delegated: true,
29910
- threadProcess: "started",
29911
- kandanThreadId,
29912
- cwd
29913
- };
30357
+ await (entry?.kind === "running" ? entry.handle.close() : handle.close()).catch(() => void 0);
30358
+ throw error51;
30359
+ }
29914
30360
  };
29915
30361
  const heartbeatPayload = () => ({
29916
30362
  instanceId,
@@ -30228,12 +30674,6 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
30228
30674
  };
30229
30675
  controlDispatcher.value = handleControl;
30230
30676
  pendingControls.splice(0).forEach(handleControl);
30231
- if (options.threadProcess?.role === "thread") {
30232
- const initialControl = options.threadProcess.initialControl;
30233
- if (initialControl !== void 0) {
30234
- handleControl(initialControl);
30235
- }
30236
- }
30237
30677
  return { instanceId, codexUrl, close };
30238
30678
  }
30239
30679
  function controlTargetsInstance(control, instanceId) {
@@ -32551,6 +32991,15 @@ function startInstanceRuntimeSettings(options, control) {
32551
32991
  allowPortForwardingByDefault: control.allowPortForwardingByDefault ?? defaults.allowPortForwardingByDefault
32552
32992
  };
32553
32993
  }
32994
+ function channelSessionPortForwardWatcherOptions(args) {
32995
+ if (args.rootPid === void 0 && args.start === void 0) {
32996
+ return void 0;
32997
+ }
32998
+ return {
32999
+ ...args.rootPid === void 0 ? {} : { rootPid: args.rootPid },
33000
+ ...args.start === void 0 ? {} : { start: args.start }
33001
+ };
33002
+ }
32554
33003
  function threadRunnerOptions(args) {
32555
33004
  const runtimeSettings = startInstanceRuntimeSettings(
32556
33005
  args.options,
@@ -32558,7 +33007,7 @@ function threadRunnerOptions(args) {
32558
33007
  );
32559
33008
  return {
32560
33009
  ...args.options,
32561
- clientId: `${args.options.clientId ?? args.options.machineId ?? args.options.runnerId}:thread:${args.kandanThreadId}`,
33010
+ clientId: void 0,
32562
33011
  machineId: void 0,
32563
33012
  runnerLockConfigPath: void 0,
32564
33013
  workspaceSlug: args.workspaceSlug,
@@ -32570,10 +33019,10 @@ function threadRunnerOptions(args) {
32570
33019
  editorRuntime: void 0,
32571
33020
  threadProcess: {
32572
33021
  role: "thread",
32573
- kandanThreadId: args.kandanThreadId,
32574
- initialControl: args.control
33022
+ kandanThreadId: args.kandanThreadId
32575
33023
  },
32576
33024
  spawnThreadRunner: void 0,
33025
+ threadRunnerReadyTimeoutMs: args.options.threadRunnerReadyTimeoutMs,
32577
33026
  runtimeDefaults: {
32578
33027
  model: runtimeSettings.model,
32579
33028
  reasoningEffort: runtimeSettings.reasoningEffort,
@@ -32601,6 +33050,7 @@ async function closeThreadRunnerEntry(entry) {
32601
33050
  }
32602
33051
  async function spawnLocalThreadRunnerProcess(options) {
32603
33052
  const scriptPath = process.argv[1];
33053
+ const kandanThreadId = options.threadProcess?.kandanThreadId ?? "";
32604
33054
  if (scriptPath === void 0 || scriptPath.trim() === "") {
32605
33055
  throw new Error(
32606
33056
  "cannot fork thread runner without current CLI script path"
@@ -32610,14 +33060,7 @@ async function spawnLocalThreadRunnerProcess(options) {
32610
33060
  const env = {
32611
33061
  ...process.env,
32612
33062
  LINZUMI_THREAD_RUNNER_ROLE: "thread",
32613
- LINZUMI_THREAD_RUNNER_TOKEN: options.token,
32614
- LINZUMI_THREAD_RUNNER_CLIENT_ID: options.clientId ?? `${options.runnerId}:thread`,
32615
- LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID: options.threadProcess?.kandanThreadId ?? "",
32616
- ...options.threadProcess?.initialControl === void 0 ? {} : {
32617
- LINZUMI_THREAD_RUNNER_INITIAL_CONTROL: JSON.stringify(
32618
- options.threadProcess.initialControl
32619
- )
32620
- }
33063
+ LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID: kandanThreadId
32621
33064
  };
32622
33065
  writeCliAuditEvent("process.spawn", {
32623
33066
  command: process.execPath,
@@ -32628,7 +33071,7 @@ async function spawnLocalThreadRunnerProcess(options) {
32628
33071
  const child = spawn6(process.execPath, [scriptPath, ...args], {
32629
33072
  cwd: options.cwd,
32630
33073
  env,
32631
- stdio: "inherit"
33074
+ stdio: ["inherit", "inherit", "inherit", "ipc"]
32632
33075
  });
32633
33076
  writeCliAuditEvent("process.spawned", {
32634
33077
  command: process.execPath,
@@ -32655,8 +33098,64 @@ async function spawnLocalThreadRunnerProcess(options) {
32655
33098
  }
32656
33099
  exitListeners.clear();
32657
33100
  });
33101
+ const ready2 = await new Promise((resolve11, reject) => {
33102
+ let settled = false;
33103
+ let timeout;
33104
+ const cleanupReadyListeners = () => {
33105
+ if (timeout !== void 0) {
33106
+ clearTimeout(timeout);
33107
+ }
33108
+ child.off("message", onMessage);
33109
+ child.off("exit", onExitBeforeReady);
33110
+ };
33111
+ const settle = (action) => {
33112
+ if (settled) {
33113
+ return;
33114
+ }
33115
+ settled = true;
33116
+ cleanupReadyListeners();
33117
+ action();
33118
+ };
33119
+ const onMessage = (message) => {
33120
+ const parsed = (() => {
33121
+ try {
33122
+ return threadRunnerReadyMessage(message, kandanThreadId);
33123
+ } catch (error51) {
33124
+ settle(
33125
+ () => reject(error51 instanceof Error ? error51 : new Error(String(error51)))
33126
+ );
33127
+ return void 0;
33128
+ }
33129
+ })();
33130
+ if (parsed === void 0) {
33131
+ return;
33132
+ }
33133
+ settle(() => resolve11(parsed));
33134
+ };
33135
+ const onExitBeforeReady = (code, signal) => {
33136
+ settle(
33137
+ () => reject(
33138
+ new Error(
33139
+ `thread Codex worker exited before ready: code=${code ?? "null"} signal=${signal ?? "null"}`
33140
+ )
33141
+ )
33142
+ );
33143
+ };
33144
+ const onReadyTimeout = () => {
33145
+ settle(() => {
33146
+ child.kill("SIGINT");
33147
+ reject(threadRunnerReadyTimeoutError(kandanThreadId, readyTimeoutMs));
33148
+ });
33149
+ };
33150
+ child.on("message", onMessage);
33151
+ child.once("exit", onExitBeforeReady);
33152
+ const readyTimeoutMs = threadRunnerReadyTimeoutMs(options);
33153
+ timeout = setTimeout(onReadyTimeout, readyTimeoutMs);
33154
+ });
32658
33155
  return {
32659
- kandanThreadId: options.threadProcess?.kandanThreadId ?? "",
33156
+ kandanThreadId: ready2.kandanThreadId,
33157
+ codex: connectThreadCodexWorkerIpc(child),
33158
+ processPid: child.pid,
32660
33159
  onExit: (listener) => {
32661
33160
  if (exitState.exited) {
32662
33161
  queueMicrotask(listener);
@@ -32674,6 +33173,78 @@ async function spawnLocalThreadRunnerProcess(options) {
32674
33173
  })
32675
33174
  };
32676
33175
  }
33176
+ function threadRunnerReadyMessage(message, expectedKandanThreadId) {
33177
+ if (!isJsonObject(message)) {
33178
+ return void 0;
33179
+ }
33180
+ if (message.type !== "linzumi_thread_codex_worker_ready") {
33181
+ return void 0;
33182
+ }
33183
+ const kandanThreadId = stringValue(message.kandanThreadId);
33184
+ if (kandanThreadId !== expectedKandanThreadId) {
33185
+ throw new Error("thread Codex worker reported invalid ready payload");
33186
+ }
33187
+ return { kandanThreadId };
33188
+ }
33189
+ function waitForThreadCodexWorkerStop(codexProcess) {
33190
+ return new Promise((resolve11) => {
33191
+ const stop = once(() => {
33192
+ process.off("disconnect", stop);
33193
+ process.off("SIGINT", stop);
33194
+ process.off("SIGTERM", stop);
33195
+ codexProcess.off("exit", stop);
33196
+ resolve11();
33197
+ });
33198
+ process.once("disconnect", stop);
33199
+ process.once("SIGINT", stop);
33200
+ process.once("SIGTERM", stop);
33201
+ codexProcess.once("exit", stop);
33202
+ });
33203
+ }
33204
+ function withThreadRunnerReadyTimeout(promise2, args) {
33205
+ return new Promise((resolve11, reject) => {
33206
+ let settled = false;
33207
+ const timeout = setTimeout(() => {
33208
+ if (settled) {
33209
+ return;
33210
+ }
33211
+ settled = true;
33212
+ reject(
33213
+ threadRunnerReadyTimeoutError(args.kandanThreadId, args.timeoutMs)
33214
+ );
33215
+ }, args.timeoutMs);
33216
+ promise2.then(
33217
+ (handle) => {
33218
+ if (settled) {
33219
+ return;
33220
+ }
33221
+ settled = true;
33222
+ clearTimeout(timeout);
33223
+ resolve11(handle);
33224
+ },
33225
+ (error51) => {
33226
+ if (settled) {
33227
+ return;
33228
+ }
33229
+ settled = true;
33230
+ clearTimeout(timeout);
33231
+ reject(error51);
33232
+ }
33233
+ );
33234
+ });
33235
+ }
33236
+ function threadRunnerReadyTimeoutError(kandanThreadId, timeoutMs) {
33237
+ return new Error(
33238
+ `thread Codex worker did not report ready within ${timeoutMs}ms for Kandan thread ${kandanThreadId}`
33239
+ );
33240
+ }
33241
+ function threadRunnerReadyTimeoutMs(options) {
33242
+ const timeoutMs = options.threadRunnerReadyTimeoutMs ?? THREAD_RUNNER_READY_TIMEOUT_MS;
33243
+ if (Number.isFinite(timeoutMs) && timeoutMs >= 0) {
33244
+ return timeoutMs;
33245
+ }
33246
+ throw new Error("thread runner ready timeout must be a non-negative number");
33247
+ }
32677
33248
  function threadRunnerCliArgs(options) {
32678
33249
  return [
32679
33250
  "--api-url",
@@ -32722,17 +33293,17 @@ function redactedThreadRunnerCliArgs(args) {
32722
33293
  function optionalCliValue(flag, value) {
32723
33294
  return value === void 0 || value === "" ? [] : [flag, value];
32724
33295
  }
32725
- async function startOwnedCodexAppServer(options) {
33296
+ async function startOwnedCodexAppServer(options, args = { linzumiMcp: true }) {
32726
33297
  ensureCodexProjectTrusted(options.cwd);
32727
33298
  const defaults = runnerRuntimeDefaults(options);
32728
- const mcpAuth = writeEphemeralMcpAuthFile(options);
33299
+ const mcpAuth = args.linzumiMcp === true ? writeEphemeralMcpAuthFile(options) : void 0;
32729
33300
  const mcpCommand = currentLinzumiCommand();
32730
33301
  try {
32731
33302
  const started = await startCodexAppServer(options.codexBin, options.cwd, {
32732
33303
  model: defaults.model,
32733
33304
  reasoningEffort: defaults.reasoningEffort,
32734
33305
  fast: options.fast,
32735
- mcpServers: [
33306
+ mcpServers: mcpAuth === void 0 ? [] : [
32736
33307
  linzumiMcpServerConfig({
32737
33308
  command: mcpCommand.command,
32738
33309
  argsPrefix: mcpCommand.argsPrefix,
@@ -32742,7 +33313,7 @@ async function startOwnedCodexAppServer(options) {
32742
33313
  })
32743
33314
  ]
32744
33315
  });
32745
- const cleanup = once(mcpAuth.cleanup);
33316
+ const cleanup = once(() => mcpAuth?.cleanup());
32746
33317
  started.process.once("exit", cleanup);
32747
33318
  return {
32748
33319
  ...started,
@@ -32752,7 +33323,7 @@ async function startOwnedCodexAppServer(options) {
32752
33323
  }
32753
33324
  };
32754
33325
  } catch (error51) {
32755
- mcpAuth.cleanup();
33326
+ mcpAuth?.cleanup();
32756
33327
  throw error51;
32757
33328
  }
32758
33329
  }
@@ -63133,24 +63704,22 @@ async function main(args) {
63133
63704
  return;
63134
63705
  case "agentRunner": {
63135
63706
  const options = await parseAgentRunnerArgs(parsed.args);
63136
- await runLocalCodexRunner(
63137
- withLocalMachineId(withThreadRunnerEnv(options))
63138
- );
63707
+ await runLocalCodexRunner(withLocalMachineId(options));
63139
63708
  return;
63140
63709
  }
63141
63710
  case "start": {
63142
63711
  const options = await parseStartRunnerArgs(parsed.args);
63143
63712
  addAllowedCwdForLinzumiUrl(options.cwd, options.kandanUrl);
63144
- await runLocalCodexRunner(
63145
- withLocalMachineId(withThreadRunnerEnv(options))
63146
- );
63713
+ await runLocalCodexRunner(withLocalMachineId(options));
63147
63714
  return;
63148
63715
  }
63149
63716
  case "run": {
63717
+ if (process.env.LINZUMI_THREAD_RUNNER_ROLE === "thread") {
63718
+ await runThreadCodexWorker(parseThreadCodexWorkerArgs(parsed.args));
63719
+ return;
63720
+ }
63150
63721
  const options = await parseRunnerArgs(parsed.args);
63151
- await runLocalCodexRunner(
63152
- withLocalMachineId(withThreadRunnerEnv(options))
63153
- );
63722
+ await runLocalCodexRunner(withLocalMachineId(options));
63154
63723
  return;
63155
63724
  }
63156
63725
  }
@@ -63779,7 +64348,13 @@ async function parseRunnerArgs(args, deps = {
63779
64348
  ) : [...localConfiguredAllowedCwds.allowedCwds];
63780
64349
  const codexBin = stringValue5(values, "codex-bin") ?? "codex";
63781
64350
  const customCodeServerBin = stringValue5(values, "code-server-bin");
63782
- const explicitToken = stringValue5(values, "token") ?? threadRunnerTokenEnv();
64351
+ const initialDependencyStatus = await deps.buildDependencyStatus({
64352
+ cwd,
64353
+ codexBin,
64354
+ codeServerBin: customCodeServerBin
64355
+ });
64356
+ assertStartDependencies(initialDependencyStatus);
64357
+ const explicitToken = stringValue5(values, "token");
63783
64358
  const token = await deps.resolveToken({
63784
64359
  kandanUrl,
63785
64360
  explicitToken,
@@ -64023,57 +64598,51 @@ function withLocalMachineId(options) {
64023
64598
  machineId: localConfigScopeKey(options.kandanUrl) === localConfigScopeKey(defaultLinzumiWebSocketUrl) ? ensureLocalMachineId() : ensureLocalMachineIdForLinzumiUrl(options.kandanUrl)
64024
64599
  };
64025
64600
  }
64026
- function withThreadRunnerEnv(options) {
64027
- if (process.env.LINZUMI_THREAD_RUNNER_ROLE !== "thread") {
64028
- return options;
64029
- }
64030
- const kandanThreadId = process.env.LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID;
64031
- if (kandanThreadId === void 0 || kandanThreadId.trim() === "") {
64032
- throw new Error(
64033
- "thread runner is missing LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID"
64034
- );
64035
- }
64601
+ function parseThreadCodexWorkerArgs(args) {
64602
+ const values = strictFlagValues2(args);
64603
+ const kandanUrl = kandanUrlValue(values) ?? defaultLinzumiWebSocketUrl;
64604
+ const cwd = stringValue5(values, "cwd") ?? process.cwd();
64605
+ const configuredAllowedCwds2 = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(
64606
+ parseAllowedCwdList(stringValue5(values, "allowed-cwd"))
64607
+ ) : assertConfiguredAllowedCwds([cwd]);
64608
+ const kandanThreadId = requiredThreadRunnerKandanThreadId();
64036
64609
  return {
64037
- ...options,
64038
- clientId: process.env.LINZUMI_THREAD_RUNNER_CLIENT_ID,
64610
+ kandanUrl,
64611
+ token: "",
64612
+ runnerId: `thread-codex-worker:${kandanThreadId}`,
64039
64613
  machineId: void 0,
64040
64614
  runnerLockConfigPath: void 0,
64615
+ workspaceSlug: stringValue5(values, "workspace"),
64616
+ cwd,
64617
+ codexBin: stringValue5(values, "codex-bin") ?? "codex",
64041
64618
  codexUrl: void 0,
64042
64619
  launchTui: false,
64620
+ fast: values.get("fast") === true ? true : void 0,
64621
+ logFile: stringValue5(values, "log-file"),
64622
+ allowedCwds: configuredAllowedCwds2.allowedCwds,
64623
+ missingAllowedCwds: configuredAllowedCwds2.missingAllowedCwds,
64624
+ allowedForwardPorts: parseAllowedPortList(
64625
+ stringValue5(values, "forward-port")
64626
+ ),
64627
+ codeServerBin: void 0,
64628
+ editorRuntime: void 0,
64629
+ socketFactory: void 0,
64630
+ runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
64043
64631
  threadProcess: {
64044
64632
  role: "thread",
64045
- kandanThreadId,
64046
- initialControl: initialThreadControlFromEnv()
64633
+ kandanThreadId
64047
64634
  },
64048
64635
  channelSession: void 0
64049
64636
  };
64050
64637
  }
64051
- function threadRunnerTokenEnv() {
64052
- if (process.env.LINZUMI_THREAD_RUNNER_ROLE !== "thread") {
64053
- return void 0;
64054
- }
64055
- const token = process.env.LINZUMI_THREAD_RUNNER_TOKEN;
64056
- if (token === void 0 || token.trim() === "") {
64057
- throw new Error("thread runner is missing LINZUMI_THREAD_RUNNER_TOKEN");
64058
- }
64059
- return token;
64060
- }
64061
- function initialThreadControlFromEnv() {
64062
- const raw = process.env.LINZUMI_THREAD_RUNNER_INITIAL_CONTROL;
64063
- if (raw === void 0 || raw.trim() === "") {
64064
- return void 0;
64065
- }
64066
- const parsed = JSON.parse(raw);
64067
- if (parsed === null || typeof parsed !== "object" || !("type" in parsed)) {
64068
- throw new Error("thread runner initial control is invalid");
64069
- }
64070
- switch (parsed.type) {
64071
- case "start_instance":
64072
- case "reconnect_thread":
64073
- return parsed;
64074
- default:
64075
- throw new Error("thread runner initial control type is invalid");
64638
+ function requiredThreadRunnerKandanThreadId() {
64639
+ const kandanThreadId = process.env.LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID;
64640
+ if (kandanThreadId === void 0 || kandanThreadId.trim() === "") {
64641
+ throw new Error(
64642
+ "thread runner is missing LINZUMI_THREAD_RUNNER_KANDAN_THREAD_ID"
64643
+ );
64076
64644
  }
64645
+ return kandanThreadId;
64077
64646
  }
64078
64647
  function required3(values, key) {
64079
64648
  const value = stringValue5(values, key);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.59-beta",
3
+ "version": "0.0.61-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": {