@linzumi/cli 0.0.29-beta → 0.0.31-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 +10 -10
  2. package/dist/index.js +101 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,8 +46,8 @@ Terms:
46
46
  - **Bootstrapper Codex**: the outer Codex started by the pasted command. It
47
47
  sets up Linzumi and local processes, but does not edit the demo app.
48
48
  - **Linzumi Commander**: the long-running local bridge started with
49
- `linzumi commander`; it owns the secure tunnel, trusted folder, browser
50
- editor, and inner Codex launch.
49
+ `linzumi commander`; it runs as the claimed human's Commander and owns the
50
+ secure tunnel, trusted folder, browser editor, and inner Codex launch.
51
51
  - **Linzumi Codex session**: the inner agent running inside the Linzumi thread;
52
52
  it edits `/tmp/hello_linzumi` and posts progress.
53
53
  - **Human**: the workspace owner who opens the one-time login link and watches
@@ -152,12 +152,12 @@ npx -y @linzumi/cli@latest commander daemon \
152
152
  npx -y @linzumi/cli@latest commander wait --runner-id "$commander_id" --timeout-ms 30000
153
153
  ```
154
154
 
155
- The agent-owned Commander reads `~/.linzumi/agent-token.json`, uses the
156
- workspace/channel scope from the approval flow, reads trusted folders from
157
- `~/.linzumi/config.json`, marks approved project directories trusted in
158
- Codex's normal project config so Codex does not stop for an interactive trust
159
- prompt, advertises the explicit preview port, and listens only to the
160
- approving human unless `--listen-user` is explicitly passed. Use a unique
155
+ The human-owned Commander reads `~/.linzumi/agent-token.json`, uses the
156
+ claimed human Commander token and workspace/channel scope from the approval
157
+ flow, reads trusted folders from `~/.linzumi/config.json`, marks approved
158
+ project directories trusted in Codex's normal project config so Codex does not
159
+ stop for an interactive trust prompt, advertises the explicit preview port, and
160
+ listens only to the approving human unless `--listen-user` is explicitly passed. Use a unique
161
161
  Commander id per launch. `linzumi commander daemon` writes a status record and
162
162
  log under `~/.linzumi/commanders`, and `linzumi commander wait` returns only
163
163
  after the Commander is connected.
@@ -293,7 +293,7 @@ intentionally. Every action is auditable from the thread.
293
293
  ## Pinning a version
294
294
 
295
295
  ```bash
296
- npm install -g @linzumi/cli@0.0.29-beta
296
+ npm install -g @linzumi/cli@0.0.31-beta
297
297
  linzumi --version
298
298
  ```
299
299
 
@@ -316,7 +316,7 @@ linzumi connect \
316
316
  ### All the flags
317
317
 
318
318
  ```text
319
- --agent-token-file <path> Agent token cache for `linzumi commander`
319
+ --agent-token-file <path> Bootstrap token cache for `linzumi commander`
320
320
  --oauth-callback-host <ip> Sign-in callback host your browser can reach
321
321
  --codex-bin <path> Codex executable, default `codex`
322
322
  --model <name> Model requested for Codex sessions and labelled in Linzumi
package/dist/index.js CHANGED
@@ -6622,6 +6622,60 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6622
6622
  cleanup.actions.push(() => channelSession.close());
6623
6623
  kandan.onReconnect(() => channelSession.handleKandanReconnect());
6624
6624
  }
6625
+ const dynamicChannelSessions = new Map;
6626
+ cleanup.actions.push(async () => {
6627
+ await Promise.all(Array.from(dynamicChannelSessions.values(), (session) => session.close()));
6628
+ dynamicChannelSessions.clear();
6629
+ });
6630
+ const attachStartedThreadSession = async (control, cwd) => {
6631
+ const workspaceSlug = normalizedWorkDescription(control.workspace);
6632
+ const channelSlug = normalizedWorkDescription(control.channel);
6633
+ const kandanThreadId = normalizedWorkDescription(control.threadId);
6634
+ if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined || dynamicChannelSessions.has(kandanThreadId)) {
6635
+ return;
6636
+ }
6637
+ const listenUser = options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
6638
+ if (listenUser === undefined) {
6639
+ throw new Error("missing listen user for Commander-started Codex session");
6640
+ }
6641
+ const session = await attachChannelSession({
6642
+ kandan,
6643
+ codex,
6644
+ topic,
6645
+ instanceId,
6646
+ options: {
6647
+ token: options.token,
6648
+ runnerId: options.runnerId,
6649
+ cwd,
6650
+ codexBin: options.codexBin,
6651
+ fast: control.fast ?? options.fast,
6652
+ launchTui: false,
6653
+ enablePortForwardWatch: true,
6654
+ initialForwardPorts: allowedForwardPorts,
6655
+ suppressedForwardPorts: () => Array.from(managedForwardPorts),
6656
+ onForwardPortApproved: (port) => {
6657
+ liveForwardPorts.add(port);
6658
+ return capabilitiesPayload();
6659
+ },
6660
+ onForwardPortRevoked: (port) => {
6661
+ liveForwardPorts.delete(port);
6662
+ return capabilitiesPayload();
6663
+ },
6664
+ channelSession: {
6665
+ workspaceSlug,
6666
+ channelSlug,
6667
+ kandanThreadId,
6668
+ listenUser,
6669
+ model: control.model,
6670
+ reasoningEffort: control.reasoningEffort,
6671
+ sandbox: control.sandbox,
6672
+ approvalPolicy: control.approvalPolicy
6673
+ }
6674
+ },
6675
+ log
6676
+ });
6677
+ dynamicChannelSessions.set(kandanThreadId, session);
6678
+ };
6625
6679
  const heartbeatPayload = () => ({
6626
6680
  instanceId,
6627
6681
  codexUrl,
@@ -6684,6 +6738,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6684
6738
  metadata
6685
6739
  });
6686
6740
  channelSession?.handleCodexNotification(notification.method, params);
6741
+ for (const session of dynamicChannelSessions.values()) {
6742
+ session.handleCodexNotification(notification.method, params);
6743
+ }
6687
6744
  });
6688
6745
  const handleControl = (control) => {
6689
6746
  log("kandan.control", { control });
@@ -6777,11 +6834,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6777
6834
  pushHeartbeat();
6778
6835
  return;
6779
6836
  }
6780
- (channelSession?.handleControl(control) ?? Promise.resolve(undefined)).then((handled) => {
6837
+ resolveSessionControl(channelSession, dynamicChannelSessions, control).then((handled) => {
6781
6838
  if (handled !== undefined) {
6782
6839
  return handled;
6783
6840
  }
6784
- return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control);
6841
+ return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control, attachStartedThreadSession);
6785
6842
  }).then((response) => {
6786
6843
  return kandan.push(topic, "codex_response", response);
6787
6844
  }).catch((error) => {
@@ -6802,6 +6859,19 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
6802
6859
  function controlTargetsInstance(control, instanceId) {
6803
6860
  return control.instanceId === undefined || control.instanceId === instanceId;
6804
6861
  }
6862
+ async function resolveSessionControl(channelSession, dynamicChannelSessions, control) {
6863
+ const primaryHandled = await (channelSession?.handleControl(control) ?? Promise.resolve(undefined));
6864
+ if (primaryHandled !== undefined) {
6865
+ return primaryHandled;
6866
+ }
6867
+ for (const session of dynamicChannelSessions.values()) {
6868
+ const handled = await session.handleControl(control);
6869
+ if (handled !== undefined) {
6870
+ return handled;
6871
+ }
6872
+ }
6873
+ return;
6874
+ }
6805
6875
  async function closeCleanupStack(cleanup) {
6806
6876
  if (cleanup.closePromise !== undefined) {
6807
6877
  return cleanup.closePromise;
@@ -6916,7 +6986,7 @@ async function prepareCodexThreadForTuiResume(codex, codexThreadId) {
6916
6986
  throw new Error(`failed to verify Codex TUI resume: ${verified.error.message}`);
6917
6987
  }
6918
6988
  }
6919
- async function applyControl(codex, kandan, topic, instanceId, options, allowedCwds, control) {
6989
+ async function applyControl(codex, kandan, topic, instanceId, options, allowedCwds, control, onStartedThread) {
6920
6990
  switch (control.type) {
6921
6991
  case "start_instance": {
6922
6992
  const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
@@ -6949,7 +7019,10 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
6949
7019
  const codexThreadId = extractStartedThreadId(response);
6950
7020
  const workDescription = normalizedWorkDescription(control.workDescription);
6951
7021
  if (codexThreadId !== undefined && developerPrompt !== undefined) {
6952
- await postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt);
7022
+ await postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId);
7023
+ }
7024
+ if (codexThreadId !== undefined && onStartedThread !== undefined) {
7025
+ await onStartedThread(control, cwd.cwd);
6953
7026
  }
6954
7027
  if (codexThreadId !== undefined && workDescription !== undefined) {
6955
7028
  await codex.request("turn/start", {
@@ -7078,7 +7151,7 @@ work in the approved project folder, keep preview servers reachable through the
7078
7151
  secure tunnel, and keep the Linzumi thread truthful.
7079
7152
  </task_reminder>`;
7080
7153
  }
7081
- async function postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt) {
7154
+ async function postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId) {
7082
7155
  const workspace = normalizedWorkDescription(control.workspace);
7083
7156
  const channel = normalizedWorkDescription(control.channel);
7084
7157
  const threadId = normalizedWorkDescription(control.threadId);
@@ -7093,8 +7166,11 @@ async function postVisibleDeveloperPrompt(kandan, topic, control, developerPromp
7093
7166
 
7094
7167
  ${developerPrompt}`,
7095
7168
  payload: {
7096
- local_codex_runner: {
7097
- event_type: "codex_start_instructions"
7169
+ metadata: {
7170
+ local_codex_runner: {
7171
+ event_type: "codex_start_instructions",
7172
+ codex_thread_id: codexThreadId
7173
+ }
7098
7174
  }
7099
7175
  },
7100
7176
  client_message_id: `codex-start-instructions-${threadId}`
@@ -7683,6 +7759,8 @@ async function runClaim(command, deps) {
7683
7759
  const tokenFile = {
7684
7760
  apiUrl: command.apiUrl,
7685
7761
  agentToken: requiredString(response, "agent_token"),
7762
+ commanderToken: requiredString(response, "commander_token"),
7763
+ commanderTokenExpiresInSeconds: numberValue2(response.commander_token_expires_in_seconds),
7686
7764
  agentId: requiredString(response, "agent_id"),
7687
7765
  workspaceId: requiredString(response, "workspace_id"),
7688
7766
  workspaceName: stringValue(response.workspace_name),
@@ -7696,7 +7774,7 @@ async function runClaim(command, deps) {
7696
7774
  cursors: {}
7697
7775
  };
7698
7776
  writeTokenFile(command.tokenFile, tokenFile, deps.writeTextFile);
7699
- deps.stdout.write(`Logged in as agent ${tokenFile.agentId}
7777
+ deps.stdout.write(`Logged in as ${tokenFile.ownerUsername ?? "claimed user"}
7700
7778
  `);
7701
7779
  if (tokenFile.workspaceName !== undefined) {
7702
7780
  deps.stdout.write(`workspace_name: ${tokenFile.workspaceName}
@@ -7938,6 +8016,8 @@ function readStoredAgentTokenFile(path, readTextFile = readOptionalTextFile) {
7938
8016
  return {
7939
8017
  apiUrl: requiredString(parsed, "apiUrl"),
7940
8018
  agentToken: requiredString(parsed, "agentToken"),
8019
+ commanderToken: requiredString(parsed, "commanderToken"),
8020
+ commanderTokenExpiresInSeconds: numberValue2(parsed.commanderTokenExpiresInSeconds),
7941
8021
  agentId: requiredString(parsed, "agentId"),
7942
8022
  workspaceId: requiredString(parsed, "workspaceId"),
7943
8023
  workspaceName: stringValue(parsed.workspaceName),
@@ -8836,7 +8916,7 @@ async function main(args) {
8836
8916
  process.stdout.write(connectGuideText());
8837
8917
  return;
8838
8918
  case "version":
8839
- process.stdout.write(`linzumi 0.0.29-beta
8919
+ process.stdout.write(`linzumi 0.0.31-beta
8840
8920
  `);
8841
8921
  return;
8842
8922
  case "auth":
@@ -9232,7 +9312,7 @@ async function parseAgentRunnerArgs(args, deps = {
9232
9312
  assertStartDependencies(initialDependencyStatus);
9233
9313
  const editorRuntime = await deps.resolveEditorRuntime({
9234
9314
  kandanUrl,
9235
- token: tokenFile.agentToken,
9315
+ token: tokenFile.commanderToken,
9236
9316
  customCodeServerBin,
9237
9317
  fetchImpl: trustedFetch(kandanTlsTrustFromEnv())
9238
9318
  });
@@ -9245,7 +9325,7 @@ async function parseAgentRunnerArgs(args, deps = {
9245
9325
  assertStartDependencies(dependencyStatus);
9246
9326
  return {
9247
9327
  kandanUrl,
9248
- token: tokenFile.agentToken,
9328
+ token: tokenFile.commanderToken,
9249
9329
  runnerId: stringValue3(values, "runner-id") ?? `agent-runner-${randomUUID3()}`,
9250
9330
  cwd,
9251
9331
  codexBin,
@@ -9276,9 +9356,15 @@ function readAgentTokenTextFile(path) {
9276
9356
  return existsSync10(path) ? readFileSync9(path, "utf8") : undefined;
9277
9357
  }
9278
9358
  function rejectAgentRunnerTargetingFlags(values) {
9279
- const unsupportedFlags = ["workspace", "channel", "token", "auth-file", "oauth-callback-host"].filter((flag) => values.has(flag));
9359
+ const unsupportedFlags = [
9360
+ "workspace",
9361
+ "channel",
9362
+ "token",
9363
+ "auth-file",
9364
+ "oauth-callback-host"
9365
+ ].filter((flag) => values.has(flag));
9280
9366
  if (unsupportedFlags.length > 0) {
9281
- throw new Error(`linzumi commander uses the claimed agent token scope; remove ${unsupportedFlags.map((flag) => `--${flag}`).join(", ")}.`);
9367
+ throw new Error(`linzumi commander uses the claimed human Commander token scope; remove ${unsupportedFlags.map((flag) => `--${flag}`).join(", ")}.`);
9282
9368
  }
9283
9369
  }
9284
9370
  function requiredStoredAgentChannel(channelId) {
@@ -9343,7 +9429,7 @@ async function parseRunnerArgs(args, deps = {
9343
9429
  process.exit(0);
9344
9430
  }
9345
9431
  if (values.get("version") === true) {
9346
- process.stdout.write(`linzumi 0.0.29-beta
9432
+ process.stdout.write(`linzumi 0.0.31-beta
9347
9433
  `);
9348
9434
  process.exit(0);
9349
9435
  }
@@ -9770,7 +9856,7 @@ Usage:
9770
9856
  linzumi agent runner <folder> [options]
9771
9857
 
9772
9858
  What it does:
9773
- Starts this computer as the claimed agent's scoped Linzumi Commander. The command
9859
+ Starts this computer as the claimed human's scoped Linzumi Commander. The command
9774
9860
  reads ~/.linzumi/agent-token.json, uses its workspace/channel scope, reads
9775
9861
  trusted folders from ~/.linzumi/config.json when no folder is passed, and
9776
9862
  listens only to the owning human recorded during claim unless --listen-user is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.29-beta",
3
+ "version": "0.0.31-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": {