@linzumi/cli 0.0.11-beta → 0.0.13-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.
@@ -8,12 +8,26 @@
8
8
  Spec: plans/2026-04-24-local-codex-runner-deep-quality-spec.md
9
9
  Relationship: Keeps local Codex version discovery bounded so channel
10
10
  availability is not blocked indefinitely by a bad executable.
11
+
12
+ - Date: 2026-05-02
13
+ Spec: plans/2026-05-02-agent-first-zero-to-codex-launch-plan.md
14
+ Relationship: Keeps the npm CLI runner in parity with the legacy local
15
+ runner by carrying Kandan attachment metadata into queued Codex input.
11
16
  */
12
17
  import { spawnSync } from "node:child_process";
13
18
  import { hostname } from "node:os";
14
- import { integerValue, objectValue, stringValue } from "./json";
19
+ import { arrayValue, integerValue, objectValue, stringValue } from "./json";
15
20
  import { type JsonObject, type JsonValue, isJsonObject } from "./protocol";
16
21
 
22
+ export type KandanChatAttachment = {
23
+ readonly id: string | undefined;
24
+ readonly kind: string | undefined;
25
+ readonly fileName: string | undefined;
26
+ readonly contentType: string | undefined;
27
+ readonly sizeBytes: number | undefined;
28
+ readonly url: string | undefined;
29
+ };
30
+
17
31
  export type KandanChatEvent = {
18
32
  readonly seq: number;
19
33
  readonly type: string;
@@ -24,6 +38,7 @@ export type KandanChatEvent = {
24
38
  readonly replyToSeq: number | undefined;
25
39
  readonly localRunnerEventType: string | undefined;
26
40
  readonly body: string;
41
+ readonly attachments: readonly KandanChatAttachment[];
27
42
  };
28
43
 
29
44
  export type RunnerIdentity = {
@@ -114,9 +129,45 @@ export function parseKandanChatEvent(
114
129
  stringValue(payload.body) ??
115
130
  stringValue(payload.text) ??
116
131
  "",
132
+ attachments: parseKandanChatAttachments(
133
+ arrayValue(payload.attachments) ?? arrayValue(value.attachments) ?? [],
134
+ ),
117
135
  };
118
136
  }
119
137
 
138
+ function parseKandanChatAttachments(
139
+ attachments: readonly JsonValue[],
140
+ ): readonly KandanChatAttachment[] {
141
+ return attachments.flatMap(attachment => {
142
+ const value = objectValue(attachment);
143
+
144
+ if (value === undefined) {
145
+ return [];
146
+ }
147
+
148
+ return [
149
+ {
150
+ id: stringValue(value.id),
151
+ kind: stringValue(value.kind),
152
+ fileName:
153
+ stringValue(value.file_name) ??
154
+ stringValue(value.fileName) ??
155
+ stringValue(value.name),
156
+ contentType:
157
+ stringValue(value.content_type) ??
158
+ stringValue(value.contentType) ??
159
+ stringValue(value.mime_type) ??
160
+ stringValue(value.mimeType),
161
+ sizeBytes:
162
+ integerValue(value.size_bytes) ??
163
+ integerValue(value.sizeBytes) ??
164
+ integerValue(value.size),
165
+ url: stringValue(value.url),
166
+ },
167
+ ];
168
+ });
169
+ }
170
+
120
171
  export function isCodexAuthoredEvent(
121
172
  event: Pick<
122
173
  KandanChatEvent,
@@ -206,10 +257,12 @@ export function localRunnerPayload(
206
257
  codexThreadId: string,
207
258
  context: RunnerPayloadContext,
208
259
  sourceMessageSeq?: number | undefined,
260
+ extraLocalRunnerMetadata?: JsonObject | undefined,
209
261
  ): JsonObject {
210
262
  return {
211
263
  metadata: {
212
264
  local_codex_runner: {
265
+ ...(extraLocalRunnerMetadata ?? {}),
213
266
  runner_id: options.runnerId,
214
267
  instance_id: instanceId,
215
268
  event_type: eventType,
@@ -394,14 +394,15 @@ function openWebSocketStream(
394
394
 
395
395
  let opened = false;
396
396
  const pendingMessages: WebSocketPendingMessage[] = [];
397
- const socket = new WebSocket(
398
- localForwardWebSocketUrl(
399
- scheme,
400
- request.port,
401
- request.path,
402
- request.queryString,
403
- ),
397
+ const url = localForwardWebSocketUrl(
398
+ scheme,
399
+ request.port,
400
+ request.path,
401
+ request.queryString,
404
402
  );
403
+ const protocols = webSocketProtocols(request.headers);
404
+ const socket =
405
+ protocols === undefined ? new WebSocket(url) : new WebSocket(url, protocols);
405
406
  streams.set(streamId, { kind: "websocket", socket, pendingMessages });
406
407
  socket.addEventListener("open", () => {
407
408
  opened = true;
@@ -425,6 +426,11 @@ function openWebSocketStream(
425
426
  });
426
427
  });
427
428
  socket.addEventListener("close", (event) => {
429
+ const stream = streams.get(streamId);
430
+ if (stream?.kind !== "websocket" || stream.socket !== socket) {
431
+ return;
432
+ }
433
+
428
434
  streams.delete(streamId);
429
435
  send({
430
436
  type: "websocket_close",
@@ -433,6 +439,11 @@ function openWebSocketStream(
433
439
  });
434
440
  });
435
441
  socket.addEventListener("error", () => {
442
+ const stream = streams.get(streamId);
443
+ if (stream?.kind !== "websocket" || stream.socket !== socket) {
444
+ return;
445
+ }
446
+
436
447
  streams.delete(streamId);
437
448
  if (!opened && scheme === "ws") {
438
449
  openWebSocketStream(
@@ -648,6 +659,20 @@ function requestHeaders(headers: JsonValue | undefined): Headers {
648
659
  return result;
649
660
  }
650
661
 
662
+ function webSocketProtocols(headers: Headers): string[] | undefined {
663
+ const protocolHeader = headers.get("sec-websocket-protocol");
664
+ if (protocolHeader === null) {
665
+ return undefined;
666
+ }
667
+
668
+ const protocols = protocolHeader
669
+ .split(",")
670
+ .map((protocol) => protocol.trim())
671
+ .filter((protocol) => protocol !== "");
672
+
673
+ return protocols.length === 0 ? undefined : protocols;
674
+ }
675
+
651
676
  function responseHeaders(headers: Headers): JsonObject[] {
652
677
  return Array.from(headers.entries())
653
678
  .filter(([name]) => !blockedFetchResponseHeaderNames.has(name.toLowerCase()))