@linzumi/cli 0.0.36-beta → 0.0.38-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 +115 -48
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -63,7 +63,7 @@ Install the CLI or run it with `npx`:
63
63
 
64
64
  ```bash
65
65
  npm install -g @linzumi/cli@latest
66
- npx -y @linzumi/cli@0.0.36-beta --version
66
+ npx -y @linzumi/cli@0.0.38-beta --version
67
67
  linzumi --version
68
68
  ```
69
69
 
package/dist/index.js CHANGED
@@ -1481,7 +1481,7 @@ var maxForwardedTurnIds = 64;
1481
1481
  async function attachChannelSession(args) {
1482
1482
  const session = args.options.channelSession;
1483
1483
  const chatTopic = `chat:${session.workspaceSlug}:${session.channelSlug}`;
1484
- const state = initialChannelSessionState(0, session.kandanThreadId, args.options);
1484
+ const state = initialChannelSessionState(0, session.rootSeq, session.kandanThreadId, session.codexThreadId, args.options);
1485
1485
  const joined = await args.kandan.join(chatTopic, { last_seq: 0 }, {
1486
1486
  rejoinPayload: () => ({ last_seq: state.minSeq })
1487
1487
  });
@@ -1599,6 +1599,7 @@ async function attachChannelSession(args) {
1599
1599
  }
1600
1600
  },
1601
1601
  handleControl: (control) => handleChannelSessionControl(args, state, payloadContext, control),
1602
+ startThreadMessageTurn: (message) => startThreadMessageTurn(args, state, payloadContext, message),
1602
1603
  currentRuntimeSettings: () => state.runtimeSettings,
1603
1604
  currentCodexThreadId: () => state.codexThreadId,
1604
1605
  currentKandanThreadId: () => state.kandanThreadId,
@@ -1641,11 +1642,11 @@ async function bindCurrentCodexThread(args, state) {
1641
1642
  instance_id: args.instanceId
1642
1643
  });
1643
1644
  }
1644
- function initialChannelSessionState(cursor, kandanThreadId, options) {
1645
+ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThreadId, options) {
1645
1646
  return {
1646
- rootSeq: undefined,
1647
+ rootSeq,
1647
1648
  kandanThreadId,
1648
- codexThreadId: undefined,
1649
+ codexThreadId,
1649
1650
  turn: { status: "idle" },
1650
1651
  closed: false,
1651
1652
  minSeq: cursor,
@@ -1682,7 +1683,7 @@ function initialChannelSessionState(cursor, kandanThreadId, options) {
1682
1683
  };
1683
1684
  }
1684
1685
  function startPortForwardWatchIfEnabled(args, state, payloadContext) {
1685
- if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined) {
1686
+ if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined || state.kandanThreadId === undefined || state.codexThreadId === undefined) {
1686
1687
  return;
1687
1688
  }
1688
1689
  const { start: configuredStart, ...watchOptions } = args.options.portForwardWatcher ?? {};
@@ -1712,6 +1713,8 @@ async function bindChannelSession(args, state, payloadContext) {
1712
1713
  if (state.rootSeq !== undefined) {
1713
1714
  state.minSeq = Math.max(state.minSeq, state.rootSeq);
1714
1715
  }
1716
+ } else if (state.codexThreadId !== undefined) {
1717
+ await bindCurrentCodexThread(args, state);
1715
1718
  } else {
1716
1719
  const resolved = await pushOk(args.kandan, args.topic, "session:resolve_thread_session", {
1717
1720
  workspace: session.workspaceSlug,
@@ -1723,13 +1726,7 @@ async function bindChannelSession(args, state, payloadContext) {
1723
1726
  if (state.codexThreadId === undefined) {
1724
1727
  throw new Error("Kandan thread root metadata did not include a Codex thread id");
1725
1728
  }
1726
- await pushOk(args.kandan, args.topic, "session:post_thread_message", {
1727
- workspace: session.workspaceSlug,
1728
- channel: session.channelSlug,
1729
- thread_id: state.kandanThreadId,
1730
- body: availabilityMessage(args.options, codexVersion, state.codexThreadId),
1731
- payload: localRunnerPayload(args.options, args.instanceId, "availability", state.codexThreadId, payloadContext)
1732
- });
1729
+ await bindCurrentCodexThread(args, state);
1733
1730
  }
1734
1731
  }
1735
1732
  async function handleChannelSessionControl(args, state, payloadContext, control) {
@@ -1745,7 +1742,10 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
1745
1742
  if (control.type !== "interrupt_queued_messages") {
1746
1743
  return;
1747
1744
  }
1748
- if (state.codexThreadId === undefined || state.kandanThreadId === undefined || control.threadId !== state.codexThreadId) {
1745
+ if (state.codexThreadId !== control.threadId) {
1746
+ return;
1747
+ }
1748
+ if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
1749
1749
  return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
1750
1750
  }
1751
1751
  const interrupted = interruptPendingKandanMessages(state.queue, control.throughSeq);
@@ -1795,7 +1795,10 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
1795
1795
  };
1796
1796
  }
1797
1797
  function updateSessionSettings(args, state, control) {
1798
- if (state.codexThreadId === undefined || control.threadId !== state.codexThreadId) {
1798
+ if (state.codexThreadId !== control.threadId) {
1799
+ return;
1800
+ }
1801
+ if (state.codexThreadId === undefined) {
1799
1802
  return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
1800
1803
  }
1801
1804
  state.runtimeSettings = mergeRuntimeSettings(state.runtimeSettings, control);
@@ -1822,7 +1825,10 @@ function updateSessionSettings(args, state, control) {
1822
1825
  };
1823
1826
  }
1824
1827
  async function resolvePendingCodexApprovalRequest(args, state, control) {
1825
- if (state.codexThreadId === undefined || state.kandanThreadId === undefined || control.threadId !== state.codexThreadId) {
1828
+ if (state.codexThreadId !== control.threadId) {
1829
+ return;
1830
+ }
1831
+ if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
1826
1832
  return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
1827
1833
  }
1828
1834
  const approval = state.pendingApprovalRequests.get(approvalRequestKey(control.requestId, control.sourceSeq));
@@ -1836,7 +1842,7 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
1836
1842
  await publishMessageState(args, state.kandanThreadId, approval.sourceSeq, {
1837
1843
  status: "processing",
1838
1844
  reason: "streaming response"
1839
- });
1845
+ }, undefined, undefined, state.codexThreadId);
1840
1846
  args.log("codex.approval_request_resolved", {
1841
1847
  request_id: control.requestId,
1842
1848
  source_seq: control.sourceSeq,
@@ -1846,6 +1852,10 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
1846
1852
  return { instanceId: args.instanceId, ok: true };
1847
1853
  }
1848
1854
  async function resolvePendingPortForwardRequest(args, state, payloadContext, control) {
1855
+ const request = state.pendingPortForwardRequests.get(control.requestId);
1856
+ if (request === undefined) {
1857
+ return;
1858
+ }
1849
1859
  if (!portForwardControlSenderAllowed(args, payloadContext, control)) {
1850
1860
  args.log("port_forward.request_resolution_ignored", {
1851
1861
  request_id: control.requestId,
@@ -1855,10 +1865,6 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1855
1865
  });
1856
1866
  return { instanceId: args.instanceId, ok: false, error: "sender_not_allowed" };
1857
1867
  }
1858
- const request = state.pendingPortForwardRequests.get(control.requestId);
1859
- if (request === undefined) {
1860
- return { instanceId: args.instanceId, ok: false, error: "port_forward_request_not_found" };
1861
- }
1862
1868
  state.pendingPortForwardRequests.delete(control.requestId);
1863
1869
  if (control.decision === "deny") {
1864
1870
  state.dismissedForwardTargets.set(request.port, approvedTargetFromRequest(request));
@@ -2175,6 +2181,7 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
2175
2181
  state.kandanThreadId = event.threadId;
2176
2182
  }
2177
2183
  }
2184
+ startPortForwardWatchIfEnabled(args, state, payloadContext);
2178
2185
  if (event.threadId !== state.kandanThreadId) {
2179
2186
  args.log("kandan.message_ignored", {
2180
2187
  seq: event.seq,
@@ -2204,6 +2211,27 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
2204
2211
  await publishKandanMessageState(args, event, { status: "queued" });
2205
2212
  await drainKandanMessageQueue(args, state, payloadContext);
2206
2213
  }
2214
+ async function startThreadMessageTurn(args, state, payloadContext, message) {
2215
+ if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
2216
+ throw new Error("cannot start a local Codex turn before thread binding");
2217
+ }
2218
+ const queued = {
2219
+ seq: message.seq,
2220
+ actorSlug: message.actorSlug,
2221
+ actorUserId: message.actorUserId,
2222
+ body: message.body,
2223
+ attachments: []
2224
+ };
2225
+ enqueuePendingKandanMessage(state.queue, queued);
2226
+ args.log("kandan.message_queued", {
2227
+ seq: queued.seq,
2228
+ actor_slug: queued.actorSlug ?? null,
2229
+ actor_user_id: queued.actorUserId ?? null,
2230
+ queue_depth: pendingKandanMessageQueueLength(state.queue)
2231
+ });
2232
+ await publishQueuedMessageState(args, state, queued, { status: "queued" });
2233
+ await drainKandanMessageQueue(args, state, payloadContext);
2234
+ }
2207
2235
  async function bindUnboundHistoricalThread(args, state, event) {
2208
2236
  if (event.threadId === undefined || event.replyToSeq === undefined) {
2209
2237
  return false;
@@ -2362,7 +2390,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2362
2390
  }
2363
2391
  async function handleCodexServerRequest(args, state, payloadContext, request) {
2364
2392
  const params = objectValue(request.params) ?? {};
2365
- const turnId = stringValue(params.turnId);
2393
+ const turnId = codexNotificationTurnId(params);
2394
+ const threadId = codexNotificationThreadId(params);
2395
+ if (state.closed || !codexNotificationBelongsToSession(state, threadId, turnId)) {
2396
+ return;
2397
+ }
2366
2398
  if (codexApprovalRequestCanAutoAccept(state.runtimeSettings, request.method)) {
2367
2399
  args.log("codex.server_request_auto_accepted", {
2368
2400
  method: request.method,
@@ -2405,7 +2437,7 @@ async function requestKandanApproval(args, state, request, turnId, payloadContex
2405
2437
  status: "processing",
2406
2438
  reason: "awaiting approval",
2407
2439
  approval
2408
- });
2440
+ }, undefined, undefined, state.codexThreadId);
2409
2441
  args.log("codex.approval_request_pending", {
2410
2442
  request_id: approval.requestId,
2411
2443
  source_seq: sourceSeq,
@@ -3576,12 +3608,13 @@ async function publishQueuedMessageState(args, state, message, messageState) {
3576
3608
  }
3577
3609
  await publishMessageState(args, state.kandanThreadId, message.seq, messageState, message.actorSlug, message.actorUserId);
3578
3610
  }
3579
- async function publishMessageState(args, threadId, seq, state, actorSlug, actorUserId) {
3611
+ async function publishMessageState(args, threadId, seq, state, actorSlug, actorUserId, codexThreadId) {
3580
3612
  const session = args.options.channelSession;
3581
3613
  const payload = {
3582
3614
  workspace: session.workspaceSlug,
3583
3615
  channel: session.channelSlug,
3584
3616
  thread_id: threadId,
3617
+ ...codexThreadId === undefined ? {} : { codex_thread_id: codexThreadId },
3585
3618
  seq,
3586
3619
  status: state.status,
3587
3620
  ..."reason" in state ? { reason: state.reason } : {},
@@ -3639,7 +3672,7 @@ async function refreshActiveProcessingHeartbeat(args, state) {
3639
3672
  if (activeProcessingState === undefined || state.kandanThreadId === undefined) {
3640
3673
  return;
3641
3674
  }
3642
- await publishMessageState(args, state.kandanThreadId, activeProcessingState.seq, processingMessageStateFromActive(activeProcessingState));
3675
+ await publishMessageState(args, state.kandanThreadId, activeProcessingState.seq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
3643
3676
  }
3644
3677
  function clearActiveProcessingState(state, seq) {
3645
3678
  if (state.activeProcessingState?.seq === seq) {
@@ -4305,12 +4338,21 @@ async function respondToServerRequest(websocket, request, callbacks) {
4305
4338
  }));
4306
4339
  return;
4307
4340
  }
4308
- const callback = Array.from(callbacks)[0];
4309
- if (callback === undefined) {
4310
- throw new Error(`unhandled Codex app-server request: ${request.method}`);
4341
+ for (const callback of callbacks) {
4342
+ const result = await callback(request);
4343
+ if (result !== undefined) {
4344
+ websocket.send(JSON.stringify({ jsonrpc: "2.0", id: request.id, result }));
4345
+ return;
4346
+ }
4311
4347
  }
4312
- const result = await callback(request);
4313
- websocket.send(JSON.stringify({ jsonrpc: "2.0", id: request.id, result }));
4348
+ websocket.send(JSON.stringify({
4349
+ jsonrpc: "2.0",
4350
+ id: request.id,
4351
+ error: {
4352
+ code: -32601,
4353
+ message: `unhandled Codex app-server request: ${request.method}`
4354
+ }
4355
+ }));
4314
4356
  } catch (error) {
4315
4357
  websocket.send(JSON.stringify({
4316
4358
  jsonrpc: "2.0",
@@ -6981,7 +7023,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6981
7023
  await Promise.all(Array.from(dynamicChannelSessions.values(), (session) => session.close()));
6982
7024
  dynamicChannelSessions.clear();
6983
7025
  });
6984
- const attachStartedThreadSession = async (control, cwd) => {
7026
+ const attachStartedThreadSession = async (control, cwd, codexThreadId) => {
6985
7027
  const workspaceSlug = normalizedWorkDescription(control.workspace);
6986
7028
  const channelSlug = normalizedWorkDescription(control.channel);
6987
7029
  const kandanThreadId = normalizedWorkDescription(control.threadId);
@@ -6992,6 +7034,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6992
7034
  if (listenUser === undefined) {
6993
7035
  throw new Error("missing listen user for Commander-started Codex session");
6994
7036
  }
7037
+ const runtimeSettings = startInstanceRuntimeSettings(options, control);
6995
7038
  const session = await attachChannelSession({
6996
7039
  kandan,
6997
7040
  codex,
@@ -7021,16 +7064,19 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7021
7064
  workspaceSlug,
7022
7065
  channelSlug,
7023
7066
  kandanThreadId,
7067
+ rootSeq: integerValue(control.rootSeq),
7068
+ codexThreadId,
7024
7069
  listenUser,
7025
- model: control.model,
7026
- reasoningEffort: control.reasoningEffort,
7027
- sandbox: control.sandbox,
7028
- approvalPolicy: control.approvalPolicy
7070
+ model: runtimeSettings.model,
7071
+ reasoningEffort: runtimeSettings.reasoningEffort,
7072
+ sandbox: runtimeSettings.sandbox,
7073
+ approvalPolicy: runtimeSettings.approvalPolicy
7029
7074
  }
7030
7075
  },
7031
7076
  log
7032
7077
  });
7033
7078
  dynamicChannelSessions.set(kandanThreadId, session);
7079
+ return session;
7034
7080
  };
7035
7081
  const heartbeatPayload = () => ({
7036
7082
  instanceId,
@@ -7401,6 +7447,7 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7401
7447
  ensureCodexProjectTrusted(cwd.cwd);
7402
7448
  }
7403
7449
  const developerPrompt = normalizedWorkDescription(control.developerPrompt);
7450
+ const runtimeSettings = startInstanceRuntimeSettings(options, control);
7404
7451
  const response = await codex.request("thread/start", {
7405
7452
  cwd: cwd.cwd,
7406
7453
  serviceName: "kandan-local-runner",
@@ -7409,25 +7456,34 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7409
7456
  cwd: cwd.cwd,
7410
7457
  developerPrompt
7411
7458
  }),
7412
- ...control.model === undefined ? {} : { model: control.model },
7413
- ...control.reasoningEffort === undefined ? {} : { reasoningEffort: control.reasoningEffort },
7414
- ...control.approvalPolicy === undefined ? {} : { approvalPolicy: control.approvalPolicy },
7415
- ...control.sandbox === undefined ? {} : { sandbox: control.sandbox },
7416
- ...control.fast === true ? { serviceTier: "fast" } : {}
7459
+ ...runtimeSettings.model === undefined ? {} : { model: runtimeSettings.model },
7460
+ ...runtimeSettings.reasoningEffort === undefined ? {} : { reasoningEffort: runtimeSettings.reasoningEffort },
7461
+ ...runtimeSettings.approvalPolicy === undefined ? {} : { approvalPolicy: runtimeSettings.approvalPolicy },
7462
+ ...runtimeSettings.sandbox === undefined ? {} : { sandbox: runtimeSettings.sandbox },
7463
+ ...runtimeSettings.fast === true ? { serviceTier: "fast" } : {}
7417
7464
  });
7418
7465
  const codexThreadId = extractStartedThreadId(response);
7419
7466
  const workDescription = normalizedWorkDescription(control.workDescription);
7420
7467
  if (codexThreadId !== undefined && developerPrompt !== undefined) {
7421
7468
  await postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId);
7422
7469
  }
7423
- if (codexThreadId !== undefined && onStartedThread !== undefined) {
7424
- await onStartedThread(control, cwd.cwd);
7425
- }
7470
+ const startedThreadSession = codexThreadId !== undefined && onStartedThread !== undefined ? await onStartedThread(control, cwd.cwd, codexThreadId) : undefined;
7426
7471
  if (codexThreadId !== undefined && workDescription !== undefined) {
7427
- await codex.request("turn/start", {
7428
- threadId: codexThreadId,
7429
- input: [{ type: "text", text: workDescription }]
7430
- });
7472
+ const rootSeq = integerValue(control.rootSeq);
7473
+ if (startedThreadSession !== undefined && rootSeq !== undefined) {
7474
+ const identity = identityFromAccessToken(options.token);
7475
+ await startedThreadSession.startThreadMessageTurn({
7476
+ seq: rootSeq,
7477
+ body: workDescription,
7478
+ actorSlug: identity.actorUsername,
7479
+ actorUserId: identity.actorUserId
7480
+ });
7481
+ } else {
7482
+ await codex.request("turn/start", {
7483
+ threadId: codexThreadId,
7484
+ input: [{ type: "text", text: workDescription }]
7485
+ });
7486
+ }
7431
7487
  }
7432
7488
  return {
7433
7489
  instanceId,
@@ -7486,6 +7542,7 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7486
7542
  case "kill_instance":
7487
7543
  case "interrupt_queued_messages":
7488
7544
  case "resolve_codex_approval_request":
7545
+ case "resolve_port_forward_request":
7489
7546
  case "forward_http_request":
7490
7547
  case "forward_websocket_open":
7491
7548
  case "forward_websocket_send":
@@ -7575,6 +7632,16 @@ ${developerPrompt}`,
7575
7632
  client_message_id: `codex-start-instructions-${threadId}`
7576
7633
  });
7577
7634
  }
7635
+ function startInstanceRuntimeSettings(options, control) {
7636
+ const session = options.channelSession;
7637
+ return {
7638
+ model: control.model ?? session?.model,
7639
+ reasoningEffort: control.reasoningEffort ?? session?.reasoningEffort,
7640
+ approvalPolicy: control.approvalPolicy ?? session?.approvalPolicy,
7641
+ sandbox: control.sandbox ?? session?.sandbox,
7642
+ fast: control.fast ?? options.fast
7643
+ };
7644
+ }
7578
7645
  async function startOwnedCodexAppServer(options) {
7579
7646
  ensureCodexProjectTrusted(options.cwd);
7580
7647
  return await startCodexAppServer(options.codexBin, options.cwd, {
@@ -9421,7 +9488,7 @@ async function main(args) {
9421
9488
  process.stdout.write(connectGuideText());
9422
9489
  return;
9423
9490
  case "version":
9424
- process.stdout.write(`linzumi 0.0.36-beta
9491
+ process.stdout.write(`linzumi 0.0.38-beta
9425
9492
  `);
9426
9493
  return;
9427
9494
  case "auth":
@@ -9935,7 +10002,7 @@ async function parseRunnerArgs(args, deps = {
9935
10002
  process.exit(0);
9936
10003
  }
9937
10004
  if (values.get("version") === true) {
9938
- process.stdout.write(`linzumi 0.0.36-beta
10005
+ process.stdout.write(`linzumi 0.0.38-beta
9939
10006
  `);
9940
10007
  process.exit(0);
9941
10008
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.36-beta",
3
+ "version": "0.0.38-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": {