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