@linzumi/cli 0.0.37-beta → 0.0.39-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 +1413 -214
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -149,19 +149,34 @@ function senderAllowed(listenUser, event, runnerIdentity) {
|
|
|
149
149
|
return runnerIdentity.actorUsername?.toLocaleLowerCase() === normalized && runnerIdentity.actorUserId !== undefined && event.actorUserId === runnerIdentity.actorUserId;
|
|
150
150
|
}
|
|
151
151
|
function identityFromAccessToken(token) {
|
|
152
|
+
const decoded = decodeAccessTokenPayload(token);
|
|
153
|
+
if (decoded === undefined) {
|
|
154
|
+
return { actorUserId: undefined, actorUsername: undefined };
|
|
155
|
+
}
|
|
156
|
+
const actorUserId = integerValue(decoded.actor_id) ?? integerValue(decoded.sub);
|
|
157
|
+
return {
|
|
158
|
+
actorUserId,
|
|
159
|
+
actorUsername: stringValue(decoded.actor_username)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function singleWorkspaceScopeFromAccessToken(token) {
|
|
163
|
+
const decoded = decodeAccessTokenPayload(token);
|
|
164
|
+
const workspaceScope = (arrayValue(decoded?.workspace_scope) ?? []).flatMap((value) => {
|
|
165
|
+
const workspace = stringValue(value)?.trim();
|
|
166
|
+
return workspace === undefined || workspace === "" ? [] : [workspace];
|
|
167
|
+
});
|
|
168
|
+
return workspaceScope.length === 1 ? workspaceScope[0] : undefined;
|
|
169
|
+
}
|
|
170
|
+
function decodeAccessTokenPayload(token) {
|
|
152
171
|
const [, payload] = token.split(".");
|
|
153
172
|
if (payload === undefined) {
|
|
154
|
-
return
|
|
173
|
+
return;
|
|
155
174
|
}
|
|
156
175
|
try {
|
|
157
176
|
const decoded = JSON.parse(Buffer.from(base64UrlToBase64(payload), "base64").toString("utf8"));
|
|
158
|
-
|
|
159
|
-
return {
|
|
160
|
-
actorUserId,
|
|
161
|
-
actorUsername: stringValue(decoded.actor_username)
|
|
162
|
-
};
|
|
177
|
+
return isJsonObject(decoded) ? decoded : undefined;
|
|
163
178
|
} catch (_error) {
|
|
164
|
-
return
|
|
179
|
+
return;
|
|
165
180
|
}
|
|
166
181
|
}
|
|
167
182
|
function detectCodexVersion(codexBin, cwd) {
|
|
@@ -801,6 +816,203 @@ function fuseQueuedMessages(selected) {
|
|
|
801
816
|
};
|
|
802
817
|
}
|
|
803
818
|
|
|
819
|
+
// src/reconnectContext.ts
|
|
820
|
+
var recentVerbatimMessageCount = 5;
|
|
821
|
+
var reconnectContextModel = "openai/gpt-oss-120b";
|
|
822
|
+
var noiseLocalRunnerEventTypes = new Set([
|
|
823
|
+
"availability",
|
|
824
|
+
"port_forward_requested",
|
|
825
|
+
"port_forward_ready",
|
|
826
|
+
"port_forward_resolved",
|
|
827
|
+
"settings",
|
|
828
|
+
"status",
|
|
829
|
+
"local_runner_config_seeded",
|
|
830
|
+
"local_runner_config_updated"
|
|
831
|
+
]);
|
|
832
|
+
function parseReconnectContextMessages(value) {
|
|
833
|
+
if (!Array.isArray(value)) {
|
|
834
|
+
return [];
|
|
835
|
+
}
|
|
836
|
+
return value.flatMap(messageFromWirePayload);
|
|
837
|
+
}
|
|
838
|
+
function filterReconnectContextMessages(messages) {
|
|
839
|
+
return messages.filter((message) => {
|
|
840
|
+
const trimmed = message.body.trim();
|
|
841
|
+
if (trimmed === "") {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
if (message.actorKind === "system") {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
const eventType = localRunnerEventType2(message.payload);
|
|
848
|
+
if (eventType !== undefined && eventType !== "codex_output") {
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
if (looksLikeLegacyRunnerNoise(message)) {
|
|
852
|
+
return false;
|
|
853
|
+
}
|
|
854
|
+
return true;
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
async function buildReconnectContextInjection(messages, summarizer = createConfiguredReconnectContextSummarizer()) {
|
|
858
|
+
const filtered = filterReconnectContextMessages(messages);
|
|
859
|
+
if (filtered.length === 0) {
|
|
860
|
+
throw new Error("durable Linzumi thread history did not include user-visible context");
|
|
861
|
+
}
|
|
862
|
+
const older = filtered.slice(0, Math.max(0, filtered.length - recentVerbatimMessageCount));
|
|
863
|
+
const recent = filtered.slice(-recentVerbatimMessageCount);
|
|
864
|
+
const summary = older.length === 0 ? undefined : await summarizeOlderReconnectContext(older, summarizer);
|
|
865
|
+
const sections = [
|
|
866
|
+
"Reconnected Linzumi thread context",
|
|
867
|
+
"",
|
|
868
|
+
"The local Codex app-server thread was restarted, so this durable Linzumi thread history is being injected before retrying the latest user message.",
|
|
869
|
+
"",
|
|
870
|
+
...summary === undefined ? [] : ["Summary of earlier messages:", summary, ""],
|
|
871
|
+
"Last five user-visible messages verbatim:",
|
|
872
|
+
recent.map(formatReconnectContextMessage).join(`
|
|
873
|
+
|
|
874
|
+
---
|
|
875
|
+
|
|
876
|
+
`)
|
|
877
|
+
];
|
|
878
|
+
return { type: "text", text: sections.join(`
|
|
879
|
+
`) };
|
|
880
|
+
}
|
|
881
|
+
function createConfiguredReconnectContextSummarizer(fetchImpl = globalThis.fetch) {
|
|
882
|
+
return {
|
|
883
|
+
summarize: (messages) => summarizeWithOpenAiCompatibleProvider(fetchImpl, messages)
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
async function summarizeOlderReconnectContext(messages, summarizer) {
|
|
887
|
+
const summary = (await summarizer.summarize(messages)).trim();
|
|
888
|
+
if (summary === "") {
|
|
889
|
+
throw new Error("reconnect context summarizer returned an empty summary");
|
|
890
|
+
}
|
|
891
|
+
return summary;
|
|
892
|
+
}
|
|
893
|
+
async function summarizeWithOpenAiCompatibleProvider(fetchImpl, messages) {
|
|
894
|
+
const provider = configuredReconnectContextProvider();
|
|
895
|
+
if (provider === undefined) {
|
|
896
|
+
throw new Error("Reconnect context summarization requires LINZUMI_RECONNECT_CONTEXT_API_KEY, GROQ_API_KEY, or OPENROUTER_API_KEY");
|
|
897
|
+
}
|
|
898
|
+
const model = envString("LINZUMI_RECONNECT_CONTEXT_MODEL") ?? envString("KANDAN_AGENT_PROGRESS_SUMMARY_MODEL") ?? reconnectContextModel;
|
|
899
|
+
const response = await fetchImpl(`${provider.baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
900
|
+
method: "POST",
|
|
901
|
+
headers: {
|
|
902
|
+
authorization: `Bearer ${provider.apiKey}`,
|
|
903
|
+
"content-type": "application/json"
|
|
904
|
+
},
|
|
905
|
+
body: JSON.stringify({
|
|
906
|
+
model,
|
|
907
|
+
temperature: 0,
|
|
908
|
+
messages: [
|
|
909
|
+
{
|
|
910
|
+
role: "system",
|
|
911
|
+
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."
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
role: "user",
|
|
915
|
+
content: messages.map(formatReconnectContextMessage).join(`
|
|
916
|
+
|
|
917
|
+
---
|
|
918
|
+
|
|
919
|
+
`)
|
|
920
|
+
}
|
|
921
|
+
]
|
|
922
|
+
})
|
|
923
|
+
});
|
|
924
|
+
if (!response.ok) {
|
|
925
|
+
throw new Error(`reconnect context summarization failed: ${response.status} ${response.statusText}`);
|
|
926
|
+
}
|
|
927
|
+
const parsed = await response.json();
|
|
928
|
+
if (!isJsonObject(parsed)) {
|
|
929
|
+
throw new Error("reconnect context summarizer returned non-object JSON");
|
|
930
|
+
}
|
|
931
|
+
const choices = parsed.choices;
|
|
932
|
+
if (!Array.isArray(choices)) {
|
|
933
|
+
throw new Error("reconnect context summarizer response did not include choices");
|
|
934
|
+
}
|
|
935
|
+
const first = choices.find(isJsonObject);
|
|
936
|
+
const message = objectValue(first?.message);
|
|
937
|
+
const content = stringValue(message?.content)?.trim();
|
|
938
|
+
if (content === undefined || content === "") {
|
|
939
|
+
throw new Error("reconnect context summarizer response did not include message content");
|
|
940
|
+
}
|
|
941
|
+
return content;
|
|
942
|
+
}
|
|
943
|
+
function configuredReconnectContextProvider() {
|
|
944
|
+
const linzumiApiKey = envString("LINZUMI_RECONNECT_CONTEXT_API_KEY");
|
|
945
|
+
if (linzumiApiKey !== undefined) {
|
|
946
|
+
return {
|
|
947
|
+
apiKey: linzumiApiKey,
|
|
948
|
+
baseUrl: envString("LINZUMI_RECONNECT_CONTEXT_BASE_URL") ?? "https://api.groq.com/openai/v1"
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
const groqApiKey = envString("GROQ_API_KEY");
|
|
952
|
+
if (groqApiKey !== undefined) {
|
|
953
|
+
return {
|
|
954
|
+
apiKey: groqApiKey,
|
|
955
|
+
baseUrl: envString("GROQ_BASE_URL") ?? "https://api.groq.com/openai/v1"
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
const openRouterApiKey = envString("OPENROUTER_API_KEY");
|
|
959
|
+
if (openRouterApiKey !== undefined) {
|
|
960
|
+
return {
|
|
961
|
+
apiKey: openRouterApiKey,
|
|
962
|
+
baseUrl: envString("OPENROUTER_BASE_URL") ?? "https://openrouter.ai/api/v1"
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
function messageFromWirePayload(value) {
|
|
968
|
+
if (!isJsonObject(value)) {
|
|
969
|
+
return [];
|
|
970
|
+
}
|
|
971
|
+
const seq = integerValue(value.seq);
|
|
972
|
+
const payload = objectValue(value.payload) ?? {};
|
|
973
|
+
const body = stringValue(value.body) ?? stringValue(payload.body) ?? stringValue(payload.text);
|
|
974
|
+
const type = stringValue(value.type) ?? "thread.message";
|
|
975
|
+
if (seq === undefined || body === undefined) {
|
|
976
|
+
return [];
|
|
977
|
+
}
|
|
978
|
+
const actor = objectValue(value.actor) ?? {};
|
|
979
|
+
return [
|
|
980
|
+
{
|
|
981
|
+
seq,
|
|
982
|
+
type,
|
|
983
|
+
actorKind: stringValue(actor.kind) ?? "unknown",
|
|
984
|
+
actorSlug: stringValue(actor.slug),
|
|
985
|
+
actorUserId: integerValue(value.actor_user_id),
|
|
986
|
+
body,
|
|
987
|
+
payload
|
|
988
|
+
}
|
|
989
|
+
];
|
|
990
|
+
}
|
|
991
|
+
function formatReconnectContextMessage(message) {
|
|
992
|
+
const sender = message.actorSlug?.trim() === "" || message.actorSlug === undefined ? message.actorKind : message.actorSlug;
|
|
993
|
+
const userId = message.actorUserId === undefined ? "unknown" : message.actorUserId.toString();
|
|
994
|
+
return [
|
|
995
|
+
`Linzumi durable message seq=${message.seq} from ${sender} (kind=${message.actorKind}, user_id=${userId}):`,
|
|
996
|
+
"",
|
|
997
|
+
message.body
|
|
998
|
+
].join(`
|
|
999
|
+
`);
|
|
1000
|
+
}
|
|
1001
|
+
function localRunnerEventType2(payload) {
|
|
1002
|
+
const metadata = objectValue(payload.metadata);
|
|
1003
|
+
const localRunner = objectValue(metadata?.local_codex_runner);
|
|
1004
|
+
const eventType = stringValue(localRunner?.event_type);
|
|
1005
|
+
return eventType === undefined || eventType.trim() === "" ? undefined : eventType.trim();
|
|
1006
|
+
}
|
|
1007
|
+
function looksLikeLegacyRunnerNoise(message) {
|
|
1008
|
+
const body = message.body.trim();
|
|
1009
|
+
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);
|
|
1010
|
+
}
|
|
1011
|
+
function envString(name) {
|
|
1012
|
+
const value = process.env[name];
|
|
1013
|
+
return value === undefined || value.trim() === "" ? undefined : value.trim();
|
|
1014
|
+
}
|
|
1015
|
+
|
|
804
1016
|
// src/localCodexMessageState.ts
|
|
805
1017
|
function codexApprovalMessageState(request) {
|
|
806
1018
|
const params = objectValue(request.params) ?? {};
|
|
@@ -843,7 +1055,11 @@ function processingReasonForCodexNotification(method, params) {
|
|
|
843
1055
|
function processingMessageStateFromActive(state) {
|
|
844
1056
|
switch (state.reason) {
|
|
845
1057
|
case "awaiting approval":
|
|
846
|
-
return {
|
|
1058
|
+
return {
|
|
1059
|
+
status: "processing",
|
|
1060
|
+
reason: state.reason,
|
|
1061
|
+
approval: state.approval
|
|
1062
|
+
};
|
|
847
1063
|
case "starting turn":
|
|
848
1064
|
case "streaming response":
|
|
849
1065
|
case "running terminal command":
|
|
@@ -1151,14 +1367,17 @@ var defaultIntervalMs = 2000;
|
|
|
1151
1367
|
var defaultDebounceMs = 750;
|
|
1152
1368
|
function startPortForwardWatcher(options) {
|
|
1153
1369
|
const rootPid = options.rootPid ?? process.pid;
|
|
1370
|
+
const rootCwd = normalizeCwd(options.rootCwd);
|
|
1154
1371
|
const intervalMs = options.intervalMs ?? defaultIntervalMs;
|
|
1155
1372
|
const debounceMs = options.debounceMs ?? defaultDebounceMs;
|
|
1373
|
+
const lostDebounceMs = options.lostDebounceMs ?? intervalMs * 2 + debounceMs;
|
|
1156
1374
|
const scanProcesses = options.scanProcesses ?? readProcessRows;
|
|
1157
1375
|
const scanListenSockets = options.scanListenSockets ?? readListenSocketRows;
|
|
1158
1376
|
const scanProcessCwds = options.scanProcessCwds ?? readProcessCwdRows;
|
|
1159
1377
|
const nowMs = options.nowMs ?? Date.now;
|
|
1160
1378
|
const candidateStabilityByPort = new Map;
|
|
1161
1379
|
const emittedByPort = new Map;
|
|
1380
|
+
const missingByPort = new Map;
|
|
1162
1381
|
const inFlight = { value: false };
|
|
1163
1382
|
const scan = () => {
|
|
1164
1383
|
if (inFlight.value) {
|
|
@@ -1168,18 +1387,26 @@ function startPortForwardWatcher(options) {
|
|
|
1168
1387
|
Promise.resolve().then(async () => {
|
|
1169
1388
|
const descendants = descendantPidSet(scanProcesses(), rootPid);
|
|
1170
1389
|
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
|
|
1390
|
+
const candidatePids = rootCwd === undefined ? sockets.filter((socket) => descendants.has(socket.pid)).map((socket) => socket.pid) : sockets.map((socket) => socket.pid);
|
|
1391
|
+
const candidates = detectedForwardCandidates(sockets, descendants, scanProcessCwds(candidatePids), { rootCwd });
|
|
1392
|
+
const scanTimeMs = nowMs();
|
|
1393
|
+
const stable = stableForwardCandidates(candidateStabilityByPort, candidates, scanTimeMs, debounceMs);
|
|
1394
|
+
const changes = debouncedForwardCandidateChanges(emittedByPort, missingByPort, stable.stableCandidates, scanTimeMs, lostDebounceMs);
|
|
1175
1395
|
candidateStabilityByPort.clear();
|
|
1176
1396
|
emittedByPort.clear();
|
|
1397
|
+
missingByPort.clear();
|
|
1177
1398
|
for (const [port, observed] of stable.nextObservedByPort) {
|
|
1178
1399
|
candidateStabilityByPort.set(port, observed);
|
|
1179
1400
|
}
|
|
1180
1401
|
for (const [port, candidate] of changes.nextObservedByPort) {
|
|
1181
1402
|
emittedByPort.set(port, candidate);
|
|
1182
1403
|
}
|
|
1404
|
+
for (const [port, missing] of changes.nextMissingByPort) {
|
|
1405
|
+
missingByPort.set(port, missing);
|
|
1406
|
+
}
|
|
1407
|
+
for (const candidate of changes.lostCandidates) {
|
|
1408
|
+
await options.onCandidateLost?.(candidate);
|
|
1409
|
+
}
|
|
1183
1410
|
for (const candidate of changes.changedCandidates) {
|
|
1184
1411
|
await options.onCandidate(candidate);
|
|
1185
1412
|
}
|
|
@@ -1195,9 +1422,11 @@ function startPortForwardWatcher(options) {
|
|
|
1195
1422
|
close: () => clearInterval(interval)
|
|
1196
1423
|
};
|
|
1197
1424
|
}
|
|
1198
|
-
function
|
|
1425
|
+
function debouncedForwardCandidateChanges(previousObservedByPort, previousMissingByPort, candidates, nowMs, lostDebounceMs) {
|
|
1199
1426
|
const nextObservedByPort = new Map;
|
|
1427
|
+
const nextMissingByPort = new Map;
|
|
1200
1428
|
const changedCandidates = [];
|
|
1429
|
+
const lostCandidates = [];
|
|
1201
1430
|
for (const candidate of candidates) {
|
|
1202
1431
|
nextObservedByPort.set(candidate.port, candidate);
|
|
1203
1432
|
const previous = previousObservedByPort.get(candidate.port);
|
|
@@ -1205,7 +1434,28 @@ function changedForwardCandidates(previousObservedByPort, candidates) {
|
|
|
1205
1434
|
changedCandidates.push(candidate);
|
|
1206
1435
|
}
|
|
1207
1436
|
}
|
|
1208
|
-
|
|
1437
|
+
for (const previous of previousObservedByPort.values()) {
|
|
1438
|
+
if (nextObservedByPort.has(previous.port)) {
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
const previousMissing = previousMissingByPort.get(previous.port);
|
|
1442
|
+
const firstMissingAtMs = previousMissing !== undefined && sameForwardCandidate(previousMissing.candidate, previous) ? previousMissing.firstMissingAtMs : nowMs;
|
|
1443
|
+
if (lostDebounceMs <= 0 || nowMs - firstMissingAtMs >= lostDebounceMs) {
|
|
1444
|
+
lostCandidates.push(previous);
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
nextObservedByPort.set(previous.port, previous);
|
|
1448
|
+
nextMissingByPort.set(previous.port, {
|
|
1449
|
+
candidate: previous,
|
|
1450
|
+
firstMissingAtMs
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
return {
|
|
1454
|
+
nextObservedByPort,
|
|
1455
|
+
nextMissingByPort,
|
|
1456
|
+
changedCandidates,
|
|
1457
|
+
lostCandidates
|
|
1458
|
+
};
|
|
1209
1459
|
}
|
|
1210
1460
|
function stableForwardCandidates(previousObservedByPort, candidates, nowMs, debounceMs) {
|
|
1211
1461
|
const nextObservedByPort = new Map;
|
|
@@ -1242,8 +1492,17 @@ function descendantPidSet(rows, rootPid) {
|
|
|
1242
1492
|
}
|
|
1243
1493
|
return descendants;
|
|
1244
1494
|
}
|
|
1245
|
-
function detectedForwardCandidates(sockets, descendantPids, processCwds = new Map) {
|
|
1246
|
-
|
|
1495
|
+
function detectedForwardCandidates(sockets, descendantPids, processCwds = new Map, options = {}) {
|
|
1496
|
+
const rootCwd = normalizeCwd(options.rootCwd);
|
|
1497
|
+
return sockets.filter((socket) => {
|
|
1498
|
+
if (descendantPids.has(socket.pid)) {
|
|
1499
|
+
return true;
|
|
1500
|
+
}
|
|
1501
|
+
if (rootCwd === undefined) {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
return cwdMatchesRoot(processCwds.get(socket.pid), rootCwd);
|
|
1505
|
+
}).filter((socket) => socket.port > 0 && socket.port < 65536).sort((left, right) => left.port - right.port).map((socket) => {
|
|
1247
1506
|
const cwd = processCwds.get(socket.pid);
|
|
1248
1507
|
return {
|
|
1249
1508
|
port: socket.port,
|
|
@@ -1253,6 +1512,20 @@ function detectedForwardCandidates(sockets, descendantPids, processCwds = new Ma
|
|
|
1253
1512
|
};
|
|
1254
1513
|
});
|
|
1255
1514
|
}
|
|
1515
|
+
function normalizeCwd(cwd) {
|
|
1516
|
+
if (cwd === undefined) {
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
const normalized = cwd.trim().replace(/\/+$/, "");
|
|
1520
|
+
return normalized === "" ? undefined : normalized;
|
|
1521
|
+
}
|
|
1522
|
+
function cwdMatchesRoot(candidateCwd, rootCwd) {
|
|
1523
|
+
const normalizedCandidate = normalizeCwd(candidateCwd);
|
|
1524
|
+
if (normalizedCandidate === undefined) {
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
return normalizedCandidate === rootCwd || normalizedCandidate.startsWith(`${rootCwd}/`);
|
|
1528
|
+
}
|
|
1256
1529
|
function parseProcessRows(output) {
|
|
1257
1530
|
return output.split(`
|
|
1258
1531
|
`).map((line) => line.trim()).filter((line) => line !== "").map((line) => {
|
|
@@ -1329,7 +1602,9 @@ function readProcessRows() {
|
|
|
1329
1602
|
return parseProcessRows(result.stdout);
|
|
1330
1603
|
}
|
|
1331
1604
|
function readListenSocketRows() {
|
|
1332
|
-
const result = spawnSync2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPcn"], {
|
|
1605
|
+
const result = spawnSync2("lsof", ["-nP", "-iTCP", "-sTCP:LISTEN", "-FpPcn"], {
|
|
1606
|
+
encoding: "utf8"
|
|
1607
|
+
});
|
|
1333
1608
|
if (result.error !== undefined) {
|
|
1334
1609
|
throw result.error;
|
|
1335
1610
|
}
|
|
@@ -1430,22 +1705,6 @@ function approvedTargetFromRequest(request) {
|
|
|
1430
1705
|
function portForwardPromptLabel(candidate) {
|
|
1431
1706
|
return commandLabel(candidate.command);
|
|
1432
1707
|
}
|
|
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
1708
|
function portForwardPromptReason(candidate) {
|
|
1450
1709
|
return [
|
|
1451
1710
|
`Port ${candidate.port}`,
|
|
@@ -1455,15 +1714,16 @@ function portForwardPromptReason(candidate) {
|
|
|
1455
1714
|
"preview protocols: HTTP, HTTPS, WebSocket"
|
|
1456
1715
|
].join(" / ");
|
|
1457
1716
|
}
|
|
1458
|
-
function portForwardDecisionCommand(decision, requestId) {
|
|
1459
|
-
return `/kandan ${decision}-port-forward ${requestId}`;
|
|
1460
|
-
}
|
|
1461
1717
|
function forwardPreviewPath(runnerId, port) {
|
|
1462
1718
|
return `/local-codex-runners/${encodeURIComponent(runnerId)}/forwards/${port}/preview`;
|
|
1463
1719
|
}
|
|
1464
1720
|
function revocationCapabilities(capabilities, port) {
|
|
1721
|
+
const existingAllowedPorts = Array.isArray(capabilities?.allowedPorts) ? capabilities.allowedPorts.filter((value) => typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= 65535) : [];
|
|
1722
|
+
const allowedPorts = existingAllowedPorts.filter((allowedPort) => allowedPort !== port);
|
|
1465
1723
|
return {
|
|
1466
1724
|
...capabilities ?? {},
|
|
1725
|
+
portForwarding: allowedPorts.length > 0,
|
|
1726
|
+
allowedPorts,
|
|
1467
1727
|
revokedPorts: [port]
|
|
1468
1728
|
};
|
|
1469
1729
|
}
|
|
@@ -1474,6 +1734,608 @@ function isInternalCodexProcess(candidate) {
|
|
|
1474
1734
|
return commandLabel(candidate.command) === "codex";
|
|
1475
1735
|
}
|
|
1476
1736
|
|
|
1737
|
+
// src/processNameCatalog.ts
|
|
1738
|
+
var processCatalogEntries = [
|
|
1739
|
+
{ appName: "Bun", iconKey: "bun", names: ["bun", "bunx"] },
|
|
1740
|
+
{
|
|
1741
|
+
appName: "Node.js",
|
|
1742
|
+
iconKey: "nodejs",
|
|
1743
|
+
names: ["node", "nodejs", "node.exe"]
|
|
1744
|
+
},
|
|
1745
|
+
{ appName: "npm", iconKey: "npm", names: ["npm", "npm-cli.js", "npx"] },
|
|
1746
|
+
{ appName: "pnpm", iconKey: "pnpm", names: ["pnpm", "pnpx"] },
|
|
1747
|
+
{ appName: "Yarn", iconKey: "yarn", names: ["yarn", "yarnpkg", "yarn.js"] },
|
|
1748
|
+
{ appName: "Corepack", iconKey: "corepack", names: ["corepack"] },
|
|
1749
|
+
{ appName: "Deno", iconKey: "deno", names: ["deno"] },
|
|
1750
|
+
{ appName: "Vite", iconKey: "vite", names: ["vite", "vite-node"] },
|
|
1751
|
+
{ appName: "Vitest", iconKey: "vitest", names: ["vitest"] },
|
|
1752
|
+
{ appName: "Next.js", iconKey: "nextjs", names: ["next", "next-server"] },
|
|
1753
|
+
{ appName: "Nuxt", iconKey: "nuxt", names: ["nuxt", "nuxi"] },
|
|
1754
|
+
{ appName: "Astro", iconKey: "astro", names: ["astro"] },
|
|
1755
|
+
{
|
|
1756
|
+
appName: "SvelteKit",
|
|
1757
|
+
iconKey: "svelte",
|
|
1758
|
+
names: ["svelte-kit", "sveltekit"]
|
|
1759
|
+
},
|
|
1760
|
+
{ appName: "Remix", iconKey: "remix", names: ["remix"] },
|
|
1761
|
+
{ appName: "Expo", iconKey: "expo", names: ["expo", "expo-cli"] },
|
|
1762
|
+
{ appName: "React Native", iconKey: "react", names: ["react-native"] },
|
|
1763
|
+
{
|
|
1764
|
+
appName: "Metro",
|
|
1765
|
+
iconKey: "metro",
|
|
1766
|
+
names: ["metro", "metro-inspector-proxy"]
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
appName: "Webpack",
|
|
1770
|
+
iconKey: "webpack",
|
|
1771
|
+
names: ["webpack", "webpack-dev-server", "webpack-cli"]
|
|
1772
|
+
},
|
|
1773
|
+
{ appName: "Parcel", iconKey: "parcel", names: ["parcel"] },
|
|
1774
|
+
{ appName: "Rollup", iconKey: "rollup", names: ["rollup"] },
|
|
1775
|
+
{ appName: "esbuild", iconKey: "esbuild", names: ["esbuild"] },
|
|
1776
|
+
{ appName: "SWC", iconKey: "swc", names: ["swc"] },
|
|
1777
|
+
{
|
|
1778
|
+
appName: "TypeScript",
|
|
1779
|
+
iconKey: "typescript",
|
|
1780
|
+
names: ["tsc", "tsserver", "tsx", "ts-node", "typescript-language-server"]
|
|
1781
|
+
},
|
|
1782
|
+
{ appName: "ESLint", iconKey: "eslint", names: ["eslint", "eslint_d"] },
|
|
1783
|
+
{ appName: "Prettier", iconKey: "prettier", names: ["prettier"] },
|
|
1784
|
+
{ appName: "Biome", iconKey: "biome", names: ["biome"] },
|
|
1785
|
+
{
|
|
1786
|
+
appName: "Storybook",
|
|
1787
|
+
iconKey: "storybook",
|
|
1788
|
+
names: ["storybook", "start-storybook"]
|
|
1789
|
+
},
|
|
1790
|
+
{ appName: "Playwright", iconKey: "playwright", names: ["playwright"] },
|
|
1791
|
+
{ appName: "Cypress", iconKey: "cypress", names: ["cypress"] },
|
|
1792
|
+
{ appName: "Jest", iconKey: "jest", names: ["jest"] },
|
|
1793
|
+
{
|
|
1794
|
+
appName: "Python",
|
|
1795
|
+
iconKey: "python",
|
|
1796
|
+
names: [
|
|
1797
|
+
"python",
|
|
1798
|
+
"python2",
|
|
1799
|
+
"python3",
|
|
1800
|
+
"python3.9",
|
|
1801
|
+
"python3.10",
|
|
1802
|
+
"python3.11",
|
|
1803
|
+
"python3.12",
|
|
1804
|
+
"python3.13",
|
|
1805
|
+
"pypy",
|
|
1806
|
+
"pypy3"
|
|
1807
|
+
]
|
|
1808
|
+
},
|
|
1809
|
+
{ appName: "pip", iconKey: "python", names: ["pip", "pip2", "pip3"] },
|
|
1810
|
+
{ appName: "pipx", iconKey: "python", names: ["pipx"] },
|
|
1811
|
+
{ appName: "uv", iconKey: "python", names: ["uv", "uvx"] },
|
|
1812
|
+
{ appName: "Poetry", iconKey: "poetry", names: ["poetry"] },
|
|
1813
|
+
{ appName: "Pipenv", iconKey: "python", names: ["pipenv"] },
|
|
1814
|
+
{
|
|
1815
|
+
appName: "Conda",
|
|
1816
|
+
iconKey: "conda",
|
|
1817
|
+
names: ["conda", "mamba", "micromamba"]
|
|
1818
|
+
},
|
|
1819
|
+
{
|
|
1820
|
+
appName: "Django",
|
|
1821
|
+
iconKey: "django",
|
|
1822
|
+
names: ["django-admin", "django-admin.py", "manage.py"]
|
|
1823
|
+
},
|
|
1824
|
+
{ appName: "Flask", iconKey: "flask", names: ["flask"] },
|
|
1825
|
+
{ appName: "Uvicorn", iconKey: "uvicorn", names: ["uvicorn"] },
|
|
1826
|
+
{ appName: "Gunicorn", iconKey: "gunicorn", names: ["gunicorn"] },
|
|
1827
|
+
{ appName: "Hypercorn", iconKey: "python", names: ["hypercorn"] },
|
|
1828
|
+
{ appName: "Celery", iconKey: "celery", names: ["celery"] },
|
|
1829
|
+
{
|
|
1830
|
+
appName: "Jupyter",
|
|
1831
|
+
iconKey: "jupyter",
|
|
1832
|
+
names: ["jupyter", "jupyter-lab", "jupyter-notebook", "ipython"]
|
|
1833
|
+
},
|
|
1834
|
+
{ appName: "Ruby", iconKey: "ruby", names: ["ruby", "erb", "irb"] },
|
|
1835
|
+
{ appName: "Bundler", iconKey: "ruby", names: ["bundle", "bundler"] },
|
|
1836
|
+
{ appName: "Rails", iconKey: "rails", names: ["rails", "bin/rails"] },
|
|
1837
|
+
{ appName: "Rack", iconKey: "ruby", names: ["rackup"] },
|
|
1838
|
+
{ appName: "Puma", iconKey: "puma", names: ["puma"] },
|
|
1839
|
+
{ appName: "Sidekiq", iconKey: "sidekiq", names: ["sidekiq"] },
|
|
1840
|
+
{ appName: "RSpec", iconKey: "ruby", names: ["rspec"] },
|
|
1841
|
+
{ appName: "Go", iconKey: "go", names: ["go", "gofmt", "gopls", "air"] },
|
|
1842
|
+
{ appName: "Delve", iconKey: "go", names: ["dlv"] },
|
|
1843
|
+
{
|
|
1844
|
+
appName: "Rust",
|
|
1845
|
+
iconKey: "rust",
|
|
1846
|
+
names: ["rustc", "rustup", "cargo", "rust-analyzer"]
|
|
1847
|
+
},
|
|
1848
|
+
{
|
|
1849
|
+
appName: "Java",
|
|
1850
|
+
iconKey: "java",
|
|
1851
|
+
names: ["java", "javac", "jar", "jshell", "jwebserver"]
|
|
1852
|
+
},
|
|
1853
|
+
{ appName: "Gradle", iconKey: "gradle", names: ["gradle", "gradlew"] },
|
|
1854
|
+
{ appName: "Maven", iconKey: "maven", names: ["mvn", "mvnw"] },
|
|
1855
|
+
{
|
|
1856
|
+
appName: "Spring Boot",
|
|
1857
|
+
iconKey: "spring",
|
|
1858
|
+
names: ["spring", "spring-boot"]
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
appName: "Kotlin",
|
|
1862
|
+
iconKey: "kotlin",
|
|
1863
|
+
names: ["kotlin", "kotlinc", "kotlin-language-server"]
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
appName: "Scala",
|
|
1867
|
+
iconKey: "scala",
|
|
1868
|
+
names: ["scala", "scalac", "sbt", "metals"]
|
|
1869
|
+
},
|
|
1870
|
+
{
|
|
1871
|
+
appName: "Clojure",
|
|
1872
|
+
iconKey: "clojure",
|
|
1873
|
+
names: ["clojure", "clj", "lein", "boot"]
|
|
1874
|
+
},
|
|
1875
|
+
{
|
|
1876
|
+
appName: "PHP",
|
|
1877
|
+
iconKey: "php",
|
|
1878
|
+
names: ["php", "php-fpm", "php-cgi", "composer"]
|
|
1879
|
+
},
|
|
1880
|
+
{ appName: "Laravel", iconKey: "laravel", names: ["artisan", "laravel"] },
|
|
1881
|
+
{ appName: "Symfony", iconKey: "symfony", names: ["symfony"] },
|
|
1882
|
+
{ appName: "Elixir", iconKey: "elixir", names: ["elixir", "iex", "mix"] },
|
|
1883
|
+
{
|
|
1884
|
+
appName: "Erlang",
|
|
1885
|
+
iconKey: "erlang",
|
|
1886
|
+
names: ["erl", "beam.smp", "beam", "epmd", "rebar3"]
|
|
1887
|
+
},
|
|
1888
|
+
{ appName: "Phoenix", iconKey: "phoenix", names: ["phx.server", "phoenix"] },
|
|
1889
|
+
{
|
|
1890
|
+
appName: "PostgreSQL",
|
|
1891
|
+
iconKey: "postgresql",
|
|
1892
|
+
names: ["postgres", "postmaster", "pg_ctl", "psql", "pg_isready"]
|
|
1893
|
+
},
|
|
1894
|
+
{
|
|
1895
|
+
appName: "Redis",
|
|
1896
|
+
iconKey: "redis",
|
|
1897
|
+
names: ["redis-server", "redis-cli", "redis-sentinel"]
|
|
1898
|
+
},
|
|
1899
|
+
{
|
|
1900
|
+
appName: "MySQL",
|
|
1901
|
+
iconKey: "mysql",
|
|
1902
|
+
names: ["mysql", "mysqld", "mysqladmin"]
|
|
1903
|
+
},
|
|
1904
|
+
{
|
|
1905
|
+
appName: "MariaDB",
|
|
1906
|
+
iconKey: "mariadb",
|
|
1907
|
+
names: ["mariadb", "mariadbd", "mariadb-admin"]
|
|
1908
|
+
},
|
|
1909
|
+
{ appName: "SQLite", iconKey: "sqlite", names: ["sqlite3", "sqlite"] },
|
|
1910
|
+
{
|
|
1911
|
+
appName: "MongoDB",
|
|
1912
|
+
iconKey: "mongodb",
|
|
1913
|
+
names: ["mongod", "mongosh", "mongo"]
|
|
1914
|
+
},
|
|
1915
|
+
{
|
|
1916
|
+
appName: "Elasticsearch",
|
|
1917
|
+
iconKey: "elasticsearch",
|
|
1918
|
+
names: ["elasticsearch"]
|
|
1919
|
+
},
|
|
1920
|
+
{ appName: "OpenSearch", iconKey: "opensearch", names: ["opensearch"] },
|
|
1921
|
+
{ appName: "Solr", iconKey: "solr", names: ["solr"] },
|
|
1922
|
+
{
|
|
1923
|
+
appName: "Kafka",
|
|
1924
|
+
iconKey: "kafka",
|
|
1925
|
+
names: [
|
|
1926
|
+
"kafka-server-start",
|
|
1927
|
+
"kafka-console-consumer",
|
|
1928
|
+
"kafka-console-producer"
|
|
1929
|
+
]
|
|
1930
|
+
},
|
|
1931
|
+
{
|
|
1932
|
+
appName: "ZooKeeper",
|
|
1933
|
+
iconKey: "zookeeper",
|
|
1934
|
+
names: ["zookeeper-server-start", "zkserver"]
|
|
1935
|
+
},
|
|
1936
|
+
{
|
|
1937
|
+
appName: "RabbitMQ",
|
|
1938
|
+
iconKey: "rabbitmq",
|
|
1939
|
+
names: ["rabbitmq-server", "rabbitmqctl"]
|
|
1940
|
+
},
|
|
1941
|
+
{ appName: "NATS", iconKey: "nats", names: ["nats-server", "nats"] },
|
|
1942
|
+
{
|
|
1943
|
+
appName: "Docker",
|
|
1944
|
+
iconKey: "docker",
|
|
1945
|
+
names: [
|
|
1946
|
+
"docker",
|
|
1947
|
+
"dockerd",
|
|
1948
|
+
"docker-compose",
|
|
1949
|
+
"docker-proxy",
|
|
1950
|
+
"com.docker.backend"
|
|
1951
|
+
]
|
|
1952
|
+
},
|
|
1953
|
+
{ appName: "Podman", iconKey: "podman", names: ["podman", "podman-compose"] },
|
|
1954
|
+
{
|
|
1955
|
+
appName: "Kubernetes",
|
|
1956
|
+
iconKey: "kubernetes",
|
|
1957
|
+
names: ["kubectl", "kube-apiserver", "kubelet", "minikube", "kind"]
|
|
1958
|
+
},
|
|
1959
|
+
{ appName: "Helm", iconKey: "helm", names: ["helm"] },
|
|
1960
|
+
{ appName: "Tilt", iconKey: "tilt", names: ["tilt"] },
|
|
1961
|
+
{ appName: "nginx", iconKey: "nginx", names: ["nginx"] },
|
|
1962
|
+
{ appName: "Caddy", iconKey: "caddy", names: ["caddy"] },
|
|
1963
|
+
{
|
|
1964
|
+
appName: "Apache HTTP Server",
|
|
1965
|
+
iconKey: "apache",
|
|
1966
|
+
names: ["httpd", "apache2", "apachectl"]
|
|
1967
|
+
},
|
|
1968
|
+
{ appName: "HAProxy", iconKey: "haproxy", names: ["haproxy"] },
|
|
1969
|
+
{ appName: "Traefik", iconKey: "traefik", names: ["traefik"] },
|
|
1970
|
+
{ appName: "Envoy", iconKey: "envoy", names: ["envoy"] },
|
|
1971
|
+
{
|
|
1972
|
+
appName: "Tailscale",
|
|
1973
|
+
iconKey: "tailscale",
|
|
1974
|
+
names: ["tailscale", "tailscaled"]
|
|
1975
|
+
},
|
|
1976
|
+
{
|
|
1977
|
+
appName: "Cloudflare Tunnel",
|
|
1978
|
+
iconKey: "cloudflare",
|
|
1979
|
+
names: ["cloudflared"]
|
|
1980
|
+
},
|
|
1981
|
+
{ appName: "ngrok", iconKey: "ngrok", names: ["ngrok"] },
|
|
1982
|
+
{ appName: "localtunnel", iconKey: "terminal", names: ["lt", "localtunnel"] },
|
|
1983
|
+
{ appName: "SSH", iconKey: "ssh", names: ["ssh", "sshd", "scp", "sftp"] },
|
|
1984
|
+
{
|
|
1985
|
+
appName: "Git",
|
|
1986
|
+
iconKey: "git",
|
|
1987
|
+
names: ["git", "git-lfs", "git-upload-pack", "git-receive-pack"]
|
|
1988
|
+
},
|
|
1989
|
+
{ appName: "GitHub CLI", iconKey: "github", names: ["gh"] },
|
|
1990
|
+
{ appName: "GitLab CLI", iconKey: "gitlab", names: ["glab"] },
|
|
1991
|
+
{ appName: "Subversion", iconKey: "subversion", names: ["svn"] },
|
|
1992
|
+
{ appName: "Mercurial", iconKey: "mercurial", names: ["hg"] },
|
|
1993
|
+
{ appName: "AWS CLI", iconKey: "aws", names: ["aws", "aws-vault", "sam"] },
|
|
1994
|
+
{ appName: "Azure CLI", iconKey: "azure", names: ["az"] },
|
|
1995
|
+
{
|
|
1996
|
+
appName: "Google Cloud CLI",
|
|
1997
|
+
iconKey: "google-cloud",
|
|
1998
|
+
names: ["gcloud", "gsutil", "bq"]
|
|
1999
|
+
},
|
|
2000
|
+
{ appName: "Terraform", iconKey: "terraform", names: ["terraform"] },
|
|
2001
|
+
{ appName: "OpenTofu", iconKey: "opentofu", names: ["tofu"] },
|
|
2002
|
+
{
|
|
2003
|
+
appName: "Ansible",
|
|
2004
|
+
iconKey: "ansible",
|
|
2005
|
+
names: ["ansible", "ansible-playbook"]
|
|
2006
|
+
},
|
|
2007
|
+
{ appName: "Pulumi", iconKey: "pulumi", names: ["pulumi"] },
|
|
2008
|
+
{ appName: "Nomad", iconKey: "nomad", names: ["nomad"] },
|
|
2009
|
+
{ appName: "Vault", iconKey: "vault", names: ["vault"] },
|
|
2010
|
+
{ appName: "Consul", iconKey: "consul", names: ["consul"] },
|
|
2011
|
+
{ appName: "Make", iconKey: "make", names: ["make", "gmake"] },
|
|
2012
|
+
{ appName: "CMake", iconKey: "cmake", names: ["cmake", "ctest", "cpack"] },
|
|
2013
|
+
{ appName: "Ninja", iconKey: "ninja", names: ["ninja"] },
|
|
2014
|
+
{ appName: "Bazel", iconKey: "bazel", names: ["bazel", "bazelisk"] },
|
|
2015
|
+
{ appName: "Buck", iconKey: "buck", names: ["buck", "buck2"] },
|
|
2016
|
+
{ appName: "Clang", iconKey: "clang", names: ["clang", "clang++", "clangd"] },
|
|
2017
|
+
{ appName: "GCC", iconKey: "gcc", names: ["gcc", "g++", "cc", "c++"] },
|
|
2018
|
+
{ appName: "LLDB", iconKey: "terminal", names: ["lldb"] },
|
|
2019
|
+
{ appName: "GDB", iconKey: "terminal", names: ["gdb"] },
|
|
2020
|
+
{
|
|
2021
|
+
appName: "Swift",
|
|
2022
|
+
iconKey: "swift",
|
|
2023
|
+
names: ["swift", "swiftc", "sourcekit-lsp"]
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
appName: "Xcode",
|
|
2027
|
+
iconKey: "xcode",
|
|
2028
|
+
names: ["xcodebuild", "xcrun", "simctl"]
|
|
2029
|
+
},
|
|
2030
|
+
{ appName: "Zig", iconKey: "zig", names: ["zig"] },
|
|
2031
|
+
{
|
|
2032
|
+
appName: "Lua",
|
|
2033
|
+
iconKey: "lua",
|
|
2034
|
+
names: ["lua", "luajit", "lua-language-server"]
|
|
2035
|
+
},
|
|
2036
|
+
{ appName: "Perl", iconKey: "perl", names: ["perl"] },
|
|
2037
|
+
{
|
|
2038
|
+
appName: "R",
|
|
2039
|
+
iconKey: "r",
|
|
2040
|
+
names: ["r", "rscript", "rserver", "rsession"]
|
|
2041
|
+
},
|
|
2042
|
+
{ appName: "Julia", iconKey: "julia", names: ["julia"] },
|
|
2043
|
+
{
|
|
2044
|
+
appName: "Haskell",
|
|
2045
|
+
iconKey: "haskell",
|
|
2046
|
+
names: ["ghc", "ghci", "cabal", "stack", "haskell-language-server-wrapper"]
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
appName: "OCaml",
|
|
2050
|
+
iconKey: "ocaml",
|
|
2051
|
+
names: ["ocaml", "ocamlc", "dune", "opam"]
|
|
2052
|
+
},
|
|
2053
|
+
{ appName: "F#", iconKey: "fsharp", names: ["fsharpi", "fsautocomplete"] },
|
|
2054
|
+
{
|
|
2055
|
+
appName: ".NET",
|
|
2056
|
+
iconKey: "dotnet",
|
|
2057
|
+
names: ["dotnet", "csharp-ls", "omnisharp"]
|
|
2058
|
+
},
|
|
2059
|
+
{
|
|
2060
|
+
appName: "PowerShell",
|
|
2061
|
+
iconKey: "powershell",
|
|
2062
|
+
names: ["pwsh", "powershell"]
|
|
2063
|
+
},
|
|
2064
|
+
{ appName: "Bash", iconKey: "terminal", names: ["bash"] },
|
|
2065
|
+
{ appName: "Zsh", iconKey: "terminal", names: ["zsh"] },
|
|
2066
|
+
{ appName: "fish", iconKey: "terminal", names: ["fish"] },
|
|
2067
|
+
{ appName: "tmux", iconKey: "terminal", names: ["tmux"] },
|
|
2068
|
+
{ appName: "GNU Screen", iconKey: "terminal", names: ["screen"] },
|
|
2069
|
+
{ appName: "direnv", iconKey: "terminal", names: ["direnv"] },
|
|
2070
|
+
{
|
|
2071
|
+
appName: "Nix",
|
|
2072
|
+
iconKey: "nix",
|
|
2073
|
+
names: ["nix", "nix-shell", "nix-daemon", "nix-build", "nix develop"]
|
|
2074
|
+
},
|
|
2075
|
+
{ appName: "Homebrew", iconKey: "homebrew", names: ["brew"] },
|
|
2076
|
+
{ appName: "mise", iconKey: "terminal", names: ["mise"] },
|
|
2077
|
+
{ appName: "asdf", iconKey: "terminal", names: ["asdf"] },
|
|
2078
|
+
{ appName: "Volta", iconKey: "volta", names: ["volta"] },
|
|
2079
|
+
{ appName: "fnm", iconKey: "nodejs", names: ["fnm"] },
|
|
2080
|
+
{ appName: "nvm", iconKey: "nodejs", names: ["nvm"] },
|
|
2081
|
+
{ appName: "curl", iconKey: "terminal", names: ["curl"] },
|
|
2082
|
+
{ appName: "HTTPie", iconKey: "httpie", names: ["http", "https", "httpie"] },
|
|
2083
|
+
{ appName: "wget", iconKey: "terminal", names: ["wget"] },
|
|
2084
|
+
{ appName: "jq", iconKey: "terminal", names: ["jq"] },
|
|
2085
|
+
{ appName: "ripgrep", iconKey: "terminal", names: ["rg", "ripgrep"] },
|
|
2086
|
+
{ appName: "fd", iconKey: "terminal", names: ["fd"] },
|
|
2087
|
+
{ appName: "fzf", iconKey: "terminal", names: ["fzf"] },
|
|
2088
|
+
{ appName: "bat", iconKey: "terminal", names: ["bat"] },
|
|
2089
|
+
{ appName: "eza", iconKey: "terminal", names: ["eza", "exa"] },
|
|
2090
|
+
{ appName: "htop", iconKey: "terminal", names: ["htop", "top", "btop"] },
|
|
2091
|
+
{ appName: "Vim", iconKey: "vim", names: ["vim", "view", "vimdiff"] },
|
|
2092
|
+
{ appName: "Neovim", iconKey: "neovim", names: ["nvim", "nvim-qt"] },
|
|
2093
|
+
{ appName: "Emacs", iconKey: "emacs", names: ["emacs", "emacsclient"] },
|
|
2094
|
+
{
|
|
2095
|
+
appName: "Visual Studio Code",
|
|
2096
|
+
iconKey: "vscode",
|
|
2097
|
+
names: [
|
|
2098
|
+
"code",
|
|
2099
|
+
"code-insiders",
|
|
2100
|
+
"vscode",
|
|
2101
|
+
"visual studio code",
|
|
2102
|
+
"visual studio code - insiders"
|
|
2103
|
+
]
|
|
2104
|
+
},
|
|
2105
|
+
{ appName: "code-server", iconKey: "vscode", names: ["code-server"] },
|
|
2106
|
+
{ appName: "Cursor", iconKey: "cursor", names: ["cursor"] },
|
|
2107
|
+
{ appName: "Zed", iconKey: "zed", names: ["zed"] },
|
|
2108
|
+
{
|
|
2109
|
+
appName: "Sublime Text",
|
|
2110
|
+
iconKey: "sublime-text",
|
|
2111
|
+
names: ["subl", "sublime_text", "sublime text"]
|
|
2112
|
+
},
|
|
2113
|
+
{ appName: "Atom", iconKey: "atom", names: ["atom"] },
|
|
2114
|
+
{
|
|
2115
|
+
appName: "IntelliJ IDEA",
|
|
2116
|
+
iconKey: "intellij-idea",
|
|
2117
|
+
names: ["idea", "idea.sh", "intellij idea", "idea64.exe"]
|
|
2118
|
+
},
|
|
2119
|
+
{
|
|
2120
|
+
appName: "WebStorm",
|
|
2121
|
+
iconKey: "webstorm",
|
|
2122
|
+
names: ["webstorm", "webstorm.sh"]
|
|
2123
|
+
},
|
|
2124
|
+
{ appName: "PyCharm", iconKey: "pycharm", names: ["pycharm", "pycharm.sh"] },
|
|
2125
|
+
{
|
|
2126
|
+
appName: "RubyMine",
|
|
2127
|
+
iconKey: "rubymine",
|
|
2128
|
+
names: ["rubymine", "rubymine.sh"]
|
|
2129
|
+
},
|
|
2130
|
+
{ appName: "GoLand", iconKey: "goland", names: ["goland", "goland.sh"] },
|
|
2131
|
+
{ appName: "CLion", iconKey: "clion", names: ["clion", "clion.sh"] },
|
|
2132
|
+
{
|
|
2133
|
+
appName: "DataGrip",
|
|
2134
|
+
iconKey: "datagrip",
|
|
2135
|
+
names: ["datagrip", "datagrip.sh"]
|
|
2136
|
+
},
|
|
2137
|
+
{ appName: "Rider", iconKey: "rider", names: ["rider", "rider.sh"] },
|
|
2138
|
+
{
|
|
2139
|
+
appName: "Android Studio",
|
|
2140
|
+
iconKey: "android-studio",
|
|
2141
|
+
names: ["studio", "studio.sh", "android studio"]
|
|
2142
|
+
},
|
|
2143
|
+
{ appName: "Xcode", iconKey: "xcode", names: ["xcode", "xcode.app"] },
|
|
2144
|
+
{ appName: "Postman", iconKey: "postman", names: ["postman"] },
|
|
2145
|
+
{ appName: "Insomnia", iconKey: "insomnia", names: ["insomnia"] },
|
|
2146
|
+
{ appName: "Hoppscotch", iconKey: "hoppscotch", names: ["hoppscotch"] },
|
|
2147
|
+
{ appName: "TablePlus", iconKey: "tableplus", names: ["tableplus"] },
|
|
2148
|
+
{ appName: "DBeaver", iconKey: "dbeaver", names: ["dbeaver"] },
|
|
2149
|
+
{ appName: "RedisInsight", iconKey: "redis", names: ["redisinsight"] },
|
|
2150
|
+
{ appName: "Prisma", iconKey: "prisma", names: ["prisma"] },
|
|
2151
|
+
{ appName: "Drizzle Kit", iconKey: "drizzle", names: ["drizzle-kit"] },
|
|
2152
|
+
{ appName: "Supabase", iconKey: "supabase", names: ["supabase"] },
|
|
2153
|
+
{ appName: "Firebase", iconKey: "firebase", names: ["firebase"] },
|
|
2154
|
+
{ appName: "Vercel", iconKey: "vercel", names: ["vercel"] },
|
|
2155
|
+
{ appName: "Netlify", iconKey: "netlify", names: ["netlify", "ntl"] },
|
|
2156
|
+
{ appName: "Fly.io", iconKey: "fly", names: ["fly", "flyctl"] },
|
|
2157
|
+
{ appName: "Heroku", iconKey: "heroku", names: ["heroku"] },
|
|
2158
|
+
{ appName: "Stripe CLI", iconKey: "stripe", names: ["stripe"] },
|
|
2159
|
+
{ appName: "Twilio CLI", iconKey: "twilio", names: ["twilio"] },
|
|
2160
|
+
{ appName: "Mailpit", iconKey: "mail", names: ["mailpit"] },
|
|
2161
|
+
{ appName: "MailHog", iconKey: "mail", names: ["mailhog"] },
|
|
2162
|
+
{ appName: "OpenSSL", iconKey: "openssl", names: ["openssl"] },
|
|
2163
|
+
{ appName: "mkcert", iconKey: "certificate", names: ["mkcert"] },
|
|
2164
|
+
{ appName: "mkdocs", iconKey: "markdown", names: ["mkdocs"] },
|
|
2165
|
+
{ appName: "Hugo", iconKey: "hugo", names: ["hugo"] },
|
|
2166
|
+
{ appName: "Jekyll", iconKey: "jekyll", names: ["jekyll"] },
|
|
2167
|
+
{ appName: "Eleventy", iconKey: "eleventy", names: ["eleventy", "11ty"] },
|
|
2168
|
+
{ appName: "Docusaurus", iconKey: "docusaurus", names: ["docusaurus"] },
|
|
2169
|
+
{ appName: "Trunk", iconKey: "rust", names: ["trunk"] },
|
|
2170
|
+
{ appName: "wasm-pack", iconKey: "webassembly", names: ["wasm-pack"] },
|
|
2171
|
+
{ appName: "wasmtime", iconKey: "webassembly", names: ["wasmtime"] },
|
|
2172
|
+
{
|
|
2173
|
+
appName: "Language Server",
|
|
2174
|
+
iconKey: "language-server",
|
|
2175
|
+
names: [
|
|
2176
|
+
"lua-language-server",
|
|
2177
|
+
"yaml-language-server",
|
|
2178
|
+
"vscode-json-language-server",
|
|
2179
|
+
"docker-langserver",
|
|
2180
|
+
"bash-language-server",
|
|
2181
|
+
"marksman",
|
|
2182
|
+
"taplo"
|
|
2183
|
+
]
|
|
2184
|
+
}
|
|
2185
|
+
];
|
|
2186
|
+
var executableNameLookup = new Map;
|
|
2187
|
+
for (const entry of processCatalogEntries) {
|
|
2188
|
+
for (const name of entry.names) {
|
|
2189
|
+
executableNameLookup.set(normalizeProcessName(name), {
|
|
2190
|
+
appName: entry.appName,
|
|
2191
|
+
iconKey: entry.iconKey
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
var processNameCatalogSize = executableNameLookup.size;
|
|
2196
|
+
function canonicalizeProcessName(processName) {
|
|
2197
|
+
const executableName = executableNameFromPathOrName(processName);
|
|
2198
|
+
if (executableName === undefined) {
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
const directMatch = lookupCanonicalProcess(executableName);
|
|
2202
|
+
if (directMatch !== undefined) {
|
|
2203
|
+
return directMatch;
|
|
2204
|
+
}
|
|
2205
|
+
const pathMatch = lookupCanonicalPathHint(processName);
|
|
2206
|
+
if (pathMatch !== undefined) {
|
|
2207
|
+
return pathMatch;
|
|
2208
|
+
}
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
function guessCanonicalProcessFromCommand(command) {
|
|
2212
|
+
const tokens = shellCommandTokens(command);
|
|
2213
|
+
const executable = executableTokenFromCommandTokens(tokens);
|
|
2214
|
+
if (executable === undefined) {
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
return canonicalizeProcessName(executable);
|
|
2218
|
+
}
|
|
2219
|
+
function lookupCanonicalProcess(processName) {
|
|
2220
|
+
const normalized = normalizeProcessName(processName);
|
|
2221
|
+
const identity = executableNameLookup.get(normalized);
|
|
2222
|
+
if (identity === undefined) {
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
return {
|
|
2226
|
+
...identity,
|
|
2227
|
+
executableName: processName,
|
|
2228
|
+
matchedName: normalized
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
function lookupCanonicalPathHint(processPath) {
|
|
2232
|
+
const normalizedPath = normalizeProcessName(processPath);
|
|
2233
|
+
const pathParts = normalizedPath.split("/").map((part) => trimExecutableExtension(part)).filter((part) => part.length > 0);
|
|
2234
|
+
for (const pathPart of pathParts.reverse()) {
|
|
2235
|
+
const pathHint = executableNameLookup.get(pathPart);
|
|
2236
|
+
if (pathHint !== undefined) {
|
|
2237
|
+
return {
|
|
2238
|
+
...pathHint,
|
|
2239
|
+
executableName: executableNameFromPathOrName(processPath) ?? processPath,
|
|
2240
|
+
matchedName: pathPart
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
function executableTokenFromCommandTokens(tokens) {
|
|
2247
|
+
const [firstToken, ...remainingTokens] = tokens;
|
|
2248
|
+
switch (firstToken) {
|
|
2249
|
+
case undefined:
|
|
2250
|
+
return;
|
|
2251
|
+
case "env":
|
|
2252
|
+
case "/usr/bin/env":
|
|
2253
|
+
return executableTokenFromCommandTokens(remainingTokens.filter((token) => !isEnvironmentAssignment(token)));
|
|
2254
|
+
case "sudo":
|
|
2255
|
+
case "command":
|
|
2256
|
+
case "exec":
|
|
2257
|
+
case "nohup":
|
|
2258
|
+
return executableTokenFromCommandTokens(remainingTokens.filter((token) => !token.startsWith("-")));
|
|
2259
|
+
default:
|
|
2260
|
+
switch (isEnvironmentAssignment(firstToken)) {
|
|
2261
|
+
case true:
|
|
2262
|
+
return executableTokenFromCommandTokens(remainingTokens);
|
|
2263
|
+
case false:
|
|
2264
|
+
return firstToken;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function executableNameFromPathOrName(processName) {
|
|
2269
|
+
const trimmed = processName.trim();
|
|
2270
|
+
if (trimmed.length === 0) {
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
const normalizedSeparators = trimmed.replaceAll("\\", "/");
|
|
2274
|
+
const [lastPathPart] = normalizedSeparators.split("/").filter((part) => part.length > 0).slice(-1);
|
|
2275
|
+
switch (lastPathPart) {
|
|
2276
|
+
case undefined:
|
|
2277
|
+
return;
|
|
2278
|
+
default:
|
|
2279
|
+
return trimExecutableExtension(lastPathPart);
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
function trimExecutableExtension(executableName) {
|
|
2283
|
+
const normalized = executableName.toLowerCase();
|
|
2284
|
+
switch (true) {
|
|
2285
|
+
case normalized.endsWith(".app"):
|
|
2286
|
+
return executableName.slice(0, -4);
|
|
2287
|
+
case normalized.endsWith(".exe"):
|
|
2288
|
+
return executableName.slice(0, -4);
|
|
2289
|
+
case normalized.endsWith(".cmd"):
|
|
2290
|
+
case normalized.endsWith(".bat"):
|
|
2291
|
+
return executableName.slice(0, -4);
|
|
2292
|
+
default:
|
|
2293
|
+
return executableName;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
function normalizeProcessName(processName) {
|
|
2297
|
+
return trimExecutableExtension(processName.trim().replaceAll("\\", "/").toLowerCase());
|
|
2298
|
+
}
|
|
2299
|
+
function isEnvironmentAssignment(token) {
|
|
2300
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
|
|
2301
|
+
}
|
|
2302
|
+
function shellCommandTokens(command) {
|
|
2303
|
+
const tokens = [];
|
|
2304
|
+
let currentToken = "";
|
|
2305
|
+
let quote = undefined;
|
|
2306
|
+
let escaping = false;
|
|
2307
|
+
for (const character of command.trim()) {
|
|
2308
|
+
switch (true) {
|
|
2309
|
+
case escaping:
|
|
2310
|
+
currentToken = `${currentToken}${character}`;
|
|
2311
|
+
escaping = false;
|
|
2312
|
+
break;
|
|
2313
|
+
case character === "\\":
|
|
2314
|
+
escaping = true;
|
|
2315
|
+
break;
|
|
2316
|
+
case (quote === undefined && (character === '"' || character === "'")):
|
|
2317
|
+
quote = character;
|
|
2318
|
+
break;
|
|
2319
|
+
case quote === character:
|
|
2320
|
+
quote = undefined;
|
|
2321
|
+
break;
|
|
2322
|
+
case (quote === undefined && /\s/.test(character)):
|
|
2323
|
+
if (currentToken.length > 0) {
|
|
2324
|
+
tokens.push(currentToken);
|
|
2325
|
+
currentToken = "";
|
|
2326
|
+
}
|
|
2327
|
+
break;
|
|
2328
|
+
default:
|
|
2329
|
+
currentToken = `${currentToken}${character}`;
|
|
2330
|
+
break;
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
if (currentToken.length > 0) {
|
|
2334
|
+
tokens.push(currentToken);
|
|
2335
|
+
}
|
|
2336
|
+
return tokens;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
1477
2339
|
// src/channelSession.ts
|
|
1478
2340
|
var codexTypingHeartbeatMs = 5000;
|
|
1479
2341
|
var defaultStreamFlushIntervalMs = 150;
|
|
@@ -1534,7 +2396,7 @@ async function attachChannelSession(args) {
|
|
|
1534
2396
|
}
|
|
1535
2397
|
switch (method) {
|
|
1536
2398
|
case "turn/started":
|
|
1537
|
-
if (turnId !== undefined) {
|
|
2399
|
+
if (threadId !== undefined && turnId !== undefined) {
|
|
1538
2400
|
rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId);
|
|
1539
2401
|
}
|
|
1540
2402
|
break;
|
|
@@ -1669,12 +2531,15 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
|
|
|
1669
2531
|
forwardedTerminalInputKeys: new Set,
|
|
1670
2532
|
webSearchProgressOutputs: createBoundedCache(maxForwardedTurnIds),
|
|
1671
2533
|
pendingApprovalRequests: new Map,
|
|
2534
|
+
approvalPromptChain: Promise.resolve(),
|
|
1672
2535
|
pendingPortForwardRequests: new Map,
|
|
2536
|
+
queuedPortForwardCandidates: new Map,
|
|
1673
2537
|
approvedForwardPorts: new Set,
|
|
1674
2538
|
approvedForwardTargets: new Map,
|
|
1675
2539
|
dismissedForwardTargets: new Map,
|
|
1676
2540
|
portForwardWatcher: undefined,
|
|
1677
2541
|
activeProcessingState: undefined,
|
|
2542
|
+
pendingReconnectContextInjection: undefined,
|
|
1678
2543
|
terminalInputForwardChain: Promise.resolve(),
|
|
1679
2544
|
webSearchProgressForwardChain: Promise.resolve(),
|
|
1680
2545
|
typingHeartbeat: undefined,
|
|
@@ -1682,8 +2547,25 @@ function initialChannelSessionState(cursor, rootSeq, kandanThreadId, codexThread
|
|
|
1682
2547
|
runtimeSettings: runtimeSettingsFromOptions(options)
|
|
1683
2548
|
};
|
|
1684
2549
|
}
|
|
2550
|
+
async function handleLostPortForwardCandidate(args, state, payloadContext, candidate) {
|
|
2551
|
+
dropLostQueuedPortForwardCandidate(args, state, candidate);
|
|
2552
|
+
await expireLostPendingPortForwardRequest(args, state, payloadContext, candidate);
|
|
2553
|
+
await revokeLostApprovedForwardPort(args, state, candidate);
|
|
2554
|
+
}
|
|
2555
|
+
function dropLostQueuedPortForwardCandidate(args, state, candidate) {
|
|
2556
|
+
const queued = state.queuedPortForwardCandidates.get(candidate.port);
|
|
2557
|
+
if (queued === undefined || !sameForwardCandidate(queued, candidate)) {
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
state.queuedPortForwardCandidates.delete(candidate.port);
|
|
2561
|
+
args.log("port_forward.queued_request_dropped", {
|
|
2562
|
+
port: candidate.port,
|
|
2563
|
+
pid: candidate.pid,
|
|
2564
|
+
reason: "listener_exited"
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
1685
2567
|
function startPortForwardWatchIfEnabled(args, state, payloadContext) {
|
|
1686
|
-
if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined) {
|
|
2568
|
+
if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined || state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
1687
2569
|
return;
|
|
1688
2570
|
}
|
|
1689
2571
|
const { start: configuredStart, ...watchOptions } = args.options.portForwardWatcher ?? {};
|
|
@@ -1692,8 +2574,10 @@ function startPortForwardWatchIfEnabled(args, state, payloadContext) {
|
|
|
1692
2574
|
state.approvedForwardPorts.add(port);
|
|
1693
2575
|
}
|
|
1694
2576
|
state.portForwardWatcher = start({
|
|
2577
|
+
rootCwd: watchOptions.rootCwd ?? args.options.cwd,
|
|
1695
2578
|
...watchOptions,
|
|
1696
2579
|
onCandidate: (candidate) => publishPortForwardPrompt(args, state, payloadContext, candidate),
|
|
2580
|
+
onCandidateLost: (candidate) => handleLostPortForwardCandidate(args, state, payloadContext, candidate),
|
|
1697
2581
|
onError: (error) => args.log("port_forward.watch_failed", { message: error.message })
|
|
1698
2582
|
});
|
|
1699
2583
|
}
|
|
@@ -1715,13 +2599,6 @@ async function bindChannelSession(args, state, payloadContext) {
|
|
|
1715
2599
|
}
|
|
1716
2600
|
} else if (state.codexThreadId !== undefined) {
|
|
1717
2601
|
await bindCurrentCodexThread(args, state);
|
|
1718
|
-
switch (state.rootSeq) {
|
|
1719
|
-
case undefined:
|
|
1720
|
-
await postBoundThreadAvailability(args, state, payloadContext, codexVersion);
|
|
1721
|
-
break;
|
|
1722
|
-
default:
|
|
1723
|
-
break;
|
|
1724
|
-
}
|
|
1725
2602
|
} else {
|
|
1726
2603
|
const resolved = await pushOk(args.kandan, args.topic, "session:resolve_thread_session", {
|
|
1727
2604
|
workspace: session.workspaceSlug,
|
|
@@ -1733,22 +2610,9 @@ async function bindChannelSession(args, state, payloadContext) {
|
|
|
1733
2610
|
if (state.codexThreadId === undefined) {
|
|
1734
2611
|
throw new Error("Kandan thread root metadata did not include a Codex thread id");
|
|
1735
2612
|
}
|
|
1736
|
-
await
|
|
2613
|
+
await bindCurrentCodexThread(args, state);
|
|
1737
2614
|
}
|
|
1738
2615
|
}
|
|
1739
|
-
async function postBoundThreadAvailability(args, state, payloadContext, codexVersion) {
|
|
1740
|
-
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
1741
|
-
throw new Error("cannot post local Codex availability before thread binding");
|
|
1742
|
-
}
|
|
1743
|
-
const session = args.options.channelSession;
|
|
1744
|
-
await pushOk(args.kandan, args.topic, "session:post_thread_message", {
|
|
1745
|
-
workspace: session.workspaceSlug,
|
|
1746
|
-
channel: session.channelSlug,
|
|
1747
|
-
thread_id: state.kandanThreadId,
|
|
1748
|
-
body: availabilityMessage(args.options, codexVersion, state.codexThreadId),
|
|
1749
|
-
payload: localRunnerPayload(args.options, args.instanceId, "availability", state.codexThreadId, payloadContext)
|
|
1750
|
-
});
|
|
1751
|
-
}
|
|
1752
2616
|
async function handleChannelSessionControl(args, state, payloadContext, control) {
|
|
1753
2617
|
if (control.type === "update_session_settings") {
|
|
1754
2618
|
return updateSessionSettings(args, state, control);
|
|
@@ -1766,7 +2630,11 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
|
|
|
1766
2630
|
return;
|
|
1767
2631
|
}
|
|
1768
2632
|
if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
|
|
1769
|
-
return {
|
|
2633
|
+
return {
|
|
2634
|
+
instanceId: args.instanceId,
|
|
2635
|
+
ok: false,
|
|
2636
|
+
error: "thread_not_bound"
|
|
2637
|
+
};
|
|
1770
2638
|
}
|
|
1771
2639
|
const interrupted = interruptPendingKandanMessages(state.queue, control.throughSeq);
|
|
1772
2640
|
const activeQueuedSeq = interruptibleQueuedSeq(state.turn);
|
|
@@ -1819,7 +2687,11 @@ function updateSessionSettings(args, state, control) {
|
|
|
1819
2687
|
return;
|
|
1820
2688
|
}
|
|
1821
2689
|
if (state.codexThreadId === undefined) {
|
|
1822
|
-
return {
|
|
2690
|
+
return {
|
|
2691
|
+
instanceId: args.instanceId,
|
|
2692
|
+
ok: false,
|
|
2693
|
+
error: "thread_not_bound"
|
|
2694
|
+
};
|
|
1823
2695
|
}
|
|
1824
2696
|
state.runtimeSettings = mergeRuntimeSettings(state.runtimeSettings, control);
|
|
1825
2697
|
publishRuntimeSettings(args, state).catch((error) => {
|
|
@@ -1849,16 +2721,27 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
|
|
|
1849
2721
|
return;
|
|
1850
2722
|
}
|
|
1851
2723
|
if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
|
|
1852
|
-
return {
|
|
2724
|
+
return {
|
|
2725
|
+
instanceId: args.instanceId,
|
|
2726
|
+
ok: false,
|
|
2727
|
+
error: "thread_not_bound"
|
|
2728
|
+
};
|
|
1853
2729
|
}
|
|
1854
2730
|
const approval = state.pendingApprovalRequests.get(approvalRequestKey(control.requestId, control.sourceSeq));
|
|
1855
2731
|
if (approval === undefined) {
|
|
1856
|
-
return {
|
|
2732
|
+
return {
|
|
2733
|
+
instanceId: args.instanceId,
|
|
2734
|
+
ok: false,
|
|
2735
|
+
error: "approval_request_not_found"
|
|
2736
|
+
};
|
|
1857
2737
|
}
|
|
1858
2738
|
state.pendingApprovalRequests.delete(approvalRequestKey(control.requestId, control.sourceSeq));
|
|
1859
2739
|
const codexDecision = control.decision === "approve" ? "accept" : "decline";
|
|
1860
2740
|
approval.resolve({ decision: codexDecision });
|
|
1861
|
-
state.activeProcessingState = {
|
|
2741
|
+
state.activeProcessingState = {
|
|
2742
|
+
seq: approval.sourceSeq,
|
|
2743
|
+
reason: "streaming response"
|
|
2744
|
+
};
|
|
1862
2745
|
await publishMessageState(args, state.kandanThreadId, approval.sourceSeq, {
|
|
1863
2746
|
status: "processing",
|
|
1864
2747
|
reason: "streaming response"
|
|
@@ -1871,6 +2754,17 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
|
|
|
1871
2754
|
});
|
|
1872
2755
|
return { instanceId: args.instanceId, ok: true };
|
|
1873
2756
|
}
|
|
2757
|
+
async function drainQueuedPortForwardPrompt(args, state, payloadContext) {
|
|
2758
|
+
if (state.pendingPortForwardRequests.size > 0 || state.queuedPortForwardCandidates.size === 0) {
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
const next = Array.from(state.queuedPortForwardCandidates.values()).sort((left, right) => left.port - right.port)[0];
|
|
2762
|
+
if (next === undefined) {
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
state.queuedPortForwardCandidates.delete(next.port);
|
|
2766
|
+
await publishPortForwardPrompt(args, state, payloadContext, next);
|
|
2767
|
+
}
|
|
1874
2768
|
async function resolvePendingPortForwardRequest(args, state, payloadContext, control) {
|
|
1875
2769
|
const request = state.pendingPortForwardRequests.get(control.requestId);
|
|
1876
2770
|
if (request === undefined) {
|
|
@@ -1883,7 +2777,11 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
1883
2777
|
actor_user_id: control.actorUserId ?? null,
|
|
1884
2778
|
reason: "sender_not_allowed"
|
|
1885
2779
|
});
|
|
1886
|
-
return {
|
|
2780
|
+
return {
|
|
2781
|
+
instanceId: args.instanceId,
|
|
2782
|
+
ok: false,
|
|
2783
|
+
error: "sender_not_allowed"
|
|
2784
|
+
};
|
|
1887
2785
|
}
|
|
1888
2786
|
state.pendingPortForwardRequests.delete(control.requestId);
|
|
1889
2787
|
if (control.decision === "deny") {
|
|
@@ -1894,14 +2792,18 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
1894
2792
|
port: request.port,
|
|
1895
2793
|
pid: request.pid
|
|
1896
2794
|
});
|
|
2795
|
+
await drainQueuedPortForwardPrompt(args, state, payloadContext);
|
|
1897
2796
|
return { instanceId: args.instanceId, ok: true };
|
|
1898
2797
|
}
|
|
1899
2798
|
state.approvedForwardPorts.add(request.port);
|
|
1900
2799
|
state.approvedForwardTargets.set(request.port, approvedTargetFromRequest(request));
|
|
2800
|
+
const processIdentity = guessCanonicalProcessFromCommand(request.command);
|
|
1901
2801
|
const capabilities = args.options.onForwardPortApproved?.(request.port, {
|
|
1902
2802
|
kandanThreadId: state.kandanThreadId ?? null,
|
|
1903
2803
|
codexThreadId: state.codexThreadId ?? null,
|
|
1904
|
-
channelSlug: args.options.channelSession.channelSlug ?? null
|
|
2804
|
+
channelSlug: args.options.channelSession.channelSlug ?? null,
|
|
2805
|
+
processName: processIdentity?.appName ?? null,
|
|
2806
|
+
processIconKey: processIdentity?.iconKey ?? null
|
|
1905
2807
|
});
|
|
1906
2808
|
await publishForwardPortResolvedEvent(args, request, capabilities);
|
|
1907
2809
|
await publishMessageStateForPortForwardResult(args, state, request, "processed");
|
|
@@ -1911,6 +2813,7 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
1911
2813
|
port: request.port,
|
|
1912
2814
|
pid: request.pid
|
|
1913
2815
|
});
|
|
2816
|
+
await drainQueuedPortForwardPrompt(args, state, payloadContext);
|
|
1914
2817
|
return { instanceId: args.instanceId, ok: true, port: request.port };
|
|
1915
2818
|
}
|
|
1916
2819
|
function portForwardControlSenderAllowed(args, payloadContext, control) {
|
|
@@ -1947,37 +2850,39 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1947
2850
|
state.approvedForwardTargets.set(review.target.port, review.target);
|
|
1948
2851
|
return;
|
|
1949
2852
|
case "revoke_and_prompt":
|
|
2853
|
+
if (state.pendingPortForwardRequests.size > 0) {
|
|
2854
|
+
state.queuedPortForwardCandidates.set(candidate.port, candidate);
|
|
2855
|
+
args.log("port_forward.request_queued", {
|
|
2856
|
+
port: candidate.port,
|
|
2857
|
+
pid: candidate.pid,
|
|
2858
|
+
pending_count: state.pendingPortForwardRequests.size
|
|
2859
|
+
});
|
|
2860
|
+
return;
|
|
2861
|
+
}
|
|
1950
2862
|
await revokeApprovedForwardPort(args, state, review.revoked, review.reason);
|
|
1951
2863
|
break;
|
|
1952
2864
|
case "prompt":
|
|
2865
|
+
if (state.pendingPortForwardRequests.size > 0) {
|
|
2866
|
+
state.queuedPortForwardCandidates.set(candidate.port, candidate);
|
|
2867
|
+
args.log("port_forward.request_queued", {
|
|
2868
|
+
port: candidate.port,
|
|
2869
|
+
pid: candidate.pid,
|
|
2870
|
+
pending_count: state.pendingPortForwardRequests.size
|
|
2871
|
+
});
|
|
2872
|
+
return;
|
|
2873
|
+
}
|
|
1953
2874
|
break;
|
|
1954
2875
|
}
|
|
1955
2876
|
const requestId = `port-forward-${randomUUID()}`;
|
|
1956
2877
|
const label = portForwardPromptLabel(candidate);
|
|
1957
|
-
const
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
reply_to_seq: state.rootSeq ?? null,
|
|
1961
|
-
structured: {
|
|
1962
|
-
kind: "local_runner_port_forward_request",
|
|
1963
|
-
request_id: requestId,
|
|
2878
|
+
const sourceSeq = portForwardApprovalSourceSeq(state);
|
|
2879
|
+
if (sourceSeq === undefined) {
|
|
2880
|
+
args.log("port_forward.prompt_skipped", {
|
|
1964
2881
|
port: candidate.port,
|
|
1965
2882
|
pid: candidate.pid,
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
}
|
|
1970
|
-
};
|
|
1971
|
-
const reply = await pushOk(args.kandan, args.topic, "session:post_thread_message", {
|
|
1972
|
-
workspace: args.options.channelSession.workspaceSlug,
|
|
1973
|
-
channel: args.options.channelSession.channelSlug,
|
|
1974
|
-
thread_id: state.kandanThreadId,
|
|
1975
|
-
body,
|
|
1976
|
-
payload
|
|
1977
|
-
});
|
|
1978
|
-
const sourceSeq = integerValue(reply.seq);
|
|
1979
|
-
if (sourceSeq === undefined) {
|
|
1980
|
-
throw new Error("port forward prompt did not return a Kandan message seq");
|
|
2883
|
+
reason: "source_seq_missing"
|
|
2884
|
+
});
|
|
2885
|
+
return;
|
|
1981
2886
|
}
|
|
1982
2887
|
const request = pendingRequestFromCandidate({
|
|
1983
2888
|
requestId,
|
|
@@ -1985,6 +2890,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1985
2890
|
candidate
|
|
1986
2891
|
});
|
|
1987
2892
|
state.pendingPortForwardRequests.set(requestId, request);
|
|
2893
|
+
const processIdentity = guessCanonicalProcessFromCommand(candidate.command);
|
|
2894
|
+
const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
|
|
2895
|
+
const processName = processIdentity?.appName ?? label;
|
|
1988
2896
|
await publishForwardPortRequestedEvent(args, request);
|
|
1989
2897
|
await publishMessageState(args, state.kandanThreadId, sourceSeq, {
|
|
1990
2898
|
status: "processing",
|
|
@@ -1992,13 +2900,20 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1992
2900
|
approval: {
|
|
1993
2901
|
requestId,
|
|
1994
2902
|
kind: "local_runner_port_forward",
|
|
1995
|
-
summary: `
|
|
2903
|
+
summary: `Make ${processName} on port ${candidate.port} accessible on Linzumi?`,
|
|
1996
2904
|
reason: portForwardPromptReason(candidate),
|
|
2905
|
+
port: candidate.port,
|
|
2906
|
+
pid: candidate.pid,
|
|
2907
|
+
command: candidate.command,
|
|
2908
|
+
...candidate.cwd === undefined ? {} : { cwd: candidate.cwd },
|
|
2909
|
+
...processIdentity?.appName === undefined ? {} : { processName },
|
|
2910
|
+
...processIdentity?.iconKey === undefined ? {} : { processIconKey: processIdentity.iconKey },
|
|
2911
|
+
...processIconPath === undefined ? {} : { processIconPath },
|
|
1997
2912
|
choices: [
|
|
1998
2913
|
{
|
|
1999
2914
|
decision: "approve",
|
|
2000
|
-
label: "
|
|
2001
|
-
description: `Allow
|
|
2915
|
+
label: "Enable",
|
|
2916
|
+
description: `Allow Linzumi to proxy HTTP, HTTPS, and WebSocket traffic for runner port ${candidate.port}.`
|
|
2002
2917
|
},
|
|
2003
2918
|
{
|
|
2004
2919
|
decision: "deny",
|
|
@@ -2016,6 +2931,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
2016
2931
|
pid: candidate.pid
|
|
2017
2932
|
});
|
|
2018
2933
|
}
|
|
2934
|
+
function portForwardApprovalSourceSeq(state) {
|
|
2935
|
+
return state.activeProcessingState?.seq ?? state.rootSeq;
|
|
2936
|
+
}
|
|
2019
2937
|
async function revokeApprovedForwardPort(args, state, target, reason) {
|
|
2020
2938
|
state.approvedForwardPorts.delete(target.port);
|
|
2021
2939
|
state.approvedForwardTargets.delete(target.port);
|
|
@@ -2035,30 +2953,58 @@ async function revokeApprovedForwardPort(args, state, target, reason) {
|
|
|
2035
2953
|
reason
|
|
2036
2954
|
});
|
|
2037
2955
|
}
|
|
2956
|
+
async function revokeLostApprovedForwardPort(args, state, candidate) {
|
|
2957
|
+
if (!state.approvedForwardPorts.has(candidate.port)) {
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
await revokeApprovedForwardPort(args, state, state.approvedForwardTargets.get(candidate.port) ?? candidate, "listener_exited");
|
|
2961
|
+
}
|
|
2962
|
+
async function expireLostPendingPortForwardRequest(args, state, payloadContext, candidate) {
|
|
2963
|
+
const request = Array.from(state.pendingPortForwardRequests.values()).find((request2) => sameForwardCandidate(approvedTargetFromRequest(request2), candidate));
|
|
2964
|
+
if (request === undefined) {
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
state.pendingPortForwardRequests.delete(request.requestId);
|
|
2968
|
+
await publishMessageStateForPortForwardResult(args, state, request, "failed", "port_forward_listener_exited");
|
|
2969
|
+
args.log("port_forward.pending_request_expired", {
|
|
2970
|
+
request_id: request.requestId,
|
|
2971
|
+
port: request.port,
|
|
2972
|
+
pid: request.pid,
|
|
2973
|
+
reason: "listener_exited"
|
|
2974
|
+
});
|
|
2975
|
+
await drainQueuedPortForwardPrompt(args, state, payloadContext);
|
|
2976
|
+
}
|
|
2038
2977
|
async function publishPortForwardReadyMessage(args, state, payloadContext, request) {
|
|
2039
2978
|
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
2040
2979
|
return;
|
|
2041
2980
|
}
|
|
2042
2981
|
const path = forwardPreviewPath(args.options.runnerId, request.port);
|
|
2982
|
+
const processIdentity = guessCanonicalProcessFromCommand(request.command);
|
|
2983
|
+
const processIconPath = processIdentity?.iconKey === undefined ? undefined : `/web/process-icons/${processIdentity.iconKey}.png`;
|
|
2984
|
+
const processName = processIdentity?.appName ?? `Port ${request.port}`;
|
|
2985
|
+
const readySummary = `${processName} on port ${request.port} is ready in Linzumi.`;
|
|
2043
2986
|
await pushOptional(args.kandan, args.topic, "session:post_thread_message", {
|
|
2044
2987
|
workspace: args.options.channelSession.workspaceSlug,
|
|
2045
2988
|
channel: args.options.channelSession.channelSlug,
|
|
2046
2989
|
thread_id: state.kandanThreadId,
|
|
2047
|
-
body:
|
|
2990
|
+
body: `${readySummary} [Open](${path})`,
|
|
2048
2991
|
payload: {
|
|
2049
2992
|
...localRunnerPayload(args.options, args.instanceId, "port_forward_ready", state.codexThreadId, payloadContext),
|
|
2050
2993
|
...state.rootSeq === undefined ? {} : { reply_to_seq: state.rootSeq },
|
|
2051
2994
|
structured: {
|
|
2052
2995
|
kind: "local_runner_port_forward_ready",
|
|
2053
2996
|
status: "ready",
|
|
2054
|
-
summary:
|
|
2997
|
+
summary: readySummary,
|
|
2055
2998
|
next_action: "Open HTTP/HTTPS/WebSocket preview",
|
|
2056
2999
|
source_path: path,
|
|
2057
|
-
link_label: "Open
|
|
3000
|
+
link_label: "Open",
|
|
2058
3001
|
request_id: request.requestId,
|
|
2059
3002
|
port: request.port,
|
|
2060
3003
|
pid: request.pid,
|
|
2061
3004
|
command: request.command,
|
|
3005
|
+
...processIdentity?.appName === undefined ? {} : { process_name: processIdentity.appName },
|
|
3006
|
+
...processIdentity?.iconKey === undefined ? {} : { process_icon_key: processIdentity.iconKey },
|
|
3007
|
+
...processIconPath === undefined ? {} : { process_icon_path: processIconPath },
|
|
2062
3008
|
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
2063
3009
|
url: path
|
|
2064
3010
|
}
|
|
@@ -2087,10 +3033,15 @@ async function publishForwardPortResolvedEvent(args, request, capabilities) {
|
|
|
2087
3033
|
...capabilities === undefined ? {} : { capabilities }
|
|
2088
3034
|
}, args.log);
|
|
2089
3035
|
}
|
|
2090
|
-
async function publishMessageStateForPortForwardResult(args, state, request, status) {
|
|
3036
|
+
async function publishMessageStateForPortForwardResult(args, state, request, status, failedReason = "port_forward_denied") {
|
|
2091
3037
|
if (state.kandanThreadId === undefined) {
|
|
2092
3038
|
return;
|
|
2093
3039
|
}
|
|
3040
|
+
const activeProcessingState = state.activeProcessingState;
|
|
3041
|
+
if (activeProcessingState !== undefined && activeProcessingState.seq === request.sourceSeq) {
|
|
3042
|
+
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, processingMessageStateFromActive(activeProcessingState), undefined, undefined, state.codexThreadId);
|
|
3043
|
+
return;
|
|
3044
|
+
}
|
|
2094
3045
|
switch (status) {
|
|
2095
3046
|
case "processed":
|
|
2096
3047
|
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
|
|
@@ -2100,7 +3051,7 @@ async function publishMessageStateForPortForwardResult(args, state, request, sta
|
|
|
2100
3051
|
case "failed":
|
|
2101
3052
|
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
|
|
2102
3053
|
status: "failed",
|
|
2103
|
-
reason:
|
|
3054
|
+
reason: failedReason
|
|
2104
3055
|
});
|
|
2105
3056
|
break;
|
|
2106
3057
|
}
|
|
@@ -2171,7 +3122,12 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
|
|
|
2171
3122
|
actorUserId: event.actorUserId,
|
|
2172
3123
|
actorSlug: event.actorSlug
|
|
2173
3124
|
});
|
|
2174
|
-
if (result
|
|
3125
|
+
if (result === undefined) {
|
|
3126
|
+
await publishKandanMessageState(args, event, {
|
|
3127
|
+
status: "failed",
|
|
3128
|
+
reason: "port_forward_decision_missing"
|
|
3129
|
+
});
|
|
3130
|
+
} else if (result.ok === true) {
|
|
2175
3131
|
await publishKandanMessageState(args, event, { status: "processed" });
|
|
2176
3132
|
} else {
|
|
2177
3133
|
await publishKandanMessageState(args, event, {
|
|
@@ -2201,6 +3157,7 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
|
|
|
2201
3157
|
state.kandanThreadId = event.threadId;
|
|
2202
3158
|
}
|
|
2203
3159
|
}
|
|
3160
|
+
startPortForwardWatchIfEnabled(args, state, payloadContext);
|
|
2204
3161
|
if (event.threadId !== state.kandanThreadId) {
|
|
2205
3162
|
args.log("kandan.message_ignored", {
|
|
2206
3163
|
seq: event.seq,
|
|
@@ -2324,7 +3281,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2324
3281
|
if (next === undefined) {
|
|
2325
3282
|
return;
|
|
2326
3283
|
}
|
|
2327
|
-
state.turn = {
|
|
3284
|
+
state.turn = {
|
|
3285
|
+
status: "starting",
|
|
3286
|
+
queuedSeq: next.seq,
|
|
3287
|
+
interruptAfterStart: false
|
|
3288
|
+
};
|
|
2328
3289
|
state.activeProcessingState = { seq: next.seq, reason: "starting turn" };
|
|
2329
3290
|
args.log("codex.turn_starting", {
|
|
2330
3291
|
queued_seq: next.seq,
|
|
@@ -2347,10 +3308,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2347
3308
|
}
|
|
2348
3309
|
const started = await args.codex.request("turn/start", {
|
|
2349
3310
|
threadId: codexThreadId,
|
|
2350
|
-
input: await codexInputItemsForQueuedKandanMessage(args, next),
|
|
3311
|
+
input: await codexInputItemsForQueuedKandanMessage(args, next, state.pendingReconnectContextInjection),
|
|
2351
3312
|
...codexTurnRuntimeOverrides(runtimeOptionsForSettings(args.options, state.runtimeSettings))
|
|
2352
3313
|
});
|
|
2353
3314
|
const turnId = extractTurnIdFromResponse(started);
|
|
3315
|
+
state.pendingReconnectContextInjection = undefined;
|
|
2354
3316
|
const interruptAfterStart = state.turn.status === "starting" && state.turn.interruptAfterStart;
|
|
2355
3317
|
state.turn = { status: "active", turnId, queuedSeq: next.seq };
|
|
2356
3318
|
rememberTurnReplyTarget(state, turnId, next.seq);
|
|
@@ -2376,12 +3338,14 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2376
3338
|
await stopCodexTyping(args, state);
|
|
2377
3339
|
const newCodexThreadId = await startCodexThread(args.codex, args.options);
|
|
2378
3340
|
state.codexThreadId = newCodexThreadId;
|
|
3341
|
+
await bindCurrentCodexThread(args, state);
|
|
2379
3342
|
args.log("codex.thread_rebound", {
|
|
2380
3343
|
kandan_thread_id: state.kandanThreadId,
|
|
2381
3344
|
old_codex_thread_id: oldCodexThreadId ?? null,
|
|
2382
3345
|
new_codex_thread_id: newCodexThreadId
|
|
2383
3346
|
});
|
|
2384
3347
|
await postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId);
|
|
3348
|
+
state.pendingReconnectContextInjection = await fetchReconnectContextInjection(args, state);
|
|
2385
3349
|
requeuePendingKandanMessageFront(state.queue, next);
|
|
2386
3350
|
state.turn = { status: "idle" };
|
|
2387
3351
|
await drainKandanMessageQueue(args, state, payloadContext);
|
|
@@ -2407,6 +3371,26 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2407
3371
|
});
|
|
2408
3372
|
}
|
|
2409
3373
|
}
|
|
3374
|
+
async function fetchReconnectContextInjection(args, state) {
|
|
3375
|
+
if (state.kandanThreadId === undefined) {
|
|
3376
|
+
throw new Error("cannot fetch reconnect context without a Linzumi thread id");
|
|
3377
|
+
}
|
|
3378
|
+
const session = args.options.channelSession;
|
|
3379
|
+
const reply = await pushOk(args.kandan, args.topic, "session:thread_context", {
|
|
3380
|
+
workspace: session.workspaceSlug,
|
|
3381
|
+
channel: session.channelSlug,
|
|
3382
|
+
thread_id: state.kandanThreadId
|
|
3383
|
+
});
|
|
3384
|
+
const messages = parseReconnectContextMessages(reply.messages);
|
|
3385
|
+
const summarizer = args.options.reconnectContextSummarizer ?? createConfiguredReconnectContextSummarizer();
|
|
3386
|
+
const injection = await buildReconnectContextInjection(messages, summarizer);
|
|
3387
|
+
args.log("codex.thread_reconnect_context_prepared", {
|
|
3388
|
+
codex_thread_id: state.codexThreadId ?? null,
|
|
3389
|
+
kandan_thread_id: state.kandanThreadId,
|
|
3390
|
+
context_message_count: messages.length
|
|
3391
|
+
});
|
|
3392
|
+
return injection;
|
|
3393
|
+
}
|
|
2410
3394
|
async function handleCodexServerRequest(args, state, payloadContext, request) {
|
|
2411
3395
|
const params = objectValue(request.params) ?? {};
|
|
2412
3396
|
const turnId = codexNotificationTurnId(params);
|
|
@@ -2444,35 +3428,48 @@ function codexApprovalRequestCanSurface(method) {
|
|
|
2444
3428
|
return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval";
|
|
2445
3429
|
}
|
|
2446
3430
|
async function requestKandanApproval(args, state, request, turnId, payloadContext) {
|
|
2447
|
-
const
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
}, undefined, undefined, state.codexThreadId);
|
|
2460
|
-
args.log("codex.approval_request_pending", {
|
|
2461
|
-
request_id: approval.requestId,
|
|
2462
|
-
source_seq: sourceSeq,
|
|
2463
|
-
turn_id: turnId,
|
|
2464
|
-
method: request.method
|
|
2465
|
-
});
|
|
2466
|
-
return new Promise((resolve2, reject) => {
|
|
2467
|
-
const request2 = {
|
|
2468
|
-
requestId: approval.requestId,
|
|
2469
|
-
sourceSeq,
|
|
2470
|
-
turnId,
|
|
2471
|
-
resolve: resolve2,
|
|
2472
|
-
reject
|
|
3431
|
+
const approvalResult = state.approvalPromptChain.then(async () => {
|
|
3432
|
+
const sourceSeq = activeQueuedSeqForTurn(state.turn, turnId);
|
|
3433
|
+
if (sourceSeq === undefined || state.kandanThreadId === undefined) {
|
|
3434
|
+
const message = `Codex approval request has no active Kandan source message: ${request.method}`;
|
|
3435
|
+
await failActiveCodexTurn(args, state, turnId, message, payloadContext);
|
|
3436
|
+
throw new Error(message);
|
|
3437
|
+
}
|
|
3438
|
+
const approval = codexApprovalMessageState(request);
|
|
3439
|
+
state.activeProcessingState = {
|
|
3440
|
+
seq: sourceSeq,
|
|
3441
|
+
reason: "awaiting approval",
|
|
3442
|
+
approval
|
|
2473
3443
|
};
|
|
2474
|
-
|
|
3444
|
+
const approvalPromise = new Promise((resolve2, reject) => {
|
|
3445
|
+
const pendingRequest = {
|
|
3446
|
+
requestId: approval.requestId,
|
|
3447
|
+
sourceSeq,
|
|
3448
|
+
turnId,
|
|
3449
|
+
resolve: resolve2,
|
|
3450
|
+
reject
|
|
3451
|
+
};
|
|
3452
|
+
state.pendingApprovalRequests.set(approvalRequestKey(pendingRequest.requestId, pendingRequest.sourceSeq), pendingRequest);
|
|
3453
|
+
});
|
|
3454
|
+
await publishMessageState(args, state.kandanThreadId, sourceSeq, {
|
|
3455
|
+
status: "processing",
|
|
3456
|
+
reason: "awaiting approval",
|
|
3457
|
+
approval
|
|
3458
|
+
}, undefined, undefined, state.codexThreadId);
|
|
3459
|
+
args.log("codex.approval_request_pending", {
|
|
3460
|
+
request_id: approval.requestId,
|
|
3461
|
+
source_seq: sourceSeq,
|
|
3462
|
+
turn_id: turnId,
|
|
3463
|
+
method: request.method
|
|
3464
|
+
});
|
|
3465
|
+
return approvalPromise;
|
|
2475
3466
|
});
|
|
3467
|
+
state.approvalPromptChain = approvalResult.then(() => {
|
|
3468
|
+
return;
|
|
3469
|
+
}, () => {
|
|
3470
|
+
return;
|
|
3471
|
+
});
|
|
3472
|
+
return approvalResult;
|
|
2476
3473
|
}
|
|
2477
3474
|
function rejectPendingApprovalRequests(state, error) {
|
|
2478
3475
|
const pendingApprovals = [...state.pendingApprovalRequests.values()];
|
|
@@ -2498,7 +3495,11 @@ async function forwardCompletedCodexTurn(args, state, turnId, payloadContext) {
|
|
|
2498
3495
|
const completingActiveTurn = completingQueuedSeq !== undefined;
|
|
2499
3496
|
const completingLocalTuiTurn = isLocalTuiTurn(state, turnId);
|
|
2500
3497
|
if (completingQueuedSeq !== undefined) {
|
|
2501
|
-
state.turn = {
|
|
3498
|
+
state.turn = {
|
|
3499
|
+
status: "completing",
|
|
3500
|
+
turnId,
|
|
3501
|
+
queuedSeq: completingQueuedSeq
|
|
3502
|
+
};
|
|
2502
3503
|
}
|
|
2503
3504
|
await waitForPendingTuiInputMirror(state, turnId);
|
|
2504
3505
|
await waitForStreamingForwardChains(args, state, payloadContext);
|
|
@@ -2860,7 +3861,10 @@ async function forwardReasoningDeltaPayload(args, state, delta, payloadContext)
|
|
|
2860
3861
|
}
|
|
2861
3862
|
} else {
|
|
2862
3863
|
await editCodexStructuredOutput(args, state, existing.seq, nextContent, codexReasoningStructuredMessage(delta.itemKey, nextContent, "streaming"));
|
|
2863
|
-
rememberStreamingReasoningOutput(state, {
|
|
3864
|
+
rememberStreamingReasoningOutput(state, {
|
|
3865
|
+
...existing,
|
|
3866
|
+
content: nextContent
|
|
3867
|
+
});
|
|
2864
3868
|
}
|
|
2865
3869
|
args.log("kandan.codex_reasoning_delta_forwarded", {
|
|
2866
3870
|
item_key: delta.itemKey,
|
|
@@ -3417,7 +4421,10 @@ function rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId) {
|
|
|
3417
4421
|
if (state.kandanThreadId !== undefined) {
|
|
3418
4422
|
startCodexTypingHeartbeat(args, state, state.kandanThreadId);
|
|
3419
4423
|
}
|
|
3420
|
-
args.log("codex.tui_turn_started", {
|
|
4424
|
+
args.log("codex.tui_turn_started", {
|
|
4425
|
+
turn_id: turnId,
|
|
4426
|
+
codex_thread_id: threadId
|
|
4427
|
+
});
|
|
3421
4428
|
}
|
|
3422
4429
|
function isLocalTuiTurn(state, turnId) {
|
|
3423
4430
|
return state.localTuiTurnIds.has(turnId);
|
|
@@ -3484,7 +4491,10 @@ function clearPendingStreamFlushTimers(state) {
|
|
|
3484
4491
|
clearStreamDeltaFlushTimer(state.fileChangeQueue);
|
|
3485
4492
|
}
|
|
3486
4493
|
function rememberTurnReplyTarget(state, turnId, replyToSeq) {
|
|
3487
|
-
rememberBoundedCacheValue(state.turnReplyTargets, turnId, {
|
|
4494
|
+
rememberBoundedCacheValue(state.turnReplyTargets, turnId, {
|
|
4495
|
+
turnId,
|
|
4496
|
+
replyToSeq
|
|
4497
|
+
});
|
|
3488
4498
|
}
|
|
3489
4499
|
function sourceMessageSeqForTurn(state, turnId) {
|
|
3490
4500
|
return getBoundedCacheValue(state.turnReplyTargets, turnId)?.replyToSeq;
|
|
@@ -3493,20 +4503,12 @@ function fileChangePaths(structured) {
|
|
|
3493
4503
|
const changes = arrayValue(structured.changes) ?? [];
|
|
3494
4504
|
return changes.filter(isJsonObject).map((change) => stringValue(change.path) ?? "").filter((path) => path.trim() !== "");
|
|
3495
4505
|
}
|
|
3496
|
-
async function postCodexThreadReboundMessage(args, state, payloadContext,
|
|
4506
|
+
async function postCodexThreadReboundMessage(args, state, payloadContext, _oldCodexThreadId, newCodexThreadId) {
|
|
3497
4507
|
if (state.kandanThreadId === undefined) {
|
|
3498
4508
|
return;
|
|
3499
4509
|
}
|
|
3500
4510
|
const session = args.options.channelSession;
|
|
3501
|
-
const body = [
|
|
3502
|
-
"Codex reconnected.",
|
|
3503
|
-
"",
|
|
3504
|
-
"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.",
|
|
3505
|
-
"",
|
|
3506
|
-
`Previous Codex thread: ${oldCodexThreadId ?? "unknown"}`,
|
|
3507
|
-
`New Codex thread: ${newCodexThreadId}`
|
|
3508
|
-
].join(`
|
|
3509
|
-
`);
|
|
4511
|
+
const body = "[codex reconnected to new thread]";
|
|
3510
4512
|
await pushOk(args.kandan, args.topic, "session:post_thread_message", {
|
|
3511
4513
|
workspace: session.workspaceSlug,
|
|
3512
4514
|
channel: session.channelSlug,
|
|
@@ -3641,10 +4643,19 @@ async function publishMessageState(args, threadId, seq, state, actorSlug, actorU
|
|
|
3641
4643
|
approval_request_id: state.approval.requestId,
|
|
3642
4644
|
approval_kind: state.approval.kind,
|
|
3643
4645
|
approval_summary: state.approval.summary,
|
|
4646
|
+
...state.approval.port === undefined ? {} : { approval_port: state.approval.port },
|
|
4647
|
+
...state.approval.pid === undefined ? {} : { approval_pid: state.approval.pid },
|
|
4648
|
+
...state.approval.command === undefined ? {} : { approval_command: state.approval.command },
|
|
4649
|
+
...state.approval.cwd === undefined ? {} : { approval_cwd: state.approval.cwd },
|
|
4650
|
+
...state.approval.processName === undefined ? {} : { approval_process_name: state.approval.processName },
|
|
4651
|
+
...state.approval.processIconKey === undefined ? {} : { approval_process_icon_key: state.approval.processIconKey },
|
|
4652
|
+
...state.approval.processIconPath === undefined ? {} : { approval_process_icon_path: state.approval.processIconPath },
|
|
3644
4653
|
...state.approval.reason === undefined ? {} : { approval_reason: state.approval.reason },
|
|
3645
4654
|
...state.approval.choices === undefined ? {} : { approval_choices: state.approval.choices },
|
|
3646
4655
|
...state.approval.allowedActorSlug === undefined ? {} : { approval_allowed_actor_slug: state.approval.allowedActorSlug },
|
|
3647
|
-
...state.approval.allowedActorUserId === undefined ? {} : {
|
|
4656
|
+
...state.approval.allowedActorUserId === undefined ? {} : {
|
|
4657
|
+
approval_allowed_actor_user_id: state.approval.allowedActorUserId
|
|
4658
|
+
}
|
|
3648
4659
|
} : {},
|
|
3649
4660
|
...actorSlug === undefined ? {} : { actor_slug: actorSlug },
|
|
3650
4661
|
...actorUserId === undefined ? {} : { actor_user_id: actorUserId }
|
|
@@ -3698,11 +4709,15 @@ function clearActiveProcessingState(state, seq) {
|
|
|
3698
4709
|
state.activeProcessingState = undefined;
|
|
3699
4710
|
}
|
|
3700
4711
|
}
|
|
3701
|
-
async function codexInputItemsForQueuedKandanMessage(args, message) {
|
|
4712
|
+
async function codexInputItemsForQueuedKandanMessage(args, message, reconnectContextInjection = undefined) {
|
|
3702
4713
|
const attachments = await downloadQueuedKandanAttachments(args, message);
|
|
3703
4714
|
const text = appendDownloadedAttachmentContext(codexInputForQueuedKandanMessage(message), attachments);
|
|
3704
4715
|
const imageItems = attachments.flatMap((attachment) => attachment.isImage ? [{ type: "localImage", path: attachment.path }] : []);
|
|
3705
|
-
return [
|
|
4716
|
+
return [
|
|
4717
|
+
...reconnectContextInjection === undefined ? [] : [reconnectContextInjection],
|
|
4718
|
+
{ type: "text", text },
|
|
4719
|
+
...imageItems
|
|
4720
|
+
];
|
|
3706
4721
|
}
|
|
3707
4722
|
async function downloadQueuedKandanAttachments(args, message) {
|
|
3708
4723
|
if (message.attachments.length === 0) {
|
|
@@ -3835,10 +4850,11 @@ async function uploadedFileIdsForCodexOutput(args, body, structured) {
|
|
|
3835
4850
|
throw new Error("Kandan attachment prepare response missing upload_url");
|
|
3836
4851
|
}
|
|
3837
4852
|
const bytes = await readFile(file.path);
|
|
4853
|
+
const uploadBody = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
3838
4854
|
const response = await fetch(resolveKandanAttachmentUrl(args.options.kandanUrl, uploadUrl), {
|
|
3839
4855
|
method: uploadMethod,
|
|
3840
4856
|
headers: { "content-type": file.contentType },
|
|
3841
|
-
body:
|
|
4857
|
+
body: uploadBody
|
|
3842
4858
|
});
|
|
3843
4859
|
if (!response.ok) {
|
|
3844
4860
|
throw new Error(`Kandan attachment upload failed for ${file.fileName}: ${response.status} ${response.statusText}`);
|
|
@@ -4078,7 +5094,7 @@ function defaultCliAuditLogFile() {
|
|
|
4078
5094
|
return override === undefined || override === "" ? join2(homedir(), ".linzumi", "logs", "command-events.jsonl") : override;
|
|
4079
5095
|
}
|
|
4080
5096
|
function redactForCliLog(value) {
|
|
4081
|
-
return
|
|
5097
|
+
return redactObject(value);
|
|
4082
5098
|
}
|
|
4083
5099
|
function redactValue(value, key) {
|
|
4084
5100
|
if (sensitiveKey(key)) {
|
|
@@ -4223,8 +5239,10 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
|
|
|
4223
5239
|
const child = spawn(codexBin, args, {
|
|
4224
5240
|
cwd,
|
|
4225
5241
|
env: process.env,
|
|
4226
|
-
stdio: ["ignore", "inherit", "inherit"]
|
|
5242
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
5243
|
+
detached: true
|
|
4227
5244
|
});
|
|
5245
|
+
const stop = () => stopCodexAppServerProcess(child);
|
|
4228
5246
|
writeCliAuditEvent("process.spawned", {
|
|
4229
5247
|
command: codexBin,
|
|
4230
5248
|
args,
|
|
@@ -4250,33 +5268,57 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
|
|
|
4250
5268
|
try {
|
|
4251
5269
|
await waitForReadyz(url, child);
|
|
4252
5270
|
} catch (error) {
|
|
4253
|
-
|
|
5271
|
+
stop();
|
|
4254
5272
|
throw error;
|
|
4255
5273
|
}
|
|
4256
|
-
return { url, process: child };
|
|
5274
|
+
return { url, process: child, stop };
|
|
5275
|
+
}
|
|
5276
|
+
function stopCodexAppServerProcess(child, killProcess = process.kill) {
|
|
5277
|
+
if (child.pid !== undefined) {
|
|
5278
|
+
try {
|
|
5279
|
+
killProcess(-child.pid, "SIGINT");
|
|
5280
|
+
return;
|
|
5281
|
+
} catch (error) {
|
|
5282
|
+
logProcessGroupSignalFailure(child.pid, error);
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
child.kill("SIGINT");
|
|
5286
|
+
}
|
|
5287
|
+
function logProcessGroupSignalFailure(pid, error) {
|
|
5288
|
+
const code = processSignalErrorCode(error);
|
|
5289
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5290
|
+
const event = code === "EPERM" ? "process.group_signal_denied" : code === "ESRCH" ? "process.group_signal_missing" : "process.group_signal_failed";
|
|
5291
|
+
writeCliAuditEvent(event, {
|
|
5292
|
+
pid,
|
|
5293
|
+
signal: "SIGINT",
|
|
5294
|
+
code,
|
|
5295
|
+
message,
|
|
5296
|
+
fallback: "child_signal",
|
|
5297
|
+
purpose: "codex.app_server"
|
|
5298
|
+
});
|
|
5299
|
+
if (code === "EPERM") {
|
|
5300
|
+
process.stderr.write(`codex app-server process-group SIGINT denied for pid ${pid}; falling back to direct child SIGINT
|
|
5301
|
+
`);
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
5304
|
+
function processSignalErrorCode(error) {
|
|
5305
|
+
if (error !== null && typeof error === "object" && "code" in error) {
|
|
5306
|
+
const code = error.code;
|
|
5307
|
+
return typeof code === "string" ? code : undefined;
|
|
5308
|
+
}
|
|
5309
|
+
return;
|
|
4257
5310
|
}
|
|
4258
5311
|
function codexAppServerArgs(listenUrl, options = {}) {
|
|
4259
|
-
return [
|
|
4260
|
-
"app-server",
|
|
4261
|
-
...codexConfigArgs(options),
|
|
4262
|
-
"--listen",
|
|
4263
|
-
listenUrl
|
|
4264
|
-
];
|
|
5312
|
+
return ["app-server", ...codexConfigArgs(options), "--listen", listenUrl];
|
|
4265
5313
|
}
|
|
4266
5314
|
function codexConfigArgs(options) {
|
|
4267
5315
|
return [
|
|
4268
|
-
...options.model === undefined ? [] : [
|
|
4269
|
-
"-c",
|
|
4270
|
-
`model=${JSON.stringify(options.model)}`
|
|
4271
|
-
],
|
|
5316
|
+
...options.model === undefined ? [] : ["-c", `model=${JSON.stringify(options.model)}`],
|
|
4272
5317
|
...options.reasoningEffort === undefined ? [] : [
|
|
4273
5318
|
"-c",
|
|
4274
5319
|
`model_reasoning_effort=${JSON.stringify(options.reasoningEffort)}`
|
|
4275
5320
|
],
|
|
4276
|
-
...options.fast === true ? [
|
|
4277
|
-
"-c",
|
|
4278
|
-
`service_tier=${JSON.stringify("fast")}`
|
|
4279
|
-
] : []
|
|
5321
|
+
...options.fast === true ? ["-c", `service_tier=${JSON.stringify("fast")}`] : []
|
|
4280
5322
|
];
|
|
4281
5323
|
}
|
|
4282
5324
|
async function connectCodexAppServer(websocketUrl, socketFactory = (url) => new WebSocket(url)) {
|
|
@@ -4630,7 +5672,9 @@ async function handleForwardHttpRequest(control, allowedPorts) {
|
|
|
4630
5672
|
method: control.method,
|
|
4631
5673
|
headers: requestHeaders(control.headers),
|
|
4632
5674
|
redirect: "manual",
|
|
4633
|
-
...bodyDecision.body === undefined ? {} : {
|
|
5675
|
+
...bodyDecision.body === undefined ? {} : {
|
|
5676
|
+
body: bodyDecision.body.buffer.slice(bodyDecision.body.byteOffset, bodyDecision.body.byteOffset + bodyDecision.body.byteLength)
|
|
5677
|
+
}
|
|
4634
5678
|
};
|
|
4635
5679
|
const response = await fetchWithHttpsFallback(control.port, control.path, control.queryString, request);
|
|
4636
5680
|
const upstreamBuffer = Buffer.from(await response.arrayBuffer());
|
|
@@ -5100,10 +6144,12 @@ function prepareCodeServerProfile(collaboration, editorRuntime) {
|
|
|
5100
6144
|
const userDataDir = mkdtempSync(join4(tmpdir(), "kandan-local-editor-"));
|
|
5101
6145
|
const extensionsDir = join4(userDataDir, "extensions");
|
|
5102
6146
|
const collaborationServerDir = join4(userDataDir, "collaboration-server");
|
|
6147
|
+
const tempDir = join4(userDataDir, "tmp");
|
|
5103
6148
|
const userSettingsDir = join4(userDataDir, "User");
|
|
5104
6149
|
mkdirSync3(userSettingsDir, { recursive: true });
|
|
5105
6150
|
mkdirSync3(extensionsDir, { recursive: true });
|
|
5106
6151
|
mkdirSync3(collaborationServerDir, { recursive: true });
|
|
6152
|
+
mkdirSync3(tempDir, { recursive: true });
|
|
5107
6153
|
if (editorRuntime !== undefined) {
|
|
5108
6154
|
installDirectory(editorRuntime.assets.documentStateExtensionDir, join4(extensionsDir, "kandan.document-state-telemetry"));
|
|
5109
6155
|
}
|
|
@@ -5136,6 +6182,12 @@ function prepareCodeServerLaunch(options) {
|
|
|
5136
6182
|
"-p",
|
|
5137
6183
|
codeServerSandboxProfile(options, codeServerExecutable.directory),
|
|
5138
6184
|
"--",
|
|
6185
|
+
"/bin/sh",
|
|
6186
|
+
"-c",
|
|
6187
|
+
'export HOME="$1"; export PWD="$1"; export TMPDIR="$2"; export TMP="$2"; export TEMP="$2"; shift 2; exec "$@"',
|
|
6188
|
+
"kandan-code-server-env",
|
|
6189
|
+
options.cwd,
|
|
6190
|
+
join4(options.userDataDir, "tmp"),
|
|
5139
6191
|
codeServerExecutable.command,
|
|
5140
6192
|
...codeServerArgs(options.port, options.cwd, options.userDataDir, options.extensionsDir)
|
|
5141
6193
|
]
|
|
@@ -5416,10 +6468,14 @@ function installDirectory(sourceDir, destinationDir) {
|
|
|
5416
6468
|
}
|
|
5417
6469
|
function codeServerEnv(env, cwd, userDataDir, collaboration) {
|
|
5418
6470
|
const { PORT: _port, ...hostEnv } = env;
|
|
6471
|
+
const tempDir = join4(userDataDir, "tmp");
|
|
5419
6472
|
const base = {
|
|
5420
6473
|
...hostEnv,
|
|
5421
6474
|
HOME: cwd,
|
|
5422
6475
|
PWD: cwd,
|
|
6476
|
+
TMPDIR: tempDir,
|
|
6477
|
+
TMP: tempDir,
|
|
6478
|
+
TEMP: tempDir,
|
|
5423
6479
|
XDG_CACHE_HOME: join4(userDataDir, "xdg-cache"),
|
|
5424
6480
|
XDG_CONFIG_HOME: join4(userDataDir, "xdg-config"),
|
|
5425
6481
|
XDG_DATA_HOME: join4(userDataDir, "xdg-data")
|
|
@@ -5808,7 +6864,7 @@ async function exchangeCodeForToken(args) {
|
|
|
5808
6864
|
};
|
|
5809
6865
|
}
|
|
5810
6866
|
function stringBodyField(body, key) {
|
|
5811
|
-
const value =
|
|
6867
|
+
const value = body[key];
|
|
5812
6868
|
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
5813
6869
|
}
|
|
5814
6870
|
function startCallbackServer(args) {
|
|
@@ -5878,7 +6934,9 @@ function isTcpAddress(address) {
|
|
|
5878
6934
|
return typeof address === "object" && address !== null && typeof address.port === "number";
|
|
5879
6935
|
}
|
|
5880
6936
|
function writeOauthResult(response, args) {
|
|
5881
|
-
response.writeHead(args.status, {
|
|
6937
|
+
response.writeHead(args.status, {
|
|
6938
|
+
"content-type": "text/html; charset=utf-8"
|
|
6939
|
+
});
|
|
5882
6940
|
response.end(oauthResultHtml(args));
|
|
5883
6941
|
}
|
|
5884
6942
|
function oauthResultHtml(args) {
|
|
@@ -6493,7 +7551,7 @@ function assertStartDependencies(status) {
|
|
|
6493
7551
|
throw new Error(`Codex is not available at ${status.codex.command}. Install Codex or pass --codex-bin <path>.`);
|
|
6494
7552
|
}
|
|
6495
7553
|
if (status.editorRuntime?.status === "unavailable" || status.codeServer?.available === false && status.codeServer.reason !== "not_configured") {
|
|
6496
|
-
throw new Error("The
|
|
7554
|
+
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.");
|
|
6497
7555
|
}
|
|
6498
7556
|
}
|
|
6499
7557
|
function dependencyStatusPayload(status) {
|
|
@@ -6877,7 +7935,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6877
7935
|
forwardPortAttributions.set(port, {
|
|
6878
7936
|
kandanThreadId: attribution.kandanThreadId ?? null,
|
|
6879
7937
|
codexThreadId: attribution.codexThreadId ?? null,
|
|
6880
|
-
channelSlug: attribution.channelSlug ?? null
|
|
7938
|
+
channelSlug: attribution.channelSlug ?? null,
|
|
7939
|
+
processName: attribution.processName ?? null,
|
|
7940
|
+
processIconKey: attribution.processIconKey ?? null
|
|
6881
7941
|
});
|
|
6882
7942
|
};
|
|
6883
7943
|
const clearForwardPortAttribution = (port) => {
|
|
@@ -6889,7 +7949,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6889
7949
|
port,
|
|
6890
7950
|
kandanThreadId: attribution?.kandanThreadId ?? null,
|
|
6891
7951
|
codexThreadId: attribution?.codexThreadId ?? null,
|
|
6892
|
-
channelSlug: attribution?.channelSlug ?? null
|
|
7952
|
+
channelSlug: attribution?.channelSlug ?? null,
|
|
7953
|
+
processName: attribution?.processName ?? null,
|
|
7954
|
+
processIconKey: attribution?.processIconKey ?? null
|
|
6893
7955
|
};
|
|
6894
7956
|
});
|
|
6895
7957
|
const allowedCwds = { value: [...options.allowedCwds] };
|
|
@@ -6908,6 +7970,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6908
7970
|
codexRemoteTui: true,
|
|
6909
7971
|
startInstance: allowedCwds.value.length > 0,
|
|
6910
7972
|
allowedCwds: allowedCwds.value,
|
|
7973
|
+
missingConfiguredAllowedCwds: options.missingConfiguredAllowedCwds ?? [],
|
|
6911
7974
|
allowedCwdSuggestions: allowedCwdSuggestions(options.cwd, allowedCwds.value),
|
|
6912
7975
|
portForwarding: liveForwardPorts.size > 0,
|
|
6913
7976
|
allowedPorts: Array.from(liveForwardPorts).sort((left, right) => left - right),
|
|
@@ -6922,7 +7985,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6922
7985
|
const joinPayload = () => ({
|
|
6923
7986
|
clientName: "kandan-local-codex-runner",
|
|
6924
7987
|
version: "0.0.1",
|
|
6925
|
-
workspace: options.channelSession?.workspaceSlug ?? null,
|
|
7988
|
+
workspace: options.channelSession?.workspaceSlug ?? options.workspaceSlug ?? null,
|
|
6926
7989
|
channel: options.channelSession?.channelSlug ?? null,
|
|
6927
7990
|
capabilities: capabilitiesPayload()
|
|
6928
7991
|
});
|
|
@@ -6940,7 +8003,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6940
8003
|
const started = options.codexUrl === undefined ? await startOwnedCodexAppServer(options) : undefined;
|
|
6941
8004
|
if (started !== undefined) {
|
|
6942
8005
|
cleanup.actions.push(() => {
|
|
6943
|
-
started.
|
|
8006
|
+
started.stop();
|
|
6944
8007
|
});
|
|
6945
8008
|
}
|
|
6946
8009
|
const codexUrl = options.codexUrl ?? started?.url;
|
|
@@ -6990,6 +8053,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6990
8053
|
cleanup.actions.push(() => codex.close());
|
|
6991
8054
|
const seq = { value: 0 };
|
|
6992
8055
|
const codexThreads = options.channelSession === undefined ? await discoverCodexThreads(codex, options.cwd) : [];
|
|
8056
|
+
const discoveredCodexThreads = { value: codexThreads };
|
|
6993
8057
|
const runnerHost = hostname2();
|
|
6994
8058
|
const instancePayload = {
|
|
6995
8059
|
instanceId,
|
|
@@ -7038,21 +8102,27 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7038
8102
|
kandan.onReconnect(() => channelSession.handleKandanReconnect());
|
|
7039
8103
|
}
|
|
7040
8104
|
const dynamicChannelSessions = new Map;
|
|
8105
|
+
kandan.onReconnect(() => rebindDynamicChannelSessionsOnReconnect(dynamicChannelSessions.values()));
|
|
7041
8106
|
cleanup.actions.push(async () => {
|
|
7042
8107
|
await Promise.all(Array.from(dynamicChannelSessions.values(), (session) => session.close()));
|
|
7043
8108
|
dynamicChannelSessions.clear();
|
|
7044
8109
|
});
|
|
7045
|
-
const
|
|
8110
|
+
const attachThreadSession = async (control, cwd, codexThreadId) => {
|
|
7046
8111
|
const workspaceSlug = normalizedWorkDescription(control.workspace);
|
|
7047
8112
|
const channelSlug = normalizedWorkDescription(control.channel);
|
|
7048
8113
|
const kandanThreadId = normalizedWorkDescription(control.threadId);
|
|
7049
|
-
if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined
|
|
8114
|
+
if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined) {
|
|
7050
8115
|
return;
|
|
7051
8116
|
}
|
|
8117
|
+
const existingSession = dynamicChannelSessions.get(kandanThreadId);
|
|
8118
|
+
if (existingSession !== undefined) {
|
|
8119
|
+
return existingSession;
|
|
8120
|
+
}
|
|
7052
8121
|
const listenUser = options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
|
|
7053
8122
|
if (listenUser === undefined) {
|
|
7054
8123
|
throw new Error("missing listen user for Commander-started Codex session");
|
|
7055
8124
|
}
|
|
8125
|
+
const runtimeSettings = startInstanceRuntimeSettings(options, control);
|
|
7056
8126
|
const session = await attachChannelSession({
|
|
7057
8127
|
kandan,
|
|
7058
8128
|
codex,
|
|
@@ -7085,10 +8155,10 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7085
8155
|
rootSeq: integerValue(control.rootSeq),
|
|
7086
8156
|
codexThreadId,
|
|
7087
8157
|
listenUser,
|
|
7088
|
-
model:
|
|
7089
|
-
reasoningEffort:
|
|
7090
|
-
sandbox:
|
|
7091
|
-
approvalPolicy:
|
|
8158
|
+
model: runtimeSettings.model,
|
|
8159
|
+
reasoningEffort: runtimeSettings.reasoningEffort,
|
|
8160
|
+
sandbox: runtimeSettings.sandbox,
|
|
8161
|
+
approvalPolicy: runtimeSettings.approvalPolicy
|
|
7092
8162
|
}
|
|
7093
8163
|
},
|
|
7094
8164
|
log
|
|
@@ -7101,10 +8171,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7101
8171
|
codexUrl,
|
|
7102
8172
|
cwd: options.cwd,
|
|
7103
8173
|
hostname: runnerHost,
|
|
7104
|
-
workspace: options.channelSession?.workspaceSlug ?? null,
|
|
8174
|
+
workspace: options.channelSession?.workspaceSlug ?? options.workspaceSlug ?? null,
|
|
7105
8175
|
channel: options.channelSession?.channelSlug ?? null,
|
|
7106
8176
|
threadId: channelSession?.currentKandanThreadId() ?? null,
|
|
7107
8177
|
codexThreadId: channelSession?.currentCodexThreadId() ?? null,
|
|
8178
|
+
codexThreads: discoveredCodexThreads.value,
|
|
7108
8179
|
model: options.channelSession?.model ?? null,
|
|
7109
8180
|
reasoningEffort: options.channelSession?.reasoningEffort ?? null,
|
|
7110
8181
|
fast: options.fast ?? false,
|
|
@@ -7115,11 +8186,19 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7115
8186
|
message: error instanceof Error ? error.message : String(error)
|
|
7116
8187
|
});
|
|
7117
8188
|
});
|
|
8189
|
+
const refreshDiscoveredCodexThreads = () => discoverCodexThreads(codex, options.cwd).then((threads) => {
|
|
8190
|
+
discoveredCodexThreads.value = threads;
|
|
8191
|
+
return kandan.push(topic, "heartbeat", heartbeatPayload());
|
|
8192
|
+
}).catch((error) => {
|
|
8193
|
+
log("kandan.codex_threads_refresh_failed", {
|
|
8194
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8195
|
+
});
|
|
8196
|
+
});
|
|
7118
8197
|
const heartbeatInterval = setInterval(() => {
|
|
7119
|
-
pushHeartbeat();
|
|
8198
|
+
channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat();
|
|
7120
8199
|
}, 15000);
|
|
7121
8200
|
cleanup.actions.push(() => clearInterval(heartbeatInterval));
|
|
7122
|
-
kandan.onReconnect(() => pushHeartbeat().then(() => {
|
|
8201
|
+
kandan.onReconnect(() => (channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat()).then(() => {
|
|
7123
8202
|
return;
|
|
7124
8203
|
}));
|
|
7125
8204
|
pushHeartbeat();
|
|
@@ -7153,6 +8232,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7153
8232
|
});
|
|
7154
8233
|
});
|
|
7155
8234
|
}
|
|
8235
|
+
if (channelSession === undefined && notification.method === "thread/started") {
|
|
8236
|
+
refreshDiscoveredCodexThreads();
|
|
8237
|
+
}
|
|
7156
8238
|
log("codex.notification", {
|
|
7157
8239
|
method: notification.method,
|
|
7158
8240
|
metadata
|
|
@@ -7264,11 +8346,35 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7264
8346
|
pushHeartbeat();
|
|
7265
8347
|
return;
|
|
7266
8348
|
}
|
|
8349
|
+
if (isSetPortForwardEnabledControl(control)) {
|
|
8350
|
+
switch (control.enabled) {
|
|
8351
|
+
case true:
|
|
8352
|
+
liveForwardPorts.add(control.port);
|
|
8353
|
+
break;
|
|
8354
|
+
case false:
|
|
8355
|
+
liveForwardPorts.delete(control.port);
|
|
8356
|
+
managedForwardPorts.delete(control.port);
|
|
8357
|
+
clearForwardPortAttribution(control.port);
|
|
8358
|
+
kandan.push(topic, "forward_port_revoked", {
|
|
8359
|
+
instanceId,
|
|
8360
|
+
port: control.port,
|
|
8361
|
+
reason: "user_disabled",
|
|
8362
|
+
capabilities: revocationCapabilities(capabilitiesPayload(), control.port)
|
|
8363
|
+
}).catch((error) => {
|
|
8364
|
+
log("kandan.forward_port_revoked_push_failed", {
|
|
8365
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8366
|
+
});
|
|
8367
|
+
});
|
|
8368
|
+
break;
|
|
8369
|
+
}
|
|
8370
|
+
pushHeartbeat();
|
|
8371
|
+
return;
|
|
8372
|
+
}
|
|
7267
8373
|
resolveSessionControl(channelSession, dynamicChannelSessions, control).then((handled) => {
|
|
7268
8374
|
if (handled !== undefined) {
|
|
7269
8375
|
return handled;
|
|
7270
8376
|
}
|
|
7271
|
-
return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control,
|
|
8377
|
+
return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control, attachThreadSession);
|
|
7272
8378
|
}).then((response) => {
|
|
7273
8379
|
return kandan.push(topic, "codex_response", response);
|
|
7274
8380
|
}).catch((error) => {
|
|
@@ -7333,6 +8439,8 @@ async function discoverCodexThreads(codex, cwd) {
|
|
|
7333
8439
|
const data = arrayValue(result?.data);
|
|
7334
8440
|
return data === undefined ? [] : data.filter(isJsonObject).map((thread) => ({
|
|
7335
8441
|
id: stringValue(thread.id) ?? "",
|
|
8442
|
+
title: stringValue(thread.title) ?? stringValue(thread.name) ?? stringValue(thread.preview) ?? "",
|
|
8443
|
+
description: stringValue(thread.description) ?? stringValue(thread.summary) ?? stringValue(thread.preview) ?? "",
|
|
7336
8444
|
preview: stringValue(thread.preview) ?? "",
|
|
7337
8445
|
cwd: stringValue(thread.cwd) ?? "",
|
|
7338
8446
|
source: stringValue(thread.source) ?? "",
|
|
@@ -7408,6 +8516,9 @@ function launchCodexTui(codexBin, codexUrl, cwd, codexThreadId, session, fast) {
|
|
|
7408
8516
|
});
|
|
7409
8517
|
return child;
|
|
7410
8518
|
}
|
|
8519
|
+
async function rebindDynamicChannelSessionsOnReconnect(sessions) {
|
|
8520
|
+
await Promise.all(Array.from(sessions, (session) => session.handleKandanReconnect()));
|
|
8521
|
+
}
|
|
7411
8522
|
function forwardedHeaderValue(headers, name) {
|
|
7412
8523
|
if (!Array.isArray(headers)) {
|
|
7413
8524
|
return;
|
|
@@ -7465,6 +8576,7 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
|
|
|
7465
8576
|
ensureCodexProjectTrusted(cwd.cwd);
|
|
7466
8577
|
}
|
|
7467
8578
|
const developerPrompt = normalizedWorkDescription(control.developerPrompt);
|
|
8579
|
+
const runtimeSettings = startInstanceRuntimeSettings(options, control);
|
|
7468
8580
|
const response = await codex.request("thread/start", {
|
|
7469
8581
|
cwd: cwd.cwd,
|
|
7470
8582
|
serviceName: "kandan-local-runner",
|
|
@@ -7473,11 +8585,11 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
|
|
|
7473
8585
|
cwd: cwd.cwd,
|
|
7474
8586
|
developerPrompt
|
|
7475
8587
|
}),
|
|
7476
|
-
...
|
|
7477
|
-
...
|
|
7478
|
-
...
|
|
7479
|
-
...
|
|
7480
|
-
...
|
|
8588
|
+
...runtimeSettings.model === undefined ? {} : { model: runtimeSettings.model },
|
|
8589
|
+
...runtimeSettings.reasoningEffort === undefined ? {} : { reasoningEffort: runtimeSettings.reasoningEffort },
|
|
8590
|
+
...runtimeSettings.approvalPolicy === undefined ? {} : { approvalPolicy: runtimeSettings.approvalPolicy },
|
|
8591
|
+
...runtimeSettings.sandbox === undefined ? {} : { sandbox: runtimeSettings.sandbox },
|
|
8592
|
+
...runtimeSettings.fast === true ? { serviceTier: "fast" } : {}
|
|
7481
8593
|
});
|
|
7482
8594
|
const codexThreadId = extractStartedThreadId(response);
|
|
7483
8595
|
const workDescription = normalizedWorkDescription(control.workDescription);
|
|
@@ -7487,10 +8599,11 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
|
|
|
7487
8599
|
const startedThreadSession = codexThreadId !== undefined && onStartedThread !== undefined ? await onStartedThread(control, cwd.cwd, codexThreadId) : undefined;
|
|
7488
8600
|
if (codexThreadId !== undefined && workDescription !== undefined) {
|
|
7489
8601
|
const rootSeq = integerValue(control.rootSeq);
|
|
7490
|
-
|
|
8602
|
+
const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
|
|
8603
|
+
if (startedThreadSession !== undefined && sourceSeq !== undefined) {
|
|
7491
8604
|
const identity = identityFromAccessToken(options.token);
|
|
7492
8605
|
await startedThreadSession.startThreadMessageTurn({
|
|
7493
|
-
seq:
|
|
8606
|
+
seq: sourceSeq,
|
|
7494
8607
|
body: workDescription,
|
|
7495
8608
|
actorSlug: identity.actorUsername,
|
|
7496
8609
|
actorUserId: identity.actorUserId
|
|
@@ -7510,6 +8623,52 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
|
|
|
7510
8623
|
response
|
|
7511
8624
|
};
|
|
7512
8625
|
}
|
|
8626
|
+
case "reconnect_thread": {
|
|
8627
|
+
const cwd = resolveAllowedCwd(control.cwd, allowedCwds);
|
|
8628
|
+
if (!cwd.ok) {
|
|
8629
|
+
return {
|
|
8630
|
+
instanceId,
|
|
8631
|
+
controlType: control.type,
|
|
8632
|
+
ok: false,
|
|
8633
|
+
error: cwd.reason
|
|
8634
|
+
};
|
|
8635
|
+
}
|
|
8636
|
+
if (options.codexUrl === undefined) {
|
|
8637
|
+
ensureCodexProjectTrusted(cwd.cwd);
|
|
8638
|
+
}
|
|
8639
|
+
const codexThreadId = normalizedWorkDescription(control.codexThreadId);
|
|
8640
|
+
if (codexThreadId === undefined) {
|
|
8641
|
+
return {
|
|
8642
|
+
instanceId,
|
|
8643
|
+
controlType: control.type,
|
|
8644
|
+
ok: false,
|
|
8645
|
+
error: "missing_codex_thread_id"
|
|
8646
|
+
};
|
|
8647
|
+
}
|
|
8648
|
+
const workDescription = normalizedWorkDescription(control.workDescription);
|
|
8649
|
+
const startedThreadSession = onStartedThread === undefined ? undefined : await onStartedThread(control, cwd.cwd, codexThreadId);
|
|
8650
|
+
if (workDescription !== undefined) {
|
|
8651
|
+
const rootSeq = integerValue(control.rootSeq);
|
|
8652
|
+
const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
|
|
8653
|
+
if (startedThreadSession === undefined || sourceSeq === undefined) {
|
|
8654
|
+
throw new Error("cannot reconnect a Kandan thread without a session");
|
|
8655
|
+
}
|
|
8656
|
+
const identity = identityFromAccessToken(options.token);
|
|
8657
|
+
await startedThreadSession.startThreadMessageTurn({
|
|
8658
|
+
seq: sourceSeq,
|
|
8659
|
+
body: workDescription,
|
|
8660
|
+
actorSlug: identity.actorUsername,
|
|
8661
|
+
actorUserId: identity.actorUserId
|
|
8662
|
+
});
|
|
8663
|
+
}
|
|
8664
|
+
return {
|
|
8665
|
+
instanceId,
|
|
8666
|
+
controlType: control.type,
|
|
8667
|
+
cwd: cwd.cwd,
|
|
8668
|
+
matchedRoot: cwd.matchedRoot,
|
|
8669
|
+
codexThreadId
|
|
8670
|
+
};
|
|
8671
|
+
}
|
|
7513
8672
|
case "start_turn": {
|
|
7514
8673
|
const response = await codex.request("turn/start", {
|
|
7515
8674
|
threadId: control.threadId,
|
|
@@ -7560,6 +8719,8 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
|
|
|
7560
8719
|
case "interrupt_queued_messages":
|
|
7561
8720
|
case "resolve_codex_approval_request":
|
|
7562
8721
|
case "resolve_port_forward_request":
|
|
8722
|
+
case "update_session_settings":
|
|
8723
|
+
case "set_port_forward_enabled":
|
|
7563
8724
|
case "forward_http_request":
|
|
7564
8725
|
case "forward_websocket_open":
|
|
7565
8726
|
case "forward_websocket_send":
|
|
@@ -7649,6 +8810,16 @@ ${developerPrompt}`,
|
|
|
7649
8810
|
client_message_id: `codex-start-instructions-${threadId}`
|
|
7650
8811
|
});
|
|
7651
8812
|
}
|
|
8813
|
+
function startInstanceRuntimeSettings(options, control) {
|
|
8814
|
+
const session = options.channelSession;
|
|
8815
|
+
return {
|
|
8816
|
+
model: control.model ?? session?.model,
|
|
8817
|
+
reasoningEffort: control.reasoningEffort ?? session?.reasoningEffort,
|
|
8818
|
+
approvalPolicy: control.approvalPolicy ?? session?.approvalPolicy,
|
|
8819
|
+
sandbox: control.sandbox ?? session?.sandbox,
|
|
8820
|
+
fast: control.fast ?? options.fast
|
|
8821
|
+
};
|
|
8822
|
+
}
|
|
7652
8823
|
async function startOwnedCodexAppServer(options) {
|
|
7653
8824
|
ensureCodexProjectTrusted(options.cwd);
|
|
7654
8825
|
return await startCodexAppServer(options.codexBin, options.cwd, {
|
|
@@ -7660,6 +8831,9 @@ async function startOwnedCodexAppServer(options) {
|
|
|
7660
8831
|
function isUpdateRunnerConfigControl(control) {
|
|
7661
8832
|
return control.type === "update_runner_config";
|
|
7662
8833
|
}
|
|
8834
|
+
function isSetPortForwardEnabledControl(control) {
|
|
8835
|
+
return control.type === "set_port_forward_enabled" && Number.isInteger(control.port) && control.port > 0 && control.port <= 65535;
|
|
8836
|
+
}
|
|
7663
8837
|
function normalizeAllowedCwds(values) {
|
|
7664
8838
|
return Array.from(new Set(values.flatMap((value) => {
|
|
7665
8839
|
const normalized = value.trim();
|
|
@@ -7810,7 +8984,13 @@ async function acquireAndCacheToken(args) {
|
|
|
7810
8984
|
}
|
|
7811
8985
|
|
|
7812
8986
|
// src/localConfig.ts
|
|
7813
|
-
import {
|
|
8987
|
+
import {
|
|
8988
|
+
existsSync as existsSync5,
|
|
8989
|
+
mkdirSync as mkdirSync6,
|
|
8990
|
+
readFileSync as readFileSync4,
|
|
8991
|
+
realpathSync as realpathSync4,
|
|
8992
|
+
writeFileSync as writeFileSync5
|
|
8993
|
+
} from "node:fs";
|
|
7814
8994
|
import { homedir as homedir6 } from "node:os";
|
|
7815
8995
|
import { dirname as dirname5, resolve as resolve5 } from "node:path";
|
|
7816
8996
|
function localConfigPath(env = process.env) {
|
|
@@ -7830,14 +9010,22 @@ function readLocalConfig(path = localConfigPath()) {
|
|
|
7830
9010
|
allowedCwds: uniqueStrings(parsed.allowedCwds)
|
|
7831
9011
|
};
|
|
7832
9012
|
}
|
|
7833
|
-
function
|
|
7834
|
-
|
|
9013
|
+
function readConfiguredAllowedCwdState(path = localConfigPath()) {
|
|
9014
|
+
const allowedCwds = [];
|
|
9015
|
+
const missingCwds = [];
|
|
9016
|
+
for (const cwd of readLocalConfig(path).allowedCwds) {
|
|
7835
9017
|
try {
|
|
7836
|
-
|
|
9018
|
+
const absolutePath = resolve5(expandUserPath(cwd));
|
|
9019
|
+
const realPath = realpathSync4(absolutePath);
|
|
9020
|
+
allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
|
|
7837
9021
|
} catch (_error) {
|
|
7838
|
-
|
|
9022
|
+
missingCwds.push(cwd);
|
|
7839
9023
|
}
|
|
7840
|
-
}
|
|
9024
|
+
}
|
|
9025
|
+
return {
|
|
9026
|
+
allowedCwds: uniqueStrings(allowedCwds),
|
|
9027
|
+
missingCwds: uniqueStrings(missingCwds)
|
|
9028
|
+
};
|
|
7841
9029
|
}
|
|
7842
9030
|
function addAllowedCwd(pathValue, path = localConfigPath()) {
|
|
7843
9031
|
const normalizedPath = realpathSync4(resolve5(expandUserPath(pathValue)));
|
|
@@ -7866,7 +9054,9 @@ function isConfigPayload(value) {
|
|
|
7866
9054
|
return typeof value === "object" && value !== null && value.version === 1 && Array.isArray(value.allowedCwds) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
|
|
7867
9055
|
}
|
|
7868
9056
|
function uniqueStrings(values) {
|
|
7869
|
-
return [
|
|
9057
|
+
return [
|
|
9058
|
+
...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))
|
|
9059
|
+
];
|
|
7870
9060
|
}
|
|
7871
9061
|
function realpathOrResolved(pathValue) {
|
|
7872
9062
|
try {
|
|
@@ -9382,7 +10572,9 @@ function readProcessIdentity(pid) {
|
|
|
9382
10572
|
if (match === null) {
|
|
9383
10573
|
return { command: output };
|
|
9384
10574
|
}
|
|
9385
|
-
|
|
10575
|
+
const startedAt = match[1];
|
|
10576
|
+
const processCommand = match[2];
|
|
10577
|
+
return startedAt === undefined || processCommand === undefined ? { command: output } : { startedAt, command: processCommand };
|
|
9386
10578
|
} catch (_error) {
|
|
9387
10579
|
return;
|
|
9388
10580
|
}
|
|
@@ -9495,7 +10687,7 @@ async function main(args) {
|
|
|
9495
10687
|
process.stdout.write(connectGuideText());
|
|
9496
10688
|
return;
|
|
9497
10689
|
case "version":
|
|
9498
|
-
process.stdout.write(`linzumi 0.0.
|
|
10690
|
+
process.stdout.write(`linzumi 0.0.39-beta
|
|
9499
10691
|
`);
|
|
9500
10692
|
return;
|
|
9501
10693
|
case "auth":
|
|
@@ -9589,12 +10781,17 @@ function runHelloCommand(args) {
|
|
|
9589
10781
|
process.stdout.write(helloHelpText());
|
|
9590
10782
|
return;
|
|
9591
10783
|
}
|
|
10784
|
+
const rootPath = stringValue3(values, "dir");
|
|
10785
|
+
const parentDir = stringValue3(values, "parent-dir");
|
|
10786
|
+
const name = stringValue3(values, "name");
|
|
10787
|
+
const port = tcpPortValue(values, "port");
|
|
10788
|
+
const host = stringValue3(values, "host");
|
|
9592
10789
|
const project = createHelloLinzumiProject({
|
|
9593
|
-
rootPath:
|
|
9594
|
-
parentDir
|
|
9595
|
-
name:
|
|
9596
|
-
port:
|
|
9597
|
-
host:
|
|
10790
|
+
...rootPath === undefined ? {} : { rootPath },
|
|
10791
|
+
...parentDir === undefined ? {} : { parentDir: resolveUserPath(parentDir) },
|
|
10792
|
+
...name === undefined ? {} : { name },
|
|
10793
|
+
...port === undefined ? {} : { port },
|
|
10794
|
+
...host === undefined ? {} : { host },
|
|
9598
10795
|
reset: values.get("reset") === true
|
|
9599
10796
|
});
|
|
9600
10797
|
if (values.get("json") === true) {
|
|
@@ -9875,12 +11072,13 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
9875
11072
|
}
|
|
9876
11073
|
const tokenFilePath = stringValue3(values, "agent-token-file") ?? defaultAgentTokenFilePath();
|
|
9877
11074
|
const tokenFile = readStoredAgentTokenFile(tokenFilePath, deps.readTextFile);
|
|
9878
|
-
const channelSlug =
|
|
9879
|
-
const listenUser = stringValue3(values, "listen-user") ?? requiredStoredOwnerUsername(tokenFile.ownerUsername);
|
|
11075
|
+
const channelSlug = tokenFile.channelId;
|
|
11076
|
+
const listenUser = channelSlug === undefined ? undefined : stringValue3(values, "listen-user") ?? requiredStoredOwnerUsername(tokenFile.ownerUsername);
|
|
9880
11077
|
const kandanUrl = stringValue3(values, "linzumi-url") ?? agentApiUrlToKandanUrl(tokenFile.apiUrl);
|
|
9881
11078
|
const requestedCwdValue = cwdArg ?? stringValue3(values, "cwd");
|
|
9882
11079
|
const requestedCwd = resolveUserPath(requestedCwdValue ?? process.cwd());
|
|
9883
|
-
const
|
|
11080
|
+
const configuredAllowedCwdState = values.has("allowed-cwd") || requestedCwdValue !== undefined ? { allowedCwds: [], missingCwds: [] } : readConfiguredAllowedCwdState();
|
|
11081
|
+
const allowedCwds = values.has("allowed-cwd") ? assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))) : requestedCwdValue === undefined ? configuredAllowedCwdState.allowedCwds : assertConfiguredAllowedCwds([requestedCwd]);
|
|
9884
11082
|
const cwd = allowedCwds[0] ?? requestedCwd;
|
|
9885
11083
|
const codexBin = stringValue3(values, "codex-bin") ?? "codex";
|
|
9886
11084
|
const customCodeServerBin = stringValue3(values, "code-server-bin");
|
|
@@ -9914,12 +11112,14 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
9914
11112
|
fast: values.get("fast") === true,
|
|
9915
11113
|
logFile: stringValue3(values, "log-file"),
|
|
9916
11114
|
allowedCwds,
|
|
11115
|
+
missingConfiguredAllowedCwds: configuredAllowedCwdState.missingCwds,
|
|
9917
11116
|
allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
|
|
9918
11117
|
codeServerBin: editorRuntime.codeServerBin,
|
|
9919
11118
|
editorRuntime: editorRuntime.runtime,
|
|
9920
11119
|
socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
|
|
9921
11120
|
dependencyStatus,
|
|
9922
|
-
|
|
11121
|
+
workspaceSlug: tokenFile.workspaceId,
|
|
11122
|
+
channelSession: channelSlug === undefined || listenUser === undefined ? undefined : {
|
|
9923
11123
|
workspaceSlug: tokenFile.workspaceId,
|
|
9924
11124
|
channelSlug,
|
|
9925
11125
|
kandanThreadId: stringValue3(values, "linzumi-thread-id"),
|
|
@@ -9947,12 +11147,6 @@ function rejectAgentRunnerTargetingFlags(values) {
|
|
|
9947
11147
|
throw new Error(`linzumi commander uses the claimed human Commander token scope; remove ${unsupportedFlags.map((flag) => `--${flag}`).join(", ")}.`);
|
|
9948
11148
|
}
|
|
9949
11149
|
}
|
|
9950
|
-
function requiredStoredAgentChannel(channelId) {
|
|
9951
|
-
if (channelId !== undefined) {
|
|
9952
|
-
return channelId;
|
|
9953
|
-
}
|
|
9954
|
-
throw new Error("agent token file is missing channelId; rerun linzumi claim before starting a Commander");
|
|
9955
|
-
}
|
|
9956
11150
|
function requiredStoredOwnerUsername(ownerUsername) {
|
|
9957
11151
|
if (ownerUsername !== undefined) {
|
|
9958
11152
|
return ownerUsername;
|
|
@@ -10009,7 +11203,7 @@ async function parseRunnerArgs(args, deps = {
|
|
|
10009
11203
|
process.exit(0);
|
|
10010
11204
|
}
|
|
10011
11205
|
if (values.get("version") === true) {
|
|
10012
|
-
process.stdout.write(`linzumi 0.0.
|
|
11206
|
+
process.stdout.write(`linzumi 0.0.39-beta
|
|
10013
11207
|
`);
|
|
10014
11208
|
process.exit(0);
|
|
10015
11209
|
}
|
|
@@ -10017,7 +11211,10 @@ async function parseRunnerArgs(args, deps = {
|
|
|
10017
11211
|
const kandanUrl = required(values, "linzumi-url");
|
|
10018
11212
|
const cwd = stringValue3(values, "cwd") ?? process.cwd();
|
|
10019
11213
|
const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
|
|
10020
|
-
const
|
|
11214
|
+
const configuredAllowedCwdState = values.has("allowed-cwd") ? {
|
|
11215
|
+
allowedCwds: assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))),
|
|
11216
|
+
missingCwds: []
|
|
11217
|
+
} : readConfiguredAllowedCwdState();
|
|
10021
11218
|
const codexBin = stringValue3(values, "codex-bin") ?? "codex";
|
|
10022
11219
|
const customCodeServerBin = stringValue3(values, "code-server-bin");
|
|
10023
11220
|
const explicitToken = stringValue3(values, "token");
|
|
@@ -10057,12 +11254,14 @@ async function parseRunnerArgs(args, deps = {
|
|
|
10057
11254
|
launchTui: values.get("launch-tui") === true,
|
|
10058
11255
|
fast: values.get("fast") === true,
|
|
10059
11256
|
logFile: stringValue3(values, "log-file"),
|
|
10060
|
-
allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...
|
|
11257
|
+
allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...configuredAllowedCwdState.allowedCwds])),
|
|
11258
|
+
missingConfiguredAllowedCwds: configuredAllowedCwdState.missingCwds,
|
|
10061
11259
|
allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
|
|
10062
11260
|
codeServerBin: editorRuntime.codeServerBin,
|
|
10063
11261
|
editorRuntime: editorRuntime.runtime,
|
|
10064
11262
|
socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
|
|
10065
11263
|
dependencyStatus,
|
|
11264
|
+
workspaceSlug: channelSession?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
|
|
10066
11265
|
channelSession
|
|
10067
11266
|
};
|
|
10068
11267
|
}
|