@linzumi/cli 0.0.40-beta → 0.0.42-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 +823 -324
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,16 +1,16 @@
1
1
  // src/index.ts
2
- import { randomUUID as randomUUID3 } from "node:crypto";
3
- import { existsSync as existsSync10, readFileSync as readFileSync9, realpathSync as realpathSync6 } from "node:fs";
2
+ import { randomUUID as randomUUID4 } from "node:crypto";
3
+ import { existsSync as existsSync11, readFileSync as readFileSync10, realpathSync as realpathSync6 } from "node:fs";
4
4
  import { homedir as homedir9 } from "node:os";
5
5
  import { resolve as resolve9 } from "node:path";
6
6
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7
7
 
8
8
  // src/runner.ts
9
9
  import { spawn as spawn6 } from "node:child_process";
10
- import { randomUUID as randomUUID2 } from "node:crypto";
11
- import { realpathSync as realpathSync4 } from "node:fs";
10
+ import { randomUUID as randomUUID3 } from "node:crypto";
11
+ import { realpathSync as realpathSync5 } from "node:fs";
12
12
  import { hostname as hostname2 } from "node:os";
13
- import { join as join6, resolve as resolve5 } from "node:path";
13
+ import { join as join8, resolve as resolve6 } from "node:path";
14
14
 
15
15
  // src/channelSessionSupport.ts
16
16
  import { spawnSync } from "node:child_process";
@@ -248,7 +248,7 @@ function codexThreadRuntimeOverrides(options) {
248
248
  ...session.model === undefined ? {} : { model: session.model },
249
249
  ...session.reasoningEffort === undefined ? {} : { reasoningEffort: session.reasoningEffort },
250
250
  ...options.fast === true ? { serviceTier: "fast" } : {},
251
- ...session.approvalPolicy === undefined ? {} : { approvalPolicy: session.approvalPolicy },
251
+ approvalPolicy: codexApprovalPolicyForRequest(session.approvalPolicy, session.sandbox),
252
252
  ...session.sandbox === undefined ? {} : { sandbox: session.sandbox }
253
253
  };
254
254
  }
@@ -259,10 +259,30 @@ function codexTurnRuntimeOverrides(options) {
259
259
  ...session.model === undefined ? {} : { model: session.model },
260
260
  ...session.reasoningEffort === undefined ? {} : { effort: session.reasoningEffort },
261
261
  ...options.fast === true ? { serviceTier: "fast" } : {},
262
- ...session.approvalPolicy === undefined ? {} : { approvalPolicy: session.approvalPolicy },
262
+ approvalPolicy: codexApprovalPolicyForRequest(session.approvalPolicy, session.sandbox),
263
263
  ...session.sandbox === undefined ? {} : { sandboxPolicy: codexSandboxPolicy(session.sandbox, options.cwd) }
264
264
  };
265
265
  }
266
+ function codexApprovalPolicySetting(approvalPolicy, sandbox) {
267
+ if (approvalPolicy === undefined || approvalPolicy === "default") {
268
+ return;
269
+ }
270
+ return codexApprovalPolicyForRequest(approvalPolicy, sandbox);
271
+ }
272
+ function codexApprovalPolicyForRequest(approvalPolicy, sandbox) {
273
+ switch (approvalPolicy) {
274
+ case undefined:
275
+ case "default":
276
+ return "on-request";
277
+ case "never":
278
+ return sandbox === "danger-full-access" ? "never" : "on-request";
279
+ case "on-request":
280
+ case "on-failure":
281
+ return approvalPolicy;
282
+ default:
283
+ throw new Error(`unsupported Codex approval policy: ${approvalPolicy}`);
284
+ }
285
+ }
266
286
  function codexSandboxPolicy(sandbox, cwd) {
267
287
  switch (sandbox) {
268
288
  case "danger-full-access":
@@ -290,6 +310,9 @@ function codexSandboxPolicy(sandbox, cwd) {
290
310
  // src/codexOutput.ts
291
311
  import { Buffer as Buffer2 } from "node:buffer";
292
312
  var maxVisibleWebSearchQueries = 6;
313
+ var maxPersistedCommandOutputChars = 14000;
314
+ var maxPersistedCommandLabelChars = 512;
315
+ var commandOutputTruncationReason = "phoenix_insert_body_limit";
293
316
  function codexOutputMessagesForTurn(response, turnId) {
294
317
  if ("error" in response) {
295
318
  return [];
@@ -355,21 +378,20 @@ function codexOutputMessagesForItem(item, index) {
355
378
  case "commandExecution": {
356
379
  const command = stringValue(item.command) ?? "command";
357
380
  const output = nonBlankStringValue(item.aggregatedOutput) ?? "";
358
- const body = [`$ ${command}`, output].filter((part) => part.trim() !== "").join(`
359
-
360
- `);
381
+ const projection = codexCommandOutputProjection(command, output);
361
382
  return [
362
383
  {
363
384
  itemKey,
364
- body,
385
+ body: projection.body,
365
386
  structured: baseStructured("codex_command_execution", {
366
- command,
387
+ command: projection.command,
367
388
  cwd: stringValue(item.cwd) ?? "",
368
389
  status: stringValue(item.status) ?? "",
369
390
  process_id: stringValue(item.processId) ?? "",
370
391
  duration_ms: integerValue(item.durationMs) ?? null,
371
392
  exit_code: integerValue(item.exitCode) ?? null,
372
- output
393
+ output: projection.output,
394
+ ...commandOutputTruncationMetadata(projection)
373
395
  })
374
396
  }
375
397
  ];
@@ -501,21 +523,29 @@ function codexCommandOutputDeltaFromNotification(params) {
501
523
  };
502
524
  }
503
525
  function codexCommandExecutionStructuredMessage(itemKey, fields, streamState) {
526
+ const projection = codexCommandOutputProjection(fields.command, fields.output);
527
+ return codexCommandExecutionStructuredMessageFromProjection(itemKey, fields, streamState, projection);
528
+ }
529
+ function codexCommandExecutionStructuredMessageFromProjection(itemKey, fields, streamState, projection) {
504
530
  return {
505
531
  kind: "codex_command_execution",
506
532
  item_id: itemKey,
507
533
  transcript_unit_id: `codex_command_execution:${itemKey}`,
508
534
  stream_state: streamState,
509
- command: fields.command,
535
+ command: projection.command,
510
536
  cwd: fields.cwd ?? "",
511
537
  status: fields.status ?? streamState,
512
538
  process_id: fields.processId ?? "",
513
539
  duration_ms: fields.durationMs ?? null,
514
540
  exit_code: fields.exitCode ?? null,
515
541
  stream: fields.stream ?? "",
516
- output: fields.output
542
+ output: projection.output,
543
+ ...commandOutputTruncationMetadata(projection)
517
544
  };
518
545
  }
546
+ function codexCommandOutputBody(command, output) {
547
+ return codexCommandOutputProjection(command, output).body;
548
+ }
519
549
  function codexFileChangeDeltaFromNotification(params) {
520
550
  const patchText = textDeltaFromParams(params);
521
551
  if (patchText === undefined || patchText === "") {
@@ -618,6 +648,65 @@ function assistantVisibleText(item) {
618
648
  function assistantDeltaText(params) {
619
649
  return textDeltaFromParams(params);
620
650
  }
651
+ function codexCommandOutputProjection(command, output) {
652
+ const normalizedCommand = truncatePersistedText(sanitizePersistedText(command), maxPersistedCommandLabelChars, "command label");
653
+ const normalizedOutput = truncateCommandOutput(sanitizePersistedText(output));
654
+ const body = commandOutputBodyForProjection(normalizedCommand.text, normalizedOutput.text);
655
+ return {
656
+ command: normalizedCommand.text,
657
+ output: normalizedOutput.text,
658
+ body,
659
+ truncated: normalizedCommand.truncated || normalizedOutput.truncated,
660
+ originalLength: output.length,
661
+ maxOutputChars: maxPersistedCommandOutputChars,
662
+ truncationReason: commandOutputTruncationReason
663
+ };
664
+ }
665
+ function commandOutputTruncationMetadata(projection) {
666
+ return projection.truncated ? {
667
+ output_truncated: true,
668
+ output_original_length: projection.originalLength,
669
+ output_max_chars: projection.maxOutputChars,
670
+ output_truncation_reason: projection.truncationReason
671
+ } : {};
672
+ }
673
+ function commandOutputBodyForProjection(command, output) {
674
+ return [`$ ${command}`, output].filter((part) => part.trim() !== "").join(`
675
+
676
+ `);
677
+ }
678
+ function truncateCommandOutput(output) {
679
+ if (output.length <= maxPersistedCommandOutputChars) {
680
+ return { text: output, truncated: false };
681
+ }
682
+ const marker = `
683
+
684
+ [Linzumi truncated command output: output was too large to persist fully. Owners should search runner logs for codex.command_output_truncated failure_class=phoenix_insert_body_limit.]
685
+
686
+ `;
687
+ const availableChars = Math.max(0, maxPersistedCommandOutputChars - marker.length);
688
+ const headChars = Math.min(2000, Math.floor(availableChars / 3));
689
+ const tailChars = availableChars - headChars;
690
+ const text = `${output.slice(0, headChars)}${marker}${output.slice(-tailChars)}`;
691
+ return {
692
+ text: text.slice(0, maxPersistedCommandOutputChars),
693
+ truncated: true
694
+ };
695
+ }
696
+ function truncatePersistedText(text, maxChars, label) {
697
+ if (text.length <= maxChars) {
698
+ return { text, truncated: false };
699
+ }
700
+ const marker = `[Linzumi truncated ${label}]`;
701
+ const availableChars = Math.max(0, maxChars - marker.length - 1);
702
+ return {
703
+ text: `${text.slice(0, availableChars)} ${marker}`.slice(0, maxChars),
704
+ truncated: true
705
+ };
706
+ }
707
+ function sanitizePersistedText(text) {
708
+ return text.replaceAll("\x00", "�");
709
+ }
621
710
  function textDeltaFromParams(params) {
622
711
  const delta = params.delta;
623
712
  if (typeof delta === "string") {
@@ -641,57 +730,67 @@ function commandMessageForFunctionCall(item, output, index) {
641
730
  const command = name === "exec_command" ? nonBlankStringValue(args?.cmd) ?? name : `${name}${toolArgumentsSummary(item.arguments)}`;
642
731
  const cwd = nonBlankStringValue(args?.cwd) ?? "";
643
732
  const outputText = toolOutputText(output);
644
- const body = [`$ ${command}`, outputText].filter((part) => part.trim() !== "").join(`
645
-
646
- `);
647
- return [{
648
- itemKey,
649
- body,
650
- structured: codexCommandExecutionStructuredMessage(itemKey, {
651
- command,
652
- cwd,
653
- output: outputText,
654
- status: output === undefined ? "started" : "completed"
655
- }, "completed")
656
- }];
733
+ const projection = codexCommandOutputProjection(command, outputText);
734
+ return [
735
+ {
736
+ itemKey,
737
+ body: projection.body,
738
+ structured: codexCommandExecutionStructuredMessage(itemKey, {
739
+ command,
740
+ cwd,
741
+ output: outputText,
742
+ status: output === undefined ? "started" : "completed"
743
+ }, "completed")
744
+ }
745
+ ];
657
746
  }
658
747
  function messageForCustomToolCall(item, output, index) {
659
748
  const name = stringValue(item.name) ?? "custom_tool_call";
660
749
  const itemKey = stringValue(item.call_id) ?? stringValue(item.id) ?? `item-${index}`;
661
750
  const input = nonBlankStringValue(item.input) ?? nonBlankStringValue(item.arguments) ?? "";
662
751
  if (name === "apply_patch") {
663
- return [{
664
- itemKey,
665
- body: input,
666
- structured: {
667
- kind: "codex_file_change",
668
- item_id: itemKey,
669
- transcript_unit_id: `codex_file_change:${itemKey}`,
670
- stream_state: "completed",
671
- status: output === undefined ? "started" : "completed",
672
- patch_text: input,
673
- changes: fileChangesFromPatch(input)
752
+ return [
753
+ {
754
+ itemKey,
755
+ body: input,
756
+ structured: {
757
+ kind: "codex_file_change",
758
+ item_id: itemKey,
759
+ transcript_unit_id: `codex_file_change:${itemKey}`,
760
+ stream_state: "completed",
761
+ status: output === undefined ? "started" : "completed",
762
+ patch_text: input,
763
+ changes: fileChangesFromPatch(input)
764
+ }
674
765
  }
675
- }];
766
+ ];
676
767
  }
677
768
  const outputText = toolOutputText(output);
678
769
  const command = `${name}${toolArgumentsSummary(item.input)}`;
679
- return [{
680
- itemKey,
681
- body: [`$ ${command}`, outputText].filter((part) => part.trim() !== "").join(`
682
-
683
- `),
684
- structured: codexCommandExecutionStructuredMessage(itemKey, { command, output: outputText, status: output === undefined ? "started" : "completed" }, "completed")
685
- }];
770
+ const projection = codexCommandOutputProjection(command, outputText);
771
+ return [
772
+ {
773
+ itemKey,
774
+ body: projection.body,
775
+ structured: codexCommandExecutionStructuredMessage(itemKey, {
776
+ command,
777
+ output: outputText,
778
+ status: output === undefined ? "started" : "completed"
779
+ }, "completed")
780
+ }
781
+ ];
686
782
  }
687
783
  function commandMessageForUnpairedToolOutput(item, index) {
688
784
  const itemKey = stringValue(item.call_id) ?? stringValue(item.id) ?? `item-${index}`;
689
785
  const output = toolOutputText(item);
690
- return [{
691
- itemKey,
692
- body: output,
693
- structured: codexCommandExecutionStructuredMessage(itemKey, { command: "tool output", output, status: "completed" }, "completed")
694
- }];
786
+ const projection = codexCommandOutputProjection("tool output", output);
787
+ return [
788
+ {
789
+ itemKey,
790
+ body: projection.output,
791
+ structured: codexCommandExecutionStructuredMessageFromProjection(itemKey, { status: "completed" }, "completed", projection)
792
+ }
793
+ ];
695
794
  }
696
795
  function toolArgumentsSummary(value) {
697
796
  const text = nonBlankStringValue(value);
@@ -710,13 +809,34 @@ function fileChangesFromPatch(patchText) {
710
809
  const addPrefix = "*** Add File: ";
711
810
  const deletePrefix = "*** Delete File: ";
712
811
  if (line.startsWith(updatePrefix)) {
713
- return [{ path: line.slice(updatePrefix.length).trim(), diff: "", kind: "update", move_path: "" }];
812
+ return [
813
+ {
814
+ path: line.slice(updatePrefix.length).trim(),
815
+ diff: "",
816
+ kind: "update",
817
+ move_path: ""
818
+ }
819
+ ];
714
820
  }
715
821
  if (line.startsWith(addPrefix)) {
716
- return [{ path: line.slice(addPrefix.length).trim(), diff: "", kind: "add", move_path: "" }];
822
+ return [
823
+ {
824
+ path: line.slice(addPrefix.length).trim(),
825
+ diff: "",
826
+ kind: "add",
827
+ move_path: ""
828
+ }
829
+ ];
717
830
  }
718
831
  if (line.startsWith(deletePrefix)) {
719
- return [{ path: line.slice(deletePrefix.length).trim(), diff: "", kind: "delete", move_path: "" }];
832
+ return [
833
+ {
834
+ path: line.slice(deletePrefix.length).trim(),
835
+ diff: "",
836
+ kind: "delete",
837
+ move_path: ""
838
+ }
839
+ ];
720
840
  }
721
841
  return [];
722
842
  });
@@ -2345,8 +2465,8 @@ async function attachChannelSession(args) {
2345
2465
  const session = args.options.channelSession;
2346
2466
  const chatTopic = `chat:${session.workspaceSlug}:${session.channelSlug}`;
2347
2467
  const state = initialChannelSessionState(0, session.rootSeq, session.kandanThreadId, session.codexThreadId, args.options);
2348
- const joined = await args.kandan.join(chatTopic, { last_seq: 0 }, {
2349
- rejoinPayload: () => ({ last_seq: state.minSeq })
2468
+ const joined = await args.kandan.join(chatTopic, chatJoinPayload(args.options.runnerId, state, 0), {
2469
+ rejoinPayload: () => chatJoinPayload(args.options.runnerId, state, state.minSeq)
2350
2470
  });
2351
2471
  const cursor = integerValue(joined.cursor) ?? 0;
2352
2472
  const runnerIdentity = identityFromAccessToken(args.options.token);
@@ -2492,6 +2612,19 @@ async function attachChannelSession(args) {
2492
2612
  }
2493
2613
  };
2494
2614
  }
2615
+ function chatJoinPayload(runnerId, state, lastSeq) {
2616
+ const base = { last_seq: lastSeq };
2617
+ switch (state.kandanThreadId) {
2618
+ case undefined:
2619
+ return base;
2620
+ default:
2621
+ return {
2622
+ ...base,
2623
+ runner_id: runnerId,
2624
+ thread_id: state.kandanThreadId
2625
+ };
2626
+ }
2627
+ }
2495
2628
  async function bindCurrentCodexThread(args, state) {
2496
2629
  if (state.codexThreadId === undefined) {
2497
2630
  return;
@@ -3498,7 +3631,7 @@ async function handleCodexServerRequest(args, state, payloadContext, request) {
3498
3631
  throw new Error(message);
3499
3632
  }
3500
3633
  function codexApprovalRequestCanAutoAccept(settings, method) {
3501
- return settings.approvalPolicy === "never" && (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval");
3634
+ return settings.approvalPolicy === "never" && settings.sandbox === "danger-full-access" && (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval");
3502
3635
  }
3503
3636
  function codexApprovalRequestCanSurface(method) {
3504
3637
  return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval";
@@ -3972,13 +4105,25 @@ async function forwardCommandOutputDeltaPayload(args, state, delta, payloadConte
3972
4105
  }
3973
4106
  const existing = findStreamingCommandOutput(state, delta.itemKey);
3974
4107
  const output = `${existing?.output ?? ""}${delta.delta}`;
3975
- const body = commandOutputBody("command output", output);
3976
4108
  const structured = codexCommandExecutionStructuredMessage(delta.itemKey, {
3977
4109
  command: "command output",
3978
4110
  output,
3979
4111
  processId: delta.processId,
3980
4112
  stream: delta.stream
3981
4113
  }, "streaming");
4114
+ const persistedOutput = typeof structured.output === "string" ? structured.output : output;
4115
+ const body = codexCommandOutputBody("command output", output);
4116
+ if (structured.output_truncated === true) {
4117
+ args.log("codex.command_output_truncated", {
4118
+ item_key: delta.itemKey,
4119
+ turn_id: turnId,
4120
+ process_id: delta.processId ?? null,
4121
+ stream: delta.stream,
4122
+ failure_class: stringValue(structured.output_truncation_reason) ?? null,
4123
+ original_length: integerValue(structured.output_original_length) ?? null,
4124
+ persisted_length: persistedOutput.length
4125
+ });
4126
+ }
3982
4127
  if (existing === undefined) {
3983
4128
  const session = args.options.channelSession;
3984
4129
  const reply = await pushOk(args.kandan, args.topic, "session:post_thread_message", {
@@ -4002,7 +4147,7 @@ async function forwardCommandOutputDeltaPayload(args, state, delta, payloadConte
4002
4147
  itemKey: delta.itemKey,
4003
4148
  turnId,
4004
4149
  seq,
4005
- output,
4150
+ output: persistedOutput,
4006
4151
  processId: delta.processId,
4007
4152
  stream: delta.stream
4008
4153
  });
@@ -4011,7 +4156,7 @@ async function forwardCommandOutputDeltaPayload(args, state, delta, payloadConte
4011
4156
  await editCodexStructuredOutput(args, state, existing.seq, body, structured);
4012
4157
  rememberStreamingCommandOutput(state, {
4013
4158
  ...existing,
4014
- output,
4159
+ output: persistedOutput,
4015
4160
  processId: delta.processId ?? existing.processId,
4016
4161
  stream: delta.stream
4017
4162
  });
@@ -4021,7 +4166,7 @@ async function forwardCommandOutputDeltaPayload(args, state, delta, payloadConte
4021
4166
  turn_id: turnId,
4022
4167
  process_id: delta.processId ?? null,
4023
4168
  stream: delta.stream,
4024
- output_length: output.length
4169
+ output_length: persistedOutput.length
4025
4170
  });
4026
4171
  }
4027
4172
  async function forwardFileChangeDeltaPayload(args, state, delta, payloadContext) {
@@ -4216,11 +4361,6 @@ function webSearchProgressClientMessageId(instanceId, turnId) {
4216
4361
  function webSearchProgressItemKey(turnId) {
4217
4362
  return `web-search:${turnId}`;
4218
4363
  }
4219
- function commandOutputBody(command, output) {
4220
- return [`$ ${command}`, output].filter((part) => part.trim() !== "").join(`
4221
-
4222
- `);
4223
- }
4224
4364
  async function streamCompletedCodexOutput(args, state, payloadContext, params) {
4225
4365
  if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
4226
4366
  return;
@@ -5012,20 +5152,27 @@ function runtimeSettingsFromOptions(options) {
5012
5152
  return {
5013
5153
  model: options.channelSession.model,
5014
5154
  reasoningEffort: options.channelSession.reasoningEffort,
5015
- approvalPolicy: options.channelSession.approvalPolicy,
5155
+ approvalPolicy: codexApprovalPolicySetting(options.channelSession.approvalPolicy, options.channelSession.sandbox),
5016
5156
  sandbox: options.channelSession.sandbox,
5017
5157
  fast: options.fast
5018
5158
  };
5019
5159
  }
5020
5160
  function mergeRuntimeSettings(current, update) {
5161
+ const sandbox = mergeOptionalStringRuntimeSetting(current.sandbox, update, "sandbox");
5021
5162
  return {
5022
5163
  model: mergeOptionalStringRuntimeSetting(current.model, update, "model"),
5023
5164
  reasoningEffort: mergeOptionalStringRuntimeSetting(current.reasoningEffort, update, "reasoningEffort"),
5024
- approvalPolicy: mergeOptionalStringRuntimeSetting(current.approvalPolicy, update, "approvalPolicy"),
5025
- sandbox: mergeOptionalStringRuntimeSetting(current.sandbox, update, "sandbox"),
5165
+ approvalPolicy: mergeOptionalApprovalPolicyRuntimeSetting(current.approvalPolicy, update, sandbox),
5166
+ sandbox,
5026
5167
  fast: update.fast ?? current.fast
5027
5168
  };
5028
5169
  }
5170
+ function mergeOptionalApprovalPolicyRuntimeSetting(current, update, sandbox) {
5171
+ if (Object.prototype.hasOwnProperty.call(update, "approvalPolicy")) {
5172
+ return codexApprovalPolicySetting(update.approvalPolicy ?? undefined, sandbox);
5173
+ }
5174
+ return codexApprovalPolicySetting(current, sandbox);
5175
+ }
5029
5176
  function mergeOptionalStringRuntimeSetting(current, update, key) {
5030
5177
  if (Object.prototype.hasOwnProperty.call(update, key)) {
5031
5178
  return update[key] ?? undefined;
@@ -6249,30 +6396,14 @@ function prepareCodeServerLaunch(options) {
6249
6396
  if (platform !== "darwin") {
6250
6397
  return filesystemSandboxUnavailable();
6251
6398
  }
6252
- const sandboxExecBin = options.sandboxExecBin ?? "/usr/bin/sandbox-exec";
6253
- if (!existsSync2(sandboxExecBin)) {
6254
- return filesystemSandboxUnavailable();
6255
- }
6256
6399
  const codeServerExecutable = resolveCodeServerExecutable(options.codeServerBin, options.envPath ?? process.env.PATH ?? "");
6257
6400
  if (!codeServerExecutable.ok) {
6258
6401
  return filesystemSandboxUnavailable();
6259
6402
  }
6260
6403
  return {
6261
6404
  ok: true,
6262
- command: sandboxExecBin,
6263
- args: [
6264
- "-p",
6265
- codeServerSandboxProfile(options, codeServerExecutable.directory),
6266
- "--",
6267
- "/bin/sh",
6268
- "-c",
6269
- 'export HOME="$1"; export PWD="$1"; export TMPDIR="$2"; export TMP="$2"; export TEMP="$2"; shift 2; exec "$@"',
6270
- "kandan-code-server-env",
6271
- options.cwd,
6272
- join4(options.userDataDir, "tmp"),
6273
- codeServerExecutable.command,
6274
- ...codeServerArgs(options.port, options.cwd, options.userDataDir, options.extensionsDir)
6275
- ]
6405
+ command: codeServerExecutable.command,
6406
+ args: codeServerArgs(options.port, options.cwd, options.userDataDir, options.extensionsDir)
6276
6407
  };
6277
6408
  }
6278
6409
  function prepareLinuxCodeServerLaunch(options) {
@@ -6343,43 +6474,6 @@ function filesystemSandboxUnavailable() {
6343
6474
  reason: "local_editor_filesystem_sandbox_unavailable"
6344
6475
  };
6345
6476
  }
6346
- function codeServerSandboxProfile(options, codeServerBinDir) {
6347
- const readOnlyRoots = uniquePaths([
6348
- "/System",
6349
- "/Library",
6350
- "/usr",
6351
- "/bin",
6352
- "/sbin",
6353
- "/etc",
6354
- "/private/etc",
6355
- "/opt/homebrew",
6356
- ...options.codeServerRuntimeRoot === undefined ? [] : sandboxPathAliases(options.codeServerRuntimeRoot),
6357
- codeServerBinDir
6358
- ]);
6359
- const readWriteRoots = uniquePaths([
6360
- ...sandboxPathAliases(options.cwd),
6361
- ...sandboxPathAliases(options.userDataDir),
6362
- ...options.extensionsDir === undefined ? [] : sandboxPathAliases(options.extensionsDir)
6363
- ]);
6364
- return [
6365
- "(version 1)",
6366
- "(deny default)",
6367
- "(allow process*)",
6368
- "(allow signal (target self))",
6369
- "(allow signal (target same-sandbox))",
6370
- "(allow sysctl*)",
6371
- "(allow mach*)",
6372
- "(allow ipc*)",
6373
- "(allow network*)",
6374
- "(allow file-read-metadata)",
6375
- "(allow file-map-executable)",
6376
- '(allow file-read* (literal "/") (literal "/private") (literal "/private/var"))',
6377
- `(allow file-read* ${readOnlyRoots.map(sandboxSubpath).join(" ")})`,
6378
- '(allow file-read* file-write* (subpath "/dev"))',
6379
- `(allow file-read* file-write* ${readWriteRoots.map(sandboxSubpath).join(" ")})`
6380
- ].join(`
6381
- `);
6382
- }
6383
6477
  function resolveCodeServerExecutable(command, envPath) {
6384
6478
  if (hasPathSeparator(command)) {
6385
6479
  const directory = safeRealpathDir(command);
@@ -6420,9 +6514,6 @@ function sandboxPathAliases(path) {
6420
6514
  function uniquePaths(paths) {
6421
6515
  return Array.from(new Set(paths.filter((path) => path.length > 0)));
6422
6516
  }
6423
- function sandboxSubpath(path) {
6424
- return `(subpath "${path.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}")`;
6425
- }
6426
6517
  function prepareLocalEditorCollaboration(collaboration, runnerId, serverPort, browserBaseUrl) {
6427
6518
  if (collaboration === undefined || serverPort === undefined) {
6428
6519
  return;
@@ -6548,19 +6639,11 @@ function installDirectory(sourceDir, destinationDir) {
6548
6639
  mkdirSync3(dirname2(destinationDir), { recursive: true });
6549
6640
  cpSync(sourceDir, destinationDir, { recursive: true });
6550
6641
  }
6551
- function codeServerEnv(env, cwd, userDataDir, collaboration) {
6642
+ function codeServerEnv(env, cwd, _userDataDir, collaboration) {
6552
6643
  const { PORT: _port, ...hostEnv } = env;
6553
- const tempDir = join4(userDataDir, "tmp");
6554
6644
  const base = {
6555
6645
  ...hostEnv,
6556
- HOME: cwd,
6557
- PWD: cwd,
6558
- TMPDIR: tempDir,
6559
- TMP: tempDir,
6560
- TEMP: tempDir,
6561
- XDG_CACHE_HOME: join4(userDataDir, "xdg-cache"),
6562
- XDG_CONFIG_HOME: join4(userDataDir, "xdg-config"),
6563
- XDG_DATA_HOME: join4(userDataDir, "xdg-data")
6646
+ PWD: cwd
6564
6647
  };
6565
6648
  if (collaboration === undefined) {
6566
6649
  return base;
@@ -7891,6 +7974,356 @@ function waitForOpen2(websocket) {
7891
7974
  });
7892
7975
  }
7893
7976
 
7977
+ // src/runnerLock.ts
7978
+ import {
7979
+ closeSync,
7980
+ existsSync as existsSync5,
7981
+ mkdirSync as mkdirSync6,
7982
+ openSync as openSync2,
7983
+ readFileSync as readFileSync4,
7984
+ unlinkSync as unlinkSync2,
7985
+ writeSync
7986
+ } from "node:fs";
7987
+ import { dirname as dirname5, join as join7 } from "node:path";
7988
+
7989
+ // src/localConfig.ts
7990
+ import { randomUUID as randomUUID2 } from "node:crypto";
7991
+ import {
7992
+ existsSync as existsSync4,
7993
+ linkSync,
7994
+ mkdirSync as mkdirSync5,
7995
+ readFileSync as readFileSync3,
7996
+ realpathSync as realpathSync4,
7997
+ unlinkSync,
7998
+ writeFileSync as writeFileSync4
7999
+ } from "node:fs";
8000
+ import { homedir as homedir5 } from "node:os";
8001
+ import { basename as basename4, dirname as dirname4, join as join6, resolve as resolve5 } from "node:path";
8002
+ function localConfigPath(env = process.env) {
8003
+ const override = env.LINZUMI_CONFIG_FILE;
8004
+ return override !== undefined && override.trim() !== "" ? resolve5(expandUserPath(override)) : resolve5(homedir5(), ".linzumi", "config.json");
8005
+ }
8006
+ function readLocalConfig(path = localConfigPath()) {
8007
+ if (!existsSync4(path)) {
8008
+ return { version: 1, allowedCwds: [] };
8009
+ }
8010
+ const parsed = JSON.parse(readFileSync3(path, "utf8"));
8011
+ if (!isConfigPayload(parsed)) {
8012
+ throw new Error(`invalid Linzumi config: ${path}`);
8013
+ }
8014
+ const allowedCwds = uniqueStrings(parsed.allowedCwds);
8015
+ return parsed.machineId === undefined ? { version: 1, allowedCwds } : { version: 1, machineId: parsed.machineId, allowedCwds };
8016
+ }
8017
+ function ensureLocalMachineId(path = localConfigPath(), createMachineId = randomUUID2) {
8018
+ const config = readLocalConfig(path);
8019
+ if (config.machineId !== undefined) {
8020
+ return config.machineId;
8021
+ }
8022
+ const machineId = ensureLocalMachineIdSeed(path, createMachineId);
8023
+ const latestConfig = readLocalConfig(path);
8024
+ const latestMachineId = latestConfig.machineId;
8025
+ if (latestMachineId !== undefined) {
8026
+ return latestMachineId;
8027
+ }
8028
+ writeLocalConfig({ ...latestConfig, machineId }, path);
8029
+ return machineId;
8030
+ }
8031
+ function localMachineIdSeedPath(configPath = localConfigPath()) {
8032
+ return join6(dirname4(configPath), `${basename4(configPath)}.machine-id`);
8033
+ }
8034
+ function readConfiguredAllowedCwdDetails(path = localConfigPath()) {
8035
+ const allowedCwds = [];
8036
+ const missingAllowedCwds = [];
8037
+ for (const cwd of readLocalConfig(path).allowedCwds) {
8038
+ const absolutePath = resolve5(expandUserPath(cwd));
8039
+ try {
8040
+ const realPath = realpathSync4(absolutePath);
8041
+ allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
8042
+ } catch (error) {
8043
+ if (isMissingPathError(error)) {
8044
+ missingAllowedCwds.push(absolutePath);
8045
+ continue;
8046
+ }
8047
+ throw error;
8048
+ }
8049
+ }
8050
+ return {
8051
+ allowedCwds: uniqueStrings(allowedCwds),
8052
+ missingAllowedCwds: uniqueStrings(missingAllowedCwds)
8053
+ };
8054
+ }
8055
+ function addAllowedCwd(pathValue, path = localConfigPath()) {
8056
+ const normalizedPath = realpathSync4(resolve5(expandUserPath(pathValue)));
8057
+ const config = readLocalConfig(path);
8058
+ const allowedCwds = uniqueStrings([...config.allowedCwds, normalizedPath]);
8059
+ writeLocalConfig({ ...config, version: 1, allowedCwds }, path);
8060
+ return allowedCwds;
8061
+ }
8062
+ function removeAllowedCwd(pathValue, path = localConfigPath()) {
8063
+ const requestedPath = resolve5(expandUserPath(pathValue));
8064
+ const normalizedRequest = realpathOrResolved(requestedPath);
8065
+ const config = readLocalConfig(path);
8066
+ const allowedCwds = config.allowedCwds.filter((cwd) => {
8067
+ const normalizedExisting = realpathOrResolved(cwd);
8068
+ return cwd !== pathValue && normalizedExisting !== normalizedRequest;
8069
+ });
8070
+ writeLocalConfig({ ...config, version: 1, allowedCwds }, path);
8071
+ return allowedCwds;
8072
+ }
8073
+ function writeLocalConfig(config, path = localConfigPath()) {
8074
+ mkdirSync5(dirname4(path), { recursive: true });
8075
+ writeFileSync4(path, `${JSON.stringify(config, null, 2)}
8076
+ `, "utf8");
8077
+ }
8078
+ function isConfigPayload(value) {
8079
+ return typeof value === "object" && value !== null && value.version === 1 && Array.isArray(value.allowedCwds) && machineIdValid(value.machineId) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
8080
+ }
8081
+ function machineIdValid(value) {
8082
+ return value === undefined || typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
8083
+ }
8084
+ function ensureLocalMachineIdSeed(configPath, createMachineId) {
8085
+ const seedPath = localMachineIdSeedPath(configPath);
8086
+ if (existsSync4(seedPath)) {
8087
+ return readMachineIdSeed(seedPath);
8088
+ }
8089
+ const machineId = createMachineId();
8090
+ if (!machineIdValid(machineId)) {
8091
+ throw new Error(`invalid generated Linzumi machine id: ${machineId}`);
8092
+ }
8093
+ mkdirSync5(dirname4(seedPath), { recursive: true });
8094
+ const tempPath = join6(dirname4(seedPath), `.${basename4(seedPath)}.${process.pid}.${randomUUID2()}.tmp`);
8095
+ writeFileSync4(tempPath, `${machineId}
8096
+ `, { encoding: "utf8", flag: "wx" });
8097
+ try {
8098
+ linkSync(tempPath, seedPath);
8099
+ return machineId;
8100
+ } catch (error) {
8101
+ if (isNodeErrorCode(error, "EEXIST")) {
8102
+ return readMachineIdSeed(seedPath);
8103
+ }
8104
+ throw error;
8105
+ } finally {
8106
+ unlinkSync(tempPath);
8107
+ }
8108
+ }
8109
+ function readMachineIdSeed(seedPath) {
8110
+ const machineId = readFileSync3(seedPath, "utf8").trim();
8111
+ if (!machineIdValid(machineId)) {
8112
+ throw new Error(`invalid Linzumi machine id seed: ${seedPath}`);
8113
+ }
8114
+ return machineId;
8115
+ }
8116
+ function uniqueStrings(values) {
8117
+ return [
8118
+ ...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))
8119
+ ];
8120
+ }
8121
+ function isMissingPathError(error) {
8122
+ return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "ELOOP" || error.code === "EIO");
8123
+ }
8124
+ function isNodeErrorCode(error, code) {
8125
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
8126
+ }
8127
+ function realpathOrResolved(pathValue) {
8128
+ try {
8129
+ return realpathSync4(resolve5(expandUserPath(pathValue)));
8130
+ } catch (_error) {
8131
+ return resolve5(expandUserPath(pathValue));
8132
+ }
8133
+ }
8134
+
8135
+ // src/version.ts
8136
+ var linzumiCliVersion = "0.0.42-beta";
8137
+ var linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
8138
+
8139
+ // src/runnerLock.ts
8140
+ function runnerLockPath(machineId, configPath = localConfigPath()) {
8141
+ return join7(dirname5(configPath), "runners", `${encodeURIComponent(machineId)}.lock`);
8142
+ }
8143
+ function acquireRunnerLock(options) {
8144
+ const path = runnerLockPath(options.machineId, options.configPath);
8145
+ const isPidAlive = options.isPidAlive ?? processIsAlive;
8146
+ const record = {
8147
+ version: 1,
8148
+ machineId: options.machineId,
8149
+ runnerId: options.runnerId,
8150
+ pid: options.pid ?? process.pid,
8151
+ cwd: options.cwd,
8152
+ workspace: options.workspace,
8153
+ startedAt: (options.now ?? (() => new Date))().toISOString(),
8154
+ cliVersion: options.cliVersion ?? linzumiCliVersion
8155
+ };
8156
+ writeLockOrHandleExisting(path, record, isPidAlive, options.beforeReadExistingLock, options.beforeReplaceStaleLock);
8157
+ return {
8158
+ path,
8159
+ record,
8160
+ release: () => releaseRunnerLock(path, record)
8161
+ };
8162
+ }
8163
+ function writeLockOrHandleExisting(path, record, isPidAlive, beforeReadExistingLock, beforeReplaceStaleLock) {
8164
+ if (tryCreateLock(path, record)) {
8165
+ return;
8166
+ }
8167
+ beforeReadExistingLock?.();
8168
+ const existing = readRunnerLockIfPresent(path);
8169
+ if (existing === undefined) {
8170
+ if (tryCreateLock(path, record)) {
8171
+ return;
8172
+ }
8173
+ throw new Error(`another Linzumi runner lock appeared while starting: ${path}`);
8174
+ }
8175
+ if (isPidAlive(existing.pid)) {
8176
+ throw new Error(activeRunnerLockMessage(path, existing));
8177
+ }
8178
+ beforeReplaceStaleLock?.();
8179
+ withStaleReplacementLock(path, isPidAlive, () => {
8180
+ const latest = existsSync5(path) ? readRunnerLock(path) : undefined;
8181
+ if (latest !== undefined && isPidAlive(latest.pid)) {
8182
+ throw new Error(activeRunnerLockMessage(path, latest));
8183
+ }
8184
+ if (latest !== undefined) {
8185
+ unlinkSync2(path);
8186
+ }
8187
+ if (!tryCreateLock(path, record)) {
8188
+ throw new Error(`another Linzumi runner lock appeared while starting: ${path}`);
8189
+ }
8190
+ });
8191
+ }
8192
+ function tryCreateLock(path, record) {
8193
+ mkdirSync6(dirname5(path), { recursive: true });
8194
+ try {
8195
+ const fd = openSync2(path, "wx");
8196
+ try {
8197
+ writeSync(fd, `${JSON.stringify(record, null, 2)}
8198
+ `);
8199
+ } finally {
8200
+ closeSync(fd);
8201
+ }
8202
+ return true;
8203
+ } catch (error) {
8204
+ if (isNodeErrorCode2(error, "EEXIST")) {
8205
+ return false;
8206
+ }
8207
+ throw error;
8208
+ }
8209
+ }
8210
+ function withStaleReplacementLock(path, isPidAlive, callback) {
8211
+ const replacementPath = `${path}.replace`;
8212
+ while (true) {
8213
+ try {
8214
+ const fd = openSync2(replacementPath, "wx");
8215
+ try {
8216
+ writeSync(fd, `${process.pid}
8217
+ `);
8218
+ callback();
8219
+ } finally {
8220
+ closeSync(fd);
8221
+ unlinkSync2(replacementPath);
8222
+ }
8223
+ return;
8224
+ } catch (error) {
8225
+ if (isNodeErrorCode2(error, "EEXIST")) {
8226
+ const replacementPid = readReplacementLockPidIfPresent(replacementPath);
8227
+ if (replacementPid === undefined) {
8228
+ continue;
8229
+ }
8230
+ if (isPidAlive(replacementPid)) {
8231
+ throw new Error([
8232
+ "another Linzumi runner is already replacing a stale runner lock",
8233
+ `lock: ${path}`,
8234
+ "Wait for that startup to finish, then retry."
8235
+ ].join(`
8236
+ `));
8237
+ }
8238
+ unlinkSync2(replacementPath);
8239
+ continue;
8240
+ }
8241
+ throw error;
8242
+ }
8243
+ }
8244
+ }
8245
+ function readReplacementLockPidIfPresent(path) {
8246
+ let value;
8247
+ try {
8248
+ value = readFileSync4(path, "utf8").trim();
8249
+ } catch (error) {
8250
+ if (isNodeErrorCode2(error, "ENOENT")) {
8251
+ return;
8252
+ }
8253
+ throw error;
8254
+ }
8255
+ const pid = Number.parseInt(value, 10);
8256
+ if (pid.toString() !== value || pid <= 0) {
8257
+ throw new Error(`invalid Linzumi runner replacement lock: ${path}`);
8258
+ }
8259
+ return pid;
8260
+ }
8261
+ function releaseRunnerLock(path, record) {
8262
+ const current = readRunnerLockForRelease(path);
8263
+ if (current !== undefined && current.machineId === record.machineId && current.runnerId === record.runnerId && current.pid === record.pid) {
8264
+ unlinkSync2(path);
8265
+ }
8266
+ }
8267
+ function readRunnerLockForRelease(path) {
8268
+ try {
8269
+ return readRunnerLockIfPresent(path);
8270
+ } catch (_error) {
8271
+ return;
8272
+ }
8273
+ }
8274
+ function readRunnerLockIfPresent(path) {
8275
+ if (!existsSync5(path)) {
8276
+ return;
8277
+ }
8278
+ try {
8279
+ return readRunnerLock(path);
8280
+ } catch (error) {
8281
+ if (isNodeErrorCode2(error, "ENOENT")) {
8282
+ return;
8283
+ }
8284
+ throw error;
8285
+ }
8286
+ }
8287
+ function readRunnerLock(path) {
8288
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
8289
+ if (!isRunnerLockRecord(parsed)) {
8290
+ throw new Error(`invalid Linzumi runner lock: ${path}`);
8291
+ }
8292
+ return parsed;
8293
+ }
8294
+ function isRunnerLockRecord(value) {
8295
+ return typeof value === "object" && value !== null && value.version === 1 && typeof value.machineId === "string" && value.machineId.trim() !== "" && typeof value.runnerId === "string" && value.runnerId.trim() !== "" && Number.isInteger(value.pid) && value.pid > 0 && typeof value.cwd === "string" && value.cwd.trim() !== "" && workspaceValid(value.workspace) && typeof value.startedAt === "string" && value.startedAt.trim() !== "" && typeof value.cliVersion === "string" && value.cliVersion.trim() !== "";
8296
+ }
8297
+ function workspaceValid(value) {
8298
+ return value === null || typeof value === "string" && value.trim() !== "";
8299
+ }
8300
+ function processIsAlive(pid) {
8301
+ try {
8302
+ process.kill(pid, 0);
8303
+ return true;
8304
+ } catch (error) {
8305
+ return isNodeErrorCode2(error, "ESRCH") ? false : true;
8306
+ }
8307
+ }
8308
+ function activeRunnerLockMessage(path, record) {
8309
+ const workspace = record.workspace === null ? "workspace: unknown" : `workspace: ${record.workspace}`;
8310
+ return [
8311
+ "another Linzumi runner is already running for this machine",
8312
+ `runner id: ${record.runnerId}`,
8313
+ `pid: ${record.pid}`,
8314
+ `cwd: ${record.cwd}`,
8315
+ workspace,
8316
+ `CLI version: ${record.cliVersion}`,
8317
+ `started at: ${record.startedAt}`,
8318
+ `lock: ${path}`,
8319
+ "Stop that process first, then retry. If the process has already exited, remove the stale lock file and retry."
8320
+ ].join(`
8321
+ `);
8322
+ }
8323
+ function isNodeErrorCode2(error, code) {
8324
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
8325
+ }
8326
+
7894
8327
  // src/runnerConsoleReporter.ts
7895
8328
  function reportRunnerConsoleEvent(event, payload) {
7896
8329
  const line = formatRunnerConsoleEvent(event, payload);
@@ -7902,7 +8335,15 @@ function reportRunnerConsoleEvent(event, payload) {
7902
8335
  function formatRunnerConsoleEvent(event, payload) {
7903
8336
  switch (event) {
7904
8337
  case "runner.instance_started":
7905
- return `Runner connected: instance=${text(payload.instanceId)} codex=${text(payload.codexUrl)}`;
8338
+ return connectedRunnerMessage(payload);
8339
+ case "runner.replaced":
8340
+ return [
8341
+ "Runner replaced: another Linzumi CLI connected from this machine.",
8342
+ `New runner: ${text(payload.replacementRunnerId)}`,
8343
+ `Version: ${text(payload.replacementVersion)}`,
8344
+ "This process is exiting."
8345
+ ].join(`
8346
+ `);
7906
8347
  case "kandan.message_ignored":
7907
8348
  return `Incoming message from ${sender(payload)}: ignored for reason ${text(payload.reason)}`;
7908
8349
  case "kandan.message_queued":
@@ -7937,6 +8378,40 @@ function formatRunnerConsoleEvent(event, payload) {
7937
8378
  return;
7938
8379
  }
7939
8380
  }
8381
+ function connectedRunnerMessage(payload) {
8382
+ return [
8383
+ "Connected to Linzumi",
8384
+ optionalLine("Computer", payload.hostname),
8385
+ optionalLine("Workspace", payload.workspace),
8386
+ `Runner: ${text(payload.runnerId)}`,
8387
+ `CLI: ${text(payload.version)}`,
8388
+ optionalLine("Codex", payload.codexUrl),
8389
+ ...replacementLines(payload.replacedRunners)
8390
+ ].filter((line) => line !== undefined).join(`
8391
+ `);
8392
+ }
8393
+ function optionalLine(label, value) {
8394
+ const normalized = stringValue2(value) ?? numberValue(value)?.toString();
8395
+ return normalized === undefined ? undefined : `${label}: ${normalized}`;
8396
+ }
8397
+ function replacementLines(value) {
8398
+ if (!Array.isArray(value)) {
8399
+ return [];
8400
+ }
8401
+ return value.flatMap((entry) => {
8402
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
8403
+ return [];
8404
+ }
8405
+ const record = entry;
8406
+ const runnerId = stringValue2(record.runnerId);
8407
+ if (runnerId === undefined) {
8408
+ return [];
8409
+ }
8410
+ const version = stringValue2(record.version);
8411
+ const suffix = version === undefined ? "" : ` (CLI ${version})`;
8412
+ return [`Replaced older runner from this machine: ${runnerId}${suffix}`];
8413
+ });
8414
+ }
7940
8415
  function sender(payload) {
7941
8416
  const slug = stringValue2(payload.actor_slug);
7942
8417
  const userId = numberValue(payload.actor_user_id);
@@ -8000,6 +8475,21 @@ async function runLocalCodexRunner(options) {
8000
8475
  kandanUrl: options.kandanUrl
8001
8476
  });
8002
8477
  try {
8478
+ if (options.machineId !== undefined) {
8479
+ const runnerLock = acquireRunnerLock({
8480
+ machineId: options.machineId,
8481
+ runnerId: options.runnerId,
8482
+ cwd: options.cwd,
8483
+ workspace: runnerWorkspaceSlug(options) ?? null,
8484
+ configPath: options.runnerLockConfigPath
8485
+ });
8486
+ cleanup.actions.push(() => runnerLock.release());
8487
+ log("runner.lock_acquired", {
8488
+ path: runnerLock.path,
8489
+ machineId: options.machineId,
8490
+ runnerId: options.runnerId
8491
+ });
8492
+ }
8003
8493
  return await openLocalCodexRunner(options, log, cleanup, close);
8004
8494
  } catch (error) {
8005
8495
  await close().catch(() => {
@@ -8067,9 +8557,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8067
8557
  const kandan = await connectPhoenixClient(options.kandanUrl, options.token, options.socketFactory);
8068
8558
  cleanup.actions.push(() => kandan.close());
8069
8559
  const topic = `local_runner:${options.runnerId}`;
8560
+ const clientId = options.machineId ?? options.runnerId;
8070
8561
  const joinPayload = () => ({
8071
8562
  clientName: "kandan-local-codex-runner",
8072
- version: "0.0.1",
8563
+ clientId,
8564
+ version: linzumiCliVersion,
8073
8565
  workspace: runnerWorkspaceSlug(options) ?? null,
8074
8566
  channel: options.channelSession?.channelSlug ?? null,
8075
8567
  capabilities: capabilitiesPayload()
@@ -8084,7 +8576,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8084
8576
  }
8085
8577
  dispatcher(control);
8086
8578
  });
8087
- await kandan.join(topic, joinPayload(), { rejoinPayload: joinPayload });
8579
+ const joinResponse = await kandan.join(topic, joinPayload(), {
8580
+ rejoinPayload: joinPayload
8581
+ });
8582
+ const replacedRunners = replacementRunnerSummaries(objectValue(joinResponse)?.replaced_runners);
8088
8583
  const started = options.codexUrl === undefined ? await startOwnedCodexAppServer(options) : undefined;
8089
8584
  if (started !== undefined) {
8090
8585
  cleanup.actions.push(() => {
@@ -8095,7 +8590,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8095
8590
  if (codexUrl === undefined) {
8096
8591
  throw new Error("missing codex app-server websocket URL");
8097
8592
  }
8098
- const instanceId = `codex-${randomUUID2()}`;
8593
+ const instanceId = `codex-${randomUUID3()}`;
8099
8594
  const publishLocalEditorStatus = (payload) => {
8100
8595
  kandan.push(topic, "local_editor_status", payload).catch((error) => {
8101
8596
  log("kandan.local_editor_status_push_failed", {
@@ -8143,6 +8638,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8143
8638
  const runtimeDefaults = runnerRuntimeDefaults(options);
8144
8639
  const instancePayload = {
8145
8640
  instanceId,
8641
+ clientId,
8146
8642
  codexUrl,
8147
8643
  tuiLaunched: options.launchTui,
8148
8644
  cwd: options.cwd,
@@ -8155,7 +8651,15 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8155
8651
  fast: options.fast ?? false
8156
8652
  };
8157
8653
  await kandan.push(topic, "instance_started", instancePayload);
8158
- log("runner.instance_started", { instanceId, codexUrl });
8654
+ log("runner.instance_started", {
8655
+ runnerId: options.runnerId,
8656
+ hostname: runnerHost,
8657
+ workspace: runnerWorkspaceSlug(options) ?? null,
8658
+ version: linzumiCliVersion,
8659
+ instanceId,
8660
+ codexUrl,
8661
+ replacedRunners
8662
+ });
8159
8663
  const channelSession = options.channelSession === undefined ? undefined : await attachChannelSession({
8160
8664
  kandan,
8161
8665
  codex,
@@ -8256,6 +8760,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8256
8760
  };
8257
8761
  const heartbeatPayload = () => ({
8258
8762
  instanceId,
8763
+ clientId,
8259
8764
  codexUrl,
8260
8765
  cwd: options.cwd,
8261
8766
  hostname: runnerHost,
@@ -8334,6 +8839,20 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
8334
8839
  });
8335
8840
  const handleControl = (control) => {
8336
8841
  log("kandan.control", { control });
8842
+ if (control.type === "replace_runner") {
8843
+ log("runner.replaced", {
8844
+ runnerId: options.runnerId,
8845
+ reason: control.reason,
8846
+ replacementRunnerId: control.replacementRunnerId,
8847
+ replacementVersion: control.replacementVersion
8848
+ });
8849
+ close().catch((error) => {
8850
+ log("runner.replace_close_failed", {
8851
+ message: error instanceof Error ? error.message : String(error)
8852
+ });
8853
+ });
8854
+ return;
8855
+ }
8337
8856
  if (!controlTargetsInstance(control, instanceId)) {
8338
8857
  log("kandan.control_ignored", {
8339
8858
  reason: "instance_id_mismatch",
@@ -8548,8 +9067,24 @@ function normalizedWorkDescription(value) {
8548
9067
  const normalized = value?.trim();
8549
9068
  return normalized === undefined || normalized === "" ? undefined : normalized;
8550
9069
  }
9070
+ function replacementRunnerSummaries(value) {
9071
+ const entries = arrayValue(value) ?? [];
9072
+ return entries.flatMap((entry) => {
9073
+ const record = objectValue(entry);
9074
+ const runnerId = stringValue(record?.runnerId);
9075
+ if (runnerId === undefined) {
9076
+ return [];
9077
+ }
9078
+ return [
9079
+ {
9080
+ runnerId,
9081
+ version: stringValue(record?.version) ?? null
9082
+ }
9083
+ ];
9084
+ });
9085
+ }
8551
9086
  function makeRunnerLogger(options) {
8552
- return createRunnerLogger(options.logFile ?? join6(options.cwd, ".linzumi-runner.log"), options.launchTui ? undefined : reportRunnerConsoleEvent);
9087
+ return createRunnerLogger(options.logFile ?? join8(options.cwd, ".linzumi-runner.log"), options.launchTui ? undefined : reportRunnerConsoleEvent);
8553
9088
  }
8554
9089
  function installCleanupHandlers(close) {
8555
9090
  const closeAndExit = () => {
@@ -8654,6 +9189,8 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
8654
9189
  switch (control.type) {
8655
9190
  case "start_instance": {
8656
9191
  const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
9192
+ let startupStage = "starting_codex_session";
9193
+ let startedCodexThreadId;
8657
9194
  if (!cwd.ok) {
8658
9195
  return {
8659
9196
  instanceId,
@@ -8672,8 +9209,10 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
8672
9209
  }
8673
9210
  try {
8674
9211
  if (options.codexUrl === undefined) {
9212
+ startupStage = "checking_project_trust";
8675
9213
  ensureCodexProjectTrusted(cwd.cwd);
8676
9214
  }
9215
+ startupStage = "starting_codex_session";
8677
9216
  const developerPrompt = normalizedWorkDescription(control.developerPrompt);
8678
9217
  const runtimeSettings = startInstanceRuntimeSettings(options, control);
8679
9218
  const response = await codex.request("thread/start", {
@@ -8691,12 +9230,18 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
8691
9230
  ...runtimeSettings.fast === true ? { serviceTier: "fast" } : {}
8692
9231
  });
8693
9232
  const codexThreadId = extractStartedThreadId(response);
9233
+ startedCodexThreadId = codexThreadId;
8694
9234
  const workDescription = normalizedWorkDescription(control.workDescription);
8695
9235
  if (codexThreadId !== undefined && developerPrompt !== undefined) {
8696
9236
  await postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId);
8697
9237
  }
8698
- const startedThreadSession = codexThreadId !== undefined && onStartedThread !== undefined ? await onStartedThread(control, cwd.cwd, codexThreadId) : undefined;
9238
+ let startedThreadSession;
9239
+ if (codexThreadId !== undefined && onStartedThread !== undefined) {
9240
+ startupStage = "binding_kandan_thread";
9241
+ startedThreadSession = await onStartedThread(control, cwd.cwd, codexThreadId);
9242
+ }
8699
9243
  if (codexThreadId !== undefined && workDescription !== undefined) {
9244
+ startupStage = "starting_first_turn";
8700
9245
  const rootSeq = integerValue(control.rootSeq);
8701
9246
  const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
8702
9247
  if (startedThreadSession !== undefined && sourceSeq !== undefined) {
@@ -8722,8 +9267,16 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
8722
9267
  response
8723
9268
  };
8724
9269
  } catch (error) {
9270
+ const failureReason = startInstanceFailureReason(startupStage, error);
9271
+ log("kandan.start_instance_failed", {
9272
+ stage: startupStage,
9273
+ message: error instanceof Error ? error.message : String(error),
9274
+ thread_id: optionalThreadControlField(control, "threadId") ?? null,
9275
+ source_seq: integerValue(control.sourceSeq) ?? integerValue(control.rootSeq) ?? null,
9276
+ codex_thread_id: startedCodexThreadId ?? null
9277
+ });
8725
9278
  try {
8726
- await publishStartInstanceMessageState(kandan, topic, control, "failed", "failed to start Codex session");
9279
+ await publishStartInstanceMessageState(kandan, topic, control, "failed", failureReason, { codexThreadId: startedCodexThreadId });
8727
9280
  } catch (_publishError) {}
8728
9281
  throw error;
8729
9282
  }
@@ -8835,6 +9388,26 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
8835
9388
  return { instanceId, controlType: control.type, skipped: true };
8836
9389
  }
8837
9390
  }
9391
+ function startInstanceFailureReason(stage, error) {
9392
+ const detail = truncateFailureDetail(error instanceof Error ? error.message : String(error));
9393
+ switch (stage) {
9394
+ case "checking_project_trust":
9395
+ return `Codex could not start because this folder is not trusted locally. Trust the folder with linzumi paths add, or restart the runner from a trusted folder. Detail: ${detail}`;
9396
+ case "starting_codex_session":
9397
+ return `Codex did not create a session. Restart the local runner and try again. Detail: ${detail}`;
9398
+ case "binding_kandan_thread":
9399
+ return `Codex started, but Linzumi could not attach it to this thread. Try starting the agent again; if it repeats, share this failed message with the runner owner. Detail: ${detail}`;
9400
+ case "starting_first_turn":
9401
+ return `Codex started, but the first prompt could not be delivered. Try sending the message again; if it repeats, reconnect the local runner. Detail: ${detail}`;
9402
+ }
9403
+ }
9404
+ function truncateFailureDetail(message) {
9405
+ const trimmed = message.trim();
9406
+ if (trimmed.length <= 240) {
9407
+ return trimmed;
9408
+ }
9409
+ return `${trimmed.slice(0, 237)}...`;
9410
+ }
8838
9411
  function commanderDeveloperInstructions(args) {
8839
9412
  const customPrompt = args.developerPrompt === undefined ? "" : `
8840
9413
  <invoker_developer_prompt>
@@ -8915,14 +9488,14 @@ ${developerPrompt}`,
8915
9488
  client_message_id: `codex-start-instructions-${threadId}`
8916
9489
  });
8917
9490
  }
8918
- async function publishStartInstanceMessageState(kandan, topic, control, status, reason) {
8919
- const payload = startInstanceMessageStatePayload(control, status, reason);
9491
+ async function publishStartInstanceMessageState(kandan, topic, control, status, reason, diagnostics = {}) {
9492
+ const payload = startInstanceMessageStatePayload(control, status, reason, diagnostics);
8920
9493
  if (payload === undefined) {
8921
9494
  return;
8922
9495
  }
8923
9496
  await kandan.push(topic, "message_state", payload);
8924
9497
  }
8925
- function startInstanceMessageStatePayload(control, status, reason) {
9498
+ function startInstanceMessageStatePayload(control, status, reason, diagnostics = {}) {
8926
9499
  const rootSeq = integerValue(control.rootSeq);
8927
9500
  const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
8928
9501
  if (sourceSeq === undefined) {
@@ -8937,7 +9510,8 @@ function startInstanceMessageStatePayload(control, status, reason) {
8937
9510
  thread_id: threadId,
8938
9511
  seq: sourceSeq,
8939
9512
  status,
8940
- reason
9513
+ reason,
9514
+ ...diagnostics.codexThreadId === undefined ? {} : { codex_thread_id: diagnostics.codexThreadId }
8941
9515
  };
8942
9516
  }
8943
9517
  function requiredStartInstanceControlField(control, field) {
@@ -8959,11 +9533,12 @@ function optionalThreadControlField(control, field) {
8959
9533
  }
8960
9534
  function startInstanceRuntimeSettings(options, control) {
8961
9535
  const defaults = runnerRuntimeDefaults(options);
9536
+ const sandbox = control.sandbox ?? defaults.sandbox;
8962
9537
  return {
8963
9538
  model: control.model ?? defaults.model,
8964
9539
  reasoningEffort: control.reasoningEffort ?? defaults.reasoningEffort,
8965
- approvalPolicy: control.approvalPolicy ?? defaults.approvalPolicy,
8966
- sandbox: control.sandbox ?? defaults.sandbox,
9540
+ approvalPolicy: codexApprovalPolicyForRequest(control.approvalPolicy ?? defaults.approvalPolicy, sandbox),
9541
+ sandbox,
8967
9542
  fast: control.fast ?? options.fast
8968
9543
  };
8969
9544
  }
@@ -8982,11 +9557,12 @@ function runnerWorkspaceSlug(options) {
8982
9557
  function runnerRuntimeDefaults(options) {
8983
9558
  const session = options.channelSession;
8984
9559
  const defaults = options.runtimeDefaults;
9560
+ const sandbox = defaults?.sandbox ?? session?.sandbox;
8985
9561
  return {
8986
9562
  model: defaults?.model ?? session?.model,
8987
9563
  reasoningEffort: defaults?.reasoningEffort ?? session?.reasoningEffort,
8988
- approvalPolicy: defaults?.approvalPolicy ?? session?.approvalPolicy,
8989
- sandbox: defaults?.sandbox ?? session?.sandbox
9564
+ approvalPolicy: codexApprovalPolicySetting(defaults?.approvalPolicy ?? session?.approvalPolicy, sandbox),
9565
+ sandbox
8990
9566
  };
8991
9567
  }
8992
9568
  function isUpdateRunnerConfigControl(control) {
@@ -9005,9 +9581,9 @@ function configuredAllowedCwds(values) {
9005
9581
  const allowedCwds = [];
9006
9582
  const missingAllowedCwds = [];
9007
9583
  for (const value of normalizeAllowedCwds(values)) {
9008
- const absolutePath = resolve5(expandUserPath(value));
9584
+ const absolutePath = resolve6(expandUserPath(value));
9009
9585
  try {
9010
- const realPath = realpathSync4(absolutePath);
9586
+ const realPath = realpathSync5(absolutePath);
9011
9587
  allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
9012
9588
  } catch (error) {
9013
9589
  if (isMissingAllowedCwdError(error)) {
@@ -9030,18 +9606,18 @@ function allowedCwdSuggestions(cwd, allowedCwds) {
9030
9606
  }
9031
9607
 
9032
9608
  // src/authCache.ts
9033
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
9034
- import { homedir as homedir5 } from "node:os";
9035
- import { dirname as dirname4, join as join7 } from "node:path";
9609
+ import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
9610
+ import { homedir as homedir6 } from "node:os";
9611
+ import { dirname as dirname6, join as join9 } from "node:path";
9036
9612
  function defaultAuthFilePath() {
9037
- const base = process.env.KANDAN_HOME ?? join7(homedir5(), ".kandan");
9038
- return join7(base, "auth.json");
9613
+ const base = process.env.KANDAN_HOME ?? join9(homedir6(), ".kandan");
9614
+ return join9(base, "auth.json");
9039
9615
  }
9040
9616
  function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePath()) {
9041
- if (!existsSync4(authFilePath)) {
9617
+ if (!existsSync6(authFilePath)) {
9042
9618
  return;
9043
9619
  }
9044
- const authFile = parseAuthFile(readFileSync3(authFilePath, "utf8"));
9620
+ const authFile = parseAuthFile(readFileSync5(authFilePath, "utf8"));
9045
9621
  const kandanBaseUrl = kandanHttpBaseUrl(kandanUrl);
9046
9622
  const entry = authFile.local_codex_runner?.[kandanBaseUrl];
9047
9623
  if (entry === undefined || entry.access_token.trim() === "") {
@@ -9059,7 +9635,7 @@ function readCachedLocalRunnerToken(kandanUrl, authFilePath = defaultAuthFilePat
9059
9635
  }
9060
9636
  function writeCachedLocalRunnerToken(args) {
9061
9637
  const authFilePath = args.authFilePath ?? defaultAuthFilePath();
9062
- const existing = existsSync4(authFilePath) ? parseAuthFile(readFileSync3(authFilePath, "utf8")) : { version: 1 };
9638
+ const existing = existsSync6(authFilePath) ? parseAuthFile(readFileSync5(authFilePath, "utf8")) : { version: 1 };
9063
9639
  const kandanBaseUrl = kandanHttpBaseUrl(args.kandanUrl);
9064
9640
  const issuedAt = new Date;
9065
9641
  const expiresAt = args.expiresInSeconds === undefined ? undefined : new Date(issuedAt.getTime() + args.expiresInSeconds * 1000).toISOString();
@@ -9075,8 +9651,8 @@ function writeCachedLocalRunnerToken(args) {
9075
9651
  }
9076
9652
  }
9077
9653
  };
9078
- mkdirSync5(dirname4(authFilePath), { recursive: true });
9079
- writeFileSync4(authFilePath, `${JSON.stringify(next, null, 2)}
9654
+ mkdirSync7(dirname6(authFilePath), { recursive: true });
9655
+ writeFileSync5(authFilePath, `${JSON.stringify(next, null, 2)}
9080
9656
  `, "utf8");
9081
9657
  return {
9082
9658
  accessToken: args.accessToken,
@@ -9168,102 +9744,12 @@ async function acquireAndCacheToken(args) {
9168
9744
  return token.accessToken;
9169
9745
  }
9170
9746
 
9171
- // src/localConfig.ts
9172
- import {
9173
- existsSync as existsSync5,
9174
- mkdirSync as mkdirSync6,
9175
- readFileSync as readFileSync4,
9176
- realpathSync as realpathSync5,
9177
- writeFileSync as writeFileSync5
9178
- } from "node:fs";
9179
- import { homedir as homedir6 } from "node:os";
9180
- import { dirname as dirname5, resolve as resolve6 } from "node:path";
9181
- function localConfigPath(env = process.env) {
9182
- const override = env.LINZUMI_CONFIG_FILE;
9183
- return override !== undefined && override.trim() !== "" ? resolve6(expandUserPath(override)) : resolve6(homedir6(), ".linzumi", "config.json");
9184
- }
9185
- function readLocalConfig(path = localConfigPath()) {
9186
- if (!existsSync5(path)) {
9187
- return { version: 1, allowedCwds: [] };
9188
- }
9189
- const parsed = JSON.parse(readFileSync4(path, "utf8"));
9190
- if (!isConfigPayload(parsed)) {
9191
- throw new Error(`invalid Linzumi config: ${path}`);
9192
- }
9193
- return {
9194
- version: 1,
9195
- allowedCwds: uniqueStrings(parsed.allowedCwds)
9196
- };
9197
- }
9198
- function readConfiguredAllowedCwdDetails(path = localConfigPath()) {
9199
- const allowedCwds = [];
9200
- const missingAllowedCwds = [];
9201
- for (const cwd of readLocalConfig(path).allowedCwds) {
9202
- const absolutePath = resolve6(expandUserPath(cwd));
9203
- try {
9204
- const realPath = realpathSync5(absolutePath);
9205
- allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
9206
- } catch (error) {
9207
- if (isMissingPathError(error)) {
9208
- missingAllowedCwds.push(absolutePath);
9209
- continue;
9210
- }
9211
- throw error;
9212
- }
9213
- }
9214
- return {
9215
- allowedCwds: uniqueStrings(allowedCwds),
9216
- missingAllowedCwds: uniqueStrings(missingAllowedCwds)
9217
- };
9218
- }
9219
- function addAllowedCwd(pathValue, path = localConfigPath()) {
9220
- const normalizedPath = realpathSync5(resolve6(expandUserPath(pathValue)));
9221
- const config = readLocalConfig(path);
9222
- const allowedCwds = uniqueStrings([...config.allowedCwds, normalizedPath]);
9223
- writeLocalConfig({ version: 1, allowedCwds }, path);
9224
- return allowedCwds;
9225
- }
9226
- function removeAllowedCwd(pathValue, path = localConfigPath()) {
9227
- const requestedPath = resolve6(expandUserPath(pathValue));
9228
- const normalizedRequest = realpathOrResolved(requestedPath);
9229
- const config = readLocalConfig(path);
9230
- const allowedCwds = config.allowedCwds.filter((cwd) => {
9231
- const normalizedExisting = realpathOrResolved(cwd);
9232
- return cwd !== pathValue && normalizedExisting !== normalizedRequest;
9233
- });
9234
- writeLocalConfig({ version: 1, allowedCwds }, path);
9235
- return allowedCwds;
9236
- }
9237
- function writeLocalConfig(config, path = localConfigPath()) {
9238
- mkdirSync6(dirname5(path), { recursive: true });
9239
- writeFileSync5(path, `${JSON.stringify(config, null, 2)}
9240
- `, "utf8");
9241
- }
9242
- function isConfigPayload(value) {
9243
- return typeof value === "object" && value !== null && value.version === 1 && Array.isArray(value.allowedCwds) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
9244
- }
9245
- function uniqueStrings(values) {
9246
- return [
9247
- ...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))
9248
- ];
9249
- }
9250
- function isMissingPathError(error) {
9251
- return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "ELOOP" || error.code === "EIO");
9252
- }
9253
- function realpathOrResolved(pathValue) {
9254
- try {
9255
- return realpathSync5(resolve6(expandUserPath(pathValue)));
9256
- } catch (_error) {
9257
- return resolve6(expandUserPath(pathValue));
9258
- }
9259
- }
9260
-
9261
9747
  // src/defaultUrls.ts
9262
9748
  var defaultLinzumiHttpUrl = "https://serve.linzumi.com";
9263
9749
  var defaultLinzumiWebSocketUrl = "wss://serve.linzumi.com";
9264
9750
 
9265
9751
  // src/kandanTls.ts
9266
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
9752
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
9267
9753
  import { Agent } from "undici";
9268
9754
  import { WebSocket as WsWebSocket } from "ws";
9269
9755
  function kandanTlsTrustFromEnv() {
@@ -9274,10 +9760,10 @@ function kandanTlsTrustFromCaFile(caFile) {
9274
9760
  return;
9275
9761
  }
9276
9762
  const trimmed = caFile.trim();
9277
- if (!existsSync6(trimmed)) {
9763
+ if (!existsSync7(trimmed)) {
9278
9764
  throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
9279
9765
  }
9280
- const ca = readFileSync5(trimmed, "utf8");
9766
+ const ca = readFileSync6(trimmed, "utf8");
9281
9767
  return {
9282
9768
  caFile: trimmed,
9283
9769
  ca,
@@ -9306,8 +9792,8 @@ function trustedWebSocketFactory(trust, WebSocketImpl = WsWebSocket) {
9306
9792
  }
9307
9793
 
9308
9794
  // src/agentBootstrap.ts
9309
- import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "node:fs";
9310
- import { dirname as dirname6, join as join8 } from "node:path";
9795
+ import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
9796
+ import { dirname as dirname7, join as join10 } from "node:path";
9311
9797
  import { homedir as homedir7 } from "node:os";
9312
9798
  async function runAgentCliCommand(args, deps = {
9313
9799
  fetchImpl: fetch,
@@ -9907,7 +10393,7 @@ function agentTokenFile(flags) {
9907
10393
  return flags.get("agent-token-file") ?? defaultAgentTokenFilePath();
9908
10394
  }
9909
10395
  function defaultAgentTokenFilePath() {
9910
- return join8(homedir7(), ".linzumi", "agent-token.json");
10396
+ return join10(homedir7(), ".linzumi", "agent-token.json");
9911
10397
  }
9912
10398
  function normalizedApiUrl(apiUrl) {
9913
10399
  return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
@@ -9916,10 +10402,10 @@ function authorizationHeaders(token) {
9916
10402
  return { authorization: `Bearer ${token}` };
9917
10403
  }
9918
10404
  function readOptionalTextFile(path) {
9919
- return existsSync7(path) ? readFileSync6(path, "utf8") : undefined;
10405
+ return existsSync8(path) ? readFileSync7(path, "utf8") : undefined;
9920
10406
  }
9921
10407
  function writeTextFile(path, content) {
9922
- mkdirSync7(dirname6(path), { recursive: true });
10408
+ mkdirSync8(dirname7(path), { recursive: true });
9923
10409
  writeFileSync6(path, content);
9924
10410
  }
9925
10411
  function readStoredAgentTokenFile(path, readTextFile = readOptionalTextFile) {
@@ -9996,8 +10482,8 @@ Launch target:
9996
10482
  }
9997
10483
 
9998
10484
  // src/helloLinzumiProject.ts
9999
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
10000
- import { dirname as dirname7, join as join9, resolve as resolve7 } from "node:path";
10485
+ import { existsSync as existsSync9, mkdirSync as mkdirSync9, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
10486
+ import { dirname as dirname8, join as join11, resolve as resolve7 } from "node:path";
10001
10487
  import { fileURLToPath } from "node:url";
10002
10488
  var defaultHelloLinzumiProjectDir = "/tmp/hello_linzumi";
10003
10489
  var defaultHelloLinzumiProjectName = "hello_linzumi";
@@ -10005,8 +10491,8 @@ var defaultHelloLinzumiParentDir = "/tmp";
10005
10491
  var defaultHelloLinzumiPort = 8787;
10006
10492
  var defaultHelloLinzumiHost = "0.0.0.0";
10007
10493
  var markerFile = ".linzumi-demo-project";
10008
- var moduleDir = dirname7(fileURLToPath(import.meta.url));
10009
- var linzumiLogoSvg = readFileSync7(join9(moduleDir, "assets", "linzumi-logo.svg"), "utf8");
10494
+ var moduleDir = dirname8(fileURLToPath(import.meta.url));
10495
+ var linzumiLogoSvg = readFileSync8(join11(moduleDir, "assets", "linzumi-logo.svg"), "utf8");
10010
10496
  function createHelloLinzumiProject(input = {}) {
10011
10497
  const options = typeof input === "string" ? { rootPath: input } : input;
10012
10498
  const root = resolveHelloProjectRoot(options);
@@ -10014,9 +10500,9 @@ function createHelloLinzumiProject(input = {}) {
10014
10500
  const host = normalizeHost(options.host);
10015
10501
  assertTcpPort(port);
10016
10502
  assertWritableDemoRoot(root, options.reset === true);
10017
- mkdirSync8(join9(root, "src"), { recursive: true });
10503
+ mkdirSync9(join11(root, "src"), { recursive: true });
10018
10504
  for (const file of demoFiles({ root, port, host })) {
10019
- writeFileSync7(join9(root, file.path), file.content, "utf8");
10505
+ writeFileSync7(join11(root, file.path), file.content, "utf8");
10020
10506
  }
10021
10507
  return {
10022
10508
  root,
@@ -10057,11 +10543,11 @@ function assertTcpPort(port) {
10057
10543
  throw new Error("--port must be a TCP port from 1 to 65535");
10058
10544
  }
10059
10545
  function assertWritableDemoRoot(root, reset) {
10060
- if (!existsSync8(root)) {
10546
+ if (!existsSync9(root)) {
10061
10547
  return;
10062
10548
  }
10063
- const markerPath = join9(root, markerFile);
10064
- const isDemoRoot = existsSync8(markerPath) && readFileSync7(markerPath, "utf8").trim() === "hello-linzumi";
10549
+ const markerPath = join11(root, markerFile);
10550
+ const isDemoRoot = existsSync9(markerPath) && readFileSync8(markerPath, "utf8").trim() === "hello-linzumi";
10065
10551
  if (isDemoRoot && reset) {
10066
10552
  rmSync2(root, { recursive: true, force: true });
10067
10553
  return;
@@ -10560,27 +11046,30 @@ To kick the agent off:
10560
11046
 
10561
11047
  // src/commanderDaemon.ts
10562
11048
  import {
10563
- existsSync as existsSync9,
10564
- closeSync,
10565
- mkdirSync as mkdirSync9,
10566
- openSync as openSync2,
10567
- readFileSync as readFileSync8,
11049
+ existsSync as existsSync10,
11050
+ closeSync as closeSync2,
11051
+ mkdirSync as mkdirSync10,
11052
+ openSync as openSync3,
11053
+ readFileSync as readFileSync9,
10568
11054
  watch,
10569
11055
  writeFileSync as writeFileSync8
10570
11056
  } from "node:fs";
10571
11057
  import { homedir as homedir8 } from "node:os";
10572
- import { dirname as dirname8, join as join10, resolve as resolve8 } from "node:path";
11058
+ import { dirname as dirname9, join as join12, resolve as resolve8 } from "node:path";
10573
11059
  import { execFileSync, spawn as spawn7 } from "node:child_process";
10574
11060
  import { fileURLToPath as fileURLToPath2 } from "node:url";
10575
- var connectedMarker = "Runner connected:";
11061
+ var connectedMarkers = ["Connected to Linzumi", "Runner connected:"];
10576
11062
  function commanderStatusDir() {
10577
- return join10(homedir8(), ".linzumi", "commanders");
11063
+ return join12(homedir8(), ".linzumi", "commanders");
10578
11064
  }
10579
11065
  function commanderStatusFile(runnerId, statusDir = commanderStatusDir()) {
10580
- return join10(statusDir, `${safeRunnerId(runnerId)}.json`);
11066
+ return join12(statusDir, `${safeRunnerId(runnerId)}.json`);
10581
11067
  }
10582
11068
  function defaultCommanderLogFile(runnerId, statusDir = commanderStatusDir()) {
10583
- return join10(statusDir, `${safeRunnerId(runnerId)}.log`);
11069
+ return join12(statusDir, `${safeRunnerId(runnerId)}.log`);
11070
+ }
11071
+ function commanderLogIsConnected(log) {
11072
+ return connectedMarkers.some((marker) => log.includes(marker));
10584
11073
  }
10585
11074
  function startCommanderDaemon(options) {
10586
11075
  const statusDir = options.statusDir ?? commanderStatusDir();
@@ -10598,10 +11087,10 @@ function startCommanderDaemon(options) {
10598
11087
  "--log-file",
10599
11088
  logFile
10600
11089
  ];
10601
- mkdirSync9(statusDir, { recursive: true });
10602
- mkdirSync9(dirname8(logFile), { recursive: true });
10603
- const out = openSync2(logFile, "a");
10604
- const err = openSync2(logFile, "a");
11090
+ mkdirSync10(statusDir, { recursive: true });
11091
+ mkdirSync10(dirname9(logFile), { recursive: true });
11092
+ const out = openSync3(logFile, "a");
11093
+ const err = openSync3(logFile, "a");
10605
11094
  writeCliAuditEvent("process.spawn", {
10606
11095
  command: nodeBin,
10607
11096
  args: command,
@@ -10620,8 +11109,8 @@ function startCommanderDaemon(options) {
10620
11109
  pid: child.pid,
10621
11110
  purpose: "commander_daemon.start"
10622
11111
  }, { sessionId: options.runnerId });
10623
- closeSync(out);
10624
- closeSync(err);
11112
+ closeSync2(out);
11113
+ closeSync2(err);
10625
11114
  child.unref();
10626
11115
  if (child.pid === undefined) {
10627
11116
  throw new Error("commander daemon did not report a pid");
@@ -10643,15 +11132,15 @@ function startCommanderDaemon(options) {
10643
11132
  }
10644
11133
  function commanderDaemonStatus(runnerId, statusDir = commanderStatusDir(), processIdentityReader = readProcessIdentity) {
10645
11134
  const statusFile = commanderStatusFile(runnerId, statusDir);
10646
- if (!existsSync9(statusFile)) {
11135
+ if (!existsSync10(statusFile)) {
10647
11136
  return { status: "missing", runnerId, statusFile };
10648
11137
  }
10649
- const record = parseRecord(readFileSync8(statusFile, "utf8"));
11138
+ const record = parseRecord(readFileSync9(statusFile, "utf8"));
10650
11139
  return processIsRunning(record.pid) && processMatchesRecord(record, processIdentityReader) ? { status: "running", record } : { status: "stopped", record };
10651
11140
  }
10652
11141
  async function waitForCommanderDaemon(options) {
10653
11142
  const now = options.now ?? (() => Date.now());
10654
- const readTextFile = options.readTextFile ?? ((path) => existsSync9(path) ? readFileSync8(path, "utf8") : undefined);
11143
+ const readTextFile = options.readTextFile ?? ((path) => existsSync10(path) ? readFileSync9(path, "utf8") : undefined);
10655
11144
  const statusImpl = options.statusImpl ?? commanderDaemonStatus;
10656
11145
  const deadline = now() + options.timeoutMs;
10657
11146
  while (now() <= deadline) {
@@ -10666,12 +11155,12 @@ async function waitForCommanderDaemon(options) {
10666
11155
  if (log === undefined) {
10667
11156
  return { ok: false, reason: "timeout" };
10668
11157
  }
10669
- if (log.includes(connectedMarker)) {
11158
+ if (commanderLogIsConnected(log)) {
10670
11159
  return { ok: true, record: status.record };
10671
11160
  }
10672
11161
  await waitForFileChangeOrTimeout(status.record.logFile, deadline, now, () => {
10673
11162
  const updatedLog = readTextFile(status.record.logFile);
10674
- return updatedLog !== undefined && updatedLog.includes(connectedMarker);
11163
+ return updatedLog !== undefined && commanderLogIsConnected(updatedLog);
10675
11164
  });
10676
11165
  }
10677
11166
  }
@@ -10881,7 +11370,7 @@ async function main(args) {
10881
11370
  process.stdout.write(connectGuideText());
10882
11371
  return;
10883
11372
  case "version":
10884
- process.stdout.write(`linzumi 0.0.40-beta
11373
+ process.stdout.write(`${linzumiCliVersionText}
10885
11374
  `);
10886
11375
  return;
10887
11376
  case "auth":
@@ -10901,18 +11390,18 @@ async function main(args) {
10901
11390
  return;
10902
11391
  case "agentRunner": {
10903
11392
  const options = await parseAgentRunnerArgs(parsed.args);
10904
- await runLocalCodexRunner(options);
11393
+ await runLocalCodexRunner(withLocalMachineId(options));
10905
11394
  return;
10906
11395
  }
10907
11396
  case "start": {
10908
11397
  const options = await parseStartRunnerArgs(parsed.args);
10909
11398
  addAllowedCwd(options.cwd);
10910
- await runLocalCodexRunner(options);
11399
+ await runLocalCodexRunner(withLocalMachineId(options));
10911
11400
  return;
10912
11401
  }
10913
11402
  case "run": {
10914
11403
  const options = await parseRunnerArgs(parsed.args);
10915
- await runLocalCodexRunner(options);
11404
+ await runLocalCodexRunner(withLocalMachineId(options));
10916
11405
  return;
10917
11406
  }
10918
11407
  }
@@ -11223,7 +11712,7 @@ async function parseStartRunnerArgs(args, deps = {
11223
11712
  return {
11224
11713
  kandanUrl,
11225
11714
  token: targetToken,
11226
- runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID3()}`,
11715
+ runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID4()}`,
11227
11716
  workspaceSlug: target.workspaceSlug,
11228
11717
  cwd,
11229
11718
  codexBin,
@@ -11311,7 +11800,7 @@ async function parseAgentRunnerArgs(args, deps = {
11311
11800
  return {
11312
11801
  kandanUrl,
11313
11802
  token: tokenFile.commanderToken,
11314
- runnerId: stringValue3(values, "runner-id") ?? `agent-runner-${randomUUID3()}`,
11803
+ runnerId: stringValue3(values, "runner-id") ?? `agent-runner-${randomUUID4()}`,
11315
11804
  workspaceSlug: tokenFile.workspaceId,
11316
11805
  cwd,
11317
11806
  codexBin,
@@ -11331,7 +11820,7 @@ async function parseAgentRunnerArgs(args, deps = {
11331
11820
  };
11332
11821
  }
11333
11822
  function readAgentTokenTextFile(path) {
11334
- return existsSync10(path) ? readFileSync9(path, "utf8") : undefined;
11823
+ return existsSync11(path) ? readFileSync10(path, "utf8") : undefined;
11335
11824
  }
11336
11825
  function rejectAgentRunnerTargetingFlags(values) {
11337
11826
  const unsupportedFlags = [
@@ -11407,12 +11896,12 @@ async function parseRunnerArgs(args, deps = {
11407
11896
  process.exit(0);
11408
11897
  }
11409
11898
  if (values.get("version") === true) {
11410
- process.stdout.write(`linzumi 0.0.40-beta
11899
+ process.stdout.write(`${linzumiCliVersionText}
11411
11900
  `);
11412
11901
  process.exit(0);
11413
11902
  }
11414
- const channelTarget = parseChannelSessionTarget(values);
11415
- const kandanUrl = required(values, "linzumi-url");
11903
+ const connectTarget = parseConnectTarget(values);
11904
+ const kandanUrl = stringValue3(values, "linzumi-url") ?? defaultLinzumiWebSocketUrl;
11416
11905
  const cwd = stringValue3(values, "cwd") ?? process.cwd();
11417
11906
  const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
11418
11907
  const localConfiguredAllowedCwds = values.has("allowed-cwd") ? { allowedCwds: [], missingAllowedCwds: [] } : readConfiguredAllowedCwdDetails();
@@ -11423,8 +11912,8 @@ async function parseRunnerArgs(args, deps = {
11423
11912
  const token = await deps.resolveToken({
11424
11913
  kandanUrl,
11425
11914
  explicitToken,
11426
- workspaceSlug: channelTarget?.workspaceSlug,
11427
- channelSlug: channelTarget?.channelSlug,
11915
+ workspaceSlug: connectTarget?.workspaceSlug,
11916
+ channelSlug: connectTarget?.channelSlug,
11428
11917
  authFilePath: stringValue3(values, "auth-file"),
11429
11918
  callbackHost: stringValue3(values, "oauth-callback-host"),
11430
11919
  reportRejectedCachedToken: () => {
@@ -11432,7 +11921,7 @@ async function parseRunnerArgs(args, deps = {
11432
11921
  `);
11433
11922
  }
11434
11923
  });
11435
- const channelSession = parseChannelSession(values, token, channelTarget);
11924
+ const channelSession = parseChannelSession(values, token, connectTarget);
11436
11925
  const editorRuntime = await deps.resolveEditorRuntime({
11437
11926
  kandanUrl,
11438
11927
  token,
@@ -11449,7 +11938,7 @@ async function parseRunnerArgs(args, deps = {
11449
11938
  return {
11450
11939
  kandanUrl,
11451
11940
  token,
11452
- runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID3()}`,
11941
+ runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID4()}`,
11453
11942
  cwd,
11454
11943
  codexBin,
11455
11944
  codexUrl: stringValue3(values, "codex-url"),
@@ -11463,7 +11952,7 @@ async function parseRunnerArgs(args, deps = {
11463
11952
  editorRuntime: editorRuntime.runtime,
11464
11953
  socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
11465
11954
  dependencyStatus,
11466
- workspaceSlug: channelSession?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
11955
+ workspaceSlug: connectTarget?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
11467
11956
  runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
11468
11957
  channelSession
11469
11958
  };
@@ -11575,7 +12064,7 @@ function resolveUserPath(pathValue) {
11575
12064
  return resolve9(pathValue);
11576
12065
  }
11577
12066
  function parseChannelSession(values, token, target) {
11578
- if (target === undefined) {
12067
+ if (target === undefined || target.channelSlug === undefined) {
11579
12068
  return;
11580
12069
  }
11581
12070
  const listenUser = stringValue3(values, "listen-user") ?? defaultListenUserFromToken(token);
@@ -11591,7 +12080,7 @@ function parseChannelSession(values, token, target) {
11591
12080
  streamFlushMs: positiveIntegerValue(values, "stream-flush-ms")
11592
12081
  };
11593
12082
  }
11594
- function parseChannelSessionTarget(values) {
12083
+ function parseConnectTarget(values) {
11595
12084
  return parseOptionalChannelTarget(values);
11596
12085
  }
11597
12086
  function defaultListenUserFromToken(token) {
@@ -11604,12 +12093,15 @@ function defaultListenUserFromToken(token) {
11604
12093
  function parseOptionalChannelTarget(values) {
11605
12094
  const channel = stringValue3(values, "channel");
11606
12095
  const workspace = stringValue3(values, "workspace");
11607
- if (channel === undefined && workspace === undefined) {
11608
- return;
12096
+ if (channel === undefined) {
12097
+ if (workspace === undefined) {
12098
+ return;
12099
+ }
12100
+ return { workspaceSlug: workspace };
11609
12101
  }
11610
- return channel !== undefined && channel.includes("/") ? parseChannelPath(channel) : {
12102
+ return channel.includes("/") ? parseChannelPath(channel) : {
11611
12103
  workspaceSlug: workspace ?? required(values, "workspace"),
11612
- channelSlug: channel ?? required(values, "channel")
12104
+ channelSlug: channel
11613
12105
  };
11614
12106
  }
11615
12107
  function parseChannelPath(channel) {
@@ -11622,6 +12114,12 @@ function parseChannelPath(channel) {
11622
12114
  channelSlug: channelSlug.trim()
11623
12115
  };
11624
12116
  }
12117
+ function withLocalMachineId(options) {
12118
+ return {
12119
+ ...options,
12120
+ machineId: ensureLocalMachineId()
12121
+ };
12122
+ }
11625
12123
  function required(values, key) {
11626
12124
  const value = stringValue3(values, key);
11627
12125
  if (value === undefined) {
@@ -11678,19 +12176,19 @@ Usage:
11678
12176
  linzumi commander <folder> [options]
11679
12177
  linzumi start <folder> [options]
11680
12178
  linzumi paths list|add|remove [path]
11681
- linzumi connect --linzumi-url <ws-url> --workspace <slug> --channel <slug> [options]
12179
+ linzumi connect --workspace <slug> [--channel <slug>] [options]
11682
12180
  linzumi auth --linzumi-url <ws-url> [--workspace <slug> --channel <slug>]
11683
12181
 
11684
- Required:
12182
+ Connection:
11685
12183
  --linzumi-url <ws-url> Linzumi backend URL, default ${defaultLinzumiWebSocketUrl}
11686
12184
  (deprecated alias: --kandan-url)
11687
12185
  --token <jwt> Optional override token. Otherwise ~/.linzumi/auth.json is validated or OAuth opens.
11688
12186
  --auth-file <path> Auth cache path, default ~/.linzumi/auth.json
11689
12187
  --oauth-callback-host <ip> Callback host reachable by your browser
11690
12188
 
11691
- Channel binding:
12189
+ Workspace and optional channel binding:
11692
12190
  --workspace <slug> Workspace slug
11693
- --channel <slug|w/c> Channel slug, or workspace/channel
12191
+ --channel <slug|w/c> Optional channel slug, or workspace/channel
11694
12192
  --linzumi-thread-id <uuid> Resume an existing Linzumi thread instead of announcing a new root
11695
12193
  (deprecated alias: --kandan-thread-id)
11696
12194
  --listen-user <user|all> User whose replies are accepted, default authenticated user
@@ -11723,8 +12221,9 @@ Examples:
11723
12221
  linzumi commander daemon --runner-id launch-commander
11724
12222
  linzumi start ~/
11725
12223
  linzumi start ~/code/my-app
11726
- linzumi connect --workspace <your-workspace> --channel <your-channel> --launch-tui
11727
- linzumi connect --workspace <your-workspace> --channel <your-channel> --model gpt-5 --reasoning-effort low --fast --launch-tui
12224
+ linzumi connect --workspace <your-workspace> --launch-tui
12225
+ linzumi connect --workspace <your-workspace> --model gpt-5 --reasoning-effort low --fast --launch-tui
12226
+ linzumi connect --workspace <your-workspace> --channel <your-channel>
11728
12227
  linzumi auth --workspace <your-workspace> --channel <your-channel>
11729
12228
  linzumi paths add ~/code/my-app
11730
12229
  linzumi paths list
@@ -11734,9 +12233,9 @@ Examples:
11734
12233
  Missing --listen-user and authenticated user is unavailable.
11735
12234
  linzumi connect --token "$TOKEN" --listen-users sean
11736
12235
  Invalid flag: use --listen-user.
11737
- linzumi connect --workspace <your-workspace> --channel <your-channel> --allowed-cwd /does/not/exist
12236
+ linzumi connect --workspace <your-workspace> --allowed-cwd /does/not/exist
11738
12237
  Invalid --allowed-cwd: allowed cwd roots must exist locally.
11739
- linzumi connect --workspace <your-workspace> --channel <your-channel> --forward-port vite
12238
+ linzumi connect --workspace <your-workspace> --forward-port vite
11740
12239
  Invalid --forward-port: value must be a TCP port from 1 to 65535.
11741
12240
  `;
11742
12241
  }
@@ -11894,7 +12393,7 @@ space, persists this folder to your trusted-paths list, and starts this
11894
12393
  computer as a local Codex runner.
11895
12394
 
11896
12395
  Advanced (when you already know your workspace and channel):
11897
- linzumi connect --workspace <your-workspace> --channel <your-channel>
12396
+ linzumi connect --workspace <your-workspace>
11898
12397
 
11899
12398
  For help:
11900
12399
  linzumi connect --help