sessix-server 0.2.9 → 0.3.1
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/dist/index.js +823 -125
- package/dist/server.js +790 -113
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,9 +24,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
|
-
var
|
|
28
|
-
var
|
|
29
|
-
var
|
|
27
|
+
var import_node_os8 = require("os");
|
|
28
|
+
var import_node_fs4 = require("fs");
|
|
29
|
+
var import_node_path7 = require("path");
|
|
30
|
+
var import_node_child_process9 = require("child_process");
|
|
30
31
|
|
|
31
32
|
// src/i18n/locales/zh.ts
|
|
32
33
|
var zh = {
|
|
@@ -51,6 +52,9 @@ var zh = {
|
|
|
51
52
|
tokenRegenerated: "\u{1F511} Token \u5DF2\u91CD\u7F6E\uFF0C\u6240\u6709\u5BA2\u6237\u7AEF\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u65B0\u626B\u7801\u914D\u5BF9",
|
|
52
53
|
tokenRegenerateFailed: "Token \u91CD\u7F6E\u5931\u8D25:",
|
|
53
54
|
updateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}}\uFF08\u5F53\u524D v{{current}}\uFF09\uFF0C\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u66F4\u65B0\uFF1A",
|
|
55
|
+
autoUpdating: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}}\uFF08\u5F53\u524D v{{current}}\uFF09\uFF0C\u6B63\u5728\u81EA\u52A8\u66F4\u65B0...",
|
|
56
|
+
autoUpdateFailed: "\u81EA\u52A8\u66F4\u65B0\u5931\u8D25\uFF0C\u7EE7\u7EED\u4F7F\u7528\u5F53\u524D\u7248\u672C",
|
|
57
|
+
skipUpdateCheck: "\u8DF3\u8FC7\u7248\u672C\u68C0\u67E5\uFF08SESSIX_NO_UPDATE_CHECK=1\uFF09",
|
|
54
58
|
receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
|
|
55
59
|
goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
|
|
56
60
|
shutdownError: "\u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:",
|
|
@@ -161,6 +165,9 @@ var en = {
|
|
|
161
165
|
tokenRegenerated: "\u{1F511} Token regenerated, all clients disconnected. Scan QR to re-pair",
|
|
162
166
|
tokenRegenerateFailed: "Token regeneration failed:",
|
|
163
167
|
updateAvailable: "New version v{{latest}} available (current v{{current}}). Update with:",
|
|
168
|
+
autoUpdating: "New version v{{latest}} available (current v{{current}}), auto-updating...",
|
|
169
|
+
autoUpdateFailed: "Auto-update failed, continuing with current version",
|
|
170
|
+
skipUpdateCheck: "Skipping update check (SESSIX_NO_UPDATE_CHECK=1)",
|
|
164
171
|
receivedSignal: "Received {{signal}}, graceful shutdown...",
|
|
165
172
|
goodbye: "All services closed, goodbye!",
|
|
166
173
|
shutdownError: "Shutdown error:",
|
|
@@ -295,11 +302,11 @@ function t(key, params) {
|
|
|
295
302
|
}
|
|
296
303
|
|
|
297
304
|
// src/server.ts
|
|
298
|
-
var
|
|
305
|
+
var import_uuid6 = require("uuid");
|
|
299
306
|
var import_promises4 = require("fs/promises");
|
|
300
|
-
var
|
|
301
|
-
var
|
|
302
|
-
var
|
|
307
|
+
var import_node_os7 = require("os");
|
|
308
|
+
var import_node_path6 = require("path");
|
|
309
|
+
var import_node_child_process8 = require("child_process");
|
|
303
310
|
var import_node_util = require("util");
|
|
304
311
|
|
|
305
312
|
// src/providers/ProcessProvider.ts
|
|
@@ -319,14 +326,14 @@ var import_node_os = require("os");
|
|
|
319
326
|
var import_node_child_process = require("child_process");
|
|
320
327
|
var isWindows = process.platform === "win32";
|
|
321
328
|
function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
|
|
322
|
-
return new Promise((
|
|
329
|
+
return new Promise((resolve2) => {
|
|
323
330
|
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
324
|
-
|
|
331
|
+
resolve2();
|
|
325
332
|
return;
|
|
326
333
|
}
|
|
327
334
|
const onExit = () => {
|
|
328
335
|
clearTimeout(timer);
|
|
329
|
-
|
|
336
|
+
resolve2();
|
|
330
337
|
};
|
|
331
338
|
proc.once("exit", onExit);
|
|
332
339
|
if (isWindows) {
|
|
@@ -342,7 +349,7 @@ function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
|
|
|
342
349
|
proc.kill("SIGKILL");
|
|
343
350
|
}
|
|
344
351
|
}
|
|
345
|
-
|
|
352
|
+
resolve2();
|
|
346
353
|
}, timeoutMs);
|
|
347
354
|
});
|
|
348
355
|
}
|
|
@@ -644,7 +651,9 @@ var ProcessProvider = class {
|
|
|
644
651
|
const event = result.value;
|
|
645
652
|
if (event.type === "assistant") {
|
|
646
653
|
for (const block of event.message.content) {
|
|
647
|
-
if (block.type === "tool_use"
|
|
654
|
+
if (block.type === "tool_use") {
|
|
655
|
+
const isQuestion = block.name === "AskUserQuestion" || block.name === "AskFollowupQuestion";
|
|
656
|
+
if (!isQuestion) continue;
|
|
648
657
|
const input = block.input;
|
|
649
658
|
const question = input.question ?? "";
|
|
650
659
|
if (!question) continue;
|
|
@@ -656,6 +665,7 @@ var ProcessProvider = class {
|
|
|
656
665
|
}
|
|
657
666
|
if (sessionSet.has(prevKey)) continue;
|
|
658
667
|
sessionSet.add(prevKey);
|
|
668
|
+
console.log(`[ProcessProvider] Session ${sessionId}: detected ${block.name} (toolUseId=${block.id})`);
|
|
659
669
|
this.emitter.emit(this.getQuestionEventName(sessionId), {
|
|
660
670
|
toolUseId: block.id,
|
|
661
671
|
question,
|
|
@@ -771,7 +781,7 @@ var ProcessProvider = class {
|
|
|
771
781
|
const prompt = `You are an AI coding assistant. Based on the following Claude Code conversation context, suggest the most valuable next instruction for the user (give the instruction directly, no explanation, no quotes):
|
|
772
782
|
|
|
773
783
|
${context}`;
|
|
774
|
-
return new Promise((
|
|
784
|
+
return new Promise((resolve2, reject) => {
|
|
775
785
|
const env = { ...process.env };
|
|
776
786
|
delete env.CLAUDECODE;
|
|
777
787
|
const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
|
|
@@ -786,7 +796,7 @@ ${context}`;
|
|
|
786
796
|
});
|
|
787
797
|
proc.once("exit", (code) => {
|
|
788
798
|
if (code === 0) {
|
|
789
|
-
|
|
799
|
+
resolve2(output.trim());
|
|
790
800
|
} else {
|
|
791
801
|
reject(new Error(`generateSuggestion process exit code: ${code}`));
|
|
792
802
|
}
|
|
@@ -813,10 +823,10 @@ ${context}`;
|
|
|
813
823
|
tool_use_id: toolUseId,
|
|
814
824
|
content: answer
|
|
815
825
|
});
|
|
816
|
-
await new Promise((
|
|
826
|
+
await new Promise((resolve2, reject) => {
|
|
817
827
|
entry.process.stdin.write(toolResult + "\n", (err) => {
|
|
818
828
|
if (err) reject(err);
|
|
819
|
-
else
|
|
829
|
+
else resolve2();
|
|
820
830
|
});
|
|
821
831
|
});
|
|
822
832
|
console.log(`[ProcessProvider] Session ${sessionId}: AskUserQuestion answered (toolUseId=${toolUseId})`);
|
|
@@ -847,11 +857,643 @@ ${context}`;
|
|
|
847
857
|
}
|
|
848
858
|
};
|
|
849
859
|
|
|
850
|
-
// src/
|
|
860
|
+
// src/providers/CodexProvider.ts
|
|
861
|
+
var import_child_process2 = require("child_process");
|
|
862
|
+
var import_readline2 = require("readline");
|
|
863
|
+
var import_events2 = require("events");
|
|
864
|
+
var import_fs = require("fs");
|
|
865
|
+
var import_path = require("path");
|
|
866
|
+
var import_os = require("os");
|
|
851
867
|
var import_uuid2 = require("uuid");
|
|
868
|
+
|
|
869
|
+
// src/utils/codexPath.ts
|
|
870
|
+
var import_node_child_process3 = require("child_process");
|
|
871
|
+
var import_node_fs2 = require("fs");
|
|
872
|
+
var import_node_path2 = require("path");
|
|
873
|
+
var import_node_os3 = require("os");
|
|
874
|
+
function findCodexPath() {
|
|
875
|
+
try {
|
|
876
|
+
const cmd = isWindows ? "where codex" : "which codex";
|
|
877
|
+
return (0, import_node_child_process3.execSync)(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
880
|
+
const candidates = isWindows ? [
|
|
881
|
+
(0, import_node_path2.join)(process.env.LOCALAPPDATA ?? "", "Programs", "codex", "codex.exe"),
|
|
882
|
+
(0, import_node_path2.join)((0, import_node_os3.homedir)(), ".codex", "local", "codex.exe")
|
|
883
|
+
] : [
|
|
884
|
+
"/opt/homebrew/bin/codex",
|
|
885
|
+
"/usr/local/bin/codex",
|
|
886
|
+
(0, import_node_path2.join)((0, import_node_os3.homedir)(), ".local", "bin", "codex")
|
|
887
|
+
];
|
|
888
|
+
for (const candidate of candidates) {
|
|
889
|
+
try {
|
|
890
|
+
(0, import_node_fs2.accessSync)(candidate, import_node_fs2.constants.X_OK);
|
|
891
|
+
return candidate;
|
|
892
|
+
} catch {
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return "codex";
|
|
896
|
+
}
|
|
897
|
+
function resolveCodexJsEntry(codexPath) {
|
|
898
|
+
try {
|
|
899
|
+
let realPath;
|
|
900
|
+
try {
|
|
901
|
+
realPath = (0, import_node_fs2.readlinkSync)(codexPath);
|
|
902
|
+
if (!realPath.startsWith("/")) {
|
|
903
|
+
realPath = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(codexPath), realPath);
|
|
904
|
+
}
|
|
905
|
+
} catch {
|
|
906
|
+
realPath = codexPath;
|
|
907
|
+
}
|
|
908
|
+
const head = (0, import_node_fs2.readFileSync)(realPath, { encoding: "utf-8", flag: "r" }).slice(0, 100);
|
|
909
|
+
if (head.startsWith("#!/usr/bin/env node") || head.startsWith("#!/usr/bin/node")) {
|
|
910
|
+
return realPath;
|
|
911
|
+
}
|
|
912
|
+
} catch {
|
|
913
|
+
}
|
|
914
|
+
return void 0;
|
|
915
|
+
}
|
|
916
|
+
var CODEX_PATH = findCodexPath();
|
|
917
|
+
var CODEX_JS_ENTRY = resolveCodexJsEntry(CODEX_PATH);
|
|
918
|
+
async function getCodexVersion() {
|
|
919
|
+
try {
|
|
920
|
+
const cmd = CODEX_JS_ENTRY ? `"${process.execPath}" "${CODEX_JS_ENTRY}" --version` : `"${CODEX_PATH}" --version`;
|
|
921
|
+
const output = (0, import_node_child_process3.execSync)(cmd, {
|
|
922
|
+
encoding: "utf-8",
|
|
923
|
+
timeout: 5e3
|
|
924
|
+
}).trim();
|
|
925
|
+
return output.replace(/^codex-cli\s+/, "");
|
|
926
|
+
} catch {
|
|
927
|
+
return void 0;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
function isCodexAvailable() {
|
|
931
|
+
if (CODEX_JS_ENTRY) {
|
|
932
|
+
try {
|
|
933
|
+
(0, import_node_fs2.accessSync)(CODEX_JS_ENTRY, import_node_fs2.constants.R_OK);
|
|
934
|
+
return true;
|
|
935
|
+
} catch {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
(0, import_node_fs2.accessSync)(CODEX_PATH, import_node_fs2.constants.X_OK);
|
|
941
|
+
return true;
|
|
942
|
+
} catch {
|
|
943
|
+
try {
|
|
944
|
+
(0, import_node_child_process3.execSync)(`"${CODEX_PATH}" --version`, { timeout: 5e3 });
|
|
945
|
+
return true;
|
|
946
|
+
} catch {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/providers/CodexProvider.ts
|
|
953
|
+
var SESSIX_DIR = (0, import_path.join)((0, import_os.homedir)(), ".sessix");
|
|
954
|
+
var CODEX_SESSIONS_FILE = (0, import_path.join)(SESSIX_DIR, "codex-sessions.json");
|
|
955
|
+
var CodexProvider = class {
|
|
956
|
+
activeSessions = /* @__PURE__ */ new Map();
|
|
957
|
+
emitter = new import_events2.EventEmitter();
|
|
958
|
+
/** 持久化的会话元数据(sessionId → metadata) */
|
|
959
|
+
persistedSessions = /* @__PURE__ */ new Map();
|
|
960
|
+
/** 自增 ID 计数器,用于生成 ClaudeStreamEvent 中的 message/block ID */
|
|
961
|
+
idCounter = 0;
|
|
962
|
+
constructor() {
|
|
963
|
+
this.loadPersistedSessions();
|
|
964
|
+
}
|
|
965
|
+
async startSession(opts) {
|
|
966
|
+
const { projectPath, message, sessionId: existingSessionId } = opts;
|
|
967
|
+
const sessionId = existingSessionId ?? (0, import_uuid2.v4)();
|
|
968
|
+
if (this.activeSessions.has(sessionId)) {
|
|
969
|
+
await this.killSession(sessionId);
|
|
970
|
+
}
|
|
971
|
+
const projectId = projectPath.split("/").filter(Boolean).pop() ?? "unknown";
|
|
972
|
+
const session = {
|
|
973
|
+
id: sessionId,
|
|
974
|
+
projectId,
|
|
975
|
+
projectPath,
|
|
976
|
+
status: "running",
|
|
977
|
+
createdAt: Date.now(),
|
|
978
|
+
lastActiveAt: Date.now(),
|
|
979
|
+
summary: message.slice(0, 80),
|
|
980
|
+
agentType: "codex"
|
|
981
|
+
};
|
|
982
|
+
const resume = opts.resume ?? !!existingSessionId;
|
|
983
|
+
let resumeThreadId;
|
|
984
|
+
if (resume && existingSessionId) {
|
|
985
|
+
const persisted = this.persistedSessions.get(existingSessionId);
|
|
986
|
+
if (persisted?.threadId) {
|
|
987
|
+
resumeThreadId = persisted.threadId;
|
|
988
|
+
console.log(`[CodexProvider] Resuming session ${sessionId} with threadId: ${resumeThreadId}`);
|
|
989
|
+
} else {
|
|
990
|
+
console.warn(`[CodexProvider] Session ${sessionId} resume requested but no persisted threadId found, creating new session`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const proc = this.spawnCodexProcess(projectPath, message, resumeThreadId, opts.model);
|
|
994
|
+
session.pid = proc.pid;
|
|
995
|
+
this.activeSessions.set(sessionId, {
|
|
996
|
+
session,
|
|
997
|
+
process: proc,
|
|
998
|
+
threadId: resumeThreadId,
|
|
999
|
+
turnStartTime: Date.now()
|
|
1000
|
+
});
|
|
1001
|
+
const initEvent = {
|
|
1002
|
+
type: "system",
|
|
1003
|
+
subtype: "init",
|
|
1004
|
+
session_id: sessionId
|
|
1005
|
+
};
|
|
1006
|
+
this.emitter.emit(this.getEventName(sessionId), initEvent);
|
|
1007
|
+
proc.on("error", (err) => {
|
|
1008
|
+
console.error(`[CodexProvider] Session ${sessionId} process error:`, err.message);
|
|
1009
|
+
this.activeSessions.delete(sessionId);
|
|
1010
|
+
this.emitError(sessionId, `Process spawn failed: ${err.message}`);
|
|
1011
|
+
});
|
|
1012
|
+
this.attachStdoutListener(sessionId, proc);
|
|
1013
|
+
this.attachStderrListener(sessionId, proc);
|
|
1014
|
+
this.attachExitListener(sessionId, proc);
|
|
1015
|
+
return session;
|
|
1016
|
+
}
|
|
1017
|
+
async killSession(sessionId) {
|
|
1018
|
+
const entry = this.activeSessions.get(sessionId);
|
|
1019
|
+
if (!entry) return;
|
|
1020
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null) {
|
|
1021
|
+
try {
|
|
1022
|
+
entry.process.stdin?.end();
|
|
1023
|
+
} catch {
|
|
1024
|
+
}
|
|
1025
|
+
await killProcessCrossPlatform(entry.process);
|
|
1026
|
+
}
|
|
1027
|
+
this.activeSessions.delete(sessionId);
|
|
1028
|
+
}
|
|
1029
|
+
async sendMessage(sessionId, message) {
|
|
1030
|
+
let entry = this.activeSessions.get(sessionId);
|
|
1031
|
+
if (!entry) {
|
|
1032
|
+
const persisted = this.persistedSessions.get(sessionId);
|
|
1033
|
+
if (!persisted?.threadId) {
|
|
1034
|
+
throw new Error(`Session ${sessionId} not found or already ended`);
|
|
1035
|
+
}
|
|
1036
|
+
const projectId = persisted.projectPath.split("/").filter(Boolean).pop() ?? "unknown";
|
|
1037
|
+
const session = {
|
|
1038
|
+
id: sessionId,
|
|
1039
|
+
projectId,
|
|
1040
|
+
projectPath: persisted.projectPath,
|
|
1041
|
+
status: "running",
|
|
1042
|
+
createdAt: persisted.createdAt,
|
|
1043
|
+
lastActiveAt: Date.now(),
|
|
1044
|
+
summary: persisted.summary,
|
|
1045
|
+
agentType: "codex"
|
|
1046
|
+
};
|
|
1047
|
+
const placeholderProc = (0, import_child_process2.spawn)("true", [], { stdio: "ignore" });
|
|
1048
|
+
entry = {
|
|
1049
|
+
session,
|
|
1050
|
+
process: placeholderProc,
|
|
1051
|
+
threadId: persisted.threadId,
|
|
1052
|
+
turnStartTime: Date.now()
|
|
1053
|
+
};
|
|
1054
|
+
this.activeSessions.set(sessionId, entry);
|
|
1055
|
+
}
|
|
1056
|
+
const procAlive = entry.process.exitCode === null && entry.process.signalCode === null;
|
|
1057
|
+
if (procAlive) {
|
|
1058
|
+
try {
|
|
1059
|
+
entry.process.stdin?.end();
|
|
1060
|
+
} catch {
|
|
1061
|
+
}
|
|
1062
|
+
await killProcessCrossPlatform(entry.process);
|
|
1063
|
+
}
|
|
1064
|
+
const threadId = entry.threadId ?? this.persistedSessions.get(sessionId)?.threadId;
|
|
1065
|
+
if (!threadId) {
|
|
1066
|
+
console.warn(`[CodexProvider] Session ${sessionId} has no thread ID, falling back to new thread`);
|
|
1067
|
+
}
|
|
1068
|
+
const proc = this.spawnCodexProcess(entry.session.projectPath, message, threadId ?? void 0);
|
|
1069
|
+
entry.session.status = "running";
|
|
1070
|
+
entry.session.lastActiveAt = Date.now();
|
|
1071
|
+
entry.session.pid = proc.pid;
|
|
1072
|
+
entry.process = proc;
|
|
1073
|
+
entry.turnStartTime = Date.now();
|
|
1074
|
+
proc.on("error", (err) => {
|
|
1075
|
+
console.error(`[CodexProvider] Session ${sessionId} sendMessage process error:`, err.message);
|
|
1076
|
+
this.activeSessions.delete(sessionId);
|
|
1077
|
+
this.emitError(sessionId, `Failed to send message: ${err.message}`);
|
|
1078
|
+
});
|
|
1079
|
+
this.attachStdoutListener(sessionId, proc);
|
|
1080
|
+
this.attachStderrListener(sessionId, proc);
|
|
1081
|
+
this.attachExitListener(sessionId, proc);
|
|
1082
|
+
}
|
|
1083
|
+
onEvent(sessionId, callback) {
|
|
1084
|
+
const eventName = this.getEventName(sessionId);
|
|
1085
|
+
this.emitter.on(eventName, callback);
|
|
1086
|
+
return () => {
|
|
1087
|
+
this.emitter.off(eventName, callback);
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
getActiveSessions() {
|
|
1091
|
+
return Array.from(this.activeSessions.values()).map((e) => e.session);
|
|
1092
|
+
}
|
|
1093
|
+
async generateSuggestion(_context) {
|
|
1094
|
+
return "";
|
|
1095
|
+
}
|
|
1096
|
+
async answerQuestion(_sessionId, _toolUseId, _answer) {
|
|
1097
|
+
}
|
|
1098
|
+
onQuestion(_sessionId, _callback) {
|
|
1099
|
+
return () => {
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
// ============================================
|
|
1103
|
+
// 私有方法
|
|
1104
|
+
// ============================================
|
|
1105
|
+
nextId() {
|
|
1106
|
+
return `codex_${++this.idCounter}`;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* 启动 codex CLI 进程
|
|
1110
|
+
*
|
|
1111
|
+
* @param projectPath 工作目录
|
|
1112
|
+
* @param message 用户消息
|
|
1113
|
+
* @param resumeThreadId 如果提供,则使用 `codex exec resume` 恢复会话
|
|
1114
|
+
*/
|
|
1115
|
+
spawnCodexProcess(projectPath, message, resumeThreadId, model) {
|
|
1116
|
+
const args = ["exec", "--json", "--full-auto"];
|
|
1117
|
+
if (model) {
|
|
1118
|
+
args.push("-m", model);
|
|
1119
|
+
}
|
|
1120
|
+
args.push("-C", projectPath);
|
|
1121
|
+
if (resumeThreadId) {
|
|
1122
|
+
args.push("resume", resumeThreadId);
|
|
1123
|
+
}
|
|
1124
|
+
args.push(message);
|
|
1125
|
+
const env = { ...process.env };
|
|
1126
|
+
let cmd;
|
|
1127
|
+
let spawnArgs;
|
|
1128
|
+
if (CODEX_JS_ENTRY) {
|
|
1129
|
+
cmd = process.execPath;
|
|
1130
|
+
spawnArgs = [CODEX_JS_ENTRY, ...args];
|
|
1131
|
+
console.log(`[CodexProvider] Spawning via node: ${cmd} ${CODEX_JS_ENTRY} ${args.join(" ")}`);
|
|
1132
|
+
} else {
|
|
1133
|
+
cmd = CODEX_PATH;
|
|
1134
|
+
spawnArgs = args;
|
|
1135
|
+
console.log(`[CodexProvider] Spawning: ${CODEX_PATH} ${args.join(" ")}`);
|
|
1136
|
+
}
|
|
1137
|
+
const proc = (0, import_child_process2.spawn)(cmd, spawnArgs, {
|
|
1138
|
+
cwd: projectPath,
|
|
1139
|
+
env,
|
|
1140
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1141
|
+
});
|
|
1142
|
+
try {
|
|
1143
|
+
proc.stdin?.end();
|
|
1144
|
+
} catch {
|
|
1145
|
+
}
|
|
1146
|
+
return proc;
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* 挂载 stdout 监听器,逐行解析 Codex NDJSON 并转换为 ClaudeStreamEvent
|
|
1150
|
+
*/
|
|
1151
|
+
attachStdoutListener(sessionId, proc) {
|
|
1152
|
+
if (!proc.stdout) return;
|
|
1153
|
+
const rl = (0, import_readline2.createInterface)({ input: proc.stdout, crlfDelay: Infinity });
|
|
1154
|
+
const entry = this.activeSessions.get(sessionId);
|
|
1155
|
+
if (entry) entry.rl = rl;
|
|
1156
|
+
rl.on("line", (line) => {
|
|
1157
|
+
const trimmed = line.trim();
|
|
1158
|
+
if (!trimmed) return;
|
|
1159
|
+
console.log(`[CodexProvider] Session ${sessionId} stdout: ${trimmed.substring(0, 200)}`);
|
|
1160
|
+
let event;
|
|
1161
|
+
try {
|
|
1162
|
+
event = JSON.parse(trimmed);
|
|
1163
|
+
} catch {
|
|
1164
|
+
console.warn(`[CodexProvider] Session ${sessionId}: failed to parse: ${trimmed.substring(0, 100)}`);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
this.handleCodexEvent(sessionId, event);
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* 处理单个 Codex NDJSON 事件,转换并发射 ClaudeStreamEvent
|
|
1172
|
+
*/
|
|
1173
|
+
handleCodexEvent(sessionId, event) {
|
|
1174
|
+
const entry = this.activeSessions.get(sessionId);
|
|
1175
|
+
if (!entry) return;
|
|
1176
|
+
entry.session.lastActiveAt = Date.now();
|
|
1177
|
+
switch (event.type) {
|
|
1178
|
+
case "thread.started": {
|
|
1179
|
+
if (event.thread_id) {
|
|
1180
|
+
entry.threadId = event.thread_id;
|
|
1181
|
+
this.persistSession(sessionId, {
|
|
1182
|
+
threadId: event.thread_id,
|
|
1183
|
+
projectPath: entry.session.projectPath,
|
|
1184
|
+
summary: entry.session.summary,
|
|
1185
|
+
createdAt: entry.session.createdAt,
|
|
1186
|
+
lastActiveAt: Date.now()
|
|
1187
|
+
});
|
|
1188
|
+
console.log(`[CodexProvider] Session ${sessionId} threadId persisted: ${event.thread_id}`);
|
|
1189
|
+
}
|
|
1190
|
+
break;
|
|
1191
|
+
}
|
|
1192
|
+
case "turn.started": {
|
|
1193
|
+
entry.session.status = "running";
|
|
1194
|
+
entry.turnStartTime = Date.now();
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
case "item.completed": {
|
|
1198
|
+
if (!event.item) break;
|
|
1199
|
+
this.handleItemCompleted(sessionId, event.item);
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
case "turn.completed": {
|
|
1203
|
+
entry.session.status = "idle";
|
|
1204
|
+
const duration = entry.turnStartTime ? Date.now() - entry.turnStartTime : 0;
|
|
1205
|
+
const resultEvent = {
|
|
1206
|
+
type: "result",
|
|
1207
|
+
subtype: "success",
|
|
1208
|
+
session_id: sessionId,
|
|
1209
|
+
is_error: false,
|
|
1210
|
+
result: "",
|
|
1211
|
+
duration_ms: duration,
|
|
1212
|
+
num_turns: 1,
|
|
1213
|
+
usage: event.usage
|
|
1214
|
+
};
|
|
1215
|
+
this.emitter.emit(this.getEventName(sessionId), resultEvent);
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
case "turn.failed": {
|
|
1219
|
+
entry.session.status = "error";
|
|
1220
|
+
const failEvent = {
|
|
1221
|
+
type: "result",
|
|
1222
|
+
subtype: "error",
|
|
1223
|
+
session_id: sessionId,
|
|
1224
|
+
is_error: true,
|
|
1225
|
+
result: event.error ?? "Turn failed",
|
|
1226
|
+
duration_ms: entry.turnStartTime ? Date.now() - entry.turnStartTime : 0,
|
|
1227
|
+
num_turns: 1
|
|
1228
|
+
};
|
|
1229
|
+
this.emitter.emit(this.getEventName(sessionId), failEvent);
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* 处理 item.completed 事件,按 item.type 转换为对应的 ClaudeStreamEvent
|
|
1236
|
+
*/
|
|
1237
|
+
handleItemCompleted(sessionId, item) {
|
|
1238
|
+
switch (item.type) {
|
|
1239
|
+
case "agent_message": {
|
|
1240
|
+
const msgEvent = {
|
|
1241
|
+
type: "assistant",
|
|
1242
|
+
session_id: sessionId,
|
|
1243
|
+
message: {
|
|
1244
|
+
id: item.id || this.nextId(),
|
|
1245
|
+
model: "codex",
|
|
1246
|
+
role: "assistant",
|
|
1247
|
+
content: [{ type: "text", text: item.text ?? "" }]
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
this.emitter.emit(this.getEventName(sessionId), msgEvent);
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
case "command_execution": {
|
|
1254
|
+
const toolUseId = item.id || this.nextId();
|
|
1255
|
+
const toolEvent = {
|
|
1256
|
+
type: "assistant",
|
|
1257
|
+
session_id: sessionId,
|
|
1258
|
+
message: {
|
|
1259
|
+
id: this.nextId(),
|
|
1260
|
+
model: "codex",
|
|
1261
|
+
role: "assistant",
|
|
1262
|
+
content: [{
|
|
1263
|
+
type: "tool_use",
|
|
1264
|
+
id: toolUseId,
|
|
1265
|
+
name: "Bash",
|
|
1266
|
+
input: { command: item.command ?? "" }
|
|
1267
|
+
}]
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
this.emitter.emit(this.getEventName(sessionId), toolEvent);
|
|
1271
|
+
const resultContent = item.aggregated_output ?? "";
|
|
1272
|
+
const isError = item.exit_code != null && item.exit_code !== 0;
|
|
1273
|
+
const toolResultEvent = {
|
|
1274
|
+
type: "user",
|
|
1275
|
+
session_id: sessionId,
|
|
1276
|
+
message: {
|
|
1277
|
+
role: "user",
|
|
1278
|
+
content: [{
|
|
1279
|
+
type: "tool_result",
|
|
1280
|
+
tool_use_id: toolUseId,
|
|
1281
|
+
content: resultContent,
|
|
1282
|
+
is_error: isError
|
|
1283
|
+
}]
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
this.emitter.emit(this.getEventName(sessionId), toolResultEvent);
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
case "file_change": {
|
|
1290
|
+
const editToolUseId = item.id || this.nextId();
|
|
1291
|
+
const toolEvent = {
|
|
1292
|
+
type: "assistant",
|
|
1293
|
+
session_id: sessionId,
|
|
1294
|
+
message: {
|
|
1295
|
+
id: this.nextId(),
|
|
1296
|
+
model: "codex",
|
|
1297
|
+
role: "assistant",
|
|
1298
|
+
content: [{
|
|
1299
|
+
type: "tool_use",
|
|
1300
|
+
id: editToolUseId,
|
|
1301
|
+
name: "Edit",
|
|
1302
|
+
input: { file_path: item.file_path ?? "", diff: item.diff ?? "" }
|
|
1303
|
+
}]
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
this.emitter.emit(this.getEventName(sessionId), toolEvent);
|
|
1307
|
+
const toolResultEvent = {
|
|
1308
|
+
type: "user",
|
|
1309
|
+
session_id: sessionId,
|
|
1310
|
+
message: {
|
|
1311
|
+
role: "user",
|
|
1312
|
+
content: [{
|
|
1313
|
+
type: "tool_result",
|
|
1314
|
+
tool_use_id: editToolUseId,
|
|
1315
|
+
content: `File edited: ${item.file_path ?? "unknown"}`
|
|
1316
|
+
}]
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
this.emitter.emit(this.getEventName(sessionId), toolResultEvent);
|
|
1320
|
+
break;
|
|
1321
|
+
}
|
|
1322
|
+
case "reasoning": {
|
|
1323
|
+
const thinkEvent = {
|
|
1324
|
+
type: "assistant",
|
|
1325
|
+
session_id: sessionId,
|
|
1326
|
+
message: {
|
|
1327
|
+
id: item.id || this.nextId(),
|
|
1328
|
+
model: "codex",
|
|
1329
|
+
role: "assistant",
|
|
1330
|
+
content: [{ type: "thinking", thinking: item.text ?? "" }]
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
this.emitter.emit(this.getEventName(sessionId), thinkEvent);
|
|
1334
|
+
break;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
attachStderrListener(sessionId, proc) {
|
|
1339
|
+
if (!proc.stderr) return;
|
|
1340
|
+
proc.stderr.on("data", (data) => {
|
|
1341
|
+
const text = data.toString().trim();
|
|
1342
|
+
if (text) {
|
|
1343
|
+
console.error(`[CodexProvider] Session ${sessionId} stderr: ${text}`);
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
attachExitListener(sessionId, proc) {
|
|
1348
|
+
proc.once("exit", (code, signal) => {
|
|
1349
|
+
const entry = this.activeSessions.get(sessionId);
|
|
1350
|
+
if (!entry) return;
|
|
1351
|
+
if (entry.process !== proc) return;
|
|
1352
|
+
console.log(`[CodexProvider] Session ${sessionId} process exited: code=${code} signal=${signal} threadId=${entry.threadId ?? "NONE"}`);
|
|
1353
|
+
if (entry.rl) {
|
|
1354
|
+
entry.rl.close();
|
|
1355
|
+
entry.rl = void 0;
|
|
1356
|
+
}
|
|
1357
|
+
entry.session.pid = void 0;
|
|
1358
|
+
entry.session.lastActiveAt = Date.now();
|
|
1359
|
+
const alreadyHasResult = entry.session.status === "idle" || entry.session.status === "error";
|
|
1360
|
+
if (alreadyHasResult) return;
|
|
1361
|
+
const isNormal = isNormalExit(code, signal);
|
|
1362
|
+
entry.session.status = isNormal ? "idle" : "error";
|
|
1363
|
+
if (!isNormal) {
|
|
1364
|
+
console.error(`[CodexProvider] Session ${sessionId}: process exited abnormally code=${code} signal=${signal}`);
|
|
1365
|
+
}
|
|
1366
|
+
const syntheticResult = {
|
|
1367
|
+
type: "result",
|
|
1368
|
+
subtype: isNormal ? "success" : "error",
|
|
1369
|
+
session_id: sessionId,
|
|
1370
|
+
is_error: !isNormal,
|
|
1371
|
+
result: isNormal ? "" : `Process exited code=${code} signal=${signal}`,
|
|
1372
|
+
duration_ms: 0,
|
|
1373
|
+
num_turns: 0
|
|
1374
|
+
};
|
|
1375
|
+
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
emitError(sessionId, message) {
|
|
1379
|
+
const event = {
|
|
1380
|
+
type: "result",
|
|
1381
|
+
subtype: "error",
|
|
1382
|
+
result: message,
|
|
1383
|
+
session_id: sessionId,
|
|
1384
|
+
duration_ms: 0,
|
|
1385
|
+
is_error: true,
|
|
1386
|
+
num_turns: 0
|
|
1387
|
+
};
|
|
1388
|
+
this.emitter.emit(this.getEventName(sessionId), event);
|
|
1389
|
+
}
|
|
1390
|
+
getEventName(sessionId) {
|
|
1391
|
+
return `claude:${sessionId}`;
|
|
1392
|
+
}
|
|
1393
|
+
// ============================================
|
|
1394
|
+
// 持久化方法
|
|
1395
|
+
// ============================================
|
|
1396
|
+
/**
|
|
1397
|
+
* 从磁盘加载持久化的 Codex 会话元数据
|
|
1398
|
+
*/
|
|
1399
|
+
loadPersistedSessions() {
|
|
1400
|
+
try {
|
|
1401
|
+
if (!(0, import_fs.existsSync)(CODEX_SESSIONS_FILE)) return;
|
|
1402
|
+
const data = JSON.parse((0, import_fs.readFileSync)(CODEX_SESSIONS_FILE, "utf-8"));
|
|
1403
|
+
for (const [sessionId, meta] of Object.entries(data)) {
|
|
1404
|
+
this.persistedSessions.set(sessionId, meta);
|
|
1405
|
+
}
|
|
1406
|
+
console.log(`[CodexProvider] Loaded ${this.persistedSessions.size} persisted sessions`);
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
console.warn("[CodexProvider] Failed to load persisted sessions:", err);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* 持久化单个会话的元数据到磁盘
|
|
1413
|
+
*/
|
|
1414
|
+
persistSession(sessionId, meta) {
|
|
1415
|
+
this.persistedSessions.set(sessionId, meta);
|
|
1416
|
+
this.flushPersistedSessions();
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* 将所有持久化数据写入磁盘
|
|
1420
|
+
*/
|
|
1421
|
+
flushPersistedSessions() {
|
|
1422
|
+
try {
|
|
1423
|
+
if (!(0, import_fs.existsSync)(SESSIX_DIR)) {
|
|
1424
|
+
(0, import_fs.mkdirSync)(SESSIX_DIR, { recursive: true });
|
|
1425
|
+
}
|
|
1426
|
+
const data = {};
|
|
1427
|
+
for (const [sessionId, meta] of this.persistedSessions) {
|
|
1428
|
+
data[sessionId] = meta;
|
|
1429
|
+
}
|
|
1430
|
+
(0, import_fs.writeFileSync)(CODEX_SESSIONS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
1431
|
+
} catch (err) {
|
|
1432
|
+
console.error("[CodexProvider] Failed to persist sessions:", err);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* 检查某个 sessionId 是否为已知的 Codex 会话(供 SessionManager 查询 agentType)
|
|
1437
|
+
*/
|
|
1438
|
+
isKnownSession(sessionId) {
|
|
1439
|
+
return this.activeSessions.has(sessionId) || this.persistedSessions.has(sessionId);
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1443
|
+
// src/providers/ProviderFactory.ts
|
|
1444
|
+
var import_node_child_process4 = require("child_process");
|
|
1445
|
+
var ProviderFactory = class {
|
|
1446
|
+
providers = /* @__PURE__ */ new Map();
|
|
1447
|
+
getProvider(agentType) {
|
|
1448
|
+
const existing = this.providers.get(agentType);
|
|
1449
|
+
if (existing) return existing;
|
|
1450
|
+
let provider;
|
|
1451
|
+
switch (agentType) {
|
|
1452
|
+
case "codex":
|
|
1453
|
+
provider = new CodexProvider();
|
|
1454
|
+
break;
|
|
1455
|
+
case "claude-code":
|
|
1456
|
+
default:
|
|
1457
|
+
provider = new ProcessProvider();
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
this.providers.set(agentType, provider);
|
|
1461
|
+
return provider;
|
|
1462
|
+
}
|
|
1463
|
+
async detectAgents() {
|
|
1464
|
+
const agents = [];
|
|
1465
|
+
try {
|
|
1466
|
+
const claudePath = findClaudePath();
|
|
1467
|
+
const claudeVersion = (0, import_node_child_process4.execSync)(`"${claudePath}" --version`, {
|
|
1468
|
+
encoding: "utf-8",
|
|
1469
|
+
timeout: 5e3
|
|
1470
|
+
}).trim();
|
|
1471
|
+
agents.push({
|
|
1472
|
+
type: "claude-code",
|
|
1473
|
+
name: "Claude Code",
|
|
1474
|
+
available: true,
|
|
1475
|
+
version: claudeVersion
|
|
1476
|
+
});
|
|
1477
|
+
} catch {
|
|
1478
|
+
agents.push({ type: "claude-code", name: "Claude Code", available: false });
|
|
1479
|
+
}
|
|
1480
|
+
if (isCodexAvailable()) {
|
|
1481
|
+
const version = await getCodexVersion();
|
|
1482
|
+
agents.push({ type: "codex", name: "Codex", available: true, version });
|
|
1483
|
+
} else {
|
|
1484
|
+
agents.push({ type: "codex", name: "Codex", available: false });
|
|
1485
|
+
}
|
|
1486
|
+
return agents;
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1490
|
+
// src/session/SessionManager.ts
|
|
1491
|
+
var import_uuid3 = require("uuid");
|
|
852
1492
|
var BUFFER_MAX = 5e3;
|
|
853
1493
|
var SessionManager = class {
|
|
854
1494
|
provider;
|
|
1495
|
+
providerFactory;
|
|
1496
|
+
sessionAgentType = /* @__PURE__ */ new Map();
|
|
855
1497
|
/** 事件回调列表(事件会被转发到 WsBridge) */
|
|
856
1498
|
eventCallbacks = [];
|
|
857
1499
|
/** 每个会话的事件流取消订阅函数 */
|
|
@@ -877,8 +1519,26 @@ var SessionManager = class {
|
|
|
877
1519
|
bufferTruncated = /* @__PURE__ */ new Set();
|
|
878
1520
|
/** sessionId → projectPath 映射,用于截断时从 JSONL 补全历史 */
|
|
879
1521
|
sessionProjectPaths = /* @__PURE__ */ new Map();
|
|
880
|
-
constructor(
|
|
881
|
-
|
|
1522
|
+
constructor(providerOrFactory) {
|
|
1523
|
+
if (providerOrFactory instanceof ProviderFactory) {
|
|
1524
|
+
this.providerFactory = providerOrFactory;
|
|
1525
|
+
this.provider = providerOrFactory.getProvider("claude-code");
|
|
1526
|
+
} else {
|
|
1527
|
+
this.provider = providerOrFactory;
|
|
1528
|
+
this.providerFactory = null;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
getProviderForSession(sessionId) {
|
|
1532
|
+
if (!this.providerFactory) return this.provider;
|
|
1533
|
+
let agentType = this.sessionAgentType.get(sessionId);
|
|
1534
|
+
if (!agentType) {
|
|
1535
|
+
const codexProvider = this.providerFactory.getProvider("codex");
|
|
1536
|
+
if (codexProvider instanceof CodexProvider && codexProvider.isKnownSession(sessionId)) {
|
|
1537
|
+
agentType = "codex";
|
|
1538
|
+
this.sessionAgentType.set(sessionId, agentType);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return this.providerFactory.getProvider(agentType ?? "claude-code");
|
|
882
1542
|
}
|
|
883
1543
|
// ============================================
|
|
884
1544
|
// 公开 API
|
|
@@ -889,8 +1549,9 @@ var SessionManager = class {
|
|
|
889
1549
|
* 调用 provider.startSession(),订阅事件流,
|
|
890
1550
|
* 将 ClaudeStreamEvent 包装为 ServerEvent 转发。
|
|
891
1551
|
*/
|
|
892
|
-
async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images) {
|
|
893
|
-
const
|
|
1552
|
+
async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType = "claude-code") {
|
|
1553
|
+
const provider = this.providerFactory ? this.providerFactory.getProvider(agentType) : this.provider;
|
|
1554
|
+
const session = await provider.startSession({
|
|
894
1555
|
projectPath,
|
|
895
1556
|
message,
|
|
896
1557
|
sessionId: resumeSessionId ?? newSessionId,
|
|
@@ -900,6 +1561,7 @@ var SessionManager = class {
|
|
|
900
1561
|
effort,
|
|
901
1562
|
images
|
|
902
1563
|
});
|
|
1564
|
+
this.sessionAgentType.set(session.id, agentType);
|
|
903
1565
|
this.lastBroadcastStatus.set(session.id, session.status);
|
|
904
1566
|
this.sessionProjectPaths.set(session.id, projectPath);
|
|
905
1567
|
this.unsubscribeSession(session.id);
|
|
@@ -911,7 +1573,8 @@ var SessionManager = class {
|
|
|
911
1573
|
* 发送消息到已有会话
|
|
912
1574
|
*/
|
|
913
1575
|
async sendMessage(sessionId, message, permissionMode, images) {
|
|
914
|
-
|
|
1576
|
+
const provider = this.getProviderForSession(sessionId);
|
|
1577
|
+
await provider.sendMessage(sessionId, message, permissionMode, images);
|
|
915
1578
|
this.updateSessionStatus(sessionId, "running");
|
|
916
1579
|
console.log(`[SessionManager] Message sent to session: ${sessionId}`);
|
|
917
1580
|
}
|
|
@@ -931,7 +1594,9 @@ var SessionManager = class {
|
|
|
931
1594
|
clearTimeout(pending.timer);
|
|
932
1595
|
this.pendingAssistantEvents.delete(sessionId);
|
|
933
1596
|
}
|
|
934
|
-
|
|
1597
|
+
const provider = this.getProviderForSession(sessionId);
|
|
1598
|
+
await provider.killSession(sessionId);
|
|
1599
|
+
this.sessionAgentType.delete(sessionId);
|
|
935
1600
|
console.log(`[SessionManager] Session killed: ${sessionId}`);
|
|
936
1601
|
}
|
|
937
1602
|
/**
|
|
@@ -1010,7 +1675,11 @@ var SessionManager = class {
|
|
|
1010
1675
|
* 获取所有活跃会话(含服务器端统计)
|
|
1011
1676
|
*/
|
|
1012
1677
|
getActiveSessions() {
|
|
1013
|
-
|
|
1678
|
+
const rawSessions = this.providerFactory ? [
|
|
1679
|
+
...this.providerFactory.getProvider("claude-code").getActiveSessions(),
|
|
1680
|
+
...this.providerFactory.getProvider("codex").getActiveSessions()
|
|
1681
|
+
] : this.provider.getActiveSessions();
|
|
1682
|
+
return rawSessions.map((session) => {
|
|
1014
1683
|
const stats = this.getSessionStats(session.id);
|
|
1015
1684
|
return stats ? { ...session, stats } : session;
|
|
1016
1685
|
});
|
|
@@ -1040,6 +1709,7 @@ var SessionManager = class {
|
|
|
1040
1709
|
this.sessionEventBuffers.clear();
|
|
1041
1710
|
this.bufferTruncated.clear();
|
|
1042
1711
|
this.sessionProjectPaths.clear();
|
|
1712
|
+
this.sessionAgentType.clear();
|
|
1043
1713
|
this.sessionStats.clear();
|
|
1044
1714
|
for (const [, pending] of this.pendingAssistantEvents) {
|
|
1045
1715
|
clearTimeout(pending.timer);
|
|
@@ -1057,10 +1727,11 @@ var SessionManager = class {
|
|
|
1057
1727
|
* 订阅指定会话的事件流(包括 AskUserQuestion 问题事件)
|
|
1058
1728
|
*/
|
|
1059
1729
|
subscribeToSession(sessionId) {
|
|
1060
|
-
const
|
|
1730
|
+
const provider = this.getProviderForSession(sessionId);
|
|
1731
|
+
const unsubscribeEvent = provider.onEvent(sessionId, (event) => {
|
|
1061
1732
|
this.handleClaudeEvent(sessionId, event);
|
|
1062
1733
|
});
|
|
1063
|
-
const unsubscribeQuestion =
|
|
1734
|
+
const unsubscribeQuestion = provider.onQuestion(
|
|
1064
1735
|
sessionId,
|
|
1065
1736
|
({ toolUseId, question, options }) => {
|
|
1066
1737
|
this.handleAskUserQuestion(sessionId, toolUseId, question, options);
|
|
@@ -1235,7 +1906,7 @@ var SessionManager = class {
|
|
|
1235
1906
|
console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
|
|
1236
1907
|
return;
|
|
1237
1908
|
}
|
|
1238
|
-
const requestId = (0,
|
|
1909
|
+
const requestId = (0, import_uuid3.v4)();
|
|
1239
1910
|
const request = {
|
|
1240
1911
|
id: requestId,
|
|
1241
1912
|
sessionId,
|
|
@@ -1246,12 +1917,13 @@ var SessionManager = class {
|
|
|
1246
1917
|
};
|
|
1247
1918
|
this.updateSessionStatus(sessionId, "waiting_question");
|
|
1248
1919
|
this.emit({ type: "question_request", request });
|
|
1249
|
-
const answerPromise = new Promise((
|
|
1250
|
-
this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve });
|
|
1920
|
+
const answerPromise = new Promise((resolve2) => {
|
|
1921
|
+
this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve: resolve2 });
|
|
1251
1922
|
});
|
|
1252
1923
|
answerPromise.then(async (answer) => {
|
|
1253
1924
|
try {
|
|
1254
|
-
|
|
1925
|
+
const provider = this.getProviderForSession(sessionId);
|
|
1926
|
+
await provider.answerQuestion(sessionId, toolUseId, answer);
|
|
1255
1927
|
} catch (err) {
|
|
1256
1928
|
console.error(`[SessionManager] answerQuestion failed (${sessionId}):`, err);
|
|
1257
1929
|
}
|
|
@@ -1488,11 +2160,11 @@ var WsBridge = class _WsBridge {
|
|
|
1488
2160
|
* 使用此方法代替 new WsBridge(),确保 EADDRINUSE 等错误能被调用方的 try-catch 捕获。
|
|
1489
2161
|
*/
|
|
1490
2162
|
static async create(options) {
|
|
1491
|
-
return new Promise((
|
|
2163
|
+
return new Promise((resolve2, reject) => {
|
|
1492
2164
|
const bridge = new _WsBridge(options);
|
|
1493
2165
|
bridge.wss.once("listening", () => {
|
|
1494
2166
|
bridge.wss.on("error", (err) => console.error(`[WsBridge] ${t("ws.serverError")}:`, err));
|
|
1495
|
-
|
|
2167
|
+
resolve2(bridge);
|
|
1496
2168
|
});
|
|
1497
2169
|
bridge.wss.once("error", reject);
|
|
1498
2170
|
});
|
|
@@ -1555,7 +2227,7 @@ var WsBridge = class _WsBridge {
|
|
|
1555
2227
|
}
|
|
1556
2228
|
/** 优雅关闭 WebSocket 服务 */
|
|
1557
2229
|
close() {
|
|
1558
|
-
return new Promise((
|
|
2230
|
+
return new Promise((resolve2, reject) => {
|
|
1559
2231
|
if (this.heartbeatTimer) {
|
|
1560
2232
|
clearInterval(this.heartbeatTimer);
|
|
1561
2233
|
this.heartbeatTimer = null;
|
|
@@ -1568,7 +2240,7 @@ var WsBridge = class _WsBridge {
|
|
|
1568
2240
|
reject(err);
|
|
1569
2241
|
} else {
|
|
1570
2242
|
console.log("[WsBridge] WebSocket server closed");
|
|
1571
|
-
|
|
2243
|
+
resolve2();
|
|
1572
2244
|
}
|
|
1573
2245
|
});
|
|
1574
2246
|
});
|
|
@@ -1674,15 +2346,15 @@ var WsBridge = class _WsBridge {
|
|
|
1674
2346
|
|
|
1675
2347
|
// src/approval/ApprovalProxy.ts
|
|
1676
2348
|
var import_node_http = __toESM(require("http"));
|
|
1677
|
-
var
|
|
1678
|
-
var
|
|
1679
|
-
var
|
|
1680
|
-
var
|
|
2349
|
+
var import_node_fs3 = __toESM(require("fs"));
|
|
2350
|
+
var import_node_path3 = __toESM(require("path"));
|
|
2351
|
+
var import_node_os4 = __toESM(require("os"));
|
|
2352
|
+
var import_uuid4 = require("uuid");
|
|
1681
2353
|
var ApprovalProxy = class _ApprovalProxy {
|
|
1682
2354
|
server;
|
|
1683
2355
|
token;
|
|
1684
2356
|
port;
|
|
1685
|
-
settingsPath =
|
|
2357
|
+
settingsPath = import_node_path3.default.join(import_node_os4.default.homedir(), ".claude", "settings.json");
|
|
1686
2358
|
/** 待处理的审批请求:requestId -> { resolve, timer, request } */
|
|
1687
2359
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
1688
2360
|
/** 审批请求回调(通知外部推送到手机) */
|
|
@@ -1708,11 +2380,11 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1708
2380
|
* 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
|
|
1709
2381
|
*/
|
|
1710
2382
|
static async create(options) {
|
|
1711
|
-
return new Promise((
|
|
2383
|
+
return new Promise((resolve2, reject) => {
|
|
1712
2384
|
const proxy = new _ApprovalProxy(options);
|
|
1713
2385
|
proxy.server.once("listening", () => {
|
|
1714
2386
|
proxy.server.on("error", (err) => console.error(`[ApprovalProxy] ${t("approval.serverError")}:`, err));
|
|
1715
|
-
|
|
2387
|
+
resolve2(proxy);
|
|
1716
2388
|
});
|
|
1717
2389
|
proxy.server.once("error", reject);
|
|
1718
2390
|
});
|
|
@@ -1787,7 +2459,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1787
2459
|
isToolInClaudeSettings(toolName, projectPath) {
|
|
1788
2460
|
const checkPath = (filepath) => {
|
|
1789
2461
|
try {
|
|
1790
|
-
const raw =
|
|
2462
|
+
const raw = import_node_fs3.default.readFileSync(filepath, "utf-8");
|
|
1791
2463
|
const settings = JSON.parse(raw);
|
|
1792
2464
|
const allow = settings?.permissions?.allow ?? [];
|
|
1793
2465
|
return allow.some((entry) => {
|
|
@@ -1801,24 +2473,24 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1801
2473
|
}
|
|
1802
2474
|
};
|
|
1803
2475
|
if (projectPath) {
|
|
1804
|
-
const projectSettingsPath =
|
|
2476
|
+
const projectSettingsPath = import_node_path3.default.join(projectPath, ".claude", "settings.json");
|
|
1805
2477
|
if (checkPath(projectSettingsPath)) return true;
|
|
1806
2478
|
}
|
|
1807
2479
|
return checkPath(this.settingsPath);
|
|
1808
2480
|
}
|
|
1809
2481
|
/** 将工具写入 settings.json permissions.allow(项目级或全局) */
|
|
1810
2482
|
addToClaudeSettings(projectPath, toolName) {
|
|
1811
|
-
const targetPath = projectPath ?
|
|
2483
|
+
const targetPath = projectPath ? import_node_path3.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
|
|
1812
2484
|
try {
|
|
1813
2485
|
if (projectPath) {
|
|
1814
|
-
const dir =
|
|
1815
|
-
if (!
|
|
1816
|
-
|
|
2486
|
+
const dir = import_node_path3.default.dirname(targetPath);
|
|
2487
|
+
if (!import_node_fs3.default.existsSync(dir)) {
|
|
2488
|
+
import_node_fs3.default.mkdirSync(dir, { recursive: true });
|
|
1817
2489
|
}
|
|
1818
2490
|
}
|
|
1819
2491
|
let settings = {};
|
|
1820
2492
|
try {
|
|
1821
|
-
settings = JSON.parse(
|
|
2493
|
+
settings = JSON.parse(import_node_fs3.default.readFileSync(targetPath, "utf-8"));
|
|
1822
2494
|
} catch {
|
|
1823
2495
|
}
|
|
1824
2496
|
if (!settings.permissions) {
|
|
@@ -1832,7 +2504,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1832
2504
|
const entry = `${toolName}(*)`;
|
|
1833
2505
|
if (!allow.includes(entry)) {
|
|
1834
2506
|
allow.push(entry);
|
|
1835
|
-
|
|
2507
|
+
import_node_fs3.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
1836
2508
|
const label = projectPath ? `${projectPath}/.claude/settings.json` : "~/.claude/settings.json";
|
|
1837
2509
|
console.log(`[ApprovalProxy] ${t("approval.alwaysAllowWritten", { entry, label })}`);
|
|
1838
2510
|
}
|
|
@@ -1869,7 +2541,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1869
2541
|
}
|
|
1870
2542
|
/** 优雅关闭 HTTP 服务 */
|
|
1871
2543
|
close() {
|
|
1872
|
-
return new Promise((
|
|
2544
|
+
return new Promise((resolve2, reject) => {
|
|
1873
2545
|
const pendingEntries = Array.from(this.pendingApprovals.entries());
|
|
1874
2546
|
for (const [, pending] of pendingEntries) {
|
|
1875
2547
|
clearTimeout(pending.timer);
|
|
@@ -1881,7 +2553,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1881
2553
|
reject(err);
|
|
1882
2554
|
} else {
|
|
1883
2555
|
console.log(`[ApprovalProxy] ${t("approval.httpClosed")}`);
|
|
1884
|
-
|
|
2556
|
+
resolve2();
|
|
1885
2557
|
}
|
|
1886
2558
|
});
|
|
1887
2559
|
});
|
|
@@ -1927,7 +2599,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1927
2599
|
try {
|
|
1928
2600
|
const body = await this.parseJsonBody(req);
|
|
1929
2601
|
const payload = body.payload ?? body;
|
|
1930
|
-
const requestId = (0,
|
|
2602
|
+
const requestId = (0, import_uuid4.v4)();
|
|
1931
2603
|
const projectPath = String(body.projectPath ?? "unknown");
|
|
1932
2604
|
const toolName = String(payload.tool_name ?? body.tool_name ?? "unknown");
|
|
1933
2605
|
const toolInput = payload.tool_input ?? body.tool_input ?? {};
|
|
@@ -1952,13 +2624,13 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1952
2624
|
return;
|
|
1953
2625
|
}
|
|
1954
2626
|
this.notifyApprovalRequest(approvalRequest);
|
|
1955
|
-
const decision = await new Promise((
|
|
2627
|
+
const decision = await new Promise((resolve2) => {
|
|
1956
2628
|
const timer = setTimeout(() => {
|
|
1957
2629
|
console.log(`[ApprovalProxy] ${t("approval.timeout", { id: requestId })}`);
|
|
1958
2630
|
this.pendingApprovals.delete(requestId);
|
|
1959
|
-
|
|
2631
|
+
resolve2({ decision: "allow" });
|
|
1960
2632
|
}, 325e3);
|
|
1961
|
-
this.pendingApprovals.set(requestId, { resolve, timer, request: approvalRequest });
|
|
2633
|
+
this.pendingApprovals.set(requestId, { resolve: resolve2, timer, request: approvalRequest });
|
|
1962
2634
|
});
|
|
1963
2635
|
this.sendJson(res, 200, decision);
|
|
1964
2636
|
} catch (err) {
|
|
@@ -2019,7 +2691,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2019
2691
|
/** 手动解析请求的 JSON body(限制最大 1MB 防止滥用) */
|
|
2020
2692
|
parseJsonBody(req) {
|
|
2021
2693
|
const MAX_BODY_SIZE = 1024 * 1024;
|
|
2022
|
-
return new Promise((
|
|
2694
|
+
return new Promise((resolve2, reject) => {
|
|
2023
2695
|
const chunks = [];
|
|
2024
2696
|
let totalSize = 0;
|
|
2025
2697
|
let destroyed = false;
|
|
@@ -2037,7 +2709,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2037
2709
|
try {
|
|
2038
2710
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
2039
2711
|
const parsed = JSON.parse(raw);
|
|
2040
|
-
|
|
2712
|
+
resolve2(parsed);
|
|
2041
2713
|
} catch {
|
|
2042
2714
|
reject(new Error(t("approval.invalidJson")));
|
|
2043
2715
|
}
|
|
@@ -2059,8 +2731,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2059
2731
|
};
|
|
2060
2732
|
|
|
2061
2733
|
// src/mdns/MdnsService.ts
|
|
2062
|
-
var
|
|
2063
|
-
var
|
|
2734
|
+
var import_node_child_process5 = require("child_process");
|
|
2735
|
+
var import_node_os5 = require("os");
|
|
2064
2736
|
function buildTxtArgs(txt) {
|
|
2065
2737
|
return Object.entries(txt).map(([k, v]) => `${k}=${v}`);
|
|
2066
2738
|
}
|
|
@@ -2078,7 +2750,7 @@ var MdnsService = class {
|
|
|
2078
2750
|
this.httpPort = options.httpPort;
|
|
2079
2751
|
this.version = options.version ?? "0.1.0";
|
|
2080
2752
|
this.pairing = options.pairing ?? "closed";
|
|
2081
|
-
this.useDnsSd = (0,
|
|
2753
|
+
this.useDnsSd = (0, import_node_os5.platform)() === "darwin";
|
|
2082
2754
|
}
|
|
2083
2755
|
getTxt() {
|
|
2084
2756
|
return {
|
|
@@ -2111,7 +2783,7 @@ var MdnsService = class {
|
|
|
2111
2783
|
String(this.wsPort),
|
|
2112
2784
|
...buildTxtArgs(this.getTxt())
|
|
2113
2785
|
];
|
|
2114
|
-
this.proc = (0,
|
|
2786
|
+
this.proc = (0, import_node_child_process5.spawn)("dns-sd", args, { stdio: "ignore" });
|
|
2115
2787
|
this.proc.on("error", (err) => {
|
|
2116
2788
|
console.warn(`[MdnsService] dns-sd failed, falling back to bonjour-service: ${err.message}`);
|
|
2117
2789
|
this.proc = null;
|
|
@@ -2132,7 +2804,7 @@ var MdnsService = class {
|
|
|
2132
2804
|
return;
|
|
2133
2805
|
}
|
|
2134
2806
|
try {
|
|
2135
|
-
const {
|
|
2807
|
+
const { Bonjour } = await import("bonjour-service");
|
|
2136
2808
|
const { networkInterfaces: networkInterfaces2 } = await import("os");
|
|
2137
2809
|
const lanAddrs = getLanAddresses(networkInterfaces2);
|
|
2138
2810
|
const opts = lanAddrs.length > 0 ? { interface: lanAddrs[0] } : {};
|
|
@@ -2223,12 +2895,12 @@ function getLanAddresses(networkInterfacesFn) {
|
|
|
2223
2895
|
|
|
2224
2896
|
// src/hooks/HookInstaller.ts
|
|
2225
2897
|
var import_promises2 = require("fs/promises");
|
|
2226
|
-
var
|
|
2227
|
-
var
|
|
2228
|
-
var SESSIX_HOOKS_DIR = (0,
|
|
2229
|
-
var HOOK_SCRIPT_PATH = (0,
|
|
2230
|
-
var PERMISSION_ACCEPT_PATH = (0,
|
|
2231
|
-
var CLAUDE_SETTINGS_PATH = (0,
|
|
2898
|
+
var import_node_path4 = require("path");
|
|
2899
|
+
var import_node_os6 = require("os");
|
|
2900
|
+
var SESSIX_HOOKS_DIR = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".sessix", "hooks");
|
|
2901
|
+
var HOOK_SCRIPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
|
|
2902
|
+
var PERMISSION_ACCEPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "permission-accept.js");
|
|
2903
|
+
var CLAUDE_SETTINGS_PATH = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude", "settings.json");
|
|
2232
2904
|
var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
|
|
2233
2905
|
var PERMISSION_ACCEPT_COMMAND = "node ~/.sessix/hooks/permission-accept.js";
|
|
2234
2906
|
var LEGACY_HOOK_COMMANDS = [
|
|
@@ -2402,7 +3074,7 @@ var HookInstaller = class {
|
|
|
2402
3074
|
* 写入 Claude Code settings.json
|
|
2403
3075
|
*/
|
|
2404
3076
|
async writeClaudeSettings(settings) {
|
|
2405
|
-
await (0, import_promises2.mkdir)((0,
|
|
3077
|
+
await (0, import_promises2.mkdir)((0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude"), { recursive: true });
|
|
2406
3078
|
await (0, import_promises2.writeFile)(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2407
3079
|
}
|
|
2408
3080
|
/**
|
|
@@ -2429,7 +3101,7 @@ var HookInstaller = class {
|
|
|
2429
3101
|
};
|
|
2430
3102
|
|
|
2431
3103
|
// src/notification/NotificationService.ts
|
|
2432
|
-
var
|
|
3104
|
+
var import_node_path5 = require("path");
|
|
2433
3105
|
var NotificationService = class {
|
|
2434
3106
|
constructor(sessionManager, expoChannel = null) {
|
|
2435
3107
|
this.sessionManager = sessionManager;
|
|
@@ -2525,7 +3197,7 @@ var NotificationService = class {
|
|
|
2525
3197
|
const dangerLevel = this.getDangerLevel(request.toolName);
|
|
2526
3198
|
const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
|
|
2527
3199
|
const categoryId = isDangerous ? "APPROVAL_DANGEROUS" : "APPROVAL_NORMAL";
|
|
2528
|
-
const projectName = (0,
|
|
3200
|
+
const projectName = (0, import_node_path5.basename)(
|
|
2529
3201
|
this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId)?.projectPath ?? ""
|
|
2530
3202
|
);
|
|
2531
3203
|
const pushTitle = isDangerous ? `\u26A0\uFE0F ${title}` : title;
|
|
@@ -2581,7 +3253,7 @@ var NotificationService = class {
|
|
|
2581
3253
|
/** 从审批请求中提取操作目标的简短描述 */
|
|
2582
3254
|
extractTarget(request) {
|
|
2583
3255
|
const input = request.toolInput;
|
|
2584
|
-
if (input.file_path) return (0,
|
|
3256
|
+
if (input.file_path) return (0, import_node_path5.basename)(String(input.file_path));
|
|
2585
3257
|
if (input.command) return String(input.command).slice(0, 40);
|
|
2586
3258
|
return request.description.slice(0, 40);
|
|
2587
3259
|
}
|
|
@@ -2684,7 +3356,7 @@ var NotificationService = class {
|
|
|
2684
3356
|
getSessionTitle(sessionId) {
|
|
2685
3357
|
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
2686
3358
|
if (!session) return "Unknown";
|
|
2687
|
-
return session.summary ?? (0,
|
|
3359
|
+
return session.summary ?? (0, import_node_path5.basename)(session.projectPath);
|
|
2688
3360
|
}
|
|
2689
3361
|
/** 获取会话的 YOLO 模式状态 */
|
|
2690
3362
|
getYoloMode(sessionId) {
|
|
@@ -2693,7 +3365,7 @@ var NotificationService = class {
|
|
|
2693
3365
|
};
|
|
2694
3366
|
|
|
2695
3367
|
// src/notification/DesktopNotificationChannel.ts
|
|
2696
|
-
var
|
|
3368
|
+
var import_node_child_process6 = require("child_process");
|
|
2697
3369
|
var DesktopNotificationChannel = class {
|
|
2698
3370
|
isAvailable() {
|
|
2699
3371
|
return process.platform === "darwin";
|
|
@@ -2704,12 +3376,12 @@ var DesktopNotificationChannel = class {
|
|
|
2704
3376
|
const body = payload.body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2705
3377
|
const sound = payload.sound ?? "Ping";
|
|
2706
3378
|
const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
|
|
2707
|
-
return new Promise((
|
|
2708
|
-
(0,
|
|
3379
|
+
return new Promise((resolve2) => {
|
|
3380
|
+
(0, import_node_child_process6.execFile)("osascript", ["-e", script], (err) => {
|
|
2709
3381
|
if (err) {
|
|
2710
3382
|
console.warn("[DesktopNotificationChannel] Send notification failed:", err.message);
|
|
2711
3383
|
}
|
|
2712
|
-
|
|
3384
|
+
resolve2();
|
|
2713
3385
|
});
|
|
2714
3386
|
});
|
|
2715
3387
|
}
|
|
@@ -2909,7 +3581,7 @@ var ActivityPushChannel = class {
|
|
|
2909
3581
|
const topic = "com.kachun.sessix.push-type.liveactivity";
|
|
2910
3582
|
const jwt = this.getJWT();
|
|
2911
3583
|
const payloadStr = JSON.stringify(payload);
|
|
2912
|
-
return new Promise((
|
|
3584
|
+
return new Promise((resolve2, reject) => {
|
|
2913
3585
|
let client;
|
|
2914
3586
|
try {
|
|
2915
3587
|
client = this.getHttp2Client();
|
|
@@ -2937,7 +3609,7 @@ var ActivityPushChannel = class {
|
|
|
2937
3609
|
});
|
|
2938
3610
|
req.on("end", () => {
|
|
2939
3611
|
if (statusCode === 200) {
|
|
2940
|
-
|
|
3612
|
+
resolve2();
|
|
2941
3613
|
} else {
|
|
2942
3614
|
if (statusCode === 0) {
|
|
2943
3615
|
this.http2Client?.destroy();
|
|
@@ -2979,12 +3651,12 @@ var ActivityPushChannel = class {
|
|
|
2979
3651
|
|
|
2980
3652
|
// src/session/ProjectReader.ts
|
|
2981
3653
|
var import_promises3 = require("fs/promises");
|
|
2982
|
-
var
|
|
2983
|
-
var
|
|
2984
|
-
var
|
|
2985
|
-
var CLAUDE_PROJECTS_DIR = (0,
|
|
3654
|
+
var import_readline3 = require("readline");
|
|
3655
|
+
var import_path2 = require("path");
|
|
3656
|
+
var import_os2 = require("os");
|
|
3657
|
+
var CLAUDE_PROJECTS_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "projects");
|
|
2986
3658
|
function getSessionFilePath(projectPath, sessionId) {
|
|
2987
|
-
return (0,
|
|
3659
|
+
return (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
|
|
2988
3660
|
}
|
|
2989
3661
|
async function getProjects() {
|
|
2990
3662
|
try {
|
|
@@ -3001,7 +3673,7 @@ async function getProjects() {
|
|
|
3001
3673
|
const encodedPath = entry.name;
|
|
3002
3674
|
const decodedPath = decodeDirName(encodedPath);
|
|
3003
3675
|
const name = decodedPath.split("/").filter(Boolean).pop() ?? encodedPath;
|
|
3004
|
-
const projectDir = (0,
|
|
3676
|
+
const projectDir = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
3005
3677
|
const { count: sessionCount, latestMtime } = await countJsonlFilesWithMtime(projectDir);
|
|
3006
3678
|
projects.push({
|
|
3007
3679
|
id: encodedPath,
|
|
@@ -3023,7 +3695,7 @@ async function getProjects() {
|
|
|
3023
3695
|
async function getHistoricalSessions(projectPath) {
|
|
3024
3696
|
try {
|
|
3025
3697
|
const encodedPath = encodeDirName(projectPath);
|
|
3026
|
-
const projectDir = (0,
|
|
3698
|
+
const projectDir = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
3027
3699
|
const dirExists = await directoryExists(projectDir);
|
|
3028
3700
|
if (!dirExists) {
|
|
3029
3701
|
return { ok: true, value: [] };
|
|
@@ -3034,7 +3706,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3034
3706
|
await Promise.all(
|
|
3035
3707
|
jsonlFiles.map(async (entry) => {
|
|
3036
3708
|
const sessionId = entry.name.slice(0, -6);
|
|
3037
|
-
const filePath = (0,
|
|
3709
|
+
const filePath = (0, import_path2.join)(projectDir, entry.name);
|
|
3038
3710
|
try {
|
|
3039
3711
|
const contentTs = await extractLastTimestamp(filePath);
|
|
3040
3712
|
if (contentTs) {
|
|
@@ -3053,13 +3725,13 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3053
3725
|
);
|
|
3054
3726
|
for (const entry of uuidDirs) {
|
|
3055
3727
|
try {
|
|
3056
|
-
const fileStat = await (0, import_promises3.stat)((0,
|
|
3728
|
+
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(projectDir, entry.name));
|
|
3057
3729
|
mtimeMap.set(entry.name, fileStat.mtimeMs);
|
|
3058
3730
|
} catch {
|
|
3059
3731
|
mtimeMap.set(entry.name, 0);
|
|
3060
3732
|
}
|
|
3061
3733
|
}
|
|
3062
|
-
const indexPath = (0,
|
|
3734
|
+
const indexPath = (0, import_path2.join)(projectDir, "sessions-index.json");
|
|
3063
3735
|
const sessionMap = /* @__PURE__ */ new Map();
|
|
3064
3736
|
try {
|
|
3065
3737
|
const indexContent = await (0, import_promises3.readFile)(indexPath, "utf-8");
|
|
@@ -3077,7 +3749,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3077
3749
|
}
|
|
3078
3750
|
await Promise.all(
|
|
3079
3751
|
Array.from(sessionMap.values()).filter((s) => (s.messageCount ?? 0) > 0 && !s.summary && !s.firstPrompt).map(async (s) => {
|
|
3080
|
-
const filePath = (0,
|
|
3752
|
+
const filePath = (0, import_path2.join)(projectDir, `${s.sessionId}.jsonl`);
|
|
3081
3753
|
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
3082
3754
|
if (firstPrompt) s.firstPrompt = firstPrompt;
|
|
3083
3755
|
})
|
|
@@ -3091,7 +3763,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3091
3763
|
if (uuidDirSet.has(sessionId)) {
|
|
3092
3764
|
sessionMap.set(sessionId, { sessionId, lastModified: mtime, messageCount: -1 });
|
|
3093
3765
|
} else {
|
|
3094
|
-
const filePath = (0,
|
|
3766
|
+
const filePath = (0, import_path2.join)(projectDir, `${sessionId}.jsonl`);
|
|
3095
3767
|
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
3096
3768
|
sessionMap.set(sessionId, { sessionId, lastModified: mtime, firstPrompt });
|
|
3097
3769
|
}
|
|
@@ -3115,7 +3787,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3115
3787
|
async function getSessionHistory(projectPath, sessionId) {
|
|
3116
3788
|
try {
|
|
3117
3789
|
const encodedPath = encodeDirName(projectPath);
|
|
3118
|
-
const filePath = (0,
|
|
3790
|
+
const filePath = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
|
|
3119
3791
|
const raw = await (0, import_promises3.readFile)(filePath, "utf-8").catch((err) => {
|
|
3120
3792
|
if (err.code === "ENOENT") return null;
|
|
3121
3793
|
throw err;
|
|
@@ -3228,7 +3900,7 @@ async function extractFirstPrompt(filePath) {
|
|
|
3228
3900
|
let fileHandle;
|
|
3229
3901
|
try {
|
|
3230
3902
|
fileHandle = await (0, import_promises3.open)(filePath, "r");
|
|
3231
|
-
const rl = (0,
|
|
3903
|
+
const rl = (0, import_readline3.createInterface)({
|
|
3232
3904
|
input: fileHandle.createReadStream({ encoding: "utf-8" }),
|
|
3233
3905
|
crlfDelay: Infinity
|
|
3234
3906
|
});
|
|
@@ -3296,15 +3968,15 @@ async function countJsonlFilesWithMtime(dirPath) {
|
|
|
3296
3968
|
await Promise.all([
|
|
3297
3969
|
...jsonlEntries.map(async (entry) => {
|
|
3298
3970
|
try {
|
|
3299
|
-
const contentTs = await extractLastTimestamp((0,
|
|
3300
|
-
const ts = contentTs ?? (await (0, import_promises3.stat)((0,
|
|
3971
|
+
const contentTs = await extractLastTimestamp((0, import_path2.join)(dirPath, entry.name));
|
|
3972
|
+
const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name))).mtimeMs;
|
|
3301
3973
|
if (ts > latestMtime) latestMtime = ts;
|
|
3302
3974
|
} catch {
|
|
3303
3975
|
}
|
|
3304
3976
|
}),
|
|
3305
3977
|
...uuidDirs.map(async (entry) => {
|
|
3306
3978
|
try {
|
|
3307
|
-
const fileStat = await (0, import_promises3.stat)((0,
|
|
3979
|
+
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name));
|
|
3308
3980
|
if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
|
|
3309
3981
|
} catch {
|
|
3310
3982
|
}
|
|
@@ -3376,14 +4048,14 @@ var PairingManager = class {
|
|
|
3376
4048
|
};
|
|
3377
4049
|
|
|
3378
4050
|
// src/auth/AuthManager.ts
|
|
3379
|
-
var import_child_process2 = require("child_process");
|
|
3380
4051
|
var import_child_process3 = require("child_process");
|
|
4052
|
+
var import_child_process4 = require("child_process");
|
|
3381
4053
|
var import_util = require("util");
|
|
3382
|
-
var
|
|
3383
|
-
var execFileAsync = (0, import_util.promisify)(
|
|
4054
|
+
var import_events3 = require("events");
|
|
4055
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
|
|
3384
4056
|
var CLAUDE_PATH2 = findClaudePath();
|
|
3385
4057
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3386
|
-
var AuthManager = class extends
|
|
4058
|
+
var AuthManager = class extends import_events3.EventEmitter {
|
|
3387
4059
|
loginProcess = null;
|
|
3388
4060
|
loginTimeout = null;
|
|
3389
4061
|
urlSent = false;
|
|
@@ -3411,7 +4083,7 @@ var AuthManager = class extends import_events2.EventEmitter {
|
|
|
3411
4083
|
}
|
|
3412
4084
|
this.clearLoginTimeout();
|
|
3413
4085
|
this.urlSent = false;
|
|
3414
|
-
const proc = (0,
|
|
4086
|
+
const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
|
|
3415
4087
|
env: { ...process.env, BROWSER: "echo" },
|
|
3416
4088
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3417
4089
|
});
|
|
@@ -3497,8 +4169,8 @@ var AuthManager = class extends import_events2.EventEmitter {
|
|
|
3497
4169
|
var import_promises5 = require("fs/promises");
|
|
3498
4170
|
|
|
3499
4171
|
// src/terminal/TerminalExecutor.ts
|
|
3500
|
-
var
|
|
3501
|
-
var
|
|
4172
|
+
var import_node_child_process7 = require("child_process");
|
|
4173
|
+
var import_uuid5 = require("uuid");
|
|
3502
4174
|
var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3503
4175
|
var TerminalExecutor = class {
|
|
3504
4176
|
processes = /* @__PURE__ */ new Map();
|
|
@@ -3520,10 +4192,10 @@ var TerminalExecutor = class {
|
|
|
3520
4192
|
}
|
|
3521
4193
|
}
|
|
3522
4194
|
exec(sessionId, command, cwd) {
|
|
3523
|
-
const execId = (0,
|
|
4195
|
+
const execId = (0, import_uuid5.v4)();
|
|
3524
4196
|
const shell = isWindows ? "powershell" : "bash";
|
|
3525
4197
|
const args = isWindows ? ["-Command", command] : ["-c", command];
|
|
3526
|
-
const proc = (0,
|
|
4198
|
+
const proc = (0, import_node_child_process7.spawn)(shell, args, {
|
|
3527
4199
|
cwd,
|
|
3528
4200
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3529
4201
|
env: { ...process.env }
|
|
@@ -3586,7 +4258,7 @@ var TerminalExecutor = class {
|
|
|
3586
4258
|
// src/server.ts
|
|
3587
4259
|
var WS_PORT = 3745;
|
|
3588
4260
|
var HTTP_PORT = 3746;
|
|
3589
|
-
var execAsync = (0, import_node_util.promisify)(
|
|
4261
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
|
|
3590
4262
|
async function killPortProcess(port) {
|
|
3591
4263
|
try {
|
|
3592
4264
|
if (isWindows) {
|
|
@@ -3610,7 +4282,7 @@ async function killPortProcess(port) {
|
|
|
3610
4282
|
await execAsync(`kill -9 ${pids.join(" ")}`);
|
|
3611
4283
|
}
|
|
3612
4284
|
}
|
|
3613
|
-
await new Promise((
|
|
4285
|
+
await new Promise((resolve2) => setTimeout(resolve2, 600));
|
|
3614
4286
|
} catch {
|
|
3615
4287
|
}
|
|
3616
4288
|
}
|
|
@@ -3628,8 +4300,8 @@ async function createWithRetry(label, port, factory) {
|
|
|
3628
4300
|
}
|
|
3629
4301
|
}
|
|
3630
4302
|
async function start(opts = {}) {
|
|
3631
|
-
const configDir = (0,
|
|
3632
|
-
const tokenFile = (0,
|
|
4303
|
+
const configDir = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix");
|
|
4304
|
+
const tokenFile = (0, import_node_path6.join)(configDir, "token");
|
|
3633
4305
|
let token;
|
|
3634
4306
|
if (opts.token !== void 0) {
|
|
3635
4307
|
token = opts.token;
|
|
@@ -3641,14 +4313,14 @@ async function start(opts = {}) {
|
|
|
3641
4313
|
try {
|
|
3642
4314
|
token = (await (0, import_promises4.readFile)(tokenFile, "utf8")).trim();
|
|
3643
4315
|
} catch {
|
|
3644
|
-
token = (0,
|
|
4316
|
+
token = (0, import_uuid6.v4)();
|
|
3645
4317
|
await (0, import_promises4.mkdir)(configDir, { recursive: true });
|
|
3646
4318
|
await (0, import_promises4.writeFile)(tokenFile, token, "utf8");
|
|
3647
4319
|
}
|
|
3648
4320
|
}
|
|
3649
4321
|
}
|
|
3650
|
-
const
|
|
3651
|
-
const sessionManager = new SessionManager(
|
|
4322
|
+
const providerFactory = new ProviderFactory();
|
|
4323
|
+
const sessionManager = new SessionManager(providerFactory);
|
|
3652
4324
|
const terminalExecutor = new TerminalExecutor();
|
|
3653
4325
|
const wsBridge = await createWithRetry(
|
|
3654
4326
|
"WsBridge",
|
|
@@ -3688,7 +4360,7 @@ async function start(opts = {}) {
|
|
|
3688
4360
|
let mdnsService = null;
|
|
3689
4361
|
const pairingManager = new PairingManager({
|
|
3690
4362
|
token,
|
|
3691
|
-
serverName: (0,
|
|
4363
|
+
serverName: (0, import_node_os7.hostname)(),
|
|
3692
4364
|
version: "0.2.0",
|
|
3693
4365
|
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
3694
4366
|
});
|
|
@@ -3745,7 +4417,8 @@ async function start(opts = {}) {
|
|
|
3745
4417
|
event.model,
|
|
3746
4418
|
event.permissionMode,
|
|
3747
4419
|
event.effort,
|
|
3748
|
-
event.images
|
|
4420
|
+
event.images,
|
|
4421
|
+
event.agentType
|
|
3749
4422
|
);
|
|
3750
4423
|
wsBridge.broadcast({
|
|
3751
4424
|
type: "session_list",
|
|
@@ -3921,7 +4594,7 @@ async function start(opts = {}) {
|
|
|
3921
4594
|
return null;
|
|
3922
4595
|
}).filter(Boolean).join("\n");
|
|
3923
4596
|
}
|
|
3924
|
-
const suggestion = await
|
|
4597
|
+
const suggestion = await providerFactory.getProvider("claude-code").generateSuggestion(context);
|
|
3925
4598
|
wsBridge.send(ws, {
|
|
3926
4599
|
type: "prompt_suggestion",
|
|
3927
4600
|
sessionId: event.sessionId,
|
|
@@ -3999,6 +4672,11 @@ async function start(opts = {}) {
|
|
|
3999
4672
|
}
|
|
4000
4673
|
break;
|
|
4001
4674
|
}
|
|
4675
|
+
case "list_agents": {
|
|
4676
|
+
const agents = await providerFactory.detectAgents();
|
|
4677
|
+
wsBridge.send(ws, { type: "agent_list", agents });
|
|
4678
|
+
break;
|
|
4679
|
+
}
|
|
4002
4680
|
default: {
|
|
4003
4681
|
wsBridge.send(ws, {
|
|
4004
4682
|
type: "error",
|
|
@@ -4162,7 +4840,7 @@ async function start(opts = {}) {
|
|
|
4162
4840
|
openPairing: (duration) => pairingManager.open(duration),
|
|
4163
4841
|
closePairing: () => pairingManager.close(),
|
|
4164
4842
|
regenerateToken: async () => {
|
|
4165
|
-
const newToken = (0,
|
|
4843
|
+
const newToken = (0, import_uuid6.v4)();
|
|
4166
4844
|
await (0, import_promises4.mkdir)(configDir, { recursive: true });
|
|
4167
4845
|
await (0, import_promises4.writeFile)(tokenFile, newToken, "utf8");
|
|
4168
4846
|
instance.token = newToken;
|
|
@@ -4181,7 +4859,7 @@ async function start(opts = {}) {
|
|
|
4181
4859
|
var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
|
|
4182
4860
|
function getPackageVersion() {
|
|
4183
4861
|
try {
|
|
4184
|
-
const pkg = JSON.parse((0,
|
|
4862
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path7.join)(__dirname, "..", "package.json"), "utf8"));
|
|
4185
4863
|
return pkg.version ?? "0.0.0";
|
|
4186
4864
|
} catch {
|
|
4187
4865
|
return "0.0.0";
|
|
@@ -4203,11 +4881,38 @@ async function fetchLatestVersion() {
|
|
|
4203
4881
|
return null;
|
|
4204
4882
|
}
|
|
4205
4883
|
}
|
|
4884
|
+
function isNewer(latest, current) {
|
|
4885
|
+
const a = latest.split(".").map(Number);
|
|
4886
|
+
const b = current.split(".").map(Number);
|
|
4887
|
+
for (let i = 0; i < 3; i++) {
|
|
4888
|
+
if ((a[i] ?? 0) > (b[i] ?? 0)) return true;
|
|
4889
|
+
if ((a[i] ?? 0) < (b[i] ?? 0)) return false;
|
|
4890
|
+
}
|
|
4891
|
+
return false;
|
|
4892
|
+
}
|
|
4893
|
+
async function autoUpdateIfNeeded() {
|
|
4894
|
+
if (process.env.SESSIX_NO_UPDATE_CHECK === "1" || process.env.__SESSIX_UPDATED === "1") return;
|
|
4895
|
+
const latest = await fetchLatestVersion();
|
|
4896
|
+
if (!latest || !isNewer(latest, PKG_VERSION)) return;
|
|
4897
|
+
console.log(` \u{1F4E6} ${t("startup.autoUpdating", { current: PKG_VERSION, latest })}`);
|
|
4898
|
+
console.log();
|
|
4899
|
+
try {
|
|
4900
|
+
(0, import_node_child_process9.execFileSync)("npx", [`sessix-server@${latest}`], {
|
|
4901
|
+
stdio: "inherit",
|
|
4902
|
+
env: { ...process.env, __SESSIX_UPDATED: "1" }
|
|
4903
|
+
});
|
|
4904
|
+
process.exit(0);
|
|
4905
|
+
} catch {
|
|
4906
|
+
console.log(` \u26A0\uFE0F ${t("startup.autoUpdateFailed")}`);
|
|
4907
|
+
console.log();
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4206
4910
|
async function main() {
|
|
4207
4911
|
console.log("=".repeat(50));
|
|
4208
4912
|
console.log(`${t("startup.banner")} v${PKG_VERSION}`);
|
|
4209
4913
|
console.log("=".repeat(50));
|
|
4210
4914
|
console.log();
|
|
4915
|
+
await autoUpdateIfNeeded();
|
|
4211
4916
|
const enableAutoConnect = process.env.SESSIX_AUTO_CONNECT !== "false";
|
|
4212
4917
|
const server = await start({ enableAutoConnect });
|
|
4213
4918
|
const localIp = getLocalIp();
|
|
@@ -4248,13 +4953,6 @@ async function main() {
|
|
|
4248
4953
|
console.log(t("startup.pairingOpen"));
|
|
4249
4954
|
console.log(t("startup.pressT"));
|
|
4250
4955
|
console.log();
|
|
4251
|
-
fetchLatestVersion().then((latest) => {
|
|
4252
|
-
if (!latest || latest === PKG_VERSION) return;
|
|
4253
|
-
console.log();
|
|
4254
|
-
console.log(` \u{1F4E6} ${t("startup.updateAvailable", { current: PKG_VERSION, latest })}`);
|
|
4255
|
-
console.log(` npx sessix-server@latest`);
|
|
4256
|
-
console.log();
|
|
4257
|
-
});
|
|
4258
4956
|
const shutdown = async (signal) => {
|
|
4259
4957
|
console.log(`
|
|
4260
4958
|
[Main] ${t("startup.receivedSignal", { signal })}`);
|
|
@@ -4305,7 +5003,7 @@ ${t("startup.pairingReopened")}`);
|
|
|
4305
5003
|
}
|
|
4306
5004
|
}
|
|
4307
5005
|
function getLocalIp() {
|
|
4308
|
-
const interfaces = (0,
|
|
5006
|
+
const interfaces = (0, import_node_os8.networkInterfaces)();
|
|
4309
5007
|
for (const iface of Object.values(interfaces)) {
|
|
4310
5008
|
for (const addr of iface ?? []) {
|
|
4311
5009
|
if (addr.family === "IPv4" && !addr.internal) {
|