@linzumi/cli 0.0.20-beta → 0.0.23-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 +65 -62
- package/bin/linzumi.js +10 -18
- package/dist/assets/linzumi-logo.svg +1 -0
- package/dist/index.js +9229 -0
- package/package.json +9 -4
- package/src/agentBootstrap.ts +0 -872
- package/src/authCache.ts +0 -157
- package/src/authResolution.ts +0 -77
- package/src/boundedCache.ts +0 -57
- package/src/channelSession.ts +0 -4301
- package/src/channelSessionSupport.ts +0 -308
- package/src/codexAppServer.ts +0 -380
- package/src/codexOutput.ts +0 -846
- package/src/codexRuntimeOptions.ts +0 -80
- package/src/dependencyStatus.ts +0 -198
- package/src/forwardTunnel.ts +0 -859
- package/src/forwardTunnelProtocol.ts +0 -324
- package/src/index.ts +0 -1080
- package/src/json.ts +0 -49
- package/src/kandanQueue.ts +0 -113
- package/src/kandanTls.ts +0 -86
- package/src/localCapabilities.ts +0 -143
- package/src/localCodexMessageState.ts +0 -135
- package/src/localCodexTurnState.ts +0 -108
- package/src/localConfig.ts +0 -99
- package/src/localEditor.ts +0 -1061
- package/src/localEditorRuntime.ts +0 -717
- package/src/localForwarding.ts +0 -523
- package/src/oauth.ts +0 -425
- package/src/pendingKandanMessageQueue.ts +0 -109
- package/src/phoenix.ts +0 -359
- package/src/portForwardApproval.ts +0 -181
- package/src/portForwardWatcher.ts +0 -404
- package/src/protocol.ts +0 -321
- package/src/runner.ts +0 -943
- package/src/runnerConsoleReporter.ts +0 -142
- package/src/runnerLogger.ts +0 -50
- package/src/streamDeltaCoalescing.ts +0 -129
- package/src/streamDeltaQueue.ts +0 -102
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
|
-
}
|