@linzumi/cli 0.0.4-beta → 0.0.6-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
@@ -13,13 +13,42 @@
13
13
  Spec: plans/2026-04-24-local-codex-runner-deep-quality-spec.md
14
14
  Relationship: Keeps default local runner identity collision-resistant for
15
15
  concurrent local starts.
16
+
17
+ - Date: 2026-04-26
18
+ Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
19
+ Relationship: Exposes the live stream flush interval as a runner option so
20
+ local users can tune Kandan persistence batching without changing the Codex
21
+ transcript protocol, and exposes explicit local preview forwarding ports as
22
+ capability metadata without creating an implicit tunnel.
16
23
  */
17
24
  import { randomUUID } from "node:crypto";
25
+ import { homedir } from "node:os";
26
+ import { resolve } from "node:path";
18
27
  import { runLocalCodexRunner, type RunnerOptions } from "./runner";
19
28
  import { writeCachedLocalRunnerToken } from "./authCache";
20
29
  import { resolveLocalRunnerToken } from "./authResolution";
21
30
  import { identityFromAccessToken } from "./channelSessionSupport";
22
- import { acquireLocalRunnerTokenDetails } from "./oauth";
31
+ import {
32
+ assertConfiguredAllowedCwds,
33
+ parseAllowedCwdList,
34
+ parseAllowedPortList,
35
+ } from "./localCapabilities";
36
+ import {
37
+ acquireLocalRunnerTokenDetails,
38
+ fetchLocalRunnerStartTarget,
39
+ type LocalRunnerStartTarget,
40
+ validateLocalRunnerToken,
41
+ } from "./oauth";
42
+ import {
43
+ assertStartDependencies,
44
+ buildRunnerDependencyStatus,
45
+ } from "./dependencyStatus";
46
+ import { resolveEditorRuntime } from "./localEditorRuntime";
47
+ import {
48
+ kandanTlsTrustFromEnv,
49
+ trustedFetch,
50
+ trustedWebSocketFactory,
51
+ } from "./kandanTls";
23
52
 
24
53
  type FlagDefinition = {
25
54
  readonly kind: "value" | "boolean";
@@ -42,6 +71,10 @@ const flagDefinitions = new Map<string, FlagDefinition>([
42
71
  ["reasoning-effort", { kind: "value" }],
43
72
  ["sandbox", { kind: "value" }],
44
73
  ["approval-policy", { kind: "value" }],
74
+ ["stream-flush-ms", { kind: "value" }],
75
+ ["allowed-cwd", { kind: "value" }],
76
+ ["forward-port", { kind: "value" }],
77
+ ["code-server-bin", { kind: "value" }],
45
78
  ["fast", { kind: "boolean" }],
46
79
  ["log-file", { kind: "value" }],
47
80
  ["auth-file", { kind: "value" }],
@@ -53,7 +86,9 @@ if (import.meta.main) {
53
86
  try {
54
87
  await main(process.argv.slice(2));
55
88
  } catch (error) {
56
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
89
+ process.stderr.write(
90
+ `${error instanceof Error ? error.message : String(error)}\n`,
91
+ );
57
92
  process.exit(1);
58
93
  }
59
94
  }
@@ -66,11 +101,16 @@ async function main(args: readonly string[]): Promise<void> {
66
101
  process.stdout.write(connectGuideText());
67
102
  return;
68
103
  case "version":
69
- process.stdout.write("linzumi 0.0.4-beta\n");
104
+ process.stdout.write("linzumi 0.0.6-beta\n");
70
105
  return;
71
106
  case "auth":
72
107
  await runAuthCommand(parsed.args);
73
108
  return;
109
+ case "start": {
110
+ const options = await parseStartRunnerArgs(parsed.args);
111
+ await runLocalCodexRunner(options);
112
+ return;
113
+ }
74
114
  case "run": {
75
115
  const options = await parseRunnerArgs(parsed.args);
76
116
  await runLocalCodexRunner(options);
@@ -83,6 +123,7 @@ type ParsedCommand =
83
123
  | { readonly command: "guide"; readonly args: readonly string[] }
84
124
  | { readonly command: "version"; readonly args: readonly string[] }
85
125
  | { readonly command: "auth"; readonly args: readonly string[] }
126
+ | { readonly command: "start"; readonly args: readonly string[] }
86
127
  | { readonly command: "run"; readonly args: readonly string[] };
87
128
 
88
129
  function parseCommand(args: readonly string[]): ParsedCommand {
@@ -102,6 +143,8 @@ function parseCommand(args: readonly string[]): ParsedCommand {
102
143
  return { command: "run", args: ["--help"] };
103
144
  case "auth":
104
145
  return { command: "auth", args: rest };
146
+ case "start":
147
+ return { command: "start", args: rest };
105
148
  case "run":
106
149
  return { command: "run", args: rest };
107
150
  default:
@@ -132,10 +175,170 @@ async function runAuthCommand(args: readonly string[]): Promise<void> {
132
175
  authFilePath: stringValue(values, "auth-file"),
133
176
  });
134
177
 
135
- process.stdout.write(`Saved Kandan local runner auth for ${cached.kandanBaseUrl}\n`);
178
+ process.stdout.write(
179
+ `Saved Kandan local runner auth for ${cached.kandanBaseUrl}\n`,
180
+ );
181
+ }
182
+
183
+ type StartRunnerDeps = {
184
+ readonly resolveToken: typeof resolveLocalRunnerToken;
185
+ readonly fetchStartTarget: typeof fetchLocalRunnerStartTarget;
186
+ readonly validateToken: typeof validateLocalRunnerToken;
187
+ readonly buildDependencyStatus: typeof buildRunnerDependencyStatus;
188
+ readonly resolveEditorRuntime: typeof resolveEditorRuntime;
189
+ };
190
+
191
+ export async function parseStartRunnerArgs(
192
+ args: readonly string[],
193
+ deps: StartRunnerDeps = {
194
+ resolveToken: resolveLocalRunnerToken,
195
+ fetchStartTarget: fetchLocalRunnerStartTarget,
196
+ validateToken: validateLocalRunnerToken,
197
+ buildDependencyStatus: buildRunnerDependencyStatus,
198
+ resolveEditorRuntime,
199
+ },
200
+ ): Promise<RunnerOptions> {
201
+ const { cwdArg, flagArgs } = splitStartArgs(args);
202
+ const values = strictFlagValues(flagArgs);
203
+
204
+ if (values.get("help") === true) {
205
+ process.stdout.write(startHelpText());
206
+ process.exit(0);
207
+ }
208
+
209
+ rejectStartTargetingFlags(values);
210
+
211
+ const kandanUrl = stringValue(values, "kandan-url") ?? "wss://serve.kandanai.com";
212
+ const requestedCwd = resolveUserPath(cwdArg ?? process.cwd());
213
+ const allowedCwds = assertConfiguredAllowedCwds([requestedCwd]);
214
+ const cwd = allowedCwds[0] ?? requestedCwd;
215
+ const codexBin = stringValue(values, "codex-bin") ?? "codex";
216
+ const customCodeServerBin = stringValue(values, "code-server-bin");
217
+ const initialDependencyStatus = await deps.buildDependencyStatus({
218
+ cwd,
219
+ codexBin,
220
+ codeServerBin: customCodeServerBin,
221
+ });
222
+ assertStartDependencies(initialDependencyStatus);
223
+ const explicitToken = stringValue(values, "token");
224
+ const authFilePath = stringValue(values, "auth-file");
225
+ const callbackHost = stringValue(values, "oauth-callback-host");
226
+ const reportRejectedCachedToken = () => {
227
+ process.stderr.write(
228
+ "Cached Kandan local runner auth was rejected; starting OAuth.\n",
229
+ );
230
+ };
231
+ const token = await deps.resolveToken({
232
+ kandanUrl,
233
+ explicitToken,
234
+ onboarding: "start",
235
+ authFilePath,
236
+ callbackHost,
237
+ reportRejectedCachedToken,
238
+ });
239
+ const target = await deps.fetchStartTarget({ kandanUrl, accessToken: token });
240
+ const tokenMatchesTarget = await deps.validateToken({
241
+ kandanUrl,
242
+ accessToken: token,
243
+ workspaceSlug: target.workspaceSlug,
244
+ channelSlug: target.channelSlug,
245
+ });
246
+ const targetToken = tokenMatchesTarget
247
+ ? token
248
+ : await resolveStartTargetToken({
249
+ kandanUrl,
250
+ explicitToken,
251
+ target,
252
+ authFilePath,
253
+ callbackHost,
254
+ resolveToken: deps.resolveToken,
255
+ reportRejectedCachedToken,
256
+ });
257
+ const editorRuntime = await deps.resolveEditorRuntime({
258
+ kandanUrl,
259
+ token: targetToken,
260
+ customCodeServerBin,
261
+ fetchImpl: trustedFetch(kandanTlsTrustFromEnv()),
262
+ });
263
+ const dependencyStatus = await deps.buildDependencyStatus({
264
+ cwd,
265
+ codexBin,
266
+ codeServerBin: editorRuntime.codeServerBin,
267
+ editorRuntime: editorRuntime.status,
268
+ });
269
+ assertStartDependencies(dependencyStatus);
270
+
271
+ return {
272
+ kandanUrl,
273
+ token: targetToken,
274
+ runnerId: stringValue(values, "runner-id") ?? `runner-${randomUUID()}`,
275
+ cwd,
276
+ codexBin,
277
+ codexUrl: stringValue(values, "codex-url"),
278
+ launchTui: values.get("launch-tui") === true,
279
+ fast: values.get("fast") === true,
280
+ logFile: stringValue(values, "log-file"),
281
+ allowedCwds,
282
+ allowedForwardPorts: parseAllowedPortList(
283
+ stringValue(values, "forward-port"),
284
+ ),
285
+ codeServerBin: editorRuntime.codeServerBin,
286
+ editorRuntime: editorRuntime.runtime,
287
+ socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
288
+ dependencyStatus,
289
+ channelSession: {
290
+ workspaceSlug: target.workspaceSlug,
291
+ channelSlug: target.channelSlug,
292
+ kandanThreadId: stringValue(values, "kandan-thread-id"),
293
+ listenUser: stringValue(values, "listen-user") ?? defaultListenUserFromToken(targetToken),
294
+ model: stringValue(values, "model"),
295
+ reasoningEffort: stringValue(values, "reasoning-effort"),
296
+ sandbox: stringValue(values, "sandbox"),
297
+ approvalPolicy: stringValue(values, "approval-policy"),
298
+ streamFlushMs: positiveIntegerValue(values, "stream-flush-ms"),
299
+ },
300
+ };
301
+ }
302
+
303
+ async function resolveStartTargetToken(args: {
304
+ readonly kandanUrl: string;
305
+ readonly explicitToken?: string | undefined;
306
+ readonly target: LocalRunnerStartTarget;
307
+ readonly authFilePath?: string | undefined;
308
+ readonly callbackHost?: string | undefined;
309
+ readonly resolveToken: typeof resolveLocalRunnerToken;
310
+ readonly reportRejectedCachedToken: () => void;
311
+ }): Promise<string> {
312
+ if (args.explicitToken !== undefined) {
313
+ throw new Error(
314
+ `provided local runner token is not scoped to ${args.target.workspaceSlug}/${args.target.channelSlug}`,
315
+ );
316
+ }
317
+
318
+ return await args.resolveToken({
319
+ kandanUrl: args.kandanUrl,
320
+ workspaceSlug: args.target.workspaceSlug,
321
+ channelSlug: args.target.channelSlug,
322
+ authFilePath: args.authFilePath,
323
+ callbackHost: args.callbackHost,
324
+ reportRejectedCachedToken: args.reportRejectedCachedToken,
325
+ });
136
326
  }
137
327
 
138
- export async function parseRunnerArgs(args: readonly string[]): Promise<RunnerOptions> {
328
+ type RunnerArgsDeps = {
329
+ readonly resolveToken: typeof resolveLocalRunnerToken;
330
+ readonly buildDependencyStatus: typeof buildRunnerDependencyStatus;
331
+ readonly resolveEditorRuntime: typeof resolveEditorRuntime;
332
+ };
333
+
334
+ export async function parseRunnerArgs(
335
+ args: readonly string[],
336
+ deps: RunnerArgsDeps = {
337
+ resolveToken: resolveLocalRunnerToken,
338
+ buildDependencyStatus: buildRunnerDependencyStatus,
339
+ resolveEditorRuntime,
340
+ },
341
+ ): Promise<RunnerOptions> {
139
342
  const values = strictFlagValues(args);
140
343
 
141
344
  if (values.get("help") === true) {
@@ -144,37 +347,65 @@ export async function parseRunnerArgs(args: readonly string[]): Promise<RunnerOp
144
347
  }
145
348
 
146
349
  if (values.get("version") === true) {
147
- process.stdout.write("linzumi 0.0.4-beta\n");
350
+ process.stdout.write("linzumi 0.0.6-beta\n");
148
351
  process.exit(0);
149
352
  }
150
353
 
151
354
  const channelTarget = parseChannelSessionTarget(values);
152
355
  const kandanUrl = required(values, "kandan-url");
356
+ const cwd = stringValue(values, "cwd") ?? process.cwd();
357
+ const codexBin = stringValue(values, "codex-bin") ?? "codex";
358
+ const customCodeServerBin = stringValue(values, "code-server-bin");
153
359
  const explicitToken = stringValue(values, "token");
154
- const token = await resolveLocalRunnerToken({
155
- kandanUrl,
156
- explicitToken,
157
- workspaceSlug: channelTarget?.workspaceSlug,
158
- channelSlug: channelTarget?.channelSlug,
159
- authFilePath: stringValue(values, "auth-file"),
160
- callbackHost: stringValue(values, "oauth-callback-host"),
161
- reportRejectedCachedToken: () => {
162
- process.stderr.write("Cached Kandan local runner auth was rejected; starting OAuth.\n");
163
- },
164
- });
360
+ const token = await deps.resolveToken({
361
+ kandanUrl,
362
+ explicitToken,
363
+ workspaceSlug: channelTarget?.workspaceSlug,
364
+ channelSlug: channelTarget?.channelSlug,
365
+ authFilePath: stringValue(values, "auth-file"),
366
+ callbackHost: stringValue(values, "oauth-callback-host"),
367
+ reportRejectedCachedToken: () => {
368
+ process.stderr.write(
369
+ "Cached Kandan local runner auth was rejected; starting OAuth.\n",
370
+ );
371
+ },
372
+ });
165
373
  const channelSession = parseChannelSession(values, token, channelTarget);
374
+ const editorRuntime = await deps.resolveEditorRuntime({
375
+ kandanUrl,
376
+ token,
377
+ customCodeServerBin,
378
+ fetchImpl: trustedFetch(kandanTlsTrustFromEnv()),
379
+ });
380
+ const dependencyStatus = await deps.buildDependencyStatus({
381
+ cwd,
382
+ codexBin,
383
+ codeServerBin: editorRuntime.codeServerBin,
384
+ editorRuntime: editorRuntime.status,
385
+ });
386
+ assertStartDependencies(dependencyStatus);
166
387
 
167
388
  return {
168
389
  kandanUrl,
169
390
  token,
170
391
  runnerId: stringValue(values, "runner-id") ?? `runner-${randomUUID()}`,
171
- cwd: stringValue(values, "cwd") ?? process.cwd(),
172
- codexBin: stringValue(values, "codex-bin") ?? "codex",
392
+ cwd,
393
+ codexBin,
173
394
  codexUrl: stringValue(values, "codex-url"),
174
395
  launchTui: values.get("launch-tui") === true,
175
396
  fast: values.get("fast") === true,
176
397
  logFile: stringValue(values, "log-file"),
177
- channelSession
398
+ allowedCwds: assertConfiguredAllowedCwds(
399
+ parseAllowedCwdList(stringValue(values, "allowed-cwd")),
400
+ ),
401
+ allowedForwardPorts: parseAllowedPortList(
402
+ stringValue(values, "forward-port"),
403
+ ),
404
+ codeServerBin: editorRuntime.codeServerBin,
405
+ editorRuntime: editorRuntime.runtime,
406
+ socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
407
+ dependencyStatus,
408
+ channelSession,
178
409
  };
179
410
  }
180
411
 
@@ -213,10 +444,78 @@ function strictFlagValues(args: readonly string[]): Map<string, string | true> {
213
444
  return values;
214
445
  }
215
446
 
447
+ function splitStartArgs(args: readonly string[]): {
448
+ readonly cwdArg: string | undefined;
449
+ readonly flagArgs: readonly string[];
450
+ } {
451
+ let cwdArg: string | undefined;
452
+ const flagArgs: string[] = [];
453
+
454
+ for (let index = 0; index < args.length; index += 1) {
455
+ const arg = args[index];
456
+
457
+ if (arg === undefined) {
458
+ continue;
459
+ }
460
+
461
+ if (!arg.startsWith("--")) {
462
+ if (cwdArg !== undefined) {
463
+ throw new Error("linzumi start accepts at most one folder path");
464
+ }
465
+
466
+ cwdArg = arg;
467
+ continue;
468
+ }
469
+
470
+ flagArgs.push(arg);
471
+ const key = arg.slice(2);
472
+ const definition = flagDefinitions.get(key);
473
+
474
+ if (definition?.kind === "value") {
475
+ const next = args[index + 1];
476
+
477
+ if (next !== undefined) {
478
+ flagArgs.push(next);
479
+ index += 1;
480
+ }
481
+ }
482
+ }
483
+
484
+ return { cwdArg, flagArgs };
485
+ }
486
+
487
+ function rejectStartTargetingFlags(values: Map<string, string | true>): void {
488
+ const unsupportedFlags = ["workspace", "channel"].filter((flag) =>
489
+ values.has(flag),
490
+ );
491
+
492
+ if (unsupportedFlags.length > 0) {
493
+ throw new Error(
494
+ `linzumi start chooses its workspace during browser onboarding; remove ${unsupportedFlags
495
+ .map((flag) => `--${flag}`)
496
+ .join(", ")} or use linzumi connect for an explicit workspace/channel.`,
497
+ );
498
+ }
499
+ }
500
+
501
+ function resolveUserPath(pathValue: string): string {
502
+ if (pathValue === "~") {
503
+ return homedir();
504
+ }
505
+
506
+ if (pathValue.startsWith("~/")) {
507
+ return resolve(homedir(), pathValue.slice(2));
508
+ }
509
+
510
+ return resolve(pathValue);
511
+ }
512
+
216
513
  function parseChannelSession(
217
514
  values: Map<string, string | true>,
218
515
  token: string,
219
- target: { readonly workspaceSlug: string; readonly channelSlug: string } | undefined,
516
+ target:
517
+ | { readonly workspaceSlug: string; readonly channelSlug: string }
518
+ | undefined,
220
519
  ): RunnerOptions["channelSession"] {
221
520
  if (target === undefined) {
222
521
  return undefined;
@@ -234,12 +533,15 @@ function parseChannelSession(
234
533
  reasoningEffort: stringValue(values, "reasoning-effort"),
235
534
  sandbox: stringValue(values, "sandbox"),
236
535
  approvalPolicy: stringValue(values, "approval-policy"),
536
+ streamFlushMs: positiveIntegerValue(values, "stream-flush-ms"),
237
537
  };
238
538
  }
239
539
 
240
540
  function parseChannelSessionTarget(
241
- values: Map<string, string | true>
242
- ): { readonly workspaceSlug: string; readonly channelSlug: string } | undefined {
541
+ values: Map<string, string | true>,
542
+ ):
543
+ | { readonly workspaceSlug: string; readonly channelSlug: string }
544
+ | undefined {
243
545
  return parseOptionalChannelTarget(values);
244
546
  }
245
547
 
@@ -250,12 +552,16 @@ function defaultListenUserFromToken(token: string): string {
250
552
  return username;
251
553
  }
252
554
 
253
- throw new Error("missing --listen-user and authenticated user is unavailable");
555
+ throw new Error(
556
+ "missing --listen-user and authenticated user is unavailable",
557
+ );
254
558
  }
255
559
 
256
560
  function parseOptionalChannelTarget(
257
- values: Map<string, string | true>
258
- ): { readonly workspaceSlug: string; readonly channelSlug: string } | undefined {
561
+ values: Map<string, string | true>,
562
+ ):
563
+ | { readonly workspaceSlug: string; readonly channelSlug: string }
564
+ | undefined {
259
565
  const channel = stringValue(values, "channel");
260
566
  const workspace = stringValue(values, "workspace");
261
567
 
@@ -267,7 +573,7 @@ function parseOptionalChannelTarget(
267
573
  ? parseChannelPath(channel)
268
574
  : {
269
575
  workspaceSlug: workspace ?? required(values, "workspace"),
270
- channelSlug: channel ?? required(values, "channel")
576
+ channelSlug: channel ?? required(values, "channel"),
271
577
  };
272
578
  }
273
579
 
@@ -289,7 +595,7 @@ function parseChannelPath(channel: string): {
289
595
 
290
596
  return {
291
597
  workspaceSlug: workspaceSlug.trim(),
292
- channelSlug: channelSlug.trim()
598
+ channelSlug: channelSlug.trim(),
293
599
  };
294
600
  }
295
601
 
@@ -303,7 +609,10 @@ function required(values: Map<string, string | true>, key: string): string {
303
609
  return value;
304
610
  }
305
611
 
306
- function stringValue(values: Map<string, string | true>, key: string): string | undefined {
612
+ function stringValue(
613
+ values: Map<string, string | true>,
614
+ key: string,
615
+ ): string | undefined {
307
616
  const value = values.get(key);
308
617
 
309
618
  if (typeof value === "string" && value.trim() !== "") {
@@ -313,11 +622,31 @@ function stringValue(values: Map<string, string | true>, key: string): string |
313
622
  return undefined;
314
623
  }
315
624
 
625
+ function positiveIntegerValue(
626
+ values: Map<string, string | true>,
627
+ key: string,
628
+ ): number | undefined {
629
+ const value = stringValue(values, key);
630
+
631
+ if (value === undefined) {
632
+ return undefined;
633
+ }
634
+
635
+ const parsed = Number(value);
636
+
637
+ if (Number.isInteger(parsed) && parsed > 0) {
638
+ return parsed;
639
+ }
640
+
641
+ throw new Error(`--${key} must be a positive integer`);
642
+ }
643
+
316
644
  function helpText(): string {
317
645
  return `Kandan local Codex runner
318
646
 
319
647
  Usage:
320
648
  linzumi
649
+ linzumi start <folder> [options]
321
650
  linzumi connect --kandan-url <ws-url> --workspace <slug> --channel <slug> [options]
322
651
  linzumi auth --kandan-url <ws-url> [--workspace <slug> --channel <slug>]
323
652
 
@@ -342,11 +671,17 @@ Codex:
342
671
  --reasoning-effort <value> Reasoning effort requested for Codex and shown in Kandan
343
672
  --sandbox <value> Sandbox metadata shown in Kandan
344
673
  --approval-policy <value> Approval-policy metadata shown in Kandan
674
+ --stream-flush-ms <ms> Batch live Codex deltas before Kandan persistence, default 150
345
675
  --fast Mark this runner as low-latency/fast in the availability message
346
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.
347
680
 
348
681
  Examples:
349
682
  Good:
683
+ linzumi start ~/
684
+ linzumi start ~/code/my-app --kandan-url wss://serve.kandanai.com
350
685
  linzumi connect --kandan-url wss://serve.kandanai.com --workspace linzumi --channel seans-playground --codex-bin codex --launch-tui
351
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
352
687
  linzumi auth --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground
@@ -354,17 +689,66 @@ Examples:
354
689
  linzumi connect --kandan-url ws://127.0.0.1:4160 --token "$TOKEN" --workspace default --channel seans-playground --cwd /tmp/kandan-runner-a
355
690
  linzumi connect --kandan-url ws://127.0.0.1:4160 --workspace default --channel seans-playground
356
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
357
695
 
358
696
  Bad:
359
697
  linzumi connect --kandan-url ws://127.0.0.1:4160 --token not-a-jwt --workspace default --channel seans-playground
360
698
  Missing --listen-user and authenticated user is unavailable.
361
699
  linzumi connect --kandan-url ws://127.0.0.1:4160 --token "$TOKEN" --listen-users sean
362
700
  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
702
+ 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
704
+ Invalid --forward-port: value must be a TCP port from 1 to 65535.
705
+ `;
706
+ }
707
+
708
+ function startHelpText(): string {
709
+ return `Linzumi one-command local runner
710
+
711
+ Usage:
712
+ linzumi start <folder> [options]
713
+
714
+ 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.
718
+
719
+ Options:
720
+ --kandan-url <ws-url> Kandan websocket base URL, default wss://serve.kandanai.com
721
+ --token <jwt> Optional scoped local-runner token override
722
+ --auth-file <path> Auth cache path, default ~/.kandan/auth.json
723
+ --oauth-callback-host <ip> Callback host reachable by your browser
724
+ --runner-id <id> Stable local runner id
725
+ --codex-bin <path> Codex executable, default codex
726
+ --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
728
+ --model <name> Model requested for Codex sessions
729
+ --reasoning-effort <value> Reasoning effort requested for Codex sessions
730
+ --sandbox <value> Sandbox metadata shown in Kandan
731
+ --approval-policy <value> Approval-policy metadata shown in Kandan
732
+ --forward-port <ports> Comma-separated local TCP ports Kandan may expose as previews
733
+ --fast Mark this runner as low-latency/fast in Kandan
734
+
735
+ 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
363
738
  `;
364
739
  }
365
740
 
366
741
  function connectGuideText(): string {
367
- return `linzumi connect
742
+ return `linzumi start
743
+
744
+ Fastest path:
745
+ linzumi start ~/
746
+
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
368
752
 
369
753
  Connect this computer to Kandan as a local Codex runner.
370
754
 
@@ -0,0 +1,86 @@
1
+ /*
2
+ - Date: 2026-04-29
3
+ Spec: kandan/server_v2/plans/2026-04-29-local-runner-editor-runtime-distribution-note.md
4
+ Relationship: Lets local proof and development runners trust a specific
5
+ Kandan CA certificate without disabling TLS verification globally.
6
+ */
7
+ import { existsSync } from "node:fs";
8
+
9
+ type BunRuntime = {
10
+ readonly file: (path: string) => unknown;
11
+ };
12
+
13
+ type TlsFetchInit = RequestInit & {
14
+ readonly tls?: {
15
+ readonly ca: unknown;
16
+ };
17
+ };
18
+
19
+ type TlsWebSocketOptions = {
20
+ readonly tls?: {
21
+ readonly ca: unknown;
22
+ };
23
+ };
24
+
25
+ export type KandanTlsTrust = {
26
+ readonly caFile: string;
27
+ readonly ca: unknown;
28
+ };
29
+
30
+ export function kandanTlsTrustFromEnv(): KandanTlsTrust | undefined {
31
+ return kandanTlsTrustFromCaFile(process.env.KANDAN_TLS_CA_FILE);
32
+ }
33
+
34
+ export function kandanTlsTrustFromCaFile(caFile: string | undefined): KandanTlsTrust | undefined {
35
+ if (caFile === undefined || caFile.trim() === "") {
36
+ return undefined;
37
+ }
38
+
39
+ const trimmed = caFile.trim();
40
+ if (!existsSync(trimmed)) {
41
+ throw new Error(`KANDAN_TLS_CA_FILE does not exist: ${trimmed}`);
42
+ }
43
+
44
+ const bun = (globalThis as unknown as { readonly Bun?: BunRuntime }).Bun;
45
+ if (bun === undefined) {
46
+ throw new Error("KANDAN_TLS_CA_FILE requires the Bun runtime");
47
+ }
48
+
49
+ return {
50
+ caFile: trimmed,
51
+ ca: bun.file(trimmed),
52
+ };
53
+ }
54
+
55
+ export function trustedFetch(
56
+ trust: KandanTlsTrust | undefined,
57
+ ): typeof fetch {
58
+ if (trust === undefined) {
59
+ return fetch;
60
+ }
61
+
62
+ return ((input: URL | RequestInfo, init?: RequestInit) => {
63
+ const request = {
64
+ ...(init ?? {}),
65
+ tls: {
66
+ ca: trust.ca,
67
+ },
68
+ } satisfies TlsFetchInit;
69
+ return fetch(input, request);
70
+ }) as typeof fetch;
71
+ }
72
+
73
+ export function trustedWebSocketFactory(
74
+ trust: KandanTlsTrust | undefined,
75
+ ): ((url: string) => WebSocket) | undefined {
76
+ if (trust === undefined) {
77
+ return undefined;
78
+ }
79
+
80
+ return (url) =>
81
+ new WebSocket(url, {
82
+ tls: {
83
+ ca: trust.ca,
84
+ },
85
+ } as TlsWebSocketOptions);
86
+ }