@linzumi/cli 0.0.20-beta → 0.0.22-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 DELETED
@@ -1,1080 +0,0 @@
1
- /*
2
- - Date: 2026-04-24
3
- Spec: plans/2026-04-24-local-codex-runner-plan.md
4
- Relationship: Provides the spec's initial Bun CLI entry point for starting
5
- a customer-machine local runner with Kandan and Codex connection options.
6
-
7
- - Date: 2026-04-24
8
- Spec: plans/2026-04-24-local-codex-channel-thread-binding-spec.md
9
- Relationship: Parses the channel-bound runner options that let a local Codex
10
- process bind to one Kandan channel/thread and filter accepted senders.
11
-
12
- - Date: 2026-04-24
13
- Spec: plans/2026-04-24-local-codex-runner-deep-quality-spec.md
14
- Relationship: Keeps default local runner identity collision-resistant for
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.
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.
28
- */
29
- import { randomUUID } from "node:crypto";
30
- import { existsSync, readFileSync, realpathSync } from "node:fs";
31
- import { homedir } from "node:os";
32
- import { resolve } from "node:path";
33
- import { runLocalCodexRunner, type RunnerOptions } from "./runner";
34
- import { writeCachedLocalRunnerToken } from "./authCache";
35
- import { resolveLocalRunnerToken } from "./authResolution";
36
- import { identityFromAccessToken } from "./channelSessionSupport";
37
- import {
38
- assertConfiguredAllowedCwds,
39
- expandUserPath,
40
- parseAllowedCwdList,
41
- parseAllowedPortList,
42
- } from "./localCapabilities";
43
- import {
44
- addAllowedCwd,
45
- localConfigPath,
46
- readConfiguredAllowedCwds,
47
- readLocalConfig,
48
- removeAllowedCwd,
49
- } from "./localConfig";
50
- import {
51
- acquireLocalRunnerTokenDetails,
52
- fetchLocalRunnerStartTarget,
53
- type LocalRunnerStartTarget,
54
- validateLocalRunnerToken,
55
- } from "./oauth";
56
- import {
57
- assertStartDependencies,
58
- buildRunnerDependencyStatus,
59
- } from "./dependencyStatus";
60
- import { resolveEditorRuntime } from "./localEditorRuntime";
61
- import {
62
- kandanTlsTrustFromEnv,
63
- trustedFetch,
64
- trustedWebSocketFactory,
65
- } from "./kandanTls";
66
- import {
67
- defaultAgentTokenFilePath,
68
- readStoredAgentTokenFile,
69
- runAgentCliCommand,
70
- } from "./agentBootstrap";
71
-
72
- type FlagDefinition = {
73
- readonly kind: "value" | "boolean";
74
- };
75
-
76
- const flagDefinitions = new Map<string, FlagDefinition>([
77
- ["version", { kind: "boolean" }],
78
- ["kandan-url", { kind: "value" }],
79
- ["token", { kind: "value" }],
80
- ["runner-id", { kind: "value" }],
81
- ["cwd", { kind: "value" }],
82
- ["codex-bin", { kind: "value" }],
83
- ["codex-url", { kind: "value" }],
84
- ["launch-tui", { kind: "boolean" }],
85
- ["workspace", { kind: "value" }],
86
- ["channel", { kind: "value" }],
87
- ["kandan-thread-id", { kind: "value" }],
88
- ["listen-user", { kind: "value" }],
89
- ["model", { kind: "value" }],
90
- ["reasoning-effort", { kind: "value" }],
91
- ["sandbox", { kind: "value" }],
92
- ["approval-policy", { kind: "value" }],
93
- ["stream-flush-ms", { kind: "value" }],
94
- ["allowed-cwd", { kind: "value" }],
95
- ["forward-port", { kind: "value" }],
96
- ["code-server-bin", { kind: "value" }],
97
- ["fast", { kind: "boolean" }],
98
- ["log-file", { kind: "value" }],
99
- ["auth-file", { kind: "value" }],
100
- ["agent-token-file", { kind: "value" }],
101
- ["oauth-callback-host", { kind: "value" }],
102
- ["help", { kind: "boolean" }],
103
- ]);
104
-
105
- if (import.meta.main) {
106
- try {
107
- await main(process.argv.slice(2));
108
- } catch (error) {
109
- process.stderr.write(
110
- `${error instanceof Error ? error.message : String(error)}\n`,
111
- );
112
- process.exit(1);
113
- }
114
- }
115
-
116
- async function main(args: readonly string[]): Promise<void> {
117
- const parsed = parseCommand(args);
118
-
119
- switch (parsed.command) {
120
- case "guide":
121
- process.stdout.write(connectGuideText());
122
- return;
123
- case "version":
124
- process.stdout.write("linzumi 0.0.20-beta\n");
125
- return;
126
- case "auth":
127
- await runAuthCommand(parsed.args);
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
- }
140
- case "start": {
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);
146
- await runLocalCodexRunner(options);
147
- return;
148
- }
149
- case "run": {
150
- const options = await parseRunnerArgs(parsed.args);
151
- await runLocalCodexRunner(options);
152
- return;
153
- }
154
- }
155
- }
156
-
157
- type ParsedCommand =
158
- | { readonly command: "guide"; readonly args: readonly string[] }
159
- | { readonly command: "version"; readonly args: readonly string[] }
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[] }
164
- | { readonly command: "start"; readonly args: readonly string[] }
165
- | { readonly command: "run"; readonly args: readonly string[] };
166
-
167
- function parseCommand(args: readonly string[]): ParsedCommand {
168
- const [first, ...rest] = args;
169
-
170
- switch (first) {
171
- case undefined:
172
- case "connect":
173
- case "local-codex-runner":
174
- return rest.length === 0
175
- ? { command: "guide", args: [] }
176
- : { command: "run", args: rest };
177
- case "--version":
178
- return { command: "version", args: [] };
179
- case "--help":
180
- case "-h":
181
- return { command: "run", args: ["--help"] };
182
- case "auth":
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 "channel":
196
- case "post":
197
- case "inbox":
198
- case "done":
199
- case "codex":
200
- case "editor":
201
- return { command: "agent", args };
202
- case "start":
203
- return { command: "start", args: rest };
204
- case "run":
205
- return { command: "run", args: rest };
206
- default:
207
- return { command: "run", args };
208
- }
209
- }
210
-
211
- function runPathsCommand(args: readonly string[]): void {
212
- const [subcommand, pathValue, ...rest] = args;
213
-
214
- if (subcommand === undefined || subcommand === "help" || subcommand === "--help") {
215
- process.stdout.write(pathsHelpText());
216
- return;
217
- }
218
-
219
- if (rest.length > 0) {
220
- throw new Error("linzumi paths accepts one path argument");
221
- }
222
-
223
- switch (subcommand) {
224
- case "list": {
225
- const config = readLocalConfig();
226
- if (config.allowedCwds.length === 0) {
227
- process.stdout.write(`No trusted paths configured in ${localConfigPath()}\n`);
228
- return;
229
- }
230
-
231
- process.stdout.write(`${config.allowedCwds.join("\n")}\n`);
232
- return;
233
- }
234
- case "add": {
235
- if (pathValue === undefined || pathValue.trim() === "") {
236
- throw new Error("missing path for linzumi paths add");
237
- }
238
-
239
- const trustedPath = realpathSync(resolve(expandUserPath(pathValue)));
240
- addAllowedCwd(pathValue);
241
- process.stdout.write(`Trusted ${trustedPath}\n`);
242
- return;
243
- }
244
- case "remove": {
245
- if (pathValue === undefined || pathValue.trim() === "") {
246
- throw new Error("missing path for linzumi paths remove");
247
- }
248
-
249
- removeAllowedCwd(pathValue);
250
- process.stdout.write(`Removed trusted path ${pathValue}\n`);
251
- return;
252
- }
253
- default:
254
- throw new Error(`invalid paths command: ${subcommand}`);
255
- }
256
- }
257
-
258
- async function runAuthCommand(args: readonly string[]): Promise<void> {
259
- const values = strictFlagValues(args);
260
-
261
- if (values.get("help") === true) {
262
- process.stdout.write(helpText());
263
- return;
264
- }
265
-
266
- const kandanUrl = required(values, "kandan-url");
267
- const target = parseOptionalChannelTarget(values);
268
- const token = await acquireLocalRunnerTokenDetails({
269
- kandanUrl,
270
- workspaceSlug: target?.workspaceSlug,
271
- channelSlug: target?.channelSlug,
272
- callbackHost: stringValue(values, "oauth-callback-host"),
273
- });
274
- const cached = writeCachedLocalRunnerToken({
275
- kandanUrl,
276
- accessToken: token.accessToken,
277
- expiresInSeconds: token.expiresInSeconds,
278
- authFilePath: stringValue(values, "auth-file"),
279
- });
280
-
281
- process.stdout.write(
282
- `Saved Kandan local runner auth for ${cached.kandanBaseUrl}\n`,
283
- );
284
- }
285
-
286
- type StartRunnerDeps = {
287
- readonly resolveToken: typeof resolveLocalRunnerToken;
288
- readonly fetchStartTarget: typeof fetchLocalRunnerStartTarget;
289
- readonly validateToken: typeof validateLocalRunnerToken;
290
- readonly buildDependencyStatus: typeof buildRunnerDependencyStatus;
291
- readonly resolveEditorRuntime: typeof resolveEditorRuntime;
292
- };
293
-
294
- export async function parseStartRunnerArgs(
295
- args: readonly string[],
296
- deps: StartRunnerDeps = {
297
- resolveToken: resolveLocalRunnerToken,
298
- fetchStartTarget: fetchLocalRunnerStartTarget,
299
- validateToken: validateLocalRunnerToken,
300
- buildDependencyStatus: buildRunnerDependencyStatus,
301
- resolveEditorRuntime,
302
- },
303
- ): Promise<RunnerOptions> {
304
- const { cwdArg, flagArgs } = splitStartArgs(args);
305
- const values = strictFlagValues(flagArgs);
306
-
307
- if (values.get("help") === true) {
308
- process.stdout.write(startHelpText());
309
- process.exit(0);
310
- }
311
-
312
- rejectStartTargetingFlags(values);
313
-
314
- const kandanUrl = stringValue(values, "kandan-url") ?? "wss://serve.linzumi.com";
315
- const requestedCwd = resolveUserPath(cwdArg ?? process.cwd());
316
- const cwd = assertConfiguredAllowedCwds([requestedCwd])[0] ?? requestedCwd;
317
- const explicitAllowedCwds = values.has("allowed-cwd")
318
- ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue(values, "allowed-cwd")))
319
- : [];
320
- const allowedCwds = Array.from(new Set([cwd, ...explicitAllowedCwds]));
321
- const codexBin = stringValue(values, "codex-bin") ?? "codex";
322
- const customCodeServerBin = stringValue(values, "code-server-bin");
323
- const initialDependencyStatus = await deps.buildDependencyStatus({
324
- cwd,
325
- codexBin,
326
- codeServerBin: customCodeServerBin,
327
- });
328
- assertStartDependencies(initialDependencyStatus);
329
- const explicitToken = stringValue(values, "token");
330
- const authFilePath = stringValue(values, "auth-file");
331
- const callbackHost = stringValue(values, "oauth-callback-host");
332
- const reportRejectedCachedToken = () => {
333
- process.stderr.write(
334
- "Cached Kandan local runner auth was rejected; starting OAuth.\n",
335
- );
336
- };
337
- const token = await deps.resolveToken({
338
- kandanUrl,
339
- explicitToken,
340
- onboarding: "start",
341
- authFilePath,
342
- callbackHost,
343
- reportRejectedCachedToken,
344
- });
345
- const target = await deps.fetchStartTarget({ kandanUrl, accessToken: token });
346
- const tokenMatchesTarget = await deps.validateToken({
347
- kandanUrl,
348
- accessToken: token,
349
- workspaceSlug: target.workspaceSlug,
350
- channelSlug: target.channelSlug,
351
- });
352
- const targetToken = tokenMatchesTarget
353
- ? token
354
- : await resolveStartTargetToken({
355
- kandanUrl,
356
- explicitToken,
357
- target,
358
- authFilePath,
359
- callbackHost,
360
- resolveToken: deps.resolveToken,
361
- reportRejectedCachedToken,
362
- });
363
- const editorRuntime = await deps.resolveEditorRuntime({
364
- kandanUrl,
365
- token: targetToken,
366
- customCodeServerBin,
367
- fetchImpl: trustedFetch(kandanTlsTrustFromEnv()),
368
- });
369
- const dependencyStatus = await deps.buildDependencyStatus({
370
- cwd,
371
- codexBin,
372
- codeServerBin: editorRuntime.codeServerBin,
373
- editorRuntime: editorRuntime.status,
374
- });
375
- assertStartDependencies(dependencyStatus);
376
-
377
- return {
378
- kandanUrl,
379
- token: targetToken,
380
- runnerId: stringValue(values, "runner-id") ?? `runner-${randomUUID()}`,
381
- cwd,
382
- codexBin,
383
- codexUrl: stringValue(values, "codex-url"),
384
- launchTui: values.get("launch-tui") === true,
385
- fast: values.get("fast") === true,
386
- logFile: stringValue(values, "log-file"),
387
- allowedCwds,
388
- allowedForwardPorts: parseAllowedPortList(
389
- stringValue(values, "forward-port"),
390
- ),
391
- codeServerBin: editorRuntime.codeServerBin,
392
- editorRuntime: editorRuntime.runtime,
393
- socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
394
- dependencyStatus,
395
- channelSession: {
396
- workspaceSlug: target.workspaceSlug,
397
- channelSlug: target.channelSlug,
398
- kandanThreadId: stringValue(values, "kandan-thread-id"),
399
- listenUser: stringValue(values, "listen-user") ?? defaultListenUserFromToken(targetToken),
400
- model: stringValue(values, "model"),
401
- reasoningEffort: stringValue(values, "reasoning-effort"),
402
- sandbox: stringValue(values, "sandbox"),
403
- approvalPolicy: stringValue(values, "approval-policy"),
404
- streamFlushMs: positiveIntegerValue(values, "stream-flush-ms"),
405
- },
406
- };
407
- }
408
-
409
- type AgentRunnerDeps = {
410
- readonly readTextFile: (path: string) => string | undefined;
411
- readonly buildDependencyStatus: typeof buildRunnerDependencyStatus;
412
- readonly resolveEditorRuntime: typeof resolveEditorRuntime;
413
- };
414
-
415
- export async function parseAgentRunnerArgs(
416
- args: readonly string[],
417
- deps: AgentRunnerDeps = {
418
- readTextFile: readAgentTokenTextFile,
419
- buildDependencyStatus: buildRunnerDependencyStatus,
420
- resolveEditorRuntime,
421
- },
422
- ): Promise<RunnerOptions> {
423
- const { cwdArg, flagArgs } = splitStartArgs(args);
424
- const values = strictFlagValues(flagArgs);
425
-
426
- if (values.get("help") === true) {
427
- process.stdout.write(agentRunnerHelpText());
428
- process.exit(0);
429
- }
430
-
431
- rejectAgentRunnerTargetingFlags(values);
432
-
433
- if (cwdArg !== undefined && values.has("cwd")) {
434
- throw new Error("linzumi agent runner accepts either <folder> or --cwd, not both");
435
- }
436
-
437
- const tokenFilePath = stringValue(values, "agent-token-file") ?? defaultAgentTokenFilePath();
438
- const tokenFile = readStoredAgentTokenFile(tokenFilePath, deps.readTextFile);
439
- const channelSlug = requiredStoredAgentChannel(tokenFile.channelId);
440
- const listenUser =
441
- stringValue(values, "listen-user") ?? requiredStoredOwnerUsername(tokenFile.ownerUsername);
442
- const kandanUrl = stringValue(values, "kandan-url") ?? agentApiUrlToKandanUrl(tokenFile.apiUrl);
443
- const requestedCwd = resolveUserPath(
444
- cwdArg ?? stringValue(values, "cwd") ?? process.cwd(),
445
- );
446
- const allowedCwds = values.has("allowed-cwd")
447
- ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue(values, "allowed-cwd")))
448
- : assertConfiguredAllowedCwds([requestedCwd]);
449
- const cwd = allowedCwds[0] ?? requestedCwd;
450
- const codexBin = stringValue(values, "codex-bin") ?? "codex";
451
- const customCodeServerBin = stringValue(values, "code-server-bin");
452
- const initialDependencyStatus = await deps.buildDependencyStatus({
453
- cwd,
454
- codexBin,
455
- codeServerBin: customCodeServerBin,
456
- });
457
- assertStartDependencies(initialDependencyStatus);
458
- const editorRuntime = await deps.resolveEditorRuntime({
459
- kandanUrl,
460
- token: tokenFile.agentToken,
461
- customCodeServerBin,
462
- fetchImpl: trustedFetch(kandanTlsTrustFromEnv()),
463
- });
464
- const dependencyStatus = await deps.buildDependencyStatus({
465
- cwd,
466
- codexBin,
467
- codeServerBin: editorRuntime.codeServerBin,
468
- editorRuntime: editorRuntime.status,
469
- });
470
- assertStartDependencies(dependencyStatus);
471
-
472
- return {
473
- kandanUrl,
474
- token: tokenFile.agentToken,
475
- runnerId: stringValue(values, "runner-id") ?? `agent-runner-${randomUUID()}`,
476
- cwd,
477
- codexBin,
478
- codexUrl: stringValue(values, "codex-url"),
479
- launchTui: values.get("launch-tui") === true,
480
- fast: values.get("fast") === true,
481
- logFile: stringValue(values, "log-file"),
482
- allowedCwds,
483
- allowedForwardPorts: parseAllowedPortList(
484
- stringValue(values, "forward-port"),
485
- ),
486
- codeServerBin: editorRuntime.codeServerBin,
487
- editorRuntime: editorRuntime.runtime,
488
- socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
489
- dependencyStatus,
490
- channelSession: {
491
- workspaceSlug: tokenFile.workspaceId,
492
- channelSlug,
493
- kandanThreadId: stringValue(values, "kandan-thread-id"),
494
- listenUser,
495
- model: stringValue(values, "model"),
496
- reasoningEffort: stringValue(values, "reasoning-effort"),
497
- sandbox: stringValue(values, "sandbox"),
498
- approvalPolicy: stringValue(values, "approval-policy"),
499
- streamFlushMs: positiveIntegerValue(values, "stream-flush-ms"),
500
- },
501
- };
502
- }
503
-
504
- function readAgentTokenTextFile(path: string): string | undefined {
505
- return existsSync(path) ? readFileSync(path, "utf8") : undefined;
506
- }
507
-
508
- function rejectAgentRunnerTargetingFlags(values: Map<string, string | true>): void {
509
- const unsupportedFlags = ["workspace", "channel", "token", "auth-file", "oauth-callback-host"]
510
- .filter((flag) => values.has(flag));
511
-
512
- if (unsupportedFlags.length > 0) {
513
- throw new Error(
514
- `linzumi agent runner uses the claimed agent token scope; remove ${unsupportedFlags
515
- .map((flag) => `--${flag}`)
516
- .join(", ")}.`,
517
- );
518
- }
519
- }
520
-
521
- function requiredStoredAgentChannel(channelId: string | undefined): string {
522
- if (channelId !== undefined) {
523
- return channelId;
524
- }
525
-
526
- throw new Error(
527
- "agent token file is missing channelId; rerun linzumi claim before starting an agent runner",
528
- );
529
- }
530
-
531
- function requiredStoredOwnerUsername(ownerUsername: string | undefined): string {
532
- if (ownerUsername !== undefined) {
533
- return ownerUsername;
534
- }
535
-
536
- throw new Error(
537
- "agent token file is missing ownerUsername; rerun linzumi claim or pass --listen-user explicitly",
538
- );
539
- }
540
-
541
- function agentApiUrlToKandanUrl(apiUrl: string): string {
542
- const url = parseAgentApiUrl(apiUrl);
543
-
544
- switch (url.protocol) {
545
- case "https:":
546
- url.protocol = "wss:";
547
- return trimTrailingSlash(url.toString());
548
- case "http:":
549
- url.protocol = "ws:";
550
- return trimTrailingSlash(url.toString());
551
- case "wss:":
552
- case "ws:":
553
- return trimTrailingSlash(url.toString());
554
- default:
555
- throw new Error("agent token file apiUrl must use http, https, ws, or wss");
556
- }
557
- }
558
-
559
- function parseAgentApiUrl(apiUrl: string): URL {
560
- try {
561
- return new URL(apiUrl);
562
- } catch (_error) {
563
- throw new Error("agent token file apiUrl is not a valid URL");
564
- }
565
- }
566
-
567
- function trimTrailingSlash(value: string): string {
568
- return value.endsWith("/") ? value.slice(0, -1) : value;
569
- }
570
-
571
- async function resolveStartTargetToken(args: {
572
- readonly kandanUrl: string;
573
- readonly explicitToken?: string | undefined;
574
- readonly target: LocalRunnerStartTarget;
575
- readonly authFilePath?: string | undefined;
576
- readonly callbackHost?: string | undefined;
577
- readonly resolveToken: typeof resolveLocalRunnerToken;
578
- readonly reportRejectedCachedToken: () => void;
579
- }): Promise<string> {
580
- if (args.explicitToken !== undefined) {
581
- throw new Error(
582
- `provided local runner token is not scoped to ${args.target.workspaceSlug}/${args.target.channelSlug}`,
583
- );
584
- }
585
-
586
- return await args.resolveToken({
587
- kandanUrl: args.kandanUrl,
588
- workspaceSlug: args.target.workspaceSlug,
589
- channelSlug: args.target.channelSlug,
590
- authFilePath: args.authFilePath,
591
- callbackHost: args.callbackHost,
592
- reportRejectedCachedToken: args.reportRejectedCachedToken,
593
- });
594
- }
595
-
596
- type RunnerArgsDeps = {
597
- readonly resolveToken: typeof resolveLocalRunnerToken;
598
- readonly buildDependencyStatus: typeof buildRunnerDependencyStatus;
599
- readonly resolveEditorRuntime: typeof resolveEditorRuntime;
600
- };
601
-
602
- export async function parseRunnerArgs(
603
- args: readonly string[],
604
- deps: RunnerArgsDeps = {
605
- resolveToken: resolveLocalRunnerToken,
606
- buildDependencyStatus: buildRunnerDependencyStatus,
607
- resolveEditorRuntime,
608
- },
609
- ): Promise<RunnerOptions> {
610
- const values = strictFlagValues(args);
611
-
612
- if (values.get("help") === true) {
613
- process.stdout.write(helpText());
614
- process.exit(0);
615
- }
616
-
617
- if (values.get("version") === true) {
618
- process.stdout.write("linzumi 0.0.20-beta\n");
619
- process.exit(0);
620
- }
621
-
622
- const channelTarget = parseChannelSessionTarget(values);
623
- const kandanUrl = required(values, "kandan-url");
624
- const cwd = stringValue(values, "cwd") ?? process.cwd();
625
- const codexBin = stringValue(values, "codex-bin") ?? "codex";
626
- const customCodeServerBin = stringValue(values, "code-server-bin");
627
- const explicitToken = stringValue(values, "token");
628
- const token = await deps.resolveToken({
629
- kandanUrl,
630
- explicitToken,
631
- workspaceSlug: channelTarget?.workspaceSlug,
632
- channelSlug: channelTarget?.channelSlug,
633
- authFilePath: stringValue(values, "auth-file"),
634
- callbackHost: stringValue(values, "oauth-callback-host"),
635
- reportRejectedCachedToken: () => {
636
- process.stderr.write(
637
- "Cached Kandan local runner auth was rejected; starting OAuth.\n",
638
- );
639
- },
640
- });
641
- const channelSession = parseChannelSession(values, token, channelTarget);
642
- const editorRuntime = await deps.resolveEditorRuntime({
643
- kandanUrl,
644
- token,
645
- customCodeServerBin,
646
- fetchImpl: trustedFetch(kandanTlsTrustFromEnv()),
647
- });
648
- const dependencyStatus = await deps.buildDependencyStatus({
649
- cwd,
650
- codexBin,
651
- codeServerBin: editorRuntime.codeServerBin,
652
- editorRuntime: editorRuntime.status,
653
- });
654
- assertStartDependencies(dependencyStatus);
655
-
656
- return {
657
- kandanUrl,
658
- token,
659
- runnerId: stringValue(values, "runner-id") ?? `runner-${randomUUID()}`,
660
- cwd,
661
- codexBin,
662
- codexUrl: stringValue(values, "codex-url"),
663
- launchTui: values.get("launch-tui") === true,
664
- fast: values.get("fast") === true,
665
- logFile: stringValue(values, "log-file"),
666
- allowedCwds: values.has("allowed-cwd")
667
- ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue(values, "allowed-cwd")))
668
- : readConfiguredAllowedCwds(),
669
- allowedForwardPorts: parseAllowedPortList(
670
- stringValue(values, "forward-port"),
671
- ),
672
- codeServerBin: editorRuntime.codeServerBin,
673
- editorRuntime: editorRuntime.runtime,
674
- socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
675
- dependencyStatus,
676
- channelSession,
677
- };
678
- }
679
-
680
- function strictFlagValues(args: readonly string[]): Map<string, string | true> {
681
- const values = new Map<string, string | true>();
682
-
683
- for (let index = 0; index < args.length; index += 1) {
684
- const arg = args[index];
685
-
686
- if (arg === undefined || !arg.startsWith("--")) {
687
- throw new Error(`invalid argument: ${arg ?? ""}`);
688
- }
689
-
690
- const key = arg.slice(2);
691
- const definition = flagDefinitions.get(key);
692
-
693
- if (definition === undefined) {
694
- throw new Error(`invalid flag: --${key}`);
695
- }
696
-
697
- if (definition.kind === "boolean") {
698
- values.set(key, true);
699
- continue;
700
- }
701
-
702
- const next = args[index + 1];
703
-
704
- if (next === undefined || next.startsWith("--")) {
705
- throw new Error(`missing value for --${key}`);
706
- }
707
-
708
- values.set(key, next);
709
- index += 1;
710
- }
711
-
712
- return values;
713
- }
714
-
715
- function splitStartArgs(args: readonly string[]): {
716
- readonly cwdArg: string | undefined;
717
- readonly flagArgs: readonly string[];
718
- } {
719
- let cwdArg: string | undefined;
720
- const flagArgs: string[] = [];
721
-
722
- for (let index = 0; index < args.length; index += 1) {
723
- const arg = args[index];
724
-
725
- if (arg === undefined) {
726
- continue;
727
- }
728
-
729
- if (!arg.startsWith("--")) {
730
- if (cwdArg !== undefined) {
731
- throw new Error("linzumi start accepts at most one folder path");
732
- }
733
-
734
- cwdArg = arg;
735
- continue;
736
- }
737
-
738
- flagArgs.push(arg);
739
- const key = arg.slice(2);
740
- const definition = flagDefinitions.get(key);
741
-
742
- if (definition?.kind === "value") {
743
- const next = args[index + 1];
744
-
745
- if (next !== undefined) {
746
- flagArgs.push(next);
747
- index += 1;
748
- }
749
- }
750
- }
751
-
752
- return { cwdArg, flagArgs };
753
- }
754
-
755
- function rejectStartTargetingFlags(values: Map<string, string | true>): void {
756
- const unsupportedFlags = ["workspace", "channel"].filter((flag) =>
757
- values.has(flag),
758
- );
759
-
760
- if (unsupportedFlags.length > 0) {
761
- throw new Error(
762
- `linzumi start chooses its workspace during browser onboarding; remove ${unsupportedFlags
763
- .map((flag) => `--${flag}`)
764
- .join(", ")} or use linzumi connect for an explicit workspace/channel.`,
765
- );
766
- }
767
- }
768
-
769
- function resolveUserPath(pathValue: string): string {
770
- if (pathValue === "~") {
771
- return homedir();
772
- }
773
-
774
- if (pathValue.startsWith("~/")) {
775
- return resolve(homedir(), pathValue.slice(2));
776
- }
777
-
778
- return resolve(pathValue);
779
- }
780
-
781
- function parseChannelSession(
782
- values: Map<string, string | true>,
783
- token: string,
784
- target:
785
- | { readonly workspaceSlug: string; readonly channelSlug: string }
786
- | undefined,
787
- ): RunnerOptions["channelSession"] {
788
- if (target === undefined) {
789
- return undefined;
790
- }
791
-
792
- const listenUser =
793
- stringValue(values, "listen-user") ?? defaultListenUserFromToken(token);
794
-
795
- return {
796
- workspaceSlug: target.workspaceSlug,
797
- channelSlug: target.channelSlug,
798
- kandanThreadId: stringValue(values, "kandan-thread-id"),
799
- listenUser,
800
- model: stringValue(values, "model"),
801
- reasoningEffort: stringValue(values, "reasoning-effort"),
802
- sandbox: stringValue(values, "sandbox"),
803
- approvalPolicy: stringValue(values, "approval-policy"),
804
- streamFlushMs: positiveIntegerValue(values, "stream-flush-ms"),
805
- };
806
- }
807
-
808
- function parseChannelSessionTarget(
809
- values: Map<string, string | true>,
810
- ):
811
- | { readonly workspaceSlug: string; readonly channelSlug: string }
812
- | undefined {
813
- return parseOptionalChannelTarget(values);
814
- }
815
-
816
- function defaultListenUserFromToken(token: string): string {
817
- const username = identityFromAccessToken(token).actorUsername;
818
-
819
- if (username !== undefined) {
820
- return username;
821
- }
822
-
823
- throw new Error(
824
- "missing --listen-user and authenticated user is unavailable",
825
- );
826
- }
827
-
828
- function parseOptionalChannelTarget(
829
- values: Map<string, string | true>,
830
- ):
831
- | { readonly workspaceSlug: string; readonly channelSlug: string }
832
- | undefined {
833
- const channel = stringValue(values, "channel");
834
- const workspace = stringValue(values, "workspace");
835
-
836
- if (channel === undefined && workspace === undefined) {
837
- return undefined;
838
- }
839
-
840
- return channel !== undefined && channel.includes("/")
841
- ? parseChannelPath(channel)
842
- : {
843
- workspaceSlug: workspace ?? required(values, "workspace"),
844
- channelSlug: channel ?? required(values, "channel"),
845
- };
846
- }
847
-
848
- function parseChannelPath(channel: string): {
849
- readonly workspaceSlug: string;
850
- readonly channelSlug: string;
851
- } {
852
- const [workspaceSlug, channelSlug, ...rest] = channel.split("/");
853
-
854
- if (
855
- workspaceSlug === undefined ||
856
- workspaceSlug.trim() === "" ||
857
- channelSlug === undefined ||
858
- channelSlug.trim() === "" ||
859
- rest.length > 0
860
- ) {
861
- throw new Error("--channel must be CHANNEL or WORKSPACE/CHANNEL");
862
- }
863
-
864
- return {
865
- workspaceSlug: workspaceSlug.trim(),
866
- channelSlug: channelSlug.trim(),
867
- };
868
- }
869
-
870
- function required(values: Map<string, string | true>, key: string): string {
871
- const value = stringValue(values, key);
872
-
873
- if (value === undefined) {
874
- throw new Error(`missing --${key}`);
875
- }
876
-
877
- return value;
878
- }
879
-
880
- function stringValue(
881
- values: Map<string, string | true>,
882
- key: string,
883
- ): string | undefined {
884
- const value = values.get(key);
885
-
886
- if (typeof value === "string" && value.trim() !== "") {
887
- return value;
888
- }
889
-
890
- return undefined;
891
- }
892
-
893
- function positiveIntegerValue(
894
- values: Map<string, string | true>,
895
- key: string,
896
- ): number | undefined {
897
- const value = stringValue(values, key);
898
-
899
- if (value === undefined) {
900
- return undefined;
901
- }
902
-
903
- const parsed = Number(value);
904
-
905
- if (Number.isInteger(parsed) && parsed > 0) {
906
- return parsed;
907
- }
908
-
909
- throw new Error(`--${key} must be a positive integer`);
910
- }
911
-
912
- function helpText(): string {
913
- return `Linzumi local Codex runner
914
-
915
- Usage:
916
- linzumi
917
- linzumi signup --email <email> --agent-name <name>
918
- linzumi claim --pending <pending_id> --code <XXXX-XXXX>
919
- linzumi thread new <title> --message <message>
920
- linzumi post <thread_id> <message>
921
- linzumi inbox <thread_id> --since-last
922
- linzumi done <thread_id> --message <message>
923
- linzumi agent runner <folder> [options]
924
- linzumi start <folder> [options]
925
- linzumi paths list|add|remove [path]
926
- linzumi connect --kandan-url <ws-url> --workspace <slug> --channel <slug> [options]
927
- linzumi auth --kandan-url <ws-url> [--workspace <slug> --channel <slug>]
928
-
929
- Required:
930
- --kandan-url <ws-url> Linzumi backend URL, default wss://serve.linzumi.com
931
- --token <jwt> Optional override token. Otherwise ~/.linzumi/auth.json is validated or OAuth opens.
932
- --auth-file <path> Auth cache path, default ~/.linzumi/auth.json
933
- --oauth-callback-host <ip> Callback host reachable by your browser
934
-
935
- Channel binding:
936
- --workspace <slug> Workspace slug
937
- --channel <slug|w/c> Channel slug, or workspace/channel
938
- --kandan-thread-id <uuid> Resume an existing Linzumi thread instead of announcing a new root
939
- --listen-user <user|all> User whose replies are accepted, default authenticated user
940
-
941
- Codex:
942
- --cwd <path> Working directory for Codex, default current directory
943
- --codex-bin <path> Codex executable, default codex
944
- --codex-url <ws-url> Existing Codex app-server websocket URL
945
- --launch-tui Launch codex --remote against the app-server
946
- --model <name> Model requested for the Codex thread and shown in Linzumi
947
- --reasoning-effort <value> Reasoning effort requested for Codex and shown in Linzumi
948
- --sandbox <value> Sandbox metadata shown in Linzumi
949
- --approval-policy <value> Approval-policy metadata shown in Linzumi
950
- --stream-flush-ms <ms> Batch live Codex deltas before Linzumi persistence, default 150
951
- --fast Mark this runner as low-latency/fast in the availability message
952
- --log-file <path> JSONL event log path, default <cwd>/.linzumi-runner.log
953
- --allowed-cwd <paths> Comma-separated roots where Linzumi may start new local Codex sessions
954
- --forward-port <ports> Comma-separated local TCP ports Linzumi may expose as authenticated previews
955
- --code-server-bin <path> Custom development code-server executable. The default editor runtime is downloaded from Linzumi.
956
-
957
- Examples:
958
- Good:
959
- linzumi signup --email alice@example.com --agent-name BuildBot
960
- linzumi claim --pending pnd_2k4f9w --code 7K2C-9X4M
961
- linzumi thread new "Hello world" --message "Starting now. ETA 3m."
962
- linzumi post thr_abc123 "PR is open"
963
- linzumi done thr_abc123 --message "Done: https://github.com/example/repo/pull/1"
964
- linzumi agent runner ~/code/my-app --runner-id launch-agent-runner
965
- linzumi start ~/
966
- linzumi start ~/code/my-app
967
- linzumi connect --workspace <your-workspace> --channel <your-channel> --launch-tui
968
- linzumi connect --workspace <your-workspace> --channel <your-channel> --model gpt-5 --reasoning-effort low --fast --launch-tui
969
- linzumi auth --workspace <your-workspace> --channel <your-channel>
970
- linzumi paths add ~/code/my-app
971
- linzumi paths list
972
-
973
- Bad:
974
- linzumi connect --token not-a-jwt --workspace <your-workspace> --channel <your-channel>
975
- Missing --listen-user and authenticated user is unavailable.
976
- linzumi connect --token "$TOKEN" --listen-users sean
977
- Invalid flag: use --listen-user.
978
- linzumi connect --workspace <your-workspace> --channel <your-channel> --allowed-cwd /does/not/exist
979
- Invalid --allowed-cwd: allowed cwd roots must exist locally.
980
- linzumi connect --workspace <your-workspace> --channel <your-channel> --forward-port vite
981
- Invalid --forward-port: value must be a TCP port from 1 to 65535.
982
- `;
983
- }
984
-
985
- function pathsHelpText(): string {
986
- return `Linzumi trusted paths
987
-
988
- Usage:
989
- linzumi paths list
990
- linzumi paths add <path>
991
- linzumi paths remove <path>
992
-
993
- Trusted paths are stored in ~/.linzumi/config.json. linzumi connect uses them
994
- unless --allowed-cwd is passed for that runner process.
995
- `;
996
- }
997
-
998
- function startHelpText(): string {
999
- return `Linzumi one-command local runner
1000
-
1001
- Usage:
1002
- linzumi start <folder> [options]
1003
-
1004
- What it does:
1005
- Opens Linzumi in your browser, creates or reuses your personal coding space,
1006
- grants a scoped local-runner token, persists <folder> to your trusted-paths
1007
- list, and starts this computer as a runner for Codex sessions, local
1008
- previews, and the browser VS Code editor.
1009
-
1010
- Options:
1011
- --kandan-url <ws-url> Linzumi backend URL, default wss://serve.linzumi.com
1012
- --token <jwt> Optional scoped local-runner token override
1013
- --auth-file <path> Auth cache path, default ~/.linzumi/auth.json
1014
- --oauth-callback-host <ip> Callback host reachable by your browser
1015
- --codex-bin <path> Codex executable, default codex
1016
- --code-server-bin <path> Custom development code-server executable. By default Linzumi installs the approved editor runtime.
1017
- --listen-user <user|all> User whose replies are accepted, default authenticated user
1018
- --model <name> Model requested for Codex sessions
1019
- --reasoning-effort <value> Reasoning effort requested for Codex sessions
1020
- --sandbox <value> Sandbox metadata shown in Linzumi
1021
- --approval-policy <value> Approval-policy metadata shown in Linzumi
1022
- --forward-port <ports> Comma-separated local TCP ports Linzumi may expose as previews
1023
- --fast Mark this runner as low-latency/fast in Linzumi
1024
-
1025
- Examples:
1026
- linzumi start ~/
1027
- linzumi start ~/code/my-app
1028
- `;
1029
- }
1030
-
1031
- function agentRunnerHelpText(): string {
1032
- return `Linzumi agent-owned local runner
1033
-
1034
- Usage:
1035
- linzumi agent runner <folder> [options]
1036
-
1037
- What it does:
1038
- Starts this computer as the claimed agent's scoped local runner. The command
1039
- reads ~/.linzumi/agent-token.json, uses its workspace/channel scope, trusts
1040
- only the selected folder by default, and listens only to the owning human
1041
- recorded during claim unless --listen-user is passed.
1042
-
1043
- Options:
1044
- --agent-token-file <path> Agent token cache, default ~/.linzumi/agent-token.json
1045
- --kandan-url <ws-url> Kandan websocket base URL. Defaults deterministically from the stored apiUrl.
1046
- --runner-id <id> Stable local runner id
1047
- --codex-bin <path> Codex executable, default codex
1048
- --code-server-bin <path> Custom development code-server executable. By default Kandan installs the approved editor runtime.
1049
- --listen-user <user> Human whose replies Codex may accept, default owner from claim
1050
- --model <name> Model requested for Codex sessions
1051
- --reasoning-effort <value> Reasoning effort requested for Codex sessions
1052
- --sandbox <value> Sandbox metadata shown in Kandan
1053
- --approval-policy <value> Approval-policy metadata shown in Kandan
1054
- --forward-port <ports> Comma-separated local TCP ports Kandan may expose as previews
1055
- --allowed-cwd <paths> Override the selected folder with comma-separated trusted roots
1056
- --fast Mark this runner as low-latency/fast in Kandan
1057
-
1058
- Examples:
1059
- linzumi agent runner "$PWD" --runner-id hello-world-agent
1060
- linzumi agent runner ~/code/my-app --kandan-url ws://127.0.0.1:4162 --runner-id local-qa-agent
1061
- `;
1062
- }
1063
-
1064
- function connectGuideText(): string {
1065
- return `linzumi start
1066
-
1067
- Fastest path:
1068
- linzumi start ~/
1069
-
1070
- This opens Linzumi in your browser, creates or reuses your personal coding
1071
- space, persists this folder to your trusted-paths list, and starts this
1072
- computer as a local Codex runner.
1073
-
1074
- Advanced (when you already know your workspace and channel):
1075
- linzumi connect --workspace <your-workspace> --channel <your-channel>
1076
-
1077
- For help:
1078
- linzumi connect --help
1079
- `;
1080
- }