@linzumi/cli 0.0.38-beta → 0.0.40-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 +1 -1
- package/dist/index.js +1695 -278
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
3
|
-
import { existsSync as existsSync10, readFileSync as readFileSync9, realpathSync as
|
|
3
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, realpathSync as realpathSync6 } from "node:fs";
|
|
4
4
|
import { homedir as homedir9 } from "node:os";
|
|
5
|
-
import { resolve as
|
|
5
|
+
import { resolve as resolve9 } from "node:path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7
7
|
|
|
8
8
|
// src/runner.ts
|
|
9
9
|
import { spawn as spawn6 } from "node:child_process";
|
|
10
10
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
11
|
+
import { realpathSync as realpathSync4 } from "node:fs";
|
|
11
12
|
import { hostname as hostname2 } from "node:os";
|
|
12
|
-
import { join as join6 } from "node:path";
|
|
13
|
+
import { join as join6, resolve as resolve5 } from "node:path";
|
|
13
14
|
|
|
14
15
|
// src/channelSessionSupport.ts
|
|
15
16
|
import { spawnSync } from "node:child_process";
|
|
@@ -149,19 +150,34 @@ function senderAllowed(listenUser, event, runnerIdentity) {
|
|
|
149
150
|
return runnerIdentity.actorUsername?.toLocaleLowerCase() === normalized && runnerIdentity.actorUserId !== undefined && event.actorUserId === runnerIdentity.actorUserId;
|
|
150
151
|
}
|
|
151
152
|
function identityFromAccessToken(token) {
|
|
153
|
+
const decoded = decodeAccessTokenPayload(token);
|
|
154
|
+
if (decoded === undefined) {
|
|
155
|
+
return { actorUserId: undefined, actorUsername: undefined };
|
|
156
|
+
}
|
|
157
|
+
const actorUserId = integerValue(decoded.actor_id) ?? integerValue(decoded.sub);
|
|
158
|
+
return {
|
|
159
|
+
actorUserId,
|
|
160
|
+
actorUsername: stringValue(decoded.actor_username)
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function singleWorkspaceScopeFromAccessToken(token) {
|
|
164
|
+
const decoded = decodeAccessTokenPayload(token);
|
|
165
|
+
const workspaceScope = (arrayValue(decoded?.workspace_scope) ?? []).flatMap((value) => {
|
|
166
|
+
const workspace = stringValue(value)?.trim();
|
|
167
|
+
return workspace === undefined || workspace === "" ? [] : [workspace];
|
|
168
|
+
});
|
|
169
|
+
return workspaceScope.length === 1 ? workspaceScope[0] : undefined;
|
|
170
|
+
}
|
|
171
|
+
function decodeAccessTokenPayload(token) {
|
|
152
172
|
const [, payload] = token.split(".");
|
|
153
173
|
if (payload === undefined) {
|
|
154
|
-
return
|
|
174
|
+
return;
|
|
155
175
|
}
|
|
156
176
|
try {
|
|
157
177
|
const decoded = JSON.parse(Buffer.from(base64UrlToBase64(payload), "base64").toString("utf8"));
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
actorUserId,
|
|
161
|
-
actorUsername: stringValue(decoded.actor_username)
|
|
162
|
-
};
|
|
178
|
+
return isJsonObject(decoded) ? decoded : undefined;
|
|
163
179
|
} catch (_error) {
|
|
164
|
-
return
|
|
180
|
+
return;
|
|
165
181
|
}
|
|
166
182
|
}
|
|
167
183
|
function detectCodexVersion(codexBin, cwd) {
|
|
@@ -801,6 +817,203 @@ function fuseQueuedMessages(selected) {
|
|
|
801
817
|
};
|
|
802
818
|
}
|
|
803
819
|
|
|
820
|
+
// src/reconnectContext.ts
|
|
821
|
+
var recentVerbatimMessageCount = 5;
|
|
822
|
+
var reconnectContextModel = "openai/gpt-oss-120b";
|
|
823
|
+
var noiseLocalRunnerEventTypes = new Set([
|
|
824
|
+
"availability",
|
|
825
|
+
"port_forward_requested",
|
|
826
|
+
"port_forward_ready",
|
|
827
|
+
"port_forward_resolved",
|
|
828
|
+
"settings",
|
|
829
|
+
"status",
|
|
830
|
+
"local_runner_config_seeded",
|
|
831
|
+
"local_runner_config_updated"
|
|
832
|
+
]);
|
|
833
|
+
function parseReconnectContextMessages(value) {
|
|
834
|
+
if (!Array.isArray(value)) {
|
|
835
|
+
return [];
|
|
836
|
+
}
|
|
837
|
+
return value.flatMap(messageFromWirePayload);
|
|
838
|
+
}
|
|
839
|
+
function filterReconnectContextMessages(messages) {
|
|
840
|
+
return messages.filter((message) => {
|
|
841
|
+
const trimmed = message.body.trim();
|
|
842
|
+
if (trimmed === "") {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
if (message.actorKind === "system") {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
const eventType = localRunnerEventType2(message.payload);
|
|
849
|
+
if (eventType !== undefined && eventType !== "codex_output") {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
if (looksLikeLegacyRunnerNoise(message)) {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
return true;
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
async function buildReconnectContextInjection(messages, summarizer = createConfiguredReconnectContextSummarizer()) {
|
|
859
|
+
const filtered = filterReconnectContextMessages(messages);
|
|
860
|
+
if (filtered.length === 0) {
|
|
861
|
+
throw new Error("durable Linzumi thread history did not include user-visible context");
|
|
862
|
+
}
|
|
863
|
+
const older = filtered.slice(0, Math.max(0, filtered.length - recentVerbatimMessageCount));
|
|
864
|
+
const recent = filtered.slice(-recentVerbatimMessageCount);
|
|
865
|
+
const summary = older.length === 0 ? undefined : await summarizeOlderReconnectContext(older, summarizer);
|
|
866
|
+
const sections = [
|
|
867
|
+
"Reconnected Linzumi thread context",
|
|
868
|
+
"",
|
|
869
|
+
"The local Codex app-server thread was restarted, so this durable Linzumi thread history is being injected before retrying the latest user message.",
|
|
870
|
+
"",
|
|
871
|
+
...summary === undefined ? [] : ["Summary of earlier messages:", summary, ""],
|
|
872
|
+
"Last five user-visible messages verbatim:",
|
|
873
|
+
recent.map(formatReconnectContextMessage).join(`
|
|
874
|
+
|
|
875
|
+
---
|
|
876
|
+
|
|
877
|
+
`)
|
|
878
|
+
];
|
|
879
|
+
return { type: "text", text: sections.join(`
|
|
880
|
+
`) };
|
|
881
|
+
}
|
|
882
|
+
function createConfiguredReconnectContextSummarizer(fetchImpl = globalThis.fetch) {
|
|
883
|
+
return {
|
|
884
|
+
summarize: (messages) => summarizeWithOpenAiCompatibleProvider(fetchImpl, messages)
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
async function summarizeOlderReconnectContext(messages, summarizer) {
|
|
888
|
+
const summary = (await summarizer.summarize(messages)).trim();
|
|
889
|
+
if (summary === "") {
|
|
890
|
+
throw new Error("reconnect context summarizer returned an empty summary");
|
|
891
|
+
}
|
|
892
|
+
return summary;
|
|
893
|
+
}
|
|
894
|
+
async function summarizeWithOpenAiCompatibleProvider(fetchImpl, messages) {
|
|
895
|
+
const provider = configuredReconnectContextProvider();
|
|
896
|
+
if (provider === undefined) {
|
|
897
|
+
throw new Error("Reconnect context summarization requires LINZUMI_RECONNECT_CONTEXT_API_KEY, GROQ_API_KEY, or OPENROUTER_API_KEY");
|
|
898
|
+
}
|
|
899
|
+
const model = envString("LINZUMI_RECONNECT_CONTEXT_MODEL") ?? envString("KANDAN_AGENT_PROGRESS_SUMMARY_MODEL") ?? reconnectContextModel;
|
|
900
|
+
const response = await fetchImpl(`${provider.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
901
|
+
method: "POST",
|
|
902
|
+
headers: {
|
|
903
|
+
authorization: `Bearer ${provider.apiKey}`,
|
|
904
|
+
"content-type": "application/json"
|
|
905
|
+
},
|
|
906
|
+
body: JSON.stringify({
|
|
907
|
+
model,
|
|
908
|
+
temperature: 0,
|
|
909
|
+
messages: [
|
|
910
|
+
{
|
|
911
|
+
role: "system",
|
|
912
|
+
content: "Summarize durable Linzumi thread history for a restarted local Codex coding agent. Preserve concrete user requests, constraints, file/module names, and decisions. Do not mention local runner availability, reconnect status, or port-forward noise."
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
role: "user",
|
|
916
|
+
content: messages.map(formatReconnectContextMessage).join(`
|
|
917
|
+
|
|
918
|
+
---
|
|
919
|
+
|
|
920
|
+
`)
|
|
921
|
+
}
|
|
922
|
+
]
|
|
923
|
+
})
|
|
924
|
+
});
|
|
925
|
+
if (!response.ok) {
|
|
926
|
+
throw new Error(`reconnect context summarization failed: ${response.status} ${response.statusText}`);
|
|
927
|
+
}
|
|
928
|
+
const parsed = await response.json();
|
|
929
|
+
if (!isJsonObject(parsed)) {
|
|
930
|
+
throw new Error("reconnect context summarizer returned non-object JSON");
|
|
931
|
+
}
|
|
932
|
+
const choices = parsed.choices;
|
|
933
|
+
if (!Array.isArray(choices)) {
|
|
934
|
+
throw new Error("reconnect context summarizer response did not include choices");
|
|
935
|
+
}
|
|
936
|
+
const first = choices.find(isJsonObject);
|
|
937
|
+
const message = objectValue(first?.message);
|
|
938
|
+
const content = stringValue(message?.content)?.trim();
|
|
939
|
+
if (content === undefined || content === "") {
|
|
940
|
+
throw new Error("reconnect context summarizer response did not include message content");
|
|
941
|
+
}
|
|
942
|
+
return content;
|
|
943
|
+
}
|
|
944
|
+
function configuredReconnectContextProvider() {
|
|
945
|
+
const linzumiApiKey = envString("LINZUMI_RECONNECT_CONTEXT_API_KEY");
|
|
946
|
+
if (linzumiApiKey !== undefined) {
|
|
947
|
+
return {
|
|
948
|
+
apiKey: linzumiApiKey,
|
|
949
|
+
baseUrl: envString("LINZUMI_RECONNECT_CONTEXT_BASE_URL") ?? "https://api.groq.com/openai/v1"
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
const groqApiKey = envString("GROQ_API_KEY");
|
|
953
|
+
if (groqApiKey !== undefined) {
|
|
954
|
+
return {
|
|
955
|
+
apiKey: groqApiKey,
|
|
956
|
+
baseUrl: envString("GROQ_BASE_URL") ?? "https://api.groq.com/openai/v1"
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const openRouterApiKey = envString("OPENROUTER_API_KEY");
|
|
960
|
+
if (openRouterApiKey !== undefined) {
|
|
961
|
+
return {
|
|
962
|
+
apiKey: openRouterApiKey,
|
|
963
|
+
baseUrl: envString("OPENROUTER_BASE_URL") ?? "https://openrouter.ai/api/v1"
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
function messageFromWirePayload(value) {
|
|
969
|
+
if (!isJsonObject(value)) {
|
|
970
|
+
return [];
|
|
971
|
+
}
|
|
972
|
+
const seq = integerValue(value.seq);
|
|
973
|
+
const payload = objectValue(value.payload) ?? {};
|
|
974
|
+
const body = stringValue(value.body) ?? stringValue(payload.body) ?? stringValue(payload.text);
|
|
975
|
+
const type = stringValue(value.type) ?? "thread.message";
|
|
976
|
+
if (seq === undefined || body === undefined) {
|
|
977
|
+
return [];
|
|
978
|
+
}
|
|
979
|
+
const actor = objectValue(value.actor) ?? {};
|
|
980
|
+
return [
|
|
981
|
+
{
|
|
982
|
+
seq,
|
|
983
|
+
type,
|
|
984
|
+
actorKind: stringValue(actor.kind) ?? "unknown",
|
|
985
|
+
actorSlug: stringValue(actor.slug),
|
|
986
|
+
actorUserId: integerValue(value.actor_user_id),
|
|
987
|
+
body,
|
|
988
|
+
payload
|
|
989
|
+
}
|
|
990
|
+
];
|
|
991
|
+
}
|
|
992
|
+
function formatReconnectContextMessage(message) {
|
|
993
|
+
const sender = message.actorSlug?.trim() === "" || message.actorSlug === undefined ? message.actorKind : message.actorSlug;
|
|
994
|
+
const userId = message.actorUserId === undefined ? "unknown" : message.actorUserId.toString();
|
|
995
|
+
return [
|
|
996
|
+
`Linzumi durable message seq=${message.seq} from ${sender} (kind=${message.actorKind}, user_id=${userId}):`,
|
|
997
|
+
"",
|
|
998
|
+
message.body
|
|
999
|
+
].join(`
|
|
1000
|
+
`);
|
|
1001
|
+
}
|
|
1002
|
+
function localRunnerEventType2(payload) {
|
|
1003
|
+
const metadata = objectValue(payload.metadata);
|
|
1004
|
+
const localRunner = objectValue(metadata?.local_codex_runner);
|
|
1005
|
+
const eventType = stringValue(localRunner?.event_type);
|
|
1006
|
+
return eventType === undefined || eventType.trim() === "" ? undefined : eventType.trim();
|
|
1007
|
+
}
|
|
1008
|
+
function looksLikeLegacyRunnerNoise(message) {
|
|
1009
|
+
const body = message.body.trim();
|
|
1010
|
+
return body.startsWith("Codex reconnected.") || body.startsWith("[codex reconnected") || body.startsWith("Codex is ready.") || /^Runner port \d+ is open in (Kandan|Linzumi):/.test(body) || /^.+ on port \d+ is ready in Linzumi\. \[Open\]\(/.test(body);
|
|
1011
|
+
}
|
|
1012
|
+
function envString(name) {
|
|
1013
|
+
const value = process.env[name];
|
|
1014
|
+
return value === undefined || value.trim() === "" ? undefined : value.trim();
|
|
1015
|
+
}
|
|
1016
|
+
|
|
804
1017
|
// src/localCodexMessageState.ts
|
|
805
1018
|
function codexApprovalMessageState(request) {
|
|
806
1019
|
const params = objectValue(request.params) ?? {};
|
|
@@ -843,7 +1056,11 @@ function processingReasonForCodexNotification(method, params) {
|
|
|
843
1056
|
function processingMessageStateFromActive(state) {
|
|
844
1057
|
switch (state.reason) {
|
|
845
1058
|
case "awaiting approval":
|
|
846
|
-
return {
|
|
1059
|
+
return {
|
|
1060
|
+
status: "processing",
|
|
1061
|
+
reason: state.reason,
|
|
1062
|
+
approval: state.approval
|
|
1063
|
+
};
|
|
847
1064
|
case "starting turn":
|
|
848
1065
|
case "streaming response":
|
|
849
1066
|
case "running terminal command":
|
|
@@ -1151,14 +1368,17 @@ var defaultIntervalMs = 2000;
|
|
|
1151
1368
|
var defaultDebounceMs = 750;
|
|
1152
1369
|
function startPortForwardWatcher(options) {
|
|
1153
1370
|
const rootPid = options.rootPid ?? process.pid;
|
|
1371
|
+
const rootCwd = normalizeCwd(options.rootCwd);
|
|
1154
1372
|
const intervalMs = options.intervalMs ?? defaultIntervalMs;
|
|
1155
1373
|
const debounceMs = options.debounceMs ?? defaultDebounceMs;
|
|
1374
|
+
const lostDebounceMs = options.lostDebounceMs ?? intervalMs * 2 + debounceMs;
|
|
1156
1375
|
const scanProcesses = options.scanProcesses ?? readProcessRows;
|
|
1157
1376
|
const scanListenSockets = options.scanListenSockets ?? readListenSocketRows;
|
|
1158
1377
|
const scanProcessCwds = options.scanProcessCwds ?? readProcessCwdRows;
|
|
1159
1378
|
const nowMs = options.nowMs ?? Date.now;
|
|
1160
1379
|
const candidateStabilityByPort = new Map;
|
|
1161
1380
|
const emittedByPort = new Map;
|
|
1381
|
+
const missingByPort = new Map;
|
|
1162
1382
|
const inFlight = { value: false };
|
|
1163
1383
|
const scan = () => {
|
|
1164
1384
|
if (inFlight.value) {
|
|
@@ -1168,18 +1388,26 @@ function startPortForwardWatcher(options) {
|
|
|
1168
1388
|
Promise.resolve().then(async () => {
|
|
1169
1389
|
const descendants = descendantPidSet(scanProcesses(), rootPid);
|
|
1170
1390
|
const sockets = scanListenSockets();
|
|
1171
|
-
const candidatePids = sockets.filter((socket) => descendants.has(socket.pid)).map((socket) => socket.pid);
|
|
1172
|
-
const candidates = detectedForwardCandidates(sockets, descendants, scanProcessCwds(candidatePids));
|
|
1173
|
-
const
|
|
1174
|
-
const
|
|
1391
|
+
const candidatePids = rootCwd === undefined ? sockets.filter((socket) => descendants.has(socket.pid)).map((socket) => socket.pid) : sockets.map((socket) => socket.pid);
|
|
1392
|
+
const candidates = detectedForwardCandidates(sockets, descendants, scanProcessCwds(candidatePids), { rootCwd });
|
|
1393
|
+
const scanTimeMs = nowMs();
|
|
1394
|
+
const stable = stableForwardCandidates(candidateStabilityByPort, candidates, scanTimeMs, debounceMs);
|
|
1395
|
+
const changes = debouncedForwardCandidateChanges(emittedByPort, missingByPort, stable.stableCandidates, scanTimeMs, lostDebounceMs);
|
|
1175
1396
|
candidateStabilityByPort.clear();
|
|
1176
1397
|
emittedByPort.clear();
|
|
1398
|
+
missingByPort.clear();
|
|
1177
1399
|
for (const [port, observed] of stable.nextObservedByPort) {
|
|
1178
1400
|
candidateStabilityByPort.set(port, observed);
|
|
1179
1401
|
}
|
|
1180
1402
|
for (const [port, candidate] of changes.nextObservedByPort) {
|
|
1181
1403
|
emittedByPort.set(port, candidate);
|
|
1182
1404
|
}
|
|
1405
|
+
for (const [port, missing] of changes.nextMissingByPort) {
|
|
1406
|
+
missingByPort.set(port, missing);
|
|
1407
|
+
}
|
|
1408
|
+
for (const candidate of changes.lostCandidates) {
|
|
1409
|
+
await options.onCandidateLost?.(candidate);
|
|
1410
|
+
}
|
|
1183
1411
|
for (const candidate of changes.changedCandidates) {
|
|
1184
1412
|
await options.onCandidate(candidate);
|
|
1185
1413
|
}
|
|
@@ -1195,9 +1423,11 @@ function startPortForwardWatcher(options) {
|
|
|
1195
1423
|
close: () => clearInterval(interval)
|
|
1196
1424
|
};
|
|
1197
1425
|
}
|
|
1198
|
-
function
|
|
1426
|
+
function debouncedForwardCandidateChanges(previousObservedByPort, previousMissingByPort, candidates, nowMs, lostDebounceMs) {
|
|
1199
1427
|
const nextObservedByPort = new Map;
|
|
1428
|
+
const nextMissingByPort = new Map;
|
|
1200
1429
|
const changedCandidates = [];
|
|
1430
|
+
const lostCandidates = [];
|
|
1201
1431
|
for (const candidate of candidates) {
|
|
1202
1432
|
nextObservedByPort.set(candidate.port, candidate);
|
|
1203
1433
|
const previous = previousObservedByPort.get(candidate.port);
|
|
@@ -1205,7 +1435,28 @@ function changedForwardCandidates(previousObservedByPort, candidates) {
|
|
|
1205
1435
|
changedCandidates.push(candidate);
|
|
1206
1436
|
}
|
|
1207
1437
|
}
|
|
1208
|
-
|
|
1438
|
+
for (const previous of previousObservedByPort.values()) {
|
|
1439
|
+
if (nextObservedByPort.has(previous.port)) {
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
const previousMissing = previousMissingByPort.get(previous.port);
|
|
1443
|
+
const firstMissingAtMs = previousMissing !== undefined && sameForwardCandidate(previousMissing.candidate, previous) ? previousMissing.firstMissingAtMs : nowMs;
|
|
1444
|
+
if (lostDebounceMs <= 0 || nowMs - firstMissingAtMs >= lostDebounceMs) {
|
|
1445
|
+
lostCandidates.push(previous);
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
nextObservedByPort.set(previous.port, previous);
|
|
1449
|
+
nextMissingByPort.set(previous.port, {
|
|
1450
|
+
candidate: previous,
|
|
1451
|
+
firstMissingAtMs
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
return {
|
|
1455
|
+
nextObservedByPort,
|
|
1456
|
+
nextMissingByPort,
|
|
1457
|
+
changedCandidates,
|
|
1458
|
+
lostCandidates
|
|
1459
|
+
};
|
|
1209
1460
|
}
|
|
1210
1461
|
function stableForwardCandidates(previousObservedByPort, candidates, nowMs, debounceMs) {
|
|
1211
1462
|
const nextObservedByPort = new Map;
|
|
@@ -1242,8 +1493,17 @@ function descendantPidSet(rows, rootPid) {
|
|
|
1242
1493
|
}
|
|
1243
1494
|
return descendants;
|
|
1244
1495
|
}
|
|
1245
|
-
function detectedForwardCandidates(sockets, descendantPids, processCwds = new Map) {
|
|
1246
|
-
|
|
1496
|
+
function detectedForwardCandidates(sockets, descendantPids, processCwds = new Map, options = {}) {
|
|
1497
|
+
const rootCwd = normalizeCwd(options.rootCwd);
|
|
1498
|
+
return sockets.filter((socket) => {
|
|
1499
|
+
if (descendantPids.has(socket.pid)) {
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
if (rootCwd === undefined) {
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
return cwdMatchesRoot(processCwds.get(socket.pid), rootCwd);
|
|
1506
|
+
}).filter((socket) => socket.port > 0 && socket.port < 65536).sort((left, right) => left.port - right.port).map((socket) => {
|
|
1247
1507
|
const cwd = processCwds.get(socket.pid);
|
|
1248
1508
|
return {
|
|
1249
1509
|
port: socket.port,
|
|
@@ -1253,6 +1513,20 @@ function detectedForwardCandidates(sockets, descendantPids, processCwds = new Ma
|
|
|
1253
1513
|
};
|
|
1254
1514
|
});
|
|
1255
1515
|
}
|
|
1516
|
+
function normalizeCwd(cwd) {
|
|
1517
|
+
if (cwd === undefined) {
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const normalized = cwd.trim().replace(/\/+$/, "");
|
|
1521
|
+
return normalized === "" ? undefined : normalized;
|
|
1522
|
+
}
|
|
1523
|
+
function cwdMatchesRoot(candidateCwd, rootCwd) {
|
|
1524
|
+
const normalizedCandidate = normalizeCwd(candidateCwd);
|
|
1525
|
+
if (normalizedCandidate === undefined) {
|
|
1526
|
+
return false;
|
|
1527
|
+
}
|
|
1528
|
+
return normalizedCandidate === rootCwd || normalizedCandidate.startsWith(`${rootCwd}/`);
|
|
1529
|
+
}
|
|
1256
1530
|
function parseProcessRows(output) {
|
|
1257
1531
|
return output.split(`
|
|
1258
1532
|
`).map((line) => line.trim()).filter((line) => line !== "").map((line) => {
|
|
@@ -1329,7 +1603,9 @@ function readProcessRows() {
|
|
|
1329
1603
|
return parseProcessRows(result.stdout);
|
|
1330
1604
|
}
|
|
1331
1605
|
function readListenSocketRows() {
|
|
1332
|
-
const result = spawnSync2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPcn"], {
|
|
1606
|
+
const result = spawnSync2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPcn"], {
|
|
1607
|
+
encoding: "utf8"
|
|
1608
|
+
});
|
|
1333
1609
|
if (result.error !== undefined) {
|
|
1334
1610
|
throw result.error;
|
|
1335
1611
|
}
|
|
@@ -1430,22 +1706,6 @@ function approvedTargetFromRequest(request) {
|
|
|
1430
1706
|
function portForwardPromptLabel(candidate) {
|
|
1431
1707
|
return commandLabel(candidate.command);
|
|
1432
1708
|
}
|
|
1433
|
-
function portForwardPromptBody(candidate, requestId) {
|
|
1434
|
-
const label = portForwardPromptLabel(candidate);
|
|
1435
|
-
const cwdLine = candidate.cwd === undefined ? undefined : `Working directory: ${candidate.cwd}`;
|
|
1436
|
-
return [
|
|
1437
|
-
`Detected ${label} listening from a descendant process.`,
|
|
1438
|
-
`Port: ${candidate.port}`,
|
|
1439
|
-
`PID: ${candidate.pid}`,
|
|
1440
|
-
`Command: ${candidate.command}`,
|
|
1441
|
-
...cwdLine === undefined ? [] : [cwdLine],
|
|
1442
|
-
"Kandan can open this as an authenticated HTTP, HTTPS, or WebSocket preview. It does not expose arbitrary TCP or UDP protocols.",
|
|
1443
|
-
"Open this preview in Kandan?",
|
|
1444
|
-
"",
|
|
1445
|
-
`Fallback commands: ${portForwardDecisionCommand("approve", requestId)} or ${portForwardDecisionCommand("deny", requestId)}`
|
|
1446
|
-
].join(`
|
|
1447
|
-
`);
|
|
1448
|
-
}
|
|
1449
1709
|
function portForwardPromptReason(candidate) {
|
|
1450
1710
|
return [
|
|
1451
1711
|
`Port ${candidate.port}`,
|
|
@@ -1455,15 +1715,16 @@ function portForwardPromptReason(candidate) {
|
|
|
1455
1715
|
"preview protocols: HTTP, HTTPS, WebSocket"
|
|
1456
1716
|
].join(" / ");
|
|
1457
1717
|
}
|
|
1458
|
-
function portForwardDecisionCommand(decision, requestId) {
|
|
1459
|
-
return `/kandan ${decision}-port-forward ${requestId}`;
|
|
1460
|
-
}
|
|
1461
1718
|
function forwardPreviewPath(runnerId, port) {
|
|
1462
1719
|
return `/local-codex-runners/${encodeURIComponent(runnerId)}/forwards/${port}/preview`;
|
|
1463
1720
|
}
|
|
1464
1721
|
function revocationCapabilities(capabilities, port) {
|
|
1722
|
+
const existingAllowedPorts = Array.isArray(capabilities?.allowedPorts) ? capabilities.allowedPorts.filter((value) => typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535) : [];
|
|
1723
|
+
const allowedPorts = existingAllowedPorts.filter((allowedPort) => allowedPort !== port);
|
|
1465
1724
|
return {
|
|
1466
1725
|
...capabilities ?? {},
|
|
1726
|
+
portForwarding: allowedPorts.length > 0,
|
|
1727
|
+
allowedPorts,
|
|
1467
1728
|
revokedPorts: [port]
|
|
1468
1729
|
};
|
|
1469
1730
|
}
|
|
@@ -1474,6 +1735,608 @@ function isInternalCodexProcess(candidate) {
|
|
|
1474
1735
|
return commandLabel(candidate.command) === "codex";
|
|
1475
1736
|
}
|
|
1476
1737
|
|
|
1738
|
+
// src/processNameCatalog.ts
|
|
1739
|
+
var processCatalogEntries = [
|
|
1740
|
+
{ appName: "Bun", iconKey: "bun", names: ["bun", "bunx"] },
|
|
1741
|
+
{
|
|
1742
|
+
appName: "Node.js",
|
|
1743
|
+
iconKey: "nodejs",
|
|
1744
|
+
names: ["node", "nodejs", "node.exe"]
|
|
1745
|
+
},
|
|
1746
|
+
{ appName: "npm", iconKey: "npm", names: ["npm", "npm-cli.js", "npx"] },
|
|
1747
|
+
{ appName: "pnpm", iconKey: "pnpm", names: ["pnpm", "pnpx"] },
|
|
1748
|
+
{ appName: "Yarn", iconKey: "yarn", names: ["yarn", "yarnpkg", "yarn.js"] },
|
|
1749
|
+
{ appName: "Corepack", iconKey: "corepack", names: ["corepack"] },
|
|
1750
|
+
{ appName: "Deno", iconKey: "deno", names: ["deno"] },
|
|
1751
|
+
{ appName: "Vite", iconKey: "vite", names: ["vite", "vite-node"] },
|
|
1752
|
+
{ appName: "Vitest", iconKey: "vitest", names: ["vitest"] },
|
|
1753
|
+
{ appName: "Next.js", iconKey: "nextjs", names: ["next", "next-server"] },
|
|
1754
|
+
{ appName: "Nuxt", iconKey: "nuxt", names: ["nuxt", "nuxi"] },
|
|
1755
|
+
{ appName: "Astro", iconKey: "astro", names: ["astro"] },
|
|
1756
|
+
{
|
|
1757
|
+
appName: "SvelteKit",
|
|
1758
|
+
iconKey: "svelte",
|
|
1759
|
+
names: ["svelte-kit", "sveltekit"]
|
|
1760
|
+
},
|
|
1761
|
+
{ appName: "Remix", iconKey: "remix", names: ["remix"] },
|
|
1762
|
+
{ appName: "Expo", iconKey: "expo", names: ["expo", "expo-cli"] },
|
|
1763
|
+
{ appName: "React Native", iconKey: "react", names: ["react-native"] },
|
|
1764
|
+
{
|
|
1765
|
+
appName: "Metro",
|
|
1766
|
+
iconKey: "metro",
|
|
1767
|
+
names: ["metro", "metro-inspector-proxy"]
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
appName: "Webpack",
|
|
1771
|
+
iconKey: "webpack",
|
|
1772
|
+
names: ["webpack", "webpack-dev-server", "webpack-cli"]
|
|
1773
|
+
},
|
|
1774
|
+
{ appName: "Parcel", iconKey: "parcel", names: ["parcel"] },
|
|
1775
|
+
{ appName: "Rollup", iconKey: "rollup", names: ["rollup"] },
|
|
1776
|
+
{ appName: "esbuild", iconKey: "esbuild", names: ["esbuild"] },
|
|
1777
|
+
{ appName: "SWC", iconKey: "swc", names: ["swc"] },
|
|
1778
|
+
{
|
|
1779
|
+
appName: "TypeScript",
|
|
1780
|
+
iconKey: "typescript",
|
|
1781
|
+
names: ["tsc", "tsserver", "tsx", "ts-node", "typescript-language-server"]
|
|
1782
|
+
},
|
|
1783
|
+
{ appName: "ESLint", iconKey: "eslint", names: ["eslint", "eslint_d"] },
|
|
1784
|
+
{ appName: "Prettier", iconKey: "prettier", names: ["prettier"] },
|
|
1785
|
+
{ appName: "Biome", iconKey: "biome", names: ["biome"] },
|
|
1786
|
+
{
|
|
1787
|
+
appName: "Storybook",
|
|
1788
|
+
iconKey: "storybook",
|
|
1789
|
+
names: ["storybook", "start-storybook"]
|
|
1790
|
+
},
|
|
1791
|
+
{ appName: "Playwright", iconKey: "playwright", names: ["playwright"] },
|
|
1792
|
+
{ appName: "Cypress", iconKey: "cypress", names: ["cypress"] },
|
|
1793
|
+
{ appName: "Jest", iconKey: "jest", names: ["jest"] },
|
|
1794
|
+
{
|
|
1795
|
+
appName: "Python",
|
|
1796
|
+
iconKey: "python",
|
|
1797
|
+
names: [
|
|
1798
|
+
"python",
|
|
1799
|
+
"python2",
|
|
1800
|
+
"python3",
|
|
1801
|
+
"python3.9",
|
|
1802
|
+
"python3.10",
|
|
1803
|
+
"python3.11",
|
|
1804
|
+
"python3.12",
|
|
1805
|
+
"python3.13",
|
|
1806
|
+
"pypy",
|
|
1807
|
+
"pypy3"
|
|
1808
|
+
]
|
|
1809
|
+
},
|
|
1810
|
+
{ appName: "pip", iconKey: "python", names: ["pip", "pip2", "pip3"] },
|
|
1811
|
+
{ appName: "pipx", iconKey: "python", names: ["pipx"] },
|
|
1812
|
+
{ appName: "uv", iconKey: "python", names: ["uv", "uvx"] },
|
|
1813
|
+
{ appName: "Poetry", iconKey: "poetry", names: ["poetry"] },
|
|
1814
|
+
{ appName: "Pipenv", iconKey: "python", names: ["pipenv"] },
|
|
1815
|
+
{
|
|
1816
|
+
appName: "Conda",
|
|
1817
|
+
iconKey: "conda",
|
|
1818
|
+
names: ["conda", "mamba", "micromamba"]
|
|
1819
|
+
},
|
|
1820
|
+
{
|
|
1821
|
+
appName: "Django",
|
|
1822
|
+
iconKey: "django",
|
|
1823
|
+
names: ["django-admin", "django-admin.py", "manage.py"]
|
|
1824
|
+
},
|
|
1825
|
+
{ appName: "Flask", iconKey: "flask", names: ["flask"] },
|
|
1826
|
+
{ appName: "Uvicorn", iconKey: "uvicorn", names: ["uvicorn"] },
|
|
1827
|
+
{ appName: "Gunicorn", iconKey: "gunicorn", names: ["gunicorn"] },
|
|
1828
|
+
{ appName: "Hypercorn", iconKey: "python", names: ["hypercorn"] },
|
|
1829
|
+
{ appName: "Celery", iconKey: "celery", names: ["celery"] },
|
|
1830
|
+
{
|
|
1831
|
+
appName: "Jupyter",
|
|
1832
|
+
iconKey: "jupyter",
|
|
1833
|
+
names: ["jupyter", "jupyter-lab", "jupyter-notebook", "ipython"]
|
|
1834
|
+
},
|
|
1835
|
+
{ appName: "Ruby", iconKey: "ruby", names: ["ruby", "erb", "irb"] },
|
|
1836
|
+
{ appName: "Bundler", iconKey: "ruby", names: ["bundle", "bundler"] },
|
|
1837
|
+
{ appName: "Rails", iconKey: "rails", names: ["rails", "bin/rails"] },
|
|
1838
|
+
{ appName: "Rack", iconKey: "ruby", names: ["rackup"] },
|
|
1839
|
+
{ appName: "Puma", iconKey: "puma", names: ["puma"] },
|
|
1840
|
+
{ appName: "Sidekiq", iconKey: "sidekiq", names: ["sidekiq"] },
|
|
1841
|
+
{ appName: "RSpec", iconKey: "ruby", names: ["rspec"] },
|
|
1842
|
+
{ appName: "Go", iconKey: "go", names: ["go", "gofmt", "gopls", "air"] },
|
|
1843
|
+
{ appName: "Delve", iconKey: "go", names: ["dlv"] },
|
|
1844
|
+
{
|
|
1845
|
+
appName: "Rust",
|
|
1846
|
+
iconKey: "rust",
|
|
1847
|
+
names: ["rustc", "rustup", "cargo", "rust-analyzer"]
|
|
1848
|
+
},
|
|
1849
|
+
{
|
|
1850
|
+
appName: "Java",
|
|
1851
|
+
iconKey: "java",
|
|
1852
|
+
names: ["java", "javac", "jar", "jshell", "jwebserver"]
|
|
1853
|
+
},
|
|
1854
|
+
{ appName: "Gradle", iconKey: "gradle", names: ["gradle", "gradlew"] },
|
|
1855
|
+
{ appName: "Maven", iconKey: "maven", names: ["mvn", "mvnw"] },
|
|
1856
|
+
{
|
|
1857
|
+
appName: "Spring Boot",
|
|
1858
|
+
iconKey: "spring",
|
|
1859
|
+
names: ["spring", "spring-boot"]
|
|
1860
|
+
},
|
|
1861
|
+
{
|
|
1862
|
+
appName: "Kotlin",
|
|
1863
|
+
iconKey: "kotlin",
|
|
1864
|
+
names: ["kotlin", "kotlinc", "kotlin-language-server"]
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
appName: "Scala",
|
|
1868
|
+
iconKey: "scala",
|
|
1869
|
+
names: ["scala", "scalac", "sbt", "metals"]
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
appName: "Clojure",
|
|
1873
|
+
iconKey: "clojure",
|
|
1874
|
+
names: ["clojure", "clj", "lein", "boot"]
|
|
1875
|
+
},
|
|
1876
|
+
{
|
|
1877
|
+
appName: "PHP",
|
|
1878
|
+
iconKey: "php",
|
|
1879
|
+
names: ["php", "php-fpm", "php-cgi", "composer"]
|
|
1880
|
+
},
|
|
1881
|
+
{ appName: "Laravel", iconKey: "laravel", names: ["artisan", "laravel"] },
|
|
1882
|
+
{ appName: "Symfony", iconKey: "symfony", names: ["symfony"] },
|
|
1883
|
+
{ appName: "Elixir", iconKey: "elixir", names: ["elixir", "iex", "mix"] },
|
|
1884
|
+
{
|
|
1885
|
+
appName: "Erlang",
|
|
1886
|
+
iconKey: "erlang",
|
|
1887
|
+
names: ["erl", "beam.smp", "beam", "epmd", "rebar3"]
|
|
1888
|
+
},
|
|
1889
|
+
{ appName: "Phoenix", iconKey: "phoenix", names: ["phx.server", "phoenix"] },
|
|
1890
|
+
{
|
|
1891
|
+
appName: "PostgreSQL",
|
|
1892
|
+
iconKey: "postgresql",
|
|
1893
|
+
names: ["postgres", "postmaster", "pg_ctl", "psql", "pg_isready"]
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
appName: "Redis",
|
|
1897
|
+
iconKey: "redis",
|
|
1898
|
+
names: ["redis-server", "redis-cli", "redis-sentinel"]
|
|
1899
|
+
},
|
|
1900
|
+
{
|
|
1901
|
+
appName: "MySQL",
|
|
1902
|
+
iconKey: "mysql",
|
|
1903
|
+
names: ["mysql", "mysqld", "mysqladmin"]
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
appName: "MariaDB",
|
|
1907
|
+
iconKey: "mariadb",
|
|
1908
|
+
names: ["mariadb", "mariadbd", "mariadb-admin"]
|
|
1909
|
+
},
|
|
1910
|
+
{ appName: "SQLite", iconKey: "sqlite", names: ["sqlite3", "sqlite"] },
|
|
1911
|
+
{
|
|
1912
|
+
appName: "MongoDB",
|
|
1913
|
+
iconKey: "mongodb",
|
|
1914
|
+
names: ["mongod", "mongosh", "mongo"]
|
|
1915
|
+
},
|
|
1916
|
+
{
|
|
1917
|
+
appName: "Elasticsearch",
|
|
1918
|
+
iconKey: "elasticsearch",
|
|
1919
|
+
names: ["elasticsearch"]
|
|
1920
|
+
},
|
|
1921
|
+
{ appName: "OpenSearch", iconKey: "opensearch", names: ["opensearch"] },
|
|
1922
|
+
{ appName: "Solr", iconKey: "solr", names: ["solr"] },
|
|
1923
|
+
{
|
|
1924
|
+
appName: "Kafka",
|
|
1925
|
+
iconKey: "kafka",
|
|
1926
|
+
names: [
|
|
1927
|
+
"kafka-server-start",
|
|
1928
|
+
"kafka-console-consumer",
|
|
1929
|
+
"kafka-console-producer"
|
|
1930
|
+
]
|
|
1931
|
+
},
|
|
1932
|
+
{
|
|
1933
|
+
appName: "ZooKeeper",
|
|
1934
|
+
iconKey: "zookeeper",
|
|
1935
|
+
names: ["zookeeper-server-start", "zkserver"]
|
|
1936
|
+
},
|
|
1937
|
+
{
|
|
1938
|
+
appName: "RabbitMQ",
|
|
1939
|
+
iconKey: "rabbitmq",
|
|
1940
|
+
names: ["rabbitmq-server", "rabbitmqctl"]
|
|
1941
|
+
},
|
|
1942
|
+
{ appName: "NATS", iconKey: "nats", names: ["nats-server", "nats"] },
|
|
1943
|
+
{
|
|
1944
|
+
appName: "Docker",
|
|
1945
|
+
iconKey: "docker",
|
|
1946
|
+
names: [
|
|
1947
|
+
"docker",
|
|
1948
|
+
"dockerd",
|
|
1949
|
+
"docker-compose",
|
|
1950
|
+
"docker-proxy",
|
|
1951
|
+
"com.docker.backend"
|
|
1952
|
+
]
|
|
1953
|
+
},
|
|
1954
|
+
{ appName: "Podman", iconKey: "podman", names: ["podman", "podman-compose"] },
|
|
1955
|
+
{
|
|
1956
|
+
appName: "Kubernetes",
|
|
1957
|
+
iconKey: "kubernetes",
|
|
1958
|
+
names: ["kubectl", "kube-apiserver", "kubelet", "minikube", "kind"]
|
|
1959
|
+
},
|
|
1960
|
+
{ appName: "Helm", iconKey: "helm", names: ["helm"] },
|
|
1961
|
+
{ appName: "Tilt", iconKey: "tilt", names: ["tilt"] },
|
|
1962
|
+
{ appName: "nginx", iconKey: "nginx", names: ["nginx"] },
|
|
1963
|
+
{ appName: "Caddy", iconKey: "caddy", names: ["caddy"] },
|
|
1964
|
+
{
|
|
1965
|
+
appName: "Apache HTTP Server",
|
|
1966
|
+
iconKey: "apache",
|
|
1967
|
+
names: ["httpd", "apache2", "apachectl"]
|
|
1968
|
+
},
|
|
1969
|
+
{ appName: "HAProxy", iconKey: "haproxy", names: ["haproxy"] },
|
|
1970
|
+
{ appName: "Traefik", iconKey: "traefik", names: ["traefik"] },
|
|
1971
|
+
{ appName: "Envoy", iconKey: "envoy", names: ["envoy"] },
|
|
1972
|
+
{
|
|
1973
|
+
appName: "Tailscale",
|
|
1974
|
+
iconKey: "tailscale",
|
|
1975
|
+
names: ["tailscale", "tailscaled"]
|
|
1976
|
+
},
|
|
1977
|
+
{
|
|
1978
|
+
appName: "Cloudflare Tunnel",
|
|
1979
|
+
iconKey: "cloudflare",
|
|
1980
|
+
names: ["cloudflared"]
|
|
1981
|
+
},
|
|
1982
|
+
{ appName: "ngrok", iconKey: "ngrok", names: ["ngrok"] },
|
|
1983
|
+
{ appName: "localtunnel", iconKey: "terminal", names: ["lt", "localtunnel"] },
|
|
1984
|
+
{ appName: "SSH", iconKey: "ssh", names: ["ssh", "sshd", "scp", "sftp"] },
|
|
1985
|
+
{
|
|
1986
|
+
appName: "Git",
|
|
1987
|
+
iconKey: "git",
|
|
1988
|
+
names: ["git", "git-lfs", "git-upload-pack", "git-receive-pack"]
|
|
1989
|
+
},
|
|
1990
|
+
{ appName: "GitHub CLI", iconKey: "github", names: ["gh"] },
|
|
1991
|
+
{ appName: "GitLab CLI", iconKey: "gitlab", names: ["glab"] },
|
|
1992
|
+
{ appName: "Subversion", iconKey: "subversion", names: ["svn"] },
|
|
1993
|
+
{ appName: "Mercurial", iconKey: "mercurial", names: ["hg"] },
|
|
1994
|
+
{ appName: "AWS CLI", iconKey: "aws", names: ["aws", "aws-vault", "sam"] },
|
|
1995
|
+
{ appName: "Azure CLI", iconKey: "azure", names: ["az"] },
|
|
1996
|
+
{
|
|
1997
|
+
appName: "Google Cloud CLI",
|
|
1998
|
+
iconKey: "google-cloud",
|
|
1999
|
+
names: ["gcloud", "gsutil", "bq"]
|
|
2000
|
+
},
|
|
2001
|
+
{ appName: "Terraform", iconKey: "terraform", names: ["terraform"] },
|
|
2002
|
+
{ appName: "OpenTofu", iconKey: "opentofu", names: ["tofu"] },
|
|
2003
|
+
{
|
|
2004
|
+
appName: "Ansible",
|
|
2005
|
+
iconKey: "ansible",
|
|
2006
|
+
names: ["ansible", "ansible-playbook"]
|
|
2007
|
+
},
|
|
2008
|
+
{ appName: "Pulumi", iconKey: "pulumi", names: ["pulumi"] },
|
|
2009
|
+
{ appName: "Nomad", iconKey: "nomad", names: ["nomad"] },
|
|
2010
|
+
{ appName: "Vault", iconKey: "vault", names: ["vault"] },
|
|
2011
|
+
{ appName: "Consul", iconKey: "consul", names: ["consul"] },
|
|
2012
|
+
{ appName: "Make", iconKey: "make", names: ["make", "gmake"] },
|
|
2013
|
+
{ appName: "CMake", iconKey: "cmake", names: ["cmake", "ctest", "cpack"] },
|
|
2014
|
+
{ appName: "Ninja", iconKey: "ninja", names: ["ninja"] },
|
|
2015
|
+
{ appName: "Bazel", iconKey: "bazel", names: ["bazel", "bazelisk"] },
|
|
2016
|
+
{ appName: "Buck", iconKey: "buck", names: ["buck", "buck2"] },
|
|
2017
|
+
{ appName: "Clang", iconKey: "clang", names: ["clang", "clang++", "clangd"] },
|
|
2018
|
+
{ appName: "GCC", iconKey: "gcc", names: ["gcc", "g++", "cc", "c++"] },
|
|
2019
|
+
{ appName: "LLDB", iconKey: "terminal", names: ["lldb"] },
|
|
2020
|
+
{ appName: "GDB", iconKey: "terminal", names: ["gdb"] },
|
|
2021
|
+
{
|
|
2022
|
+
appName: "Swift",
|
|
2023
|
+
iconKey: "swift",
|
|
2024
|
+
names: ["swift", "swiftc", "sourcekit-lsp"]
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
appName: "Xcode",
|
|
2028
|
+
iconKey: "xcode",
|
|
2029
|
+
names: ["xcodebuild", "xcrun", "simctl"]
|
|
2030
|
+
},
|
|
2031
|
+
{ appName: "Zig", iconKey: "zig", names: ["zig"] },
|
|
2032
|
+
{
|
|
2033
|
+
appName: "Lua",
|
|
2034
|
+
iconKey: "lua",
|
|
2035
|
+
names: ["lua", "luajit", "lua-language-server"]
|
|
2036
|
+
},
|
|
2037
|
+
{ appName: "Perl", iconKey: "perl", names: ["perl"] },
|
|
2038
|
+
{
|
|
2039
|
+
appName: "R",
|
|
2040
|
+
iconKey: "r",
|
|
2041
|
+
names: ["r", "rscript", "rserver", "rsession"]
|
|
2042
|
+
},
|
|
2043
|
+
{ appName: "Julia", iconKey: "julia", names: ["julia"] },
|
|
2044
|
+
{
|
|
2045
|
+
appName: "Haskell",
|
|
2046
|
+
iconKey: "haskell",
|
|
2047
|
+
names: ["ghc", "ghci", "cabal", "stack", "haskell-language-server-wrapper"]
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
appName: "OCaml",
|
|
2051
|
+
iconKey: "ocaml",
|
|
2052
|
+
names: ["ocaml", "ocamlc", "dune", "opam"]
|
|
2053
|
+
},
|
|
2054
|
+
{ appName: "F#", iconKey: "fsharp", names: ["fsharpi", "fsautocomplete"] },
|
|
2055
|
+
{
|
|
2056
|
+
appName: ".NET",
|
|
2057
|
+
iconKey: "dotnet",
|
|
2058
|
+
names: ["dotnet", "csharp-ls", "omnisharp"]
|
|
2059
|
+
},
|
|
2060
|
+
{
|
|
2061
|
+
appName: "PowerShell",
|
|
2062
|
+
iconKey: "powershell",
|
|
2063
|
+
names: ["pwsh", "powershell"]
|
|
2064
|
+
},
|
|
2065
|
+
{ appName: "Bash", iconKey: "terminal", names: ["bash"] },
|
|
2066
|
+
{ appName: "Zsh", iconKey: "terminal", names: ["zsh"] },
|
|
2067
|
+
{ appName: "fish", iconKey: "terminal", names: ["fish"] },
|
|
2068
|
+
{ appName: "tmux", iconKey: "terminal", names: ["tmux"] },
|
|
2069
|
+
{ appName: "GNU Screen", iconKey: "terminal", names: ["screen"] },
|
|
2070
|
+
{ appName: "direnv", iconKey: "terminal", names: ["direnv"] },
|
|
2071
|
+
{
|
|
2072
|
+
appName: "Nix",
|
|
2073
|
+
iconKey: "nix",
|
|
2074
|
+
names: ["nix", "nix-shell", "nix-daemon", "nix-build", "nix develop"]
|
|
2075
|
+
},
|
|
2076
|
+
{ appName: "Homebrew", iconKey: "homebrew", names: ["brew"] },
|
|
2077
|
+
{ appName: "mise", iconKey: "terminal", names: ["mise"] },
|
|
2078
|
+
{ appName: "asdf", iconKey: "terminal", names: ["asdf"] },
|
|
2079
|
+
{ appName: "Volta", iconKey: "volta", names: ["volta"] },
|
|
2080
|
+
{ appName: "fnm", iconKey: "nodejs", names: ["fnm"] },
|
|
2081
|
+
{ appName: "nvm", iconKey: "nodejs", names: ["nvm"] },
|
|
2082
|
+
{ appName: "curl", iconKey: "terminal", names: ["curl"] },
|
|
2083
|
+
{ appName: "HTTPie", iconKey: "httpie", names: ["http", "https", "httpie"] },
|
|
2084
|
+
{ appName: "wget", iconKey: "terminal", names: ["wget"] },
|
|
2085
|
+
{ appName: "jq", iconKey: "terminal", names: ["jq"] },
|
|
2086
|
+
{ appName: "ripgrep", iconKey: "terminal", names: ["rg", "ripgrep"] },
|
|
2087
|
+
{ appName: "fd", iconKey: "terminal", names: ["fd"] },
|
|
2088
|
+
{ appName: "fzf", iconKey: "terminal", names: ["fzf"] },
|
|
2089
|
+
{ appName: "bat", iconKey: "terminal", names: ["bat"] },
|
|
2090
|
+
{ appName: "eza", iconKey: "terminal", names: ["eza", "exa"] },
|
|
2091
|
+
{ appName: "htop", iconKey: "terminal", names: ["htop", "top", "btop"] },
|
|
2092
|
+
{ appName: "Vim", iconKey: "vim", names: ["vim", "view", "vimdiff"] },
|
|
2093
|
+
{ appName: "Neovim", iconKey: "neovim", names: ["nvim", "nvim-qt"] },
|
|
2094
|
+
{ appName: "Emacs", iconKey: "emacs", names: ["emacs", "emacsclient"] },
|
|
2095
|
+
{
|
|
2096
|
+
appName: "Visual Studio Code",
|
|
2097
|
+
iconKey: "vscode",
|
|
2098
|
+
names: [
|
|
2099
|
+
"code",
|
|
2100
|
+
"code-insiders",
|
|
2101
|
+
"vscode",
|
|
2102
|
+
"visual studio code",
|
|
2103
|
+
"visual studio code - insiders"
|
|
2104
|
+
]
|
|
2105
|
+
},
|
|
2106
|
+
{ appName: "code-server", iconKey: "vscode", names: ["code-server"] },
|
|
2107
|
+
{ appName: "Cursor", iconKey: "cursor", names: ["cursor"] },
|
|
2108
|
+
{ appName: "Zed", iconKey: "zed", names: ["zed"] },
|
|
2109
|
+
{
|
|
2110
|
+
appName: "Sublime Text",
|
|
2111
|
+
iconKey: "sublime-text",
|
|
2112
|
+
names: ["subl", "sublime_text", "sublime text"]
|
|
2113
|
+
},
|
|
2114
|
+
{ appName: "Atom", iconKey: "atom", names: ["atom"] },
|
|
2115
|
+
{
|
|
2116
|
+
appName: "IntelliJ IDEA",
|
|
2117
|
+
iconKey: "intellij-idea",
|
|
2118
|
+
names: ["idea", "idea.sh", "intellij idea", "idea64.exe"]
|
|
2119
|
+
},
|
|
2120
|
+
{
|
|
2121
|
+
appName: "WebStorm",
|
|
2122
|
+
iconKey: "webstorm",
|
|
2123
|
+
names: ["webstorm", "webstorm.sh"]
|
|
2124
|
+
},
|
|
2125
|
+
{ appName: "PyCharm", iconKey: "pycharm", names: ["pycharm", "pycharm.sh"] },
|
|
2126
|
+
{
|
|
2127
|
+
appName: "RubyMine",
|
|
2128
|
+
iconKey: "rubymine",
|
|
2129
|
+
names: ["rubymine", "rubymine.sh"]
|
|
2130
|
+
},
|
|
2131
|
+
{ appName: "GoLand", iconKey: "goland", names: ["goland", "goland.sh"] },
|
|
2132
|
+
{ appName: "CLion", iconKey: "clion", names: ["clion", "clion.sh"] },
|
|
2133
|
+
{
|
|
2134
|
+
appName: "DataGrip",
|
|
2135
|
+
iconKey: "datagrip",
|
|
2136
|
+
names: ["datagrip", "datagrip.sh"]
|
|
2137
|
+
},
|
|
2138
|
+
{ appName: "Rider", iconKey: "rider", names: ["rider", "rider.sh"] },
|
|
2139
|
+
{
|
|
2140
|
+
appName: "Android Studio",
|
|
2141
|
+
iconKey: "android-studio",
|
|
2142
|
+
names: ["studio", "studio.sh", "android studio"]
|
|
2143
|
+
},
|
|
2144
|
+
{ appName: "Xcode", iconKey: "xcode", names: ["xcode", "xcode.app"] },
|
|
2145
|
+
{ appName: "Postman", iconKey: "postman", names: ["postman"] },
|
|
2146
|
+
{ appName: "Insomnia", iconKey: "insomnia", names: ["insomnia"] },
|
|
2147
|
+
{ appName: "Hoppscotch", iconKey: "hoppscotch", names: ["hoppscotch"] },
|
|
2148
|
+
{ appName: "TablePlus", iconKey: "tableplus", names: ["tableplus"] },
|
|
2149
|
+
{ appName: "DBeaver", iconKey: "dbeaver", names: ["dbeaver"] },
|
|
2150
|
+
{ appName: "RedisInsight", iconKey: "redis", names: ["redisinsight"] },
|
|
2151
|
+
{ appName: "Prisma", iconKey: "prisma", names: ["prisma"] },
|
|
2152
|
+
{ appName: "Drizzle Kit", iconKey: "drizzle", names: ["drizzle-kit"] },
|
|
2153
|
+
{ appName: "Supabase", iconKey: "supabase", names: ["supabase"] },
|
|
2154
|
+
{ appName: "Firebase", iconKey: "firebase", names: ["firebase"] },
|
|
2155
|
+
{ appName: "Vercel", iconKey: "vercel", names: ["vercel"] },
|
|
2156
|
+
{ appName: "Netlify", iconKey: "netlify", names: ["netlify", "ntl"] },
|
|
2157
|
+
{ appName: "Fly.io", iconKey: "fly", names: ["fly", "flyctl"] },
|
|
2158
|
+
{ appName: "Heroku", iconKey: "heroku", names: ["heroku"] },
|
|
2159
|
+
{ appName: "Stripe CLI", iconKey: "stripe", names: ["stripe"] },
|
|
2160
|
+
{ appName: "Twilio CLI", iconKey: "twilio", names: ["twilio"] },
|
|
2161
|
+
{ appName: "Mailpit", iconKey: "mail", names: ["mailpit"] },
|
|
2162
|
+
{ appName: "MailHog", iconKey: "mail", names: ["mailhog"] },
|
|
2163
|
+
{ appName: "OpenSSL", iconKey: "openssl", names: ["openssl"] },
|
|
2164
|
+
{ appName: "mkcert", iconKey: "certificate", names: ["mkcert"] },
|
|
2165
|
+
{ appName: "mkdocs", iconKey: "markdown", names: ["mkdocs"] },
|
|
2166
|
+
{ appName: "Hugo", iconKey: "hugo", names: ["hugo"] },
|
|
2167
|
+
{ appName: "Jekyll", iconKey: "jekyll", names: ["jekyll"] },
|
|
2168
|
+
{ appName: "Eleventy", iconKey: "eleventy", names: ["eleventy", "11ty"] },
|
|
2169
|
+
{ appName: "Docusaurus", iconKey: "docusaurus", names: ["docusaurus"] },
|
|
2170
|
+
{ appName: "Trunk", iconKey: "rust", names: ["trunk"] },
|
|
2171
|
+
{ appName: "wasm-pack", iconKey: "webassembly", names: ["wasm-pack"] },
|
|
2172
|
+
{ appName: "wasmtime", iconKey: "webassembly", names: ["wasmtime"] },
|
|
2173
|
+
{
|
|
2174
|
+
appName: "Language Server",
|
|
2175
|
+
iconKey: "language-server",
|
|
2176
|
+
names: [
|
|
2177
|
+
"lua-language-server",
|
|
2178
|
+
"yaml-language-server",
|
|
2179
|
+
"vscode-json-language-server",
|
|
2180
|
+
"docker-langserver",
|
|
2181
|
+
"bash-language-server",
|
|
2182
|
+
"marksman",
|
|
2183
|
+
"taplo"
|
|
2184
|
+
]
|
|
2185
|
+
}
|
|
2186
|
+
];
|
|
2187
|
+
var executableNameLookup = new Map;
|
|
2188
|
+
for (const entry of processCatalogEntries) {
|
|
2189
|
+
for (const name of entry.names) {
|
|
2190
|
+
executableNameLookup.set(normalizeProcessName(name), {
|
|
2191
|
+
appName: entry.appName,
|
|
2192
|
+
iconKey: entry.iconKey
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
var processNameCatalogSize = executableNameLookup.size;
|
|
2197
|
+
function canonicalizeProcessName(processName) {
|
|
2198
|
+
const executableName = executableNameFromPathOrName(processName);
|
|
2199
|
+
if (executableName === undefined) {
|
|
2200
|
+
return;
|
|
2201
|
+
}
|
|
2202
|
+
const directMatch = lookupCanonicalProcess(executableName);
|
|
2203
|
+
if (directMatch !== undefined) {
|
|
2204
|
+
return directMatch;
|
|
2205
|
+
}
|
|
2206
|
+
const pathMatch = lookupCanonicalPathHint(processName);
|
|
2207
|
+
if (pathMatch !== undefined) {
|
|
2208
|
+
return pathMatch;
|
|
2209
|
+
}
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
function guessCanonicalProcessFromCommand(command) {
|
|
2213
|
+
const tokens = shellCommandTokens(command);
|
|
2214
|
+
const executable = executableTokenFromCommandTokens(tokens);
|
|
2215
|
+
if (executable === undefined) {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
return canonicalizeProcessName(executable);
|
|
2219
|
+
}
|
|
2220
|
+
function lookupCanonicalProcess(processName) {
|
|
2221
|
+
const normalized = normalizeProcessName(processName);
|
|
2222
|
+
const identity = executableNameLookup.get(normalized);
|
|
2223
|
+
if (identity === undefined) {
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
return {
|
|
2227
|
+
...identity,
|
|
2228
|
+
executableName: processName,
|
|
2229
|
+
matchedName: normalized
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
function lookupCanonicalPathHint(processPath) {
|
|
2233
|
+
const normalizedPath = normalizeProcessName(processPath);
|
|
2234
|
+
const pathParts = normalizedPath.split("/").map((part) => trimExecutableExtension(part)).filter((part) => part.length > 0);
|
|
2235
|
+
for (const pathPart of pathParts.reverse()) {
|
|
2236
|
+
const pathHint = executableNameLookup.get(pathPart);
|
|
2237
|
+
if (pathHint !== undefined) {
|
|
2238
|
+
return {
|
|
2239
|
+
...pathHint,
|
|
2240
|
+
executableName: executableNameFromPathOrName(processPath) ?? processPath,
|
|
2241
|
+
matchedName: pathPart
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
function executableTokenFromCommandTokens(tokens) {
|
|
2248
|
+
const [firstToken, ...remainingTokens] = tokens;
|
|
2249
|
+
switch (firstToken) {
|
|
2250
|
+
case undefined:
|
|
2251
|
+
return;
|
|
2252
|
+
case "env":
|
|
2253
|
+
case "/usr/bin/env":
|
|
2254
|
+
return executableTokenFromCommandTokens(remainingTokens.filter((token) => !isEnvironmentAssignment(token)));
|
|
2255
|
+
case "sudo":
|
|
2256
|
+
case "command":
|
|
2257
|
+
case "exec":
|
|
2258
|
+
case "nohup":
|
|
2259
|
+
return executableTokenFromCommandTokens(remainingTokens.filter((token) => !token.startsWith("-")));
|
|
2260
|
+
default:
|
|
2261
|
+
switch (isEnvironmentAssignment(firstToken)) {
|
|
2262
|
+
case true:
|
|
2263
|
+
return executableTokenFromCommandTokens(remainingTokens);
|
|
2264
|
+
case false:
|
|
2265
|
+
return firstToken;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
function executableNameFromPathOrName(processName) {
|
|
2270
|
+
const trimmed = processName.trim();
|
|
2271
|
+
if (trimmed.length === 0) {
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const normalizedSeparators = trimmed.replaceAll("\\", "/");
|
|
2275
|
+
const [lastPathPart] = normalizedSeparators.split("/").filter((part) => part.length > 0).slice(-1);
|
|
2276
|
+
switch (lastPathPart) {
|
|
2277
|
+
case undefined:
|
|
2278
|
+
return;
|
|
2279
|
+
default:
|
|
2280
|
+
return trimExecutableExtension(lastPathPart);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
function trimExecutableExtension(executableName) {
|
|
2284
|
+
const normalized = executableName.toLowerCase();
|
|
2285
|
+
switch (true) {
|
|
2286
|
+
case normalized.endsWith(".app"):
|
|
2287
|
+
return executableName.slice(0, -4);
|
|
2288
|
+
case normalized.endsWith(".exe"):
|
|
2289
|
+
return executableName.slice(0, -4);
|
|
2290
|
+
case normalized.endsWith(".cmd"):
|
|
2291
|
+
case normalized.endsWith(".bat"):
|
|
2292
|
+
return executableName.slice(0, -4);
|
|
2293
|
+
default:
|
|
2294
|
+
return executableName;
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
function normalizeProcessName(processName) {
|
|
2298
|
+
return trimExecutableExtension(processName.trim().replaceAll("\\", "/").toLowerCase());
|
|
2299
|
+
}
|
|
2300
|
+
function isEnvironmentAssignment(token) {
|
|
2301
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
|
|
2302
|
+
}
|
|
2303
|
+
function shellCommandTokens(command) {
|
|
2304
|
+
const tokens = [];
|
|
2305
|
+
let currentToken = "";
|
|
2306
|
+
let quote = undefined;
|
|
2307
|
+
let escaping = false;
|
|
2308
|
+
for (const character of command.trim()) {
|
|
2309
|
+
switch (true) {
|
|
2310
|
+
case escaping:
|
|
2311
|
+
currentToken = `${currentToken}${character}`;
|
|
2312
|
+
escaping = false;
|
|
2313
|
+
break;
|
|
2314
|
+
case character === "\\":
|
|
2315
|
+
escaping = true;
|
|
2316
|
+
break;
|
|
2317
|
+
case (quote === undefined && (character === '"' || character === "'")):
|
|
2318
|
+
quote = character;
|
|
2319
|
+
break;
|
|
2320
|
+
case quote === character:
|
|
2321
|
+
quote = undefined;
|
|
2322
|
+
break;
|
|
2323
|
+
case (quote === undefined && /\s/.test(character)):
|
|
2324
|
+
if (currentToken.length > 0) {
|
|
2325
|
+
tokens.push(currentToken);
|
|
2326
|
+
currentToken = "";
|
|
2327
|
+
}
|
|
2328
|
+
break;
|
|
2329
|
+
default:
|
|
2330
|
+
currentToken = `${currentToken}${character}`;
|
|
2331
|
+
break;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
if (currentToken.length > 0) {
|
|
2335
|
+
tokens.push(currentToken);
|
|
2336
|
+
}
|
|
2337
|
+
return tokens;
|
|
2338
|
+
}
|
|
2339
|
+
|
|
1477
2340
|
// src/channelSession.ts
|
|
1478
2341
|
var codexTypingHeartbeatMs = 5000;
|
|
1479
2342
|
var defaultStreamFlushIntervalMs = 150;
|
|
@@ -1534,7 +2397,7 @@ async function attachChannelSession(args) {
|
|
|
1534
2397
|
}
|
|
1535
2398
|
switch (method) {
|
|
1536
2399
|
case "turn/started":
|
|
1537
|
-
if (turnId !== undefined) {
|
|
2400
|
+
if (threadId !== undefined && turnId !== undefined) {
|
|
1538
2401
|
rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId);
|
|
1539
2402
|
}
|
|
1540
2403
|
break;
|
|
@@ -1669,19 +2532,42 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
|
|
|
1669
2532
|
forwardedTerminalInputKeys: new Set,
|
|
1670
2533
|
webSearchProgressOutputs: createBoundedCache(maxForwardedTurnIds),
|
|
1671
2534
|
pendingApprovalRequests: new Map,
|
|
2535
|
+
approvalPromptChain: Promise.resolve(),
|
|
1672
2536
|
pendingPortForwardRequests: new Map,
|
|
2537
|
+
queuedPortForwardCandidates: new Map,
|
|
1673
2538
|
approvedForwardPorts: new Set,
|
|
1674
2539
|
approvedForwardTargets: new Map,
|
|
1675
2540
|
dismissedForwardTargets: new Map,
|
|
1676
2541
|
portForwardWatcher: undefined,
|
|
1677
2542
|
activeProcessingState: undefined,
|
|
2543
|
+
pendingReconnectContextInjection: undefined,
|
|
1678
2544
|
terminalInputForwardChain: Promise.resolve(),
|
|
1679
2545
|
webSearchProgressForwardChain: Promise.resolve(),
|
|
1680
2546
|
typingHeartbeat: undefined,
|
|
1681
2547
|
typingHeartbeatInFlight: false,
|
|
1682
|
-
runtimeSettings: runtimeSettingsFromOptions(options)
|
|
2548
|
+
runtimeSettings: runtimeSettingsFromOptions(options),
|
|
2549
|
+
pendingRuntimeSettingsResume: false,
|
|
2550
|
+
runtimeSettingsGeneration: 0,
|
|
2551
|
+
runtimeSettingsResumeChain: Promise.resolve()
|
|
1683
2552
|
};
|
|
1684
2553
|
}
|
|
2554
|
+
async function handleLostPortForwardCandidate(args, state, payloadContext, candidate) {
|
|
2555
|
+
dropLostQueuedPortForwardCandidate(args, state, candidate);
|
|
2556
|
+
await expireLostPendingPortForwardRequest(args, state, payloadContext, candidate);
|
|
2557
|
+
await revokeLostApprovedForwardPort(args, state, candidate);
|
|
2558
|
+
}
|
|
2559
|
+
function dropLostQueuedPortForwardCandidate(args, state, candidate) {
|
|
2560
|
+
const queued = state.queuedPortForwardCandidates.get(candidate.port);
|
|
2561
|
+
if (queued === undefined || !sameForwardCandidate(queued, candidate)) {
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
state.queuedPortForwardCandidates.delete(candidate.port);
|
|
2565
|
+
args.log("port_forward.queued_request_dropped", {
|
|
2566
|
+
port: candidate.port,
|
|
2567
|
+
pid: candidate.pid,
|
|
2568
|
+
reason: "listener_exited"
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
1685
2571
|
function startPortForwardWatchIfEnabled(args, state, payloadContext) {
|
|
1686
2572
|
if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined || state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
1687
2573
|
return;
|
|
@@ -1692,8 +2578,10 @@ function startPortForwardWatchIfEnabled(args, state, payloadContext) {
|
|
|
1692
2578
|
state.approvedForwardPorts.add(port);
|
|
1693
2579
|
}
|
|
1694
2580
|
state.portForwardWatcher = start({
|
|
2581
|
+
rootCwd: watchOptions.rootCwd ?? args.options.cwd,
|
|
1695
2582
|
...watchOptions,
|
|
1696
2583
|
onCandidate: (candidate) => publishPortForwardPrompt(args, state, payloadContext, candidate),
|
|
2584
|
+
onCandidateLost: (candidate) => handleLostPortForwardCandidate(args, state, payloadContext, candidate),
|
|
1697
2585
|
onError: (error) => args.log("port_forward.watch_failed", { message: error.message })
|
|
1698
2586
|
});
|
|
1699
2587
|
}
|
|
@@ -1746,7 +2634,11 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
|
|
|
1746
2634
|
return;
|
|
1747
2635
|
}
|
|
1748
2636
|
if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
|
|
1749
|
-
return {
|
|
2637
|
+
return {
|
|
2638
|
+
instanceId: args.instanceId,
|
|
2639
|
+
ok: false,
|
|
2640
|
+
error: "thread_not_bound"
|
|
2641
|
+
};
|
|
1750
2642
|
}
|
|
1751
2643
|
const interrupted = interruptPendingKandanMessages(state.queue, control.throughSeq);
|
|
1752
2644
|
const activeQueuedSeq = interruptibleQueuedSeq(state.turn);
|
|
@@ -1794,14 +2686,20 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
|
|
|
1794
2686
|
interruptedQueuedMessages: interrupted.ok ? interrupted.selectedCount : 0
|
|
1795
2687
|
};
|
|
1796
2688
|
}
|
|
1797
|
-
function updateSessionSettings(args, state, control) {
|
|
2689
|
+
async function updateSessionSettings(args, state, control) {
|
|
1798
2690
|
if (state.codexThreadId !== control.threadId) {
|
|
1799
2691
|
return;
|
|
1800
2692
|
}
|
|
1801
2693
|
if (state.codexThreadId === undefined) {
|
|
1802
|
-
return {
|
|
2694
|
+
return {
|
|
2695
|
+
instanceId: args.instanceId,
|
|
2696
|
+
ok: false,
|
|
2697
|
+
error: "thread_not_bound"
|
|
2698
|
+
};
|
|
1803
2699
|
}
|
|
1804
2700
|
state.runtimeSettings = mergeRuntimeSettings(state.runtimeSettings, control);
|
|
2701
|
+
state.pendingRuntimeSettingsResume = true;
|
|
2702
|
+
state.runtimeSettingsGeneration += 1;
|
|
1805
2703
|
publishRuntimeSettings(args, state).catch((error) => {
|
|
1806
2704
|
args.log("kandan.session_settings_publish_failed", {
|
|
1807
2705
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -1814,6 +2712,7 @@ function updateSessionSettings(args, state, control) {
|
|
|
1814
2712
|
sandbox: state.runtimeSettings.sandbox ?? null,
|
|
1815
2713
|
fast: state.runtimeSettings.fast ?? null
|
|
1816
2714
|
});
|
|
2715
|
+
await tryResumeCodexThreadForPendingRuntimeSettings(args, state);
|
|
1817
2716
|
return {
|
|
1818
2717
|
instanceId: args.instanceId,
|
|
1819
2718
|
ok: true,
|
|
@@ -1824,21 +2723,97 @@ function updateSessionSettings(args, state, control) {
|
|
|
1824
2723
|
fast: state.runtimeSettings.fast ?? null
|
|
1825
2724
|
};
|
|
1826
2725
|
}
|
|
2726
|
+
async function resumeCodexThreadForPendingRuntimeSettings(args, state) {
|
|
2727
|
+
const requestedGeneration = state.runtimeSettingsGeneration;
|
|
2728
|
+
const previousResume = state.runtimeSettingsResumeChain.catch(() => {
|
|
2729
|
+
return;
|
|
2730
|
+
});
|
|
2731
|
+
const resume = previousResume.then(() => performCodexThreadResumeForPendingRuntimeSettings(args, state, requestedGeneration));
|
|
2732
|
+
state.runtimeSettingsResumeChain = resume.then(() => {
|
|
2733
|
+
return;
|
|
2734
|
+
}, () => {
|
|
2735
|
+
return;
|
|
2736
|
+
});
|
|
2737
|
+
return await resume;
|
|
2738
|
+
}
|
|
2739
|
+
async function performCodexThreadResumeForPendingRuntimeSettings(args, state, requestedGeneration) {
|
|
2740
|
+
if (state.pendingRuntimeSettingsResume !== true || state.closed || state.turn.status !== "idle" || localTuiTurnIsActive(state)) {
|
|
2741
|
+
return true;
|
|
2742
|
+
}
|
|
2743
|
+
const codexThreadId = state.codexThreadId;
|
|
2744
|
+
if (codexThreadId === undefined) {
|
|
2745
|
+
return true;
|
|
2746
|
+
}
|
|
2747
|
+
if (state.runtimeSettingsGeneration !== requestedGeneration) {
|
|
2748
|
+
return false;
|
|
2749
|
+
}
|
|
2750
|
+
const resumeGeneration = requestedGeneration;
|
|
2751
|
+
const resumeSettings = state.runtimeSettings;
|
|
2752
|
+
const runtimeOptions = runtimeOptionsForSettings(args.options, resumeSettings);
|
|
2753
|
+
const resumeParams = {
|
|
2754
|
+
threadId: codexThreadId,
|
|
2755
|
+
...codexThreadRuntimeOverrides(runtimeOptions)
|
|
2756
|
+
};
|
|
2757
|
+
args.log("codex.session_settings_resume_requested", {
|
|
2758
|
+
codex_thread_id: codexThreadId,
|
|
2759
|
+
model: resumeSettings.model ?? null,
|
|
2760
|
+
reasoning_effort: resumeSettings.reasoningEffort ?? null,
|
|
2761
|
+
approval_policy: resumeSettings.approvalPolicy ?? null,
|
|
2762
|
+
sandbox: resumeSettings.sandbox ?? null,
|
|
2763
|
+
fast: resumeSettings.fast ?? null,
|
|
2764
|
+
generation: resumeGeneration
|
|
2765
|
+
});
|
|
2766
|
+
const resumed = await args.codex.request("thread/resume", resumeParams);
|
|
2767
|
+
if ("error" in resumed) {
|
|
2768
|
+
throw new Error(`failed to resume Codex thread with updated settings: ${resumed.error.message}`);
|
|
2769
|
+
}
|
|
2770
|
+
if (state.pendingRuntimeSettingsResume === true && state.runtimeSettingsGeneration === resumeGeneration) {
|
|
2771
|
+
state.pendingRuntimeSettingsResume = false;
|
|
2772
|
+
}
|
|
2773
|
+
args.log("codex.session_settings_resume_completed", {
|
|
2774
|
+
codex_thread_id: codexThreadId,
|
|
2775
|
+
generation: resumeGeneration,
|
|
2776
|
+
current_generation: state.runtimeSettingsGeneration,
|
|
2777
|
+
pending: state.pendingRuntimeSettingsResume
|
|
2778
|
+
});
|
|
2779
|
+
return state.pendingRuntimeSettingsResume !== true;
|
|
2780
|
+
}
|
|
2781
|
+
async function tryResumeCodexThreadForPendingRuntimeSettings(args, state) {
|
|
2782
|
+
try {
|
|
2783
|
+
return await resumeCodexThreadForPendingRuntimeSettings(args, state);
|
|
2784
|
+
} catch (error) {
|
|
2785
|
+
args.log("codex.session_settings_resume_failed", {
|
|
2786
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2787
|
+
});
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
1827
2791
|
async function resolvePendingCodexApprovalRequest(args, state, control) {
|
|
1828
2792
|
if (state.codexThreadId !== control.threadId) {
|
|
1829
2793
|
return;
|
|
1830
2794
|
}
|
|
1831
2795
|
if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
|
|
1832
|
-
return {
|
|
2796
|
+
return {
|
|
2797
|
+
instanceId: args.instanceId,
|
|
2798
|
+
ok: false,
|
|
2799
|
+
error: "thread_not_bound"
|
|
2800
|
+
};
|
|
1833
2801
|
}
|
|
1834
2802
|
const approval = state.pendingApprovalRequests.get(approvalRequestKey(control.requestId, control.sourceSeq));
|
|
1835
2803
|
if (approval === undefined) {
|
|
1836
|
-
return {
|
|
2804
|
+
return {
|
|
2805
|
+
instanceId: args.instanceId,
|
|
2806
|
+
ok: false,
|
|
2807
|
+
error: "approval_request_not_found"
|
|
2808
|
+
};
|
|
1837
2809
|
}
|
|
1838
2810
|
state.pendingApprovalRequests.delete(approvalRequestKey(control.requestId, control.sourceSeq));
|
|
1839
2811
|
const codexDecision = control.decision === "approve" ? "accept" : "decline";
|
|
1840
2812
|
approval.resolve({ decision: codexDecision });
|
|
1841
|
-
state.activeProcessingState = {
|
|
2813
|
+
state.activeProcessingState = {
|
|
2814
|
+
seq: approval.sourceSeq,
|
|
2815
|
+
reason: "streaming response"
|
|
2816
|
+
};
|
|
1842
2817
|
await publishMessageState(args, state.kandanThreadId, approval.sourceSeq, {
|
|
1843
2818
|
status: "processing",
|
|
1844
2819
|
reason: "streaming response"
|
|
@@ -1851,6 +2826,17 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
|
|
|
1851
2826
|
});
|
|
1852
2827
|
return { instanceId: args.instanceId, ok: true };
|
|
1853
2828
|
}
|
|
2829
|
+
async function drainQueuedPortForwardPrompt(args, state, payloadContext) {
|
|
2830
|
+
if (state.pendingPortForwardRequests.size > 0 || state.queuedPortForwardCandidates.size === 0) {
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
const next = Array.from(state.queuedPortForwardCandidates.values()).sort((left, right) => left.port - right.port)[0];
|
|
2834
|
+
if (next === undefined) {
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
state.queuedPortForwardCandidates.delete(next.port);
|
|
2838
|
+
await publishPortForwardPrompt(args, state, payloadContext, next);
|
|
2839
|
+
}
|
|
1854
2840
|
async function resolvePendingPortForwardRequest(args, state, payloadContext, control) {
|
|
1855
2841
|
const request = state.pendingPortForwardRequests.get(control.requestId);
|
|
1856
2842
|
if (request === undefined) {
|
|
@@ -1863,7 +2849,11 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
1863
2849
|
actor_user_id: control.actorUserId ?? null,
|
|
1864
2850
|
reason: "sender_not_allowed"
|
|
1865
2851
|
});
|
|
1866
|
-
return {
|
|
2852
|
+
return {
|
|
2853
|
+
instanceId: args.instanceId,
|
|
2854
|
+
ok: false,
|
|
2855
|
+
error: "sender_not_allowed"
|
|
2856
|
+
};
|
|
1867
2857
|
}
|
|
1868
2858
|
state.pendingPortForwardRequests.delete(control.requestId);
|
|
1869
2859
|
if (control.decision === "deny") {
|
|
@@ -1874,14 +2864,18 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
1874
2864
|
port: request.port,
|
|
1875
2865
|
pid: request.pid
|
|
1876
2866
|
});
|
|
2867
|
+
await drainQueuedPortForwardPrompt(args, state, payloadContext);
|
|
1877
2868
|
return { instanceId: args.instanceId, ok: true };
|
|
1878
2869
|
}
|
|
1879
2870
|
state.approvedForwardPorts.add(request.port);
|
|
1880
2871
|
state.approvedForwardTargets.set(request.port, approvedTargetFromRequest(request));
|
|
2872
|
+
const processIdentity = guessCanonicalProcessFromCommand(request.command);
|
|
1881
2873
|
const capabilities = args.options.onForwardPortApproved?.(request.port, {
|
|
1882
2874
|
kandanThreadId: state.kandanThreadId ?? null,
|
|
1883
2875
|
codexThreadId: state.codexThreadId ?? null,
|
|
1884
|
-
channelSlug: args.options.channelSession.channelSlug ?? null
|
|
2876
|
+
channelSlug: args.options.channelSession.channelSlug ?? null,
|
|
2877
|
+
processName: processIdentity?.appName ?? null,
|
|
2878
|
+
processIconKey: processIdentity?.iconKey ?? null
|
|
1885
2879
|
});
|
|
1886
2880
|
await publishForwardPortResolvedEvent(args, request, capabilities);
|
|
1887
2881
|
await publishMessageStateForPortForwardResult(args, state, request, "processed");
|
|
@@ -1891,6 +2885,7 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
1891
2885
|
port: request.port,
|
|
1892
2886
|
pid: request.pid
|
|
1893
2887
|
});
|
|
2888
|
+
await drainQueuedPortForwardPrompt(args, state, payloadContext);
|
|
1894
2889
|
return { instanceId: args.instanceId, ok: true, port: request.port };
|
|
1895
2890
|
}
|
|
1896
2891
|
function portForwardControlSenderAllowed(args, payloadContext, control) {
|
|
@@ -1927,37 +2922,39 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1927
2922
|
state.approvedForwardTargets.set(review.target.port, review.target);
|
|
1928
2923
|
return;
|
|
1929
2924
|
case "revoke_and_prompt":
|
|
2925
|
+
if (state.pendingPortForwardRequests.size > 0) {
|
|
2926
|
+
state.queuedPortForwardCandidates.set(candidate.port, candidate);
|
|
2927
|
+
args.log("port_forward.request_queued", {
|
|
2928
|
+
port: candidate.port,
|
|
2929
|
+
pid: candidate.pid,
|
|
2930
|
+
pending_count: state.pendingPortForwardRequests.size
|
|
2931
|
+
});
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
1930
2934
|
await revokeApprovedForwardPort(args, state, review.revoked, review.reason);
|
|
1931
2935
|
break;
|
|
1932
2936
|
case "prompt":
|
|
2937
|
+
if (state.pendingPortForwardRequests.size > 0) {
|
|
2938
|
+
state.queuedPortForwardCandidates.set(candidate.port, candidate);
|
|
2939
|
+
args.log("port_forward.request_queued", {
|
|
2940
|
+
port: candidate.port,
|
|
2941
|
+
pid: candidate.pid,
|
|
2942
|
+
pending_count: state.pendingPortForwardRequests.size
|
|
2943
|
+
});
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
1933
2946
|
break;
|
|
1934
2947
|
}
|
|
1935
2948
|
const requestId = `port-forward-${randomUUID()}`;
|
|
1936
2949
|
const label = portForwardPromptLabel(candidate);
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
reply_to_seq: state.rootSeq ?? null,
|
|
1941
|
-
structured: {
|
|
1942
|
-
kind: "local_runner_port_forward_request",
|
|
1943
|
-
request_id: requestId,
|
|
2950
|
+
const sourceSeq = portForwardApprovalSourceSeq(state);
|
|
2951
|
+
if (sourceSeq === undefined) {
|
|
2952
|
+
args.log("port_forward.prompt_skipped", {
|
|
1944
2953
|
port: candidate.port,
|
|
1945
2954
|
pid: candidate.pid,
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
}
|
|
1950
|
-
};
|
|
1951
|
-
const reply = await pushOk(args.kandan, args.topic, "session:post_thread_message", {
|
|
1952
|
-
workspace: args.options.channelSession.workspaceSlug,
|
|
1953
|
-
channel: args.options.channelSession.channelSlug,
|
|
1954
|
-
thread_id: state.kandanThreadId,
|
|
1955
|
-
body,
|
|
1956
|
-
payload
|
|
1957
|
-
});
|
|
1958
|
-
const sourceSeq = integerValue(reply.seq);
|
|
1959
|
-
if (sourceSeq === undefined) {
|
|
1960
|
-
throw new Error("port forward prompt did not return a Kandan message seq");
|
|
2955
|
+
reason: "source_seq_missing"
|
|
2956
|
+
});
|
|
2957
|
+
return;
|
|
1961
2958
|
}
|
|
1962
2959
|
const request = pendingRequestFromCandidate({
|
|
1963
2960
|
requestId,
|
|
@@ -1965,6 +2962,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1965
2962
|
candidate
|
|
1966
2963
|
});
|
|
1967
2964
|
state.pendingPortForwardRequests.set(requestId, request);
|
|
2965
|
+
const processIdentity = guessCanonicalProcessFromCommand(candidate.command);
|
|
2966
|
+
const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
|
|
2967
|
+
const processName = processIdentity?.appName ?? label;
|
|
1968
2968
|
await publishForwardPortRequestedEvent(args, request);
|
|
1969
2969
|
await publishMessageState(args, state.kandanThreadId, sourceSeq, {
|
|
1970
2970
|
status: "processing",
|
|
@@ -1972,13 +2972,20 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1972
2972
|
approval: {
|
|
1973
2973
|
requestId,
|
|
1974
2974
|
kind: "local_runner_port_forward",
|
|
1975
|
-
summary: `
|
|
2975
|
+
summary: `Make ${processName} on port ${candidate.port} accessible on Linzumi?`,
|
|
1976
2976
|
reason: portForwardPromptReason(candidate),
|
|
2977
|
+
port: candidate.port,
|
|
2978
|
+
pid: candidate.pid,
|
|
2979
|
+
command: candidate.command,
|
|
2980
|
+
...candidate.cwd === undefined ? {} : { cwd: candidate.cwd },
|
|
2981
|
+
...processIdentity?.appName === undefined ? {} : { processName },
|
|
2982
|
+
...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey },
|
|
2983
|
+
...processIconPath === undefined ? {} : { processIconPath },
|
|
1977
2984
|
choices: [
|
|
1978
2985
|
{
|
|
1979
2986
|
decision: "approve",
|
|
1980
|
-
label: "
|
|
1981
|
-
description: `Allow
|
|
2987
|
+
label: "Enable",
|
|
2988
|
+
description: `Allow Linzumi to proxy HTTP, HTTPS, and WebSocket traffic for runner port ${candidate.port}.`
|
|
1982
2989
|
},
|
|
1983
2990
|
{
|
|
1984
2991
|
decision: "deny",
|
|
@@ -1996,6 +3003,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1996
3003
|
pid: candidate.pid
|
|
1997
3004
|
});
|
|
1998
3005
|
}
|
|
3006
|
+
function portForwardApprovalSourceSeq(state) {
|
|
3007
|
+
return state.activeProcessingState?.seq ?? state.rootSeq;
|
|
3008
|
+
}
|
|
1999
3009
|
async function revokeApprovedForwardPort(args, state, target, reason) {
|
|
2000
3010
|
state.approvedForwardPorts.delete(target.port);
|
|
2001
3011
|
state.approvedForwardTargets.delete(target.port);
|
|
@@ -2015,30 +3025,58 @@ async function revokeApprovedForwardPort(args, state, target, reason) {
|
|
|
2015
3025
|
reason
|
|
2016
3026
|
});
|
|
2017
3027
|
}
|
|
3028
|
+
async function revokeLostApprovedForwardPort(args, state, candidate) {
|
|
3029
|
+
if (!state.approvedForwardPorts.has(candidate.port)) {
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
await revokeApprovedForwardPort(args, state, state.approvedForwardTargets.get(candidate.port) ?? candidate, "listener_exited");
|
|
3033
|
+
}
|
|
3034
|
+
async function expireLostPendingPortForwardRequest(args, state, payloadContext, candidate) {
|
|
3035
|
+
const request = Array.from(state.pendingPortForwardRequests.values()).find((request2) => sameForwardCandidate(approvedTargetFromRequest(request2), candidate));
|
|
3036
|
+
if (request === undefined) {
|
|
3037
|
+
return;
|
|
3038
|
+
}
|
|
3039
|
+
state.pendingPortForwardRequests.delete(request.requestId);
|
|
3040
|
+
await publishMessageStateForPortForwardResult(args, state, request, "failed", "port_forward_listener_exited");
|
|
3041
|
+
args.log("port_forward.pending_request_expired", {
|
|
3042
|
+
request_id: request.requestId,
|
|
3043
|
+
port: request.port,
|
|
3044
|
+
pid: request.pid,
|
|
3045
|
+
reason: "listener_exited"
|
|
3046
|
+
});
|
|
3047
|
+
await drainQueuedPortForwardPrompt(args, state, payloadContext);
|
|
3048
|
+
}
|
|
2018
3049
|
async function publishPortForwardReadyMessage(args, state, payloadContext, request) {
|
|
2019
3050
|
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
2020
3051
|
return;
|
|
2021
3052
|
}
|
|
2022
3053
|
const path = forwardPreviewPath(args.options.runnerId, request.port);
|
|
3054
|
+
const processIdentity = guessCanonicalProcessFromCommand(request.command);
|
|
3055
|
+
const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
|
|
3056
|
+
const processName = processIdentity?.appName ?? `Port ${request.port}`;
|
|
3057
|
+
const readySummary = `${processName} on port ${request.port} is ready in Linzumi.`;
|
|
2023
3058
|
await pushOptional(args.kandan, args.topic, "session:post_thread_message", {
|
|
2024
3059
|
workspace: args.options.channelSession.workspaceSlug,
|
|
2025
3060
|
channel: args.options.channelSession.channelSlug,
|
|
2026
3061
|
thread_id: state.kandanThreadId,
|
|
2027
|
-
body:
|
|
3062
|
+
body: `${readySummary} [Open](${path})`,
|
|
2028
3063
|
payload: {
|
|
2029
3064
|
...localRunnerPayload(args.options, args.instanceId, "port_forward_ready", state.codexThreadId, payloadContext),
|
|
2030
3065
|
...state.rootSeq === undefined ? {} : { reply_to_seq: state.rootSeq },
|
|
2031
3066
|
structured: {
|
|
2032
3067
|
kind: "local_runner_port_forward_ready",
|
|
2033
3068
|
status: "ready",
|
|
2034
|
-
summary:
|
|
3069
|
+
summary: readySummary,
|
|
2035
3070
|
next_action: "Open HTTP/HTTPS/WebSocket preview",
|
|
2036
3071
|
source_path: path,
|
|
2037
|
-
link_label: "Open
|
|
3072
|
+
link_label: "Open",
|
|
2038
3073
|
request_id: request.requestId,
|
|
2039
3074
|
port: request.port,
|
|
2040
3075
|
pid: request.pid,
|
|
2041
3076
|
command: request.command,
|
|
3077
|
+
...processIdentity?.appName === undefined ? {} : { process_name: processIdentity.appName },
|
|
3078
|
+
...processIdentity?.iconKey === undefined ? {} : { process_icon_key: processIdentity.iconKey },
|
|
3079
|
+
...processIconPath === undefined ? {} : { process_icon_path: processIconPath },
|
|
2042
3080
|
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
2043
3081
|
url: path
|
|
2044
3082
|
}
|
|
@@ -2067,10 +3105,15 @@ async function publishForwardPortResolvedEvent(args, request, capabilities) {
|
|
|
2067
3105
|
...capabilities === undefined ? {} : { capabilities }
|
|
2068
3106
|
}, args.log);
|
|
2069
3107
|
}
|
|
2070
|
-
async function publishMessageStateForPortForwardResult(args, state, request, status) {
|
|
3108
|
+
async function publishMessageStateForPortForwardResult(args, state, request, status, failedReason = "port_forward_denied") {
|
|
2071
3109
|
if (state.kandanThreadId === undefined) {
|
|
2072
3110
|
return;
|
|
2073
3111
|
}
|
|
3112
|
+
const activeProcessingState = state.activeProcessingState;
|
|
3113
|
+
if (activeProcessingState !== undefined && activeProcessingState.seq === request.sourceSeq) {
|
|
3114
|
+
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
|
|
3115
|
+
return;
|
|
3116
|
+
}
|
|
2074
3117
|
switch (status) {
|
|
2075
3118
|
case "processed":
|
|
2076
3119
|
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
|
|
@@ -2080,7 +3123,7 @@ async function publishMessageStateForPortForwardResult(args, state, request, sta
|
|
|
2080
3123
|
case "failed":
|
|
2081
3124
|
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
|
|
2082
3125
|
status: "failed",
|
|
2083
|
-
reason:
|
|
3126
|
+
reason: failedReason
|
|
2084
3127
|
});
|
|
2085
3128
|
break;
|
|
2086
3129
|
}
|
|
@@ -2151,7 +3194,12 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
|
|
|
2151
3194
|
actorUserId: event.actorUserId,
|
|
2152
3195
|
actorSlug: event.actorSlug
|
|
2153
3196
|
});
|
|
2154
|
-
if (result
|
|
3197
|
+
if (result === undefined) {
|
|
3198
|
+
await publishKandanMessageState(args, event, {
|
|
3199
|
+
status: "failed",
|
|
3200
|
+
reason: "port_forward_decision_missing"
|
|
3201
|
+
});
|
|
3202
|
+
} else if (result.ok === true) {
|
|
2155
3203
|
await publishKandanMessageState(args, event, { status: "processed" });
|
|
2156
3204
|
} else {
|
|
2157
3205
|
await publishKandanMessageState(args, event, {
|
|
@@ -2301,11 +3349,19 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2301
3349
|
if (state.closed || state.turn.status !== "idle" || localTuiTurnIsActive(state)) {
|
|
2302
3350
|
return;
|
|
2303
3351
|
}
|
|
3352
|
+
const resumed = await tryResumeCodexThreadForPendingRuntimeSettings(args, state);
|
|
3353
|
+
if (!resumed) {
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
2304
3356
|
const next = dequeuePendingKandanMessage(state.queue);
|
|
2305
3357
|
if (next === undefined) {
|
|
2306
3358
|
return;
|
|
2307
3359
|
}
|
|
2308
|
-
state.turn = {
|
|
3360
|
+
state.turn = {
|
|
3361
|
+
status: "starting",
|
|
3362
|
+
queuedSeq: next.seq,
|
|
3363
|
+
interruptAfterStart: false
|
|
3364
|
+
};
|
|
2309
3365
|
state.activeProcessingState = { seq: next.seq, reason: "starting turn" };
|
|
2310
3366
|
args.log("codex.turn_starting", {
|
|
2311
3367
|
queued_seq: next.seq,
|
|
@@ -2328,10 +3384,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2328
3384
|
}
|
|
2329
3385
|
const started = await args.codex.request("turn/start", {
|
|
2330
3386
|
threadId: codexThreadId,
|
|
2331
|
-
input: await codexInputItemsForQueuedKandanMessage(args, next),
|
|
3387
|
+
input: await codexInputItemsForQueuedKandanMessage(args, next, state.pendingReconnectContextInjection),
|
|
2332
3388
|
...codexTurnRuntimeOverrides(runtimeOptionsForSettings(args.options, state.runtimeSettings))
|
|
2333
3389
|
});
|
|
2334
3390
|
const turnId = extractTurnIdFromResponse(started);
|
|
3391
|
+
state.pendingReconnectContextInjection = undefined;
|
|
2335
3392
|
const interruptAfterStart = state.turn.status === "starting" && state.turn.interruptAfterStart;
|
|
2336
3393
|
state.turn = { status: "active", turnId, queuedSeq: next.seq };
|
|
2337
3394
|
rememberTurnReplyTarget(state, turnId, next.seq);
|
|
@@ -2357,12 +3414,14 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2357
3414
|
await stopCodexTyping(args, state);
|
|
2358
3415
|
const newCodexThreadId = await startCodexThread(args.codex, args.options);
|
|
2359
3416
|
state.codexThreadId = newCodexThreadId;
|
|
3417
|
+
await bindCurrentCodexThread(args, state);
|
|
2360
3418
|
args.log("codex.thread_rebound", {
|
|
2361
3419
|
kandan_thread_id: state.kandanThreadId,
|
|
2362
3420
|
old_codex_thread_id: oldCodexThreadId ?? null,
|
|
2363
3421
|
new_codex_thread_id: newCodexThreadId
|
|
2364
3422
|
});
|
|
2365
3423
|
await postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId);
|
|
3424
|
+
state.pendingReconnectContextInjection = await fetchReconnectContextInjection(args, state);
|
|
2366
3425
|
requeuePendingKandanMessageFront(state.queue, next);
|
|
2367
3426
|
state.turn = { status: "idle" };
|
|
2368
3427
|
await drainKandanMessageQueue(args, state, payloadContext);
|
|
@@ -2388,6 +3447,26 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2388
3447
|
});
|
|
2389
3448
|
}
|
|
2390
3449
|
}
|
|
3450
|
+
async function fetchReconnectContextInjection(args, state) {
|
|
3451
|
+
if (state.kandanThreadId === undefined) {
|
|
3452
|
+
throw new Error("cannot fetch reconnect context without a Linzumi thread id");
|
|
3453
|
+
}
|
|
3454
|
+
const session = args.options.channelSession;
|
|
3455
|
+
const reply = await pushOk(args.kandan, args.topic, "session:thread_context", {
|
|
3456
|
+
workspace: session.workspaceSlug,
|
|
3457
|
+
channel: session.channelSlug,
|
|
3458
|
+
thread_id: state.kandanThreadId
|
|
3459
|
+
});
|
|
3460
|
+
const messages = parseReconnectContextMessages(reply.messages);
|
|
3461
|
+
const summarizer = args.options.reconnectContextSummarizer ?? createConfiguredReconnectContextSummarizer();
|
|
3462
|
+
const injection = await buildReconnectContextInjection(messages, summarizer);
|
|
3463
|
+
args.log("codex.thread_reconnect_context_prepared", {
|
|
3464
|
+
codex_thread_id: state.codexThreadId ?? null,
|
|
3465
|
+
kandan_thread_id: state.kandanThreadId,
|
|
3466
|
+
context_message_count: messages.length
|
|
3467
|
+
});
|
|
3468
|
+
return injection;
|
|
3469
|
+
}
|
|
2391
3470
|
async function handleCodexServerRequest(args, state, payloadContext, request) {
|
|
2392
3471
|
const params = objectValue(request.params) ?? {};
|
|
2393
3472
|
const turnId = codexNotificationTurnId(params);
|
|
@@ -2425,35 +3504,48 @@ function codexApprovalRequestCanSurface(method) {
|
|
|
2425
3504
|
return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval";
|
|
2426
3505
|
}
|
|
2427
3506
|
async function requestKandanApproval(args, state, request, turnId, payloadContext) {
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
}, undefined, undefined, state.codexThreadId);
|
|
2441
|
-
args.log("codex.approval_request_pending", {
|
|
2442
|
-
request_id: approval.requestId,
|
|
2443
|
-
source_seq: sourceSeq,
|
|
2444
|
-
turn_id: turnId,
|
|
2445
|
-
method: request.method
|
|
2446
|
-
});
|
|
2447
|
-
return new Promise((resolve2, reject) => {
|
|
2448
|
-
const request2 = {
|
|
2449
|
-
requestId: approval.requestId,
|
|
2450
|
-
sourceSeq,
|
|
2451
|
-
turnId,
|
|
2452
|
-
resolve: resolve2,
|
|
2453
|
-
reject
|
|
3507
|
+
const approvalResult = state.approvalPromptChain.then(async () => {
|
|
3508
|
+
const sourceSeq = activeQueuedSeqForTurn(state.turn, turnId);
|
|
3509
|
+
if (sourceSeq === undefined || state.kandanThreadId === undefined) {
|
|
3510
|
+
const message = `Codex approval request has no active Kandan source message: ${request.method}`;
|
|
3511
|
+
await failActiveCodexTurn(args, state, turnId, message, payloadContext);
|
|
3512
|
+
throw new Error(message);
|
|
3513
|
+
}
|
|
3514
|
+
const approval = codexApprovalMessageState(request);
|
|
3515
|
+
state.activeProcessingState = {
|
|
3516
|
+
seq: sourceSeq,
|
|
3517
|
+
reason: "awaiting approval",
|
|
3518
|
+
approval
|
|
2454
3519
|
};
|
|
2455
|
-
|
|
3520
|
+
const approvalPromise = new Promise((resolve2, reject) => {
|
|
3521
|
+
const pendingRequest = {
|
|
3522
|
+
requestId: approval.requestId,
|
|
3523
|
+
sourceSeq,
|
|
3524
|
+
turnId,
|
|
3525
|
+
resolve: resolve2,
|
|
3526
|
+
reject
|
|
3527
|
+
};
|
|
3528
|
+
state.pendingApprovalRequests.set(approvalRequestKey(pendingRequest.requestId, pendingRequest.sourceSeq), pendingRequest);
|
|
3529
|
+
});
|
|
3530
|
+
await publishMessageState(args, state.kandanThreadId, sourceSeq, {
|
|
3531
|
+
status: "processing",
|
|
3532
|
+
reason: "awaiting approval",
|
|
3533
|
+
approval
|
|
3534
|
+
}, undefined, undefined, state.codexThreadId);
|
|
3535
|
+
args.log("codex.approval_request_pending", {
|
|
3536
|
+
request_id: approval.requestId,
|
|
3537
|
+
source_seq: sourceSeq,
|
|
3538
|
+
turn_id: turnId,
|
|
3539
|
+
method: request.method
|
|
3540
|
+
});
|
|
3541
|
+
return approvalPromise;
|
|
3542
|
+
});
|
|
3543
|
+
state.approvalPromptChain = approvalResult.then(() => {
|
|
3544
|
+
return;
|
|
3545
|
+
}, () => {
|
|
3546
|
+
return;
|
|
2456
3547
|
});
|
|
3548
|
+
return approvalResult;
|
|
2457
3549
|
}
|
|
2458
3550
|
function rejectPendingApprovalRequests(state, error) {
|
|
2459
3551
|
const pendingApprovals = [...state.pendingApprovalRequests.values()];
|
|
@@ -2479,7 +3571,11 @@ async function forwardCompletedCodexTurn(args, state, turnId, payloadContext) {
|
|
|
2479
3571
|
const completingActiveTurn = completingQueuedSeq !== undefined;
|
|
2480
3572
|
const completingLocalTuiTurn = isLocalTuiTurn(state, turnId);
|
|
2481
3573
|
if (completingQueuedSeq !== undefined) {
|
|
2482
|
-
state.turn = {
|
|
3574
|
+
state.turn = {
|
|
3575
|
+
status: "completing",
|
|
3576
|
+
turnId,
|
|
3577
|
+
queuedSeq: completingQueuedSeq
|
|
3578
|
+
};
|
|
2483
3579
|
}
|
|
2484
3580
|
await waitForPendingTuiInputMirror(state, turnId);
|
|
2485
3581
|
await waitForStreamingForwardChains(args, state, payloadContext);
|
|
@@ -2582,11 +3678,17 @@ async function forwardCompletedCodexTurn(args, state, turnId, payloadContext) {
|
|
|
2582
3678
|
if (completingActiveTurn && state.turn.status === "completing" && state.turn.turnId === turnId) {
|
|
2583
3679
|
state.turn = { status: "idle" };
|
|
2584
3680
|
await stopCodexTyping(args, state);
|
|
2585
|
-
await
|
|
3681
|
+
const resumed = await tryResumeCodexThreadForPendingRuntimeSettings(args, state);
|
|
3682
|
+
if (resumed) {
|
|
3683
|
+
await drainKandanMessageQueue(args, state, payloadContext);
|
|
3684
|
+
}
|
|
2586
3685
|
}
|
|
2587
3686
|
if (completingLocalTuiTurn && !completingActiveTurn) {
|
|
2588
3687
|
await stopCodexTyping(args, state);
|
|
2589
|
-
await
|
|
3688
|
+
const resumed = await tryResumeCodexThreadForPendingRuntimeSettings(args, state);
|
|
3689
|
+
if (resumed) {
|
|
3690
|
+
await drainKandanMessageQueue(args, state, payloadContext);
|
|
3691
|
+
}
|
|
2590
3692
|
}
|
|
2591
3693
|
}
|
|
2592
3694
|
}
|
|
@@ -2841,7 +3943,10 @@ async function forwardReasoningDeltaPayload(args, state, delta, payloadContext)
|
|
|
2841
3943
|
}
|
|
2842
3944
|
} else {
|
|
2843
3945
|
await editCodexStructuredOutput(args, state, existing.seq, nextContent, codexReasoningStructuredMessage(delta.itemKey, nextContent, "streaming"));
|
|
2844
|
-
rememberStreamingReasoningOutput(state, {
|
|
3946
|
+
rememberStreamingReasoningOutput(state, {
|
|
3947
|
+
...existing,
|
|
3948
|
+
content: nextContent
|
|
3949
|
+
});
|
|
2845
3950
|
}
|
|
2846
3951
|
args.log("kandan.codex_reasoning_delta_forwarded", {
|
|
2847
3952
|
item_key: delta.itemKey,
|
|
@@ -3398,7 +4503,10 @@ function rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId) {
|
|
|
3398
4503
|
if (state.kandanThreadId !== undefined) {
|
|
3399
4504
|
startCodexTypingHeartbeat(args, state, state.kandanThreadId);
|
|
3400
4505
|
}
|
|
3401
|
-
args.log("codex.tui_turn_started", {
|
|
4506
|
+
args.log("codex.tui_turn_started", {
|
|
4507
|
+
turn_id: turnId,
|
|
4508
|
+
codex_thread_id: threadId
|
|
4509
|
+
});
|
|
3402
4510
|
}
|
|
3403
4511
|
function isLocalTuiTurn(state, turnId) {
|
|
3404
4512
|
return state.localTuiTurnIds.has(turnId);
|
|
@@ -3465,7 +4573,10 @@ function clearPendingStreamFlushTimers(state) {
|
|
|
3465
4573
|
clearStreamDeltaFlushTimer(state.fileChangeQueue);
|
|
3466
4574
|
}
|
|
3467
4575
|
function rememberTurnReplyTarget(state, turnId, replyToSeq) {
|
|
3468
|
-
rememberBoundedCacheValue(state.turnReplyTargets, turnId, {
|
|
4576
|
+
rememberBoundedCacheValue(state.turnReplyTargets, turnId, {
|
|
4577
|
+
turnId,
|
|
4578
|
+
replyToSeq
|
|
4579
|
+
});
|
|
3469
4580
|
}
|
|
3470
4581
|
function sourceMessageSeqForTurn(state, turnId) {
|
|
3471
4582
|
return getBoundedCacheValue(state.turnReplyTargets, turnId)?.replyToSeq;
|
|
@@ -3474,20 +4585,12 @@ function fileChangePaths(structured) {
|
|
|
3474
4585
|
const changes = arrayValue(structured.changes) ?? [];
|
|
3475
4586
|
return changes.filter(isJsonObject).map((change) => stringValue(change.path) ?? "").filter((path) => path.trim() !== "");
|
|
3476
4587
|
}
|
|
3477
|
-
async function postCodexThreadReboundMessage(args, state, payloadContext,
|
|
4588
|
+
async function postCodexThreadReboundMessage(args, state, payloadContext, _oldCodexThreadId, newCodexThreadId) {
|
|
3478
4589
|
if (state.kandanThreadId === undefined) {
|
|
3479
4590
|
return;
|
|
3480
4591
|
}
|
|
3481
4592
|
const session = args.options.channelSession;
|
|
3482
|
-
const body = [
|
|
3483
|
-
"Codex reconnected.",
|
|
3484
|
-
"",
|
|
3485
|
-
"The previous local Codex app-server thread was not available in this process, so this runner started a new local Codex thread for this Kandan thread.",
|
|
3486
|
-
"",
|
|
3487
|
-
`Previous Codex thread: ${oldCodexThreadId ?? "unknown"}`,
|
|
3488
|
-
`New Codex thread: ${newCodexThreadId}`
|
|
3489
|
-
].join(`
|
|
3490
|
-
`);
|
|
4593
|
+
const body = "[codex reconnected to new thread]";
|
|
3491
4594
|
await pushOk(args.kandan, args.topic, "session:post_thread_message", {
|
|
3492
4595
|
workspace: session.workspaceSlug,
|
|
3493
4596
|
channel: session.channelSlug,
|
|
@@ -3622,10 +4725,19 @@ async function publishMessageState(args, threadId, seq, state, actorSlug, actorU
|
|
|
3622
4725
|
approval_request_id: state.approval.requestId,
|
|
3623
4726
|
approval_kind: state.approval.kind,
|
|
3624
4727
|
approval_summary: state.approval.summary,
|
|
4728
|
+
...state.approval.port === undefined ? {} : { approval_port: state.approval.port },
|
|
4729
|
+
...state.approval.pid === undefined ? {} : { approval_pid: state.approval.pid },
|
|
4730
|
+
...state.approval.command === undefined ? {} : { approval_command: state.approval.command },
|
|
4731
|
+
...state.approval.cwd === undefined ? {} : { approval_cwd: state.approval.cwd },
|
|
4732
|
+
...state.approval.processName === undefined ? {} : { approval_process_name: state.approval.processName },
|
|
4733
|
+
...state.approval.processIconKey === undefined ? {} : { approval_process_icon_key: state.approval.processIconKey },
|
|
4734
|
+
...state.approval.processIconPath === undefined ? {} : { approval_process_icon_path: state.approval.processIconPath },
|
|
3625
4735
|
...state.approval.reason === undefined ? {} : { approval_reason: state.approval.reason },
|
|
3626
4736
|
...state.approval.choices === undefined ? {} : { approval_choices: state.approval.choices },
|
|
3627
4737
|
...state.approval.allowedActorSlug === undefined ? {} : { approval_allowed_actor_slug: state.approval.allowedActorSlug },
|
|
3628
|
-
...state.approval.allowedActorUserId === undefined ? {} : {
|
|
4738
|
+
...state.approval.allowedActorUserId === undefined ? {} : {
|
|
4739
|
+
approval_allowed_actor_user_id: state.approval.allowedActorUserId
|
|
4740
|
+
}
|
|
3629
4741
|
} : {},
|
|
3630
4742
|
...actorSlug === undefined ? {} : { actor_slug: actorSlug },
|
|
3631
4743
|
...actorUserId === undefined ? {} : { actor_user_id: actorUserId }
|
|
@@ -3679,11 +4791,15 @@ function clearActiveProcessingState(state, seq) {
|
|
|
3679
4791
|
state.activeProcessingState = undefined;
|
|
3680
4792
|
}
|
|
3681
4793
|
}
|
|
3682
|
-
async function codexInputItemsForQueuedKandanMessage(args, message) {
|
|
4794
|
+
async function codexInputItemsForQueuedKandanMessage(args, message, reconnectContextInjection = undefined) {
|
|
3683
4795
|
const attachments = await downloadQueuedKandanAttachments(args, message);
|
|
3684
4796
|
const text = appendDownloadedAttachmentContext(codexInputForQueuedKandanMessage(message), attachments);
|
|
3685
4797
|
const imageItems = attachments.flatMap((attachment) => attachment.isImage ? [{ type: "localImage", path: attachment.path }] : []);
|
|
3686
|
-
return [
|
|
4798
|
+
return [
|
|
4799
|
+
...reconnectContextInjection === undefined ? [] : [reconnectContextInjection],
|
|
4800
|
+
{ type: "text", text },
|
|
4801
|
+
...imageItems
|
|
4802
|
+
];
|
|
3687
4803
|
}
|
|
3688
4804
|
async function downloadQueuedKandanAttachments(args, message) {
|
|
3689
4805
|
if (message.attachments.length === 0) {
|
|
@@ -3816,10 +4932,11 @@ async function uploadedFileIdsForCodexOutput(args, body, structured) {
|
|
|
3816
4932
|
throw new Error("Kandan attachment prepare response missing upload_url");
|
|
3817
4933
|
}
|
|
3818
4934
|
const bytes = await readFile(file.path);
|
|
4935
|
+
const uploadBody = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
3819
4936
|
const response = await fetch(resolveKandanAttachmentUrl(args.options.kandanUrl, uploadUrl), {
|
|
3820
4937
|
method: uploadMethod,
|
|
3821
4938
|
headers: { "content-type": file.contentType },
|
|
3822
|
-
body:
|
|
4939
|
+
body: uploadBody
|
|
3823
4940
|
});
|
|
3824
4941
|
if (!response.ok) {
|
|
3825
4942
|
throw new Error(`Kandan attachment upload failed for ${file.fileName}: ${response.status} ${response.statusText}`);
|
|
@@ -4059,7 +5176,7 @@ function defaultCliAuditLogFile() {
|
|
|
4059
5176
|
return override === undefined || override === "" ? join2(homedir(), ".linzumi", "logs", "command-events.jsonl") : override;
|
|
4060
5177
|
}
|
|
4061
5178
|
function redactForCliLog(value) {
|
|
4062
|
-
return
|
|
5179
|
+
return redactObject(value);
|
|
4063
5180
|
}
|
|
4064
5181
|
function redactValue(value, key) {
|
|
4065
5182
|
if (sensitiveKey(key)) {
|
|
@@ -4204,8 +5321,10 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
|
|
|
4204
5321
|
const child = spawn(codexBin, args, {
|
|
4205
5322
|
cwd,
|
|
4206
5323
|
env: process.env,
|
|
4207
|
-
stdio: ["ignore", "inherit", "inherit"]
|
|
5324
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
5325
|
+
detached: true
|
|
4208
5326
|
});
|
|
5327
|
+
const stop = () => stopCodexAppServerProcess(child);
|
|
4209
5328
|
writeCliAuditEvent("process.spawned", {
|
|
4210
5329
|
command: codexBin,
|
|
4211
5330
|
args,
|
|
@@ -4231,33 +5350,57 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
|
|
|
4231
5350
|
try {
|
|
4232
5351
|
await waitForReadyz(url, child);
|
|
4233
5352
|
} catch (error) {
|
|
4234
|
-
|
|
5353
|
+
stop();
|
|
4235
5354
|
throw error;
|
|
4236
5355
|
}
|
|
4237
|
-
return { url, process: child };
|
|
5356
|
+
return { url, process: child, stop };
|
|
5357
|
+
}
|
|
5358
|
+
function stopCodexAppServerProcess(child, killProcess = process.kill) {
|
|
5359
|
+
if (child.pid !== undefined) {
|
|
5360
|
+
try {
|
|
5361
|
+
killProcess(-child.pid, "SIGINT");
|
|
5362
|
+
return;
|
|
5363
|
+
} catch (error) {
|
|
5364
|
+
logProcessGroupSignalFailure(child.pid, error);
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
5367
|
+
child.kill("SIGINT");
|
|
5368
|
+
}
|
|
5369
|
+
function logProcessGroupSignalFailure(pid, error) {
|
|
5370
|
+
const code = processSignalErrorCode(error);
|
|
5371
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5372
|
+
const event = code === "EPERM" ? "process.group_signal_denied" : code === "ESRCH" ? "process.group_signal_missing" : "process.group_signal_failed";
|
|
5373
|
+
writeCliAuditEvent(event, {
|
|
5374
|
+
pid,
|
|
5375
|
+
signal: "SIGINT",
|
|
5376
|
+
code,
|
|
5377
|
+
message,
|
|
5378
|
+
fallback: "child_signal",
|
|
5379
|
+
purpose: "codex.app_server"
|
|
5380
|
+
});
|
|
5381
|
+
if (code === "EPERM") {
|
|
5382
|
+
process.stderr.write(`codex app-server process-group SIGINT denied for pid ${pid}; falling back to direct child SIGINT
|
|
5383
|
+
`);
|
|
5384
|
+
}
|
|
5385
|
+
}
|
|
5386
|
+
function processSignalErrorCode(error) {
|
|
5387
|
+
if (error !== null && typeof error === "object" && "code" in error) {
|
|
5388
|
+
const code = error.code;
|
|
5389
|
+
return typeof code === "string" ? code : undefined;
|
|
5390
|
+
}
|
|
5391
|
+
return;
|
|
4238
5392
|
}
|
|
4239
5393
|
function codexAppServerArgs(listenUrl, options = {}) {
|
|
4240
|
-
return [
|
|
4241
|
-
"app-server",
|
|
4242
|
-
...codexConfigArgs(options),
|
|
4243
|
-
"--listen",
|
|
4244
|
-
listenUrl
|
|
4245
|
-
];
|
|
5394
|
+
return ["app-server", ...codexConfigArgs(options), "--listen", listenUrl];
|
|
4246
5395
|
}
|
|
4247
5396
|
function codexConfigArgs(options) {
|
|
4248
5397
|
return [
|
|
4249
|
-
...options.model === undefined ? [] : [
|
|
4250
|
-
"-c",
|
|
4251
|
-
`model=${JSON.stringify(options.model)}`
|
|
4252
|
-
],
|
|
5398
|
+
...options.model === undefined ? [] : ["-c", `model=${JSON.stringify(options.model)}`],
|
|
4253
5399
|
...options.reasoningEffort === undefined ? [] : [
|
|
4254
5400
|
"-c",
|
|
4255
5401
|
`model_reasoning_effort=${JSON.stringify(options.reasoningEffort)}`
|
|
4256
5402
|
],
|
|
4257
|
-
...options.fast === true ? [
|
|
4258
|
-
"-c",
|
|
4259
|
-
`service_tier=${JSON.stringify("fast")}`
|
|
4260
|
-
] : []
|
|
5403
|
+
...options.fast === true ? ["-c", `service_tier=${JSON.stringify("fast")}`] : []
|
|
4261
5404
|
];
|
|
4262
5405
|
}
|
|
4263
5406
|
async function connectCodexAppServer(websocketUrl, socketFactory = (url) => new WebSocket(url)) {
|
|
@@ -4611,7 +5754,9 @@ async function handleForwardHttpRequest(control, allowedPorts) {
|
|
|
4611
5754
|
method: control.method,
|
|
4612
5755
|
headers: requestHeaders(control.headers),
|
|
4613
5756
|
redirect: "manual",
|
|
4614
|
-
...bodyDecision.body === undefined ? {} : {
|
|
5757
|
+
...bodyDecision.body === undefined ? {} : {
|
|
5758
|
+
body: bodyDecision.body.buffer.slice(bodyDecision.body.byteOffset, bodyDecision.body.byteOffset + bodyDecision.body.byteLength)
|
|
5759
|
+
}
|
|
4615
5760
|
};
|
|
4616
5761
|
const response = await fetchWithHttpsFallback(control.port, control.path, control.queryString, request);
|
|
4617
5762
|
const upstreamBuffer = Buffer.from(await response.arrayBuffer());
|
|
@@ -5081,10 +6226,12 @@ function prepareCodeServerProfile(collaboration, editorRuntime) {
|
|
|
5081
6226
|
const userDataDir = mkdtempSync(join4(tmpdir(), "kandan-local-editor-"));
|
|
5082
6227
|
const extensionsDir = join4(userDataDir, "extensions");
|
|
5083
6228
|
const collaborationServerDir = join4(userDataDir, "collaboration-server");
|
|
6229
|
+
const tempDir = join4(userDataDir, "tmp");
|
|
5084
6230
|
const userSettingsDir = join4(userDataDir, "User");
|
|
5085
6231
|
mkdirSync3(userSettingsDir, { recursive: true });
|
|
5086
6232
|
mkdirSync3(extensionsDir, { recursive: true });
|
|
5087
6233
|
mkdirSync3(collaborationServerDir, { recursive: true });
|
|
6234
|
+
mkdirSync3(tempDir, { recursive: true });
|
|
5088
6235
|
if (editorRuntime !== undefined) {
|
|
5089
6236
|
installDirectory(editorRuntime.assets.documentStateExtensionDir, join4(extensionsDir, "kandan.document-state-telemetry"));
|
|
5090
6237
|
}
|
|
@@ -5117,6 +6264,12 @@ function prepareCodeServerLaunch(options) {
|
|
|
5117
6264
|
"-p",
|
|
5118
6265
|
codeServerSandboxProfile(options, codeServerExecutable.directory),
|
|
5119
6266
|
"--",
|
|
6267
|
+
"/bin/sh",
|
|
6268
|
+
"-c",
|
|
6269
|
+
'export HOME="$1"; export PWD="$1"; export TMPDIR="$2"; export TMP="$2"; export TEMP="$2"; shift 2; exec "$@"',
|
|
6270
|
+
"kandan-code-server-env",
|
|
6271
|
+
options.cwd,
|
|
6272
|
+
join4(options.userDataDir, "tmp"),
|
|
5120
6273
|
codeServerExecutable.command,
|
|
5121
6274
|
...codeServerArgs(options.port, options.cwd, options.userDataDir, options.extensionsDir)
|
|
5122
6275
|
]
|
|
@@ -5200,7 +6353,6 @@ function codeServerSandboxProfile(options, codeServerBinDir) {
|
|
|
5200
6353
|
"/etc",
|
|
5201
6354
|
"/private/etc",
|
|
5202
6355
|
"/opt/homebrew",
|
|
5203
|
-
"/dev",
|
|
5204
6356
|
...options.codeServerRuntimeRoot === undefined ? [] : sandboxPathAliases(options.codeServerRuntimeRoot),
|
|
5205
6357
|
codeServerBinDir
|
|
5206
6358
|
]);
|
|
@@ -5223,6 +6375,7 @@ function codeServerSandboxProfile(options, codeServerBinDir) {
|
|
|
5223
6375
|
"(allow file-map-executable)",
|
|
5224
6376
|
'(allow file-read* (literal "/") (literal "/private") (literal "/private/var"))',
|
|
5225
6377
|
`(allow file-read* ${readOnlyRoots.map(sandboxSubpath).join(" ")})`,
|
|
6378
|
+
'(allow file-read* file-write* (subpath "/dev"))',
|
|
5226
6379
|
`(allow file-read* file-write* ${readWriteRoots.map(sandboxSubpath).join(" ")})`
|
|
5227
6380
|
].join(`
|
|
5228
6381
|
`);
|
|
@@ -5397,10 +6550,14 @@ function installDirectory(sourceDir, destinationDir) {
|
|
|
5397
6550
|
}
|
|
5398
6551
|
function codeServerEnv(env, cwd, userDataDir, collaboration) {
|
|
5399
6552
|
const { PORT: _port, ...hostEnv } = env;
|
|
6553
|
+
const tempDir = join4(userDataDir, "tmp");
|
|
5400
6554
|
const base = {
|
|
5401
6555
|
...hostEnv,
|
|
5402
6556
|
HOME: cwd,
|
|
5403
6557
|
PWD: cwd,
|
|
6558
|
+
TMPDIR: tempDir,
|
|
6559
|
+
TMP: tempDir,
|
|
6560
|
+
TEMP: tempDir,
|
|
5404
6561
|
XDG_CACHE_HOME: join4(userDataDir, "xdg-cache"),
|
|
5405
6562
|
XDG_CONFIG_HOME: join4(userDataDir, "xdg-config"),
|
|
5406
6563
|
XDG_DATA_HOME: join4(userDataDir, "xdg-data")
|
|
@@ -5789,7 +6946,7 @@ async function exchangeCodeForToken(args) {
|
|
|
5789
6946
|
};
|
|
5790
6947
|
}
|
|
5791
6948
|
function stringBodyField(body, key) {
|
|
5792
|
-
const value =
|
|
6949
|
+
const value = body[key];
|
|
5793
6950
|
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
5794
6951
|
}
|
|
5795
6952
|
function startCallbackServer(args) {
|
|
@@ -5859,7 +7016,9 @@ function isTcpAddress(address) {
|
|
|
5859
7016
|
return typeof address === "object" && address !== null && typeof address.port === "number";
|
|
5860
7017
|
}
|
|
5861
7018
|
function writeOauthResult(response, args) {
|
|
5862
|
-
response.writeHead(args.status, {
|
|
7019
|
+
response.writeHead(args.status, {
|
|
7020
|
+
"content-type": "text/html; charset=utf-8"
|
|
7021
|
+
});
|
|
5863
7022
|
response.end(oauthResultHtml(args));
|
|
5864
7023
|
}
|
|
5865
7024
|
function oauthResultHtml(args) {
|
|
@@ -6474,7 +7633,7 @@ function assertStartDependencies(status) {
|
|
|
6474
7633
|
throw new Error(`Codex is not available at ${status.codex.command}. Install Codex or pass --codex-bin <path>.`);
|
|
6475
7634
|
}
|
|
6476
7635
|
if (status.editorRuntime?.status === "unavailable" || status.codeServer?.available === false && status.codeServer.reason !== "not_configured") {
|
|
6477
|
-
throw new Error("The
|
|
7636
|
+
throw new Error("The Linzumi editor runtime is not available. Reconnect when the runtime update finishes or pass --code-server-bin <path> for a custom development runtime.");
|
|
6478
7637
|
}
|
|
6479
7638
|
}
|
|
6480
7639
|
function dependencyStatusPayload(status) {
|
|
@@ -6858,7 +8017,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6858
8017
|
forwardPortAttributions.set(port, {
|
|
6859
8018
|
kandanThreadId: attribution.kandanThreadId ?? null,
|
|
6860
8019
|
codexThreadId: attribution.codexThreadId ?? null,
|
|
6861
|
-
channelSlug: attribution.channelSlug ?? null
|
|
8020
|
+
channelSlug: attribution.channelSlug ?? null,
|
|
8021
|
+
processName: attribution.processName ?? null,
|
|
8022
|
+
processIconKey: attribution.processIconKey ?? null
|
|
6862
8023
|
});
|
|
6863
8024
|
};
|
|
6864
8025
|
const clearForwardPortAttribution = (port) => {
|
|
@@ -6870,10 +8031,15 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6870
8031
|
port,
|
|
6871
8032
|
kandanThreadId: attribution?.kandanThreadId ?? null,
|
|
6872
8033
|
codexThreadId: attribution?.codexThreadId ?? null,
|
|
6873
|
-
channelSlug: attribution?.channelSlug ?? null
|
|
8034
|
+
channelSlug: attribution?.channelSlug ?? null,
|
|
8035
|
+
processName: attribution?.processName ?? null,
|
|
8036
|
+
processIconKey: attribution?.processIconKey ?? null
|
|
6874
8037
|
};
|
|
6875
8038
|
});
|
|
6876
8039
|
const allowedCwds = { value: [...options.allowedCwds] };
|
|
8040
|
+
const missingAllowedCwds = {
|
|
8041
|
+
value: normalizeAllowedCwds(options.missingAllowedCwds ?? [])
|
|
8042
|
+
};
|
|
6877
8043
|
const localEditorState = {
|
|
6878
8044
|
value: { status: "disabled" }
|
|
6879
8045
|
};
|
|
@@ -6889,6 +8055,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6889
8055
|
codexRemoteTui: true,
|
|
6890
8056
|
startInstance: allowedCwds.value.length > 0,
|
|
6891
8057
|
allowedCwds: allowedCwds.value,
|
|
8058
|
+
missingAllowedCwds: missingAllowedCwds.value,
|
|
6892
8059
|
allowedCwdSuggestions: allowedCwdSuggestions(options.cwd, allowedCwds.value),
|
|
6893
8060
|
portForwarding: liveForwardPorts.size > 0,
|
|
6894
8061
|
allowedPorts: Array.from(liveForwardPorts).sort((left, right) => left - right),
|
|
@@ -6903,7 +8070,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6903
8070
|
const joinPayload = () => ({
|
|
6904
8071
|
clientName: "kandan-local-codex-runner",
|
|
6905
8072
|
version: "0.0.1",
|
|
6906
|
-
workspace: options
|
|
8073
|
+
workspace: runnerWorkspaceSlug(options) ?? null,
|
|
6907
8074
|
channel: options.channelSession?.channelSlug ?? null,
|
|
6908
8075
|
capabilities: capabilitiesPayload()
|
|
6909
8076
|
});
|
|
@@ -6921,7 +8088,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6921
8088
|
const started = options.codexUrl === undefined ? await startOwnedCodexAppServer(options) : undefined;
|
|
6922
8089
|
if (started !== undefined) {
|
|
6923
8090
|
cleanup.actions.push(() => {
|
|
6924
|
-
started.
|
|
8091
|
+
started.stop();
|
|
6925
8092
|
});
|
|
6926
8093
|
}
|
|
6927
8094
|
const codexUrl = options.codexUrl ?? started?.url;
|
|
@@ -6971,7 +8138,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6971
8138
|
cleanup.actions.push(() => codex.close());
|
|
6972
8139
|
const seq = { value: 0 };
|
|
6973
8140
|
const codexThreads = options.channelSession === undefined ? await discoverCodexThreads(codex, options.cwd) : [];
|
|
8141
|
+
const discoveredCodexThreads = { value: codexThreads };
|
|
6974
8142
|
const runnerHost = hostname2();
|
|
8143
|
+
const runtimeDefaults = runnerRuntimeDefaults(options);
|
|
6975
8144
|
const instancePayload = {
|
|
6976
8145
|
instanceId,
|
|
6977
8146
|
codexUrl,
|
|
@@ -6979,8 +8148,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6979
8148
|
cwd: options.cwd,
|
|
6980
8149
|
hostname: runnerHost,
|
|
6981
8150
|
codexThreads,
|
|
6982
|
-
|
|
6983
|
-
|
|
8151
|
+
workspace: runnerWorkspaceSlug(options) ?? null,
|
|
8152
|
+
channel: options.channelSession?.channelSlug ?? null,
|
|
8153
|
+
model: runtimeDefaults.model ?? null,
|
|
8154
|
+
reasoningEffort: runtimeDefaults.reasoningEffort ?? null,
|
|
6984
8155
|
fast: options.fast ?? false
|
|
6985
8156
|
};
|
|
6986
8157
|
await kandan.push(topic, "instance_started", instancePayload);
|
|
@@ -7019,17 +8190,22 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7019
8190
|
kandan.onReconnect(() => channelSession.handleKandanReconnect());
|
|
7020
8191
|
}
|
|
7021
8192
|
const dynamicChannelSessions = new Map;
|
|
8193
|
+
kandan.onReconnect(() => rebindDynamicChannelSessionsOnReconnect(dynamicChannelSessions.values()));
|
|
7022
8194
|
cleanup.actions.push(async () => {
|
|
7023
8195
|
await Promise.all(Array.from(dynamicChannelSessions.values(), (session) => session.close()));
|
|
7024
8196
|
dynamicChannelSessions.clear();
|
|
7025
8197
|
});
|
|
7026
|
-
const
|
|
7027
|
-
const workspaceSlug =
|
|
7028
|
-
const channelSlug =
|
|
7029
|
-
const kandanThreadId =
|
|
7030
|
-
if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined
|
|
8198
|
+
const attachThreadSession = async (control, cwd, codexThreadId) => {
|
|
8199
|
+
const workspaceSlug = optionalThreadControlField(control, "workspace");
|
|
8200
|
+
const channelSlug = optionalThreadControlField(control, "channel");
|
|
8201
|
+
const kandanThreadId = optionalThreadControlField(control, "threadId");
|
|
8202
|
+
if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined) {
|
|
7031
8203
|
return;
|
|
7032
8204
|
}
|
|
8205
|
+
const existingSession = dynamicChannelSessions.get(kandanThreadId);
|
|
8206
|
+
if (existingSession !== undefined) {
|
|
8207
|
+
return existingSession;
|
|
8208
|
+
}
|
|
7033
8209
|
const listenUser = options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
|
|
7034
8210
|
if (listenUser === undefined) {
|
|
7035
8211
|
throw new Error("missing listen user for Commander-started Codex session");
|
|
@@ -7083,12 +8259,13 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7083
8259
|
codexUrl,
|
|
7084
8260
|
cwd: options.cwd,
|
|
7085
8261
|
hostname: runnerHost,
|
|
7086
|
-
workspace: options
|
|
8262
|
+
workspace: runnerWorkspaceSlug(options) ?? null,
|
|
7087
8263
|
channel: options.channelSession?.channelSlug ?? null,
|
|
7088
8264
|
threadId: channelSession?.currentKandanThreadId() ?? null,
|
|
7089
8265
|
codexThreadId: channelSession?.currentCodexThreadId() ?? null,
|
|
7090
|
-
|
|
7091
|
-
|
|
8266
|
+
codexThreads: discoveredCodexThreads.value,
|
|
8267
|
+
model: runtimeDefaults.model ?? null,
|
|
8268
|
+
reasoningEffort: runtimeDefaults.reasoningEffort ?? null,
|
|
7092
8269
|
fast: options.fast ?? false,
|
|
7093
8270
|
capabilities: capabilitiesPayload()
|
|
7094
8271
|
});
|
|
@@ -7097,11 +8274,19 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7097
8274
|
message: error instanceof Error ? error.message : String(error)
|
|
7098
8275
|
});
|
|
7099
8276
|
});
|
|
8277
|
+
const refreshDiscoveredCodexThreads = () => discoverCodexThreads(codex, options.cwd).then((threads) => {
|
|
8278
|
+
discoveredCodexThreads.value = threads;
|
|
8279
|
+
return kandan.push(topic, "heartbeat", heartbeatPayload());
|
|
8280
|
+
}).catch((error) => {
|
|
8281
|
+
log("kandan.codex_threads_refresh_failed", {
|
|
8282
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8283
|
+
});
|
|
8284
|
+
});
|
|
7100
8285
|
const heartbeatInterval = setInterval(() => {
|
|
7101
|
-
pushHeartbeat();
|
|
8286
|
+
channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat();
|
|
7102
8287
|
}, 15000);
|
|
7103
8288
|
cleanup.actions.push(() => clearInterval(heartbeatInterval));
|
|
7104
|
-
kandan.onReconnect(() => pushHeartbeat().then(() => {
|
|
8289
|
+
kandan.onReconnect(() => (channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat()).then(() => {
|
|
7105
8290
|
return;
|
|
7106
8291
|
}));
|
|
7107
8292
|
pushHeartbeat();
|
|
@@ -7135,6 +8320,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7135
8320
|
});
|
|
7136
8321
|
});
|
|
7137
8322
|
}
|
|
8323
|
+
if (channelSession === undefined && notification.method === "thread/started") {
|
|
8324
|
+
refreshDiscoveredCodexThreads();
|
|
8325
|
+
}
|
|
7138
8326
|
log("codex.notification", {
|
|
7139
8327
|
method: notification.method,
|
|
7140
8328
|
metadata
|
|
@@ -7242,7 +8430,33 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7242
8430
|
return;
|
|
7243
8431
|
}
|
|
7244
8432
|
if (isUpdateRunnerConfigControl(control)) {
|
|
7245
|
-
|
|
8433
|
+
const updatedAllowedCwds = configuredAllowedCwds(control.allowedCwds);
|
|
8434
|
+
allowedCwds.value = updatedAllowedCwds.allowedCwds;
|
|
8435
|
+
missingAllowedCwds.value = updatedAllowedCwds.missingAllowedCwds;
|
|
8436
|
+
pushHeartbeat();
|
|
8437
|
+
return;
|
|
8438
|
+
}
|
|
8439
|
+
if (isSetPortForwardEnabledControl(control)) {
|
|
8440
|
+
switch (control.enabled) {
|
|
8441
|
+
case true:
|
|
8442
|
+
liveForwardPorts.add(control.port);
|
|
8443
|
+
break;
|
|
8444
|
+
case false:
|
|
8445
|
+
liveForwardPorts.delete(control.port);
|
|
8446
|
+
managedForwardPorts.delete(control.port);
|
|
8447
|
+
clearForwardPortAttribution(control.port);
|
|
8448
|
+
kandan.push(topic, "forward_port_revoked", {
|
|
8449
|
+
instanceId,
|
|
8450
|
+
port: control.port,
|
|
8451
|
+
reason: "user_disabled",
|
|
8452
|
+
capabilities: revocationCapabilities(capabilitiesPayload(), control.port)
|
|
8453
|
+
}).catch((error) => {
|
|
8454
|
+
log("kandan.forward_port_revoked_push_failed", {
|
|
8455
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8456
|
+
});
|
|
8457
|
+
});
|
|
8458
|
+
break;
|
|
8459
|
+
}
|
|
7246
8460
|
pushHeartbeat();
|
|
7247
8461
|
return;
|
|
7248
8462
|
}
|
|
@@ -7250,7 +8464,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7250
8464
|
if (handled !== undefined) {
|
|
7251
8465
|
return handled;
|
|
7252
8466
|
}
|
|
7253
|
-
return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control,
|
|
8467
|
+
return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control, log, attachThreadSession);
|
|
7254
8468
|
}).then((response) => {
|
|
7255
8469
|
return kandan.push(topic, "codex_response", response);
|
|
7256
8470
|
}).catch((error) => {
|
|
@@ -7315,6 +8529,8 @@ async function discoverCodexThreads(codex, cwd) {
|
|
|
7315
8529
|
const data = arrayValue(result?.data);
|
|
7316
8530
|
return data === undefined ? [] : data.filter(isJsonObject).map((thread) => ({
|
|
7317
8531
|
id: stringValue(thread.id) ?? "",
|
|
8532
|
+
title: stringValue(thread.title) ?? stringValue(thread.name) ?? stringValue(thread.preview) ?? "",
|
|
8533
|
+
description: stringValue(thread.description) ?? stringValue(thread.summary) ?? stringValue(thread.preview) ?? "",
|
|
7318
8534
|
preview: stringValue(thread.preview) ?? "",
|
|
7319
8535
|
cwd: stringValue(thread.cwd) ?? "",
|
|
7320
8536
|
source: stringValue(thread.source) ?? "",
|
|
@@ -7324,7 +8540,7 @@ async function discoverCodexThreads(codex, cwd) {
|
|
|
7324
8540
|
}
|
|
7325
8541
|
function extractStartedThreadId(response) {
|
|
7326
8542
|
if ("error" in response) {
|
|
7327
|
-
|
|
8543
|
+
throw new Error(`thread/start failed: ${response.error.message}`);
|
|
7328
8544
|
}
|
|
7329
8545
|
return stringValue(objectValue(objectValue(response.result)?.thread)?.id);
|
|
7330
8546
|
}
|
|
@@ -7390,6 +8606,9 @@ function launchCodexTui(codexBin, codexUrl, cwd, codexThreadId, session, fast) {
|
|
|
7390
8606
|
});
|
|
7391
8607
|
return child;
|
|
7392
8608
|
}
|
|
8609
|
+
async function rebindDynamicChannelSessionsOnReconnect(sessions) {
|
|
8610
|
+
await Promise.all(Array.from(sessions, (session) => session.handleKandanReconnect()));
|
|
8611
|
+
}
|
|
7393
8612
|
function forwardedHeaderValue(headers, name) {
|
|
7394
8613
|
if (!Array.isArray(headers)) {
|
|
7395
8614
|
return;
|
|
@@ -7431,9 +8650,85 @@ async function prepareCodexThreadForTuiResume(codex, codexThreadId) {
|
|
|
7431
8650
|
throw new Error(`failed to verify Codex TUI resume: ${verified.error.message}`);
|
|
7432
8651
|
}
|
|
7433
8652
|
}
|
|
7434
|
-
async function applyControl(codex, kandan, topic, instanceId, options, allowedCwds, control, onStartedThread) {
|
|
8653
|
+
async function applyControl(codex, kandan, topic, instanceId, options, allowedCwds, control, log, onStartedThread) {
|
|
7435
8654
|
switch (control.type) {
|
|
7436
8655
|
case "start_instance": {
|
|
8656
|
+
const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
|
|
8657
|
+
if (!cwd.ok) {
|
|
8658
|
+
return {
|
|
8659
|
+
instanceId,
|
|
8660
|
+
controlType: control.type,
|
|
8661
|
+
ok: false,
|
|
8662
|
+
error: cwd.reason
|
|
8663
|
+
};
|
|
8664
|
+
}
|
|
8665
|
+
const processingStatePayload = startInstanceMessageStatePayload(control, "processing", "starting Codex session");
|
|
8666
|
+
if (processingStatePayload !== undefined) {
|
|
8667
|
+
kandan.push(topic, "message_state", processingStatePayload).catch((error) => {
|
|
8668
|
+
log("kandan.start_instance_processing_state_push_failed", {
|
|
8669
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8670
|
+
});
|
|
8671
|
+
});
|
|
8672
|
+
}
|
|
8673
|
+
try {
|
|
8674
|
+
if (options.codexUrl === undefined) {
|
|
8675
|
+
ensureCodexProjectTrusted(cwd.cwd);
|
|
8676
|
+
}
|
|
8677
|
+
const developerPrompt = normalizedWorkDescription(control.developerPrompt);
|
|
8678
|
+
const runtimeSettings = startInstanceRuntimeSettings(options, control);
|
|
8679
|
+
const response = await codex.request("thread/start", {
|
|
8680
|
+
cwd: cwd.cwd,
|
|
8681
|
+
serviceName: "kandan-local-runner",
|
|
8682
|
+
personality: "pragmatic",
|
|
8683
|
+
developerInstructions: commanderDeveloperInstructions({
|
|
8684
|
+
cwd: cwd.cwd,
|
|
8685
|
+
developerPrompt
|
|
8686
|
+
}),
|
|
8687
|
+
...runtimeSettings.model === undefined ? {} : { model: runtimeSettings.model },
|
|
8688
|
+
...runtimeSettings.reasoningEffort === undefined ? {} : { reasoningEffort: runtimeSettings.reasoningEffort },
|
|
8689
|
+
...runtimeSettings.approvalPolicy === undefined ? {} : { approvalPolicy: runtimeSettings.approvalPolicy },
|
|
8690
|
+
...runtimeSettings.sandbox === undefined ? {} : { sandbox: runtimeSettings.sandbox },
|
|
8691
|
+
...runtimeSettings.fast === true ? { serviceTier: "fast" } : {}
|
|
8692
|
+
});
|
|
8693
|
+
const codexThreadId = extractStartedThreadId(response);
|
|
8694
|
+
const workDescription = normalizedWorkDescription(control.workDescription);
|
|
8695
|
+
if (codexThreadId !== undefined && developerPrompt !== undefined) {
|
|
8696
|
+
await postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId);
|
|
8697
|
+
}
|
|
8698
|
+
const startedThreadSession = codexThreadId !== undefined && onStartedThread !== undefined ? await onStartedThread(control, cwd.cwd, codexThreadId) : undefined;
|
|
8699
|
+
if (codexThreadId !== undefined && workDescription !== undefined) {
|
|
8700
|
+
const rootSeq = integerValue(control.rootSeq);
|
|
8701
|
+
const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
|
|
8702
|
+
if (startedThreadSession !== undefined && sourceSeq !== undefined) {
|
|
8703
|
+
const identity = identityFromAccessToken(options.token);
|
|
8704
|
+
await startedThreadSession.startThreadMessageTurn({
|
|
8705
|
+
seq: sourceSeq,
|
|
8706
|
+
body: workDescription,
|
|
8707
|
+
actorSlug: identity.actorUsername,
|
|
8708
|
+
actorUserId: identity.actorUserId
|
|
8709
|
+
});
|
|
8710
|
+
} else {
|
|
8711
|
+
await codex.request("turn/start", {
|
|
8712
|
+
threadId: codexThreadId,
|
|
8713
|
+
input: [{ type: "text", text: workDescription }]
|
|
8714
|
+
});
|
|
8715
|
+
}
|
|
8716
|
+
}
|
|
8717
|
+
return {
|
|
8718
|
+
instanceId,
|
|
8719
|
+
controlType: control.type,
|
|
8720
|
+
cwd: cwd.cwd,
|
|
8721
|
+
matchedRoot: cwd.matchedRoot,
|
|
8722
|
+
response
|
|
8723
|
+
};
|
|
8724
|
+
} catch (error) {
|
|
8725
|
+
try {
|
|
8726
|
+
await publishStartInstanceMessageState(kandan, topic, control, "failed", "failed to start Codex session");
|
|
8727
|
+
} catch (_publishError) {}
|
|
8728
|
+
throw error;
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
8731
|
+
case "reconnect_thread": {
|
|
7437
8732
|
const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
|
|
7438
8733
|
if (!cwd.ok) {
|
|
7439
8734
|
return {
|
|
@@ -7446,51 +8741,37 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
|
|
|
7446
8741
|
if (options.codexUrl === undefined) {
|
|
7447
8742
|
ensureCodexProjectTrusted(cwd.cwd);
|
|
7448
8743
|
}
|
|
7449
|
-
const
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
developerPrompt
|
|
7458
|
-
}),
|
|
7459
|
-
...runtimeSettings.model === undefined ? {} : { model: runtimeSettings.model },
|
|
7460
|
-
...runtimeSettings.reasoningEffort === undefined ? {} : { reasoningEffort: runtimeSettings.reasoningEffort },
|
|
7461
|
-
...runtimeSettings.approvalPolicy === undefined ? {} : { approvalPolicy: runtimeSettings.approvalPolicy },
|
|
7462
|
-
...runtimeSettings.sandbox === undefined ? {} : { sandbox: runtimeSettings.sandbox },
|
|
7463
|
-
...runtimeSettings.fast === true ? { serviceTier: "fast" } : {}
|
|
7464
|
-
});
|
|
7465
|
-
const codexThreadId = extractStartedThreadId(response);
|
|
7466
|
-
const workDescription = normalizedWorkDescription(control.workDescription);
|
|
7467
|
-
if (codexThreadId !== undefined && developerPrompt !== undefined) {
|
|
7468
|
-
await postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId);
|
|
8744
|
+
const codexThreadId = normalizedWorkDescription(control.codexThreadId);
|
|
8745
|
+
if (codexThreadId === undefined) {
|
|
8746
|
+
return {
|
|
8747
|
+
instanceId,
|
|
8748
|
+
controlType: control.type,
|
|
8749
|
+
ok: false,
|
|
8750
|
+
error: "missing_codex_thread_id"
|
|
8751
|
+
};
|
|
7469
8752
|
}
|
|
7470
|
-
const
|
|
7471
|
-
|
|
8753
|
+
const workDescription = normalizedWorkDescription(control.workDescription);
|
|
8754
|
+
const startedThreadSession = onStartedThread === undefined ? undefined : await onStartedThread(control, cwd.cwd, codexThreadId);
|
|
8755
|
+
if (workDescription !== undefined) {
|
|
7472
8756
|
const rootSeq = integerValue(control.rootSeq);
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
seq: rootSeq,
|
|
7477
|
-
body: workDescription,
|
|
7478
|
-
actorSlug: identity.actorUsername,
|
|
7479
|
-
actorUserId: identity.actorUserId
|
|
7480
|
-
});
|
|
7481
|
-
} else {
|
|
7482
|
-
await codex.request("turn/start", {
|
|
7483
|
-
threadId: codexThreadId,
|
|
7484
|
-
input: [{ type: "text", text: workDescription }]
|
|
7485
|
-
});
|
|
8757
|
+
const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
|
|
8758
|
+
if (startedThreadSession === undefined || sourceSeq === undefined) {
|
|
8759
|
+
throw new Error("cannot reconnect a Kandan thread without a session");
|
|
7486
8760
|
}
|
|
8761
|
+
const identity = identityFromAccessToken(options.token);
|
|
8762
|
+
await startedThreadSession.startThreadMessageTurn({
|
|
8763
|
+
seq: sourceSeq,
|
|
8764
|
+
body: workDescription,
|
|
8765
|
+
actorSlug: identity.actorUsername,
|
|
8766
|
+
actorUserId: identity.actorUserId
|
|
8767
|
+
});
|
|
7487
8768
|
}
|
|
7488
8769
|
return {
|
|
7489
8770
|
instanceId,
|
|
7490
8771
|
controlType: control.type,
|
|
7491
8772
|
cwd: cwd.cwd,
|
|
7492
8773
|
matchedRoot: cwd.matchedRoot,
|
|
7493
|
-
|
|
8774
|
+
codexThreadId
|
|
7494
8775
|
};
|
|
7495
8776
|
}
|
|
7496
8777
|
case "start_turn": {
|
|
@@ -7543,6 +8824,8 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
|
|
|
7543
8824
|
case "interrupt_queued_messages":
|
|
7544
8825
|
case "resolve_codex_approval_request":
|
|
7545
8826
|
case "resolve_port_forward_request":
|
|
8827
|
+
case "update_session_settings":
|
|
8828
|
+
case "set_port_forward_enabled":
|
|
7546
8829
|
case "forward_http_request":
|
|
7547
8830
|
case "forward_websocket_open":
|
|
7548
8831
|
case "forward_websocket_send":
|
|
@@ -7608,9 +8891,9 @@ secure tunnel, and keep the Linzumi thread truthful.
|
|
|
7608
8891
|
</task_reminder>`;
|
|
7609
8892
|
}
|
|
7610
8893
|
async function postVisibleDeveloperPrompt(kandan, topic, control, developerPrompt, codexThreadId) {
|
|
7611
|
-
const workspace =
|
|
7612
|
-
const channel =
|
|
7613
|
-
const threadId =
|
|
8894
|
+
const workspace = optionalThreadControlField(control, "workspace");
|
|
8895
|
+
const channel = optionalThreadControlField(control, "channel");
|
|
8896
|
+
const threadId = optionalThreadControlField(control, "threadId");
|
|
7614
8897
|
if (workspace === undefined || channel === undefined || threadId === undefined) {
|
|
7615
8898
|
return;
|
|
7616
8899
|
}
|
|
@@ -7632,33 +8915,116 @@ ${developerPrompt}`,
|
|
|
7632
8915
|
client_message_id: `codex-start-instructions-${threadId}`
|
|
7633
8916
|
});
|
|
7634
8917
|
}
|
|
8918
|
+
async function publishStartInstanceMessageState(kandan, topic, control, status, reason) {
|
|
8919
|
+
const payload = startInstanceMessageStatePayload(control, status, reason);
|
|
8920
|
+
if (payload === undefined) {
|
|
8921
|
+
return;
|
|
8922
|
+
}
|
|
8923
|
+
await kandan.push(topic, "message_state", payload);
|
|
8924
|
+
}
|
|
8925
|
+
function startInstanceMessageStatePayload(control, status, reason) {
|
|
8926
|
+
const rootSeq = integerValue(control.rootSeq);
|
|
8927
|
+
const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
|
|
8928
|
+
if (sourceSeq === undefined) {
|
|
8929
|
+
return;
|
|
8930
|
+
}
|
|
8931
|
+
const workspace = requiredStartInstanceControlField(control, "workspace");
|
|
8932
|
+
const channel = requiredStartInstanceControlField(control, "channel");
|
|
8933
|
+
const threadId = requiredStartInstanceControlField(control, "threadId");
|
|
8934
|
+
return {
|
|
8935
|
+
workspace,
|
|
8936
|
+
channel,
|
|
8937
|
+
thread_id: threadId,
|
|
8938
|
+
seq: sourceSeq,
|
|
8939
|
+
status,
|
|
8940
|
+
reason
|
|
8941
|
+
};
|
|
8942
|
+
}
|
|
8943
|
+
function requiredStartInstanceControlField(control, field) {
|
|
8944
|
+
return requiredThreadControlField(control, field);
|
|
8945
|
+
}
|
|
8946
|
+
function requiredThreadControlField(control, field) {
|
|
8947
|
+
const value = optionalThreadControlField(control, field);
|
|
8948
|
+
if (value === undefined) {
|
|
8949
|
+
throw new Error(`${control.type} control missing ${field}`);
|
|
8950
|
+
}
|
|
8951
|
+
return value;
|
|
8952
|
+
}
|
|
8953
|
+
function optionalThreadControlField(control, field) {
|
|
8954
|
+
const value = stringValue(control[field])?.trim();
|
|
8955
|
+
if (value === undefined || value === "") {
|
|
8956
|
+
return;
|
|
8957
|
+
}
|
|
8958
|
+
return value;
|
|
8959
|
+
}
|
|
7635
8960
|
function startInstanceRuntimeSettings(options, control) {
|
|
7636
|
-
const
|
|
8961
|
+
const defaults = runnerRuntimeDefaults(options);
|
|
7637
8962
|
return {
|
|
7638
|
-
model: control.model ??
|
|
7639
|
-
reasoningEffort: control.reasoningEffort ??
|
|
7640
|
-
approvalPolicy: control.approvalPolicy ??
|
|
7641
|
-
sandbox: control.sandbox ??
|
|
8963
|
+
model: control.model ?? defaults.model,
|
|
8964
|
+
reasoningEffort: control.reasoningEffort ?? defaults.reasoningEffort,
|
|
8965
|
+
approvalPolicy: control.approvalPolicy ?? defaults.approvalPolicy,
|
|
8966
|
+
sandbox: control.sandbox ?? defaults.sandbox,
|
|
7642
8967
|
fast: control.fast ?? options.fast
|
|
7643
8968
|
};
|
|
7644
8969
|
}
|
|
7645
8970
|
async function startOwnedCodexAppServer(options) {
|
|
7646
8971
|
ensureCodexProjectTrusted(options.cwd);
|
|
8972
|
+
const defaults = runnerRuntimeDefaults(options);
|
|
7647
8973
|
return await startCodexAppServer(options.codexBin, options.cwd, {
|
|
7648
|
-
model:
|
|
7649
|
-
reasoningEffort:
|
|
8974
|
+
model: defaults.model,
|
|
8975
|
+
reasoningEffort: defaults.reasoningEffort,
|
|
7650
8976
|
fast: options.fast
|
|
7651
8977
|
});
|
|
7652
8978
|
}
|
|
8979
|
+
function runnerWorkspaceSlug(options) {
|
|
8980
|
+
return options.channelSession?.workspaceSlug ?? options.workspaceSlug;
|
|
8981
|
+
}
|
|
8982
|
+
function runnerRuntimeDefaults(options) {
|
|
8983
|
+
const session = options.channelSession;
|
|
8984
|
+
const defaults = options.runtimeDefaults;
|
|
8985
|
+
return {
|
|
8986
|
+
model: defaults?.model ?? session?.model,
|
|
8987
|
+
reasoningEffort: defaults?.reasoningEffort ?? session?.reasoningEffort,
|
|
8988
|
+
approvalPolicy: defaults?.approvalPolicy ?? session?.approvalPolicy,
|
|
8989
|
+
sandbox: defaults?.sandbox ?? session?.sandbox
|
|
8990
|
+
};
|
|
8991
|
+
}
|
|
7653
8992
|
function isUpdateRunnerConfigControl(control) {
|
|
7654
8993
|
return control.type === "update_runner_config";
|
|
7655
8994
|
}
|
|
8995
|
+
function isSetPortForwardEnabledControl(control) {
|
|
8996
|
+
return control.type === "set_port_forward_enabled" && Number.isInteger(control.port) && control.port > 0 && control.port <= 65535;
|
|
8997
|
+
}
|
|
7656
8998
|
function normalizeAllowedCwds(values) {
|
|
7657
8999
|
return Array.from(new Set(values.flatMap((value) => {
|
|
7658
9000
|
const normalized = value.trim();
|
|
7659
9001
|
return normalized === "" ? [] : [normalized];
|
|
7660
9002
|
})));
|
|
7661
9003
|
}
|
|
9004
|
+
function configuredAllowedCwds(values) {
|
|
9005
|
+
const allowedCwds = [];
|
|
9006
|
+
const missingAllowedCwds = [];
|
|
9007
|
+
for (const value of normalizeAllowedCwds(values)) {
|
|
9008
|
+
const absolutePath = resolve5(expandUserPath(value));
|
|
9009
|
+
try {
|
|
9010
|
+
const realPath = realpathSync4(absolutePath);
|
|
9011
|
+
allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
|
|
9012
|
+
} catch (error) {
|
|
9013
|
+
if (isMissingAllowedCwdError(error)) {
|
|
9014
|
+
missingAllowedCwds.push(absolutePath);
|
|
9015
|
+
continue;
|
|
9016
|
+
}
|
|
9017
|
+
throw error;
|
|
9018
|
+
}
|
|
9019
|
+
}
|
|
9020
|
+
return {
|
|
9021
|
+
allowedCwds: normalizeAllowedCwds(allowedCwds),
|
|
9022
|
+
missingAllowedCwds: normalizeAllowedCwds(missingAllowedCwds)
|
|
9023
|
+
};
|
|
9024
|
+
}
|
|
9025
|
+
function isMissingAllowedCwdError(error) {
|
|
9026
|
+
return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "ELOOP" || error.code === "EIO");
|
|
9027
|
+
}
|
|
7662
9028
|
function allowedCwdSuggestions(cwd, allowedCwds) {
|
|
7663
9029
|
return normalizeAllowedCwds([cwd, ...allowedCwds]);
|
|
7664
9030
|
}
|
|
@@ -7803,12 +9169,18 @@ async function acquireAndCacheToken(args) {
|
|
|
7803
9169
|
}
|
|
7804
9170
|
|
|
7805
9171
|
// src/localConfig.ts
|
|
7806
|
-
import {
|
|
9172
|
+
import {
|
|
9173
|
+
existsSync as existsSync5,
|
|
9174
|
+
mkdirSync as mkdirSync6,
|
|
9175
|
+
readFileSync as readFileSync4,
|
|
9176
|
+
realpathSync as realpathSync5,
|
|
9177
|
+
writeFileSync as writeFileSync5
|
|
9178
|
+
} from "node:fs";
|
|
7807
9179
|
import { homedir as homedir6 } from "node:os";
|
|
7808
|
-
import { dirname as dirname5, resolve as
|
|
9180
|
+
import { dirname as dirname5, resolve as resolve6 } from "node:path";
|
|
7809
9181
|
function localConfigPath(env = process.env) {
|
|
7810
9182
|
const override = env.LINZUMI_CONFIG_FILE;
|
|
7811
|
-
return override !== undefined && override.trim() !== "" ?
|
|
9183
|
+
return override !== undefined && override.trim() !== "" ? resolve6(expandUserPath(override)) : resolve6(homedir6(), ".linzumi", "config.json");
|
|
7812
9184
|
}
|
|
7813
9185
|
function readLocalConfig(path = localConfigPath()) {
|
|
7814
9186
|
if (!existsSync5(path)) {
|
|
@@ -7823,24 +9195,36 @@ function readLocalConfig(path = localConfigPath()) {
|
|
|
7823
9195
|
allowedCwds: uniqueStrings(parsed.allowedCwds)
|
|
7824
9196
|
};
|
|
7825
9197
|
}
|
|
7826
|
-
function
|
|
7827
|
-
|
|
9198
|
+
function readConfiguredAllowedCwdDetails(path = localConfigPath()) {
|
|
9199
|
+
const allowedCwds = [];
|
|
9200
|
+
const missingAllowedCwds = [];
|
|
9201
|
+
for (const cwd of readLocalConfig(path).allowedCwds) {
|
|
9202
|
+
const absolutePath = resolve6(expandUserPath(cwd));
|
|
7828
9203
|
try {
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
9204
|
+
const realPath = realpathSync5(absolutePath);
|
|
9205
|
+
allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
|
|
9206
|
+
} catch (error) {
|
|
9207
|
+
if (isMissingPathError(error)) {
|
|
9208
|
+
missingAllowedCwds.push(absolutePath);
|
|
9209
|
+
continue;
|
|
9210
|
+
}
|
|
9211
|
+
throw error;
|
|
7832
9212
|
}
|
|
7833
|
-
}
|
|
9213
|
+
}
|
|
9214
|
+
return {
|
|
9215
|
+
allowedCwds: uniqueStrings(allowedCwds),
|
|
9216
|
+
missingAllowedCwds: uniqueStrings(missingAllowedCwds)
|
|
9217
|
+
};
|
|
7834
9218
|
}
|
|
7835
9219
|
function addAllowedCwd(pathValue, path = localConfigPath()) {
|
|
7836
|
-
const normalizedPath =
|
|
9220
|
+
const normalizedPath = realpathSync5(resolve6(expandUserPath(pathValue)));
|
|
7837
9221
|
const config = readLocalConfig(path);
|
|
7838
9222
|
const allowedCwds = uniqueStrings([...config.allowedCwds, normalizedPath]);
|
|
7839
9223
|
writeLocalConfig({ version: 1, allowedCwds }, path);
|
|
7840
9224
|
return allowedCwds;
|
|
7841
9225
|
}
|
|
7842
9226
|
function removeAllowedCwd(pathValue, path = localConfigPath()) {
|
|
7843
|
-
const requestedPath =
|
|
9227
|
+
const requestedPath = resolve6(expandUserPath(pathValue));
|
|
7844
9228
|
const normalizedRequest = realpathOrResolved(requestedPath);
|
|
7845
9229
|
const config = readLocalConfig(path);
|
|
7846
9230
|
const allowedCwds = config.allowedCwds.filter((cwd) => {
|
|
@@ -7859,13 +9243,18 @@ function isConfigPayload(value) {
|
|
|
7859
9243
|
return typeof value === "object" && value !== null && value.version === 1 && Array.isArray(value.allowedCwds) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
|
|
7860
9244
|
}
|
|
7861
9245
|
function uniqueStrings(values) {
|
|
7862
|
-
return [
|
|
9246
|
+
return [
|
|
9247
|
+
...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))
|
|
9248
|
+
];
|
|
9249
|
+
}
|
|
9250
|
+
function isMissingPathError(error) {
|
|
9251
|
+
return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "EACCES" || error.code === "ELOOP" || error.code === "EIO");
|
|
7863
9252
|
}
|
|
7864
9253
|
function realpathOrResolved(pathValue) {
|
|
7865
9254
|
try {
|
|
7866
|
-
return
|
|
9255
|
+
return realpathSync5(resolve6(expandUserPath(pathValue)));
|
|
7867
9256
|
} catch (_error) {
|
|
7868
|
-
return
|
|
9257
|
+
return resolve6(expandUserPath(pathValue));
|
|
7869
9258
|
}
|
|
7870
9259
|
}
|
|
7871
9260
|
|
|
@@ -8263,7 +9652,7 @@ async function runClaim(command, deps) {
|
|
|
8263
9652
|
workspaceName: stringValue(response.workspace_name),
|
|
8264
9653
|
channelId: stringValue(response.channel_id),
|
|
8265
9654
|
ownerUsername: stringValue(response.owner_username),
|
|
8266
|
-
channelUrl:
|
|
9655
|
+
channelUrl: stringValue(response.channel_url),
|
|
8267
9656
|
loginUrl: requiredString(response, "login_url"),
|
|
8268
9657
|
supportChannelId: requiredString(response, "support_channel_id"),
|
|
8269
9658
|
supportChannelUrl: requiredString(response, "support_channel_url"),
|
|
@@ -8282,8 +9671,10 @@ async function runClaim(command, deps) {
|
|
|
8282
9671
|
deps.stdout.write(`login_url: ${tokenFile.loginUrl}
|
|
8283
9672
|
`);
|
|
8284
9673
|
writeHumanLoginUrlWarning(deps);
|
|
8285
|
-
|
|
9674
|
+
if (tokenFile.channelUrl !== undefined) {
|
|
9675
|
+
deps.stdout.write(`channel_url: ${tokenFile.channelUrl}
|
|
8286
9676
|
`);
|
|
9677
|
+
}
|
|
8287
9678
|
deps.stdout.write(`support_channel_id: ${tokenFile.supportChannelId}
|
|
8288
9679
|
`);
|
|
8289
9680
|
deps.stdout.write(`support_channel_url: ${tokenFile.supportChannelUrl}
|
|
@@ -8552,7 +9943,7 @@ function readStoredAgentTokenFile(path, readTextFile = readOptionalTextFile) {
|
|
|
8552
9943
|
workspaceName: stringValue(parsed.workspaceName),
|
|
8553
9944
|
channelId: stringValue(parsed.channelId),
|
|
8554
9945
|
ownerUsername: stringValue(parsed.ownerUsername),
|
|
8555
|
-
channelUrl:
|
|
9946
|
+
channelUrl: stringValue(parsed.channelUrl),
|
|
8556
9947
|
loginUrl: requiredString(parsed, "loginUrl"),
|
|
8557
9948
|
supportChannelId: requiredString(parsed, "supportChannelId"),
|
|
8558
9949
|
supportChannelUrl: stringValue(parsed.supportChannelUrl),
|
|
@@ -8606,7 +9997,7 @@ Launch target:
|
|
|
8606
9997
|
|
|
8607
9998
|
// src/helloLinzumiProject.ts
|
|
8608
9999
|
import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
8609
|
-
import { dirname as dirname7, join as join9, resolve as
|
|
10000
|
+
import { dirname as dirname7, join as join9, resolve as resolve7 } from "node:path";
|
|
8610
10001
|
import { fileURLToPath } from "node:url";
|
|
8611
10002
|
var defaultHelloLinzumiProjectDir = "/tmp/hello_linzumi";
|
|
8612
10003
|
var defaultHelloLinzumiProjectName = "hello_linzumi";
|
|
@@ -8640,10 +10031,10 @@ function resolveHelloProjectRoot(options) {
|
|
|
8640
10031
|
throw new Error("linzumi init-hello-linzumi-demo-app accepts either --dir or --parent-dir/--name, not both");
|
|
8641
10032
|
}
|
|
8642
10033
|
if (options.rootPath !== undefined) {
|
|
8643
|
-
return
|
|
10034
|
+
return resolve7(options.rootPath);
|
|
8644
10035
|
}
|
|
8645
10036
|
const name = normalizeProjectName(options.name ?? defaultHelloLinzumiProjectName);
|
|
8646
|
-
return
|
|
10037
|
+
return resolve7(options.parentDir ?? defaultHelloLinzumiParentDir, name);
|
|
8647
10038
|
}
|
|
8648
10039
|
function normalizeProjectName(value) {
|
|
8649
10040
|
const name = value.trim();
|
|
@@ -9178,7 +10569,7 @@ import {
|
|
|
9178
10569
|
writeFileSync as writeFileSync8
|
|
9179
10570
|
} from "node:fs";
|
|
9180
10571
|
import { homedir as homedir8 } from "node:os";
|
|
9181
|
-
import { dirname as dirname8, join as join10, resolve as
|
|
10572
|
+
import { dirname as dirname8, join as join10, resolve as resolve8 } from "node:path";
|
|
9182
10573
|
import { execFileSync, spawn as spawn7 } from "node:child_process";
|
|
9183
10574
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
9184
10575
|
var connectedMarker = "Runner connected:";
|
|
@@ -9194,7 +10585,7 @@ function defaultCommanderLogFile(runnerId, statusDir = commanderStatusDir()) {
|
|
|
9194
10585
|
function startCommanderDaemon(options) {
|
|
9195
10586
|
const statusDir = options.statusDir ?? commanderStatusDir();
|
|
9196
10587
|
const statusFile = commanderStatusFile(options.runnerId, statusDir);
|
|
9197
|
-
const logFile =
|
|
10588
|
+
const logFile = resolve8(options.logFile ?? defaultCommanderLogFile(options.runnerId, statusDir));
|
|
9198
10589
|
const entrypoint = options.entrypoint ?? currentEntrypoint();
|
|
9199
10590
|
const nodeBin = options.nodeBin ?? process.execPath;
|
|
9200
10591
|
const command = [
|
|
@@ -9375,7 +10766,9 @@ function readProcessIdentity(pid) {
|
|
|
9375
10766
|
if (match === null) {
|
|
9376
10767
|
return { command: output };
|
|
9377
10768
|
}
|
|
9378
|
-
|
|
10769
|
+
const startedAt = match[1];
|
|
10770
|
+
const processCommand = match[2];
|
|
10771
|
+
return startedAt === undefined || processCommand === undefined ? { command: output } : { startedAt, command: processCommand };
|
|
9379
10772
|
} catch (_error) {
|
|
9380
10773
|
return;
|
|
9381
10774
|
}
|
|
@@ -9389,7 +10782,7 @@ function safeRunnerId(runnerId) {
|
|
|
9389
10782
|
}
|
|
9390
10783
|
async function waitForFileChangeOrTimeout(path, deadline, now, ready2 = () => false) {
|
|
9391
10784
|
const remaining = Math.max(0, deadline - now());
|
|
9392
|
-
await new Promise((
|
|
10785
|
+
await new Promise((resolve9) => {
|
|
9393
10786
|
let resolved = false;
|
|
9394
10787
|
let watcher;
|
|
9395
10788
|
const finish = () => {
|
|
@@ -9399,7 +10792,7 @@ async function waitForFileChangeOrTimeout(path, deadline, now, ready2 = () => fa
|
|
|
9399
10792
|
resolved = true;
|
|
9400
10793
|
watcher?.close();
|
|
9401
10794
|
clearTimeout(timer);
|
|
9402
|
-
|
|
10795
|
+
resolve9();
|
|
9403
10796
|
};
|
|
9404
10797
|
const timer = setTimeout(finish, remaining);
|
|
9405
10798
|
try {
|
|
@@ -9479,7 +10872,7 @@ function isMainModule() {
|
|
|
9479
10872
|
if (scriptPath === undefined) {
|
|
9480
10873
|
return false;
|
|
9481
10874
|
}
|
|
9482
|
-
return fileURLToPath3(import.meta.url) ===
|
|
10875
|
+
return fileURLToPath3(import.meta.url) === resolve9(scriptPath);
|
|
9483
10876
|
}
|
|
9484
10877
|
async function main(args) {
|
|
9485
10878
|
const parsed = parseCommand(args);
|
|
@@ -9488,7 +10881,7 @@ async function main(args) {
|
|
|
9488
10881
|
process.stdout.write(connectGuideText());
|
|
9489
10882
|
return;
|
|
9490
10883
|
case "version":
|
|
9491
|
-
process.stdout.write(`linzumi 0.0.
|
|
10884
|
+
process.stdout.write(`linzumi 0.0.40-beta
|
|
9492
10885
|
`);
|
|
9493
10886
|
return;
|
|
9494
10887
|
case "auth":
|
|
@@ -9582,12 +10975,17 @@ function runHelloCommand(args) {
|
|
|
9582
10975
|
process.stdout.write(helloHelpText());
|
|
9583
10976
|
return;
|
|
9584
10977
|
}
|
|
10978
|
+
const rootPath = stringValue3(values, "dir");
|
|
10979
|
+
const parentDir = stringValue3(values, "parent-dir");
|
|
10980
|
+
const name = stringValue3(values, "name");
|
|
10981
|
+
const port = tcpPortValue(values, "port");
|
|
10982
|
+
const host = stringValue3(values, "host");
|
|
9585
10983
|
const project = createHelloLinzumiProject({
|
|
9586
|
-
rootPath:
|
|
9587
|
-
parentDir
|
|
9588
|
-
name:
|
|
9589
|
-
port:
|
|
9590
|
-
host:
|
|
10984
|
+
...rootPath === undefined ? {} : { rootPath },
|
|
10985
|
+
...parentDir === undefined ? {} : { parentDir: resolveUserPath(parentDir) },
|
|
10986
|
+
...name === undefined ? {} : { name },
|
|
10987
|
+
...port === undefined ? {} : { port },
|
|
10988
|
+
...host === undefined ? {} : { host },
|
|
9591
10989
|
reset: values.get("reset") === true
|
|
9592
10990
|
});
|
|
9593
10991
|
if (values.get("json") === true) {
|
|
@@ -9636,7 +11034,7 @@ function runPathsCommand(args) {
|
|
|
9636
11034
|
if (pathValue === undefined || pathValue.trim() === "") {
|
|
9637
11035
|
throw new Error("missing path for linzumi paths add");
|
|
9638
11036
|
}
|
|
9639
|
-
const trustedPath =
|
|
11037
|
+
const trustedPath = realpathSync6(resolve9(expandUserPath(pathValue)));
|
|
9640
11038
|
addAllowedCwd(pathValue);
|
|
9641
11039
|
process.stdout.write(`Trusted ${trustedPath}
|
|
9642
11040
|
`);
|
|
@@ -9826,6 +11224,7 @@ async function parseStartRunnerArgs(args, deps = {
|
|
|
9826
11224
|
kandanUrl,
|
|
9827
11225
|
token: targetToken,
|
|
9828
11226
|
runnerId: stringValue3(values, "runner-id") ?? `runner-${randomUUID3()}`,
|
|
11227
|
+
workspaceSlug: target.workspaceSlug,
|
|
9829
11228
|
cwd,
|
|
9830
11229
|
codexBin,
|
|
9831
11230
|
codexUrl: stringValue3(values, "codex-url"),
|
|
@@ -9838,6 +11237,7 @@ async function parseStartRunnerArgs(args, deps = {
|
|
|
9838
11237
|
editorRuntime: editorRuntime.runtime,
|
|
9839
11238
|
socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
|
|
9840
11239
|
dependencyStatus,
|
|
11240
|
+
runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
|
|
9841
11241
|
channelSession: {
|
|
9842
11242
|
workspaceSlug: target.workspaceSlug,
|
|
9843
11243
|
channelSlug: target.channelSlug,
|
|
@@ -9868,12 +11268,24 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
9868
11268
|
}
|
|
9869
11269
|
const tokenFilePath = stringValue3(values, "agent-token-file") ?? defaultAgentTokenFilePath();
|
|
9870
11270
|
const tokenFile = readStoredAgentTokenFile(tokenFilePath, deps.readTextFile);
|
|
9871
|
-
const channelSlug =
|
|
9872
|
-
|
|
11271
|
+
const channelSlug = tokenFile.channelId;
|
|
11272
|
+
rejectWorkspaceCommanderThreadFlags(values, channelSlug);
|
|
11273
|
+
const channelSession = channelSlug === undefined ? undefined : {
|
|
11274
|
+
workspaceSlug: tokenFile.workspaceId,
|
|
11275
|
+
channelSlug,
|
|
11276
|
+
kandanThreadId: stringValue3(values, "linzumi-thread-id"),
|
|
11277
|
+
listenUser: stringValue3(values, "listen-user") ?? requiredStoredOwnerUsername(tokenFile.ownerUsername),
|
|
11278
|
+
model: stringValue3(values, "model"),
|
|
11279
|
+
reasoningEffort: stringValue3(values, "reasoning-effort"),
|
|
11280
|
+
sandbox: stringValue3(values, "sandbox"),
|
|
11281
|
+
approvalPolicy: stringValue3(values, "approval-policy"),
|
|
11282
|
+
streamFlushMs: positiveIntegerValue(values, "stream-flush-ms")
|
|
11283
|
+
};
|
|
9873
11284
|
const kandanUrl = stringValue3(values, "linzumi-url") ?? agentApiUrlToKandanUrl(tokenFile.apiUrl);
|
|
9874
11285
|
const requestedCwdValue = cwdArg ?? stringValue3(values, "cwd");
|
|
9875
11286
|
const requestedCwd = resolveUserPath(requestedCwdValue ?? process.cwd());
|
|
9876
|
-
const
|
|
11287
|
+
const configuredAllowedCwds2 = requestedCwdValue === undefined && !values.has("allowed-cwd") ? readConfiguredAllowedCwdDetails() : { allowedCwds: [], missingAllowedCwds: [] };
|
|
11288
|
+
const allowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : requestedCwdValue === undefined ? configuredAllowedCwds2.allowedCwds.length > 0 ? [...configuredAllowedCwds2.allowedCwds] : assertConfiguredAllowedCwds([requestedCwd]) : assertConfiguredAllowedCwds([requestedCwd]);
|
|
9877
11289
|
const cwd = allowedCwds[0] ?? requestedCwd;
|
|
9878
11290
|
const codexBin = stringValue3(values, "codex-bin") ?? "codex";
|
|
9879
11291
|
const customCodeServerBin = stringValue3(values, "code-server-bin");
|
|
@@ -9900,6 +11312,7 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
9900
11312
|
kandanUrl,
|
|
9901
11313
|
token: tokenFile.commanderToken,
|
|
9902
11314
|
runnerId: stringValue3(values, "runner-id") ?? `agent-runner-${randomUUID3()}`,
|
|
11315
|
+
workspaceSlug: tokenFile.workspaceId,
|
|
9903
11316
|
cwd,
|
|
9904
11317
|
codexBin,
|
|
9905
11318
|
codexUrl: stringValue3(values, "codex-url"),
|
|
@@ -9907,22 +11320,14 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
9907
11320
|
fast: values.get("fast") === true,
|
|
9908
11321
|
logFile: stringValue3(values, "log-file"),
|
|
9909
11322
|
allowedCwds,
|
|
11323
|
+
missingAllowedCwds: configuredAllowedCwds2.missingAllowedCwds,
|
|
9910
11324
|
allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
|
|
9911
11325
|
codeServerBin: editorRuntime.codeServerBin,
|
|
9912
11326
|
editorRuntime: editorRuntime.runtime,
|
|
9913
11327
|
socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
|
|
9914
11328
|
dependencyStatus,
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
channelSlug,
|
|
9918
|
-
kandanThreadId: stringValue3(values, "linzumi-thread-id"),
|
|
9919
|
-
listenUser,
|
|
9920
|
-
model: stringValue3(values, "model"),
|
|
9921
|
-
reasoningEffort: stringValue3(values, "reasoning-effort"),
|
|
9922
|
-
sandbox: stringValue3(values, "sandbox"),
|
|
9923
|
-
approvalPolicy: stringValue3(values, "approval-policy"),
|
|
9924
|
-
streamFlushMs: positiveIntegerValue(values, "stream-flush-ms")
|
|
9925
|
-
}
|
|
11329
|
+
runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
|
|
11330
|
+
channelSession
|
|
9926
11331
|
};
|
|
9927
11332
|
}
|
|
9928
11333
|
function readAgentTokenTextFile(path) {
|
|
@@ -9940,11 +11345,11 @@ function rejectAgentRunnerTargetingFlags(values) {
|
|
|
9940
11345
|
throw new Error(`linzumi commander uses the claimed human Commander token scope; remove ${unsupportedFlags.map((flag) => `--${flag}`).join(", ")}.`);
|
|
9941
11346
|
}
|
|
9942
11347
|
}
|
|
9943
|
-
function
|
|
9944
|
-
if (
|
|
9945
|
-
return
|
|
11348
|
+
function rejectWorkspaceCommanderThreadFlags(values, channelSlug) {
|
|
11349
|
+
if (channelSlug !== undefined || !values.has("linzumi-thread-id")) {
|
|
11350
|
+
return;
|
|
9946
11351
|
}
|
|
9947
|
-
throw new Error("
|
|
11352
|
+
throw new Error("linzumi commander cannot bind --linzumi-thread-id because the agent token file has no channelId");
|
|
9948
11353
|
}
|
|
9949
11354
|
function requiredStoredOwnerUsername(ownerUsername) {
|
|
9950
11355
|
if (ownerUsername !== undefined) {
|
|
@@ -10002,7 +11407,7 @@ async function parseRunnerArgs(args, deps = {
|
|
|
10002
11407
|
process.exit(0);
|
|
10003
11408
|
}
|
|
10004
11409
|
if (values.get("version") === true) {
|
|
10005
|
-
process.stdout.write(`linzumi 0.0.
|
|
11410
|
+
process.stdout.write(`linzumi 0.0.40-beta
|
|
10006
11411
|
`);
|
|
10007
11412
|
process.exit(0);
|
|
10008
11413
|
}
|
|
@@ -10010,7 +11415,8 @@ async function parseRunnerArgs(args, deps = {
|
|
|
10010
11415
|
const kandanUrl = required(values, "linzumi-url");
|
|
10011
11416
|
const cwd = stringValue3(values, "cwd") ?? process.cwd();
|
|
10012
11417
|
const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
|
|
10013
|
-
const
|
|
11418
|
+
const localConfiguredAllowedCwds = values.has("allowed-cwd") ? { allowedCwds: [], missingAllowedCwds: [] } : readConfiguredAllowedCwdDetails();
|
|
11419
|
+
const configuredAllowedCwds2 = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : [...localConfiguredAllowedCwds.allowedCwds];
|
|
10014
11420
|
const codexBin = stringValue3(values, "codex-bin") ?? "codex";
|
|
10015
11421
|
const customCodeServerBin = stringValue3(values, "code-server-bin");
|
|
10016
11422
|
const explicitToken = stringValue3(values, "token");
|
|
@@ -10050,15 +11456,26 @@ async function parseRunnerArgs(args, deps = {
|
|
|
10050
11456
|
launchTui: values.get("launch-tui") === true,
|
|
10051
11457
|
fast: values.get("fast") === true,
|
|
10052
11458
|
logFile: stringValue3(values, "log-file"),
|
|
10053
|
-
allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...
|
|
11459
|
+
allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...configuredAllowedCwds2])),
|
|
11460
|
+
missingAllowedCwds: localConfiguredAllowedCwds.missingAllowedCwds,
|
|
10054
11461
|
allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
|
|
10055
11462
|
codeServerBin: editorRuntime.codeServerBin,
|
|
10056
11463
|
editorRuntime: editorRuntime.runtime,
|
|
10057
11464
|
socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
|
|
10058
11465
|
dependencyStatus,
|
|
11466
|
+
workspaceSlug: channelSession?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
|
|
11467
|
+
runtimeDefaults: runnerRuntimeDefaultsFromValues(values),
|
|
10059
11468
|
channelSession
|
|
10060
11469
|
};
|
|
10061
11470
|
}
|
|
11471
|
+
function runnerRuntimeDefaultsFromValues(values) {
|
|
11472
|
+
return {
|
|
11473
|
+
model: stringValue3(values, "model"),
|
|
11474
|
+
reasoningEffort: stringValue3(values, "reasoning-effort"),
|
|
11475
|
+
approvalPolicy: stringValue3(values, "approval-policy"),
|
|
11476
|
+
sandbox: stringValue3(values, "sandbox")
|
|
11477
|
+
};
|
|
11478
|
+
}
|
|
10062
11479
|
function strictFlagValues(args, definitions = flagDefinitions) {
|
|
10063
11480
|
const values = new Map;
|
|
10064
11481
|
for (let index = 0;index < args.length; index += 1) {
|
|
@@ -10153,9 +11570,9 @@ function resolveUserPath(pathValue) {
|
|
|
10153
11570
|
return homedir9();
|
|
10154
11571
|
}
|
|
10155
11572
|
if (pathValue.startsWith("~/")) {
|
|
10156
|
-
return
|
|
11573
|
+
return resolve9(homedir9(), pathValue.slice(2));
|
|
10157
11574
|
}
|
|
10158
|
-
return
|
|
11575
|
+
return resolve9(pathValue);
|
|
10159
11576
|
}
|
|
10160
11577
|
function parseChannelSession(values, token, target) {
|
|
10161
11578
|
if (target === undefined) {
|
|
@@ -10439,10 +11856,10 @@ Usage:
|
|
|
10439
11856
|
|
|
10440
11857
|
What it does:
|
|
10441
11858
|
Starts this computer as the claimed human's scoped Linzumi Commander. The command
|
|
10442
|
-
reads ~/.linzumi/agent-token.json,
|
|
10443
|
-
trusted folders from ~/.linzumi/config.json when no
|
|
10444
|
-
|
|
10445
|
-
|
|
11859
|
+
reads ~/.linzumi/agent-token.json, connects to its workspace even when no
|
|
11860
|
+
channel is assigned, reads trusted folders from ~/.linzumi/config.json when no
|
|
11861
|
+
folder is passed, and uses the token's channel scope for channel-bound startup
|
|
11862
|
+
when one is present.
|
|
10446
11863
|
|
|
10447
11864
|
Options:
|
|
10448
11865
|
--agent-token-file <path> Agent token cache, default ~/.linzumi/agent-token.json
|