@linzumi/cli 0.0.11-beta → 0.0.13-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -229
- package/package.json +2 -2
- package/src/agentBootstrap.ts +806 -0
- package/src/channelSession.ts +630 -31
- package/src/channelSessionSupport.ts +54 -1
- package/src/forwardTunnel.ts +32 -7
- package/src/index.ts +373 -55
- package/src/kandanQueue.ts +11 -0
- package/src/localCapabilities.ts +14 -1
- package/src/localConfig.ts +99 -0
- package/src/localEditor.ts +1 -1
- package/src/localForwarding.ts +31 -8
- package/src/protocol.ts +16 -0
- package/src/runner.ts +49 -15
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- Date: 2026-05-01
|
|
3
|
+
Spec: ../../kandan/server_v2/plans/2026-05-01-runner-editor-dropdown-and-thread-controls.md
|
|
4
|
+
Relationship: Owns the npm-first local runner's trusted-folder config at
|
|
5
|
+
~/.linzumi/config.json so README path-management commands are product truth.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { dirname, resolve } from "node:path";
|
|
10
|
+
import { expandUserPath } from "./localCapabilities";
|
|
11
|
+
|
|
12
|
+
export type LinzumiConfig = {
|
|
13
|
+
readonly version: 1;
|
|
14
|
+
readonly allowedCwds: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function localConfigPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
18
|
+
const override = env.LINZUMI_CONFIG_FILE;
|
|
19
|
+
|
|
20
|
+
return override !== undefined && override.trim() !== ""
|
|
21
|
+
? resolve(expandUserPath(override))
|
|
22
|
+
: resolve(homedir(), ".linzumi", "config.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readLocalConfig(path: string = localConfigPath()): LinzumiConfig {
|
|
26
|
+
if (!existsSync(path)) {
|
|
27
|
+
return { version: 1, allowedCwds: [] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
31
|
+
|
|
32
|
+
if (!isConfigPayload(parsed)) {
|
|
33
|
+
throw new Error(`invalid Linzumi config: ${path}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
allowedCwds: uniqueStrings(parsed.allowedCwds),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readConfiguredAllowedCwds(path: string = localConfigPath()): string[] {
|
|
43
|
+
return readLocalConfig(path).allowedCwds.map((cwd) => {
|
|
44
|
+
try {
|
|
45
|
+
return realpathSync(resolve(expandUserPath(cwd)));
|
|
46
|
+
} catch (_error) {
|
|
47
|
+
throw new Error(`invalid Linzumi config allowed path: ${cwd} does not exist`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function addAllowedCwd(pathValue: string, path: string = localConfigPath()): string[] {
|
|
53
|
+
const normalizedPath = realpathSync(resolve(expandUserPath(pathValue)));
|
|
54
|
+
const config = readLocalConfig(path);
|
|
55
|
+
const allowedCwds = uniqueStrings([...config.allowedCwds, normalizedPath]);
|
|
56
|
+
writeLocalConfig({ version: 1, allowedCwds }, path);
|
|
57
|
+
return allowedCwds;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function removeAllowedCwd(pathValue: string, path: string = localConfigPath()): string[] {
|
|
61
|
+
const requestedPath = resolve(expandUserPath(pathValue));
|
|
62
|
+
const normalizedRequest = realpathOrResolved(requestedPath);
|
|
63
|
+
const config = readLocalConfig(path);
|
|
64
|
+
const allowedCwds = config.allowedCwds.filter((cwd) => {
|
|
65
|
+
const normalizedExisting = realpathOrResolved(cwd);
|
|
66
|
+
return cwd !== pathValue && normalizedExisting !== normalizedRequest;
|
|
67
|
+
});
|
|
68
|
+
writeLocalConfig({ version: 1, allowedCwds }, path);
|
|
69
|
+
return allowedCwds;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function writeLocalConfig(config: LinzumiConfig, path: string = localConfigPath()): void {
|
|
73
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
74
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isConfigPayload(value: unknown): value is LinzumiConfig {
|
|
78
|
+
return (
|
|
79
|
+
typeof value === "object" &&
|
|
80
|
+
value !== null &&
|
|
81
|
+
(value as { readonly version?: unknown }).version === 1 &&
|
|
82
|
+
Array.isArray((value as { readonly allowedCwds?: unknown }).allowedCwds) &&
|
|
83
|
+
(value as { readonly allowedCwds: readonly unknown[] }).allowedCwds.every(
|
|
84
|
+
(cwd) => typeof cwd === "string" && cwd.trim() !== "",
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function uniqueStrings(values: readonly string[]): string[] {
|
|
90
|
+
return [...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function realpathOrResolved(pathValue: string): string {
|
|
94
|
+
try {
|
|
95
|
+
return realpathSync(resolve(expandUserPath(pathValue)));
|
|
96
|
+
} catch (_error) {
|
|
97
|
+
return resolve(expandUserPath(pathValue));
|
|
98
|
+
}
|
|
99
|
+
}
|
package/src/localEditor.ts
CHANGED
|
@@ -592,7 +592,7 @@ export function prepareLocalEditorCollaboration(
|
|
|
592
592
|
return undefined;
|
|
593
593
|
}
|
|
594
594
|
|
|
595
|
-
const targetPath = `/local-codex-
|
|
595
|
+
const targetPath = `/local-codex-runners/${encodeURIComponent(runnerId)}/forwards/${serverPort}/preview-target`;
|
|
596
596
|
const serverUrl = new URL(targetPath, `${browserBaseUrl}/`).toString();
|
|
597
597
|
const bootstrapServerUrl =
|
|
598
598
|
collaboration.bootstrapToken === undefined || collaboration.bootstrapToken === ""
|
package/src/localForwarding.ts
CHANGED
|
@@ -211,14 +211,15 @@ function openLocalWebSocket(
|
|
|
211
211
|
scheme: "ws" | "wss",
|
|
212
212
|
): void {
|
|
213
213
|
let opened = false;
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
214
|
+
const url = localForwardUrl(
|
|
215
|
+
scheme === "ws" ? "http" : "https",
|
|
216
|
+
control.port,
|
|
217
|
+
control.path,
|
|
218
|
+
control.queryString,
|
|
219
|
+
).replace(/^http/, scheme);
|
|
220
|
+
const protocols = webSocketProtocols(control.headers);
|
|
221
|
+
const websocket =
|
|
222
|
+
protocols === undefined ? new WebSocket(url) : new WebSocket(url, protocols);
|
|
222
223
|
sockets.set(control.socketId, websocket);
|
|
223
224
|
websocket.addEventListener("open", () => {
|
|
224
225
|
opened = true;
|
|
@@ -260,6 +261,28 @@ function openLocalWebSocket(
|
|
|
260
261
|
});
|
|
261
262
|
}
|
|
262
263
|
|
|
264
|
+
function webSocketProtocols(headers: JsonValue | undefined): string[] | undefined {
|
|
265
|
+
if (!Array.isArray(headers)) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const protocols = headers.flatMap(header => {
|
|
270
|
+
if (
|
|
271
|
+
!isWireHeader(header) ||
|
|
272
|
+
header.name.toLowerCase() !== "sec-websocket-protocol"
|
|
273
|
+
) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return header.value
|
|
278
|
+
.split(",")
|
|
279
|
+
.map(protocol => protocol.trim())
|
|
280
|
+
.filter(protocol => protocol !== "");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return protocols.length === 0 ? undefined : protocols;
|
|
284
|
+
}
|
|
285
|
+
|
|
263
286
|
function localForwardUrl(
|
|
264
287
|
scheme: "http" | "https",
|
|
265
288
|
port: number,
|
package/src/protocol.ts
CHANGED
|
@@ -37,6 +37,11 @@
|
|
|
37
37
|
Spec: plans/2026-04-26-local-runner-subdomain-forwarding-epr.md
|
|
38
38
|
Relationship: Defines the typed WebSocket forwarding controls used by
|
|
39
39
|
isolated preview subdomains.
|
|
40
|
+
|
|
41
|
+
- Date: 2026-05-02
|
|
42
|
+
Spec: plans/2026-05-02-agent-first-zero-to-codex-launch-plan.md
|
|
43
|
+
Relationship: Keeps the npm CLI runner protocol in parity with the legacy
|
|
44
|
+
runner for runtime session setting updates.
|
|
40
45
|
*/
|
|
41
46
|
export type JsonValue =
|
|
42
47
|
| null
|
|
@@ -133,6 +138,7 @@ export type KandanControl =
|
|
|
133
138
|
readonly type: "start_instance";
|
|
134
139
|
readonly instanceId?: string;
|
|
135
140
|
readonly cwd?: string;
|
|
141
|
+
readonly workDescription?: string;
|
|
136
142
|
readonly launchTui?: boolean;
|
|
137
143
|
readonly model?: string;
|
|
138
144
|
readonly reasoningEffort?: string;
|
|
@@ -181,6 +187,16 @@ export type KandanControl =
|
|
|
181
187
|
readonly requestId: string;
|
|
182
188
|
readonly decision: "approve" | "deny";
|
|
183
189
|
}
|
|
190
|
+
| {
|
|
191
|
+
readonly type: "update_session_settings";
|
|
192
|
+
readonly instanceId?: string;
|
|
193
|
+
readonly threadId: string;
|
|
194
|
+
readonly model?: string | null;
|
|
195
|
+
readonly reasoningEffort?: string | null;
|
|
196
|
+
readonly approvalPolicy?: string | null;
|
|
197
|
+
readonly sandbox?: string | null;
|
|
198
|
+
readonly fast?: boolean;
|
|
199
|
+
}
|
|
184
200
|
| {
|
|
185
201
|
readonly type: "resolve_port_forward_request";
|
|
186
202
|
readonly instanceId: string;
|
package/src/runner.ts
CHANGED
|
@@ -60,7 +60,6 @@ import { join } from "node:path";
|
|
|
60
60
|
import { attachChannelSession } from "./channelSession";
|
|
61
61
|
import { connectCodexAppServer, startCodexAppServer } from "./codexAppServer";
|
|
62
62
|
import { arrayValue, integerValue, objectValue, stringValue } from "./json";
|
|
63
|
-
import { connectForwardTunnel } from "./forwardTunnel";
|
|
64
63
|
import { resolveAllowedCwd } from "./localCapabilities";
|
|
65
64
|
import {
|
|
66
65
|
createForwardWebSocketManager,
|
|
@@ -82,6 +81,7 @@ import {
|
|
|
82
81
|
import { connectPhoenixClient } from "./phoenix";
|
|
83
82
|
import {
|
|
84
83
|
type JsonObject,
|
|
84
|
+
type JsonRpcResponse,
|
|
85
85
|
type JsonValue,
|
|
86
86
|
type KandanChannelSessionOptions,
|
|
87
87
|
type KandanControl,
|
|
@@ -183,8 +183,6 @@ async function openLocalCodexRunner(
|
|
|
183
183
|
allowedPorts: Array.from(liveForwardPorts).sort(
|
|
184
184
|
(left, right) => left - right,
|
|
185
185
|
),
|
|
186
|
-
streamingForwarding: true,
|
|
187
|
-
streamingForwardingVersion: 1,
|
|
188
186
|
toolStatus:
|
|
189
187
|
options.dependencyStatus === undefined
|
|
190
188
|
? null
|
|
@@ -209,18 +207,26 @@ async function openLocalCodexRunner(
|
|
|
209
207
|
const joinPayload = (): JsonObject => ({
|
|
210
208
|
clientName: "kandan-local-codex-runner",
|
|
211
209
|
version: "0.0.1",
|
|
210
|
+
workspace: options.channelSession?.workspaceSlug ?? null,
|
|
211
|
+
channel: options.channelSession?.channelSlug ?? null,
|
|
212
212
|
capabilities: capabilitiesPayload(),
|
|
213
213
|
});
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
214
|
+
|
|
215
|
+
const pendingControls: KandanControl[] = [];
|
|
216
|
+
const controlDispatcher: {
|
|
217
|
+
value: ((control: KandanControl) => void) | undefined;
|
|
218
|
+
} = { value: undefined };
|
|
219
|
+
kandan.onControl((control) => {
|
|
220
|
+
const dispatcher = controlDispatcher.value;
|
|
221
|
+
if (dispatcher === undefined) {
|
|
222
|
+
pendingControls.push(control);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
dispatcher(control);
|
|
222
227
|
});
|
|
223
|
-
|
|
228
|
+
|
|
229
|
+
await kandan.join(topic, joinPayload(), { rejoinPayload: joinPayload });
|
|
224
230
|
|
|
225
231
|
const started =
|
|
226
232
|
options.codexUrl === undefined
|
|
@@ -440,7 +446,7 @@ async function openLocalCodexRunner(
|
|
|
440
446
|
channelSession?.handleCodexNotification(notification.method, params);
|
|
441
447
|
});
|
|
442
448
|
|
|
443
|
-
|
|
449
|
+
const handleControl = (control: KandanControl) => {
|
|
444
450
|
log("kandan.control", { control });
|
|
445
451
|
if (!controlTargetsInstance(control, instanceId)) {
|
|
446
452
|
log("kandan.control_ignored", {
|
|
@@ -593,7 +599,10 @@ async function openLocalCodexRunner(
|
|
|
593
599
|
message: error instanceof Error ? error.message : String(error),
|
|
594
600
|
});
|
|
595
601
|
});
|
|
596
|
-
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
controlDispatcher.value = handleControl;
|
|
605
|
+
pendingControls.splice(0).forEach(handleControl);
|
|
597
606
|
|
|
598
607
|
return { instanceId, codexUrl, close };
|
|
599
608
|
}
|
|
@@ -661,9 +670,25 @@ async function discoverCodexThreads(
|
|
|
661
670
|
.filter((thread) => thread.id !== "");
|
|
662
671
|
}
|
|
663
672
|
|
|
673
|
+
function extractStartedThreadId(response: JsonRpcResponse): string | undefined {
|
|
674
|
+
if ("error" in response) {
|
|
675
|
+
return undefined;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return stringValue(objectValue(objectValue(response.result)?.thread)?.id);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function normalizedWorkDescription(value: string | undefined): string | undefined {
|
|
682
|
+
const normalized = value?.trim();
|
|
683
|
+
|
|
684
|
+
return normalized === undefined || normalized === ""
|
|
685
|
+
? undefined
|
|
686
|
+
: normalized;
|
|
687
|
+
}
|
|
688
|
+
|
|
664
689
|
function makeRunnerLogger(options: RunnerOptions): RunnerLogger {
|
|
665
690
|
return createRunnerLogger(
|
|
666
|
-
options.logFile ?? join(options.cwd, ".
|
|
691
|
+
options.logFile ?? join(options.cwd, ".linzumi-runner.log"),
|
|
667
692
|
options.launchTui ? undefined : reportRunnerConsoleEvent,
|
|
668
693
|
);
|
|
669
694
|
}
|
|
@@ -808,6 +833,15 @@ async function applyControl(
|
|
|
808
833
|
...(control.sandbox === undefined ? {} : { sandbox: control.sandbox }),
|
|
809
834
|
...(control.fast === true ? { serviceTier: "fast" } : {}),
|
|
810
835
|
});
|
|
836
|
+
const codexThreadId = extractStartedThreadId(response);
|
|
837
|
+
const workDescription = normalizedWorkDescription(control.workDescription);
|
|
838
|
+
|
|
839
|
+
if (codexThreadId !== undefined && workDescription !== undefined) {
|
|
840
|
+
await codex.request("turn/start", {
|
|
841
|
+
threadId: codexThreadId,
|
|
842
|
+
input: [{ type: "text", text: workDescription }],
|
|
843
|
+
});
|
|
844
|
+
}
|
|
811
845
|
|
|
812
846
|
return {
|
|
813
847
|
instanceId,
|