@linzumi/cli 0.0.38-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 +1389 -183
- 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,6 +2547,23 @@ 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
2568
|
if (args.options.enablePortForwardWatch !== true || state.portForwardWatcher !== undefined || state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
1687
2569
|
return;
|
|
@@ -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
|
}
|
|
@@ -1746,7 +2630,11 @@ async function handleChannelSessionControl(args, state, payloadContext, control)
|
|
|
1746
2630
|
return;
|
|
1747
2631
|
}
|
|
1748
2632
|
if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
|
|
1749
|
-
return {
|
|
2633
|
+
return {
|
|
2634
|
+
instanceId: args.instanceId,
|
|
2635
|
+
ok: false,
|
|
2636
|
+
error: "thread_not_bound"
|
|
2637
|
+
};
|
|
1750
2638
|
}
|
|
1751
2639
|
const interrupted = interruptPendingKandanMessages(state.queue, control.throughSeq);
|
|
1752
2640
|
const activeQueuedSeq = interruptibleQueuedSeq(state.turn);
|
|
@@ -1799,7 +2687,11 @@ function updateSessionSettings(args, state, control) {
|
|
|
1799
2687
|
return;
|
|
1800
2688
|
}
|
|
1801
2689
|
if (state.codexThreadId === undefined) {
|
|
1802
|
-
return {
|
|
2690
|
+
return {
|
|
2691
|
+
instanceId: args.instanceId,
|
|
2692
|
+
ok: false,
|
|
2693
|
+
error: "thread_not_bound"
|
|
2694
|
+
};
|
|
1803
2695
|
}
|
|
1804
2696
|
state.runtimeSettings = mergeRuntimeSettings(state.runtimeSettings, control);
|
|
1805
2697
|
publishRuntimeSettings(args, state).catch((error) => {
|
|
@@ -1829,16 +2721,27 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
|
|
|
1829
2721
|
return;
|
|
1830
2722
|
}
|
|
1831
2723
|
if (state.codexThreadId === undefined || state.kandanThreadId === undefined) {
|
|
1832
|
-
return {
|
|
2724
|
+
return {
|
|
2725
|
+
instanceId: args.instanceId,
|
|
2726
|
+
ok: false,
|
|
2727
|
+
error: "thread_not_bound"
|
|
2728
|
+
};
|
|
1833
2729
|
}
|
|
1834
2730
|
const approval = state.pendingApprovalRequests.get(approvalRequestKey(control.requestId, control.sourceSeq));
|
|
1835
2731
|
if (approval === undefined) {
|
|
1836
|
-
return {
|
|
2732
|
+
return {
|
|
2733
|
+
instanceId: args.instanceId,
|
|
2734
|
+
ok: false,
|
|
2735
|
+
error: "approval_request_not_found"
|
|
2736
|
+
};
|
|
1837
2737
|
}
|
|
1838
2738
|
state.pendingApprovalRequests.delete(approvalRequestKey(control.requestId, control.sourceSeq));
|
|
1839
2739
|
const codexDecision = control.decision === "approve" ? "accept" : "decline";
|
|
1840
2740
|
approval.resolve({ decision: codexDecision });
|
|
1841
|
-
state.activeProcessingState = {
|
|
2741
|
+
state.activeProcessingState = {
|
|
2742
|
+
seq: approval.sourceSeq,
|
|
2743
|
+
reason: "streaming response"
|
|
2744
|
+
};
|
|
1842
2745
|
await publishMessageState(args, state.kandanThreadId, approval.sourceSeq, {
|
|
1843
2746
|
status: "processing",
|
|
1844
2747
|
reason: "streaming response"
|
|
@@ -1851,6 +2754,17 @@ async function resolvePendingCodexApprovalRequest(args, state, control) {
|
|
|
1851
2754
|
});
|
|
1852
2755
|
return { instanceId: args.instanceId, ok: true };
|
|
1853
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
|
+
}
|
|
1854
2768
|
async function resolvePendingPortForwardRequest(args, state, payloadContext, control) {
|
|
1855
2769
|
const request = state.pendingPortForwardRequests.get(control.requestId);
|
|
1856
2770
|
if (request === undefined) {
|
|
@@ -1863,7 +2777,11 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
1863
2777
|
actor_user_id: control.actorUserId ?? null,
|
|
1864
2778
|
reason: "sender_not_allowed"
|
|
1865
2779
|
});
|
|
1866
|
-
return {
|
|
2780
|
+
return {
|
|
2781
|
+
instanceId: args.instanceId,
|
|
2782
|
+
ok: false,
|
|
2783
|
+
error: "sender_not_allowed"
|
|
2784
|
+
};
|
|
1867
2785
|
}
|
|
1868
2786
|
state.pendingPortForwardRequests.delete(control.requestId);
|
|
1869
2787
|
if (control.decision === "deny") {
|
|
@@ -1874,14 +2792,18 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
1874
2792
|
port: request.port,
|
|
1875
2793
|
pid: request.pid
|
|
1876
2794
|
});
|
|
2795
|
+
await drainQueuedPortForwardPrompt(args, state, payloadContext);
|
|
1877
2796
|
return { instanceId: args.instanceId, ok: true };
|
|
1878
2797
|
}
|
|
1879
2798
|
state.approvedForwardPorts.add(request.port);
|
|
1880
2799
|
state.approvedForwardTargets.set(request.port, approvedTargetFromRequest(request));
|
|
2800
|
+
const processIdentity = guessCanonicalProcessFromCommand(request.command);
|
|
1881
2801
|
const capabilities = args.options.onForwardPortApproved?.(request.port, {
|
|
1882
2802
|
kandanThreadId: state.kandanThreadId ?? null,
|
|
1883
2803
|
codexThreadId: state.codexThreadId ?? null,
|
|
1884
|
-
channelSlug: args.options.channelSession.channelSlug ?? null
|
|
2804
|
+
channelSlug: args.options.channelSession.channelSlug ?? null,
|
|
2805
|
+
processName: processIdentity?.appName ?? null,
|
|
2806
|
+
processIconKey: processIdentity?.iconKey ?? null
|
|
1885
2807
|
});
|
|
1886
2808
|
await publishForwardPortResolvedEvent(args, request, capabilities);
|
|
1887
2809
|
await publishMessageStateForPortForwardResult(args, state, request, "processed");
|
|
@@ -1891,6 +2813,7 @@ async function resolvePendingPortForwardRequest(args, state, payloadContext, con
|
|
|
1891
2813
|
port: request.port,
|
|
1892
2814
|
pid: request.pid
|
|
1893
2815
|
});
|
|
2816
|
+
await drainQueuedPortForwardPrompt(args, state, payloadContext);
|
|
1894
2817
|
return { instanceId: args.instanceId, ok: true, port: request.port };
|
|
1895
2818
|
}
|
|
1896
2819
|
function portForwardControlSenderAllowed(args, payloadContext, control) {
|
|
@@ -1927,37 +2850,39 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1927
2850
|
state.approvedForwardTargets.set(review.target.port, review.target);
|
|
1928
2851
|
return;
|
|
1929
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
|
+
}
|
|
1930
2862
|
await revokeApprovedForwardPort(args, state, review.revoked, review.reason);
|
|
1931
2863
|
break;
|
|
1932
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
|
+
}
|
|
1933
2874
|
break;
|
|
1934
2875
|
}
|
|
1935
2876
|
const requestId = `port-forward-${randomUUID()}`;
|
|
1936
2877
|
const label = portForwardPromptLabel(candidate);
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
reply_to_seq: state.rootSeq ?? null,
|
|
1941
|
-
structured: {
|
|
1942
|
-
kind: "local_runner_port_forward_request",
|
|
1943
|
-
request_id: requestId,
|
|
2878
|
+
const sourceSeq = portForwardApprovalSourceSeq(state);
|
|
2879
|
+
if (sourceSeq === undefined) {
|
|
2880
|
+
args.log("port_forward.prompt_skipped", {
|
|
1944
2881
|
port: candidate.port,
|
|
1945
2882
|
pid: candidate.pid,
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
}
|
|
1950
|
-
};
|
|
1951
|
-
const reply = await pushOk(args.kandan, args.topic, "session:post_thread_message", {
|
|
1952
|
-
workspace: args.options.channelSession.workspaceSlug,
|
|
1953
|
-
channel: args.options.channelSession.channelSlug,
|
|
1954
|
-
thread_id: state.kandanThreadId,
|
|
1955
|
-
body,
|
|
1956
|
-
payload
|
|
1957
|
-
});
|
|
1958
|
-
const sourceSeq = integerValue(reply.seq);
|
|
1959
|
-
if (sourceSeq === undefined) {
|
|
1960
|
-
throw new Error("port forward prompt did not return a Kandan message seq");
|
|
2883
|
+
reason: "source_seq_missing"
|
|
2884
|
+
});
|
|
2885
|
+
return;
|
|
1961
2886
|
}
|
|
1962
2887
|
const request = pendingRequestFromCandidate({
|
|
1963
2888
|
requestId,
|
|
@@ -1965,6 +2890,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1965
2890
|
candidate
|
|
1966
2891
|
});
|
|
1967
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;
|
|
1968
2896
|
await publishForwardPortRequestedEvent(args, request);
|
|
1969
2897
|
await publishMessageState(args, state.kandanThreadId, sourceSeq, {
|
|
1970
2898
|
status: "processing",
|
|
@@ -1972,13 +2900,20 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1972
2900
|
approval: {
|
|
1973
2901
|
requestId,
|
|
1974
2902
|
kind: "local_runner_port_forward",
|
|
1975
|
-
summary: `
|
|
2903
|
+
summary: `Make ${processName} on port ${candidate.port} accessible on Linzumi?`,
|
|
1976
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 },
|
|
1977
2912
|
choices: [
|
|
1978
2913
|
{
|
|
1979
2914
|
decision: "approve",
|
|
1980
|
-
label: "
|
|
1981
|
-
description: `Allow
|
|
2915
|
+
label: "Enable",
|
|
2916
|
+
description: `Allow Linzumi to proxy HTTP, HTTPS, and WebSocket traffic for runner port ${candidate.port}.`
|
|
1982
2917
|
},
|
|
1983
2918
|
{
|
|
1984
2919
|
decision: "deny",
|
|
@@ -1996,6 +2931,9 @@ async function publishPortForwardPrompt(args, state, payloadContext, candidate)
|
|
|
1996
2931
|
pid: candidate.pid
|
|
1997
2932
|
});
|
|
1998
2933
|
}
|
|
2934
|
+
function portForwardApprovalSourceSeq(state) {
|
|
2935
|
+
return state.activeProcessingState?.seq ?? state.rootSeq;
|
|
2936
|
+
}
|
|
1999
2937
|
async function revokeApprovedForwardPort(args, state, target, reason) {
|
|
2000
2938
|
state.approvedForwardPorts.delete(target.port);
|
|
2001
2939
|
state.approvedForwardTargets.delete(target.port);
|
|
@@ -2015,30 +2953,58 @@ async function revokeApprovedForwardPort(args, state, target, reason) {
|
|
|
2015
2953
|
reason
|
|
2016
2954
|
});
|
|
2017
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
|
+
}
|
|
2018
2977
|
async function publishPortForwardReadyMessage(args, state, payloadContext, request) {
|
|
2019
2978
|
if (state.kandanThreadId === undefined || state.codexThreadId === undefined) {
|
|
2020
2979
|
return;
|
|
2021
2980
|
}
|
|
2022
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.`;
|
|
2023
2986
|
await pushOptional(args.kandan, args.topic, "session:post_thread_message", {
|
|
2024
2987
|
workspace: args.options.channelSession.workspaceSlug,
|
|
2025
2988
|
channel: args.options.channelSession.channelSlug,
|
|
2026
2989
|
thread_id: state.kandanThreadId,
|
|
2027
|
-
body:
|
|
2990
|
+
body: `${readySummary} [Open](${path})`,
|
|
2028
2991
|
payload: {
|
|
2029
2992
|
...localRunnerPayload(args.options, args.instanceId, "port_forward_ready", state.codexThreadId, payloadContext),
|
|
2030
2993
|
...state.rootSeq === undefined ? {} : { reply_to_seq: state.rootSeq },
|
|
2031
2994
|
structured: {
|
|
2032
2995
|
kind: "local_runner_port_forward_ready",
|
|
2033
2996
|
status: "ready",
|
|
2034
|
-
summary:
|
|
2997
|
+
summary: readySummary,
|
|
2035
2998
|
next_action: "Open HTTP/HTTPS/WebSocket preview",
|
|
2036
2999
|
source_path: path,
|
|
2037
|
-
link_label: "Open
|
|
3000
|
+
link_label: "Open",
|
|
2038
3001
|
request_id: request.requestId,
|
|
2039
3002
|
port: request.port,
|
|
2040
3003
|
pid: request.pid,
|
|
2041
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 },
|
|
2042
3008
|
...request.cwd === undefined ? {} : { cwd: request.cwd },
|
|
2043
3009
|
url: path
|
|
2044
3010
|
}
|
|
@@ -2067,10 +3033,15 @@ async function publishForwardPortResolvedEvent(args, request, capabilities) {
|
|
|
2067
3033
|
...capabilities === undefined ? {} : { capabilities }
|
|
2068
3034
|
}, args.log);
|
|
2069
3035
|
}
|
|
2070
|
-
async function publishMessageStateForPortForwardResult(args, state, request, status) {
|
|
3036
|
+
async function publishMessageStateForPortForwardResult(args, state, request, status, failedReason = "port_forward_denied") {
|
|
2071
3037
|
if (state.kandanThreadId === undefined) {
|
|
2072
3038
|
return;
|
|
2073
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
|
+
}
|
|
2074
3045
|
switch (status) {
|
|
2075
3046
|
case "processed":
|
|
2076
3047
|
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
|
|
@@ -2080,7 +3051,7 @@ async function publishMessageStateForPortForwardResult(args, state, request, sta
|
|
|
2080
3051
|
case "failed":
|
|
2081
3052
|
await publishMessageState(args, state.kandanThreadId, request.sourceSeq, {
|
|
2082
3053
|
status: "failed",
|
|
2083
|
-
reason:
|
|
3054
|
+
reason: failedReason
|
|
2084
3055
|
});
|
|
2085
3056
|
break;
|
|
2086
3057
|
}
|
|
@@ -2151,7 +3122,12 @@ async function handleKandanChatEvent(args, state, runnerIdentity, payloadContext
|
|
|
2151
3122
|
actorUserId: event.actorUserId,
|
|
2152
3123
|
actorSlug: event.actorSlug
|
|
2153
3124
|
});
|
|
2154
|
-
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) {
|
|
2155
3131
|
await publishKandanMessageState(args, event, { status: "processed" });
|
|
2156
3132
|
} else {
|
|
2157
3133
|
await publishKandanMessageState(args, event, {
|
|
@@ -2305,7 +3281,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2305
3281
|
if (next === undefined) {
|
|
2306
3282
|
return;
|
|
2307
3283
|
}
|
|
2308
|
-
state.turn = {
|
|
3284
|
+
state.turn = {
|
|
3285
|
+
status: "starting",
|
|
3286
|
+
queuedSeq: next.seq,
|
|
3287
|
+
interruptAfterStart: false
|
|
3288
|
+
};
|
|
2309
3289
|
state.activeProcessingState = { seq: next.seq, reason: "starting turn" };
|
|
2310
3290
|
args.log("codex.turn_starting", {
|
|
2311
3291
|
queued_seq: next.seq,
|
|
@@ -2328,10 +3308,11 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2328
3308
|
}
|
|
2329
3309
|
const started = await args.codex.request("turn/start", {
|
|
2330
3310
|
threadId: codexThreadId,
|
|
2331
|
-
input: await codexInputItemsForQueuedKandanMessage(args, next),
|
|
3311
|
+
input: await codexInputItemsForQueuedKandanMessage(args, next, state.pendingReconnectContextInjection),
|
|
2332
3312
|
...codexTurnRuntimeOverrides(runtimeOptionsForSettings(args.options, state.runtimeSettings))
|
|
2333
3313
|
});
|
|
2334
3314
|
const turnId = extractTurnIdFromResponse(started);
|
|
3315
|
+
state.pendingReconnectContextInjection = undefined;
|
|
2335
3316
|
const interruptAfterStart = state.turn.status === "starting" && state.turn.interruptAfterStart;
|
|
2336
3317
|
state.turn = { status: "active", turnId, queuedSeq: next.seq };
|
|
2337
3318
|
rememberTurnReplyTarget(state, turnId, next.seq);
|
|
@@ -2357,12 +3338,14 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2357
3338
|
await stopCodexTyping(args, state);
|
|
2358
3339
|
const newCodexThreadId = await startCodexThread(args.codex, args.options);
|
|
2359
3340
|
state.codexThreadId = newCodexThreadId;
|
|
3341
|
+
await bindCurrentCodexThread(args, state);
|
|
2360
3342
|
args.log("codex.thread_rebound", {
|
|
2361
3343
|
kandan_thread_id: state.kandanThreadId,
|
|
2362
3344
|
old_codex_thread_id: oldCodexThreadId ?? null,
|
|
2363
3345
|
new_codex_thread_id: newCodexThreadId
|
|
2364
3346
|
});
|
|
2365
3347
|
await postCodexThreadReboundMessage(args, state, payloadContext, oldCodexThreadId, newCodexThreadId);
|
|
3348
|
+
state.pendingReconnectContextInjection = await fetchReconnectContextInjection(args, state);
|
|
2366
3349
|
requeuePendingKandanMessageFront(state.queue, next);
|
|
2367
3350
|
state.turn = { status: "idle" };
|
|
2368
3351
|
await drainKandanMessageQueue(args, state, payloadContext);
|
|
@@ -2388,6 +3371,26 @@ async function drainKandanMessageQueue(args, state, payloadContext) {
|
|
|
2388
3371
|
});
|
|
2389
3372
|
}
|
|
2390
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
|
+
}
|
|
2391
3394
|
async function handleCodexServerRequest(args, state, payloadContext, request) {
|
|
2392
3395
|
const params = objectValue(request.params) ?? {};
|
|
2393
3396
|
const turnId = codexNotificationTurnId(params);
|
|
@@ -2425,35 +3428,48 @@ function codexApprovalRequestCanSurface(method) {
|
|
|
2425
3428
|
return method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval";
|
|
2426
3429
|
}
|
|
2427
3430
|
async function requestKandanApproval(args, state, request, turnId, payloadContext) {
|
|
2428
|
-
const
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
}, undefined, undefined, state.codexThreadId);
|
|
2441
|
-
args.log("codex.approval_request_pending", {
|
|
2442
|
-
request_id: approval.requestId,
|
|
2443
|
-
source_seq: sourceSeq,
|
|
2444
|
-
turn_id: turnId,
|
|
2445
|
-
method: request.method
|
|
2446
|
-
});
|
|
2447
|
-
return new Promise((resolve2, reject) => {
|
|
2448
|
-
const request2 = {
|
|
2449
|
-
requestId: approval.requestId,
|
|
2450
|
-
sourceSeq,
|
|
2451
|
-
turnId,
|
|
2452
|
-
resolve: resolve2,
|
|
2453
|
-
reject
|
|
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
|
|
2454
3443
|
};
|
|
2455
|
-
|
|
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;
|
|
3466
|
+
});
|
|
3467
|
+
state.approvalPromptChain = approvalResult.then(() => {
|
|
3468
|
+
return;
|
|
3469
|
+
}, () => {
|
|
3470
|
+
return;
|
|
2456
3471
|
});
|
|
3472
|
+
return approvalResult;
|
|
2457
3473
|
}
|
|
2458
3474
|
function rejectPendingApprovalRequests(state, error) {
|
|
2459
3475
|
const pendingApprovals = [...state.pendingApprovalRequests.values()];
|
|
@@ -2479,7 +3495,11 @@ async function forwardCompletedCodexTurn(args, state, turnId, payloadContext) {
|
|
|
2479
3495
|
const completingActiveTurn = completingQueuedSeq !== undefined;
|
|
2480
3496
|
const completingLocalTuiTurn = isLocalTuiTurn(state, turnId);
|
|
2481
3497
|
if (completingQueuedSeq !== undefined) {
|
|
2482
|
-
state.turn = {
|
|
3498
|
+
state.turn = {
|
|
3499
|
+
status: "completing",
|
|
3500
|
+
turnId,
|
|
3501
|
+
queuedSeq: completingQueuedSeq
|
|
3502
|
+
};
|
|
2483
3503
|
}
|
|
2484
3504
|
await waitForPendingTuiInputMirror(state, turnId);
|
|
2485
3505
|
await waitForStreamingForwardChains(args, state, payloadContext);
|
|
@@ -2841,7 +3861,10 @@ async function forwardReasoningDeltaPayload(args, state, delta, payloadContext)
|
|
|
2841
3861
|
}
|
|
2842
3862
|
} else {
|
|
2843
3863
|
await editCodexStructuredOutput(args, state, existing.seq, nextContent, codexReasoningStructuredMessage(delta.itemKey, nextContent, "streaming"));
|
|
2844
|
-
rememberStreamingReasoningOutput(state, {
|
|
3864
|
+
rememberStreamingReasoningOutput(state, {
|
|
3865
|
+
...existing,
|
|
3866
|
+
content: nextContent
|
|
3867
|
+
});
|
|
2845
3868
|
}
|
|
2846
3869
|
args.log("kandan.codex_reasoning_delta_forwarded", {
|
|
2847
3870
|
item_key: delta.itemKey,
|
|
@@ -3398,7 +4421,10 @@ function rememberLocalTuiTurnIfNeeded(args, state, threadId, turnId) {
|
|
|
3398
4421
|
if (state.kandanThreadId !== undefined) {
|
|
3399
4422
|
startCodexTypingHeartbeat(args, state, state.kandanThreadId);
|
|
3400
4423
|
}
|
|
3401
|
-
args.log("codex.tui_turn_started", {
|
|
4424
|
+
args.log("codex.tui_turn_started", {
|
|
4425
|
+
turn_id: turnId,
|
|
4426
|
+
codex_thread_id: threadId
|
|
4427
|
+
});
|
|
3402
4428
|
}
|
|
3403
4429
|
function isLocalTuiTurn(state, turnId) {
|
|
3404
4430
|
return state.localTuiTurnIds.has(turnId);
|
|
@@ -3465,7 +4491,10 @@ function clearPendingStreamFlushTimers(state) {
|
|
|
3465
4491
|
clearStreamDeltaFlushTimer(state.fileChangeQueue);
|
|
3466
4492
|
}
|
|
3467
4493
|
function rememberTurnReplyTarget(state, turnId, replyToSeq) {
|
|
3468
|
-
rememberBoundedCacheValue(state.turnReplyTargets, turnId, {
|
|
4494
|
+
rememberBoundedCacheValue(state.turnReplyTargets, turnId, {
|
|
4495
|
+
turnId,
|
|
4496
|
+
replyToSeq
|
|
4497
|
+
});
|
|
3469
4498
|
}
|
|
3470
4499
|
function sourceMessageSeqForTurn(state, turnId) {
|
|
3471
4500
|
return getBoundedCacheValue(state.turnReplyTargets, turnId)?.replyToSeq;
|
|
@@ -3474,20 +4503,12 @@ function fileChangePaths(structured) {
|
|
|
3474
4503
|
const changes = arrayValue(structured.changes) ?? [];
|
|
3475
4504
|
return changes.filter(isJsonObject).map((change) => stringValue(change.path) ?? "").filter((path) => path.trim() !== "");
|
|
3476
4505
|
}
|
|
3477
|
-
async function postCodexThreadReboundMessage(args, state, payloadContext,
|
|
4506
|
+
async function postCodexThreadReboundMessage(args, state, payloadContext, _oldCodexThreadId, newCodexThreadId) {
|
|
3478
4507
|
if (state.kandanThreadId === undefined) {
|
|
3479
4508
|
return;
|
|
3480
4509
|
}
|
|
3481
4510
|
const session = args.options.channelSession;
|
|
3482
|
-
const body = [
|
|
3483
|
-
"Codex reconnected.",
|
|
3484
|
-
"",
|
|
3485
|
-
"The previous local Codex app-server thread was not available in this process, so this runner started a new local Codex thread for this Kandan thread.",
|
|
3486
|
-
"",
|
|
3487
|
-
`Previous Codex thread: ${oldCodexThreadId ?? "unknown"}`,
|
|
3488
|
-
`New Codex thread: ${newCodexThreadId}`
|
|
3489
|
-
].join(`
|
|
3490
|
-
`);
|
|
4511
|
+
const body = "[codex reconnected to new thread]";
|
|
3491
4512
|
await pushOk(args.kandan, args.topic, "session:post_thread_message", {
|
|
3492
4513
|
workspace: session.workspaceSlug,
|
|
3493
4514
|
channel: session.channelSlug,
|
|
@@ -3622,10 +4643,19 @@ async function publishMessageState(args, threadId, seq, state, actorSlug, actorU
|
|
|
3622
4643
|
approval_request_id: state.approval.requestId,
|
|
3623
4644
|
approval_kind: state.approval.kind,
|
|
3624
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 },
|
|
3625
4653
|
...state.approval.reason === undefined ? {} : { approval_reason: state.approval.reason },
|
|
3626
4654
|
...state.approval.choices === undefined ? {} : { approval_choices: state.approval.choices },
|
|
3627
4655
|
...state.approval.allowedActorSlug === undefined ? {} : { approval_allowed_actor_slug: state.approval.allowedActorSlug },
|
|
3628
|
-
...state.approval.allowedActorUserId === undefined ? {} : {
|
|
4656
|
+
...state.approval.allowedActorUserId === undefined ? {} : {
|
|
4657
|
+
approval_allowed_actor_user_id: state.approval.allowedActorUserId
|
|
4658
|
+
}
|
|
3629
4659
|
} : {},
|
|
3630
4660
|
...actorSlug === undefined ? {} : { actor_slug: actorSlug },
|
|
3631
4661
|
...actorUserId === undefined ? {} : { actor_user_id: actorUserId }
|
|
@@ -3679,11 +4709,15 @@ function clearActiveProcessingState(state, seq) {
|
|
|
3679
4709
|
state.activeProcessingState = undefined;
|
|
3680
4710
|
}
|
|
3681
4711
|
}
|
|
3682
|
-
async function codexInputItemsForQueuedKandanMessage(args, message) {
|
|
4712
|
+
async function codexInputItemsForQueuedKandanMessage(args, message, reconnectContextInjection = undefined) {
|
|
3683
4713
|
const attachments = await downloadQueuedKandanAttachments(args, message);
|
|
3684
4714
|
const text = appendDownloadedAttachmentContext(codexInputForQueuedKandanMessage(message), attachments);
|
|
3685
4715
|
const imageItems = attachments.flatMap((attachment) => attachment.isImage ? [{ type: "localImage", path: attachment.path }] : []);
|
|
3686
|
-
return [
|
|
4716
|
+
return [
|
|
4717
|
+
...reconnectContextInjection === undefined ? [] : [reconnectContextInjection],
|
|
4718
|
+
{ type: "text", text },
|
|
4719
|
+
...imageItems
|
|
4720
|
+
];
|
|
3687
4721
|
}
|
|
3688
4722
|
async function downloadQueuedKandanAttachments(args, message) {
|
|
3689
4723
|
if (message.attachments.length === 0) {
|
|
@@ -3816,10 +4850,11 @@ async function uploadedFileIdsForCodexOutput(args, body, structured) {
|
|
|
3816
4850
|
throw new Error("Kandan attachment prepare response missing upload_url");
|
|
3817
4851
|
}
|
|
3818
4852
|
const bytes = await readFile(file.path);
|
|
4853
|
+
const uploadBody = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
3819
4854
|
const response = await fetch(resolveKandanAttachmentUrl(args.options.kandanUrl, uploadUrl), {
|
|
3820
4855
|
method: uploadMethod,
|
|
3821
4856
|
headers: { "content-type": file.contentType },
|
|
3822
|
-
body:
|
|
4857
|
+
body: uploadBody
|
|
3823
4858
|
});
|
|
3824
4859
|
if (!response.ok) {
|
|
3825
4860
|
throw new Error(`Kandan attachment upload failed for ${file.fileName}: ${response.status} ${response.statusText}`);
|
|
@@ -4059,7 +5094,7 @@ function defaultCliAuditLogFile() {
|
|
|
4059
5094
|
return override === undefined || override === "" ? join2(homedir(), ".linzumi", "logs", "command-events.jsonl") : override;
|
|
4060
5095
|
}
|
|
4061
5096
|
function redactForCliLog(value) {
|
|
4062
|
-
return
|
|
5097
|
+
return redactObject(value);
|
|
4063
5098
|
}
|
|
4064
5099
|
function redactValue(value, key) {
|
|
4065
5100
|
if (sensitiveKey(key)) {
|
|
@@ -4204,8 +5239,10 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
|
|
|
4204
5239
|
const child = spawn(codexBin, args, {
|
|
4205
5240
|
cwd,
|
|
4206
5241
|
env: process.env,
|
|
4207
|
-
stdio: ["ignore", "inherit", "inherit"]
|
|
5242
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
5243
|
+
detached: true
|
|
4208
5244
|
});
|
|
5245
|
+
const stop = () => stopCodexAppServerProcess(child);
|
|
4209
5246
|
writeCliAuditEvent("process.spawned", {
|
|
4210
5247
|
command: codexBin,
|
|
4211
5248
|
args,
|
|
@@ -4231,33 +5268,57 @@ async function startCodexAppServer(codexBin, cwd, options = {}) {
|
|
|
4231
5268
|
try {
|
|
4232
5269
|
await waitForReadyz(url, child);
|
|
4233
5270
|
} catch (error) {
|
|
4234
|
-
|
|
5271
|
+
stop();
|
|
4235
5272
|
throw error;
|
|
4236
5273
|
}
|
|
4237
|
-
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;
|
|
4238
5310
|
}
|
|
4239
5311
|
function codexAppServerArgs(listenUrl, options = {}) {
|
|
4240
|
-
return [
|
|
4241
|
-
"app-server",
|
|
4242
|
-
...codexConfigArgs(options),
|
|
4243
|
-
"--listen",
|
|
4244
|
-
listenUrl
|
|
4245
|
-
];
|
|
5312
|
+
return ["app-server", ...codexConfigArgs(options), "--listen", listenUrl];
|
|
4246
5313
|
}
|
|
4247
5314
|
function codexConfigArgs(options) {
|
|
4248
5315
|
return [
|
|
4249
|
-
...options.model === undefined ? [] : [
|
|
4250
|
-
"-c",
|
|
4251
|
-
`model=${JSON.stringify(options.model)}`
|
|
4252
|
-
],
|
|
5316
|
+
...options.model === undefined ? [] : ["-c", `model=${JSON.stringify(options.model)}`],
|
|
4253
5317
|
...options.reasoningEffort === undefined ? [] : [
|
|
4254
5318
|
"-c",
|
|
4255
5319
|
`model_reasoning_effort=${JSON.stringify(options.reasoningEffort)}`
|
|
4256
5320
|
],
|
|
4257
|
-
...options.fast === true ? [
|
|
4258
|
-
"-c",
|
|
4259
|
-
`service_tier=${JSON.stringify("fast")}`
|
|
4260
|
-
] : []
|
|
5321
|
+
...options.fast === true ? ["-c", `service_tier=${JSON.stringify("fast")}`] : []
|
|
4261
5322
|
];
|
|
4262
5323
|
}
|
|
4263
5324
|
async function connectCodexAppServer(websocketUrl, socketFactory = (url) => new WebSocket(url)) {
|
|
@@ -4611,7 +5672,9 @@ async function handleForwardHttpRequest(control, allowedPorts) {
|
|
|
4611
5672
|
method: control.method,
|
|
4612
5673
|
headers: requestHeaders(control.headers),
|
|
4613
5674
|
redirect: "manual",
|
|
4614
|
-
...bodyDecision.body === undefined ? {} : {
|
|
5675
|
+
...bodyDecision.body === undefined ? {} : {
|
|
5676
|
+
body: bodyDecision.body.buffer.slice(bodyDecision.body.byteOffset, bodyDecision.body.byteOffset + bodyDecision.body.byteLength)
|
|
5677
|
+
}
|
|
4615
5678
|
};
|
|
4616
5679
|
const response = await fetchWithHttpsFallback(control.port, control.path, control.queryString, request);
|
|
4617
5680
|
const upstreamBuffer = Buffer.from(await response.arrayBuffer());
|
|
@@ -5081,10 +6144,12 @@ function prepareCodeServerProfile(collaboration, editorRuntime) {
|
|
|
5081
6144
|
const userDataDir = mkdtempSync(join4(tmpdir(), "kandan-local-editor-"));
|
|
5082
6145
|
const extensionsDir = join4(userDataDir, "extensions");
|
|
5083
6146
|
const collaborationServerDir = join4(userDataDir, "collaboration-server");
|
|
6147
|
+
const tempDir = join4(userDataDir, "tmp");
|
|
5084
6148
|
const userSettingsDir = join4(userDataDir, "User");
|
|
5085
6149
|
mkdirSync3(userSettingsDir, { recursive: true });
|
|
5086
6150
|
mkdirSync3(extensionsDir, { recursive: true });
|
|
5087
6151
|
mkdirSync3(collaborationServerDir, { recursive: true });
|
|
6152
|
+
mkdirSync3(tempDir, { recursive: true });
|
|
5088
6153
|
if (editorRuntime !== undefined) {
|
|
5089
6154
|
installDirectory(editorRuntime.assets.documentStateExtensionDir, join4(extensionsDir, "kandan.document-state-telemetry"));
|
|
5090
6155
|
}
|
|
@@ -5117,6 +6182,12 @@ function prepareCodeServerLaunch(options) {
|
|
|
5117
6182
|
"-p",
|
|
5118
6183
|
codeServerSandboxProfile(options, codeServerExecutable.directory),
|
|
5119
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"),
|
|
5120
6191
|
codeServerExecutable.command,
|
|
5121
6192
|
...codeServerArgs(options.port, options.cwd, options.userDataDir, options.extensionsDir)
|
|
5122
6193
|
]
|
|
@@ -5397,10 +6468,14 @@ function installDirectory(sourceDir, destinationDir) {
|
|
|
5397
6468
|
}
|
|
5398
6469
|
function codeServerEnv(env, cwd, userDataDir, collaboration) {
|
|
5399
6470
|
const { PORT: _port, ...hostEnv } = env;
|
|
6471
|
+
const tempDir = join4(userDataDir, "tmp");
|
|
5400
6472
|
const base = {
|
|
5401
6473
|
...hostEnv,
|
|
5402
6474
|
HOME: cwd,
|
|
5403
6475
|
PWD: cwd,
|
|
6476
|
+
TMPDIR: tempDir,
|
|
6477
|
+
TMP: tempDir,
|
|
6478
|
+
TEMP: tempDir,
|
|
5404
6479
|
XDG_CACHE_HOME: join4(userDataDir, "xdg-cache"),
|
|
5405
6480
|
XDG_CONFIG_HOME: join4(userDataDir, "xdg-config"),
|
|
5406
6481
|
XDG_DATA_HOME: join4(userDataDir, "xdg-data")
|
|
@@ -5789,7 +6864,7 @@ async function exchangeCodeForToken(args) {
|
|
|
5789
6864
|
};
|
|
5790
6865
|
}
|
|
5791
6866
|
function stringBodyField(body, key) {
|
|
5792
|
-
const value =
|
|
6867
|
+
const value = body[key];
|
|
5793
6868
|
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
5794
6869
|
}
|
|
5795
6870
|
function startCallbackServer(args) {
|
|
@@ -5859,7 +6934,9 @@ function isTcpAddress(address) {
|
|
|
5859
6934
|
return typeof address === "object" && address !== null && typeof address.port === "number";
|
|
5860
6935
|
}
|
|
5861
6936
|
function writeOauthResult(response, args) {
|
|
5862
|
-
response.writeHead(args.status, {
|
|
6937
|
+
response.writeHead(args.status, {
|
|
6938
|
+
"content-type": "text/html; charset=utf-8"
|
|
6939
|
+
});
|
|
5863
6940
|
response.end(oauthResultHtml(args));
|
|
5864
6941
|
}
|
|
5865
6942
|
function oauthResultHtml(args) {
|
|
@@ -6474,7 +7551,7 @@ function assertStartDependencies(status) {
|
|
|
6474
7551
|
throw new Error(`Codex is not available at ${status.codex.command}. Install Codex or pass --codex-bin <path>.`);
|
|
6475
7552
|
}
|
|
6476
7553
|
if (status.editorRuntime?.status === "unavailable" || status.codeServer?.available === false && status.codeServer.reason !== "not_configured") {
|
|
6477
|
-
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.");
|
|
6478
7555
|
}
|
|
6479
7556
|
}
|
|
6480
7557
|
function dependencyStatusPayload(status) {
|
|
@@ -6858,7 +7935,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6858
7935
|
forwardPortAttributions.set(port, {
|
|
6859
7936
|
kandanThreadId: attribution.kandanThreadId ?? null,
|
|
6860
7937
|
codexThreadId: attribution.codexThreadId ?? null,
|
|
6861
|
-
channelSlug: attribution.channelSlug ?? null
|
|
7938
|
+
channelSlug: attribution.channelSlug ?? null,
|
|
7939
|
+
processName: attribution.processName ?? null,
|
|
7940
|
+
processIconKey: attribution.processIconKey ?? null
|
|
6862
7941
|
});
|
|
6863
7942
|
};
|
|
6864
7943
|
const clearForwardPortAttribution = (port) => {
|
|
@@ -6870,7 +7949,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6870
7949
|
port,
|
|
6871
7950
|
kandanThreadId: attribution?.kandanThreadId ?? null,
|
|
6872
7951
|
codexThreadId: attribution?.codexThreadId ?? null,
|
|
6873
|
-
channelSlug: attribution?.channelSlug ?? null
|
|
7952
|
+
channelSlug: attribution?.channelSlug ?? null,
|
|
7953
|
+
processName: attribution?.processName ?? null,
|
|
7954
|
+
processIconKey: attribution?.processIconKey ?? null
|
|
6874
7955
|
};
|
|
6875
7956
|
});
|
|
6876
7957
|
const allowedCwds = { value: [...options.allowedCwds] };
|
|
@@ -6889,6 +7970,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6889
7970
|
codexRemoteTui: true,
|
|
6890
7971
|
startInstance: allowedCwds.value.length > 0,
|
|
6891
7972
|
allowedCwds: allowedCwds.value,
|
|
7973
|
+
missingConfiguredAllowedCwds: options.missingConfiguredAllowedCwds ?? [],
|
|
6892
7974
|
allowedCwdSuggestions: allowedCwdSuggestions(options.cwd, allowedCwds.value),
|
|
6893
7975
|
portForwarding: liveForwardPorts.size > 0,
|
|
6894
7976
|
allowedPorts: Array.from(liveForwardPorts).sort((left, right) => left - right),
|
|
@@ -6903,7 +7985,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6903
7985
|
const joinPayload = () => ({
|
|
6904
7986
|
clientName: "kandan-local-codex-runner",
|
|
6905
7987
|
version: "0.0.1",
|
|
6906
|
-
workspace: options.channelSession?.workspaceSlug ?? null,
|
|
7988
|
+
workspace: options.channelSession?.workspaceSlug ?? options.workspaceSlug ?? null,
|
|
6907
7989
|
channel: options.channelSession?.channelSlug ?? null,
|
|
6908
7990
|
capabilities: capabilitiesPayload()
|
|
6909
7991
|
});
|
|
@@ -6921,7 +8003,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6921
8003
|
const started = options.codexUrl === undefined ? await startOwnedCodexAppServer(options) : undefined;
|
|
6922
8004
|
if (started !== undefined) {
|
|
6923
8005
|
cleanup.actions.push(() => {
|
|
6924
|
-
started.
|
|
8006
|
+
started.stop();
|
|
6925
8007
|
});
|
|
6926
8008
|
}
|
|
6927
8009
|
const codexUrl = options.codexUrl ?? started?.url;
|
|
@@ -6971,6 +8053,7 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
6971
8053
|
cleanup.actions.push(() => codex.close());
|
|
6972
8054
|
const seq = { value: 0 };
|
|
6973
8055
|
const codexThreads = options.channelSession === undefined ? await discoverCodexThreads(codex, options.cwd) : [];
|
|
8056
|
+
const discoveredCodexThreads = { value: codexThreads };
|
|
6974
8057
|
const runnerHost = hostname2();
|
|
6975
8058
|
const instancePayload = {
|
|
6976
8059
|
instanceId,
|
|
@@ -7019,17 +8102,22 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7019
8102
|
kandan.onReconnect(() => channelSession.handleKandanReconnect());
|
|
7020
8103
|
}
|
|
7021
8104
|
const dynamicChannelSessions = new Map;
|
|
8105
|
+
kandan.onReconnect(() => rebindDynamicChannelSessionsOnReconnect(dynamicChannelSessions.values()));
|
|
7022
8106
|
cleanup.actions.push(async () => {
|
|
7023
8107
|
await Promise.all(Array.from(dynamicChannelSessions.values(), (session) => session.close()));
|
|
7024
8108
|
dynamicChannelSessions.clear();
|
|
7025
8109
|
});
|
|
7026
|
-
const
|
|
8110
|
+
const attachThreadSession = async (control, cwd, codexThreadId) => {
|
|
7027
8111
|
const workspaceSlug = normalizedWorkDescription(control.workspace);
|
|
7028
8112
|
const channelSlug = normalizedWorkDescription(control.channel);
|
|
7029
8113
|
const kandanThreadId = normalizedWorkDescription(control.threadId);
|
|
7030
|
-
if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined
|
|
8114
|
+
if (workspaceSlug === undefined || channelSlug === undefined || kandanThreadId === undefined) {
|
|
7031
8115
|
return;
|
|
7032
8116
|
}
|
|
8117
|
+
const existingSession = dynamicChannelSessions.get(kandanThreadId);
|
|
8118
|
+
if (existingSession !== undefined) {
|
|
8119
|
+
return existingSession;
|
|
8120
|
+
}
|
|
7033
8121
|
const listenUser = options.channelSession?.listenUser ?? identityFromAccessToken(options.token).actorUsername;
|
|
7034
8122
|
if (listenUser === undefined) {
|
|
7035
8123
|
throw new Error("missing listen user for Commander-started Codex session");
|
|
@@ -7083,10 +8171,11 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7083
8171
|
codexUrl,
|
|
7084
8172
|
cwd: options.cwd,
|
|
7085
8173
|
hostname: runnerHost,
|
|
7086
|
-
workspace: options.channelSession?.workspaceSlug ?? null,
|
|
8174
|
+
workspace: options.channelSession?.workspaceSlug ?? options.workspaceSlug ?? null,
|
|
7087
8175
|
channel: options.channelSession?.channelSlug ?? null,
|
|
7088
8176
|
threadId: channelSession?.currentKandanThreadId() ?? null,
|
|
7089
8177
|
codexThreadId: channelSession?.currentCodexThreadId() ?? null,
|
|
8178
|
+
codexThreads: discoveredCodexThreads.value,
|
|
7090
8179
|
model: options.channelSession?.model ?? null,
|
|
7091
8180
|
reasoningEffort: options.channelSession?.reasoningEffort ?? null,
|
|
7092
8181
|
fast: options.fast ?? false,
|
|
@@ -7097,11 +8186,19 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7097
8186
|
message: error instanceof Error ? error.message : String(error)
|
|
7098
8187
|
});
|
|
7099
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
|
+
});
|
|
7100
8197
|
const heartbeatInterval = setInterval(() => {
|
|
7101
|
-
pushHeartbeat();
|
|
8198
|
+
channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat();
|
|
7102
8199
|
}, 15000);
|
|
7103
8200
|
cleanup.actions.push(() => clearInterval(heartbeatInterval));
|
|
7104
|
-
kandan.onReconnect(() => pushHeartbeat().then(() => {
|
|
8201
|
+
kandan.onReconnect(() => (channelSession === undefined ? refreshDiscoveredCodexThreads() : pushHeartbeat()).then(() => {
|
|
7105
8202
|
return;
|
|
7106
8203
|
}));
|
|
7107
8204
|
pushHeartbeat();
|
|
@@ -7135,6 +8232,9 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7135
8232
|
});
|
|
7136
8233
|
});
|
|
7137
8234
|
}
|
|
8235
|
+
if (channelSession === undefined && notification.method === "thread/started") {
|
|
8236
|
+
refreshDiscoveredCodexThreads();
|
|
8237
|
+
}
|
|
7138
8238
|
log("codex.notification", {
|
|
7139
8239
|
method: notification.method,
|
|
7140
8240
|
metadata
|
|
@@ -7246,11 +8346,35 @@ async function openLocalCodexRunner(options, log, cleanup, close) {
|
|
|
7246
8346
|
pushHeartbeat();
|
|
7247
8347
|
return;
|
|
7248
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
|
+
}
|
|
7249
8373
|
resolveSessionControl(channelSession, dynamicChannelSessions, control).then((handled) => {
|
|
7250
8374
|
if (handled !== undefined) {
|
|
7251
8375
|
return handled;
|
|
7252
8376
|
}
|
|
7253
|
-
return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control,
|
|
8377
|
+
return applyControl(codex, kandan, topic, instanceId, options, allowedCwds.value, control, attachThreadSession);
|
|
7254
8378
|
}).then((response) => {
|
|
7255
8379
|
return kandan.push(topic, "codex_response", response);
|
|
7256
8380
|
}).catch((error) => {
|
|
@@ -7315,6 +8439,8 @@ async function discoverCodexThreads(codex, cwd) {
|
|
|
7315
8439
|
const data = arrayValue(result?.data);
|
|
7316
8440
|
return data === undefined ? [] : data.filter(isJsonObject).map((thread) => ({
|
|
7317
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) ?? "",
|
|
7318
8444
|
preview: stringValue(thread.preview) ?? "",
|
|
7319
8445
|
cwd: stringValue(thread.cwd) ?? "",
|
|
7320
8446
|
source: stringValue(thread.source) ?? "",
|
|
@@ -7390,6 +8516,9 @@ function launchCodexTui(codexBin, codexUrl, cwd, codexThreadId, session, fast) {
|
|
|
7390
8516
|
});
|
|
7391
8517
|
return child;
|
|
7392
8518
|
}
|
|
8519
|
+
async function rebindDynamicChannelSessionsOnReconnect(sessions) {
|
|
8520
|
+
await Promise.all(Array.from(sessions, (session) => session.handleKandanReconnect()));
|
|
8521
|
+
}
|
|
7393
8522
|
function forwardedHeaderValue(headers, name) {
|
|
7394
8523
|
if (!Array.isArray(headers)) {
|
|
7395
8524
|
return;
|
|
@@ -7470,10 +8599,11 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
|
|
|
7470
8599
|
const startedThreadSession = codexThreadId !== undefined && onStartedThread !== undefined ? await onStartedThread(control, cwd.cwd, codexThreadId) : undefined;
|
|
7471
8600
|
if (codexThreadId !== undefined && workDescription !== undefined) {
|
|
7472
8601
|
const rootSeq = integerValue(control.rootSeq);
|
|
7473
|
-
|
|
8602
|
+
const sourceSeq = integerValue(control.sourceSeq) ?? rootSeq;
|
|
8603
|
+
if (startedThreadSession !== undefined && sourceSeq !== undefined) {
|
|
7474
8604
|
const identity = identityFromAccessToken(options.token);
|
|
7475
8605
|
await startedThreadSession.startThreadMessageTurn({
|
|
7476
|
-
seq:
|
|
8606
|
+
seq: sourceSeq,
|
|
7477
8607
|
body: workDescription,
|
|
7478
8608
|
actorSlug: identity.actorUsername,
|
|
7479
8609
|
actorUserId: identity.actorUserId
|
|
@@ -7493,6 +8623,52 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
|
|
|
7493
8623
|
response
|
|
7494
8624
|
};
|
|
7495
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
|
+
}
|
|
7496
8672
|
case "start_turn": {
|
|
7497
8673
|
const response = await codex.request("turn/start", {
|
|
7498
8674
|
threadId: control.threadId,
|
|
@@ -7543,6 +8719,8 @@ async function applyControl(codex, kandan, topic, instanceId, options, allowedCw
|
|
|
7543
8719
|
case "interrupt_queued_messages":
|
|
7544
8720
|
case "resolve_codex_approval_request":
|
|
7545
8721
|
case "resolve_port_forward_request":
|
|
8722
|
+
case "update_session_settings":
|
|
8723
|
+
case "set_port_forward_enabled":
|
|
7546
8724
|
case "forward_http_request":
|
|
7547
8725
|
case "forward_websocket_open":
|
|
7548
8726
|
case "forward_websocket_send":
|
|
@@ -7653,6 +8831,9 @@ async function startOwnedCodexAppServer(options) {
|
|
|
7653
8831
|
function isUpdateRunnerConfigControl(control) {
|
|
7654
8832
|
return control.type === "update_runner_config";
|
|
7655
8833
|
}
|
|
8834
|
+
function isSetPortForwardEnabledControl(control) {
|
|
8835
|
+
return control.type === "set_port_forward_enabled" && Number.isInteger(control.port) && control.port > 0 && control.port <= 65535;
|
|
8836
|
+
}
|
|
7656
8837
|
function normalizeAllowedCwds(values) {
|
|
7657
8838
|
return Array.from(new Set(values.flatMap((value) => {
|
|
7658
8839
|
const normalized = value.trim();
|
|
@@ -7803,7 +8984,13 @@ async function acquireAndCacheToken(args) {
|
|
|
7803
8984
|
}
|
|
7804
8985
|
|
|
7805
8986
|
// src/localConfig.ts
|
|
7806
|
-
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";
|
|
7807
8994
|
import { homedir as homedir6 } from "node:os";
|
|
7808
8995
|
import { dirname as dirname5, resolve as resolve5 } from "node:path";
|
|
7809
8996
|
function localConfigPath(env = process.env) {
|
|
@@ -7823,14 +9010,22 @@ function readLocalConfig(path = localConfigPath()) {
|
|
|
7823
9010
|
allowedCwds: uniqueStrings(parsed.allowedCwds)
|
|
7824
9011
|
};
|
|
7825
9012
|
}
|
|
7826
|
-
function
|
|
7827
|
-
|
|
9013
|
+
function readConfiguredAllowedCwdState(path = localConfigPath()) {
|
|
9014
|
+
const allowedCwds = [];
|
|
9015
|
+
const missingCwds = [];
|
|
9016
|
+
for (const cwd of readLocalConfig(path).allowedCwds) {
|
|
7828
9017
|
try {
|
|
7829
|
-
|
|
9018
|
+
const absolutePath = resolve5(expandUserPath(cwd));
|
|
9019
|
+
const realPath = realpathSync4(absolutePath);
|
|
9020
|
+
allowedCwds.push(...realPath === absolutePath ? [realPath] : [realPath, absolutePath]);
|
|
7830
9021
|
} catch (_error) {
|
|
7831
|
-
|
|
9022
|
+
missingCwds.push(cwd);
|
|
7832
9023
|
}
|
|
7833
|
-
}
|
|
9024
|
+
}
|
|
9025
|
+
return {
|
|
9026
|
+
allowedCwds: uniqueStrings(allowedCwds),
|
|
9027
|
+
missingCwds: uniqueStrings(missingCwds)
|
|
9028
|
+
};
|
|
7834
9029
|
}
|
|
7835
9030
|
function addAllowedCwd(pathValue, path = localConfigPath()) {
|
|
7836
9031
|
const normalizedPath = realpathSync4(resolve5(expandUserPath(pathValue)));
|
|
@@ -7859,7 +9054,9 @@ function isConfigPayload(value) {
|
|
|
7859
9054
|
return typeof value === "object" && value !== null && value.version === 1 && Array.isArray(value.allowedCwds) && value.allowedCwds.every((cwd) => typeof cwd === "string" && cwd.trim() !== "");
|
|
7860
9055
|
}
|
|
7861
9056
|
function uniqueStrings(values) {
|
|
7862
|
-
return [
|
|
9057
|
+
return [
|
|
9058
|
+
...new Set(values.map((value) => value.trim()).filter((value) => value !== ""))
|
|
9059
|
+
];
|
|
7863
9060
|
}
|
|
7864
9061
|
function realpathOrResolved(pathValue) {
|
|
7865
9062
|
try {
|
|
@@ -9375,7 +10572,9 @@ function readProcessIdentity(pid) {
|
|
|
9375
10572
|
if (match === null) {
|
|
9376
10573
|
return { command: output };
|
|
9377
10574
|
}
|
|
9378
|
-
|
|
10575
|
+
const startedAt = match[1];
|
|
10576
|
+
const processCommand = match[2];
|
|
10577
|
+
return startedAt === undefined || processCommand === undefined ? { command: output } : { startedAt, command: processCommand };
|
|
9379
10578
|
} catch (_error) {
|
|
9380
10579
|
return;
|
|
9381
10580
|
}
|
|
@@ -9488,7 +10687,7 @@ async function main(args) {
|
|
|
9488
10687
|
process.stdout.write(connectGuideText());
|
|
9489
10688
|
return;
|
|
9490
10689
|
case "version":
|
|
9491
|
-
process.stdout.write(`linzumi 0.0.
|
|
10690
|
+
process.stdout.write(`linzumi 0.0.39-beta
|
|
9492
10691
|
`);
|
|
9493
10692
|
return;
|
|
9494
10693
|
case "auth":
|
|
@@ -9582,12 +10781,17 @@ function runHelloCommand(args) {
|
|
|
9582
10781
|
process.stdout.write(helloHelpText());
|
|
9583
10782
|
return;
|
|
9584
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");
|
|
9585
10789
|
const project = createHelloLinzumiProject({
|
|
9586
|
-
rootPath:
|
|
9587
|
-
parentDir
|
|
9588
|
-
name:
|
|
9589
|
-
port:
|
|
9590
|
-
host:
|
|
10790
|
+
...rootPath === undefined ? {} : { rootPath },
|
|
10791
|
+
...parentDir === undefined ? {} : { parentDir: resolveUserPath(parentDir) },
|
|
10792
|
+
...name === undefined ? {} : { name },
|
|
10793
|
+
...port === undefined ? {} : { port },
|
|
10794
|
+
...host === undefined ? {} : { host },
|
|
9591
10795
|
reset: values.get("reset") === true
|
|
9592
10796
|
});
|
|
9593
10797
|
if (values.get("json") === true) {
|
|
@@ -9868,12 +11072,13 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
9868
11072
|
}
|
|
9869
11073
|
const tokenFilePath = stringValue3(values, "agent-token-file") ?? defaultAgentTokenFilePath();
|
|
9870
11074
|
const tokenFile = readStoredAgentTokenFile(tokenFilePath, deps.readTextFile);
|
|
9871
|
-
const channelSlug =
|
|
9872
|
-
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);
|
|
9873
11077
|
const kandanUrl = stringValue3(values, "linzumi-url") ?? agentApiUrlToKandanUrl(tokenFile.apiUrl);
|
|
9874
11078
|
const requestedCwdValue = cwdArg ?? stringValue3(values, "cwd");
|
|
9875
11079
|
const requestedCwd = resolveUserPath(requestedCwdValue ?? process.cwd());
|
|
9876
|
-
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]);
|
|
9877
11082
|
const cwd = allowedCwds[0] ?? requestedCwd;
|
|
9878
11083
|
const codexBin = stringValue3(values, "codex-bin") ?? "codex";
|
|
9879
11084
|
const customCodeServerBin = stringValue3(values, "code-server-bin");
|
|
@@ -9907,12 +11112,14 @@ async function parseAgentRunnerArgs(args, deps = {
|
|
|
9907
11112
|
fast: values.get("fast") === true,
|
|
9908
11113
|
logFile: stringValue3(values, "log-file"),
|
|
9909
11114
|
allowedCwds,
|
|
11115
|
+
missingConfiguredAllowedCwds: configuredAllowedCwdState.missingCwds,
|
|
9910
11116
|
allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
|
|
9911
11117
|
codeServerBin: editorRuntime.codeServerBin,
|
|
9912
11118
|
editorRuntime: editorRuntime.runtime,
|
|
9913
11119
|
socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
|
|
9914
11120
|
dependencyStatus,
|
|
9915
|
-
|
|
11121
|
+
workspaceSlug: tokenFile.workspaceId,
|
|
11122
|
+
channelSession: channelSlug === undefined || listenUser === undefined ? undefined : {
|
|
9916
11123
|
workspaceSlug: tokenFile.workspaceId,
|
|
9917
11124
|
channelSlug,
|
|
9918
11125
|
kandanThreadId: stringValue3(values, "linzumi-thread-id"),
|
|
@@ -9940,12 +11147,6 @@ function rejectAgentRunnerTargetingFlags(values) {
|
|
|
9940
11147
|
throw new Error(`linzumi commander uses the claimed human Commander token scope; remove ${unsupportedFlags.map((flag) => `--${flag}`).join(", ")}.`);
|
|
9941
11148
|
}
|
|
9942
11149
|
}
|
|
9943
|
-
function requiredStoredAgentChannel(channelId) {
|
|
9944
|
-
if (channelId !== undefined) {
|
|
9945
|
-
return channelId;
|
|
9946
|
-
}
|
|
9947
|
-
throw new Error("agent token file is missing channelId; rerun linzumi claim before starting a Commander");
|
|
9948
|
-
}
|
|
9949
11150
|
function requiredStoredOwnerUsername(ownerUsername) {
|
|
9950
11151
|
if (ownerUsername !== undefined) {
|
|
9951
11152
|
return ownerUsername;
|
|
@@ -10002,7 +11203,7 @@ async function parseRunnerArgs(args, deps = {
|
|
|
10002
11203
|
process.exit(0);
|
|
10003
11204
|
}
|
|
10004
11205
|
if (values.get("version") === true) {
|
|
10005
|
-
process.stdout.write(`linzumi 0.0.
|
|
11206
|
+
process.stdout.write(`linzumi 0.0.39-beta
|
|
10006
11207
|
`);
|
|
10007
11208
|
process.exit(0);
|
|
10008
11209
|
}
|
|
@@ -10010,7 +11211,10 @@ async function parseRunnerArgs(args, deps = {
|
|
|
10010
11211
|
const kandanUrl = required(values, "linzumi-url");
|
|
10011
11212
|
const cwd = stringValue3(values, "cwd") ?? process.cwd();
|
|
10012
11213
|
const cwdAllowedCwds = assertConfiguredAllowedCwds([cwd]);
|
|
10013
|
-
const
|
|
11214
|
+
const configuredAllowedCwdState = values.has("allowed-cwd") ? {
|
|
11215
|
+
allowedCwds: assertConfiguredAllowedCwds(parseAllowedCwdList(stringValue3(values, "allowed-cwd"))),
|
|
11216
|
+
missingCwds: []
|
|
11217
|
+
} : readConfiguredAllowedCwdState();
|
|
10014
11218
|
const codexBin = stringValue3(values, "codex-bin") ?? "codex";
|
|
10015
11219
|
const customCodeServerBin = stringValue3(values, "code-server-bin");
|
|
10016
11220
|
const explicitToken = stringValue3(values, "token");
|
|
@@ -10050,12 +11254,14 @@ async function parseRunnerArgs(args, deps = {
|
|
|
10050
11254
|
launchTui: values.get("launch-tui") === true,
|
|
10051
11255
|
fast: values.get("fast") === true,
|
|
10052
11256
|
logFile: stringValue3(values, "log-file"),
|
|
10053
|
-
allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...
|
|
11257
|
+
allowedCwds: Array.from(new Set([...cwdAllowedCwds, ...configuredAllowedCwdState.allowedCwds])),
|
|
11258
|
+
missingConfiguredAllowedCwds: configuredAllowedCwdState.missingCwds,
|
|
10054
11259
|
allowedForwardPorts: parseAllowedPortList(stringValue3(values, "forward-port")),
|
|
10055
11260
|
codeServerBin: editorRuntime.codeServerBin,
|
|
10056
11261
|
editorRuntime: editorRuntime.runtime,
|
|
10057
11262
|
socketFactory: trustedWebSocketFactory(kandanTlsTrustFromEnv()),
|
|
10058
11263
|
dependencyStatus,
|
|
11264
|
+
workspaceSlug: channelSession?.workspaceSlug ?? singleWorkspaceScopeFromAccessToken(token),
|
|
10059
11265
|
channelSession
|
|
10060
11266
|
};
|
|
10061
11267
|
}
|