@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.
package/src/index.ts CHANGED
@@ -20,8 +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";
30
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
25
31
  import { homedir } from "node:os";
26
32
  import { resolve } from "node:path";
27
33
  import { runLocalCodexRunner, type RunnerOptions } from "./runner";
@@ -30,9 +36,17 @@ import { resolveLocalRunnerToken } from "./authResolution";
30
36
  import { identityFromAccessToken } from "./channelSessionSupport";
31
37
  import {
32
38
  assertConfiguredAllowedCwds,
39
+ expandUserPath,
33
40
  parseAllowedCwdList,
34
41
  parseAllowedPortList,
35
42
  } from "./localCapabilities";
43
+ import {
44
+ addAllowedCwd,
45
+ localConfigPath,
46
+ readConfiguredAllowedCwds,
47
+ readLocalConfig,
48
+ removeAllowedCwd,
49
+ } from "./localConfig";
36
50
  import {
37
51
  acquireLocalRunnerTokenDetails,
38
52
  fetchLocalRunnerStartTarget,
@@ -49,6 +63,11 @@ import {
49
63
  trustedFetch,
50
64
  trustedWebSocketFactory,
51
65
  } from "./kandanTls";
66
+ import {
67
+ defaultAgentTokenFilePath,
68
+ readStoredAgentTokenFile,
69
+ runAgentCliCommand,
70
+ } from "./agentBootstrap";
52
71
 
53
72
  type FlagDefinition = {
54
73
  readonly kind: "value" | "boolean";
@@ -78,6 +97,7 @@ const flagDefinitions = new Map<string, FlagDefinition>([
78
97
  ["fast", { kind: "boolean" }],
79
98
  ["log-file", { kind: "value" }],
80
99
  ["auth-file", { kind: "value" }],
100
+ ["agent-token-file", { kind: "value" }],
81
101
  ["oauth-callback-host", { kind: "value" }],
82
102
  ["help", { kind: "boolean" }],
83
103
  ]);
@@ -101,13 +121,28 @@ async function main(args: readonly string[]): Promise<void> {
101
121
  process.stdout.write(connectGuideText());
102
122
  return;
103
123
  case "version":
104
- process.stdout.write("linzumi 0.0.11-beta\n");
124
+ process.stdout.write("linzumi 0.0.13-beta\n");
105
125
  return;
106
126
  case "auth":
107
127
  await runAuthCommand(parsed.args);
108
128
  return;
129
+ case "paths":
130
+ runPathsCommand(parsed.args);
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
+ }
109
140
  case "start": {
110
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);
111
146
  await runLocalCodexRunner(options);
112
147
  return;
113
148
  }
@@ -123,6 +158,9 @@ type ParsedCommand =
123
158
  | { readonly command: "guide"; readonly args: readonly string[] }
124
159
  | { readonly command: "version"; readonly args: readonly string[] }
125
160
  | { readonly command: "auth"; readonly args: readonly string[] }
161
+ | { readonly command: "paths"; readonly args: readonly string[] }
162
+ | { readonly command: "agent"; readonly args: readonly string[] }
163
+ | { readonly command: "agentRunner"; readonly args: readonly string[] }
126
164
  | { readonly command: "start"; readonly args: readonly string[] }
127
165
  | { readonly command: "run"; readonly args: readonly string[] };
128
166
 
@@ -143,6 +181,23 @@ function parseCommand(args: readonly string[]): ParsedCommand {
143
181
  return { command: "run", args: ["--help"] };
144
182
  case "auth":
145
183
  return { command: "auth", args: rest };
184
+ case "paths":
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 };
146
201
  case "start":
147
202
  return { command: "start", args: rest };
148
203
  case "run":
@@ -152,6 +207,53 @@ function parseCommand(args: readonly string[]): ParsedCommand {
152
207
  }
153
208
  }
154
209
 
210
+ function runPathsCommand(args: readonly string[]): void {
211
+ const [subcommand, pathValue, ...rest] = args;
212
+
213
+ if (subcommand === undefined || subcommand === "help" || subcommand === "--help") {
214
+ process.stdout.write(pathsHelpText());
215
+ return;
216
+ }
217
+
218
+ if (rest.length > 0) {
219
+ throw new Error("linzumi paths accepts one path argument");
220
+ }
221
+
222
+ switch (subcommand) {
223
+ case "list": {
224
+ const config = readLocalConfig();
225
+ if (config.allowedCwds.length === 0) {
226
+ process.stdout.write(`No trusted paths configured in ${localConfigPath()}\n`);
227
+ return;
228
+ }
229
+
230
+ process.stdout.write(`${config.allowedCwds.join("\n")}\n`);
231
+ return;
232
+ }
233
+ case "add": {
234
+ if (pathValue === undefined || pathValue.trim() === "") {
235
+ throw new Error("missing path for linzumi paths add");
236
+ }
237
+
238
+ const trustedPath = realpathSync(resolve(expandUserPath(pathValue)));
239
+ addAllowedCwd(pathValue);
240
+ process.stdout.write(`Trusted ${trustedPath}\n`);
241
+ return;
242
+ }
243
+ case "remove": {
244
+ if (pathValue === undefined || pathValue.trim() === "") {
245
+ throw new Error("missing path for linzumi paths remove");
246
+ }
247
+
248
+ removeAllowedCwd(pathValue);
249
+ process.stdout.write(`Removed trusted path ${pathValue}\n`);
250
+ return;
251
+ }
252
+ default:
253
+ throw new Error(`invalid paths command: ${subcommand}`);
254
+ }
255
+ }
256
+
155
257
  async function runAuthCommand(args: readonly string[]): Promise<void> {
156
258
  const values = strictFlagValues(args);
157
259
 
@@ -208,10 +310,13 @@ export async function parseStartRunnerArgs(
208
310
 
209
311
  rejectStartTargetingFlags(values);
210
312
 
211
- const kandanUrl = stringValue(values, "kandan-url") ?? "wss://serve.kandanai.com";
313
+ const kandanUrl = stringValue(values, "kandan-url") ?? "wss://serve.linzumi.com";
212
314
  const requestedCwd = resolveUserPath(cwdArg ?? process.cwd());
213
- const allowedCwds = assertConfiguredAllowedCwds([requestedCwd]);
214
- const cwd = allowedCwds[0] ?? requestedCwd;
315
+ const cwd = assertConfiguredAllowedCwds([requestedCwd])[0] ?? requestedCwd;
316
+ const explicitAllowedCwds = values.has("allowed-cwd")
317
+ ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue(values, "allowed-cwd")))
318
+ : [];
319
+ const allowedCwds = Array.from(new Set([cwd, ...explicitAllowedCwds]));
215
320
  const codexBin = stringValue(values, "codex-bin") ?? "codex";
216
321
  const customCodeServerBin = stringValue(values, "code-server-bin");
217
322
  const initialDependencyStatus = await deps.buildDependencyStatus({
@@ -300,6 +405,168 @@ export async function parseStartRunnerArgs(
300
405
  };
301
406
  }
302
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
+
303
570
  async function resolveStartTargetToken(args: {
304
571
  readonly kandanUrl: string;
305
572
  readonly explicitToken?: string | undefined;
@@ -347,7 +614,7 @@ export async function parseRunnerArgs(
347
614
  }
348
615
 
349
616
  if (values.get("version") === true) {
350
- process.stdout.write("linzumi 0.0.11-beta\n");
617
+ process.stdout.write("linzumi 0.0.13-beta\n");
351
618
  process.exit(0);
352
619
  }
353
620
 
@@ -395,9 +662,9 @@ export async function parseRunnerArgs(
395
662
  launchTui: values.get("launch-tui") === true,
396
663
  fast: values.get("fast") === true,
397
664
  logFile: stringValue(values, "log-file"),
398
- allowedCwds: assertConfiguredAllowedCwds(
399
- parseAllowedCwdList(stringValue(values, "allowed-cwd")),
400
- ),
665
+ allowedCwds: values.has("allowed-cwd")
666
+ ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue(values, "allowed-cwd")))
667
+ : readConfiguredAllowedCwds(),
401
668
  allowedForwardPorts: parseAllowedPortList(
402
669
  stringValue(values, "forward-port"),
403
670
  ),
@@ -642,24 +909,32 @@ function positiveIntegerValue(
642
909
  }
643
910
 
644
911
  function helpText(): string {
645
- return `Kandan local Codex runner
912
+ return `Linzumi local Codex runner
646
913
 
647
914
  Usage:
648
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]
649
923
  linzumi start <folder> [options]
924
+ linzumi paths list|add|remove [path]
650
925
  linzumi connect --kandan-url <ws-url> --workspace <slug> --channel <slug> [options]
651
926
  linzumi auth --kandan-url <ws-url> [--workspace <slug> --channel <slug>]
652
927
 
653
928
  Required:
654
- --kandan-url <ws-url> Kandan websocket base URL, for example ws://127.0.0.1:4160
655
- --token <jwt> Optional override token. Otherwise ~/.kandan/auth.json is validated or OAuth opens.
656
- --auth-file <path> Auth cache path, default ~/.kandan/auth.json
657
- --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
658
933
 
659
934
  Channel binding:
660
935
  --workspace <slug> Workspace slug
661
936
  --channel <slug|w/c> Channel slug, or workspace/channel
662
- --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
663
938
  --listen-user <user|all> User whose replies are accepted, default authenticated user
664
939
 
665
940
  Codex:
@@ -667,44 +942,58 @@ Codex:
667
942
  --codex-bin <path> Codex executable, default codex
668
943
  --codex-url <ws-url> Existing Codex app-server websocket URL
669
944
  --launch-tui Launch codex --remote against the app-server
670
- --model <name> Model requested for the Codex thread and shown in Kandan
671
- --reasoning-effort <value> Reasoning effort requested for Codex and shown in Kandan
672
- --sandbox <value> Sandbox metadata shown in Kandan
673
- --approval-policy <value> Approval-policy metadata shown in Kandan
674
- --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
675
950
  --fast Mark this runner as low-latency/fast in the availability message
676
- --log-file <path> JSONL event log path, default <cwd>/.kandan-local-codex-runner.log
677
- --allowed-cwd <paths> Comma-separated roots where Kandan may start new local Codex sessions
678
- --forward-port <ports> Comma-separated local TCP ports Kandan may expose as authenticated previews
679
- --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.
680
955
 
681
956
  Examples:
682
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
683
964
  linzumi start ~/
684
- linzumi start ~/code/my-app --kandan-url wss://serve.kandanai.com
685
- linzumi connect --kandan-url wss://serve.kandanai.com --workspace linzumi --channel seans-playground --codex-bin codex --launch-tui
686
- 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
687
- linzumi auth --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground
688
- linzumi auth --kandan-url ws://100.71.192.98:4160 --oauth-callback-host 100.71.192.98 --workspace default --channel seans-playground
689
- linzumi connect --kandan-url ws://127.0.0.1:4160 --token "$TOKEN" --workspace default --channel seans-playground --cwd /tmp/kandan-runner-a
690
- linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground
691
- linzumi connect --kandan-url ws://127.0.0.1:4160 --token "$TOKEN" --channel default/seans-playground --listen-user all --launch-tui
692
- linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground --allowed-cwd ~/code/linzumi,~/scratch
693
- linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground --forward-port 3000,5173
694
- linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground --allowed-cwd ~/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
970
+ linzumi paths list
695
971
 
696
972
  Bad:
697
- 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>
698
974
  Missing --listen-user and authenticated user is unavailable.
699
- linzumi connect --kandan-url ws://127.0.0.1:4160 --token "$TOKEN" --listen-users sean
975
+ linzumi connect --token "$TOKEN" --listen-users sean
700
976
  Invalid flag: use --listen-user.
701
- 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
702
978
  Invalid --allowed-cwd: allowed cwd roots must exist locally.
703
- 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
704
980
  Invalid --forward-port: value must be a TCP port from 1 to 65535.
705
981
  `;
706
982
  }
707
983
 
984
+ function pathsHelpText(): string {
985
+ return `Linzumi trusted paths
986
+
987
+ Usage:
988
+ linzumi paths list
989
+ linzumi paths add <path>
990
+ linzumi paths remove <path>
991
+
992
+ Trusted paths are stored in ~/.linzumi/config.json. linzumi connect uses them
993
+ unless --allowed-cwd is passed for that runner process.
994
+ `;
995
+ }
996
+
708
997
  function startHelpText(): string {
709
998
  return `Linzumi one-command local runner
710
999
 
@@ -712,29 +1001,62 @@ Usage:
712
1001
  linzumi start <folder> [options]
713
1002
 
714
1003
  What it does:
715
- Opens Kandan in your browser, creates or reuses your personal coding space,
716
- grants a scoped local-runner token, and starts this computer as a runner for
717
- 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.
718
1008
 
719
1009
  Options:
720
- --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
721
1011
  --token <jwt> Optional scoped local-runner token override
722
- --auth-file <path> Auth cache path, default ~/.kandan/auth.json
1012
+ --auth-file <path> Auth cache path, default ~/.linzumi/auth.json
723
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.
724
1045
  --runner-id <id> Stable local runner id
725
1046
  --codex-bin <path> Codex executable, default codex
726
1047
  --code-server-bin <path> Custom development code-server executable. By default Kandan installs the approved editor runtime.
727
- --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
728
1049
  --model <name> Model requested for Codex sessions
729
1050
  --reasoning-effort <value> Reasoning effort requested for Codex sessions
730
1051
  --sandbox <value> Sandbox metadata shown in Kandan
731
1052
  --approval-policy <value> Approval-policy metadata shown in Kandan
732
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
733
1055
  --fast Mark this runner as low-latency/fast in Kandan
734
1056
 
735
1057
  Examples:
736
- linzumi start ~/
737
- 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
738
1060
  `;
739
1061
  }
740
1062
 
@@ -744,16 +1066,12 @@ function connectGuideText(): string {
744
1066
  Fastest path:
745
1067
  linzumi start ~/
746
1068
 
747
- This opens Kandan in your browser, creates or reuses your personal coding
748
- space, and starts this computer as a local Codex runner.
749
-
750
- Advanced:
751
- 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.
752
1072
 
753
- Connect this computer to Kandan as a local Codex runner.
754
-
755
- Use:
756
- 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>
757
1075
 
758
1076
  For help:
759
1077
  linzumi connect --help
@@ -8,12 +8,22 @@
8
8
  Spec: plans/2026-04-24-local-codex-runner-deep-quality-spec.md
9
9
  Relationship: Carries selected queued message seqs through interrupt fusion
10
10
  so the UI can show which pending replies were accepted by the interrupt.
11
+
12
+ - Date: 2026-05-02
13
+ Spec: plans/2026-05-02-agent-first-zero-to-codex-launch-plan.md
14
+ Relationship: Carries Kandan attachment metadata through npm CLI runner queue
15
+ fusion so attachment-backed replies stay intact before Codex sees them.
11
16
  */
17
+ import { type KandanChatAttachment } from "./channelSessionSupport";
18
+
19
+ export type QueuedKandanAttachment = KandanChatAttachment;
20
+
12
21
  export type QueuedKandanMessage = {
13
22
  readonly seq: number;
14
23
  readonly actorSlug: string | undefined;
15
24
  readonly actorUserId: number | undefined;
16
25
  readonly body: string;
26
+ readonly attachments: readonly QueuedKandanAttachment[];
17
27
  };
18
28
 
19
29
  export type QueueInterruptResult =
@@ -98,5 +108,6 @@ function fuseQueuedMessages(
98
108
  actorSlug: "kandan",
99
109
  actorUserId: undefined,
100
110
  body: selected.map(codexInputForQueuedKandanMessage).join("\n\n---\n\n"),
111
+ attachments: selected.flatMap(message => message.attachments),
101
112
  };
102
113
  }
@@ -7,6 +7,7 @@
7
7
  forwarding is advertised only for explicitly configured local ports.
8
8
  */
9
9
  import { realpathSync } from "node:fs";
10
+ import { homedir } from "node:os";
10
11
  import { isAbsolute, relative, resolve } from "node:path";
11
12
 
12
13
  export type CwdCapabilityDecision =
@@ -66,13 +67,25 @@ export function assertConfiguredAllowedCwds(
66
67
  ): string[] {
67
68
  return paths.map((path) => {
68
69
  try {
69
- return realpathSync(resolve(path));
70
+ return realpathSync(resolve(expandUserPath(path)));
70
71
  } catch (_error) {
71
72
  throw new Error(`invalid --allowed-cwd: ${path} does not exist`);
72
73
  }
73
74
  });
74
75
  }
75
76
 
77
+ export function expandUserPath(pathValue: string): string {
78
+ if (pathValue === "~") {
79
+ return homedir();
80
+ }
81
+
82
+ if (pathValue.startsWith("~/")) {
83
+ return resolve(homedir(), pathValue.slice(2));
84
+ }
85
+
86
+ return pathValue;
87
+ }
88
+
76
89
  export function resolveAllowedCwd(
77
90
  requestedCwd: string | undefined,
78
91
  allowedRoots: readonly string[],