@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/runner.ts
DELETED
|
@@ -1,943 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
- Date: 2026-04-24
|
|
3
|
-
Spec: plans/2026-04-24-local-codex-runner-plan.md
|
|
4
|
-
Relationship: Bridges the spec's desired local instance lifecycle between
|
|
5
|
-
Kandan controls, Codex app-server JSON-RPC, and optional remote TUI launch.
|
|
6
|
-
|
|
7
|
-
- Date: 2026-04-24
|
|
8
|
-
Spec: plans/2026-04-24-local-codex-channel-thread-binding-spec.md
|
|
9
|
-
Relationship: Hosts the process lifecycle used by the channel-bound local
|
|
10
|
-
Codex product flow while delegating session behavior to `channelSession.ts`.
|
|
11
|
-
|
|
12
|
-
- Date: 2026-04-24
|
|
13
|
-
Spec: plans/2026-04-24-local-codex-runner-quality-pass-spec.md
|
|
14
|
-
Relationship: Keeps the runner focused on transport lifecycle and Codex
|
|
15
|
-
process orchestration instead of embedding pure channel-session policy.
|
|
16
|
-
|
|
17
|
-
- Date: 2026-04-24
|
|
18
|
-
Spec: plans/2026-04-24-local-codex-channel-session-module-spec.md
|
|
19
|
-
Relationship: Delegates channel-bound Kandan/Codex session orchestration to
|
|
20
|
-
the dedicated channel session module.
|
|
21
|
-
|
|
22
|
-
- Date: 2026-04-24
|
|
23
|
-
Spec: plans/2026-04-24-local-codex-runner-deep-quality-spec.md
|
|
24
|
-
Relationship: Owns channel-mode startup cost and process-level cleanup
|
|
25
|
-
requirements for the deep quality pass, including cleanup of partially
|
|
26
|
-
opened resources when startup fails.
|
|
27
|
-
|
|
28
|
-
- Date: 2026-04-25
|
|
29
|
-
Spec: plans/2026-04-24-local-codex-runner-deep-quality-spec.md
|
|
30
|
-
Relationship: Leaves channel-scoped approval controls to `channelSession.ts`
|
|
31
|
-
so the process runner remains lifecycle-only while Kandan safely resolves
|
|
32
|
-
Codex app-server approval requests from the thread UI.
|
|
33
|
-
|
|
34
|
-
- Date: 2026-04-26
|
|
35
|
-
Spec: plans/2026_04_26_linzumi_cli_review_followups_note.md
|
|
36
|
-
Relationship: Rejects stale Kandan controls for previous local runner
|
|
37
|
-
instances before they can mutate the current Codex app-server session.
|
|
38
|
-
|
|
39
|
-
- Date: 2026-04-26
|
|
40
|
-
Spec: plans/2026-04-26-local-codex-driver-worldclass-spec.md
|
|
41
|
-
Relationship: Enforces local machine cwd capabilities for Kandan-requested
|
|
42
|
-
Codex session starts and advertises cwd plus local preview-port capabilities
|
|
43
|
-
to the server on the runner join payload.
|
|
44
|
-
|
|
45
|
-
- Date: 2026-04-26
|
|
46
|
-
Spec: plans/2026-04-26-local-runner-forwarding-and-editor-plan.md
|
|
47
|
-
Relationship: Routes Kandan-authenticated local preview forwarding controls
|
|
48
|
-
to the local loopback HTTP fetcher after verifying the control targets this
|
|
49
|
-
runner instance.
|
|
50
|
-
|
|
51
|
-
- Date: 2026-04-26
|
|
52
|
-
Spec: plans/2026-04-26-local-runner-port-forward-approval.md
|
|
53
|
-
Relationship: Lets channel-session approval add descendant listener ports to
|
|
54
|
-
the runner's live forwarding capability before forwarding requests use them.
|
|
55
|
-
*/
|
|
56
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
57
|
-
import { randomUUID } from "node:crypto";
|
|
58
|
-
import { hostname } from "node:os";
|
|
59
|
-
import { join } from "node:path";
|
|
60
|
-
import { attachChannelSession } from "./channelSession";
|
|
61
|
-
import { connectCodexAppServer, startCodexAppServer } from "./codexAppServer";
|
|
62
|
-
import { arrayValue, integerValue, objectValue, stringValue } from "./json";
|
|
63
|
-
import { resolveAllowedCwd } from "./localCapabilities";
|
|
64
|
-
import {
|
|
65
|
-
createForwardWebSocketManager,
|
|
66
|
-
handleForwardHttpRequest,
|
|
67
|
-
isForwardHttpRequestControl,
|
|
68
|
-
isForwardWebSocketControl,
|
|
69
|
-
} from "./localForwarding";
|
|
70
|
-
import {
|
|
71
|
-
isStartLocalEditorControl,
|
|
72
|
-
localEditorCapabilities,
|
|
73
|
-
startLocalEditor,
|
|
74
|
-
type LocalEditorState,
|
|
75
|
-
} from "./localEditor";
|
|
76
|
-
import type { InstalledEditorRuntime } from "./localEditorRuntime";
|
|
77
|
-
import {
|
|
78
|
-
dependencyStatusPayload,
|
|
79
|
-
type RunnerDependencyStatus,
|
|
80
|
-
} from "./dependencyStatus";
|
|
81
|
-
import { connectPhoenixClient } from "./phoenix";
|
|
82
|
-
import {
|
|
83
|
-
type JsonObject,
|
|
84
|
-
type JsonRpcResponse,
|
|
85
|
-
type JsonValue,
|
|
86
|
-
type KandanChannelSessionOptions,
|
|
87
|
-
type KandanControl,
|
|
88
|
-
extractCodexIds,
|
|
89
|
-
isJsonObject,
|
|
90
|
-
} from "./protocol";
|
|
91
|
-
import { createRunnerLogger, type RunnerLogger } from "./runnerLogger";
|
|
92
|
-
import { reportRunnerConsoleEvent } from "./runnerConsoleReporter";
|
|
93
|
-
|
|
94
|
-
export type RunnerOptions = {
|
|
95
|
-
readonly kandanUrl: string;
|
|
96
|
-
readonly token: string;
|
|
97
|
-
readonly runnerId: string;
|
|
98
|
-
readonly cwd: string;
|
|
99
|
-
readonly codexBin: string;
|
|
100
|
-
readonly codexUrl: string | undefined;
|
|
101
|
-
readonly launchTui: boolean;
|
|
102
|
-
readonly fast?: boolean | undefined;
|
|
103
|
-
readonly logFile?: string | undefined;
|
|
104
|
-
readonly allowedCwds: readonly string[];
|
|
105
|
-
readonly allowedForwardPorts?: readonly number[] | undefined;
|
|
106
|
-
readonly codeServerBin?: string | undefined;
|
|
107
|
-
readonly editorRuntime?: InstalledEditorRuntime | undefined;
|
|
108
|
-
readonly dependencyStatus?: RunnerDependencyStatus | undefined;
|
|
109
|
-
readonly socketFactory?: ((url: string) => WebSocket) | undefined;
|
|
110
|
-
readonly channelSession: KandanChannelSessionOptions | undefined;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
export type LocalCodexRunnerHandle = {
|
|
114
|
-
readonly instanceId: string;
|
|
115
|
-
readonly codexUrl: string;
|
|
116
|
-
readonly close: () => Promise<void>;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
type CleanupAction = () => void | Promise<void>;
|
|
120
|
-
|
|
121
|
-
type CleanupStack = {
|
|
122
|
-
readonly actions: CleanupAction[];
|
|
123
|
-
closePromise: Promise<void> | undefined;
|
|
124
|
-
removeHandlers: (() => void) | undefined;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
export async function runLocalCodexRunner(
|
|
128
|
-
options: RunnerOptions,
|
|
129
|
-
): Promise<LocalCodexRunnerHandle> {
|
|
130
|
-
const log = makeRunnerLogger(options);
|
|
131
|
-
const cleanup: CleanupStack = {
|
|
132
|
-
actions: [() => log.close()],
|
|
133
|
-
closePromise: undefined,
|
|
134
|
-
removeHandlers: undefined,
|
|
135
|
-
};
|
|
136
|
-
const close = () => closeCleanupStack(cleanup);
|
|
137
|
-
cleanup.removeHandlers = installCleanupHandlers(close);
|
|
138
|
-
|
|
139
|
-
log("runner.starting", {
|
|
140
|
-
runnerId: options.runnerId,
|
|
141
|
-
cwd: options.cwd,
|
|
142
|
-
kandanUrl: options.kandanUrl,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
return await openLocalCodexRunner(options, log, cleanup, close);
|
|
147
|
-
} catch (error) {
|
|
148
|
-
await close().catch(() => undefined);
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
async function openLocalCodexRunner(
|
|
154
|
-
options: RunnerOptions,
|
|
155
|
-
log: RunnerLogger,
|
|
156
|
-
cleanup: CleanupStack,
|
|
157
|
-
close: () => Promise<void>,
|
|
158
|
-
): Promise<LocalCodexRunnerHandle> {
|
|
159
|
-
const allowedForwardPorts = options.allowedForwardPorts ?? [];
|
|
160
|
-
const liveForwardPorts = new Set<number>(allowedForwardPorts);
|
|
161
|
-
const managedForwardPorts = new Set<number>();
|
|
162
|
-
const allowedCwds = { value: [...options.allowedCwds] };
|
|
163
|
-
const localEditorState: { value: LocalEditorState } = {
|
|
164
|
-
value: { status: "disabled" },
|
|
165
|
-
};
|
|
166
|
-
const localEditorGeneration = { value: 0 };
|
|
167
|
-
cleanup.actions.push(() => {
|
|
168
|
-
if (localEditorState.value.status === "running") {
|
|
169
|
-
localEditorState.value.process.kill("SIGINT");
|
|
170
|
-
localEditorState.value.collaboration?.process.kill("SIGINT");
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
const capabilitiesPayload = (): JsonObject => ({
|
|
174
|
-
codexAppServer: true,
|
|
175
|
-
codexRemoteTui: true,
|
|
176
|
-
startInstance: allowedCwds.value.length > 0,
|
|
177
|
-
allowedCwds: allowedCwds.value,
|
|
178
|
-
allowedCwdSuggestions: allowedCwdSuggestions(
|
|
179
|
-
options.cwd,
|
|
180
|
-
allowedCwds.value,
|
|
181
|
-
),
|
|
182
|
-
portForwarding: liveForwardPorts.size > 0,
|
|
183
|
-
allowedPorts: Array.from(liveForwardPorts).sort(
|
|
184
|
-
(left, right) => left - right,
|
|
185
|
-
),
|
|
186
|
-
toolStatus:
|
|
187
|
-
options.dependencyStatus === undefined
|
|
188
|
-
? null
|
|
189
|
-
: dependencyStatusPayload(options.dependencyStatus),
|
|
190
|
-
editorRuntime:
|
|
191
|
-
options.dependencyStatus?.editorRuntime === undefined
|
|
192
|
-
? null
|
|
193
|
-
: dependencyStatusPayload(options.dependencyStatus).editorRuntime,
|
|
194
|
-
...localEditorCapabilities(
|
|
195
|
-
options.editorRuntime,
|
|
196
|
-
allowedCwds.value,
|
|
197
|
-
localEditorState.value,
|
|
198
|
-
),
|
|
199
|
-
});
|
|
200
|
-
const kandan = await connectPhoenixClient(
|
|
201
|
-
options.kandanUrl,
|
|
202
|
-
options.token,
|
|
203
|
-
options.socketFactory,
|
|
204
|
-
);
|
|
205
|
-
cleanup.actions.push(() => kandan.close());
|
|
206
|
-
const topic = `local_runner:${options.runnerId}`;
|
|
207
|
-
const joinPayload = (): JsonObject => ({
|
|
208
|
-
clientName: "kandan-local-codex-runner",
|
|
209
|
-
version: "0.0.1",
|
|
210
|
-
workspace: options.channelSession?.workspaceSlug ?? null,
|
|
211
|
-
channel: options.channelSession?.channelSlug ?? null,
|
|
212
|
-
capabilities: capabilitiesPayload(),
|
|
213
|
-
});
|
|
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);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
await kandan.join(topic, joinPayload(), { rejoinPayload: joinPayload });
|
|
230
|
-
|
|
231
|
-
const started =
|
|
232
|
-
options.codexUrl === undefined
|
|
233
|
-
? await startCodexAppServer(options.codexBin, options.cwd, {
|
|
234
|
-
model: options.channelSession?.model,
|
|
235
|
-
reasoningEffort: options.channelSession?.reasoningEffort,
|
|
236
|
-
fast: options.fast,
|
|
237
|
-
})
|
|
238
|
-
: undefined;
|
|
239
|
-
|
|
240
|
-
if (started !== undefined) {
|
|
241
|
-
cleanup.actions.push(() => {
|
|
242
|
-
started.process.kill("SIGINT");
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const codexUrl = options.codexUrl ?? started?.url;
|
|
247
|
-
|
|
248
|
-
if (codexUrl === undefined) {
|
|
249
|
-
throw new Error("missing codex app-server websocket URL");
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const instanceId = `codex-${randomUUID()}`;
|
|
253
|
-
const publishLocalEditorStatus = (payload: JsonObject): void => {
|
|
254
|
-
void kandan.push(topic, "local_editor_status", payload).catch((error) => {
|
|
255
|
-
log("kandan.local_editor_status_push_failed", {
|
|
256
|
-
message: error instanceof Error ? error.message : String(error),
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
};
|
|
260
|
-
const watchLocalEditorExit = (
|
|
261
|
-
state: Extract<LocalEditorState, { status: "running" }>,
|
|
262
|
-
generation: number,
|
|
263
|
-
initialStatusPushed: Promise<unknown>,
|
|
264
|
-
): void => {
|
|
265
|
-
const handleExit = () => {
|
|
266
|
-
if (
|
|
267
|
-
localEditorGeneration.value !== generation ||
|
|
268
|
-
localEditorState.value.status !== "running" ||
|
|
269
|
-
localEditorState.value.process !== state.process
|
|
270
|
-
) {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
localEditorState.value = { status: "disabled" };
|
|
275
|
-
liveForwardPorts.delete(state.port);
|
|
276
|
-
managedForwardPorts.delete(state.port);
|
|
277
|
-
if (state.collaboration !== undefined) {
|
|
278
|
-
liveForwardPorts.delete(state.collaboration.serverPort);
|
|
279
|
-
managedForwardPorts.delete(state.collaboration.serverPort);
|
|
280
|
-
}
|
|
281
|
-
publishLocalEditorStatus({
|
|
282
|
-
instanceId,
|
|
283
|
-
ok: true,
|
|
284
|
-
cwd: state.cwd,
|
|
285
|
-
capabilities: {
|
|
286
|
-
...capabilitiesPayload(),
|
|
287
|
-
revokedPorts:
|
|
288
|
-
state.collaboration === undefined
|
|
289
|
-
? [state.port]
|
|
290
|
-
: [state.port, state.collaboration.serverPort],
|
|
291
|
-
},
|
|
292
|
-
});
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const handleExitAfterInitialStatus = () => {
|
|
296
|
-
void initialStatusPushed.then(handleExit).catch((error) => {
|
|
297
|
-
log("kandan.local_editor_initial_status_failed", {
|
|
298
|
-
message: error instanceof Error ? error.message : String(error),
|
|
299
|
-
});
|
|
300
|
-
});
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
void state.exited.then(handleExitAfterInitialStatus);
|
|
304
|
-
};
|
|
305
|
-
const codex = await connectCodexAppServer(codexUrl);
|
|
306
|
-
cleanup.actions.push(() => codex.close());
|
|
307
|
-
|
|
308
|
-
const seq = { value: 0 };
|
|
309
|
-
const codexThreads =
|
|
310
|
-
options.channelSession === undefined
|
|
311
|
-
? await discoverCodexThreads(codex, options.cwd)
|
|
312
|
-
: [];
|
|
313
|
-
|
|
314
|
-
const runnerHost = hostname();
|
|
315
|
-
const instancePayload = {
|
|
316
|
-
instanceId,
|
|
317
|
-
codexUrl,
|
|
318
|
-
tuiLaunched: options.launchTui,
|
|
319
|
-
cwd: options.cwd,
|
|
320
|
-
hostname: runnerHost,
|
|
321
|
-
codexThreads,
|
|
322
|
-
model: options.channelSession?.model ?? null,
|
|
323
|
-
reasoningEffort: options.channelSession?.reasoningEffort ?? null,
|
|
324
|
-
fast: options.fast ?? false,
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
await kandan.push(topic, "instance_started", instancePayload);
|
|
328
|
-
log("runner.instance_started", { instanceId, codexUrl });
|
|
329
|
-
|
|
330
|
-
const channelSession =
|
|
331
|
-
options.channelSession === undefined
|
|
332
|
-
? undefined
|
|
333
|
-
: await attachChannelSession({
|
|
334
|
-
kandan,
|
|
335
|
-
codex,
|
|
336
|
-
topic,
|
|
337
|
-
instanceId,
|
|
338
|
-
options: {
|
|
339
|
-
token: options.token,
|
|
340
|
-
runnerId: options.runnerId,
|
|
341
|
-
cwd: options.cwd,
|
|
342
|
-
codexBin: options.codexBin,
|
|
343
|
-
fast: options.fast,
|
|
344
|
-
launchTui: options.launchTui,
|
|
345
|
-
enablePortForwardWatch: true,
|
|
346
|
-
initialForwardPorts: allowedForwardPorts,
|
|
347
|
-
suppressedForwardPorts: () => Array.from(managedForwardPorts),
|
|
348
|
-
onForwardPortApproved: (port) => {
|
|
349
|
-
liveForwardPorts.add(port);
|
|
350
|
-
return capabilitiesPayload();
|
|
351
|
-
},
|
|
352
|
-
onForwardPortRevoked: (port) => {
|
|
353
|
-
liveForwardPorts.delete(port);
|
|
354
|
-
return capabilitiesPayload();
|
|
355
|
-
},
|
|
356
|
-
channelSession: options.channelSession,
|
|
357
|
-
},
|
|
358
|
-
log,
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
if (channelSession !== undefined) {
|
|
362
|
-
cleanup.actions.push(() => channelSession.close());
|
|
363
|
-
kandan.onReconnect(() => channelSession.handleKandanReconnect());
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const heartbeatPayload = (): JsonObject => ({
|
|
367
|
-
instanceId,
|
|
368
|
-
codexUrl,
|
|
369
|
-
cwd: options.cwd,
|
|
370
|
-
hostname: runnerHost,
|
|
371
|
-
workspace: options.channelSession?.workspaceSlug ?? null,
|
|
372
|
-
channel: options.channelSession?.channelSlug ?? null,
|
|
373
|
-
threadId: channelSession?.currentKandanThreadId() ?? null,
|
|
374
|
-
codexThreadId: channelSession?.currentCodexThreadId() ?? null,
|
|
375
|
-
model: options.channelSession?.model ?? null,
|
|
376
|
-
reasoningEffort: options.channelSession?.reasoningEffort ?? null,
|
|
377
|
-
fast: options.fast ?? false,
|
|
378
|
-
capabilities: capabilitiesPayload(),
|
|
379
|
-
});
|
|
380
|
-
const pushHeartbeat = () =>
|
|
381
|
-
kandan.push(topic, "heartbeat", heartbeatPayload()).catch((error) => {
|
|
382
|
-
log("kandan.heartbeat_push_failed", {
|
|
383
|
-
message: error instanceof Error ? error.message : String(error),
|
|
384
|
-
});
|
|
385
|
-
});
|
|
386
|
-
const heartbeatInterval = setInterval(() => {
|
|
387
|
-
void pushHeartbeat();
|
|
388
|
-
}, 15_000);
|
|
389
|
-
cleanup.actions.push(() => clearInterval(heartbeatInterval));
|
|
390
|
-
kandan.onReconnect(() => pushHeartbeat().then(() => undefined));
|
|
391
|
-
void pushHeartbeat();
|
|
392
|
-
|
|
393
|
-
const forwardWebSockets = createForwardWebSocketManager(kandan, topic, () =>
|
|
394
|
-
Array.from(liveForwardPorts),
|
|
395
|
-
);
|
|
396
|
-
cleanup.actions.push(() => forwardWebSockets.close());
|
|
397
|
-
|
|
398
|
-
const channelCodexThreadId = channelSession?.currentCodexThreadId();
|
|
399
|
-
if (options.launchTui && channelCodexThreadId !== undefined) {
|
|
400
|
-
await prepareCodexThreadForTuiResume(codex, channelCodexThreadId);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const tui = options.launchTui
|
|
404
|
-
? launchCodexTui(
|
|
405
|
-
options.codexBin,
|
|
406
|
-
codexUrl,
|
|
407
|
-
options.cwd,
|
|
408
|
-
channelCodexThreadId,
|
|
409
|
-
options.channelSession,
|
|
410
|
-
options.fast,
|
|
411
|
-
)
|
|
412
|
-
: undefined;
|
|
413
|
-
|
|
414
|
-
if (tui !== undefined) {
|
|
415
|
-
cleanup.actions.push(() => {
|
|
416
|
-
tui.kill("SIGINT");
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
codex.onNotification((notification) => {
|
|
421
|
-
seq.value += 1;
|
|
422
|
-
const params = (notification.params ?? {}) as JsonObject;
|
|
423
|
-
const metadata = extractCodexIds(params);
|
|
424
|
-
|
|
425
|
-
if (channelSession === undefined) {
|
|
426
|
-
void kandan
|
|
427
|
-
.push(topic, "codex_notification", {
|
|
428
|
-
instanceId,
|
|
429
|
-
seq: seq.value,
|
|
430
|
-
method: notification.method,
|
|
431
|
-
params,
|
|
432
|
-
metadata,
|
|
433
|
-
receivedAt: new Date().toISOString(),
|
|
434
|
-
})
|
|
435
|
-
.catch((error) => {
|
|
436
|
-
log("kandan.codex_notification_push_failed", {
|
|
437
|
-
message: error instanceof Error ? error.message : String(error),
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
log("codex.notification", {
|
|
443
|
-
method: notification.method,
|
|
444
|
-
metadata,
|
|
445
|
-
});
|
|
446
|
-
channelSession?.handleCodexNotification(notification.method, params);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
const handleControl = (control: KandanControl) => {
|
|
450
|
-
log("kandan.control", { control });
|
|
451
|
-
if (!controlTargetsInstance(control, instanceId)) {
|
|
452
|
-
log("kandan.control_ignored", {
|
|
453
|
-
reason: "instance_id_mismatch",
|
|
454
|
-
instanceId,
|
|
455
|
-
controlInstanceId: control.instanceId,
|
|
456
|
-
controlType: control.type,
|
|
457
|
-
});
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (isForwardHttpRequestControl(control)) {
|
|
462
|
-
void handleForwardHttpRequest(control, Array.from(liveForwardPorts))
|
|
463
|
-
.then((response) =>
|
|
464
|
-
kandan.push(topic, "forward:http_response", response),
|
|
465
|
-
)
|
|
466
|
-
.catch((error) =>
|
|
467
|
-
kandan.push(topic, "forward:http_response", {
|
|
468
|
-
requestId: control.requestId,
|
|
469
|
-
ok: false,
|
|
470
|
-
error: error instanceof Error ? error.message : String(error),
|
|
471
|
-
}),
|
|
472
|
-
)
|
|
473
|
-
.catch((error) => {
|
|
474
|
-
log("kandan.forward_response_push_failed", {
|
|
475
|
-
message: error instanceof Error ? error.message : String(error),
|
|
476
|
-
});
|
|
477
|
-
});
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (isForwardWebSocketControl(control)) {
|
|
482
|
-
forwardWebSockets.handle(control);
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (isStartLocalEditorControl(control)) {
|
|
487
|
-
void startLocalEditor(control, {
|
|
488
|
-
codeServerBin: options.codeServerBin,
|
|
489
|
-
editorRuntime: options.editorRuntime,
|
|
490
|
-
allowedCwds: allowedCwds.value,
|
|
491
|
-
currentState: localEditorState.value,
|
|
492
|
-
browserBaseUrl:
|
|
493
|
-
control.browserBaseUrl ?? process.env.KANDAN_LOCAL_RUNNER_PUBLIC_BASE_URL,
|
|
494
|
-
runnerId: options.runnerId,
|
|
495
|
-
})
|
|
496
|
-
.then((result) => {
|
|
497
|
-
if (result.ok) {
|
|
498
|
-
localEditorGeneration.value += 1;
|
|
499
|
-
const editorGeneration = localEditorGeneration.value;
|
|
500
|
-
if (localEditorState.value.status === "running") {
|
|
501
|
-
liveForwardPorts.delete(localEditorState.value.port);
|
|
502
|
-
managedForwardPorts.delete(localEditorState.value.port);
|
|
503
|
-
if (localEditorState.value.collaboration !== undefined) {
|
|
504
|
-
liveForwardPorts.delete(
|
|
505
|
-
localEditorState.value.collaboration.serverPort,
|
|
506
|
-
);
|
|
507
|
-
managedForwardPorts.delete(
|
|
508
|
-
localEditorState.value.collaboration.serverPort,
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
localEditorState.value = result.state;
|
|
513
|
-
liveForwardPorts.add(result.event.port);
|
|
514
|
-
managedForwardPorts.add(result.event.port);
|
|
515
|
-
if (result.event.collaboration !== undefined) {
|
|
516
|
-
liveForwardPorts.add(result.event.collaboration.serverPort);
|
|
517
|
-
managedForwardPorts.add(result.event.collaboration.serverPort);
|
|
518
|
-
}
|
|
519
|
-
const initialStatusPushed = kandan.push(
|
|
520
|
-
topic,
|
|
521
|
-
"local_editor_status",
|
|
522
|
-
{
|
|
523
|
-
instanceId,
|
|
524
|
-
requestId: control.requestId,
|
|
525
|
-
ok: true,
|
|
526
|
-
capabilities: capabilitiesPayload(),
|
|
527
|
-
...result.event,
|
|
528
|
-
},
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
if (result.state.status === "running") {
|
|
532
|
-
watchLocalEditorExit(
|
|
533
|
-
result.state,
|
|
534
|
-
editorGeneration,
|
|
535
|
-
initialStatusPushed,
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return initialStatusPushed;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
localEditorState.value = result.state;
|
|
543
|
-
return kandan.push(topic, "local_editor_status", {
|
|
544
|
-
instanceId,
|
|
545
|
-
requestId: control.requestId,
|
|
546
|
-
ok: false,
|
|
547
|
-
reason: result.reason,
|
|
548
|
-
capabilities: capabilitiesPayload(),
|
|
549
|
-
});
|
|
550
|
-
})
|
|
551
|
-
.catch((error) =>
|
|
552
|
-
kandan.push(topic, "local_editor_status", {
|
|
553
|
-
instanceId,
|
|
554
|
-
requestId: control.requestId,
|
|
555
|
-
ok: false,
|
|
556
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
557
|
-
capabilities: capabilitiesPayload(),
|
|
558
|
-
}),
|
|
559
|
-
)
|
|
560
|
-
.catch((error) => {
|
|
561
|
-
log("kandan.local_editor_status_push_failed", {
|
|
562
|
-
message: error instanceof Error ? error.message : String(error),
|
|
563
|
-
});
|
|
564
|
-
});
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
if (isUpdateRunnerConfigControl(control)) {
|
|
569
|
-
allowedCwds.value = normalizeAllowedCwds(control.allowedCwds);
|
|
570
|
-
void pushHeartbeat();
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
void (channelSession?.handleControl(control) ?? Promise.resolve(undefined))
|
|
575
|
-
.then((handled) => {
|
|
576
|
-
if (handled !== undefined) {
|
|
577
|
-
return handled;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return applyControl(
|
|
581
|
-
codex,
|
|
582
|
-
instanceId,
|
|
583
|
-
options,
|
|
584
|
-
allowedCwds.value,
|
|
585
|
-
control,
|
|
586
|
-
);
|
|
587
|
-
})
|
|
588
|
-
.then((response) => {
|
|
589
|
-
return kandan.push(topic, "codex_response", response);
|
|
590
|
-
})
|
|
591
|
-
.catch((error) => {
|
|
592
|
-
return kandan.push(topic, "codex_error", {
|
|
593
|
-
instanceId,
|
|
594
|
-
message: error instanceof Error ? error.message : String(error),
|
|
595
|
-
});
|
|
596
|
-
})
|
|
597
|
-
.catch((error) => {
|
|
598
|
-
log("kandan.control_response_push_failed", {
|
|
599
|
-
message: error instanceof Error ? error.message : String(error),
|
|
600
|
-
});
|
|
601
|
-
});
|
|
602
|
-
};
|
|
603
|
-
|
|
604
|
-
controlDispatcher.value = handleControl;
|
|
605
|
-
pendingControls.splice(0).forEach(handleControl);
|
|
606
|
-
|
|
607
|
-
return { instanceId, codexUrl, close };
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function controlTargetsInstance(
|
|
611
|
-
control: KandanControl,
|
|
612
|
-
instanceId: string,
|
|
613
|
-
): boolean {
|
|
614
|
-
return control.instanceId === undefined || control.instanceId === instanceId;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
async function closeCleanupStack(cleanup: CleanupStack): Promise<void> {
|
|
618
|
-
if (cleanup.closePromise !== undefined) {
|
|
619
|
-
return cleanup.closePromise;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
cleanup.closePromise = (async () => {
|
|
623
|
-
const errors: Error[] = [];
|
|
624
|
-
cleanup.removeHandlers?.();
|
|
625
|
-
cleanup.removeHandlers = undefined;
|
|
626
|
-
|
|
627
|
-
for (const action of [...cleanup.actions].reverse()) {
|
|
628
|
-
try {
|
|
629
|
-
await action();
|
|
630
|
-
} catch (error) {
|
|
631
|
-
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
cleanup.actions.splice(0);
|
|
636
|
-
|
|
637
|
-
if (errors[0] !== undefined) {
|
|
638
|
-
throw errors[0];
|
|
639
|
-
}
|
|
640
|
-
})();
|
|
641
|
-
|
|
642
|
-
return cleanup.closePromise;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
async function discoverCodexThreads(
|
|
646
|
-
codex: Awaited<ReturnType<typeof connectCodexAppServer>>,
|
|
647
|
-
cwd: string,
|
|
648
|
-
): Promise<JsonValue[]> {
|
|
649
|
-
const response = await codex.request("thread/list", { cwd });
|
|
650
|
-
|
|
651
|
-
if ("error" in response) {
|
|
652
|
-
return [];
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const result = objectValue(response.result);
|
|
656
|
-
const data = arrayValue(result?.data);
|
|
657
|
-
|
|
658
|
-
return data === undefined
|
|
659
|
-
? []
|
|
660
|
-
: data
|
|
661
|
-
.filter(isJsonObject)
|
|
662
|
-
.map((thread) => ({
|
|
663
|
-
id: stringValue(thread.id) ?? "",
|
|
664
|
-
preview: stringValue(thread.preview) ?? "",
|
|
665
|
-
cwd: stringValue(thread.cwd) ?? "",
|
|
666
|
-
source: stringValue(thread.source) ?? "",
|
|
667
|
-
updatedAt: integerValue(thread.updatedAt) ?? null,
|
|
668
|
-
status: objectValue(thread.status) ?? null,
|
|
669
|
-
}))
|
|
670
|
-
.filter((thread) => thread.id !== "");
|
|
671
|
-
}
|
|
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
|
-
|
|
689
|
-
function makeRunnerLogger(options: RunnerOptions): RunnerLogger {
|
|
690
|
-
return createRunnerLogger(
|
|
691
|
-
options.logFile ?? join(options.cwd, ".linzumi-runner.log"),
|
|
692
|
-
options.launchTui ? undefined : reportRunnerConsoleEvent,
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function installCleanupHandlers(close: () => Promise<void>): () => void {
|
|
697
|
-
const closeAndExit = () => {
|
|
698
|
-
void close()
|
|
699
|
-
.catch(() => undefined)
|
|
700
|
-
.finally(() => process.exit(0));
|
|
701
|
-
};
|
|
702
|
-
const closeOnExit = () => {
|
|
703
|
-
void close().catch(() => undefined);
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
process.once("SIGINT", closeAndExit);
|
|
707
|
-
process.once("SIGTERM", closeAndExit);
|
|
708
|
-
process.once("SIGHUP", closeAndExit);
|
|
709
|
-
process.once("exit", closeOnExit);
|
|
710
|
-
|
|
711
|
-
return () => {
|
|
712
|
-
process.off("SIGINT", closeAndExit);
|
|
713
|
-
process.off("SIGTERM", closeAndExit);
|
|
714
|
-
process.off("SIGHUP", closeAndExit);
|
|
715
|
-
process.off("exit", closeOnExit);
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function launchCodexTui(
|
|
720
|
-
codexBin: string,
|
|
721
|
-
codexUrl: string,
|
|
722
|
-
cwd: string,
|
|
723
|
-
codexThreadId: string | undefined,
|
|
724
|
-
session: KandanChannelSessionOptions | undefined,
|
|
725
|
-
fast: boolean | undefined,
|
|
726
|
-
): ChildProcess {
|
|
727
|
-
return spawn(codexBin, codexTuiArgs(codexUrl, codexThreadId, session, fast), {
|
|
728
|
-
cwd,
|
|
729
|
-
env: process.env,
|
|
730
|
-
stdio: "inherit",
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
export function codexTuiArgs(
|
|
735
|
-
codexUrl: string,
|
|
736
|
-
codexThreadId: string | undefined,
|
|
737
|
-
session?: KandanChannelSessionOptions | undefined,
|
|
738
|
-
fast?: boolean | undefined,
|
|
739
|
-
): string[] {
|
|
740
|
-
const overrides = codexTuiConfigArgs(session, fast);
|
|
741
|
-
|
|
742
|
-
return codexThreadId === undefined
|
|
743
|
-
? ["--remote", codexUrl, ...overrides]
|
|
744
|
-
: ["resume", "--remote", codexUrl, ...overrides, codexThreadId];
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function codexTuiConfigArgs(
|
|
748
|
-
session: KandanChannelSessionOptions | undefined,
|
|
749
|
-
fast: boolean | undefined,
|
|
750
|
-
): string[] {
|
|
751
|
-
const modelArgs =
|
|
752
|
-
session?.model === undefined ? [] : ["--model", session.model];
|
|
753
|
-
const reasoningArgs =
|
|
754
|
-
session?.reasoningEffort === undefined
|
|
755
|
-
? []
|
|
756
|
-
: ["-c", `model_reasoning_effort="${session.reasoningEffort}"`];
|
|
757
|
-
const tierArgs = fast === true ? ["-c", 'service_tier="fast"'] : [];
|
|
758
|
-
|
|
759
|
-
return [...modelArgs, ...reasoningArgs, ...tierArgs];
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
export async function prepareCodexThreadForTuiResume(
|
|
763
|
-
codex: Pick<Awaited<ReturnType<typeof connectCodexAppServer>>, "request">,
|
|
764
|
-
codexThreadId: string,
|
|
765
|
-
): Promise<void> {
|
|
766
|
-
const resume = await codex.request("thread/resume", {
|
|
767
|
-
threadId: codexThreadId,
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
if (!("error" in resume)) {
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
if (!resume.error.message.includes("no rollout found")) {
|
|
775
|
-
throw new Error(
|
|
776
|
-
`failed to prepare Codex TUI resume: ${resume.error.message}`,
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
const injected = await codex.request("thread/inject_items", {
|
|
781
|
-
threadId: codexThreadId,
|
|
782
|
-
items: [{ type: "agentMessage", text: "" }],
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
if ("error" in injected) {
|
|
786
|
-
throw new Error(
|
|
787
|
-
`failed to prepare Codex TUI resume: ${injected.error.message}`,
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
const verified = await codex.request("thread/resume", {
|
|
792
|
-
threadId: codexThreadId,
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
if ("error" in verified) {
|
|
796
|
-
throw new Error(
|
|
797
|
-
`failed to verify Codex TUI resume: ${verified.error.message}`,
|
|
798
|
-
);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
async function applyControl(
|
|
803
|
-
codex: Awaited<ReturnType<typeof connectCodexAppServer>>,
|
|
804
|
-
instanceId: string,
|
|
805
|
-
options: RunnerOptions,
|
|
806
|
-
allowedCwds: readonly string[],
|
|
807
|
-
control: KandanControl,
|
|
808
|
-
): Promise<JsonObject> {
|
|
809
|
-
switch (control.type) {
|
|
810
|
-
case "start_instance": {
|
|
811
|
-
const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
|
|
812
|
-
|
|
813
|
-
if (!cwd.ok) {
|
|
814
|
-
return {
|
|
815
|
-
instanceId,
|
|
816
|
-
controlType: control.type,
|
|
817
|
-
ok: false,
|
|
818
|
-
error: cwd.reason,
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
const response = await codex.request("thread/start", {
|
|
823
|
-
cwd: cwd.cwd,
|
|
824
|
-
serviceName: "kandan-local-runner",
|
|
825
|
-
personality: "pragmatic",
|
|
826
|
-
...(control.model === undefined ? {} : { model: control.model }),
|
|
827
|
-
...(control.reasoningEffort === undefined
|
|
828
|
-
? {}
|
|
829
|
-
: { reasoningEffort: control.reasoningEffort }),
|
|
830
|
-
...(control.approvalPolicy === undefined
|
|
831
|
-
? {}
|
|
832
|
-
: { approvalPolicy: control.approvalPolicy }),
|
|
833
|
-
...(control.sandbox === undefined ? {} : { sandbox: control.sandbox }),
|
|
834
|
-
...(control.fast === true ? { serviceTier: "fast" } : {}),
|
|
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
|
-
}
|
|
845
|
-
|
|
846
|
-
return {
|
|
847
|
-
instanceId,
|
|
848
|
-
controlType: control.type,
|
|
849
|
-
cwd: cwd.cwd,
|
|
850
|
-
matchedRoot: cwd.matchedRoot,
|
|
851
|
-
response: response as JsonObject,
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
case "start_turn": {
|
|
856
|
-
const response = await codex.request("turn/start", {
|
|
857
|
-
threadId: control.threadId,
|
|
858
|
-
input: control.input,
|
|
859
|
-
});
|
|
860
|
-
return {
|
|
861
|
-
instanceId,
|
|
862
|
-
controlType: control.type,
|
|
863
|
-
response: response as JsonObject,
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
case "steer_turn": {
|
|
868
|
-
const response = await codex.request("turn/steer", {
|
|
869
|
-
threadId: control.threadId,
|
|
870
|
-
turnId: control.turnId,
|
|
871
|
-
input: control.input,
|
|
872
|
-
});
|
|
873
|
-
return {
|
|
874
|
-
instanceId,
|
|
875
|
-
controlType: control.type,
|
|
876
|
-
response: response as JsonObject,
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
case "interrupt_turn": {
|
|
881
|
-
const response = await codex.request("turn/interrupt", {
|
|
882
|
-
threadId: control.threadId,
|
|
883
|
-
turnId: control.turnId ?? null,
|
|
884
|
-
});
|
|
885
|
-
return {
|
|
886
|
-
instanceId,
|
|
887
|
-
controlType: control.type,
|
|
888
|
-
response: response as JsonObject,
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
case "read_thread": {
|
|
893
|
-
const response = await codex.request("thread/read", {
|
|
894
|
-
threadId: control.threadId,
|
|
895
|
-
includeTurns: control.includeTurns ?? true,
|
|
896
|
-
});
|
|
897
|
-
return {
|
|
898
|
-
instanceId,
|
|
899
|
-
controlType: control.type,
|
|
900
|
-
response: response as JsonObject,
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
case "stop_instance":
|
|
905
|
-
case "kill_instance":
|
|
906
|
-
case "interrupt_queued_messages":
|
|
907
|
-
case "resolve_codex_approval_request":
|
|
908
|
-
case "forward_http_request":
|
|
909
|
-
case "forward_websocket_open":
|
|
910
|
-
case "forward_websocket_send":
|
|
911
|
-
case "forward_websocket_close":
|
|
912
|
-
case "start_local_editor":
|
|
913
|
-
case "update_runner_config":
|
|
914
|
-
return { instanceId, controlType: control.type, skipped: true };
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function isUpdateRunnerConfigControl(
|
|
919
|
-
control: KandanControl,
|
|
920
|
-
): control is Extract<
|
|
921
|
-
KandanControl,
|
|
922
|
-
{ readonly type: "update_runner_config" }
|
|
923
|
-
> {
|
|
924
|
-
return control.type === "update_runner_config";
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function normalizeAllowedCwds(values: readonly string[]): string[] {
|
|
928
|
-
return Array.from(
|
|
929
|
-
new Set(
|
|
930
|
-
values.flatMap((value) => {
|
|
931
|
-
const normalized = value.trim();
|
|
932
|
-
return normalized === "" ? [] : [normalized];
|
|
933
|
-
}),
|
|
934
|
-
),
|
|
935
|
-
);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
function allowedCwdSuggestions(
|
|
939
|
-
cwd: string,
|
|
940
|
-
allowedCwds: readonly string[],
|
|
941
|
-
): string[] {
|
|
942
|
-
return normalizeAllowedCwds([cwd, ...allowedCwds]);
|
|
943
|
-
}
|