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