@linzumi/cli 0.0.63-beta → 0.0.64-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 +465 -78
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -62,7 +62,7 @@ Install the CLI or run it with `npx`:
62
62
  ```bash
63
63
  npm install -g @linzumi/cli@latest
64
64
  npx -y @linzumi/cli@latest signup
65
- npx -y @linzumi/cli@0.0.63-beta --version
65
+ npx -y @linzumi/cli@0.0.64-beta --version
66
66
  linzumi --version
67
67
  ```
68
68
 
package/dist/index.js CHANGED
@@ -18214,6 +18214,35 @@ function codexFileChangeDeltaFromNotification(params) {
18214
18214
  patchText
18215
18215
  };
18216
18216
  }
18217
+ function codexCompletedFileChangeFromNotification(params) {
18218
+ const item = objectValue(params.item) ?? objectValue(params.payload) ?? params;
18219
+ const status = stringValue(item.status);
18220
+ const turnId = stringValue(params.turnId) ?? stringValue(params.turn_id) ?? stringValue(objectValue(params.turn)?.id) ?? stringValue(item.turnId) ?? stringValue(item.turn_id) ?? stringValue(objectValue(item.turn)?.id);
18221
+ if (codexItemTypeIsFileChange(item) && (status === void 0 || status === "completed")) {
18222
+ const changes = normalizeFileChangeEntries(arrayValue(item.changes) ?? []);
18223
+ const patchText2 = nonBlankStringValue(item.patchText) ?? nonBlankStringValue(item.patch) ?? patchTextFromFileChangeEntries(changes);
18224
+ if (patchText2 === void 0) {
18225
+ return void 0;
18226
+ }
18227
+ return {
18228
+ itemKey: stringValue(item.id) ?? "file-change",
18229
+ turnId,
18230
+ patchText: patchText2
18231
+ };
18232
+ }
18233
+ if (stringValue(item.type) !== "custom_tool_call" || stringValue(item.name) !== "apply_patch" || status !== void 0 && status !== "completed") {
18234
+ return void 0;
18235
+ }
18236
+ const patchText = applyPatchTextFromValue(item.input) ?? applyPatchTextFromValue(item.arguments);
18237
+ if (patchText === void 0) {
18238
+ return void 0;
18239
+ }
18240
+ return {
18241
+ itemKey: stringValue(item.call_id) ?? stringValue(item.id) ?? "file-change",
18242
+ turnId,
18243
+ patchText
18244
+ };
18245
+ }
18217
18246
  function codexFileChangeStructuredMessage(itemKey, patchText, streamState, status) {
18218
18247
  return {
18219
18248
  kind: "codex_file_change",
@@ -18413,20 +18442,20 @@ function commandMessageForFunctionCall(item, output, index) {
18413
18442
  function messageForCustomToolCall(item, output, index) {
18414
18443
  const name = stringValue(item.name) ?? "custom_tool_call";
18415
18444
  const itemKey = stringValue(item.call_id) ?? stringValue(item.id) ?? `item-${index}`;
18416
- const input = nonBlankStringValue(item.input) ?? nonBlankStringValue(item.arguments) ?? "";
18417
18445
  if (name === "apply_patch") {
18446
+ const patchText = applyPatchTextFromValue(item.input) ?? applyPatchTextFromValue(item.arguments) ?? "";
18418
18447
  return [
18419
18448
  {
18420
18449
  itemKey,
18421
- body: input,
18450
+ body: patchText,
18422
18451
  structured: {
18423
18452
  kind: "codex_file_change",
18424
18453
  item_id: itemKey,
18425
18454
  transcript_unit_id: `codex_file_change:${itemKey}`,
18426
18455
  stream_state: "completed",
18427
18456
  status: output === void 0 ? "started" : "completed",
18428
- patch_text: input,
18429
- changes: fileChangesFromPatch(input)
18457
+ patch_text: patchText,
18458
+ changes: fileChangesFromPatch(patchText)
18430
18459
  }
18431
18460
  }
18432
18461
  ];
@@ -18477,43 +18506,120 @@ function toolOutputText(item) {
18477
18506
  }
18478
18507
  return nonBlankStringValue(item.output) ?? nonBlankStringValue(item.result) ?? nonBlankStringValue(item.content) ?? "";
18479
18508
  }
18480
- function fileChangesFromPatch(patchText) {
18481
- return patchText.split("\n").flatMap((line) => {
18482
- const updatePrefix = "*** Update File: ";
18483
- const addPrefix = "*** Add File: ";
18484
- const deletePrefix = "*** Delete File: ";
18485
- if (line.startsWith(updatePrefix)) {
18486
- return [
18487
- {
18488
- path: line.slice(updatePrefix.length).trim(),
18489
- diff: "",
18490
- kind: "update",
18491
- move_path: ""
18492
- }
18493
- ];
18494
- }
18495
- if (line.startsWith(addPrefix)) {
18496
- return [
18497
- {
18498
- path: line.slice(addPrefix.length).trim(),
18499
- diff: "",
18500
- kind: "add",
18501
- move_path: ""
18502
- }
18503
- ];
18509
+ function patchTextFromFileChangeEntries(changes) {
18510
+ if (changes.length === 0) {
18511
+ return void 0;
18512
+ }
18513
+ const body = changes.flatMap((change) => {
18514
+ const path2 = stringValue(change.path);
18515
+ if (path2 === void 0 || path2.trim() === "") {
18516
+ return [];
18504
18517
  }
18505
- if (line.startsWith(deletePrefix)) {
18506
- return [
18507
- {
18508
- path: line.slice(deletePrefix.length).trim(),
18509
- diff: "",
18510
- kind: "delete",
18511
- move_path: ""
18518
+ const kind = stringValue(change.kind);
18519
+ const header = kind === "add" ? `*** Add File: ${path2}` : kind === "delete" ? `*** Delete File: ${path2}` : `*** Update File: ${path2}`;
18520
+ const diff = nonBlankStringValue(change.diff);
18521
+ return diff === void 0 ? [header] : [header, diff];
18522
+ }).join("\n");
18523
+ return body === "" ? void 0 : `*** Begin Patch
18524
+ ${body}
18525
+ *** End Patch
18526
+ `;
18527
+ }
18528
+ function fileChangesFromPatch(patchText) {
18529
+ const finalState = patchText.split("\n").reduce(
18530
+ (state, line) => {
18531
+ const header = patchHeaderForLine(line);
18532
+ if (header !== void 0) {
18533
+ return {
18534
+ changes: appendParsedPatchChange(state.changes, state.current),
18535
+ current: {
18536
+ path: header.path,
18537
+ kind: header.kind,
18538
+ movePath: "",
18539
+ diffLines: []
18540
+ }
18541
+ };
18542
+ }
18543
+ if (line.startsWith("*** Move to: ")) {
18544
+ const movePath = line.slice("*** Move to: ".length).trim();
18545
+ return {
18546
+ ...state,
18547
+ current: state.current === void 0 ? void 0 : { ...state.current, movePath }
18548
+ };
18549
+ }
18550
+ if (line === "*** Begin Patch" || line === "*** End Patch" || line.startsWith("*** ")) {
18551
+ return state;
18552
+ }
18553
+ return {
18554
+ ...state,
18555
+ current: state.current === void 0 ? void 0 : {
18556
+ ...state.current,
18557
+ diffLines: [...state.current.diffLines, line]
18512
18558
  }
18513
- ];
18514
- }
18515
- return [];
18516
- });
18559
+ };
18560
+ },
18561
+ { changes: [], current: void 0 }
18562
+ );
18563
+ return appendParsedPatchChange(finalState.changes, finalState.current).map(
18564
+ (change) => ({
18565
+ path: change.path,
18566
+ diff: trimDiffLines(change.diffLines),
18567
+ kind: change.kind,
18568
+ move_path: change.movePath
18569
+ })
18570
+ );
18571
+ }
18572
+ function patchHeaderForLine(line) {
18573
+ const updatePrefix = "*** Update File: ";
18574
+ const addPrefix = "*** Add File: ";
18575
+ const deletePrefix = "*** Delete File: ";
18576
+ if (line.startsWith(updatePrefix)) {
18577
+ return {
18578
+ path: line.slice(updatePrefix.length).trim(),
18579
+ kind: "update"
18580
+ };
18581
+ }
18582
+ if (line.startsWith(addPrefix)) {
18583
+ return {
18584
+ path: line.slice(addPrefix.length).trim(),
18585
+ kind: "add"
18586
+ };
18587
+ }
18588
+ if (line.startsWith(deletePrefix)) {
18589
+ return {
18590
+ path: line.slice(deletePrefix.length).trim(),
18591
+ kind: "delete"
18592
+ };
18593
+ }
18594
+ return void 0;
18595
+ }
18596
+ function appendParsedPatchChange(changes, change) {
18597
+ if (change === void 0 || change.path.trim() === "") {
18598
+ return [...changes];
18599
+ }
18600
+ return [...changes, change];
18601
+ }
18602
+ function trimDiffLines(lines) {
18603
+ const firstContentIndex = lines.findIndex((line) => line.trim() !== "");
18604
+ if (firstContentIndex < 0) {
18605
+ return "";
18606
+ }
18607
+ const lastContentIndexFromEnd = [...lines].reverse().findIndex((line) => line.trim() !== "");
18608
+ const end = lastContentIndexFromEnd < 0 ? lines.length : lines.length - lastContentIndexFromEnd;
18609
+ return lines.slice(firstContentIndex, end).join("\n");
18610
+ }
18611
+ function codexItemTypeIsFileChange(item) {
18612
+ const itemType = stringValue(item.type);
18613
+ return itemType === "fileChange" || itemType === "file_change";
18614
+ }
18615
+ function applyPatchTextFromValue(value) {
18616
+ const text2 = nonBlankStringValue(value);
18617
+ if (text2 !== void 0) {
18618
+ const parsed = parseJsonObjectOrUndefined(text2);
18619
+ return nonBlankStringValue(parsed?.patch) ?? nonBlankStringValue(parsed?.input) ?? nonBlankStringValue(parsed?.text) ?? text2;
18620
+ }
18621
+ const object = objectValue(value);
18622
+ return nonBlankStringValue(object?.patch) ?? nonBlankStringValue(object?.input) ?? nonBlankStringValue(object?.text);
18517
18623
  }
18518
18624
  function webSearchQueriesForItem(item) {
18519
18625
  const action = objectValue(item.action);
@@ -19192,7 +19298,13 @@ import { basename as basename2 } from "node:path";
19192
19298
  var defaultIntervalMs = 2e3;
19193
19299
  var defaultDebounceMs = 750;
19194
19300
  function startPortForwardWatcher(options) {
19195
- const rootPid = options.rootPid ?? process.pid;
19301
+ if (options.rootPid === void 0) {
19302
+ throw new Error("port_forward_watcher_root_pid_missing");
19303
+ }
19304
+ if (!Number.isInteger(options.rootPid) || options.rootPid <= 0) {
19305
+ throw new Error("port_forward_watcher_root_pid_invalid");
19306
+ }
19307
+ const rootPid = options.rootPid;
19196
19308
  const intervalMs = options.intervalMs ?? defaultIntervalMs;
19197
19309
  const debounceMs = options.debounceMs ?? defaultDebounceMs;
19198
19310
  const lostDebounceMs = options.lostDebounceMs ?? intervalMs * 2 + debounceMs;
@@ -19314,7 +19426,7 @@ function stableForwardCandidates(previousObservedByPort, candidates, nowMs, debo
19314
19426
  return { nextObservedByPort, stableCandidates };
19315
19427
  }
19316
19428
  function sameForwardCandidate(left, right) {
19317
- return left.port === right.port && left.pid === right.pid && left.command === right.command && left.cwd === right.cwd;
19429
+ return left.port === right.port && left.pid === right.pid;
19318
19430
  }
19319
19431
  function descendantPidSet(rows, rootPid) {
19320
19432
  const childrenByParent = /* @__PURE__ */ new Map();
@@ -19480,16 +19592,6 @@ function reviewPortForwardCandidate(options) {
19480
19592
  if (!options.threadBound) {
19481
19593
  return { type: "skip", reason: "thread_not_bound" };
19482
19594
  }
19483
- if (options.suppressedPorts?.has(options.candidate.port) === true) {
19484
- return { type: "skip", reason: "suppressed_port" };
19485
- }
19486
- if (isInternalCodexProcess(options.candidate)) {
19487
- return { type: "skip", reason: "internal_codex_process" };
19488
- }
19489
- const dismissedTarget = options.dismissedTargets?.get(options.candidate.port);
19490
- if (dismissedTarget !== void 0 && sameDismissedPortForwardTarget(dismissedTarget, options.candidate)) {
19491
- return { type: "skip", reason: "recently_dismissed" };
19492
- }
19493
19595
  if (options.approvedPorts.has(options.candidate.port)) {
19494
19596
  const approvedTarget = options.approvedTargets.get(options.candidate.port);
19495
19597
  if (approvedTarget === void 0) {
@@ -19568,12 +19670,6 @@ function revocationCapabilities(capabilities, port) {
19568
19670
  revokedPorts: [port]
19569
19671
  };
19570
19672
  }
19571
- function sameDismissedPortForwardTarget(left, right) {
19572
- return left.port === right.port && left.command === right.command && left.cwd === right.cwd;
19573
- }
19574
- function isInternalCodexProcess(candidate) {
19575
- return commandLabel(candidate.command) === "codex";
19576
- }
19577
19673
 
19578
19674
  // src/processNameCatalog.ts
19579
19675
  var processCatalogEntries = [
@@ -20259,20 +20355,21 @@ async function attachChannelSession(args) {
20259
20355
  handleCodexNotification: (method, params) => {
20260
20356
  const turnId = codexNotificationTurnId(params);
20261
20357
  const threadId = codexNotificationThreadId(params);
20262
- if (codexNotificationBelongsToSession(state, threadId, turnId)) {
20358
+ const routedTurnId = turnId ?? codexNotificationActiveTurnFallback(method, state, params);
20359
+ if (codexNotificationBelongsToSession(state, threadId, routedTurnId)) {
20263
20360
  const processingReason = processingReasonForCodexNotification(
20264
20361
  method,
20265
20362
  params
20266
20363
  );
20267
- if (turnId !== void 0 && processingReason !== void 0) {
20364
+ if (routedTurnId !== void 0 && processingReason !== void 0) {
20268
20365
  void refreshActiveProcessingState(
20269
20366
  args,
20270
20367
  state,
20271
- turnId,
20368
+ routedTurnId,
20272
20369
  processingReason
20273
20370
  ).catch((error) => {
20274
20371
  args.log("kandan.message_state_refresh_failed", {
20275
- turn_id: turnId,
20372
+ turn_id: routedTurnId,
20276
20373
  reason: processingReason,
20277
20374
  message: error instanceof Error ? error.message : String(error)
20278
20375
  });
@@ -20341,6 +20438,7 @@ async function attachChannelSession(args) {
20341
20438
  break;
20342
20439
  case "item/completed":
20343
20440
  enqueueWebSearchProgress(args, state, params, payloadContext);
20441
+ enqueueCompletedFileChange(args, state, params, payloadContext);
20344
20442
  if (turnId !== void 0) {
20345
20443
  const promise = mirrorLocalTuiInputFromNotification(
20346
20444
  args,
@@ -20358,6 +20456,9 @@ async function attachChannelSession(args) {
20358
20456
  }).finally(() => forgetPendingTuiInputMirror(state, turnId));
20359
20457
  }
20360
20458
  break;
20459
+ case "response_item":
20460
+ enqueueCompletedFileChange(args, state, params, payloadContext);
20461
+ break;
20361
20462
  }
20362
20463
  }
20363
20464
  },
@@ -20448,6 +20549,7 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
20448
20549
  webSearchProgressOutputs: createBoundedCache(maxForwardedTurnIds),
20449
20550
  pendingApprovalRequests: /* @__PURE__ */ new Map(),
20450
20551
  approvalPromptChain: Promise.resolve(),
20552
+ approvedThreadInteractionRequesters: /* @__PURE__ */ new Set(),
20451
20553
  pendingPortForwardRequests: /* @__PURE__ */ new Map(),
20452
20554
  portForwardPreviousProcessingStates: /* @__PURE__ */ new Map(),
20453
20555
  queuedPortForwardCandidates: /* @__PURE__ */ new Map(),
@@ -20497,6 +20599,12 @@ function startPortForwardWatchIfEnabled(args, state, payloadContext) {
20497
20599
  return;
20498
20600
  }
20499
20601
  const { start: configuredStart, ...watchOptions } = args.options.portForwardWatcher ?? {};
20602
+ if (configuredStart === void 0 && watchOptions.rootPid === void 0) {
20603
+ args.log("port_forward.watch_skipped", {
20604
+ reason: "worker_pid_missing"
20605
+ });
20606
+ return;
20607
+ }
20500
20608
  const start = configuredStart ?? startPortForwardWatcher;
20501
20609
  for (const port of args.options.initialForwardPorts ?? []) {
20502
20610
  state.approvedForwardPorts.add(port);
@@ -20572,6 +20680,9 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
20572
20680
  control
20573
20681
  );
20574
20682
  }
20683
+ if (control.type === "update_thread_interaction_access") {
20684
+ return updateThreadInteractionAccess(args, state, control);
20685
+ }
20575
20686
  if (control.type !== "interrupt_queued_messages") {
20576
20687
  return void 0;
20577
20688
  }
@@ -20639,6 +20750,76 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
20639
20750
  interruptedQueuedMessages: interrupted.ok ? interrupted.selectedCount : 0
20640
20751
  };
20641
20752
  }
20753
+ async function updateThreadInteractionAccess(args, state, control) {
20754
+ const requesterKeys = threadInteractionRequesterKeys(control);
20755
+ if (state.kandanThreadId !== control.threadId) {
20756
+ args.log("thread_interaction.access_update_ignored", {
20757
+ thread_id: control.threadId,
20758
+ expected_thread_id: state.kandanThreadId ?? null,
20759
+ requester_slug: control.requesterSlug ?? null,
20760
+ requester_user_id: control.requesterUserId ?? null,
20761
+ reason: "different_thread"
20762
+ });
20763
+ return {
20764
+ instanceId: args.instanceId,
20765
+ ok: false,
20766
+ error: "different_thread"
20767
+ };
20768
+ }
20769
+ if (requesterKeys.length === 0) {
20770
+ args.log("thread_interaction.access_update_ignored", {
20771
+ thread_id: control.threadId,
20772
+ requester_slug: control.requesterSlug ?? null,
20773
+ requester_user_id: control.requesterUserId ?? null,
20774
+ reason: "missing_requester"
20775
+ });
20776
+ return {
20777
+ instanceId: args.instanceId,
20778
+ ok: false,
20779
+ error: "missing_requester"
20780
+ };
20781
+ }
20782
+ for (const requesterKey of requesterKeys) {
20783
+ if (control.grant) {
20784
+ state.approvedThreadInteractionRequesters.add(requesterKey);
20785
+ } else {
20786
+ state.approvedThreadInteractionRequesters.delete(requesterKey);
20787
+ }
20788
+ }
20789
+ args.log("thread_interaction.access_updated", {
20790
+ thread_id: control.threadId,
20791
+ requester_slug: control.requesterSlug ?? null,
20792
+ requester_user_id: control.requesterUserId ?? null,
20793
+ grant: control.grant
20794
+ });
20795
+ return {
20796
+ instanceId: args.instanceId,
20797
+ ok: true,
20798
+ grant: control.grant
20799
+ };
20800
+ }
20801
+ function threadInteractionRequesterKeys(args) {
20802
+ if (args.threadId === void 0) {
20803
+ return [];
20804
+ }
20805
+ const keys = [];
20806
+ if (args.requesterUserId !== void 0) {
20807
+ keys.push(`${args.threadId}:user:${args.requesterUserId}`);
20808
+ }
20809
+ if (args.requesterSlug !== void 0 && args.requesterSlug.trim() !== "") {
20810
+ keys.push(`${args.threadId}:slug:${args.requesterSlug}`);
20811
+ }
20812
+ return keys;
20813
+ }
20814
+ function threadInteractionSenderAllowed(state, event) {
20815
+ return threadInteractionRequesterKeys({
20816
+ threadId: event.threadId,
20817
+ requesterUserId: event.actorUserId,
20818
+ requesterSlug: event.actorSlug
20819
+ }).some(
20820
+ (requesterKey) => state.approvedThreadInteractionRequesters.has(requesterKey)
20821
+ );
20822
+ }
20642
20823
  async function updateSessionSettings(args, state, payloadContext, control) {
20643
20824
  if (state.codexThreadId !== control.threadId) {
20644
20825
  return void 0;
@@ -21337,13 +21518,16 @@ function parsePortForwardDecision(body) {
21337
21518
  }
21338
21519
  async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext, event) {
21339
21520
  const session = args.options.channelSession;
21340
- if (event.type !== "thread.message" || event.threadId === void 0 || event.body.trim() === "" && event.attachments.length === 0) {
21341
- args.log("kandan.message_ignored", {
21342
- seq: event.seq,
21343
- actor_slug: event.actorSlug ?? null,
21344
- actor_user_id: event.actorUserId ?? null,
21345
- reason: "not_thread_message_or_empty"
21346
- });
21521
+ if (event.type !== "thread.message") {
21522
+ logIgnoredKandanMessage(args, event, "non_thread_message_event");
21523
+ return;
21524
+ }
21525
+ if (event.threadId === void 0) {
21526
+ logIgnoredKandanMessage(args, event, "missing_thread_id");
21527
+ return;
21528
+ }
21529
+ if (event.body.trim() === "" && event.attachments.length === 0) {
21530
+ logIgnoredKandanMessage(args, event, "empty_thread_message");
21347
21531
  return;
21348
21532
  }
21349
21533
  if (isCodexAuthoredEvent(event)) {
@@ -21355,7 +21539,7 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
21355
21539
  });
21356
21540
  return;
21357
21541
  }
21358
- if (!senderAllowed(session.listenUser, event, runnerIdentity)) {
21542
+ if (!senderAllowed(session.listenUser, event, runnerIdentity) && !threadInteractionSenderAllowed(state, event)) {
21359
21543
  args.log("kandan.message_ignored", {
21360
21544
  seq: event.seq,
21361
21545
  actor_slug: event.actorSlug ?? null,
@@ -21466,6 +21650,19 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
21466
21650
  await publishKandanMessageState(args, event, { status: "queued" });
21467
21651
  await drainKandanMessageQueue(args, state, payloadContext);
21468
21652
  }
21653
+ function logIgnoredKandanMessage(args, event, reason) {
21654
+ args.log("kandan.message_ignored", {
21655
+ seq: event.seq,
21656
+ type: event.type,
21657
+ thread_id: event.threadId ?? null,
21658
+ actor_slug: event.actorSlug ?? null,
21659
+ actor_user_id: event.actorUserId ?? null,
21660
+ body_length: event.body.length,
21661
+ attachment_count: event.attachments.length,
21662
+ local_runner_event_type: event.localRunnerEventType ?? null,
21663
+ reason
21664
+ });
21665
+ }
21469
21666
  function kandanMessageClaimKey(args, threadId, seq) {
21470
21667
  const session = args.options.channelSession;
21471
21668
  return [
@@ -22079,6 +22276,7 @@ function completedOutputProjectionsForTurn(state, turnId, messages) {
22079
22276
  }),
22080
22277
  ...cachedOutputs.map((output) => output.sortOrder)
22081
22278
  ]);
22279
+ const fallbackSnapshotPositions = fallbackSnapshotOutputPositions(snapshotOutputs);
22082
22280
  const orderedSnapshotOutputs = snapshotOutputs.map((output) => {
22083
22281
  switch (output.match.kind) {
22084
22282
  case "none":
@@ -22086,7 +22284,7 @@ function completedOutputProjectionsForTurn(state, turnId, messages) {
22086
22284
  ...output,
22087
22285
  sortOrder: fallbackSnapshotOutputOrder(
22088
22286
  fallbackSortOrderBase,
22089
- output.snapshotIndex
22287
+ fallbackSnapshotPosition(fallbackSnapshotPositions, output)
22090
22288
  )
22091
22289
  };
22092
22290
  case "assistant":
@@ -22288,8 +22486,44 @@ function compareCompletedOutputProjection(left, right) {
22288
22486
  function fallbackSnapshotOutputBase(sortOrders) {
22289
22487
  return sortOrders.reduce((max, sortOrder) => Math.max(max, sortOrder), 0) + 1;
22290
22488
  }
22291
- function fallbackSnapshotOutputOrder(base, snapshotIndex) {
22292
- return base + snapshotIndex;
22489
+ function fallbackSnapshotOutputOrder(base, fallbackPosition) {
22490
+ return base + fallbackPosition;
22491
+ }
22492
+ function fallbackSnapshotOutputPositions(outputs) {
22493
+ const unmatched = outputs.filter((output) => output.match.kind === "none");
22494
+ const firstAssistantIndex = unmatched.findIndex(
22495
+ (output) => stringValue(output.message.structured.kind) === "codex_assistant_message"
22496
+ );
22497
+ if (firstAssistantIndex < 0) {
22498
+ return snapshotOutputPositionMap(unmatched);
22499
+ }
22500
+ const beforeFirstAssistant = unmatched.slice(0, firstAssistantIndex);
22501
+ const afterFirstAssistant = unmatched.slice(firstAssistantIndex);
22502
+ const fileChangesAfterAssistant = afterFirstAssistant.filter(
22503
+ (output) => stringValue(output.message.structured.kind) === "codex_file_change"
22504
+ );
22505
+ const remainingAfterAssistant = afterFirstAssistant.filter(
22506
+ (output) => stringValue(output.message.structured.kind) !== "codex_file_change"
22507
+ );
22508
+ return snapshotOutputPositionMap([
22509
+ ...beforeFirstAssistant,
22510
+ ...fileChangesAfterAssistant,
22511
+ ...remainingAfterAssistant
22512
+ ]);
22513
+ }
22514
+ function snapshotOutputPositionMap(outputs) {
22515
+ return new Map(
22516
+ outputs.map((output, position) => [output.snapshotIndex, position])
22517
+ );
22518
+ }
22519
+ function fallbackSnapshotPosition(positions, output) {
22520
+ const position = positions.get(output.snapshotIndex);
22521
+ if (position === void 0) {
22522
+ throw new Error(
22523
+ `Missing fallback completed-output position for snapshot index ${output.snapshotIndex}`
22524
+ );
22525
+ }
22526
+ return position;
22293
22527
  }
22294
22528
  function structuredOutputMatchKey(kind, itemKey) {
22295
22529
  return `${kind}:${itemKey}`;
@@ -22527,6 +22761,27 @@ function enqueueFileChangeCompletion(args, state, turnId) {
22527
22761
  });
22528
22762
  });
22529
22763
  }
22764
+ function enqueueCompletedFileChange(args, state, params, payloadContext) {
22765
+ const delta = codexCompletedFileChangeFromNotification(params);
22766
+ if (delta === void 0) {
22767
+ return;
22768
+ }
22769
+ flushStreamDeltaQueue(
22770
+ state.fileChangeQueue,
22771
+ fileChangeQueueRuntime(args, state, payloadContext)
22772
+ );
22773
+ const previous = state.fileChangeQueue.chain;
22774
+ const next = previous.catch(() => void 0).then(
22775
+ () => forwardCompletedFileChangePayload(args, state, delta, payloadContext)
22776
+ );
22777
+ state.fileChangeQueue.chain = next.catch((error) => {
22778
+ args.log("codex.completed_file_change_forward_failed", {
22779
+ turn_id: delta.turnId ?? null,
22780
+ item_key: delta.itemKey,
22781
+ message: error instanceof Error ? error.message : String(error)
22782
+ });
22783
+ });
22784
+ }
22530
22785
  async function forwardReasoningDeltaPayload(args, state, delta, payloadContext) {
22531
22786
  if (state.kandanThreadId === void 0 || state.codexThreadId === void 0) {
22532
22787
  return;
@@ -22794,6 +23049,87 @@ async function forwardFileChangeDeltaPayload(args, state, delta, payloadContext)
22794
23049
  patch_length: patchText.length
22795
23050
  });
22796
23051
  }
23052
+ async function forwardCompletedFileChangePayload(args, state, delta, payloadContext) {
23053
+ if (state.kandanThreadId === void 0 || state.codexThreadId === void 0) {
23054
+ return;
23055
+ }
23056
+ const turnId = delta.turnId ?? activeTurnId(state.turn);
23057
+ if (turnId === void 0 || turnIsFinalizingOrForwarded(state, turnId)) {
23058
+ return;
23059
+ }
23060
+ const sourceMessageSeq = sourceMessageSeqForTurn(state, turnId);
23061
+ if (sourceMessageSeq === void 0) {
23062
+ args.log("codex.completed_file_change_without_source_message", {
23063
+ turn_id: turnId,
23064
+ item_key: delta.itemKey
23065
+ });
23066
+ return;
23067
+ }
23068
+ const structured = codexFileChangeStructuredMessage(
23069
+ delta.itemKey,
23070
+ delta.patchText,
23071
+ "completed",
23072
+ "completed"
23073
+ );
23074
+ const existing = findStreamingFileChangeOutput(state, delta.itemKey);
23075
+ if (existing === void 0) {
23076
+ const session = args.options.channelSession;
23077
+ const reply = await pushOk2(
23078
+ args.kandan,
23079
+ args.topic,
23080
+ "session:post_thread_message",
23081
+ {
23082
+ workspace: session.workspaceSlug,
23083
+ channel: session.channelSlug,
23084
+ thread_id: state.kandanThreadId,
23085
+ body: delta.patchText,
23086
+ payload: {
23087
+ ...localRunnerPayload(
23088
+ args.options,
23089
+ args.instanceId,
23090
+ "codex_output",
23091
+ state.codexThreadId,
23092
+ payloadContext,
23093
+ sourceMessageSeq
23094
+ ),
23095
+ ...state.rootSeq === void 0 ? {} : { reply_to_seq: state.rootSeq },
23096
+ structured
23097
+ },
23098
+ client_message_id: streamingClientMessageId(args.instanceId, {
23099
+ itemKey: `file-change:${delta.itemKey}`,
23100
+ turnId
23101
+ })
23102
+ }
23103
+ );
23104
+ const seq = integerValue(reply.seq);
23105
+ if (seq !== void 0) {
23106
+ rememberStreamingFileChangeOutput(state, {
23107
+ itemKey: delta.itemKey,
23108
+ turnId,
23109
+ seq,
23110
+ patchText: delta.patchText
23111
+ });
23112
+ }
23113
+ } else {
23114
+ await editCodexStructuredOutput(
23115
+ args,
23116
+ state,
23117
+ existing.seq,
23118
+ delta.patchText,
23119
+ structured
23120
+ );
23121
+ rememberStreamingFileChangeOutput(state, {
23122
+ ...existing,
23123
+ turnId: existing.turnId ?? turnId,
23124
+ patchText: delta.patchText
23125
+ });
23126
+ }
23127
+ args.log("kandan.codex_file_change_forwarded", {
23128
+ item_key: delta.itemKey,
23129
+ turn_id: turnId,
23130
+ patch_length: delta.patchText.length
23131
+ });
23132
+ }
22797
23133
  async function forwardTerminalInput(args, state, params, payloadContext) {
22798
23134
  if (state.kandanThreadId === void 0 || state.codexThreadId === void 0) {
22799
23135
  return;
@@ -23342,6 +23678,12 @@ function codexNotificationBelongsToSession(state, threadId, turnId) {
23342
23678
  }
23343
23679
  return activeTurnId(state.turn) === turnId || isLocalTuiTurn(state, turnId);
23344
23680
  }
23681
+ function codexNotificationActiveTurnFallback(method, state, params) {
23682
+ if (method !== "response_item" || codexCompletedFileChangeFromNotification(params) === void 0) {
23683
+ return void 0;
23684
+ }
23685
+ return activeTurnId(state.turn);
23686
+ }
23345
23687
  function rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId) {
23346
23688
  if (args.options.launchTui !== true || state.codexThreadId !== threadId || state.turn.status !== "idle") {
23347
23689
  return;
@@ -24598,6 +24940,9 @@ function defaultCliAuditLogFile() {
24598
24940
  const override = process.env.LINZUMI_CLI_AUDIT_LOG?.trim();
24599
24941
  return override === void 0 || override === "" ? join3(homedir2(), ".linzumi", "logs", "command-events.jsonl") : override;
24600
24942
  }
24943
+ function defaultRunnerLogFile() {
24944
+ return join3(homedir2(), ".linzumi", "logs", "runner-events.jsonl");
24945
+ }
24601
24946
  function redactForCliLog(value) {
24602
24947
  return redactObject(value);
24603
24948
  }
@@ -28295,7 +28640,7 @@ function realpathOrResolved(pathValue) {
28295
28640
  }
28296
28641
 
28297
28642
  // src/version.ts
28298
- var linzumiCliVersion = "0.0.63-beta";
28643
+ var linzumiCliVersion = "0.0.64-beta";
28299
28644
  var linzumiCliVersionText = `linzumi ${linzumiCliVersion}`;
28300
28645
 
28301
28646
  // src/runnerLock.ts
@@ -28565,7 +28910,7 @@ function formatRunnerConsoleEvent(event, payload) {
28565
28910
  "This process is exiting."
28566
28911
  ].join("\n");
28567
28912
  case "kandan.message_ignored":
28568
- return `Incoming message from ${sender(payload)}: ignored for reason ${text(payload.reason)}`;
28913
+ return ignoredMessage(payload);
28569
28914
  case "kandan.message_queued":
28570
28915
  return `Incoming message from ${sender(payload)}: queued seq=${text(payload.seq)} depth=${text(payload.queue_depth)}`;
28571
28916
  case "kandan.chat_event_failed":
@@ -28613,6 +28958,22 @@ function optionalLine(label, value) {
28613
28958
  const normalized = stringValue2(value) ?? numberValue(value)?.toString();
28614
28959
  return normalized === void 0 ? void 0 : `${label}: ${normalized}`;
28615
28960
  }
28961
+ function optionalField(label, value) {
28962
+ const normalized = stringValue2(value) ?? numberValue(value)?.toString();
28963
+ return normalized === void 0 ? void 0 : `${label}=${normalized}`;
28964
+ }
28965
+ function ignoredMessage(payload) {
28966
+ return [
28967
+ `Incoming message from ${sender(payload)}: ignored`,
28968
+ optionalField("seq", payload.seq),
28969
+ `reason=${text(payload.reason)}`,
28970
+ optionalField("type", payload.type),
28971
+ optionalField("thread", payload.thread_id),
28972
+ optionalField("body_chars", payload.body_length),
28973
+ optionalField("attachments", payload.attachment_count),
28974
+ optionalField("local_event", payload.local_runner_event_type)
28975
+ ].filter((part) => part !== void 0).join(" ");
28976
+ }
28616
28977
  function replacementLines(value) {
28617
28978
  if (!Array.isArray(value)) {
28618
28979
  return [];
@@ -29945,9 +30306,16 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
29945
30306
  if (claudeCodeForwardSessions.has(args.sessionId)) {
29946
30307
  return;
29947
30308
  }
30309
+ if (options.portForwardWatcher === void 0) {
30310
+ log("port_forward.claude_code_watch_skipped", {
30311
+ reason: "worker_pid_missing",
30312
+ claude_session_id: args.sessionId
30313
+ });
30314
+ return;
30315
+ }
29948
30316
  const session = {
29949
30317
  ...args,
29950
- watcher: (options.portForwardWatcher ?? startPortForwardWatcher)({
30318
+ watcher: options.portForwardWatcher({
29951
30319
  onCandidate: (candidate) => publishClaudeCodeForwardPortPrompt(session, candidate),
29952
30320
  onCandidateLost: (candidate) => expireLostClaudeCodeForwardCandidate(session, candidate),
29953
30321
  onError: (error) => {
@@ -30779,7 +31147,8 @@ function controlThreadId(control) {
30779
31147
  case "forward_http_request":
30780
31148
  case "forward_tcp_open":
30781
31149
  case "forward_tcp_send":
30782
- case "forward_tcp_close": {
31150
+ case "forward_tcp_close":
31151
+ case "update_thread_interaction_access": {
30783
31152
  const threadId = stringValue(control.threadId)?.trim();
30784
31153
  return threadId === void 0 || threadId === "" ? void 0 : threadId;
30785
31154
  }
@@ -30788,6 +31157,23 @@ function controlThreadId(control) {
30788
31157
  }
30789
31158
  }
30790
31159
  async function resolveSessionControl(channelSession, dynamicChannelSessions, control) {
31160
+ const targetThreadId = controlThreadId(control);
31161
+ if (targetThreadId !== void 0) {
31162
+ const targetDynamicSession = dynamicChannelSessions.get(targetThreadId);
31163
+ const targetSessions = [
31164
+ ...channelSession?.currentKandanThreadId() === targetThreadId ? [channelSession] : [],
31165
+ ...targetDynamicSession !== void 0 && targetDynamicSession !== channelSession ? [targetDynamicSession] : []
31166
+ ].filter(
31167
+ (session) => session !== void 0
31168
+ );
31169
+ for (const session of targetSessions) {
31170
+ const handled = await session.handleControl(control);
31171
+ if (handled !== void 0) {
31172
+ return handled;
31173
+ }
31174
+ }
31175
+ return void 0;
31176
+ }
30791
31177
  const primaryHandled = await (channelSession?.handleControl(control) ?? Promise.resolve(void 0));
30792
31178
  if (primaryHandled !== void 0) {
30793
31179
  return primaryHandled;
@@ -30850,7 +31236,7 @@ function replacementRunnerSummaries(value) {
30850
31236
  }
30851
31237
  function makeRunnerLogger(options) {
30852
31238
  return createRunnerLogger(
30853
- options.logFile ?? join10(options.cwd, ".linzumi-runner.log"),
31239
+ options.logFile ?? defaultRunnerLogFile(),
30854
31240
  options.launchTui ? void 0 : reportRunnerConsoleEvent
30855
31241
  );
30856
31242
  }
@@ -31641,6 +32027,7 @@ async function applyControl(codex, kandan, topic, instanceId, options, agentProv
31641
32027
  case "stop_instance":
31642
32028
  case "kill_instance":
31643
32029
  case "resolve_port_forward_request":
32030
+ case "update_thread_interaction_access":
31644
32031
  case "update_session_settings":
31645
32032
  case "set_port_forward_enabled":
31646
32033
  case "forward_http_request":
@@ -54361,7 +54748,7 @@ Codex:
54361
54748
  Auto-approve detected port-forward candidates for started sessions
54362
54749
  --stream-flush-ms <ms> Batch live Codex deltas before Linzumi persistence, default 150
54363
54750
  --fast Mark this runner as low-latency/fast in the availability message
54364
- --log-file <path> JSONL event log path, default <cwd>/.linzumi-runner.log
54751
+ --log-file <path> JSONL event log path, default ~/.linzumi/logs/runner-events.jsonl
54365
54752
  --allowed-cwd <paths> Extra comma-separated roots where Linzumi may start local Codex sessions
54366
54753
  --forward-port <ports> Comma-separated local TCP ports Linzumi may expose as authenticated previews
54367
54754
  --code-server-bin <path> Custom development code-server executable. The default editor runtime is downloaded from Linzumi.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.63-beta",
3
+ "version": "0.0.64-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": {