@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/README.md +208 -229
- package/package.json +2 -2
- package/src/agentBootstrap.ts +806 -0
- package/src/channelSession.ts +630 -31
- package/src/channelSessionSupport.ts +54 -1
- package/src/forwardTunnel.ts +32 -7
- package/src/index.ts +373 -55
- package/src/kandanQueue.ts +11 -0
- package/src/localCapabilities.ts +14 -1
- package/src/localConfig.ts +99 -0
- package/src/localEditor.ts +1 -1
- package/src/localForwarding.ts +31 -8
- package/src/protocol.ts +16 -0
- package/src/runner.ts +49 -15
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.
|
|
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.
|
|
313
|
+
const kandanUrl = stringValue(values, "kandan-url") ?? "wss://serve.linzumi.com";
|
|
212
314
|
const requestedCwd = resolveUserPath(cwdArg ?? process.cwd());
|
|
213
|
-
const
|
|
214
|
-
const
|
|
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.
|
|
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:
|
|
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 `
|
|
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>
|
|
655
|
-
--token <jwt> Optional override token. Otherwise ~/.
|
|
656
|
-
--auth-file <path> Auth cache path, default ~/.
|
|
657
|
-
--oauth-callback-host <ip> Callback host reachable by your browser
|
|
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
|
|
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
|
|
671
|
-
--reasoning-effort <value> Reasoning effort requested for Codex and shown in
|
|
672
|
-
--sandbox <value> Sandbox metadata shown in
|
|
673
|
-
--approval-policy <value> Approval-policy metadata shown in
|
|
674
|
-
--stream-flush-ms <ms> Batch live Codex deltas before
|
|
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>/.
|
|
677
|
-
--allowed-cwd <paths> Comma-separated roots where
|
|
678
|
-
--forward-port <ports> Comma-separated local TCP ports
|
|
679
|
-
--code-server-bin <path> Custom development code-server executable. The default editor runtime is downloaded from
|
|
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
|
|
685
|
-
linzumi connect --
|
|
686
|
-
linzumi connect --
|
|
687
|
-
linzumi auth --
|
|
688
|
-
linzumi
|
|
689
|
-
linzumi
|
|
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 --
|
|
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 --
|
|
975
|
+
linzumi connect --token "$TOKEN" --listen-users sean
|
|
700
976
|
Invalid flag: use --listen-user.
|
|
701
|
-
linzumi connect --
|
|
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 --
|
|
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
|
|
716
|
-
grants a scoped local-runner token,
|
|
717
|
-
|
|
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>
|
|
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 ~/.
|
|
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
|
|
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
|
|
737
|
-
linzumi
|
|
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
|
|
748
|
-
space,
|
|
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
|
-
|
|
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
|
package/src/kandanQueue.ts
CHANGED
|
@@ -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
|
}
|
package/src/localCapabilities.ts
CHANGED
|
@@ -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[],
|