@linzumi/cli 0.0.12-beta → 0.0.14-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()))
package/src/index.ts CHANGED
@@ -20,9 +20,14 @@
20
20
  local users can tune Kandan persistence batching without changing the Codex
21
21
  transcript protocol, and exposes explicit local preview forwarding ports as
22
22
  capability metadata without creating an implicit tunnel.
23
+
24
+ - Date: 2026-05-02
25
+ Spec: plans/2026-05-02-agent-first-zero-to-codex-launch-plan.md
26
+ Relationship: Routes the agent-first bootstrap commands that support the
27
+ zero-to-hello-world-pr+editor launch path.
23
28
  */
24
29
  import { randomUUID } from "node:crypto";
25
- import { realpathSync } from "node:fs";
30
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
26
31
  import { homedir } from "node:os";
27
32
  import { resolve } from "node:path";
28
33
  import { runLocalCodexRunner, type RunnerOptions } from "./runner";
@@ -58,6 +63,11 @@ import {
58
63
  trustedFetch,
59
64
  trustedWebSocketFactory,
60
65
  } from "./kandanTls";
66
+ import {
67
+ defaultAgentTokenFilePath,
68
+ readStoredAgentTokenFile,
69
+ runAgentCliCommand,
70
+ } from "./agentBootstrap";
61
71
 
62
72
  type FlagDefinition = {
63
73
  readonly kind: "value" | "boolean";
@@ -87,6 +97,7 @@ const flagDefinitions = new Map<string, FlagDefinition>([
87
97
  ["fast", { kind: "boolean" }],
88
98
  ["log-file", { kind: "value" }],
89
99
  ["auth-file", { kind: "value" }],
100
+ ["agent-token-file", { kind: "value" }],
90
101
  ["oauth-callback-host", { kind: "value" }],
91
102
  ["help", { kind: "boolean" }],
92
103
  ]);
@@ -110,7 +121,7 @@ async function main(args: readonly string[]): Promise<void> {
110
121
  process.stdout.write(connectGuideText());
111
122
  return;
112
123
  case "version":
113
- process.stdout.write("linzumi 0.0.12-beta\n");
124
+ process.stdout.write("linzumi 0.0.14-beta\n");
114
125
  return;
115
126
  case "auth":
116
127
  await runAuthCommand(parsed.args);
@@ -118,8 +129,20 @@ async function main(args: readonly string[]): Promise<void> {
118
129
  case "paths":
119
130
  runPathsCommand(parsed.args);
120
131
  return;
132
+ case "agent":
133
+ await runAgentCliCommand(parsed.args);
134
+ return;
135
+ case "agentRunner": {
136
+ const options = await parseAgentRunnerArgs(parsed.args);
137
+ await runLocalCodexRunner(options);
138
+ return;
139
+ }
121
140
  case "start": {
122
141
  const options = await parseStartRunnerArgs(parsed.args);
142
+ // Persist the resolved cwd to the trusted-paths list so the user
143
+ // doesn't have to remember to run `linzumi paths add` separately.
144
+ // addAllowedCwd is idempotent and honors LINZUMI_CONFIG_FILE for tests.
145
+ addAllowedCwd(options.cwd);
123
146
  await runLocalCodexRunner(options);
124
147
  return;
125
148
  }
@@ -136,6 +159,8 @@ type ParsedCommand =
136
159
  | { readonly command: "version"; readonly args: readonly string[] }
137
160
  | { readonly command: "auth"; readonly args: readonly string[] }
138
161
  | { readonly command: "paths"; readonly args: readonly string[] }
162
+ | { readonly command: "agent"; readonly args: readonly string[] }
163
+ | { readonly command: "agentRunner"; readonly args: readonly string[] }
139
164
  | { readonly command: "start"; readonly args: readonly string[] }
140
165
  | { readonly command: "run"; readonly args: readonly string[] };
141
166
 
@@ -158,6 +183,21 @@ function parseCommand(args: readonly string[]): ParsedCommand {
158
183
  return { command: "auth", args: rest };
159
184
  case "paths":
160
185
  return { command: "paths", args: rest };
186
+ case "agent":
187
+ return rest[0] === "runner"
188
+ ? { command: "agentRunner", args: rest.slice(1) }
189
+ : { command: "agent", args: rest };
190
+ case "agent-runner":
191
+ return { command: "agentRunner", args: rest };
192
+ case "signup":
193
+ case "claim":
194
+ case "thread":
195
+ case "post":
196
+ case "inbox":
197
+ case "done":
198
+ case "codex":
199
+ case "editor":
200
+ return { command: "agent", args };
161
201
  case "start":
162
202
  return { command: "start", args: rest };
163
203
  case "run":
@@ -270,12 +310,13 @@ export async function parseStartRunnerArgs(
270
310
 
271
311
  rejectStartTargetingFlags(values);
272
312
 
273
- const kandanUrl = stringValue(values, "kandan-url") ?? "wss://serve.kandanai.com";
313
+ const kandanUrl = stringValue(values, "kandan-url") ?? "wss://serve.linzumi.com";
274
314
  const requestedCwd = resolveUserPath(cwdArg ?? process.cwd());
275
- const allowedCwds = values.has("allowed-cwd")
315
+ const cwd = assertConfiguredAllowedCwds([requestedCwd])[0] ?? requestedCwd;
316
+ const explicitAllowedCwds = values.has("allowed-cwd")
276
317
  ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue(values, "allowed-cwd")))
277
- : assertConfiguredAllowedCwds([requestedCwd]);
278
- const cwd = allowedCwds[0] ?? requestedCwd;
318
+ : [];
319
+ const allowedCwds = Array.from(new Set([cwd, ...explicitAllowedCwds]));
279
320
  const codexBin = stringValue(values, "codex-bin") ?? "codex";
280
321
  const customCodeServerBin = stringValue(values, "code-server-bin");
281
322
  const initialDependencyStatus = await deps.buildDependencyStatus({
@@ -364,6 +405,168 @@ export async function parseStartRunnerArgs(
364
405
  };
365
406
  }
366
407
 
408
+ type AgentRunnerDeps = {
409
+ readonly readTextFile: (path: string) => string | undefined;
410
+ readonly buildDependencyStatus: typeof buildRunnerDependencyStatus;
411
+ readonly resolveEditorRuntime: typeof resolveEditorRuntime;
412
+ };
413
+
414
+ export async function parseAgentRunnerArgs(
415
+ args: readonly string[],
416
+ deps: AgentRunnerDeps = {
417
+ readTextFile: readAgentTokenTextFile,
418
+ buildDependencyStatus: buildRunnerDependencyStatus,
419
+ resolveEditorRuntime,
420
+ },
421
+ ): Promise<RunnerOptions> {
422
+ const { cwdArg, flagArgs } = splitStartArgs(args);
423
+ const values = strictFlagValues(flagArgs);
424
+
425
+ if (values.get("help") === true) {
426
+ process.stdout.write(agentRunnerHelpText());
427
+ process.exit(0);
428
+ }
429
+
430
+ rejectAgentRunnerTargetingFlags(values);
431
+
432
+ if (cwdArg !== undefined && values.has("cwd")) {
433
+ throw new Error("linzumi agent runner accepts either <folder> or --cwd, not both");
434
+ }
435
+
436
+ const tokenFilePath = stringValue(values, "agent-token-file") ?? defaultAgentTokenFilePath();
437
+ const tokenFile = readStoredAgentTokenFile(tokenFilePath, deps.readTextFile);
438
+ const channelSlug = requiredStoredAgentChannel(tokenFile.channelId);
439
+ const listenUser =
440
+ stringValue(values, "listen-user") ?? requiredStoredOwnerUsername(tokenFile.ownerUsername);
441
+ const kandanUrl = stringValue(values, "kandan-url") ?? agentApiUrlToKandanUrl(tokenFile.apiUrl);
442
+ const requestedCwd = resolveUserPath(
443
+ cwdArg ?? stringValue(values, "cwd") ?? process.cwd(),
444
+ );
445
+ const allowedCwds = values.has("allowed-cwd")
446
+ ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue(values, "allowed-cwd")))
447
+ : assertConfiguredAllowedCwds([requestedCwd]);
448
+ const cwd = allowedCwds[0] ?? requestedCwd;
449
+ const codexBin = stringValue(values, "codex-bin") ?? "codex";
450
+ const customCodeServerBin = stringValue(values, "code-server-bin");
451
+ const initialDependencyStatus = await deps.buildDependencyStatus({
452
+ cwd,
453
+ codexBin,
454
+ codeServerBin: customCodeServerBin,
455
+ });
456
+ assertStartDependencies(initialDependencyStatus);
457
+ const editorRuntime = await deps.resolveEditorRuntime({
458
+ kandanUrl,
459
+ token: tokenFile.agentToken,
460
+ customCodeServerBin,
461
+ fetchImpl: trustedFetch(kandanTlsTrustFromEnv()),
462
+ });
463
+ const dependencyStatus = await deps.buildDependencyStatus({
464
+ cwd,
465
+ codexBin,
466
+ codeServerBin: editorRuntime.codeServerBin,
467
+ editorRuntime: editorRuntime.status,
468
+ });
469
+ assertStartDependencies(dependencyStatus);
470
+
471
+ return {
472
+ kandanUrl,
473
+ token: tokenFile.agentToken,
474
+ runnerId: stringValue(values, "runner-id") ?? `agent-runner-${randomUUID()}`,
475
+ cwd,
476
+ codexBin,
477
+ codexUrl: stringValue(values, "codex-url"),
478
+ launchTui: values.get("launch-tui") === true,
479
+ fast: values.get("fast") === true,
480
+ logFile: stringValue(values, "log-file"),
481
+ allowedCwds,
482
+ allowedForwardPorts: parseAllowedPortList(
483
+ stringValue(values, "forward-port"),
484
+ ),
485
+ codeServerBin: editorRuntime.codeServerBin,
486
+ editorRuntime: editorRuntime.runtime,
487
+ socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
488
+ dependencyStatus,
489
+ channelSession: {
490
+ workspaceSlug: tokenFile.workspaceId,
491
+ channelSlug,
492
+ kandanThreadId: stringValue(values, "kandan-thread-id"),
493
+ listenUser,
494
+ model: stringValue(values, "model"),
495
+ reasoningEffort: stringValue(values, "reasoning-effort"),
496
+ sandbox: stringValue(values, "sandbox"),
497
+ approvalPolicy: stringValue(values, "approval-policy"),
498
+ streamFlushMs: positiveIntegerValue(values, "stream-flush-ms"),
499
+ },
500
+ };
501
+ }
502
+
503
+ function readAgentTokenTextFile(path: string): string | undefined {
504
+ return existsSync(path) ? readFileSync(path, "utf8") : undefined;
505
+ }
506
+
507
+ function rejectAgentRunnerTargetingFlags(values: Map<string, string | true>): void {
508
+ const unsupportedFlags = ["workspace", "channel", "token", "auth-file", "oauth-callback-host"]
509
+ .filter((flag) => values.has(flag));
510
+
511
+ if (unsupportedFlags.length > 0) {
512
+ throw new Error(
513
+ `linzumi agent runner uses the claimed agent token scope; remove ${unsupportedFlags
514
+ .map((flag) => `--${flag}`)
515
+ .join(", ")}.`,
516
+ );
517
+ }
518
+ }
519
+
520
+ function requiredStoredAgentChannel(channelId: string | undefined): string {
521
+ if (channelId !== undefined) {
522
+ return channelId;
523
+ }
524
+
525
+ throw new Error(
526
+ "agent token file is missing channelId; rerun linzumi claim before starting an agent runner",
527
+ );
528
+ }
529
+
530
+ function requiredStoredOwnerUsername(ownerUsername: string | undefined): string {
531
+ if (ownerUsername !== undefined) {
532
+ return ownerUsername;
533
+ }
534
+
535
+ throw new Error(
536
+ "agent token file is missing ownerUsername; rerun linzumi claim or pass --listen-user explicitly",
537
+ );
538
+ }
539
+
540
+ function agentApiUrlToKandanUrl(apiUrl: string): string {
541
+ const url = parseAgentApiUrl(apiUrl);
542
+
543
+ switch (url.protocol) {
544
+ case "https:":
545
+ url.protocol = "wss:";
546
+ return trimTrailingSlash(url.toString());
547
+ case "http:":
548
+ url.protocol = "ws:";
549
+ return trimTrailingSlash(url.toString());
550
+ case "wss:":
551
+ case "ws:":
552
+ return trimTrailingSlash(url.toString());
553
+ default:
554
+ throw new Error("agent token file apiUrl must use http, https, ws, or wss");
555
+ }
556
+ }
557
+
558
+ function parseAgentApiUrl(apiUrl: string): URL {
559
+ try {
560
+ return new URL(apiUrl);
561
+ } catch (_error) {
562
+ throw new Error("agent token file apiUrl is not a valid URL");
563
+ }
564
+ }
565
+
566
+ function trimTrailingSlash(value: string): string {
567
+ return value.endsWith("/") ? value.slice(0, -1) : value;
568
+ }
569
+
367
570
  async function resolveStartTargetToken(args: {
368
571
  readonly kandanUrl: string;
369
572
  readonly explicitToken?: string | undefined;
@@ -411,7 +614,7 @@ export async function parseRunnerArgs(
411
614
  }
412
615
 
413
616
  if (values.get("version") === true) {
414
- process.stdout.write("linzumi 0.0.12-beta\n");
617
+ process.stdout.write("linzumi 0.0.14-beta\n");
415
618
  process.exit(0);
416
619
  }
417
620
 
@@ -706,25 +909,32 @@ function positiveIntegerValue(
706
909
  }
707
910
 
708
911
  function helpText(): string {
709
- return `Kandan local Codex runner
912
+ return `Linzumi local Codex runner
710
913
 
711
914
  Usage:
712
915
  linzumi
916
+ linzumi signup --email <email> --agent-name <name>
917
+ linzumi claim --pending <pending_id> --code <XXXX-XXXX>
918
+ linzumi thread new <title> --message <message>
919
+ linzumi post <thread_id> <message>
920
+ linzumi inbox <thread_id> --since-last
921
+ linzumi done <thread_id> --message <message>
922
+ linzumi agent runner <folder> [options]
713
923
  linzumi start <folder> [options]
714
924
  linzumi paths list|add|remove [path]
715
925
  linzumi connect --kandan-url <ws-url> --workspace <slug> --channel <slug> [options]
716
926
  linzumi auth --kandan-url <ws-url> [--workspace <slug> --channel <slug>]
717
927
 
718
928
  Required:
719
- --kandan-url <ws-url> Kandan websocket base URL, for example ws://127.0.0.1:4160
720
- --token <jwt> Optional override token. Otherwise ~/.kandan/auth.json is validated or OAuth opens.
721
- --auth-file <path> Auth cache path, default ~/.kandan/auth.json
722
- --oauth-callback-host <ip> Callback host reachable by your browser, for example a Tailscale IP
929
+ --kandan-url <ws-url> Linzumi backend URL, default wss://serve.linzumi.com
930
+ --token <jwt> Optional override token. Otherwise ~/.linzumi/auth.json is validated or OAuth opens.
931
+ --auth-file <path> Auth cache path, default ~/.linzumi/auth.json
932
+ --oauth-callback-host <ip> Callback host reachable by your browser
723
933
 
724
934
  Channel binding:
725
935
  --workspace <slug> Workspace slug
726
936
  --channel <slug|w/c> Channel slug, or workspace/channel
727
- --kandan-thread-id <uuid> Resume an existing Kandan thread instead of announcing a new root
937
+ --kandan-thread-id <uuid> Resume an existing Linzumi thread instead of announcing a new root
728
938
  --listen-user <user|all> User whose replies are accepted, default authenticated user
729
939
 
730
940
  Codex:
@@ -732,42 +942,41 @@ Codex:
732
942
  --codex-bin <path> Codex executable, default codex
733
943
  --codex-url <ws-url> Existing Codex app-server websocket URL
734
944
  --launch-tui Launch codex --remote against the app-server
735
- --model <name> Model requested for the Codex thread and shown in Kandan
736
- --reasoning-effort <value> Reasoning effort requested for Codex and shown in Kandan
737
- --sandbox <value> Sandbox metadata shown in Kandan
738
- --approval-policy <value> Approval-policy metadata shown in Kandan
739
- --stream-flush-ms <ms> Batch live Codex deltas before Kandan persistence, default 150
945
+ --model <name> Model requested for the Codex thread and shown in Linzumi
946
+ --reasoning-effort <value> Reasoning effort requested for Codex and shown in Linzumi
947
+ --sandbox <value> Sandbox metadata shown in Linzumi
948
+ --approval-policy <value> Approval-policy metadata shown in Linzumi
949
+ --stream-flush-ms <ms> Batch live Codex deltas before Linzumi persistence, default 150
740
950
  --fast Mark this runner as low-latency/fast in the availability message
741
- --log-file <path> JSONL event log path, default <cwd>/.kandan-local-codex-runner.log
742
- --allowed-cwd <paths> Comma-separated roots where Kandan may start new local Codex sessions
743
- --forward-port <ports> Comma-separated local TCP ports Kandan may expose as authenticated previews
744
- --code-server-bin <path> Custom development code-server executable. The default editor runtime is downloaded from Kandan.
951
+ --log-file <path> JSONL event log path, default <cwd>/.linzumi-runner.log
952
+ --allowed-cwd <paths> Comma-separated roots where Linzumi may start new local Codex sessions
953
+ --forward-port <ports> Comma-separated local TCP ports Linzumi may expose as authenticated previews
954
+ --code-server-bin <path> Custom development code-server executable. The default editor runtime is downloaded from Linzumi.
745
955
 
746
956
  Examples:
747
957
  Good:
958
+ linzumi signup --email alice@example.com --agent-name BuildBot
959
+ linzumi claim --pending pnd_2k4f9w --code 7K2C-9X4M
960
+ linzumi thread new "Hello world" --message "Starting now. ETA 3m."
961
+ linzumi post thr_abc123 "PR is open"
962
+ linzumi done thr_abc123 --message "Done: https://github.com/example/repo/pull/1"
963
+ linzumi agent runner ~/code/my-app --runner-id launch-agent-runner
748
964
  linzumi start ~/
749
- linzumi start ~/code/my-app --kandan-url wss://serve.kandanai.com
750
- linzumi connect --kandan-url wss://serve.kandanai.com --workspace linzumi --channel seans-playground --codex-bin codex --launch-tui
751
- linzumi connect --kandan-url wss://serve.kandanai.com --workspace linzumi --channel seans-playground --codex-bin codex --model gpt-5.5 --reasoning-effort low --fast --launch-tui
752
- linzumi auth --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground
753
- linzumi auth --kandan-url ws://100.71.192.98:4160 --oauth-callback-host 100.71.192.98 --workspace default --channel seans-playground
754
- linzumi paths add ~/code/linzumi
965
+ linzumi start ~/code/my-app
966
+ linzumi connect --workspace <your-workspace> --channel <your-channel> --launch-tui
967
+ linzumi connect --workspace <your-workspace> --channel <your-channel> --model gpt-5 --reasoning-effort low --fast --launch-tui
968
+ linzumi auth --workspace <your-workspace> --channel <your-channel>
969
+ linzumi paths add ~/code/my-app
755
970
  linzumi paths list
756
- linzumi connect --kandan-url ws://127.0.0.1:4160 --token "$TOKEN" --workspace default --channel seans-playground --cwd /tmp/kandan-runner-a
757
- linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground
758
- linzumi connect --kandan-url ws://127.0.0.1:4160 --token "$TOKEN" --channel default/seans-playground --listen-user all --launch-tui
759
- linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground --allowed-cwd ~/code/linzumi,~/scratch
760
- linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground --forward-port 3000,5173
761
- linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground --allowed-cwd ~/code/linzumi
762
971
 
763
972
  Bad:
764
- linzumi connect --kandan-url ws://127.0.0.1:4160 --token not-a-jwt --workspace default --channel seans-playground
973
+ linzumi connect --token not-a-jwt --workspace <your-workspace> --channel <your-channel>
765
974
  Missing --listen-user and authenticated user is unavailable.
766
- linzumi connect --kandan-url ws://127.0.0.1:4160 --token "$TOKEN" --listen-users sean
975
+ linzumi connect --token "$TOKEN" --listen-users sean
767
976
  Invalid flag: use --listen-user.
768
- linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground --allowed-cwd /does/not/exist
977
+ linzumi connect --workspace <your-workspace> --channel <your-channel> --allowed-cwd /does/not/exist
769
978
  Invalid --allowed-cwd: allowed cwd roots must exist locally.
770
- linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground --forward-port vite
979
+ linzumi connect --workspace <your-workspace> --channel <your-channel> --forward-port vite
771
980
  Invalid --forward-port: value must be a TCP port from 1 to 65535.
772
981
  `;
773
982
  }
@@ -792,29 +1001,62 @@ Usage:
792
1001
  linzumi start <folder> [options]
793
1002
 
794
1003
  What it does:
795
- Opens Kandan in your browser, creates or reuses your personal coding space,
796
- grants a scoped local-runner token, and starts this computer as a runner for
797
- Codex sessions, local previews, and the Kandan editor.
1004
+ Opens Linzumi in your browser, creates or reuses your personal coding space,
1005
+ grants a scoped local-runner token, persists <folder> to your trusted-paths
1006
+ list, and starts this computer as a runner for Codex sessions, local
1007
+ previews, and the browser VS Code editor.
798
1008
 
799
1009
  Options:
800
- --kandan-url <ws-url> Kandan websocket base URL, default wss://serve.kandanai.com
1010
+ --kandan-url <ws-url> Linzumi backend URL, default wss://serve.linzumi.com
801
1011
  --token <jwt> Optional scoped local-runner token override
802
- --auth-file <path> Auth cache path, default ~/.kandan/auth.json
1012
+ --auth-file <path> Auth cache path, default ~/.linzumi/auth.json
803
1013
  --oauth-callback-host <ip> Callback host reachable by your browser
1014
+ --codex-bin <path> Codex executable, default codex
1015
+ --code-server-bin <path> Custom development code-server executable. By default Linzumi installs the approved editor runtime.
1016
+ --listen-user <user|all> User whose replies are accepted, default authenticated user
1017
+ --model <name> Model requested for Codex sessions
1018
+ --reasoning-effort <value> Reasoning effort requested for Codex sessions
1019
+ --sandbox <value> Sandbox metadata shown in Linzumi
1020
+ --approval-policy <value> Approval-policy metadata shown in Linzumi
1021
+ --forward-port <ports> Comma-separated local TCP ports Linzumi may expose as previews
1022
+ --fast Mark this runner as low-latency/fast in Linzumi
1023
+
1024
+ Examples:
1025
+ linzumi start ~/
1026
+ linzumi start ~/code/my-app
1027
+ `;
1028
+ }
1029
+
1030
+ function agentRunnerHelpText(): string {
1031
+ return `Linzumi agent-owned local runner
1032
+
1033
+ Usage:
1034
+ linzumi agent runner <folder> [options]
1035
+
1036
+ What it does:
1037
+ Starts this computer as the claimed agent's scoped local runner. The command
1038
+ reads ~/.linzumi/agent-token.json, uses its workspace/channel scope, trusts
1039
+ only the selected folder by default, and listens only to the owning human
1040
+ recorded during claim unless --listen-user is passed.
1041
+
1042
+ Options:
1043
+ --agent-token-file <path> Agent token cache, default ~/.linzumi/agent-token.json
1044
+ --kandan-url <ws-url> Kandan websocket base URL. Defaults deterministically from the stored apiUrl.
804
1045
  --runner-id <id> Stable local runner id
805
1046
  --codex-bin <path> Codex executable, default codex
806
1047
  --code-server-bin <path> Custom development code-server executable. By default Kandan installs the approved editor runtime.
807
- --listen-user <user|all> User whose replies are accepted, default authenticated user
1048
+ --listen-user <user> Human whose replies Codex may accept, default owner from claim
808
1049
  --model <name> Model requested for Codex sessions
809
1050
  --reasoning-effort <value> Reasoning effort requested for Codex sessions
810
1051
  --sandbox <value> Sandbox metadata shown in Kandan
811
1052
  --approval-policy <value> Approval-policy metadata shown in Kandan
812
1053
  --forward-port <ports> Comma-separated local TCP ports Kandan may expose as previews
1054
+ --allowed-cwd <paths> Override the selected folder with comma-separated trusted roots
813
1055
  --fast Mark this runner as low-latency/fast in Kandan
814
1056
 
815
1057
  Examples:
816
- linzumi start ~/
817
- linzumi start ~/code/my-app --kandan-url ws://100.71.192.98:4162 --oauth-callback-host 100.71.192.98
1058
+ linzumi agent runner "$PWD" --runner-id hello-world-agent
1059
+ linzumi agent runner ~/code/my-app --kandan-url ws://127.0.0.1:4162 --runner-id local-qa-agent
818
1060
  `;
819
1061
  }
820
1062
 
@@ -824,17 +1066,12 @@ function connectGuideText(): string {
824
1066
  Fastest path:
825
1067
  linzumi start ~/
826
1068
 
827
- This opens Kandan in your browser, creates or reuses your personal coding
828
- space, and starts this computer as a local Codex runner.
829
-
830
- Advanced:
831
- linzumi paths add "$PWD"
832
- linzumi connect
1069
+ This opens Linzumi in your browser, creates or reuses your personal coding
1070
+ space, persists this folder to your trusted-paths list, and starts this
1071
+ computer as a local Codex runner.
833
1072
 
834
- Connect this computer to Kandan as a local Codex runner.
835
-
836
- Use:
837
- linzumi connect --kandan-url <ws-url> --workspace <slug> --channel <slug> [options]
1073
+ Advanced (when you already know your workspace and channel):
1074
+ linzumi connect --workspace <your-workspace> --channel <your-channel>
838
1075
 
839
1076
  For help:
840
1077
  linzumi connect --help