@linzumi/cli 0.0.35-beta → 0.0.37-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 +3 -2
  2. package/dist/index.js +119 -42
  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.35-beta --version
66
+ npx -y @linzumi/cli@0.0.37-beta --version
67
67
  linzumi --version
68
68
  ```
69
69
 
@@ -114,7 +114,8 @@ linzumi paths add ~/code/my-app
114
114
  ```
115
115
 
116
116
  The trust list lives at `~/.linzumi/config.json`. The bootstrap agent
117
- adds `/tmp/hello_linzumi` for you on first run.
117
+ adds `/tmp/hello_linzumi` for you on first run. For `linzumi connect`, the
118
+ selected `--cwd` is also trusted for that runner process.
118
119
 
119
120
  ## When something looks wrong
120
121
 
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,
@@ -1712,6 +1713,15 @@ 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);
1718
+ switch (state.rootSeq) {
1719
+ case undefined:
1720
+ await postBoundThreadAvailability(args, state, payloadContext, codexVersion);
1721
+ break;
1722
+ default:
1723
+ break;
1724
+ }
1715
1725
  } else {
1716
1726
  const resolved = await pushOk(args.kandan, args.topic, "session:resolve_thread_session", {
1717
1727
  workspace: session.workspaceSlug,
@@ -1723,15 +1733,22 @@ async function bindChannelSession(args, state, payloadContext) {
1723
1733
  if (state.codexThreadId === undefined) {
1724
1734
  throw new Error("Kandan thread root metadata did not include a Codex thread id");
1725
1735
  }
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
- });
1736
+ await postBoundThreadAvailability(args, state, payloadContext, codexVersion);
1733
1737
  }
1734
1738
  }
1739
+ async function postBoundThreadAvailability(args, state, payloadContext, codexVersion) {
1740
+ if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
1741
+ throw new Error("cannot post local Codex availability before thread binding");
1742
+ }
1743
+ const session = args.options.channelSession;
1744
+ await pushOk(args.kandan, args.topic, "session:post_thread_message", {
1745
+ workspace: session.workspaceSlug,
1746
+ channel: session.channelSlug,
1747
+ thread_id: state.kandanThreadId,
1748
+ body: availabilityMessage(args.options, codexVersion, state.codexThreadId),
1749
+ payload: localRunnerPayload(args.options, args.instanceId, "availability", state.codexThreadId, payloadContext)
1750
+ });
1751
+ }
1735
1752
  async function handleChannelSessionControl(args, state, payloadContext, control) {
1736
1753
  if (control.type === "update_session_settings") {
1737
1754
  return updateSessionSettings(args, state, control);
@@ -1745,7 +1762,10 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
1745
1762
  if (control.type !== "interrupt_queued_messages") {
1746
1763
  return;
1747
1764
  }
1748
- if (state.codexThreadId === undefined || state.kandanThreadId === undefined || control.threadId !== state.codexThreadId) {
1765
+ if (state.codexThreadId !== control.threadId) {
1766
+ return;
1767
+ }
1768
+ if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
1749
1769
  return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
1750
1770
  }
1751
1771
  const interrupted = interruptPendingKandanMessages(state.queue, control.throughSeq);
@@ -1795,7 +1815,10 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
1795
1815
  };
1796
1816
  }
1797
1817
  function updateSessionSettings(args, state, control) {
1798
- if (state.codexThreadId === undefined || control.threadId !== state.codexThreadId) {
1818
+ if (state.codexThreadId !== control.threadId) {
1819
+ return;
1820
+ }
1821
+ if (state.codexThreadId === undefined) {
1799
1822
  return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
1800
1823
  }
1801
1824
  state.runtimeSettings = mergeRuntimeSettings(state.runtimeSettings, control);
@@ -1822,7 +1845,10 @@ function updateSessionSettings(args, state, control) {
1822
1845
  };
1823
1846
  }
1824
1847
  async function resolvePendingCodexApprovalRequest(args, state, control) {
1825
- if (state.codexThreadId === undefined || state.kandanThreadId === undefined || control.threadId !== state.codexThreadId) {
1848
+ if (state.codexThreadId !== control.threadId) {
1849
+ return;
1850
+ }
1851
+ if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
1826
1852
  return { instanceId: args.instanceId, ok: false, error: "thread_not_bound" };
1827
1853
  }
1828
1854
  const approval = state.pendingApprovalRequests.get(approvalRequestKey(control.requestId, control.sourceSeq));
@@ -1836,7 +1862,7 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
1836
1862
  await publishMessageState(args, state.kandanThreadId, approval.sourceSeq, {
1837
1863
  status: "processing",
1838
1864
  reason: "streaming response"
1839
- });
1865
+ }, undefined, undefined, state.codexThreadId);
1840
1866
  args.log("codex.approval_request_resolved", {
1841
1867
  request_id: control.requestId,
1842
1868
  source_seq: control.sourceSeq,
@@ -1846,6 +1872,10 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
1846
1872
  return { instanceId: args.instanceId, ok: true };
1847
1873
  }
1848
1874
  async function resolvePendingPortForwardRequest(args, state, payloadContext, control) {
1875
+ const request = state.pendingPortForwardRequests.get(control.requestId);
1876
+ if (request === undefined) {
1877
+ return;
1878
+ }
1849
1879
  if (!portForwardControlSenderAllowed(args, payloadContext, control)) {
1850
1880
  args.log("port_forward.request_resolution_ignored", {
1851
1881
  request_id: control.requestId,
@@ -1855,10 +1885,6 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
1855
1885
  });
1856
1886
  return { instanceId: args.instanceId, ok: false, error: "sender_not_allowed" };
1857
1887
  }
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
1888
  state.pendingPortForwardRequests.delete(control.requestId);
1863
1889
  if (control.decision === "deny") {
1864
1890
  state.dismissedForwardTargets.set(request.port, approvedTargetFromRequest(request));
@@ -2204,6 +2230,27 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
2204
2230
  await publishKandanMessageState(args, event, { status: "queued" });
2205
2231
  await drainKandanMessageQueue(args, state, payloadContext);
2206
2232
  }
2233
+ async function startThreadMessageTurn(args, state, payloadContext, message) {
2234
+ if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
2235
+ throw new Error("cannot start a local Codex turn before thread binding");
2236
+ }
2237
+ const queued = {
2238
+ seq: message.seq,
2239
+ actorSlug: message.actorSlug,
2240
+ actorUserId: message.actorUserId,
2241
+ body: message.body,
2242
+ attachments: []
2243
+ };
2244
+ enqueuePendingKandanMessage(state.queue, queued);
2245
+ args.log("kandan.message_queued", {
2246
+ seq: queued.seq,
2247
+ actor_slug: queued.actorSlug ?? null,
2248
+ actor_user_id: queued.actorUserId ?? null,
2249
+ queue_depth: pendingKandanMessageQueueLength(state.queue)
2250
+ });
2251
+ await publishQueuedMessageState(args, state, queued, { status: "queued" });
2252
+ await drainKandanMessageQueue(args, state, payloadContext);
2253
+ }
2207
2254
  async function bindUnboundHistoricalThread(args, state, event) {
2208
2255
  if (event.threadId === undefined || event.replyToSeq === undefined) {
2209
2256
  return false;
@@ -2362,7 +2409,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
2362
2409
  }
2363
2410
  async function handleCodexServerRequest(args, state, payloadContext, request) {
2364
2411
  const params = objectValue(request.params) ?? {};
2365
- const turnId = stringValue(params.turnId);
2412
+ const turnId = codexNotificationTurnId(params);
2413
+ const threadId = codexNotificationThreadId(params);
2414
+ if (state.closed || !codexNotificationBelongsToSession(state, threadId, turnId)) {
2415
+ return;
2416
+ }
2366
2417
  if (codexApprovalRequestCanAutoAccept(state.runtimeSettings, request.method)) {
2367
2418
  args.log("codex.server_request_auto_accepted", {
2368
2419
  method: request.method,
@@ -2405,7 +2456,7 @@ async function requestKandanApproval(args, state, request, turnId, payloadContex
2405
2456
  status: "processing",
2406
2457
  reason: "awaiting approval",
2407
2458
  approval
2408
- });
2459
+ }, undefined, undefined, state.codexThreadId);
2409
2460
  args.log("codex.approval_request_pending", {
2410
2461
  request_id: approval.requestId,
2411
2462
  source_seq: sourceSeq,
@@ -3576,12 +3627,13 @@ async function publishQueuedMessageState(args, state, message, messageState) {
3576
3627
  }
3577
3628
  await publishMessageState(args, state.kandanThreadId, message.seq, messageState, message.actorSlug, message.actorUserId);
3578
3629
  }
3579
- async function publishMessageState(args, threadId, seq, state, actorSlug, actorUserId) {
3630
+ async function publishMessageState(args, threadId, seq, state, actorSlug, actorUserId, codexThreadId) {
3580
3631
  const session = args.options.channelSession;
3581
3632
  const payload = {
3582
3633
  workspace: session.workspaceSlug,
3583
3634
  channel: session.channelSlug,
3584
3635
  thread_id: threadId,
3636
+ ...codexThreadId === undefined ? {} : { codex_thread_id: codexThreadId },
3585
3637
  seq,
3586
3638
  status: state.status,
3587
3639
  ..."reason" in state ? { reason: state.reason } : {},
@@ -3639,7 +3691,7 @@ async function refreshActiveProcessingHeartbeat(args, state) {
3639
3691
  if (activeProcessingState === undefined || state.kandanThreadId === undefined) {
3640
3692
  return;
3641
3693
  }
3642
- await publishMessageState(args, state.kandanThreadId, activeProcessingState.seq, processingMessageStateFromActive(activeProcessingState));
3694
+ await publishMessageState(args, state.kandanThreadId, activeProcessingState.seq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
3643
3695
  }
3644
3696
  function clearActiveProcessingState(state, seq) {
3645
3697
  if (state.activeProcessingState?.seq === seq) {
@@ -4305,12 +4357,21 @@ async function respondToServerRequest(websocket, request, callbacks) {
4305
4357
  }));
4306
4358
  return;
4307
4359
  }
4308
- const callback = Array.from(callbacks)[0];
4309
- if (callback === undefined) {
4310
- throw new Error(`unhandled Codex app-server request: ${request.method}`);
4360
+ for (const callback of callbacks) {
4361
+ const result = await callback(request);
4362
+ if (result !== undefined) {
4363
+ websocket.send(JSON.stringify({ jsonrpc: "2.0", id: request.id, result }));
4364
+ return;
4365
+ }
4311
4366
  }
4312
- const result = await callback(request);
4313
- websocket.send(JSON.stringify({ jsonrpc: "2.0", id: request.id, result }));
4367
+ websocket.send(JSON.stringify({
4368
+ jsonrpc: "2.0",
4369
+ id: request.id,
4370
+ error: {
4371
+ code: -32601,
4372
+ message: `unhandled Codex app-server request: ${request.method}`
4373
+ }
4374
+ }));
4314
4375
  } catch (error) {
4315
4376
  websocket.send(JSON.stringify({
4316
4377
  jsonrpc: "2.0",
@@ -6981,7 +7042,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6981
7042
  await Promise.all(Array.from(dynamicChannelSessions.values(), (session) => session.close()));
6982
7043
  dynamicChannelSessions.clear();
6983
7044
  });
6984
- const attachStartedThreadSession = async (control, cwd) => {
7045
+ const attachStartedThreadSession = async (control, cwd, codexThreadId) => {
6985
7046
  const workspaceSlug = normalizedWorkDescription(control.workspace);
6986
7047
  const channelSlug = normalizedWorkDescription(control.channel);
6987
7048
  const kandanThreadId = normalizedWorkDescription(control.threadId);
@@ -7021,6 +7082,8 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7021
7082
  workspaceSlug,
7022
7083
  channelSlug,
7023
7084
  kandanThreadId,
7085
+ rootSeq: integerValue(control.rootSeq),
7086
+ codexThreadId,
7024
7087
  listenUser,
7025
7088
  model: control.model,
7026
7089
  reasoningEffort: control.reasoningEffort,
@@ -7031,6 +7094,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
7031
7094
  log
7032
7095
  });
7033
7096
  dynamicChannelSessions.set(kandanThreadId, session);
7097
+ return session;
7034
7098
  };
7035
7099
  const heartbeatPayload = () => ({
7036
7100
  instanceId,
@@ -7420,14 +7484,23 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7420
7484
  if (codexThreadId !== undefined && developerPrompt !== undefined) {
7421
7485
  await postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId);
7422
7486
  }
7423
- if (codexThreadId !== undefined && onStartedThread !== undefined) {
7424
- await onStartedThread(control, cwd.cwd);
7425
- }
7487
+ const startedThreadSession = codexThreadId !== undefined && onStartedThread !== undefined ? await onStartedThread(control, cwd.cwd, codexThreadId) : undefined;
7426
7488
  if (codexThreadId !== undefined && workDescription !== undefined) {
7427
- await codex.request("turn/start", {
7428
- threadId: codexThreadId,
7429
- input: [{ type: "text", text: workDescription }]
7430
- });
7489
+ const rootSeq = integerValue(control.rootSeq);
7490
+ if (startedThreadSession !== undefined && rootSeq !== undefined) {
7491
+ const identity = identityFromAccessToken(options.token);
7492
+ await startedThreadSession.startThreadMessageTurn({
7493
+ seq: rootSeq,
7494
+ body: workDescription,
7495
+ actorSlug: identity.actorUsername,
7496
+ actorUserId: identity.actorUserId
7497
+ });
7498
+ } else {
7499
+ await codex.request("turn/start", {
7500
+ threadId: codexThreadId,
7501
+ input: [{ type: "text", text: workDescription }]
7502
+ });
7503
+ }
7431
7504
  }
7432
7505
  return {
7433
7506
  instanceId,
@@ -7486,6 +7559,7 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
7486
7559
  case "kill_instance":
7487
7560
  case "interrupt_queued_messages":
7488
7561
  case "resolve_codex_approval_request":
7562
+ case "resolve_port_forward_request":
7489
7563
  case "forward_http_request":
7490
7564
  case "forward_websocket_open":
7491
7565
  case "forward_websocket_send":
@@ -9421,7 +9495,7 @@ async function main(args) {
9421
9495
  process.stdout.write(connectGuideText());
9422
9496
  return;
9423
9497
  case "version":
9424
- process.stdout.write(`linzumi 0.0.35-beta
9498
+ process.stdout.write(`linzumi 0.0.37-beta
9425
9499
  `);
9426
9500
  return;
9427
9501
  case "auth":
@@ -9935,13 +10009,15 @@ async function parseRunnerArgs(args, deps = {
9935
10009
  process.exit(0);
9936
10010
  }
9937
10011
  if (values.get("version") === true) {
9938
- process.stdout.write(`linzumi 0.0.35-beta
10012
+ process.stdout.write(`linzumi 0.0.37-beta
9939
10013
  `);
9940
10014
  process.exit(0);
9941
10015
  }
9942
10016
  const channelTarget = parseChannelSessionTarget(values);
9943
10017
  const kandanUrl = required(values, "linzumi-url");
9944
10018
  const cwd = stringValue3(values, "cwd") ?? process.cwd();
10019
+ const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
10020
+ const configuredAllowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : readConfiguredAllowedCwds();
9945
10021
  const codexBin = stringValue3(values, "codex-bin") ?? "codex";
9946
10022
  const customCodeServerBin = stringValue3(values, "code-server-bin");
9947
10023
  const explicitToken = stringValue3(values, "token");
@@ -9981,7 +10057,7 @@ async function parseRunnerArgs(args, deps = {
9981
10057
  launchTui: values.get("launch-tui") === true,
9982
10058
  fast: values.get("fast") === true,
9983
10059
  logFile: stringValue3(values, "log-file"),
9984
- allowedCwds: values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : readConfiguredAllowedCwds(),
10060
+ allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...configuredAllowedCwds])),
9985
10061
  allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
9986
10062
  codeServerBin: editorRuntime.codeServerBin,
9987
10063
  editorRuntime: editorRuntime.runtime,
@@ -10221,7 +10297,7 @@ Codex:
10221
10297
  --stream-flush-ms <ms> Batch live Codex deltas before Linzumi persistence, default 150
10222
10298
  --fast Mark this runner as low-latency/fast in the availability message
10223
10299
  --log-file <path> JSONL event log path, default <cwd>/.linzumi-runner.log
10224
- --allowed-cwd <paths> Comma-separated roots where Linzumi may start new local Codex sessions
10300
+ --allowed-cwd <paths> Extra comma-separated roots where Linzumi may start local Codex sessions
10225
10301
  --forward-port <ports> Comma-separated local TCP ports Linzumi may expose as authenticated previews
10226
10302
  --code-server-bin <path> Custom development code-server executable. The default editor runtime is downloaded from Linzumi.
10227
10303
 
@@ -10318,8 +10394,9 @@ Usage:
10318
10394
  linzumi paths add <path>
10319
10395
  linzumi paths remove <path>
10320
10396
 
10321
- Trusted paths are stored in ~/.linzumi/config.json. linzumi connect uses them
10322
- unless --allowed-cwd is passed for that runner process.
10397
+ Trusted paths are stored in ~/.linzumi/config.json. linzumi connect always
10398
+ trusts its selected cwd for that runner process and also uses configured paths
10399
+ unless --allowed-cwd is passed with explicit extra roots.
10323
10400
  `;
10324
10401
  }
10325
10402
  function startHelpText() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.35-beta",
3
+ "version": "0.0.37-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": {