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/server.js
CHANGED
|
@@ -57,6 +57,9 @@ var zh = {
|
|
|
57
57
|
tokenRegenerated: "\u{1F511} Token \u5DF2\u91CD\u7F6E\uFF0C\u6240\u6709\u5BA2\u6237\u7AEF\u5DF2\u65AD\u5F00\uFF0C\u8BF7\u91CD\u65B0\u626B\u7801\u914D\u5BF9",
|
|
58
58
|
tokenRegenerateFailed: "Token \u91CD\u7F6E\u5931\u8D25:",
|
|
59
59
|
updateAvailable: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}}\uFF08\u5F53\u524D v{{current}}\uFF09\uFF0C\u8FD0\u884C\u4EE5\u4E0B\u547D\u4EE4\u66F4\u65B0\uFF1A",
|
|
60
|
+
autoUpdating: "\u53D1\u73B0\u65B0\u7248\u672C v{{latest}}\uFF08\u5F53\u524D v{{current}}\uFF09\uFF0C\u6B63\u5728\u81EA\u52A8\u66F4\u65B0...",
|
|
61
|
+
autoUpdateFailed: "\u81EA\u52A8\u66F4\u65B0\u5931\u8D25\uFF0C\u7EE7\u7EED\u4F7F\u7528\u5F53\u524D\u7248\u672C",
|
|
62
|
+
skipUpdateCheck: "\u8DF3\u8FC7\u7248\u672C\u68C0\u67E5\uFF08SESSIX_NO_UPDATE_CHECK=1\uFF09",
|
|
60
63
|
receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
|
|
61
64
|
goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
|
|
62
65
|
shutdownError: "\u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:",
|
|
@@ -167,6 +170,9 @@ var en = {
|
|
|
167
170
|
tokenRegenerated: "\u{1F511} Token regenerated, all clients disconnected. Scan QR to re-pair",
|
|
168
171
|
tokenRegenerateFailed: "Token regeneration failed:",
|
|
169
172
|
updateAvailable: "New version v{{latest}} available (current v{{current}}). Update with:",
|
|
173
|
+
autoUpdating: "New version v{{latest}} available (current v{{current}}), auto-updating...",
|
|
174
|
+
autoUpdateFailed: "Auto-update failed, continuing with current version",
|
|
175
|
+
skipUpdateCheck: "Skipping update check (SESSIX_NO_UPDATE_CHECK=1)",
|
|
170
176
|
receivedSignal: "Received {{signal}}, graceful shutdown...",
|
|
171
177
|
goodbye: "All services closed, goodbye!",
|
|
172
178
|
shutdownError: "Shutdown error:",
|
|
@@ -301,11 +307,11 @@ function t(key, params) {
|
|
|
301
307
|
}
|
|
302
308
|
|
|
303
309
|
// src/server.ts
|
|
304
|
-
var
|
|
310
|
+
var import_uuid6 = require("uuid");
|
|
305
311
|
var import_promises4 = require("fs/promises");
|
|
306
|
-
var
|
|
307
|
-
var
|
|
308
|
-
var
|
|
312
|
+
var import_node_os7 = require("os");
|
|
313
|
+
var import_node_path6 = require("path");
|
|
314
|
+
var import_node_child_process8 = require("child_process");
|
|
309
315
|
var import_node_util = require("util");
|
|
310
316
|
|
|
311
317
|
// src/providers/ProcessProvider.ts
|
|
@@ -325,14 +331,14 @@ var import_node_os = require("os");
|
|
|
325
331
|
var import_node_child_process = require("child_process");
|
|
326
332
|
var isWindows = process.platform === "win32";
|
|
327
333
|
function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
|
|
328
|
-
return new Promise((
|
|
334
|
+
return new Promise((resolve2) => {
|
|
329
335
|
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
330
|
-
|
|
336
|
+
resolve2();
|
|
331
337
|
return;
|
|
332
338
|
}
|
|
333
339
|
const onExit = () => {
|
|
334
340
|
clearTimeout(timer);
|
|
335
|
-
|
|
341
|
+
resolve2();
|
|
336
342
|
};
|
|
337
343
|
proc.once("exit", onExit);
|
|
338
344
|
if (isWindows) {
|
|
@@ -348,7 +354,7 @@ function killProcessCrossPlatform(proc, timeoutMs = 3e3) {
|
|
|
348
354
|
proc.kill("SIGKILL");
|
|
349
355
|
}
|
|
350
356
|
}
|
|
351
|
-
|
|
357
|
+
resolve2();
|
|
352
358
|
}, timeoutMs);
|
|
353
359
|
});
|
|
354
360
|
}
|
|
@@ -650,7 +656,9 @@ var ProcessProvider = class {
|
|
|
650
656
|
const event = result.value;
|
|
651
657
|
if (event.type === "assistant") {
|
|
652
658
|
for (const block of event.message.content) {
|
|
653
|
-
if (block.type === "tool_use"
|
|
659
|
+
if (block.type === "tool_use") {
|
|
660
|
+
const isQuestion = block.name === "AskUserQuestion" || block.name === "AskFollowupQuestion";
|
|
661
|
+
if (!isQuestion) continue;
|
|
654
662
|
const input = block.input;
|
|
655
663
|
const question = input.question ?? "";
|
|
656
664
|
if (!question) continue;
|
|
@@ -662,6 +670,7 @@ var ProcessProvider = class {
|
|
|
662
670
|
}
|
|
663
671
|
if (sessionSet.has(prevKey)) continue;
|
|
664
672
|
sessionSet.add(prevKey);
|
|
673
|
+
console.log(`[ProcessProvider] Session ${sessionId}: detected ${block.name} (toolUseId=${block.id})`);
|
|
665
674
|
this.emitter.emit(this.getQuestionEventName(sessionId), {
|
|
666
675
|
toolUseId: block.id,
|
|
667
676
|
question,
|
|
@@ -777,7 +786,7 @@ var ProcessProvider = class {
|
|
|
777
786
|
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):
|
|
778
787
|
|
|
779
788
|
${context}`;
|
|
780
|
-
return new Promise((
|
|
789
|
+
return new Promise((resolve2, reject) => {
|
|
781
790
|
const env = { ...process.env };
|
|
782
791
|
delete env.CLAUDECODE;
|
|
783
792
|
const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
|
|
@@ -792,7 +801,7 @@ ${context}`;
|
|
|
792
801
|
});
|
|
793
802
|
proc.once("exit", (code) => {
|
|
794
803
|
if (code === 0) {
|
|
795
|
-
|
|
804
|
+
resolve2(output.trim());
|
|
796
805
|
} else {
|
|
797
806
|
reject(new Error(`generateSuggestion process exit code: ${code}`));
|
|
798
807
|
}
|
|
@@ -819,10 +828,10 @@ ${context}`;
|
|
|
819
828
|
tool_use_id: toolUseId,
|
|
820
829
|
content: answer
|
|
821
830
|
});
|
|
822
|
-
await new Promise((
|
|
831
|
+
await new Promise((resolve2, reject) => {
|
|
823
832
|
entry.process.stdin.write(toolResult + "\n", (err) => {
|
|
824
833
|
if (err) reject(err);
|
|
825
|
-
else
|
|
834
|
+
else resolve2();
|
|
826
835
|
});
|
|
827
836
|
});
|
|
828
837
|
console.log(`[ProcessProvider] Session ${sessionId}: AskUserQuestion answered (toolUseId=${toolUseId})`);
|
|
@@ -853,11 +862,643 @@ ${context}`;
|
|
|
853
862
|
}
|
|
854
863
|
};
|
|
855
864
|
|
|
856
|
-
// src/
|
|
865
|
+
// src/providers/CodexProvider.ts
|
|
866
|
+
var import_child_process2 = require("child_process");
|
|
867
|
+
var import_readline2 = require("readline");
|
|
868
|
+
var import_events2 = require("events");
|
|
869
|
+
var import_fs = require("fs");
|
|
870
|
+
var import_path = require("path");
|
|
871
|
+
var import_os = require("os");
|
|
857
872
|
var import_uuid2 = require("uuid");
|
|
873
|
+
|
|
874
|
+
// src/utils/codexPath.ts
|
|
875
|
+
var import_node_child_process3 = require("child_process");
|
|
876
|
+
var import_node_fs2 = require("fs");
|
|
877
|
+
var import_node_path2 = require("path");
|
|
878
|
+
var import_node_os3 = require("os");
|
|
879
|
+
function findCodexPath() {
|
|
880
|
+
try {
|
|
881
|
+
const cmd = isWindows ? "where codex" : "which codex";
|
|
882
|
+
return (0, import_node_child_process3.execSync)(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
|
|
883
|
+
} catch {
|
|
884
|
+
}
|
|
885
|
+
const candidates = isWindows ? [
|
|
886
|
+
(0, import_node_path2.join)(process.env.LOCALAPPDATA ?? "", "Programs", "codex", "codex.exe"),
|
|
887
|
+
(0, import_node_path2.join)((0, import_node_os3.homedir)(), ".codex", "local", "codex.exe")
|
|
888
|
+
] : [
|
|
889
|
+
"/opt/homebrew/bin/codex",
|
|
890
|
+
"/usr/local/bin/codex",
|
|
891
|
+
(0, import_node_path2.join)((0, import_node_os3.homedir)(), ".local", "bin", "codex")
|
|
892
|
+
];
|
|
893
|
+
for (const candidate of candidates) {
|
|
894
|
+
try {
|
|
895
|
+
(0, import_node_fs2.accessSync)(candidate, import_node_fs2.constants.X_OK);
|
|
896
|
+
return candidate;
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return "codex";
|
|
901
|
+
}
|
|
902
|
+
function resolveCodexJsEntry(codexPath) {
|
|
903
|
+
try {
|
|
904
|
+
let realPath;
|
|
905
|
+
try {
|
|
906
|
+
realPath = (0, import_node_fs2.readlinkSync)(codexPath);
|
|
907
|
+
if (!realPath.startsWith("/")) {
|
|
908
|
+
realPath = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(codexPath), realPath);
|
|
909
|
+
}
|
|
910
|
+
} catch {
|
|
911
|
+
realPath = codexPath;
|
|
912
|
+
}
|
|
913
|
+
const head = (0, import_node_fs2.readFileSync)(realPath, { encoding: "utf-8", flag: "r" }).slice(0, 100);
|
|
914
|
+
if (head.startsWith("#!/usr/bin/env node") || head.startsWith("#!/usr/bin/node")) {
|
|
915
|
+
return realPath;
|
|
916
|
+
}
|
|
917
|
+
} catch {
|
|
918
|
+
}
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
var CODEX_PATH = findCodexPath();
|
|
922
|
+
var CODEX_JS_ENTRY = resolveCodexJsEntry(CODEX_PATH);
|
|
923
|
+
async function getCodexVersion() {
|
|
924
|
+
try {
|
|
925
|
+
const cmd = CODEX_JS_ENTRY ? `"${process.execPath}" "${CODEX_JS_ENTRY}" --version` : `"${CODEX_PATH}" --version`;
|
|
926
|
+
const output = (0, import_node_child_process3.execSync)(cmd, {
|
|
927
|
+
encoding: "utf-8",
|
|
928
|
+
timeout: 5e3
|
|
929
|
+
}).trim();
|
|
930
|
+
return output.replace(/^codex-cli\s+/, "");
|
|
931
|
+
} catch {
|
|
932
|
+
return void 0;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function isCodexAvailable() {
|
|
936
|
+
if (CODEX_JS_ENTRY) {
|
|
937
|
+
try {
|
|
938
|
+
(0, import_node_fs2.accessSync)(CODEX_JS_ENTRY, import_node_fs2.constants.R_OK);
|
|
939
|
+
return true;
|
|
940
|
+
} catch {
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
(0, import_node_fs2.accessSync)(CODEX_PATH, import_node_fs2.constants.X_OK);
|
|
946
|
+
return true;
|
|
947
|
+
} catch {
|
|
948
|
+
try {
|
|
949
|
+
(0, import_node_child_process3.execSync)(`"${CODEX_PATH}" --version`, { timeout: 5e3 });
|
|
950
|
+
return true;
|
|
951
|
+
} catch {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// src/providers/CodexProvider.ts
|
|
958
|
+
var SESSIX_DIR = (0, import_path.join)((0, import_os.homedir)(), ".sessix");
|
|
959
|
+
var CODEX_SESSIONS_FILE = (0, import_path.join)(SESSIX_DIR, "codex-sessions.json");
|
|
960
|
+
var CodexProvider = class {
|
|
961
|
+
activeSessions = /* @__PURE__ */ new Map();
|
|
962
|
+
emitter = new import_events2.EventEmitter();
|
|
963
|
+
/** 持久化的会话元数据(sessionId → metadata) */
|
|
964
|
+
persistedSessions = /* @__PURE__ */ new Map();
|
|
965
|
+
/** 自增 ID 计数器,用于生成 ClaudeStreamEvent 中的 message/block ID */
|
|
966
|
+
idCounter = 0;
|
|
967
|
+
constructor() {
|
|
968
|
+
this.loadPersistedSessions();
|
|
969
|
+
}
|
|
970
|
+
async startSession(opts) {
|
|
971
|
+
const { projectPath, message, sessionId: existingSessionId } = opts;
|
|
972
|
+
const sessionId = existingSessionId ?? (0, import_uuid2.v4)();
|
|
973
|
+
if (this.activeSessions.has(sessionId)) {
|
|
974
|
+
await this.killSession(sessionId);
|
|
975
|
+
}
|
|
976
|
+
const projectId = projectPath.split("/").filter(Boolean).pop() ?? "unknown";
|
|
977
|
+
const session = {
|
|
978
|
+
id: sessionId,
|
|
979
|
+
projectId,
|
|
980
|
+
projectPath,
|
|
981
|
+
status: "running",
|
|
982
|
+
createdAt: Date.now(),
|
|
983
|
+
lastActiveAt: Date.now(),
|
|
984
|
+
summary: message.slice(0, 80),
|
|
985
|
+
agentType: "codex"
|
|
986
|
+
};
|
|
987
|
+
const resume = opts.resume ?? !!existingSessionId;
|
|
988
|
+
let resumeThreadId;
|
|
989
|
+
if (resume && existingSessionId) {
|
|
990
|
+
const persisted = this.persistedSessions.get(existingSessionId);
|
|
991
|
+
if (persisted?.threadId) {
|
|
992
|
+
resumeThreadId = persisted.threadId;
|
|
993
|
+
console.log(`[CodexProvider] Resuming session ${sessionId} with threadId: ${resumeThreadId}`);
|
|
994
|
+
} else {
|
|
995
|
+
console.warn(`[CodexProvider] Session ${sessionId} resume requested but no persisted threadId found, creating new session`);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const proc = this.spawnCodexProcess(projectPath, message, resumeThreadId, opts.model);
|
|
999
|
+
session.pid = proc.pid;
|
|
1000
|
+
this.activeSessions.set(sessionId, {
|
|
1001
|
+
session,
|
|
1002
|
+
process: proc,
|
|
1003
|
+
threadId: resumeThreadId,
|
|
1004
|
+
turnStartTime: Date.now()
|
|
1005
|
+
});
|
|
1006
|
+
const initEvent = {
|
|
1007
|
+
type: "system",
|
|
1008
|
+
subtype: "init",
|
|
1009
|
+
session_id: sessionId
|
|
1010
|
+
};
|
|
1011
|
+
this.emitter.emit(this.getEventName(sessionId), initEvent);
|
|
1012
|
+
proc.on("error", (err) => {
|
|
1013
|
+
console.error(`[CodexProvider] Session ${sessionId} process error:`, err.message);
|
|
1014
|
+
this.activeSessions.delete(sessionId);
|
|
1015
|
+
this.emitError(sessionId, `Process spawn failed: ${err.message}`);
|
|
1016
|
+
});
|
|
1017
|
+
this.attachStdoutListener(sessionId, proc);
|
|
1018
|
+
this.attachStderrListener(sessionId, proc);
|
|
1019
|
+
this.attachExitListener(sessionId, proc);
|
|
1020
|
+
return session;
|
|
1021
|
+
}
|
|
1022
|
+
async killSession(sessionId) {
|
|
1023
|
+
const entry = this.activeSessions.get(sessionId);
|
|
1024
|
+
if (!entry) return;
|
|
1025
|
+
if (entry.process.exitCode === null && entry.process.signalCode === null) {
|
|
1026
|
+
try {
|
|
1027
|
+
entry.process.stdin?.end();
|
|
1028
|
+
} catch {
|
|
1029
|
+
}
|
|
1030
|
+
await killProcessCrossPlatform(entry.process);
|
|
1031
|
+
}
|
|
1032
|
+
this.activeSessions.delete(sessionId);
|
|
1033
|
+
}
|
|
1034
|
+
async sendMessage(sessionId, message) {
|
|
1035
|
+
let entry = this.activeSessions.get(sessionId);
|
|
1036
|
+
if (!entry) {
|
|
1037
|
+
const persisted = this.persistedSessions.get(sessionId);
|
|
1038
|
+
if (!persisted?.threadId) {
|
|
1039
|
+
throw new Error(`Session ${sessionId} not found or already ended`);
|
|
1040
|
+
}
|
|
1041
|
+
const projectId = persisted.projectPath.split("/").filter(Boolean).pop() ?? "unknown";
|
|
1042
|
+
const session = {
|
|
1043
|
+
id: sessionId,
|
|
1044
|
+
projectId,
|
|
1045
|
+
projectPath: persisted.projectPath,
|
|
1046
|
+
status: "running",
|
|
1047
|
+
createdAt: persisted.createdAt,
|
|
1048
|
+
lastActiveAt: Date.now(),
|
|
1049
|
+
summary: persisted.summary,
|
|
1050
|
+
agentType: "codex"
|
|
1051
|
+
};
|
|
1052
|
+
const placeholderProc = (0, import_child_process2.spawn)("true", [], { stdio: "ignore" });
|
|
1053
|
+
entry = {
|
|
1054
|
+
session,
|
|
1055
|
+
process: placeholderProc,
|
|
1056
|
+
threadId: persisted.threadId,
|
|
1057
|
+
turnStartTime: Date.now()
|
|
1058
|
+
};
|
|
1059
|
+
this.activeSessions.set(sessionId, entry);
|
|
1060
|
+
}
|
|
1061
|
+
const procAlive = entry.process.exitCode === null && entry.process.signalCode === null;
|
|
1062
|
+
if (procAlive) {
|
|
1063
|
+
try {
|
|
1064
|
+
entry.process.stdin?.end();
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
1067
|
+
await killProcessCrossPlatform(entry.process);
|
|
1068
|
+
}
|
|
1069
|
+
const threadId = entry.threadId ?? this.persistedSessions.get(sessionId)?.threadId;
|
|
1070
|
+
if (!threadId) {
|
|
1071
|
+
console.warn(`[CodexProvider] Session ${sessionId} has no thread ID, falling back to new thread`);
|
|
1072
|
+
}
|
|
1073
|
+
const proc = this.spawnCodexProcess(entry.session.projectPath, message, threadId ?? void 0);
|
|
1074
|
+
entry.session.status = "running";
|
|
1075
|
+
entry.session.lastActiveAt = Date.now();
|
|
1076
|
+
entry.session.pid = proc.pid;
|
|
1077
|
+
entry.process = proc;
|
|
1078
|
+
entry.turnStartTime = Date.now();
|
|
1079
|
+
proc.on("error", (err) => {
|
|
1080
|
+
console.error(`[CodexProvider] Session ${sessionId} sendMessage process error:`, err.message);
|
|
1081
|
+
this.activeSessions.delete(sessionId);
|
|
1082
|
+
this.emitError(sessionId, `Failed to send message: ${err.message}`);
|
|
1083
|
+
});
|
|
1084
|
+
this.attachStdoutListener(sessionId, proc);
|
|
1085
|
+
this.attachStderrListener(sessionId, proc);
|
|
1086
|
+
this.attachExitListener(sessionId, proc);
|
|
1087
|
+
}
|
|
1088
|
+
onEvent(sessionId, callback) {
|
|
1089
|
+
const eventName = this.getEventName(sessionId);
|
|
1090
|
+
this.emitter.on(eventName, callback);
|
|
1091
|
+
return () => {
|
|
1092
|
+
this.emitter.off(eventName, callback);
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
getActiveSessions() {
|
|
1096
|
+
return Array.from(this.activeSessions.values()).map((e) => e.session);
|
|
1097
|
+
}
|
|
1098
|
+
async generateSuggestion(_context) {
|
|
1099
|
+
return "";
|
|
1100
|
+
}
|
|
1101
|
+
async answerQuestion(_sessionId, _toolUseId, _answer) {
|
|
1102
|
+
}
|
|
1103
|
+
onQuestion(_sessionId, _callback) {
|
|
1104
|
+
return () => {
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
// ============================================
|
|
1108
|
+
// 私有方法
|
|
1109
|
+
// ============================================
|
|
1110
|
+
nextId() {
|
|
1111
|
+
return `codex_${++this.idCounter}`;
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* 启动 codex CLI 进程
|
|
1115
|
+
*
|
|
1116
|
+
* @param projectPath 工作目录
|
|
1117
|
+
* @param message 用户消息
|
|
1118
|
+
* @param resumeThreadId 如果提供,则使用 `codex exec resume` 恢复会话
|
|
1119
|
+
*/
|
|
1120
|
+
spawnCodexProcess(projectPath, message, resumeThreadId, model) {
|
|
1121
|
+
const args = ["exec", "--json", "--full-auto"];
|
|
1122
|
+
if (model) {
|
|
1123
|
+
args.push("-m", model);
|
|
1124
|
+
}
|
|
1125
|
+
args.push("-C", projectPath);
|
|
1126
|
+
if (resumeThreadId) {
|
|
1127
|
+
args.push("resume", resumeThreadId);
|
|
1128
|
+
}
|
|
1129
|
+
args.push(message);
|
|
1130
|
+
const env = { ...process.env };
|
|
1131
|
+
let cmd;
|
|
1132
|
+
let spawnArgs;
|
|
1133
|
+
if (CODEX_JS_ENTRY) {
|
|
1134
|
+
cmd = process.execPath;
|
|
1135
|
+
spawnArgs = [CODEX_JS_ENTRY, ...args];
|
|
1136
|
+
console.log(`[CodexProvider] Spawning via node: ${cmd} ${CODEX_JS_ENTRY} ${args.join(" ")}`);
|
|
1137
|
+
} else {
|
|
1138
|
+
cmd = CODEX_PATH;
|
|
1139
|
+
spawnArgs = args;
|
|
1140
|
+
console.log(`[CodexProvider] Spawning: ${CODEX_PATH} ${args.join(" ")}`);
|
|
1141
|
+
}
|
|
1142
|
+
const proc = (0, import_child_process2.spawn)(cmd, spawnArgs, {
|
|
1143
|
+
cwd: projectPath,
|
|
1144
|
+
env,
|
|
1145
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1146
|
+
});
|
|
1147
|
+
try {
|
|
1148
|
+
proc.stdin?.end();
|
|
1149
|
+
} catch {
|
|
1150
|
+
}
|
|
1151
|
+
return proc;
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* 挂载 stdout 监听器,逐行解析 Codex NDJSON 并转换为 ClaudeStreamEvent
|
|
1155
|
+
*/
|
|
1156
|
+
attachStdoutListener(sessionId, proc) {
|
|
1157
|
+
if (!proc.stdout) return;
|
|
1158
|
+
const rl = (0, import_readline2.createInterface)({ input: proc.stdout, crlfDelay: Infinity });
|
|
1159
|
+
const entry = this.activeSessions.get(sessionId);
|
|
1160
|
+
if (entry) entry.rl = rl;
|
|
1161
|
+
rl.on("line", (line) => {
|
|
1162
|
+
const trimmed = line.trim();
|
|
1163
|
+
if (!trimmed) return;
|
|
1164
|
+
console.log(`[CodexProvider] Session ${sessionId} stdout: ${trimmed.substring(0, 200)}`);
|
|
1165
|
+
let event;
|
|
1166
|
+
try {
|
|
1167
|
+
event = JSON.parse(trimmed);
|
|
1168
|
+
} catch {
|
|
1169
|
+
console.warn(`[CodexProvider] Session ${sessionId}: failed to parse: ${trimmed.substring(0, 100)}`);
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
this.handleCodexEvent(sessionId, event);
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* 处理单个 Codex NDJSON 事件,转换并发射 ClaudeStreamEvent
|
|
1177
|
+
*/
|
|
1178
|
+
handleCodexEvent(sessionId, event) {
|
|
1179
|
+
const entry = this.activeSessions.get(sessionId);
|
|
1180
|
+
if (!entry) return;
|
|
1181
|
+
entry.session.lastActiveAt = Date.now();
|
|
1182
|
+
switch (event.type) {
|
|
1183
|
+
case "thread.started": {
|
|
1184
|
+
if (event.thread_id) {
|
|
1185
|
+
entry.threadId = event.thread_id;
|
|
1186
|
+
this.persistSession(sessionId, {
|
|
1187
|
+
threadId: event.thread_id,
|
|
1188
|
+
projectPath: entry.session.projectPath,
|
|
1189
|
+
summary: entry.session.summary,
|
|
1190
|
+
createdAt: entry.session.createdAt,
|
|
1191
|
+
lastActiveAt: Date.now()
|
|
1192
|
+
});
|
|
1193
|
+
console.log(`[CodexProvider] Session ${sessionId} threadId persisted: ${event.thread_id}`);
|
|
1194
|
+
}
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
case "turn.started": {
|
|
1198
|
+
entry.session.status = "running";
|
|
1199
|
+
entry.turnStartTime = Date.now();
|
|
1200
|
+
break;
|
|
1201
|
+
}
|
|
1202
|
+
case "item.completed": {
|
|
1203
|
+
if (!event.item) break;
|
|
1204
|
+
this.handleItemCompleted(sessionId, event.item);
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
case "turn.completed": {
|
|
1208
|
+
entry.session.status = "idle";
|
|
1209
|
+
const duration = entry.turnStartTime ? Date.now() - entry.turnStartTime : 0;
|
|
1210
|
+
const resultEvent = {
|
|
1211
|
+
type: "result",
|
|
1212
|
+
subtype: "success",
|
|
1213
|
+
session_id: sessionId,
|
|
1214
|
+
is_error: false,
|
|
1215
|
+
result: "",
|
|
1216
|
+
duration_ms: duration,
|
|
1217
|
+
num_turns: 1,
|
|
1218
|
+
usage: event.usage
|
|
1219
|
+
};
|
|
1220
|
+
this.emitter.emit(this.getEventName(sessionId), resultEvent);
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
case "turn.failed": {
|
|
1224
|
+
entry.session.status = "error";
|
|
1225
|
+
const failEvent = {
|
|
1226
|
+
type: "result",
|
|
1227
|
+
subtype: "error",
|
|
1228
|
+
session_id: sessionId,
|
|
1229
|
+
is_error: true,
|
|
1230
|
+
result: event.error ?? "Turn failed",
|
|
1231
|
+
duration_ms: entry.turnStartTime ? Date.now() - entry.turnStartTime : 0,
|
|
1232
|
+
num_turns: 1
|
|
1233
|
+
};
|
|
1234
|
+
this.emitter.emit(this.getEventName(sessionId), failEvent);
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* 处理 item.completed 事件,按 item.type 转换为对应的 ClaudeStreamEvent
|
|
1241
|
+
*/
|
|
1242
|
+
handleItemCompleted(sessionId, item) {
|
|
1243
|
+
switch (item.type) {
|
|
1244
|
+
case "agent_message": {
|
|
1245
|
+
const msgEvent = {
|
|
1246
|
+
type: "assistant",
|
|
1247
|
+
session_id: sessionId,
|
|
1248
|
+
message: {
|
|
1249
|
+
id: item.id || this.nextId(),
|
|
1250
|
+
model: "codex",
|
|
1251
|
+
role: "assistant",
|
|
1252
|
+
content: [{ type: "text", text: item.text ?? "" }]
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
this.emitter.emit(this.getEventName(sessionId), msgEvent);
|
|
1256
|
+
break;
|
|
1257
|
+
}
|
|
1258
|
+
case "command_execution": {
|
|
1259
|
+
const toolUseId = item.id || this.nextId();
|
|
1260
|
+
const toolEvent = {
|
|
1261
|
+
type: "assistant",
|
|
1262
|
+
session_id: sessionId,
|
|
1263
|
+
message: {
|
|
1264
|
+
id: this.nextId(),
|
|
1265
|
+
model: "codex",
|
|
1266
|
+
role: "assistant",
|
|
1267
|
+
content: [{
|
|
1268
|
+
type: "tool_use",
|
|
1269
|
+
id: toolUseId,
|
|
1270
|
+
name: "Bash",
|
|
1271
|
+
input: { command: item.command ?? "" }
|
|
1272
|
+
}]
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
this.emitter.emit(this.getEventName(sessionId), toolEvent);
|
|
1276
|
+
const resultContent = item.aggregated_output ?? "";
|
|
1277
|
+
const isError = item.exit_code != null && item.exit_code !== 0;
|
|
1278
|
+
const toolResultEvent = {
|
|
1279
|
+
type: "user",
|
|
1280
|
+
session_id: sessionId,
|
|
1281
|
+
message: {
|
|
1282
|
+
role: "user",
|
|
1283
|
+
content: [{
|
|
1284
|
+
type: "tool_result",
|
|
1285
|
+
tool_use_id: toolUseId,
|
|
1286
|
+
content: resultContent,
|
|
1287
|
+
is_error: isError
|
|
1288
|
+
}]
|
|
1289
|
+
}
|
|
1290
|
+
};
|
|
1291
|
+
this.emitter.emit(this.getEventName(sessionId), toolResultEvent);
|
|
1292
|
+
break;
|
|
1293
|
+
}
|
|
1294
|
+
case "file_change": {
|
|
1295
|
+
const editToolUseId = item.id || this.nextId();
|
|
1296
|
+
const toolEvent = {
|
|
1297
|
+
type: "assistant",
|
|
1298
|
+
session_id: sessionId,
|
|
1299
|
+
message: {
|
|
1300
|
+
id: this.nextId(),
|
|
1301
|
+
model: "codex",
|
|
1302
|
+
role: "assistant",
|
|
1303
|
+
content: [{
|
|
1304
|
+
type: "tool_use",
|
|
1305
|
+
id: editToolUseId,
|
|
1306
|
+
name: "Edit",
|
|
1307
|
+
input: { file_path: item.file_path ?? "", diff: item.diff ?? "" }
|
|
1308
|
+
}]
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
this.emitter.emit(this.getEventName(sessionId), toolEvent);
|
|
1312
|
+
const toolResultEvent = {
|
|
1313
|
+
type: "user",
|
|
1314
|
+
session_id: sessionId,
|
|
1315
|
+
message: {
|
|
1316
|
+
role: "user",
|
|
1317
|
+
content: [{
|
|
1318
|
+
type: "tool_result",
|
|
1319
|
+
tool_use_id: editToolUseId,
|
|
1320
|
+
content: `File edited: ${item.file_path ?? "unknown"}`
|
|
1321
|
+
}]
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
this.emitter.emit(this.getEventName(sessionId), toolResultEvent);
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
case "reasoning": {
|
|
1328
|
+
const thinkEvent = {
|
|
1329
|
+
type: "assistant",
|
|
1330
|
+
session_id: sessionId,
|
|
1331
|
+
message: {
|
|
1332
|
+
id: item.id || this.nextId(),
|
|
1333
|
+
model: "codex",
|
|
1334
|
+
role: "assistant",
|
|
1335
|
+
content: [{ type: "thinking", thinking: item.text ?? "" }]
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
this.emitter.emit(this.getEventName(sessionId), thinkEvent);
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
attachStderrListener(sessionId, proc) {
|
|
1344
|
+
if (!proc.stderr) return;
|
|
1345
|
+
proc.stderr.on("data", (data) => {
|
|
1346
|
+
const text = data.toString().trim();
|
|
1347
|
+
if (text) {
|
|
1348
|
+
console.error(`[CodexProvider] Session ${sessionId} stderr: ${text}`);
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
attachExitListener(sessionId, proc) {
|
|
1353
|
+
proc.once("exit", (code, signal) => {
|
|
1354
|
+
const entry = this.activeSessions.get(sessionId);
|
|
1355
|
+
if (!entry) return;
|
|
1356
|
+
if (entry.process !== proc) return;
|
|
1357
|
+
console.log(`[CodexProvider] Session ${sessionId} process exited: code=${code} signal=${signal} threadId=${entry.threadId ?? "NONE"}`);
|
|
1358
|
+
if (entry.rl) {
|
|
1359
|
+
entry.rl.close();
|
|
1360
|
+
entry.rl = void 0;
|
|
1361
|
+
}
|
|
1362
|
+
entry.session.pid = void 0;
|
|
1363
|
+
entry.session.lastActiveAt = Date.now();
|
|
1364
|
+
const alreadyHasResult = entry.session.status === "idle" || entry.session.status === "error";
|
|
1365
|
+
if (alreadyHasResult) return;
|
|
1366
|
+
const isNormal = isNormalExit(code, signal);
|
|
1367
|
+
entry.session.status = isNormal ? "idle" : "error";
|
|
1368
|
+
if (!isNormal) {
|
|
1369
|
+
console.error(`[CodexProvider] Session ${sessionId}: process exited abnormally code=${code} signal=${signal}`);
|
|
1370
|
+
}
|
|
1371
|
+
const syntheticResult = {
|
|
1372
|
+
type: "result",
|
|
1373
|
+
subtype: isNormal ? "success" : "error",
|
|
1374
|
+
session_id: sessionId,
|
|
1375
|
+
is_error: !isNormal,
|
|
1376
|
+
result: isNormal ? "" : `Process exited code=${code} signal=${signal}`,
|
|
1377
|
+
duration_ms: 0,
|
|
1378
|
+
num_turns: 0
|
|
1379
|
+
};
|
|
1380
|
+
this.emitter.emit(this.getEventName(sessionId), syntheticResult);
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
emitError(sessionId, message) {
|
|
1384
|
+
const event = {
|
|
1385
|
+
type: "result",
|
|
1386
|
+
subtype: "error",
|
|
1387
|
+
result: message,
|
|
1388
|
+
session_id: sessionId,
|
|
1389
|
+
duration_ms: 0,
|
|
1390
|
+
is_error: true,
|
|
1391
|
+
num_turns: 0
|
|
1392
|
+
};
|
|
1393
|
+
this.emitter.emit(this.getEventName(sessionId), event);
|
|
1394
|
+
}
|
|
1395
|
+
getEventName(sessionId) {
|
|
1396
|
+
return `claude:${sessionId}`;
|
|
1397
|
+
}
|
|
1398
|
+
// ============================================
|
|
1399
|
+
// 持久化方法
|
|
1400
|
+
// ============================================
|
|
1401
|
+
/**
|
|
1402
|
+
* 从磁盘加载持久化的 Codex 会话元数据
|
|
1403
|
+
*/
|
|
1404
|
+
loadPersistedSessions() {
|
|
1405
|
+
try {
|
|
1406
|
+
if (!(0, import_fs.existsSync)(CODEX_SESSIONS_FILE)) return;
|
|
1407
|
+
const data = JSON.parse((0, import_fs.readFileSync)(CODEX_SESSIONS_FILE, "utf-8"));
|
|
1408
|
+
for (const [sessionId, meta] of Object.entries(data)) {
|
|
1409
|
+
this.persistedSessions.set(sessionId, meta);
|
|
1410
|
+
}
|
|
1411
|
+
console.log(`[CodexProvider] Loaded ${this.persistedSessions.size} persisted sessions`);
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
console.warn("[CodexProvider] Failed to load persisted sessions:", err);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* 持久化单个会话的元数据到磁盘
|
|
1418
|
+
*/
|
|
1419
|
+
persistSession(sessionId, meta) {
|
|
1420
|
+
this.persistedSessions.set(sessionId, meta);
|
|
1421
|
+
this.flushPersistedSessions();
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* 将所有持久化数据写入磁盘
|
|
1425
|
+
*/
|
|
1426
|
+
flushPersistedSessions() {
|
|
1427
|
+
try {
|
|
1428
|
+
if (!(0, import_fs.existsSync)(SESSIX_DIR)) {
|
|
1429
|
+
(0, import_fs.mkdirSync)(SESSIX_DIR, { recursive: true });
|
|
1430
|
+
}
|
|
1431
|
+
const data = {};
|
|
1432
|
+
for (const [sessionId, meta] of this.persistedSessions) {
|
|
1433
|
+
data[sessionId] = meta;
|
|
1434
|
+
}
|
|
1435
|
+
(0, import_fs.writeFileSync)(CODEX_SESSIONS_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
1436
|
+
} catch (err) {
|
|
1437
|
+
console.error("[CodexProvider] Failed to persist sessions:", err);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* 检查某个 sessionId 是否为已知的 Codex 会话(供 SessionManager 查询 agentType)
|
|
1442
|
+
*/
|
|
1443
|
+
isKnownSession(sessionId) {
|
|
1444
|
+
return this.activeSessions.has(sessionId) || this.persistedSessions.has(sessionId);
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
// src/providers/ProviderFactory.ts
|
|
1449
|
+
var import_node_child_process4 = require("child_process");
|
|
1450
|
+
var ProviderFactory = class {
|
|
1451
|
+
providers = /* @__PURE__ */ new Map();
|
|
1452
|
+
getProvider(agentType) {
|
|
1453
|
+
const existing = this.providers.get(agentType);
|
|
1454
|
+
if (existing) return existing;
|
|
1455
|
+
let provider;
|
|
1456
|
+
switch (agentType) {
|
|
1457
|
+
case "codex":
|
|
1458
|
+
provider = new CodexProvider();
|
|
1459
|
+
break;
|
|
1460
|
+
case "claude-code":
|
|
1461
|
+
default:
|
|
1462
|
+
provider = new ProcessProvider();
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
this.providers.set(agentType, provider);
|
|
1466
|
+
return provider;
|
|
1467
|
+
}
|
|
1468
|
+
async detectAgents() {
|
|
1469
|
+
const agents = [];
|
|
1470
|
+
try {
|
|
1471
|
+
const claudePath = findClaudePath();
|
|
1472
|
+
const claudeVersion = (0, import_node_child_process4.execSync)(`"${claudePath}" --version`, {
|
|
1473
|
+
encoding: "utf-8",
|
|
1474
|
+
timeout: 5e3
|
|
1475
|
+
}).trim();
|
|
1476
|
+
agents.push({
|
|
1477
|
+
type: "claude-code",
|
|
1478
|
+
name: "Claude Code",
|
|
1479
|
+
available: true,
|
|
1480
|
+
version: claudeVersion
|
|
1481
|
+
});
|
|
1482
|
+
} catch {
|
|
1483
|
+
agents.push({ type: "claude-code", name: "Claude Code", available: false });
|
|
1484
|
+
}
|
|
1485
|
+
if (isCodexAvailable()) {
|
|
1486
|
+
const version = await getCodexVersion();
|
|
1487
|
+
agents.push({ type: "codex", name: "Codex", available: true, version });
|
|
1488
|
+
} else {
|
|
1489
|
+
agents.push({ type: "codex", name: "Codex", available: false });
|
|
1490
|
+
}
|
|
1491
|
+
return agents;
|
|
1492
|
+
}
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
// src/session/SessionManager.ts
|
|
1496
|
+
var import_uuid3 = require("uuid");
|
|
858
1497
|
var BUFFER_MAX = 5e3;
|
|
859
1498
|
var SessionManager = class {
|
|
860
1499
|
provider;
|
|
1500
|
+
providerFactory;
|
|
1501
|
+
sessionAgentType = /* @__PURE__ */ new Map();
|
|
861
1502
|
/** 事件回调列表(事件会被转发到 WsBridge) */
|
|
862
1503
|
eventCallbacks = [];
|
|
863
1504
|
/** 每个会话的事件流取消订阅函数 */
|
|
@@ -883,8 +1524,26 @@ var SessionManager = class {
|
|
|
883
1524
|
bufferTruncated = /* @__PURE__ */ new Set();
|
|
884
1525
|
/** sessionId → projectPath 映射,用于截断时从 JSONL 补全历史 */
|
|
885
1526
|
sessionProjectPaths = /* @__PURE__ */ new Map();
|
|
886
|
-
constructor(
|
|
887
|
-
|
|
1527
|
+
constructor(providerOrFactory) {
|
|
1528
|
+
if (providerOrFactory instanceof ProviderFactory) {
|
|
1529
|
+
this.providerFactory = providerOrFactory;
|
|
1530
|
+
this.provider = providerOrFactory.getProvider("claude-code");
|
|
1531
|
+
} else {
|
|
1532
|
+
this.provider = providerOrFactory;
|
|
1533
|
+
this.providerFactory = null;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
getProviderForSession(sessionId) {
|
|
1537
|
+
if (!this.providerFactory) return this.provider;
|
|
1538
|
+
let agentType = this.sessionAgentType.get(sessionId);
|
|
1539
|
+
if (!agentType) {
|
|
1540
|
+
const codexProvider = this.providerFactory.getProvider("codex");
|
|
1541
|
+
if (codexProvider instanceof CodexProvider && codexProvider.isKnownSession(sessionId)) {
|
|
1542
|
+
agentType = "codex";
|
|
1543
|
+
this.sessionAgentType.set(sessionId, agentType);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return this.providerFactory.getProvider(agentType ?? "claude-code");
|
|
888
1547
|
}
|
|
889
1548
|
// ============================================
|
|
890
1549
|
// 公开 API
|
|
@@ -895,8 +1554,9 @@ var SessionManager = class {
|
|
|
895
1554
|
* 调用 provider.startSession(),订阅事件流,
|
|
896
1555
|
* 将 ClaudeStreamEvent 包装为 ServerEvent 转发。
|
|
897
1556
|
*/
|
|
898
|
-
async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images) {
|
|
899
|
-
const
|
|
1557
|
+
async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType = "claude-code") {
|
|
1558
|
+
const provider = this.providerFactory ? this.providerFactory.getProvider(agentType) : this.provider;
|
|
1559
|
+
const session = await provider.startSession({
|
|
900
1560
|
projectPath,
|
|
901
1561
|
message,
|
|
902
1562
|
sessionId: resumeSessionId ?? newSessionId,
|
|
@@ -906,6 +1566,7 @@ var SessionManager = class {
|
|
|
906
1566
|
effort,
|
|
907
1567
|
images
|
|
908
1568
|
});
|
|
1569
|
+
this.sessionAgentType.set(session.id, agentType);
|
|
909
1570
|
this.lastBroadcastStatus.set(session.id, session.status);
|
|
910
1571
|
this.sessionProjectPaths.set(session.id, projectPath);
|
|
911
1572
|
this.unsubscribeSession(session.id);
|
|
@@ -917,7 +1578,8 @@ var SessionManager = class {
|
|
|
917
1578
|
* 发送消息到已有会话
|
|
918
1579
|
*/
|
|
919
1580
|
async sendMessage(sessionId, message, permissionMode, images) {
|
|
920
|
-
|
|
1581
|
+
const provider = this.getProviderForSession(sessionId);
|
|
1582
|
+
await provider.sendMessage(sessionId, message, permissionMode, images);
|
|
921
1583
|
this.updateSessionStatus(sessionId, "running");
|
|
922
1584
|
console.log(`[SessionManager] Message sent to session: ${sessionId}`);
|
|
923
1585
|
}
|
|
@@ -937,7 +1599,9 @@ var SessionManager = class {
|
|
|
937
1599
|
clearTimeout(pending.timer);
|
|
938
1600
|
this.pendingAssistantEvents.delete(sessionId);
|
|
939
1601
|
}
|
|
940
|
-
|
|
1602
|
+
const provider = this.getProviderForSession(sessionId);
|
|
1603
|
+
await provider.killSession(sessionId);
|
|
1604
|
+
this.sessionAgentType.delete(sessionId);
|
|
941
1605
|
console.log(`[SessionManager] Session killed: ${sessionId}`);
|
|
942
1606
|
}
|
|
943
1607
|
/**
|
|
@@ -1016,7 +1680,11 @@ var SessionManager = class {
|
|
|
1016
1680
|
* 获取所有活跃会话(含服务器端统计)
|
|
1017
1681
|
*/
|
|
1018
1682
|
getActiveSessions() {
|
|
1019
|
-
|
|
1683
|
+
const rawSessions = this.providerFactory ? [
|
|
1684
|
+
...this.providerFactory.getProvider("claude-code").getActiveSessions(),
|
|
1685
|
+
...this.providerFactory.getProvider("codex").getActiveSessions()
|
|
1686
|
+
] : this.provider.getActiveSessions();
|
|
1687
|
+
return rawSessions.map((session) => {
|
|
1020
1688
|
const stats = this.getSessionStats(session.id);
|
|
1021
1689
|
return stats ? { ...session, stats } : session;
|
|
1022
1690
|
});
|
|
@@ -1046,6 +1714,7 @@ var SessionManager = class {
|
|
|
1046
1714
|
this.sessionEventBuffers.clear();
|
|
1047
1715
|
this.bufferTruncated.clear();
|
|
1048
1716
|
this.sessionProjectPaths.clear();
|
|
1717
|
+
this.sessionAgentType.clear();
|
|
1049
1718
|
this.sessionStats.clear();
|
|
1050
1719
|
for (const [, pending] of this.pendingAssistantEvents) {
|
|
1051
1720
|
clearTimeout(pending.timer);
|
|
@@ -1063,10 +1732,11 @@ var SessionManager = class {
|
|
|
1063
1732
|
* 订阅指定会话的事件流(包括 AskUserQuestion 问题事件)
|
|
1064
1733
|
*/
|
|
1065
1734
|
subscribeToSession(sessionId) {
|
|
1066
|
-
const
|
|
1735
|
+
const provider = this.getProviderForSession(sessionId);
|
|
1736
|
+
const unsubscribeEvent = provider.onEvent(sessionId, (event) => {
|
|
1067
1737
|
this.handleClaudeEvent(sessionId, event);
|
|
1068
1738
|
});
|
|
1069
|
-
const unsubscribeQuestion =
|
|
1739
|
+
const unsubscribeQuestion = provider.onQuestion(
|
|
1070
1740
|
sessionId,
|
|
1071
1741
|
({ toolUseId, question, options }) => {
|
|
1072
1742
|
this.handleAskUserQuestion(sessionId, toolUseId, question, options);
|
|
@@ -1241,7 +1911,7 @@ var SessionManager = class {
|
|
|
1241
1911
|
console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
|
|
1242
1912
|
return;
|
|
1243
1913
|
}
|
|
1244
|
-
const requestId = (0,
|
|
1914
|
+
const requestId = (0, import_uuid3.v4)();
|
|
1245
1915
|
const request = {
|
|
1246
1916
|
id: requestId,
|
|
1247
1917
|
sessionId,
|
|
@@ -1252,12 +1922,13 @@ var SessionManager = class {
|
|
|
1252
1922
|
};
|
|
1253
1923
|
this.updateSessionStatus(sessionId, "waiting_question");
|
|
1254
1924
|
this.emit({ type: "question_request", request });
|
|
1255
|
-
const answerPromise = new Promise((
|
|
1256
|
-
this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve });
|
|
1925
|
+
const answerPromise = new Promise((resolve2) => {
|
|
1926
|
+
this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve: resolve2 });
|
|
1257
1927
|
});
|
|
1258
1928
|
answerPromise.then(async (answer) => {
|
|
1259
1929
|
try {
|
|
1260
|
-
|
|
1930
|
+
const provider = this.getProviderForSession(sessionId);
|
|
1931
|
+
await provider.answerQuestion(sessionId, toolUseId, answer);
|
|
1261
1932
|
} catch (err) {
|
|
1262
1933
|
console.error(`[SessionManager] answerQuestion failed (${sessionId}):`, err);
|
|
1263
1934
|
}
|
|
@@ -1494,11 +2165,11 @@ var WsBridge = class _WsBridge {
|
|
|
1494
2165
|
* 使用此方法代替 new WsBridge(),确保 EADDRINUSE 等错误能被调用方的 try-catch 捕获。
|
|
1495
2166
|
*/
|
|
1496
2167
|
static async create(options) {
|
|
1497
|
-
return new Promise((
|
|
2168
|
+
return new Promise((resolve2, reject) => {
|
|
1498
2169
|
const bridge = new _WsBridge(options);
|
|
1499
2170
|
bridge.wss.once("listening", () => {
|
|
1500
2171
|
bridge.wss.on("error", (err) => console.error(`[WsBridge] ${t("ws.serverError")}:`, err));
|
|
1501
|
-
|
|
2172
|
+
resolve2(bridge);
|
|
1502
2173
|
});
|
|
1503
2174
|
bridge.wss.once("error", reject);
|
|
1504
2175
|
});
|
|
@@ -1561,7 +2232,7 @@ var WsBridge = class _WsBridge {
|
|
|
1561
2232
|
}
|
|
1562
2233
|
/** 优雅关闭 WebSocket 服务 */
|
|
1563
2234
|
close() {
|
|
1564
|
-
return new Promise((
|
|
2235
|
+
return new Promise((resolve2, reject) => {
|
|
1565
2236
|
if (this.heartbeatTimer) {
|
|
1566
2237
|
clearInterval(this.heartbeatTimer);
|
|
1567
2238
|
this.heartbeatTimer = null;
|
|
@@ -1574,7 +2245,7 @@ var WsBridge = class _WsBridge {
|
|
|
1574
2245
|
reject(err);
|
|
1575
2246
|
} else {
|
|
1576
2247
|
console.log("[WsBridge] WebSocket server closed");
|
|
1577
|
-
|
|
2248
|
+
resolve2();
|
|
1578
2249
|
}
|
|
1579
2250
|
});
|
|
1580
2251
|
});
|
|
@@ -1680,15 +2351,15 @@ var WsBridge = class _WsBridge {
|
|
|
1680
2351
|
|
|
1681
2352
|
// src/approval/ApprovalProxy.ts
|
|
1682
2353
|
var import_node_http = __toESM(require("http"));
|
|
1683
|
-
var
|
|
1684
|
-
var
|
|
1685
|
-
var
|
|
1686
|
-
var
|
|
2354
|
+
var import_node_fs3 = __toESM(require("fs"));
|
|
2355
|
+
var import_node_path3 = __toESM(require("path"));
|
|
2356
|
+
var import_node_os4 = __toESM(require("os"));
|
|
2357
|
+
var import_uuid4 = require("uuid");
|
|
1687
2358
|
var ApprovalProxy = class _ApprovalProxy {
|
|
1688
2359
|
server;
|
|
1689
2360
|
token;
|
|
1690
2361
|
port;
|
|
1691
|
-
settingsPath =
|
|
2362
|
+
settingsPath = import_node_path3.default.join(import_node_os4.default.homedir(), ".claude", "settings.json");
|
|
1692
2363
|
/** 待处理的审批请求:requestId -> { resolve, timer, request } */
|
|
1693
2364
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
1694
2365
|
/** 审批请求回调(通知外部推送到手机) */
|
|
@@ -1714,11 +2385,11 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1714
2385
|
* 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
|
|
1715
2386
|
*/
|
|
1716
2387
|
static async create(options) {
|
|
1717
|
-
return new Promise((
|
|
2388
|
+
return new Promise((resolve2, reject) => {
|
|
1718
2389
|
const proxy = new _ApprovalProxy(options);
|
|
1719
2390
|
proxy.server.once("listening", () => {
|
|
1720
2391
|
proxy.server.on("error", (err) => console.error(`[ApprovalProxy] ${t("approval.serverError")}:`, err));
|
|
1721
|
-
|
|
2392
|
+
resolve2(proxy);
|
|
1722
2393
|
});
|
|
1723
2394
|
proxy.server.once("error", reject);
|
|
1724
2395
|
});
|
|
@@ -1793,7 +2464,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1793
2464
|
isToolInClaudeSettings(toolName, projectPath) {
|
|
1794
2465
|
const checkPath = (filepath) => {
|
|
1795
2466
|
try {
|
|
1796
|
-
const raw =
|
|
2467
|
+
const raw = import_node_fs3.default.readFileSync(filepath, "utf-8");
|
|
1797
2468
|
const settings = JSON.parse(raw);
|
|
1798
2469
|
const allow = settings?.permissions?.allow ?? [];
|
|
1799
2470
|
return allow.some((entry) => {
|
|
@@ -1807,24 +2478,24 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1807
2478
|
}
|
|
1808
2479
|
};
|
|
1809
2480
|
if (projectPath) {
|
|
1810
|
-
const projectSettingsPath =
|
|
2481
|
+
const projectSettingsPath = import_node_path3.default.join(projectPath, ".claude", "settings.json");
|
|
1811
2482
|
if (checkPath(projectSettingsPath)) return true;
|
|
1812
2483
|
}
|
|
1813
2484
|
return checkPath(this.settingsPath);
|
|
1814
2485
|
}
|
|
1815
2486
|
/** 将工具写入 settings.json permissions.allow(项目级或全局) */
|
|
1816
2487
|
addToClaudeSettings(projectPath, toolName) {
|
|
1817
|
-
const targetPath = projectPath ?
|
|
2488
|
+
const targetPath = projectPath ? import_node_path3.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
|
|
1818
2489
|
try {
|
|
1819
2490
|
if (projectPath) {
|
|
1820
|
-
const dir =
|
|
1821
|
-
if (!
|
|
1822
|
-
|
|
2491
|
+
const dir = import_node_path3.default.dirname(targetPath);
|
|
2492
|
+
if (!import_node_fs3.default.existsSync(dir)) {
|
|
2493
|
+
import_node_fs3.default.mkdirSync(dir, { recursive: true });
|
|
1823
2494
|
}
|
|
1824
2495
|
}
|
|
1825
2496
|
let settings = {};
|
|
1826
2497
|
try {
|
|
1827
|
-
settings = JSON.parse(
|
|
2498
|
+
settings = JSON.parse(import_node_fs3.default.readFileSync(targetPath, "utf-8"));
|
|
1828
2499
|
} catch {
|
|
1829
2500
|
}
|
|
1830
2501
|
if (!settings.permissions) {
|
|
@@ -1838,7 +2509,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1838
2509
|
const entry = `${toolName}(*)`;
|
|
1839
2510
|
if (!allow.includes(entry)) {
|
|
1840
2511
|
allow.push(entry);
|
|
1841
|
-
|
|
2512
|
+
import_node_fs3.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
1842
2513
|
const label = projectPath ? `${projectPath}/.claude/settings.json` : "~/.claude/settings.json";
|
|
1843
2514
|
console.log(`[ApprovalProxy] ${t("approval.alwaysAllowWritten", { entry, label })}`);
|
|
1844
2515
|
}
|
|
@@ -1875,7 +2546,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1875
2546
|
}
|
|
1876
2547
|
/** 优雅关闭 HTTP 服务 */
|
|
1877
2548
|
close() {
|
|
1878
|
-
return new Promise((
|
|
2549
|
+
return new Promise((resolve2, reject) => {
|
|
1879
2550
|
const pendingEntries = Array.from(this.pendingApprovals.entries());
|
|
1880
2551
|
for (const [, pending] of pendingEntries) {
|
|
1881
2552
|
clearTimeout(pending.timer);
|
|
@@ -1887,7 +2558,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1887
2558
|
reject(err);
|
|
1888
2559
|
} else {
|
|
1889
2560
|
console.log(`[ApprovalProxy] ${t("approval.httpClosed")}`);
|
|
1890
|
-
|
|
2561
|
+
resolve2();
|
|
1891
2562
|
}
|
|
1892
2563
|
});
|
|
1893
2564
|
});
|
|
@@ -1933,7 +2604,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1933
2604
|
try {
|
|
1934
2605
|
const body = await this.parseJsonBody(req);
|
|
1935
2606
|
const payload = body.payload ?? body;
|
|
1936
|
-
const requestId = (0,
|
|
2607
|
+
const requestId = (0, import_uuid4.v4)();
|
|
1937
2608
|
const projectPath = String(body.projectPath ?? "unknown");
|
|
1938
2609
|
const toolName = String(payload.tool_name ?? body.tool_name ?? "unknown");
|
|
1939
2610
|
const toolInput = payload.tool_input ?? body.tool_input ?? {};
|
|
@@ -1958,13 +2629,13 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1958
2629
|
return;
|
|
1959
2630
|
}
|
|
1960
2631
|
this.notifyApprovalRequest(approvalRequest);
|
|
1961
|
-
const decision = await new Promise((
|
|
2632
|
+
const decision = await new Promise((resolve2) => {
|
|
1962
2633
|
const timer = setTimeout(() => {
|
|
1963
2634
|
console.log(`[ApprovalProxy] ${t("approval.timeout", { id: requestId })}`);
|
|
1964
2635
|
this.pendingApprovals.delete(requestId);
|
|
1965
|
-
|
|
2636
|
+
resolve2({ decision: "allow" });
|
|
1966
2637
|
}, 325e3);
|
|
1967
|
-
this.pendingApprovals.set(requestId, { resolve, timer, request: approvalRequest });
|
|
2638
|
+
this.pendingApprovals.set(requestId, { resolve: resolve2, timer, request: approvalRequest });
|
|
1968
2639
|
});
|
|
1969
2640
|
this.sendJson(res, 200, decision);
|
|
1970
2641
|
} catch (err) {
|
|
@@ -2025,7 +2696,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2025
2696
|
/** 手动解析请求的 JSON body(限制最大 1MB 防止滥用) */
|
|
2026
2697
|
parseJsonBody(req) {
|
|
2027
2698
|
const MAX_BODY_SIZE = 1024 * 1024;
|
|
2028
|
-
return new Promise((
|
|
2699
|
+
return new Promise((resolve2, reject) => {
|
|
2029
2700
|
const chunks = [];
|
|
2030
2701
|
let totalSize = 0;
|
|
2031
2702
|
let destroyed = false;
|
|
@@ -2043,7 +2714,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2043
2714
|
try {
|
|
2044
2715
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
2045
2716
|
const parsed = JSON.parse(raw);
|
|
2046
|
-
|
|
2717
|
+
resolve2(parsed);
|
|
2047
2718
|
} catch {
|
|
2048
2719
|
reject(new Error(t("approval.invalidJson")));
|
|
2049
2720
|
}
|
|
@@ -2065,8 +2736,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2065
2736
|
};
|
|
2066
2737
|
|
|
2067
2738
|
// src/mdns/MdnsService.ts
|
|
2068
|
-
var
|
|
2069
|
-
var
|
|
2739
|
+
var import_node_child_process5 = require("child_process");
|
|
2740
|
+
var import_node_os5 = require("os");
|
|
2070
2741
|
function buildTxtArgs(txt) {
|
|
2071
2742
|
return Object.entries(txt).map(([k, v]) => `${k}=${v}`);
|
|
2072
2743
|
}
|
|
@@ -2084,7 +2755,7 @@ var MdnsService = class {
|
|
|
2084
2755
|
this.httpPort = options.httpPort;
|
|
2085
2756
|
this.version = options.version ?? "0.1.0";
|
|
2086
2757
|
this.pairing = options.pairing ?? "closed";
|
|
2087
|
-
this.useDnsSd = (0,
|
|
2758
|
+
this.useDnsSd = (0, import_node_os5.platform)() === "darwin";
|
|
2088
2759
|
}
|
|
2089
2760
|
getTxt() {
|
|
2090
2761
|
return {
|
|
@@ -2117,7 +2788,7 @@ var MdnsService = class {
|
|
|
2117
2788
|
String(this.wsPort),
|
|
2118
2789
|
...buildTxtArgs(this.getTxt())
|
|
2119
2790
|
];
|
|
2120
|
-
this.proc = (0,
|
|
2791
|
+
this.proc = (0, import_node_child_process5.spawn)("dns-sd", args, { stdio: "ignore" });
|
|
2121
2792
|
this.proc.on("error", (err) => {
|
|
2122
2793
|
console.warn(`[MdnsService] dns-sd failed, falling back to bonjour-service: ${err.message}`);
|
|
2123
2794
|
this.proc = null;
|
|
@@ -2138,7 +2809,7 @@ var MdnsService = class {
|
|
|
2138
2809
|
return;
|
|
2139
2810
|
}
|
|
2140
2811
|
try {
|
|
2141
|
-
const {
|
|
2812
|
+
const { Bonjour } = await import("bonjour-service");
|
|
2142
2813
|
const { networkInterfaces } = await import("os");
|
|
2143
2814
|
const lanAddrs = getLanAddresses(networkInterfaces);
|
|
2144
2815
|
const opts = lanAddrs.length > 0 ? { interface: lanAddrs[0] } : {};
|
|
@@ -2229,12 +2900,12 @@ function getLanAddresses(networkInterfacesFn) {
|
|
|
2229
2900
|
|
|
2230
2901
|
// src/hooks/HookInstaller.ts
|
|
2231
2902
|
var import_promises2 = require("fs/promises");
|
|
2232
|
-
var
|
|
2233
|
-
var
|
|
2234
|
-
var SESSIX_HOOKS_DIR = (0,
|
|
2235
|
-
var HOOK_SCRIPT_PATH = (0,
|
|
2236
|
-
var PERMISSION_ACCEPT_PATH = (0,
|
|
2237
|
-
var CLAUDE_SETTINGS_PATH = (0,
|
|
2903
|
+
var import_node_path4 = require("path");
|
|
2904
|
+
var import_node_os6 = require("os");
|
|
2905
|
+
var SESSIX_HOOKS_DIR = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".sessix", "hooks");
|
|
2906
|
+
var HOOK_SCRIPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
|
|
2907
|
+
var PERMISSION_ACCEPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "permission-accept.js");
|
|
2908
|
+
var CLAUDE_SETTINGS_PATH = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude", "settings.json");
|
|
2238
2909
|
var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
|
|
2239
2910
|
var PERMISSION_ACCEPT_COMMAND = "node ~/.sessix/hooks/permission-accept.js";
|
|
2240
2911
|
var LEGACY_HOOK_COMMANDS = [
|
|
@@ -2408,7 +3079,7 @@ var HookInstaller = class {
|
|
|
2408
3079
|
* 写入 Claude Code settings.json
|
|
2409
3080
|
*/
|
|
2410
3081
|
async writeClaudeSettings(settings) {
|
|
2411
|
-
await (0, import_promises2.mkdir)((0,
|
|
3082
|
+
await (0, import_promises2.mkdir)((0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude"), { recursive: true });
|
|
2412
3083
|
await (0, import_promises2.writeFile)(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
2413
3084
|
}
|
|
2414
3085
|
/**
|
|
@@ -2435,7 +3106,7 @@ var HookInstaller = class {
|
|
|
2435
3106
|
};
|
|
2436
3107
|
|
|
2437
3108
|
// src/notification/NotificationService.ts
|
|
2438
|
-
var
|
|
3109
|
+
var import_node_path5 = require("path");
|
|
2439
3110
|
var NotificationService = class {
|
|
2440
3111
|
constructor(sessionManager, expoChannel = null) {
|
|
2441
3112
|
this.sessionManager = sessionManager;
|
|
@@ -2531,7 +3202,7 @@ var NotificationService = class {
|
|
|
2531
3202
|
const dangerLevel = this.getDangerLevel(request.toolName);
|
|
2532
3203
|
const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
|
|
2533
3204
|
const categoryId = isDangerous ? "APPROVAL_DANGEROUS" : "APPROVAL_NORMAL";
|
|
2534
|
-
const projectName = (0,
|
|
3205
|
+
const projectName = (0, import_node_path5.basename)(
|
|
2535
3206
|
this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId)?.projectPath ?? ""
|
|
2536
3207
|
);
|
|
2537
3208
|
const pushTitle = isDangerous ? `\u26A0\uFE0F ${title}` : title;
|
|
@@ -2587,7 +3258,7 @@ var NotificationService = class {
|
|
|
2587
3258
|
/** 从审批请求中提取操作目标的简短描述 */
|
|
2588
3259
|
extractTarget(request) {
|
|
2589
3260
|
const input = request.toolInput;
|
|
2590
|
-
if (input.file_path) return (0,
|
|
3261
|
+
if (input.file_path) return (0, import_node_path5.basename)(String(input.file_path));
|
|
2591
3262
|
if (input.command) return String(input.command).slice(0, 40);
|
|
2592
3263
|
return request.description.slice(0, 40);
|
|
2593
3264
|
}
|
|
@@ -2690,7 +3361,7 @@ var NotificationService = class {
|
|
|
2690
3361
|
getSessionTitle(sessionId) {
|
|
2691
3362
|
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
2692
3363
|
if (!session) return "Unknown";
|
|
2693
|
-
return session.summary ?? (0,
|
|
3364
|
+
return session.summary ?? (0, import_node_path5.basename)(session.projectPath);
|
|
2694
3365
|
}
|
|
2695
3366
|
/** 获取会话的 YOLO 模式状态 */
|
|
2696
3367
|
getYoloMode(sessionId) {
|
|
@@ -2699,7 +3370,7 @@ var NotificationService = class {
|
|
|
2699
3370
|
};
|
|
2700
3371
|
|
|
2701
3372
|
// src/notification/DesktopNotificationChannel.ts
|
|
2702
|
-
var
|
|
3373
|
+
var import_node_child_process6 = require("child_process");
|
|
2703
3374
|
var DesktopNotificationChannel = class {
|
|
2704
3375
|
isAvailable() {
|
|
2705
3376
|
return process.platform === "darwin";
|
|
@@ -2710,12 +3381,12 @@ var DesktopNotificationChannel = class {
|
|
|
2710
3381
|
const body = payload.body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2711
3382
|
const sound = payload.sound ?? "Ping";
|
|
2712
3383
|
const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
|
|
2713
|
-
return new Promise((
|
|
2714
|
-
(0,
|
|
3384
|
+
return new Promise((resolve2) => {
|
|
3385
|
+
(0, import_node_child_process6.execFile)("osascript", ["-e", script], (err) => {
|
|
2715
3386
|
if (err) {
|
|
2716
3387
|
console.warn("[DesktopNotificationChannel] Send notification failed:", err.message);
|
|
2717
3388
|
}
|
|
2718
|
-
|
|
3389
|
+
resolve2();
|
|
2719
3390
|
});
|
|
2720
3391
|
});
|
|
2721
3392
|
}
|
|
@@ -2915,7 +3586,7 @@ var ActivityPushChannel = class {
|
|
|
2915
3586
|
const topic = "com.kachun.sessix.push-type.liveactivity";
|
|
2916
3587
|
const jwt = this.getJWT();
|
|
2917
3588
|
const payloadStr = JSON.stringify(payload);
|
|
2918
|
-
return new Promise((
|
|
3589
|
+
return new Promise((resolve2, reject) => {
|
|
2919
3590
|
let client;
|
|
2920
3591
|
try {
|
|
2921
3592
|
client = this.getHttp2Client();
|
|
@@ -2943,7 +3614,7 @@ var ActivityPushChannel = class {
|
|
|
2943
3614
|
});
|
|
2944
3615
|
req.on("end", () => {
|
|
2945
3616
|
if (statusCode === 200) {
|
|
2946
|
-
|
|
3617
|
+
resolve2();
|
|
2947
3618
|
} else {
|
|
2948
3619
|
if (statusCode === 0) {
|
|
2949
3620
|
this.http2Client?.destroy();
|
|
@@ -2985,12 +3656,12 @@ var ActivityPushChannel = class {
|
|
|
2985
3656
|
|
|
2986
3657
|
// src/session/ProjectReader.ts
|
|
2987
3658
|
var import_promises3 = require("fs/promises");
|
|
2988
|
-
var
|
|
2989
|
-
var
|
|
2990
|
-
var
|
|
2991
|
-
var CLAUDE_PROJECTS_DIR = (0,
|
|
3659
|
+
var import_readline3 = require("readline");
|
|
3660
|
+
var import_path2 = require("path");
|
|
3661
|
+
var import_os2 = require("os");
|
|
3662
|
+
var CLAUDE_PROJECTS_DIR = (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "projects");
|
|
2992
3663
|
function getSessionFilePath(projectPath, sessionId) {
|
|
2993
|
-
return (0,
|
|
3664
|
+
return (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
|
|
2994
3665
|
}
|
|
2995
3666
|
async function getProjects() {
|
|
2996
3667
|
try {
|
|
@@ -3007,7 +3678,7 @@ async function getProjects() {
|
|
|
3007
3678
|
const encodedPath = entry.name;
|
|
3008
3679
|
const decodedPath = decodeDirName(encodedPath);
|
|
3009
3680
|
const name = decodedPath.split("/").filter(Boolean).pop() ?? encodedPath;
|
|
3010
|
-
const projectDir = (0,
|
|
3681
|
+
const projectDir = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
3011
3682
|
const { count: sessionCount, latestMtime } = await countJsonlFilesWithMtime(projectDir);
|
|
3012
3683
|
projects.push({
|
|
3013
3684
|
id: encodedPath,
|
|
@@ -3029,7 +3700,7 @@ async function getProjects() {
|
|
|
3029
3700
|
async function getHistoricalSessions(projectPath) {
|
|
3030
3701
|
try {
|
|
3031
3702
|
const encodedPath = encodeDirName(projectPath);
|
|
3032
|
-
const projectDir = (0,
|
|
3703
|
+
const projectDir = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath);
|
|
3033
3704
|
const dirExists = await directoryExists(projectDir);
|
|
3034
3705
|
if (!dirExists) {
|
|
3035
3706
|
return { ok: true, value: [] };
|
|
@@ -3040,7 +3711,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3040
3711
|
await Promise.all(
|
|
3041
3712
|
jsonlFiles.map(async (entry) => {
|
|
3042
3713
|
const sessionId = entry.name.slice(0, -6);
|
|
3043
|
-
const filePath = (0,
|
|
3714
|
+
const filePath = (0, import_path2.join)(projectDir, entry.name);
|
|
3044
3715
|
try {
|
|
3045
3716
|
const contentTs = await extractLastTimestamp(filePath);
|
|
3046
3717
|
if (contentTs) {
|
|
@@ -3059,13 +3730,13 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3059
3730
|
);
|
|
3060
3731
|
for (const entry of uuidDirs) {
|
|
3061
3732
|
try {
|
|
3062
|
-
const fileStat = await (0, import_promises3.stat)((0,
|
|
3733
|
+
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(projectDir, entry.name));
|
|
3063
3734
|
mtimeMap.set(entry.name, fileStat.mtimeMs);
|
|
3064
3735
|
} catch {
|
|
3065
3736
|
mtimeMap.set(entry.name, 0);
|
|
3066
3737
|
}
|
|
3067
3738
|
}
|
|
3068
|
-
const indexPath = (0,
|
|
3739
|
+
const indexPath = (0, import_path2.join)(projectDir, "sessions-index.json");
|
|
3069
3740
|
const sessionMap = /* @__PURE__ */ new Map();
|
|
3070
3741
|
try {
|
|
3071
3742
|
const indexContent = await (0, import_promises3.readFile)(indexPath, "utf-8");
|
|
@@ -3083,7 +3754,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3083
3754
|
}
|
|
3084
3755
|
await Promise.all(
|
|
3085
3756
|
Array.from(sessionMap.values()).filter((s) => (s.messageCount ?? 0) > 0 && !s.summary && !s.firstPrompt).map(async (s) => {
|
|
3086
|
-
const filePath = (0,
|
|
3757
|
+
const filePath = (0, import_path2.join)(projectDir, `${s.sessionId}.jsonl`);
|
|
3087
3758
|
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
3088
3759
|
if (firstPrompt) s.firstPrompt = firstPrompt;
|
|
3089
3760
|
})
|
|
@@ -3097,7 +3768,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3097
3768
|
if (uuidDirSet.has(sessionId)) {
|
|
3098
3769
|
sessionMap.set(sessionId, { sessionId, lastModified: mtime, messageCount: -1 });
|
|
3099
3770
|
} else {
|
|
3100
|
-
const filePath = (0,
|
|
3771
|
+
const filePath = (0, import_path2.join)(projectDir, `${sessionId}.jsonl`);
|
|
3101
3772
|
const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
|
|
3102
3773
|
sessionMap.set(sessionId, { sessionId, lastModified: mtime, firstPrompt });
|
|
3103
3774
|
}
|
|
@@ -3121,7 +3792,7 @@ async function getHistoricalSessions(projectPath) {
|
|
|
3121
3792
|
async function getSessionHistory(projectPath, sessionId) {
|
|
3122
3793
|
try {
|
|
3123
3794
|
const encodedPath = encodeDirName(projectPath);
|
|
3124
|
-
const filePath = (0,
|
|
3795
|
+
const filePath = (0, import_path2.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
|
|
3125
3796
|
const raw = await (0, import_promises3.readFile)(filePath, "utf-8").catch((err) => {
|
|
3126
3797
|
if (err.code === "ENOENT") return null;
|
|
3127
3798
|
throw err;
|
|
@@ -3234,7 +3905,7 @@ async function extractFirstPrompt(filePath) {
|
|
|
3234
3905
|
let fileHandle;
|
|
3235
3906
|
try {
|
|
3236
3907
|
fileHandle = await (0, import_promises3.open)(filePath, "r");
|
|
3237
|
-
const rl = (0,
|
|
3908
|
+
const rl = (0, import_readline3.createInterface)({
|
|
3238
3909
|
input: fileHandle.createReadStream({ encoding: "utf-8" }),
|
|
3239
3910
|
crlfDelay: Infinity
|
|
3240
3911
|
});
|
|
@@ -3302,15 +3973,15 @@ async function countJsonlFilesWithMtime(dirPath) {
|
|
|
3302
3973
|
await Promise.all([
|
|
3303
3974
|
...jsonlEntries.map(async (entry) => {
|
|
3304
3975
|
try {
|
|
3305
|
-
const contentTs = await extractLastTimestamp((0,
|
|
3306
|
-
const ts = contentTs ?? (await (0, import_promises3.stat)((0,
|
|
3976
|
+
const contentTs = await extractLastTimestamp((0, import_path2.join)(dirPath, entry.name));
|
|
3977
|
+
const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name))).mtimeMs;
|
|
3307
3978
|
if (ts > latestMtime) latestMtime = ts;
|
|
3308
3979
|
} catch {
|
|
3309
3980
|
}
|
|
3310
3981
|
}),
|
|
3311
3982
|
...uuidDirs.map(async (entry) => {
|
|
3312
3983
|
try {
|
|
3313
|
-
const fileStat = await (0, import_promises3.stat)((0,
|
|
3984
|
+
const fileStat = await (0, import_promises3.stat)((0, import_path2.join)(dirPath, entry.name));
|
|
3314
3985
|
if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
|
|
3315
3986
|
} catch {
|
|
3316
3987
|
}
|
|
@@ -3382,14 +4053,14 @@ var PairingManager = class {
|
|
|
3382
4053
|
};
|
|
3383
4054
|
|
|
3384
4055
|
// src/auth/AuthManager.ts
|
|
3385
|
-
var import_child_process2 = require("child_process");
|
|
3386
4056
|
var import_child_process3 = require("child_process");
|
|
4057
|
+
var import_child_process4 = require("child_process");
|
|
3387
4058
|
var import_util = require("util");
|
|
3388
|
-
var
|
|
3389
|
-
var execFileAsync = (0, import_util.promisify)(
|
|
4059
|
+
var import_events3 = require("events");
|
|
4060
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
|
|
3390
4061
|
var CLAUDE_PATH2 = findClaudePath();
|
|
3391
4062
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3392
|
-
var AuthManager = class extends
|
|
4063
|
+
var AuthManager = class extends import_events3.EventEmitter {
|
|
3393
4064
|
loginProcess = null;
|
|
3394
4065
|
loginTimeout = null;
|
|
3395
4066
|
urlSent = false;
|
|
@@ -3417,7 +4088,7 @@ var AuthManager = class extends import_events2.EventEmitter {
|
|
|
3417
4088
|
}
|
|
3418
4089
|
this.clearLoginTimeout();
|
|
3419
4090
|
this.urlSent = false;
|
|
3420
|
-
const proc = (0,
|
|
4091
|
+
const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
|
|
3421
4092
|
env: { ...process.env, BROWSER: "echo" },
|
|
3422
4093
|
stdio: ["pipe", "pipe", "pipe"]
|
|
3423
4094
|
});
|
|
@@ -3503,8 +4174,8 @@ var AuthManager = class extends import_events2.EventEmitter {
|
|
|
3503
4174
|
var import_promises5 = require("fs/promises");
|
|
3504
4175
|
|
|
3505
4176
|
// src/terminal/TerminalExecutor.ts
|
|
3506
|
-
var
|
|
3507
|
-
var
|
|
4177
|
+
var import_node_child_process7 = require("child_process");
|
|
4178
|
+
var import_uuid5 = require("uuid");
|
|
3508
4179
|
var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3509
4180
|
var TerminalExecutor = class {
|
|
3510
4181
|
processes = /* @__PURE__ */ new Map();
|
|
@@ -3526,10 +4197,10 @@ var TerminalExecutor = class {
|
|
|
3526
4197
|
}
|
|
3527
4198
|
}
|
|
3528
4199
|
exec(sessionId, command, cwd) {
|
|
3529
|
-
const execId = (0,
|
|
4200
|
+
const execId = (0, import_uuid5.v4)();
|
|
3530
4201
|
const shell = isWindows ? "powershell" : "bash";
|
|
3531
4202
|
const args = isWindows ? ["-Command", command] : ["-c", command];
|
|
3532
|
-
const proc = (0,
|
|
4203
|
+
const proc = (0, import_node_child_process7.spawn)(shell, args, {
|
|
3533
4204
|
cwd,
|
|
3534
4205
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3535
4206
|
env: { ...process.env }
|
|
@@ -3592,7 +4263,7 @@ var TerminalExecutor = class {
|
|
|
3592
4263
|
// src/server.ts
|
|
3593
4264
|
var WS_PORT = 3745;
|
|
3594
4265
|
var HTTP_PORT = 3746;
|
|
3595
|
-
var execAsync = (0, import_node_util.promisify)(
|
|
4266
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
|
|
3596
4267
|
async function killPortProcess(port) {
|
|
3597
4268
|
try {
|
|
3598
4269
|
if (isWindows) {
|
|
@@ -3616,7 +4287,7 @@ async function killPortProcess(port) {
|
|
|
3616
4287
|
await execAsync(`kill -9 ${pids.join(" ")}`);
|
|
3617
4288
|
}
|
|
3618
4289
|
}
|
|
3619
|
-
await new Promise((
|
|
4290
|
+
await new Promise((resolve2) => setTimeout(resolve2, 600));
|
|
3620
4291
|
} catch {
|
|
3621
4292
|
}
|
|
3622
4293
|
}
|
|
@@ -3634,8 +4305,8 @@ async function createWithRetry(label, port, factory) {
|
|
|
3634
4305
|
}
|
|
3635
4306
|
}
|
|
3636
4307
|
async function start(opts = {}) {
|
|
3637
|
-
const configDir = (0,
|
|
3638
|
-
const tokenFile = (0,
|
|
4308
|
+
const configDir = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix");
|
|
4309
|
+
const tokenFile = (0, import_node_path6.join)(configDir, "token");
|
|
3639
4310
|
let token;
|
|
3640
4311
|
if (opts.token !== void 0) {
|
|
3641
4312
|
token = opts.token;
|
|
@@ -3647,14 +4318,14 @@ async function start(opts = {}) {
|
|
|
3647
4318
|
try {
|
|
3648
4319
|
token = (await (0, import_promises4.readFile)(tokenFile, "utf8")).trim();
|
|
3649
4320
|
} catch {
|
|
3650
|
-
token = (0,
|
|
4321
|
+
token = (0, import_uuid6.v4)();
|
|
3651
4322
|
await (0, import_promises4.mkdir)(configDir, { recursive: true });
|
|
3652
4323
|
await (0, import_promises4.writeFile)(tokenFile, token, "utf8");
|
|
3653
4324
|
}
|
|
3654
4325
|
}
|
|
3655
4326
|
}
|
|
3656
|
-
const
|
|
3657
|
-
const sessionManager = new SessionManager(
|
|
4327
|
+
const providerFactory = new ProviderFactory();
|
|
4328
|
+
const sessionManager = new SessionManager(providerFactory);
|
|
3658
4329
|
const terminalExecutor = new TerminalExecutor();
|
|
3659
4330
|
const wsBridge = await createWithRetry(
|
|
3660
4331
|
"WsBridge",
|
|
@@ -3694,7 +4365,7 @@ async function start(opts = {}) {
|
|
|
3694
4365
|
let mdnsService = null;
|
|
3695
4366
|
const pairingManager = new PairingManager({
|
|
3696
4367
|
token,
|
|
3697
|
-
serverName: (0,
|
|
4368
|
+
serverName: (0, import_node_os7.hostname)(),
|
|
3698
4369
|
version: "0.2.0",
|
|
3699
4370
|
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
3700
4371
|
});
|
|
@@ -3751,7 +4422,8 @@ async function start(opts = {}) {
|
|
|
3751
4422
|
event.model,
|
|
3752
4423
|
event.permissionMode,
|
|
3753
4424
|
event.effort,
|
|
3754
|
-
event.images
|
|
4425
|
+
event.images,
|
|
4426
|
+
event.agentType
|
|
3755
4427
|
);
|
|
3756
4428
|
wsBridge.broadcast({
|
|
3757
4429
|
type: "session_list",
|
|
@@ -3927,7 +4599,7 @@ async function start(opts = {}) {
|
|
|
3927
4599
|
return null;
|
|
3928
4600
|
}).filter(Boolean).join("\n");
|
|
3929
4601
|
}
|
|
3930
|
-
const suggestion = await
|
|
4602
|
+
const suggestion = await providerFactory.getProvider("claude-code").generateSuggestion(context);
|
|
3931
4603
|
wsBridge.send(ws, {
|
|
3932
4604
|
type: "prompt_suggestion",
|
|
3933
4605
|
sessionId: event.sessionId,
|
|
@@ -4005,6 +4677,11 @@ async function start(opts = {}) {
|
|
|
4005
4677
|
}
|
|
4006
4678
|
break;
|
|
4007
4679
|
}
|
|
4680
|
+
case "list_agents": {
|
|
4681
|
+
const agents = await providerFactory.detectAgents();
|
|
4682
|
+
wsBridge.send(ws, { type: "agent_list", agents });
|
|
4683
|
+
break;
|
|
4684
|
+
}
|
|
4008
4685
|
default: {
|
|
4009
4686
|
wsBridge.send(ws, {
|
|
4010
4687
|
type: "error",
|
|
@@ -4168,7 +4845,7 @@ async function start(opts = {}) {
|
|
|
4168
4845
|
openPairing: (duration) => pairingManager.open(duration),
|
|
4169
4846
|
closePairing: () => pairingManager.close(),
|
|
4170
4847
|
regenerateToken: async () => {
|
|
4171
|
-
const newToken = (0,
|
|
4848
|
+
const newToken = (0, import_uuid6.v4)();
|
|
4172
4849
|
await (0, import_promises4.mkdir)(configDir, { recursive: true });
|
|
4173
4850
|
await (0, import_promises4.writeFile)(tokenFile, newToken, "utf8");
|
|
4174
4851
|
instance.token = newToken;
|