sessix-server 0.2.8 → 0.3.0

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.
Files changed (60) hide show
  1. package/dist/index.js +693 -105
  2. package/dist/server.js +660 -93
  3. package/package.json +10 -1
  4. package/dist/approval/ApprovalProxy.d.ts +0 -86
  5. package/dist/approval/ApprovalProxy.d.ts.map +0 -1
  6. package/dist/approval/ApprovalProxy.js +0 -363
  7. package/dist/approval/ApprovalProxy.js.map +0 -1
  8. package/dist/hooks/HookInstaller.d.ts +0 -55
  9. package/dist/hooks/HookInstaller.d.ts.map +0 -1
  10. package/dist/hooks/HookInstaller.js +0 -215
  11. package/dist/hooks/HookInstaller.js.map +0 -1
  12. package/dist/index.d.ts +0 -2
  13. package/dist/index.d.ts.map +0 -1
  14. package/dist/index.js.map +0 -1
  15. package/dist/mdns/MdnsService.d.ts +0 -36
  16. package/dist/mdns/MdnsService.d.ts.map +0 -1
  17. package/dist/mdns/MdnsService.js +0 -66
  18. package/dist/mdns/MdnsService.js.map +0 -1
  19. package/dist/notification/ActivityPushChannel.d.ts +0 -54
  20. package/dist/notification/ActivityPushChannel.d.ts.map +0 -1
  21. package/dist/notification/ActivityPushChannel.js +0 -235
  22. package/dist/notification/ActivityPushChannel.js.map +0 -1
  23. package/dist/notification/ExpoNotificationChannel.d.ts +0 -17
  24. package/dist/notification/ExpoNotificationChannel.d.ts.map +0 -1
  25. package/dist/notification/ExpoNotificationChannel.js +0 -57
  26. package/dist/notification/ExpoNotificationChannel.js.map +0 -1
  27. package/dist/notification/MacNotificationChannel.d.ts +0 -22
  28. package/dist/notification/MacNotificationChannel.d.ts.map +0 -1
  29. package/dist/notification/MacNotificationChannel.js +0 -33
  30. package/dist/notification/MacNotificationChannel.js.map +0 -1
  31. package/dist/notification/NotificationService.d.ts +0 -50
  32. package/dist/notification/NotificationService.d.ts.map +0 -1
  33. package/dist/notification/NotificationService.js +0 -177
  34. package/dist/notification/NotificationService.js.map +0 -1
  35. package/dist/providers/ExecutionProvider.d.ts +0 -60
  36. package/dist/providers/ExecutionProvider.d.ts.map +0 -1
  37. package/dist/providers/ExecutionProvider.js +0 -3
  38. package/dist/providers/ExecutionProvider.js.map +0 -1
  39. package/dist/providers/ProcessProvider.d.ts +0 -117
  40. package/dist/providers/ProcessProvider.d.ts.map +0 -1
  41. package/dist/providers/ProcessProvider.js +0 -507
  42. package/dist/providers/ProcessProvider.js.map +0 -1
  43. package/dist/server.d.ts.map +0 -1
  44. package/dist/server.js.map +0 -1
  45. package/dist/session/ProjectReader.d.ts +0 -44
  46. package/dist/session/ProjectReader.d.ts.map +0 -1
  47. package/dist/session/ProjectReader.js +0 -471
  48. package/dist/session/ProjectReader.js.map +0 -1
  49. package/dist/session/SessionFileWatcher.d.ts +0 -35
  50. package/dist/session/SessionFileWatcher.d.ts.map +0 -1
  51. package/dist/session/SessionFileWatcher.js +0 -207
  52. package/dist/session/SessionFileWatcher.js.map +0 -1
  53. package/dist/session/SessionManager.d.ts +0 -114
  54. package/dist/session/SessionManager.d.ts.map +0 -1
  55. package/dist/session/SessionManager.js +0 -356
  56. package/dist/session/SessionManager.js.map +0 -1
  57. package/dist/ws/WsBridge.d.ts +0 -55
  58. package/dist/ws/WsBridge.d.ts.map +0 -1
  59. package/dist/ws/WsBridge.js +0 -220
  60. package/dist/ws/WsBridge.js.map +0 -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 import_node_os7 = require("os");
28
- var import_node_fs3 = require("fs");
29
- var import_node_path6 = require("path");
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 import_uuid5 = require("uuid");
305
+ var import_uuid6 = require("uuid");
299
306
  var import_promises4 = require("fs/promises");
300
- var import_node_os6 = require("os");
301
- var import_node_path5 = require("path");
302
- var import_node_child_process6 = require("child_process");
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
@@ -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" && block.name === "AskUserQuestion") {
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,
@@ -847,11 +857,500 @@ ${context}`;
847
857
  }
848
858
  };
849
859
 
850
- // src/session/SessionManager.ts
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");
851
864
  var import_uuid2 = require("uuid");
865
+
866
+ // src/utils/codexPath.ts
867
+ var import_node_child_process3 = require("child_process");
868
+ var import_node_fs2 = require("fs");
869
+ var import_node_path2 = require("path");
870
+ var import_node_os3 = require("os");
871
+ function findCodexPath() {
872
+ try {
873
+ const cmd = isWindows ? "where codex" : "which codex";
874
+ return (0, import_node_child_process3.execSync)(cmd, { encoding: "utf-8" }).trim().split("\n")[0];
875
+ } catch {
876
+ }
877
+ const candidates = isWindows ? [
878
+ (0, import_node_path2.join)(process.env.LOCALAPPDATA ?? "", "Programs", "codex", "codex.exe"),
879
+ (0, import_node_path2.join)((0, import_node_os3.homedir)(), ".codex", "local", "codex.exe")
880
+ ] : [
881
+ "/opt/homebrew/bin/codex",
882
+ "/usr/local/bin/codex",
883
+ (0, import_node_path2.join)((0, import_node_os3.homedir)(), ".local", "bin", "codex")
884
+ ];
885
+ for (const candidate of candidates) {
886
+ try {
887
+ (0, import_node_fs2.accessSync)(candidate, import_node_fs2.constants.X_OK);
888
+ return candidate;
889
+ } catch {
890
+ }
891
+ }
892
+ return "codex";
893
+ }
894
+ var CODEX_PATH = findCodexPath();
895
+ async function getCodexVersion() {
896
+ try {
897
+ const output = (0, import_node_child_process3.execSync)(`"${CODEX_PATH}" --version`, {
898
+ encoding: "utf-8",
899
+ timeout: 5e3
900
+ }).trim();
901
+ return output.replace(/^codex-cli\s+/, "");
902
+ } catch {
903
+ return void 0;
904
+ }
905
+ }
906
+ function isCodexAvailable() {
907
+ try {
908
+ (0, import_node_fs2.accessSync)(CODEX_PATH, import_node_fs2.constants.X_OK);
909
+ return true;
910
+ } catch {
911
+ try {
912
+ (0, import_node_child_process3.execSync)(`"${CODEX_PATH}" --version`, { timeout: 5e3 });
913
+ return true;
914
+ } catch {
915
+ return false;
916
+ }
917
+ }
918
+ }
919
+
920
+ // src/providers/CodexProvider.ts
921
+ var CodexProvider = class {
922
+ activeSessions = /* @__PURE__ */ new Map();
923
+ emitter = new import_events2.EventEmitter();
924
+ /** 自增 ID 计数器,用于生成 ClaudeStreamEvent 中的 message/block ID */
925
+ idCounter = 0;
926
+ async startSession(opts) {
927
+ const { projectPath, message, sessionId: existingSessionId } = opts;
928
+ const sessionId = existingSessionId ?? (0, import_uuid2.v4)();
929
+ if (this.activeSessions.has(sessionId)) {
930
+ await this.killSession(sessionId);
931
+ }
932
+ const projectId = projectPath.split("/").filter(Boolean).pop() ?? "unknown";
933
+ const session = {
934
+ id: sessionId,
935
+ projectId,
936
+ projectPath,
937
+ status: "running",
938
+ createdAt: Date.now(),
939
+ lastActiveAt: Date.now(),
940
+ summary: message.slice(0, 80),
941
+ agentType: "codex"
942
+ };
943
+ const resume = opts.resume ?? !!existingSessionId;
944
+ const proc = this.spawnCodexProcess(projectPath, message, void 0);
945
+ session.pid = proc.pid;
946
+ this.activeSessions.set(sessionId, {
947
+ session,
948
+ process: proc,
949
+ threadId: void 0,
950
+ turnStartTime: Date.now()
951
+ });
952
+ const initEvent = {
953
+ type: "system",
954
+ subtype: "init",
955
+ session_id: sessionId
956
+ };
957
+ this.emitter.emit(this.getEventName(sessionId), initEvent);
958
+ proc.on("error", (err) => {
959
+ console.error(`[CodexProvider] Session ${sessionId} process error:`, err.message);
960
+ this.activeSessions.delete(sessionId);
961
+ this.emitError(sessionId, `Process spawn failed: ${err.message}`);
962
+ });
963
+ this.attachStdoutListener(sessionId, proc);
964
+ this.attachStderrListener(sessionId, proc);
965
+ this.attachExitListener(sessionId, proc);
966
+ return session;
967
+ }
968
+ async killSession(sessionId) {
969
+ const entry = this.activeSessions.get(sessionId);
970
+ if (!entry) return;
971
+ if (entry.process.exitCode === null && entry.process.signalCode === null) {
972
+ try {
973
+ entry.process.stdin?.end();
974
+ } catch {
975
+ }
976
+ await killProcessCrossPlatform(entry.process);
977
+ }
978
+ this.activeSessions.delete(sessionId);
979
+ }
980
+ async sendMessage(sessionId, message) {
981
+ const entry = this.activeSessions.get(sessionId);
982
+ if (!entry) {
983
+ throw new Error(`Session ${sessionId} not found or already ended`);
984
+ }
985
+ if (entry.process.exitCode === null && entry.process.signalCode === null) {
986
+ try {
987
+ entry.process.stdin?.end();
988
+ } catch {
989
+ }
990
+ await killProcessCrossPlatform(entry.process);
991
+ }
992
+ const threadId = entry.threadId;
993
+ if (!threadId) {
994
+ throw new Error(`Session ${sessionId} has no thread ID, cannot resume`);
995
+ }
996
+ const proc = this.spawnCodexProcess(entry.session.projectPath, message, threadId);
997
+ entry.session.status = "running";
998
+ entry.session.lastActiveAt = Date.now();
999
+ entry.session.pid = proc.pid;
1000
+ entry.process = proc;
1001
+ entry.turnStartTime = Date.now();
1002
+ proc.on("error", (err) => {
1003
+ console.error(`[CodexProvider] Session ${sessionId} sendMessage process error:`, err.message);
1004
+ this.activeSessions.delete(sessionId);
1005
+ this.emitError(sessionId, `Failed to send message: ${err.message}`);
1006
+ });
1007
+ this.attachStdoutListener(sessionId, proc);
1008
+ this.attachStderrListener(sessionId, proc);
1009
+ this.attachExitListener(sessionId, proc);
1010
+ }
1011
+ onEvent(sessionId, callback) {
1012
+ const eventName = this.getEventName(sessionId);
1013
+ this.emitter.on(eventName, callback);
1014
+ return () => {
1015
+ this.emitter.off(eventName, callback);
1016
+ };
1017
+ }
1018
+ getActiveSessions() {
1019
+ return Array.from(this.activeSessions.values()).map((e) => e.session);
1020
+ }
1021
+ async generateSuggestion(_context) {
1022
+ return "";
1023
+ }
1024
+ async answerQuestion(_sessionId, _toolUseId, _answer) {
1025
+ }
1026
+ onQuestion(_sessionId, _callback) {
1027
+ return () => {
1028
+ };
1029
+ }
1030
+ // ============================================
1031
+ // 私有方法
1032
+ // ============================================
1033
+ nextId() {
1034
+ return `codex_${++this.idCounter}`;
1035
+ }
1036
+ /**
1037
+ * 启动 codex CLI 进程
1038
+ *
1039
+ * @param projectPath 工作目录
1040
+ * @param message 用户消息
1041
+ * @param resumeThreadId 如果提供,则使用 `codex exec resume` 恢复会话
1042
+ */
1043
+ spawnCodexProcess(projectPath, message, resumeThreadId) {
1044
+ const args = ["exec", "--json", "--full-auto"];
1045
+ if (resumeThreadId) {
1046
+ args.push("resume", resumeThreadId);
1047
+ } else {
1048
+ args.push("-C", projectPath);
1049
+ }
1050
+ args.push(message);
1051
+ const env = { ...process.env };
1052
+ const proc = (0, import_child_process2.spawn)(CODEX_PATH, args, {
1053
+ cwd: projectPath,
1054
+ env,
1055
+ stdio: ["pipe", "pipe", "pipe"]
1056
+ });
1057
+ try {
1058
+ proc.stdin?.end();
1059
+ } catch {
1060
+ }
1061
+ return proc;
1062
+ }
1063
+ /**
1064
+ * 挂载 stdout 监听器,逐行解析 Codex NDJSON 并转换为 ClaudeStreamEvent
1065
+ */
1066
+ attachStdoutListener(sessionId, proc) {
1067
+ if (!proc.stdout) return;
1068
+ const rl = (0, import_readline2.createInterface)({ input: proc.stdout, crlfDelay: Infinity });
1069
+ const entry = this.activeSessions.get(sessionId);
1070
+ if (entry) entry.rl = rl;
1071
+ rl.on("line", (line) => {
1072
+ const trimmed = line.trim();
1073
+ if (!trimmed) return;
1074
+ let event;
1075
+ try {
1076
+ event = JSON.parse(trimmed);
1077
+ } catch {
1078
+ console.warn(`[CodexProvider] Session ${sessionId}: failed to parse: ${trimmed.substring(0, 100)}`);
1079
+ return;
1080
+ }
1081
+ this.handleCodexEvent(sessionId, event);
1082
+ });
1083
+ }
1084
+ /**
1085
+ * 处理单个 Codex NDJSON 事件,转换并发射 ClaudeStreamEvent
1086
+ */
1087
+ handleCodexEvent(sessionId, event) {
1088
+ const entry = this.activeSessions.get(sessionId);
1089
+ if (!entry) return;
1090
+ entry.session.lastActiveAt = Date.now();
1091
+ switch (event.type) {
1092
+ case "thread.started": {
1093
+ if (event.thread_id) {
1094
+ entry.threadId = event.thread_id;
1095
+ }
1096
+ break;
1097
+ }
1098
+ case "turn.started": {
1099
+ entry.session.status = "running";
1100
+ entry.turnStartTime = Date.now();
1101
+ break;
1102
+ }
1103
+ case "item.completed": {
1104
+ if (!event.item) break;
1105
+ this.handleItemCompleted(sessionId, event.item);
1106
+ break;
1107
+ }
1108
+ case "turn.completed": {
1109
+ entry.session.status = "idle";
1110
+ const duration = entry.turnStartTime ? Date.now() - entry.turnStartTime : 0;
1111
+ const resultEvent = {
1112
+ type: "result",
1113
+ subtype: "success",
1114
+ session_id: sessionId,
1115
+ is_error: false,
1116
+ result: "",
1117
+ duration_ms: duration,
1118
+ num_turns: 1,
1119
+ usage: event.usage
1120
+ };
1121
+ this.emitter.emit(this.getEventName(sessionId), resultEvent);
1122
+ break;
1123
+ }
1124
+ case "turn.failed": {
1125
+ entry.session.status = "error";
1126
+ const failEvent = {
1127
+ type: "result",
1128
+ subtype: "error",
1129
+ session_id: sessionId,
1130
+ is_error: true,
1131
+ result: event.error ?? "Turn failed",
1132
+ duration_ms: entry.turnStartTime ? Date.now() - entry.turnStartTime : 0,
1133
+ num_turns: 1
1134
+ };
1135
+ this.emitter.emit(this.getEventName(sessionId), failEvent);
1136
+ break;
1137
+ }
1138
+ }
1139
+ }
1140
+ /**
1141
+ * 处理 item.completed 事件,按 item.type 转换为对应的 ClaudeStreamEvent
1142
+ */
1143
+ handleItemCompleted(sessionId, item) {
1144
+ switch (item.type) {
1145
+ case "agent_message": {
1146
+ const msgEvent = {
1147
+ type: "assistant",
1148
+ session_id: sessionId,
1149
+ message: {
1150
+ id: item.id || this.nextId(),
1151
+ model: "codex",
1152
+ role: "assistant",
1153
+ content: [{ type: "text", text: item.text ?? "" }]
1154
+ }
1155
+ };
1156
+ this.emitter.emit(this.getEventName(sessionId), msgEvent);
1157
+ break;
1158
+ }
1159
+ case "command_execution": {
1160
+ const toolUseId = item.id || this.nextId();
1161
+ const toolEvent = {
1162
+ type: "assistant",
1163
+ session_id: sessionId,
1164
+ message: {
1165
+ id: this.nextId(),
1166
+ model: "codex",
1167
+ role: "assistant",
1168
+ content: [{
1169
+ type: "tool_use",
1170
+ id: toolUseId,
1171
+ name: "Bash",
1172
+ input: { command: item.command ?? "" }
1173
+ }]
1174
+ }
1175
+ };
1176
+ this.emitter.emit(this.getEventName(sessionId), toolEvent);
1177
+ const resultContent = item.aggregated_output ?? "";
1178
+ const isError = item.exit_code != null && item.exit_code !== 0;
1179
+ const toolResultEvent = {
1180
+ type: "user",
1181
+ session_id: sessionId,
1182
+ message: {
1183
+ role: "user",
1184
+ content: [{
1185
+ type: "tool_result",
1186
+ tool_use_id: toolUseId,
1187
+ content: resultContent,
1188
+ is_error: isError
1189
+ }]
1190
+ }
1191
+ };
1192
+ this.emitter.emit(this.getEventName(sessionId), toolResultEvent);
1193
+ break;
1194
+ }
1195
+ case "file_change": {
1196
+ const editToolUseId = item.id || this.nextId();
1197
+ const toolEvent = {
1198
+ type: "assistant",
1199
+ session_id: sessionId,
1200
+ message: {
1201
+ id: this.nextId(),
1202
+ model: "codex",
1203
+ role: "assistant",
1204
+ content: [{
1205
+ type: "tool_use",
1206
+ id: editToolUseId,
1207
+ name: "Edit",
1208
+ input: { file_path: item.file_path ?? "", diff: item.diff ?? "" }
1209
+ }]
1210
+ }
1211
+ };
1212
+ this.emitter.emit(this.getEventName(sessionId), toolEvent);
1213
+ const toolResultEvent = {
1214
+ type: "user",
1215
+ session_id: sessionId,
1216
+ message: {
1217
+ role: "user",
1218
+ content: [{
1219
+ type: "tool_result",
1220
+ tool_use_id: editToolUseId,
1221
+ content: `File edited: ${item.file_path ?? "unknown"}`
1222
+ }]
1223
+ }
1224
+ };
1225
+ this.emitter.emit(this.getEventName(sessionId), toolResultEvent);
1226
+ break;
1227
+ }
1228
+ case "reasoning": {
1229
+ const thinkEvent = {
1230
+ type: "assistant",
1231
+ session_id: sessionId,
1232
+ message: {
1233
+ id: item.id || this.nextId(),
1234
+ model: "codex",
1235
+ role: "assistant",
1236
+ content: [{ type: "thinking", thinking: item.text ?? "" }]
1237
+ }
1238
+ };
1239
+ this.emitter.emit(this.getEventName(sessionId), thinkEvent);
1240
+ break;
1241
+ }
1242
+ }
1243
+ }
1244
+ attachStderrListener(sessionId, proc) {
1245
+ if (!proc.stderr) return;
1246
+ proc.stderr.on("data", (data) => {
1247
+ const text = data.toString().trim();
1248
+ if (text) {
1249
+ console.error(`[CodexProvider] Session ${sessionId} stderr: ${text}`);
1250
+ }
1251
+ });
1252
+ }
1253
+ attachExitListener(sessionId, proc) {
1254
+ proc.once("exit", (code, signal) => {
1255
+ const entry = this.activeSessions.get(sessionId);
1256
+ if (!entry) return;
1257
+ if (entry.process !== proc) return;
1258
+ if (entry.rl) {
1259
+ entry.rl.close();
1260
+ entry.rl = void 0;
1261
+ }
1262
+ entry.session.pid = void 0;
1263
+ entry.session.lastActiveAt = Date.now();
1264
+ const alreadyHasResult = entry.session.status === "idle" || entry.session.status === "error";
1265
+ if (alreadyHasResult) return;
1266
+ const isNormal = isNormalExit(code, signal);
1267
+ entry.session.status = isNormal ? "idle" : "error";
1268
+ if (!isNormal) {
1269
+ console.error(`[CodexProvider] Session ${sessionId}: process exited abnormally code=${code} signal=${signal}`);
1270
+ }
1271
+ const syntheticResult = {
1272
+ type: "result",
1273
+ subtype: isNormal ? "success" : "error",
1274
+ session_id: sessionId,
1275
+ is_error: !isNormal,
1276
+ result: isNormal ? "" : `Process exited code=${code} signal=${signal}`,
1277
+ duration_ms: 0,
1278
+ num_turns: 0
1279
+ };
1280
+ this.emitter.emit(this.getEventName(sessionId), syntheticResult);
1281
+ });
1282
+ }
1283
+ emitError(sessionId, message) {
1284
+ const event = {
1285
+ type: "result",
1286
+ subtype: "error",
1287
+ result: message,
1288
+ session_id: sessionId,
1289
+ duration_ms: 0,
1290
+ is_error: true,
1291
+ num_turns: 0
1292
+ };
1293
+ this.emitter.emit(this.getEventName(sessionId), event);
1294
+ }
1295
+ getEventName(sessionId) {
1296
+ return `claude:${sessionId}`;
1297
+ }
1298
+ };
1299
+
1300
+ // src/providers/ProviderFactory.ts
1301
+ var import_node_child_process4 = require("child_process");
1302
+ var ProviderFactory = class {
1303
+ providers = /* @__PURE__ */ new Map();
1304
+ getProvider(agentType) {
1305
+ const existing = this.providers.get(agentType);
1306
+ if (existing) return existing;
1307
+ let provider;
1308
+ switch (agentType) {
1309
+ case "codex":
1310
+ provider = new CodexProvider();
1311
+ break;
1312
+ case "claude-code":
1313
+ default:
1314
+ provider = new ProcessProvider();
1315
+ break;
1316
+ }
1317
+ this.providers.set(agentType, provider);
1318
+ return provider;
1319
+ }
1320
+ async detectAgents() {
1321
+ const agents = [];
1322
+ try {
1323
+ const claudePath = findClaudePath();
1324
+ const claudeVersion = (0, import_node_child_process4.execSync)(`"${claudePath}" --version`, {
1325
+ encoding: "utf-8",
1326
+ timeout: 5e3
1327
+ }).trim();
1328
+ agents.push({
1329
+ type: "claude-code",
1330
+ name: "Claude Code",
1331
+ available: true,
1332
+ version: claudeVersion
1333
+ });
1334
+ } catch {
1335
+ agents.push({ type: "claude-code", name: "Claude Code", available: false });
1336
+ }
1337
+ if (isCodexAvailable()) {
1338
+ const version = await getCodexVersion();
1339
+ agents.push({ type: "codex", name: "Codex", available: true, version });
1340
+ } else {
1341
+ agents.push({ type: "codex", name: "Codex", available: false });
1342
+ }
1343
+ return agents;
1344
+ }
1345
+ };
1346
+
1347
+ // src/session/SessionManager.ts
1348
+ var import_uuid3 = require("uuid");
852
1349
  var BUFFER_MAX = 5e3;
853
1350
  var SessionManager = class {
854
1351
  provider;
1352
+ providerFactory;
1353
+ sessionAgentType = /* @__PURE__ */ new Map();
855
1354
  /** 事件回调列表(事件会被转发到 WsBridge) */
856
1355
  eventCallbacks = [];
857
1356
  /** 每个会话的事件流取消订阅函数 */
@@ -877,8 +1376,19 @@ var SessionManager = class {
877
1376
  bufferTruncated = /* @__PURE__ */ new Set();
878
1377
  /** sessionId → projectPath 映射,用于截断时从 JSONL 补全历史 */
879
1378
  sessionProjectPaths = /* @__PURE__ */ new Map();
880
- constructor(provider) {
881
- this.provider = provider;
1379
+ constructor(providerOrFactory) {
1380
+ if (providerOrFactory instanceof ProviderFactory) {
1381
+ this.providerFactory = providerOrFactory;
1382
+ this.provider = providerOrFactory.getProvider("claude-code");
1383
+ } else {
1384
+ this.provider = providerOrFactory;
1385
+ this.providerFactory = null;
1386
+ }
1387
+ }
1388
+ getProviderForSession(sessionId) {
1389
+ if (!this.providerFactory) return this.provider;
1390
+ const agentType = this.sessionAgentType.get(sessionId) ?? "claude-code";
1391
+ return this.providerFactory.getProvider(agentType);
882
1392
  }
883
1393
  // ============================================
884
1394
  // 公开 API
@@ -889,8 +1399,9 @@ var SessionManager = class {
889
1399
  * 调用 provider.startSession(),订阅事件流,
890
1400
  * 将 ClaudeStreamEvent 包装为 ServerEvent 转发。
891
1401
  */
892
- async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images) {
893
- const session = await this.provider.startSession({
1402
+ async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images, agentType = "claude-code") {
1403
+ const provider = this.providerFactory ? this.providerFactory.getProvider(agentType) : this.provider;
1404
+ const session = await provider.startSession({
894
1405
  projectPath,
895
1406
  message,
896
1407
  sessionId: resumeSessionId ?? newSessionId,
@@ -900,6 +1411,7 @@ var SessionManager = class {
900
1411
  effort,
901
1412
  images
902
1413
  });
1414
+ this.sessionAgentType.set(session.id, agentType);
903
1415
  this.lastBroadcastStatus.set(session.id, session.status);
904
1416
  this.sessionProjectPaths.set(session.id, projectPath);
905
1417
  this.unsubscribeSession(session.id);
@@ -911,7 +1423,8 @@ var SessionManager = class {
911
1423
  * 发送消息到已有会话
912
1424
  */
913
1425
  async sendMessage(sessionId, message, permissionMode, images) {
914
- await this.provider.sendMessage(sessionId, message, permissionMode, images);
1426
+ const provider = this.getProviderForSession(sessionId);
1427
+ await provider.sendMessage(sessionId, message, permissionMode, images);
915
1428
  this.updateSessionStatus(sessionId, "running");
916
1429
  console.log(`[SessionManager] Message sent to session: ${sessionId}`);
917
1430
  }
@@ -931,7 +1444,9 @@ var SessionManager = class {
931
1444
  clearTimeout(pending.timer);
932
1445
  this.pendingAssistantEvents.delete(sessionId);
933
1446
  }
934
- await this.provider.killSession(sessionId);
1447
+ const provider = this.getProviderForSession(sessionId);
1448
+ await provider.killSession(sessionId);
1449
+ this.sessionAgentType.delete(sessionId);
935
1450
  console.log(`[SessionManager] Session killed: ${sessionId}`);
936
1451
  }
937
1452
  /**
@@ -1010,7 +1525,11 @@ var SessionManager = class {
1010
1525
  * 获取所有活跃会话(含服务器端统计)
1011
1526
  */
1012
1527
  getActiveSessions() {
1013
- return this.provider.getActiveSessions().map((session) => {
1528
+ const rawSessions = this.providerFactory ? [
1529
+ ...this.providerFactory.getProvider("claude-code").getActiveSessions(),
1530
+ ...this.providerFactory.getProvider("codex").getActiveSessions()
1531
+ ] : this.provider.getActiveSessions();
1532
+ return rawSessions.map((session) => {
1014
1533
  const stats = this.getSessionStats(session.id);
1015
1534
  return stats ? { ...session, stats } : session;
1016
1535
  });
@@ -1040,6 +1559,7 @@ var SessionManager = class {
1040
1559
  this.sessionEventBuffers.clear();
1041
1560
  this.bufferTruncated.clear();
1042
1561
  this.sessionProjectPaths.clear();
1562
+ this.sessionAgentType.clear();
1043
1563
  this.sessionStats.clear();
1044
1564
  for (const [, pending] of this.pendingAssistantEvents) {
1045
1565
  clearTimeout(pending.timer);
@@ -1057,10 +1577,11 @@ var SessionManager = class {
1057
1577
  * 订阅指定会话的事件流(包括 AskUserQuestion 问题事件)
1058
1578
  */
1059
1579
  subscribeToSession(sessionId) {
1060
- const unsubscribeEvent = this.provider.onEvent(sessionId, (event) => {
1580
+ const provider = this.getProviderForSession(sessionId);
1581
+ const unsubscribeEvent = provider.onEvent(sessionId, (event) => {
1061
1582
  this.handleClaudeEvent(sessionId, event);
1062
1583
  });
1063
- const unsubscribeQuestion = this.provider.onQuestion(
1584
+ const unsubscribeQuestion = provider.onQuestion(
1064
1585
  sessionId,
1065
1586
  ({ toolUseId, question, options }) => {
1066
1587
  this.handleAskUserQuestion(sessionId, toolUseId, question, options);
@@ -1235,7 +1756,7 @@ var SessionManager = class {
1235
1756
  console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
1236
1757
  return;
1237
1758
  }
1238
- const requestId = (0, import_uuid2.v4)();
1759
+ const requestId = (0, import_uuid3.v4)();
1239
1760
  const request = {
1240
1761
  id: requestId,
1241
1762
  sessionId,
@@ -1251,7 +1772,8 @@ var SessionManager = class {
1251
1772
  });
1252
1773
  answerPromise.then(async (answer) => {
1253
1774
  try {
1254
- await this.provider.answerQuestion(sessionId, toolUseId, answer);
1775
+ const provider = this.getProviderForSession(sessionId);
1776
+ await provider.answerQuestion(sessionId, toolUseId, answer);
1255
1777
  } catch (err) {
1256
1778
  console.error(`[SessionManager] answerQuestion failed (${sessionId}):`, err);
1257
1779
  }
@@ -1674,15 +2196,15 @@ var WsBridge = class _WsBridge {
1674
2196
 
1675
2197
  // src/approval/ApprovalProxy.ts
1676
2198
  var import_node_http = __toESM(require("http"));
1677
- var import_node_fs2 = __toESM(require("fs"));
1678
- var import_node_path2 = __toESM(require("path"));
1679
- var import_node_os3 = __toESM(require("os"));
1680
- var import_uuid3 = require("uuid");
2199
+ var import_node_fs3 = __toESM(require("fs"));
2200
+ var import_node_path3 = __toESM(require("path"));
2201
+ var import_node_os4 = __toESM(require("os"));
2202
+ var import_uuid4 = require("uuid");
1681
2203
  var ApprovalProxy = class _ApprovalProxy {
1682
2204
  server;
1683
2205
  token;
1684
2206
  port;
1685
- settingsPath = import_node_path2.default.join(import_node_os3.default.homedir(), ".claude", "settings.json");
2207
+ settingsPath = import_node_path3.default.join(import_node_os4.default.homedir(), ".claude", "settings.json");
1686
2208
  /** 待处理的审批请求:requestId -> { resolve, timer, request } */
1687
2209
  pendingApprovals = /* @__PURE__ */ new Map();
1688
2210
  /** 审批请求回调(通知外部推送到手机) */
@@ -1787,7 +2309,7 @@ var ApprovalProxy = class _ApprovalProxy {
1787
2309
  isToolInClaudeSettings(toolName, projectPath) {
1788
2310
  const checkPath = (filepath) => {
1789
2311
  try {
1790
- const raw = import_node_fs2.default.readFileSync(filepath, "utf-8");
2312
+ const raw = import_node_fs3.default.readFileSync(filepath, "utf-8");
1791
2313
  const settings = JSON.parse(raw);
1792
2314
  const allow = settings?.permissions?.allow ?? [];
1793
2315
  return allow.some((entry) => {
@@ -1801,24 +2323,24 @@ var ApprovalProxy = class _ApprovalProxy {
1801
2323
  }
1802
2324
  };
1803
2325
  if (projectPath) {
1804
- const projectSettingsPath = import_node_path2.default.join(projectPath, ".claude", "settings.json");
2326
+ const projectSettingsPath = import_node_path3.default.join(projectPath, ".claude", "settings.json");
1805
2327
  if (checkPath(projectSettingsPath)) return true;
1806
2328
  }
1807
2329
  return checkPath(this.settingsPath);
1808
2330
  }
1809
2331
  /** 将工具写入 settings.json permissions.allow(项目级或全局) */
1810
2332
  addToClaudeSettings(projectPath, toolName) {
1811
- const targetPath = projectPath ? import_node_path2.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
2333
+ const targetPath = projectPath ? import_node_path3.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
1812
2334
  try {
1813
2335
  if (projectPath) {
1814
- const dir = import_node_path2.default.dirname(targetPath);
1815
- if (!import_node_fs2.default.existsSync(dir)) {
1816
- import_node_fs2.default.mkdirSync(dir, { recursive: true });
2336
+ const dir = import_node_path3.default.dirname(targetPath);
2337
+ if (!import_node_fs3.default.existsSync(dir)) {
2338
+ import_node_fs3.default.mkdirSync(dir, { recursive: true });
1817
2339
  }
1818
2340
  }
1819
2341
  let settings = {};
1820
2342
  try {
1821
- settings = JSON.parse(import_node_fs2.default.readFileSync(targetPath, "utf-8"));
2343
+ settings = JSON.parse(import_node_fs3.default.readFileSync(targetPath, "utf-8"));
1822
2344
  } catch {
1823
2345
  }
1824
2346
  if (!settings.permissions) {
@@ -1832,7 +2354,7 @@ var ApprovalProxy = class _ApprovalProxy {
1832
2354
  const entry = `${toolName}(*)`;
1833
2355
  if (!allow.includes(entry)) {
1834
2356
  allow.push(entry);
1835
- import_node_fs2.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
2357
+ import_node_fs3.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
1836
2358
  const label = projectPath ? `${projectPath}/.claude/settings.json` : "~/.claude/settings.json";
1837
2359
  console.log(`[ApprovalProxy] ${t("approval.alwaysAllowWritten", { entry, label })}`);
1838
2360
  }
@@ -1927,7 +2449,7 @@ var ApprovalProxy = class _ApprovalProxy {
1927
2449
  try {
1928
2450
  const body = await this.parseJsonBody(req);
1929
2451
  const payload = body.payload ?? body;
1930
- const requestId = (0, import_uuid3.v4)();
2452
+ const requestId = (0, import_uuid4.v4)();
1931
2453
  const projectPath = String(body.projectPath ?? "unknown");
1932
2454
  const toolName = String(payload.tool_name ?? body.tool_name ?? "unknown");
1933
2455
  const toolInput = payload.tool_input ?? body.tool_input ?? {};
@@ -2059,8 +2581,8 @@ var ApprovalProxy = class _ApprovalProxy {
2059
2581
  };
2060
2582
 
2061
2583
  // src/mdns/MdnsService.ts
2062
- var import_node_child_process3 = require("child_process");
2063
- var import_node_os4 = require("os");
2584
+ var import_node_child_process5 = require("child_process");
2585
+ var import_node_os5 = require("os");
2064
2586
  function buildTxtArgs(txt) {
2065
2587
  return Object.entries(txt).map(([k, v]) => `${k}=${v}`);
2066
2588
  }
@@ -2078,7 +2600,7 @@ var MdnsService = class {
2078
2600
  this.httpPort = options.httpPort;
2079
2601
  this.version = options.version ?? "0.1.0";
2080
2602
  this.pairing = options.pairing ?? "closed";
2081
- this.useDnsSd = (0, import_node_os4.platform)() === "darwin";
2603
+ this.useDnsSd = (0, import_node_os5.platform)() === "darwin";
2082
2604
  }
2083
2605
  getTxt() {
2084
2606
  return {
@@ -2111,7 +2633,7 @@ var MdnsService = class {
2111
2633
  String(this.wsPort),
2112
2634
  ...buildTxtArgs(this.getTxt())
2113
2635
  ];
2114
- this.proc = (0, import_node_child_process3.spawn)("dns-sd", args, { stdio: "ignore" });
2636
+ this.proc = (0, import_node_child_process5.spawn)("dns-sd", args, { stdio: "ignore" });
2115
2637
  this.proc.on("error", (err) => {
2116
2638
  console.warn(`[MdnsService] dns-sd failed, falling back to bonjour-service: ${err.message}`);
2117
2639
  this.proc = null;
@@ -2132,7 +2654,7 @@ var MdnsService = class {
2132
2654
  return;
2133
2655
  }
2134
2656
  try {
2135
- const { default: Bonjour } = await import("bonjour-service");
2657
+ const { Bonjour } = await import("bonjour-service");
2136
2658
  const { networkInterfaces: networkInterfaces2 } = await import("os");
2137
2659
  const lanAddrs = getLanAddresses(networkInterfaces2);
2138
2660
  const opts = lanAddrs.length > 0 ? { interface: lanAddrs[0] } : {};
@@ -2223,12 +2745,12 @@ function getLanAddresses(networkInterfacesFn) {
2223
2745
 
2224
2746
  // src/hooks/HookInstaller.ts
2225
2747
  var import_promises2 = require("fs/promises");
2226
- var import_node_path3 = require("path");
2227
- var import_node_os5 = require("os");
2228
- var SESSIX_HOOKS_DIR = (0, import_node_path3.join)((0, import_node_os5.homedir)(), ".sessix", "hooks");
2229
- var HOOK_SCRIPT_PATH = (0, import_node_path3.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
2230
- var PERMISSION_ACCEPT_PATH = (0, import_node_path3.join)(SESSIX_HOOKS_DIR, "permission-accept.js");
2231
- var CLAUDE_SETTINGS_PATH = (0, import_node_path3.join)((0, import_node_os5.homedir)(), ".claude", "settings.json");
2748
+ var import_node_path4 = require("path");
2749
+ var import_node_os6 = require("os");
2750
+ var SESSIX_HOOKS_DIR = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".sessix", "hooks");
2751
+ var HOOK_SCRIPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "approval-hook.js");
2752
+ var PERMISSION_ACCEPT_PATH = (0, import_node_path4.join)(SESSIX_HOOKS_DIR, "permission-accept.js");
2753
+ var CLAUDE_SETTINGS_PATH = (0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude", "settings.json");
2232
2754
  var HOOK_COMMAND = "node ~/.sessix/hooks/approval-hook.js";
2233
2755
  var PERMISSION_ACCEPT_COMMAND = "node ~/.sessix/hooks/permission-accept.js";
2234
2756
  var LEGACY_HOOK_COMMANDS = [
@@ -2402,7 +2924,7 @@ var HookInstaller = class {
2402
2924
  * 写入 Claude Code settings.json
2403
2925
  */
2404
2926
  async writeClaudeSettings(settings) {
2405
- await (0, import_promises2.mkdir)((0, import_node_path3.join)((0, import_node_os5.homedir)(), ".claude"), { recursive: true });
2927
+ await (0, import_promises2.mkdir)((0, import_node_path4.join)((0, import_node_os6.homedir)(), ".claude"), { recursive: true });
2406
2928
  await (0, import_promises2.writeFile)(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2407
2929
  }
2408
2930
  /**
@@ -2429,7 +2951,7 @@ var HookInstaller = class {
2429
2951
  };
2430
2952
 
2431
2953
  // src/notification/NotificationService.ts
2432
- var import_node_path4 = require("path");
2954
+ var import_node_path5 = require("path");
2433
2955
  var NotificationService = class {
2434
2956
  constructor(sessionManager, expoChannel = null) {
2435
2957
  this.sessionManager = sessionManager;
@@ -2525,7 +3047,7 @@ var NotificationService = class {
2525
3047
  const dangerLevel = this.getDangerLevel(request.toolName);
2526
3048
  const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
2527
3049
  const categoryId = isDangerous ? "APPROVAL_DANGEROUS" : "APPROVAL_NORMAL";
2528
- const projectName = (0, import_node_path4.basename)(
3050
+ const projectName = (0, import_node_path5.basename)(
2529
3051
  this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId)?.projectPath ?? ""
2530
3052
  );
2531
3053
  const pushTitle = isDangerous ? `\u26A0\uFE0F ${title}` : title;
@@ -2581,7 +3103,7 @@ var NotificationService = class {
2581
3103
  /** 从审批请求中提取操作目标的简短描述 */
2582
3104
  extractTarget(request) {
2583
3105
  const input = request.toolInput;
2584
- if (input.file_path) return (0, import_node_path4.basename)(String(input.file_path));
3106
+ if (input.file_path) return (0, import_node_path5.basename)(String(input.file_path));
2585
3107
  if (input.command) return String(input.command).slice(0, 40);
2586
3108
  return request.description.slice(0, 40);
2587
3109
  }
@@ -2684,7 +3206,7 @@ var NotificationService = class {
2684
3206
  getSessionTitle(sessionId) {
2685
3207
  const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
2686
3208
  if (!session) return "Unknown";
2687
- return session.summary ?? (0, import_node_path4.basename)(session.projectPath);
3209
+ return session.summary ?? (0, import_node_path5.basename)(session.projectPath);
2688
3210
  }
2689
3211
  /** 获取会话的 YOLO 模式状态 */
2690
3212
  getYoloMode(sessionId) {
@@ -2693,7 +3215,7 @@ var NotificationService = class {
2693
3215
  };
2694
3216
 
2695
3217
  // src/notification/DesktopNotificationChannel.ts
2696
- var import_node_child_process4 = require("child_process");
3218
+ var import_node_child_process6 = require("child_process");
2697
3219
  var DesktopNotificationChannel = class {
2698
3220
  isAvailable() {
2699
3221
  return process.platform === "darwin";
@@ -2705,7 +3227,7 @@ var DesktopNotificationChannel = class {
2705
3227
  const sound = payload.sound ?? "Ping";
2706
3228
  const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
2707
3229
  return new Promise((resolve) => {
2708
- (0, import_node_child_process4.execFile)("osascript", ["-e", script], (err) => {
3230
+ (0, import_node_child_process6.execFile)("osascript", ["-e", script], (err) => {
2709
3231
  if (err) {
2710
3232
  console.warn("[DesktopNotificationChannel] Send notification failed:", err.message);
2711
3233
  }
@@ -2979,7 +3501,7 @@ var ActivityPushChannel = class {
2979
3501
 
2980
3502
  // src/session/ProjectReader.ts
2981
3503
  var import_promises3 = require("fs/promises");
2982
- var import_readline2 = require("readline");
3504
+ var import_readline3 = require("readline");
2983
3505
  var import_path = require("path");
2984
3506
  var import_os = require("os");
2985
3507
  var CLAUDE_PROJECTS_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
@@ -3031,16 +3553,23 @@ async function getHistoricalSessions(projectPath) {
3031
3553
  const entries = await (0, import_promises3.readdir)(projectDir, { withFileTypes: true });
3032
3554
  const jsonlFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
3033
3555
  const mtimeMap = /* @__PURE__ */ new Map();
3034
- for (const entry of jsonlFiles) {
3035
- const sessionId = entry.name.slice(0, -6);
3036
- const filePath = (0, import_path.join)(projectDir, entry.name);
3037
- try {
3038
- const fileStat = await (0, import_promises3.stat)(filePath);
3039
- mtimeMap.set(sessionId, fileStat.mtimeMs);
3040
- } catch {
3041
- mtimeMap.set(sessionId, 0);
3042
- }
3043
- }
3556
+ await Promise.all(
3557
+ jsonlFiles.map(async (entry) => {
3558
+ const sessionId = entry.name.slice(0, -6);
3559
+ const filePath = (0, import_path.join)(projectDir, entry.name);
3560
+ try {
3561
+ const contentTs = await extractLastTimestamp(filePath);
3562
+ if (contentTs) {
3563
+ mtimeMap.set(sessionId, contentTs);
3564
+ } else {
3565
+ const fileStat = await (0, import_promises3.stat)(filePath);
3566
+ mtimeMap.set(sessionId, fileStat.mtimeMs);
3567
+ }
3568
+ } catch {
3569
+ mtimeMap.set(sessionId, 0);
3570
+ }
3571
+ })
3572
+ );
3044
3573
  const uuidDirs = entries.filter(
3045
3574
  (e) => e.isDirectory() && UUID_RE.test(e.name) && !mtimeMap.has(e.name)
3046
3575
  );
@@ -3191,11 +3720,37 @@ async function getSessionHistory(projectPath, sessionId) {
3191
3720
  };
3192
3721
  }
3193
3722
  }
3723
+ async function extractLastTimestamp(filePath) {
3724
+ let fileHandle;
3725
+ try {
3726
+ fileHandle = await (0, import_promises3.open)(filePath, "r");
3727
+ const fileStat = await fileHandle.stat();
3728
+ const readSize = Math.min(fileStat.size, 8192);
3729
+ const buffer = Buffer.alloc(readSize);
3730
+ await fileHandle.read(buffer, 0, readSize, fileStat.size - readSize);
3731
+ const tail = buffer.toString("utf-8");
3732
+ const lines = tail.split("\n").filter((l) => l.trim());
3733
+ for (let i = lines.length - 1; i >= 0; i--) {
3734
+ try {
3735
+ const obj = JSON.parse(lines[i]);
3736
+ if (obj.timestamp) {
3737
+ const ts = new Date(obj.timestamp).getTime();
3738
+ if (!isNaN(ts)) return ts;
3739
+ }
3740
+ } catch {
3741
+ }
3742
+ }
3743
+ } catch {
3744
+ } finally {
3745
+ await fileHandle?.close();
3746
+ }
3747
+ return void 0;
3748
+ }
3194
3749
  async function extractFirstPrompt(filePath) {
3195
3750
  let fileHandle;
3196
3751
  try {
3197
3752
  fileHandle = await (0, import_promises3.open)(filePath, "r");
3198
- const rl = (0, import_readline2.createInterface)({
3753
+ const rl = (0, import_readline3.createInterface)({
3199
3754
  input: fileHandle.createReadStream({ encoding: "utf-8" }),
3200
3755
  crlfDelay: Infinity
3201
3756
  });
@@ -3259,17 +3814,24 @@ async function countJsonlFilesWithMtime(dirPath) {
3259
3814
  (e) => e.isDirectory() && UUID_RE.test(e.name) && !jsonlNames.has(e.name)
3260
3815
  );
3261
3816
  let latestMtime = 0;
3262
- const allEntries = [
3263
- ...entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")),
3264
- ...uuidDirs
3265
- ];
3266
- for (const entry of allEntries) {
3267
- try {
3268
- const fileStat = await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name));
3269
- if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
3270
- } catch {
3271
- }
3272
- }
3817
+ const jsonlEntries = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
3818
+ await Promise.all([
3819
+ ...jsonlEntries.map(async (entry) => {
3820
+ try {
3821
+ const contentTs = await extractLastTimestamp((0, import_path.join)(dirPath, entry.name));
3822
+ const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name))).mtimeMs;
3823
+ if (ts > latestMtime) latestMtime = ts;
3824
+ } catch {
3825
+ }
3826
+ }),
3827
+ ...uuidDirs.map(async (entry) => {
3828
+ try {
3829
+ const fileStat = await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name));
3830
+ if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
3831
+ } catch {
3832
+ }
3833
+ })
3834
+ ]);
3273
3835
  return { count: jsonlNames.size + uuidDirs.length, latestMtime };
3274
3836
  } catch {
3275
3837
  return { count: 0, latestMtime: 0 };
@@ -3336,14 +3898,14 @@ var PairingManager = class {
3336
3898
  };
3337
3899
 
3338
3900
  // src/auth/AuthManager.ts
3339
- var import_child_process2 = require("child_process");
3340
3901
  var import_child_process3 = require("child_process");
3902
+ var import_child_process4 = require("child_process");
3341
3903
  var import_util = require("util");
3342
- var import_events2 = require("events");
3343
- var execFileAsync = (0, import_util.promisify)(import_child_process3.execFile);
3904
+ var import_events3 = require("events");
3905
+ var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
3344
3906
  var CLAUDE_PATH2 = findClaudePath();
3345
3907
  var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
3346
- var AuthManager = class extends import_events2.EventEmitter {
3908
+ var AuthManager = class extends import_events3.EventEmitter {
3347
3909
  loginProcess = null;
3348
3910
  loginTimeout = null;
3349
3911
  urlSent = false;
@@ -3371,7 +3933,7 @@ var AuthManager = class extends import_events2.EventEmitter {
3371
3933
  }
3372
3934
  this.clearLoginTimeout();
3373
3935
  this.urlSent = false;
3374
- const proc = (0, import_child_process2.spawn)(CLAUDE_PATH2, ["auth", "login"], {
3936
+ const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
3375
3937
  env: { ...process.env, BROWSER: "echo" },
3376
3938
  stdio: ["pipe", "pipe", "pipe"]
3377
3939
  });
@@ -3457,8 +4019,8 @@ var AuthManager = class extends import_events2.EventEmitter {
3457
4019
  var import_promises5 = require("fs/promises");
3458
4020
 
3459
4021
  // src/terminal/TerminalExecutor.ts
3460
- var import_node_child_process5 = require("child_process");
3461
- var import_uuid4 = require("uuid");
4022
+ var import_node_child_process7 = require("child_process");
4023
+ var import_uuid5 = require("uuid");
3462
4024
  var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
3463
4025
  var TerminalExecutor = class {
3464
4026
  processes = /* @__PURE__ */ new Map();
@@ -3480,10 +4042,10 @@ var TerminalExecutor = class {
3480
4042
  }
3481
4043
  }
3482
4044
  exec(sessionId, command, cwd) {
3483
- const execId = (0, import_uuid4.v4)();
4045
+ const execId = (0, import_uuid5.v4)();
3484
4046
  const shell = isWindows ? "powershell" : "bash";
3485
4047
  const args = isWindows ? ["-Command", command] : ["-c", command];
3486
- const proc = (0, import_node_child_process5.spawn)(shell, args, {
4048
+ const proc = (0, import_node_child_process7.spawn)(shell, args, {
3487
4049
  cwd,
3488
4050
  stdio: ["ignore", "pipe", "pipe"],
3489
4051
  env: { ...process.env }
@@ -3546,7 +4108,7 @@ var TerminalExecutor = class {
3546
4108
  // src/server.ts
3547
4109
  var WS_PORT = 3745;
3548
4110
  var HTTP_PORT = 3746;
3549
- var execAsync = (0, import_node_util.promisify)(import_node_child_process6.exec);
4111
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
3550
4112
  async function killPortProcess(port) {
3551
4113
  try {
3552
4114
  if (isWindows) {
@@ -3588,8 +4150,8 @@ async function createWithRetry(label, port, factory) {
3588
4150
  }
3589
4151
  }
3590
4152
  async function start(opts = {}) {
3591
- const configDir = (0, import_node_path5.join)((0, import_node_os6.homedir)(), ".sessix");
3592
- const tokenFile = (0, import_node_path5.join)(configDir, "token");
4153
+ const configDir = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix");
4154
+ const tokenFile = (0, import_node_path6.join)(configDir, "token");
3593
4155
  let token;
3594
4156
  if (opts.token !== void 0) {
3595
4157
  token = opts.token;
@@ -3601,14 +4163,14 @@ async function start(opts = {}) {
3601
4163
  try {
3602
4164
  token = (await (0, import_promises4.readFile)(tokenFile, "utf8")).trim();
3603
4165
  } catch {
3604
- token = (0, import_uuid5.v4)();
4166
+ token = (0, import_uuid6.v4)();
3605
4167
  await (0, import_promises4.mkdir)(configDir, { recursive: true });
3606
4168
  await (0, import_promises4.writeFile)(tokenFile, token, "utf8");
3607
4169
  }
3608
4170
  }
3609
4171
  }
3610
- const provider = new ProcessProvider();
3611
- const sessionManager = new SessionManager(provider);
4172
+ const providerFactory = new ProviderFactory();
4173
+ const sessionManager = new SessionManager(providerFactory);
3612
4174
  const terminalExecutor = new TerminalExecutor();
3613
4175
  const wsBridge = await createWithRetry(
3614
4176
  "WsBridge",
@@ -3648,7 +4210,7 @@ async function start(opts = {}) {
3648
4210
  let mdnsService = null;
3649
4211
  const pairingManager = new PairingManager({
3650
4212
  token,
3651
- serverName: (0, import_node_os6.hostname)(),
4213
+ serverName: (0, import_node_os7.hostname)(),
3652
4214
  version: "0.2.0",
3653
4215
  onStateChange: (state) => mdnsService?.updatePairingState(state)
3654
4216
  });
@@ -3705,7 +4267,8 @@ async function start(opts = {}) {
3705
4267
  event.model,
3706
4268
  event.permissionMode,
3707
4269
  event.effort,
3708
- event.images
4270
+ event.images,
4271
+ event.agentType
3709
4272
  );
3710
4273
  wsBridge.broadcast({
3711
4274
  type: "session_list",
@@ -3881,7 +4444,7 @@ async function start(opts = {}) {
3881
4444
  return null;
3882
4445
  }).filter(Boolean).join("\n");
3883
4446
  }
3884
- const suggestion = await provider.generateSuggestion(context);
4447
+ const suggestion = await providerFactory.getProvider("claude-code").generateSuggestion(context);
3885
4448
  wsBridge.send(ws, {
3886
4449
  type: "prompt_suggestion",
3887
4450
  sessionId: event.sessionId,
@@ -3903,9 +4466,9 @@ async function start(opts = {}) {
3903
4466
  }
3904
4467
  case "terminal_exec": {
3905
4468
  const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
3906
- const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId);
4469
+ const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId) ?? event.projectPath;
3907
4470
  if (!cwd) {
3908
- wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: "Session not found or no project path", sessionId: event.sessionId });
4471
+ wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: `Session not found (id: ${event.sessionId.slice(0, 8)}\u2026)`, sessionId: event.sessionId });
3909
4472
  break;
3910
4473
  }
3911
4474
  terminalExecutor.exec(event.sessionId, event.command, cwd);
@@ -3959,6 +4522,11 @@ async function start(opts = {}) {
3959
4522
  }
3960
4523
  break;
3961
4524
  }
4525
+ case "list_agents": {
4526
+ const agents = await providerFactory.detectAgents();
4527
+ wsBridge.send(ws, { type: "agent_list", agents });
4528
+ break;
4529
+ }
3962
4530
  default: {
3963
4531
  wsBridge.send(ws, {
3964
4532
  type: "error",
@@ -4122,7 +4690,7 @@ async function start(opts = {}) {
4122
4690
  openPairing: (duration) => pairingManager.open(duration),
4123
4691
  closePairing: () => pairingManager.close(),
4124
4692
  regenerateToken: async () => {
4125
- const newToken = (0, import_uuid5.v4)();
4693
+ const newToken = (0, import_uuid6.v4)();
4126
4694
  await (0, import_promises4.mkdir)(configDir, { recursive: true });
4127
4695
  await (0, import_promises4.writeFile)(tokenFile, newToken, "utf8");
4128
4696
  instance.token = newToken;
@@ -4141,7 +4709,7 @@ async function start(opts = {}) {
4141
4709
  var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
4142
4710
  function getPackageVersion() {
4143
4711
  try {
4144
- const pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path6.join)(__dirname, "..", "package.json"), "utf8"));
4712
+ const pkg = JSON.parse((0, import_node_fs4.readFileSync)((0, import_node_path7.join)(__dirname, "..", "package.json"), "utf8"));
4145
4713
  return pkg.version ?? "0.0.0";
4146
4714
  } catch {
4147
4715
  return "0.0.0";
@@ -4163,11 +4731,38 @@ async function fetchLatestVersion() {
4163
4731
  return null;
4164
4732
  }
4165
4733
  }
4734
+ function isNewer(latest, current) {
4735
+ const a = latest.split(".").map(Number);
4736
+ const b = current.split(".").map(Number);
4737
+ for (let i = 0; i < 3; i++) {
4738
+ if ((a[i] ?? 0) > (b[i] ?? 0)) return true;
4739
+ if ((a[i] ?? 0) < (b[i] ?? 0)) return false;
4740
+ }
4741
+ return false;
4742
+ }
4743
+ async function autoUpdateIfNeeded() {
4744
+ if (process.env.SESSIX_NO_UPDATE_CHECK === "1" || process.env.__SESSIX_UPDATED === "1") return;
4745
+ const latest = await fetchLatestVersion();
4746
+ if (!latest || !isNewer(latest, PKG_VERSION)) return;
4747
+ console.log(` \u{1F4E6} ${t("startup.autoUpdating", { current: PKG_VERSION, latest })}`);
4748
+ console.log();
4749
+ try {
4750
+ (0, import_node_child_process9.execFileSync)("npx", [`sessix-server@${latest}`], {
4751
+ stdio: "inherit",
4752
+ env: { ...process.env, __SESSIX_UPDATED: "1" }
4753
+ });
4754
+ process.exit(0);
4755
+ } catch {
4756
+ console.log(` \u26A0\uFE0F ${t("startup.autoUpdateFailed")}`);
4757
+ console.log();
4758
+ }
4759
+ }
4166
4760
  async function main() {
4167
4761
  console.log("=".repeat(50));
4168
4762
  console.log(`${t("startup.banner")} v${PKG_VERSION}`);
4169
4763
  console.log("=".repeat(50));
4170
4764
  console.log();
4765
+ await autoUpdateIfNeeded();
4171
4766
  const enableAutoConnect = process.env.SESSIX_AUTO_CONNECT !== "false";
4172
4767
  const server = await start({ enableAutoConnect });
4173
4768
  const localIp = getLocalIp();
@@ -4208,13 +4803,6 @@ async function main() {
4208
4803
  console.log(t("startup.pairingOpen"));
4209
4804
  console.log(t("startup.pressT"));
4210
4805
  console.log();
4211
- fetchLatestVersion().then((latest) => {
4212
- if (!latest || latest === PKG_VERSION) return;
4213
- console.log();
4214
- console.log(` \u{1F4E6} ${t("startup.updateAvailable", { current: PKG_VERSION, latest })}`);
4215
- console.log(` npx sessix-server@latest`);
4216
- console.log();
4217
- });
4218
4806
  const shutdown = async (signal) => {
4219
4807
  console.log(`
4220
4808
  [Main] ${t("startup.receivedSignal", { signal })}`);
@@ -4265,7 +4853,7 @@ ${t("startup.pairingReopened")}`);
4265
4853
  }
4266
4854
  }
4267
4855
  function getLocalIp() {
4268
- const interfaces = (0, import_node_os7.networkInterfaces)();
4856
+ const interfaces = (0, import_node_os8.networkInterfaces)();
4269
4857
  for (const iface of Object.values(interfaces)) {
4270
4858
  for (const addr of iface ?? []) {
4271
4859
  if (addr.family === "IPv4" && !addr.internal) {