sessix-server 0.2.8 → 0.2.9

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 +63 -23
  2. package/dist/server.js +63 -23
  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
@@ -3031,16 +3031,23 @@ async function getHistoricalSessions(projectPath) {
3031
3031
  const entries = await (0, import_promises3.readdir)(projectDir, { withFileTypes: true });
3032
3032
  const jsonlFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
3033
3033
  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
- }
3034
+ await Promise.all(
3035
+ jsonlFiles.map(async (entry) => {
3036
+ const sessionId = entry.name.slice(0, -6);
3037
+ const filePath = (0, import_path.join)(projectDir, entry.name);
3038
+ try {
3039
+ const contentTs = await extractLastTimestamp(filePath);
3040
+ if (contentTs) {
3041
+ mtimeMap.set(sessionId, contentTs);
3042
+ } else {
3043
+ const fileStat = await (0, import_promises3.stat)(filePath);
3044
+ mtimeMap.set(sessionId, fileStat.mtimeMs);
3045
+ }
3046
+ } catch {
3047
+ mtimeMap.set(sessionId, 0);
3048
+ }
3049
+ })
3050
+ );
3044
3051
  const uuidDirs = entries.filter(
3045
3052
  (e) => e.isDirectory() && UUID_RE.test(e.name) && !mtimeMap.has(e.name)
3046
3053
  );
@@ -3191,6 +3198,32 @@ async function getSessionHistory(projectPath, sessionId) {
3191
3198
  };
3192
3199
  }
3193
3200
  }
3201
+ async function extractLastTimestamp(filePath) {
3202
+ let fileHandle;
3203
+ try {
3204
+ fileHandle = await (0, import_promises3.open)(filePath, "r");
3205
+ const fileStat = await fileHandle.stat();
3206
+ const readSize = Math.min(fileStat.size, 8192);
3207
+ const buffer = Buffer.alloc(readSize);
3208
+ await fileHandle.read(buffer, 0, readSize, fileStat.size - readSize);
3209
+ const tail = buffer.toString("utf-8");
3210
+ const lines = tail.split("\n").filter((l) => l.trim());
3211
+ for (let i = lines.length - 1; i >= 0; i--) {
3212
+ try {
3213
+ const obj = JSON.parse(lines[i]);
3214
+ if (obj.timestamp) {
3215
+ const ts = new Date(obj.timestamp).getTime();
3216
+ if (!isNaN(ts)) return ts;
3217
+ }
3218
+ } catch {
3219
+ }
3220
+ }
3221
+ } catch {
3222
+ } finally {
3223
+ await fileHandle?.close();
3224
+ }
3225
+ return void 0;
3226
+ }
3194
3227
  async function extractFirstPrompt(filePath) {
3195
3228
  let fileHandle;
3196
3229
  try {
@@ -3259,17 +3292,24 @@ async function countJsonlFilesWithMtime(dirPath) {
3259
3292
  (e) => e.isDirectory() && UUID_RE.test(e.name) && !jsonlNames.has(e.name)
3260
3293
  );
3261
3294
  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
- }
3295
+ const jsonlEntries = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
3296
+ await Promise.all([
3297
+ ...jsonlEntries.map(async (entry) => {
3298
+ try {
3299
+ const contentTs = await extractLastTimestamp((0, import_path.join)(dirPath, entry.name));
3300
+ const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name))).mtimeMs;
3301
+ if (ts > latestMtime) latestMtime = ts;
3302
+ } catch {
3303
+ }
3304
+ }),
3305
+ ...uuidDirs.map(async (entry) => {
3306
+ try {
3307
+ const fileStat = await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name));
3308
+ if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
3309
+ } catch {
3310
+ }
3311
+ })
3312
+ ]);
3273
3313
  return { count: jsonlNames.size + uuidDirs.length, latestMtime };
3274
3314
  } catch {
3275
3315
  return { count: 0, latestMtime: 0 };
@@ -3903,9 +3943,9 @@ async function start(opts = {}) {
3903
3943
  }
3904
3944
  case "terminal_exec": {
3905
3945
  const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
3906
- const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId);
3946
+ const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId) ?? event.projectPath;
3907
3947
  if (!cwd) {
3908
- wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: "Session not found or no project path", sessionId: event.sessionId });
3948
+ wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: `Session not found (id: ${event.sessionId.slice(0, 8)}\u2026)`, sessionId: event.sessionId });
3909
3949
  break;
3910
3950
  }
3911
3951
  terminalExecutor.exec(event.sessionId, event.command, cwd);
package/dist/server.js CHANGED
@@ -3037,16 +3037,23 @@ async function getHistoricalSessions(projectPath) {
3037
3037
  const entries = await (0, import_promises3.readdir)(projectDir, { withFileTypes: true });
3038
3038
  const jsonlFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
3039
3039
  const mtimeMap = /* @__PURE__ */ new Map();
3040
- for (const entry of jsonlFiles) {
3041
- const sessionId = entry.name.slice(0, -6);
3042
- const filePath = (0, import_path.join)(projectDir, entry.name);
3043
- try {
3044
- const fileStat = await (0, import_promises3.stat)(filePath);
3045
- mtimeMap.set(sessionId, fileStat.mtimeMs);
3046
- } catch {
3047
- mtimeMap.set(sessionId, 0);
3048
- }
3049
- }
3040
+ await Promise.all(
3041
+ jsonlFiles.map(async (entry) => {
3042
+ const sessionId = entry.name.slice(0, -6);
3043
+ const filePath = (0, import_path.join)(projectDir, entry.name);
3044
+ try {
3045
+ const contentTs = await extractLastTimestamp(filePath);
3046
+ if (contentTs) {
3047
+ mtimeMap.set(sessionId, contentTs);
3048
+ } else {
3049
+ const fileStat = await (0, import_promises3.stat)(filePath);
3050
+ mtimeMap.set(sessionId, fileStat.mtimeMs);
3051
+ }
3052
+ } catch {
3053
+ mtimeMap.set(sessionId, 0);
3054
+ }
3055
+ })
3056
+ );
3050
3057
  const uuidDirs = entries.filter(
3051
3058
  (e) => e.isDirectory() && UUID_RE.test(e.name) && !mtimeMap.has(e.name)
3052
3059
  );
@@ -3197,6 +3204,32 @@ async function getSessionHistory(projectPath, sessionId) {
3197
3204
  };
3198
3205
  }
3199
3206
  }
3207
+ async function extractLastTimestamp(filePath) {
3208
+ let fileHandle;
3209
+ try {
3210
+ fileHandle = await (0, import_promises3.open)(filePath, "r");
3211
+ const fileStat = await fileHandle.stat();
3212
+ const readSize = Math.min(fileStat.size, 8192);
3213
+ const buffer = Buffer.alloc(readSize);
3214
+ await fileHandle.read(buffer, 0, readSize, fileStat.size - readSize);
3215
+ const tail = buffer.toString("utf-8");
3216
+ const lines = tail.split("\n").filter((l) => l.trim());
3217
+ for (let i = lines.length - 1; i >= 0; i--) {
3218
+ try {
3219
+ const obj = JSON.parse(lines[i]);
3220
+ if (obj.timestamp) {
3221
+ const ts = new Date(obj.timestamp).getTime();
3222
+ if (!isNaN(ts)) return ts;
3223
+ }
3224
+ } catch {
3225
+ }
3226
+ }
3227
+ } catch {
3228
+ } finally {
3229
+ await fileHandle?.close();
3230
+ }
3231
+ return void 0;
3232
+ }
3200
3233
  async function extractFirstPrompt(filePath) {
3201
3234
  let fileHandle;
3202
3235
  try {
@@ -3265,17 +3298,24 @@ async function countJsonlFilesWithMtime(dirPath) {
3265
3298
  (e) => e.isDirectory() && UUID_RE.test(e.name) && !jsonlNames.has(e.name)
3266
3299
  );
3267
3300
  let latestMtime = 0;
3268
- const allEntries = [
3269
- ...entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")),
3270
- ...uuidDirs
3271
- ];
3272
- for (const entry of allEntries) {
3273
- try {
3274
- const fileStat = await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name));
3275
- if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
3276
- } catch {
3277
- }
3278
- }
3301
+ const jsonlEntries = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
3302
+ await Promise.all([
3303
+ ...jsonlEntries.map(async (entry) => {
3304
+ try {
3305
+ const contentTs = await extractLastTimestamp((0, import_path.join)(dirPath, entry.name));
3306
+ const ts = contentTs ?? (await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name))).mtimeMs;
3307
+ if (ts > latestMtime) latestMtime = ts;
3308
+ } catch {
3309
+ }
3310
+ }),
3311
+ ...uuidDirs.map(async (entry) => {
3312
+ try {
3313
+ const fileStat = await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name));
3314
+ if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
3315
+ } catch {
3316
+ }
3317
+ })
3318
+ ]);
3279
3319
  return { count: jsonlNames.size + uuidDirs.length, latestMtime };
3280
3320
  } catch {
3281
3321
  return { count: 0, latestMtime: 0 };
@@ -3909,9 +3949,9 @@ async function start(opts = {}) {
3909
3949
  }
3910
3950
  case "terminal_exec": {
3911
3951
  const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
3912
- const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId);
3952
+ const cwd = activeSession?.projectPath ?? sessionManager.getSessionProjectPath(event.sessionId) ?? event.projectPath;
3913
3953
  if (!cwd) {
3914
- wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: "Session not found or no project path", sessionId: event.sessionId });
3954
+ wsBridge.send(ws, { type: "error", code: "TERMINAL_EXEC_ERROR", message: `Session not found (id: ${event.sessionId.slice(0, 8)}\u2026)`, sessionId: event.sessionId });
3915
3955
  break;
3916
3956
  }
3917
3957
  terminalExecutor.exec(event.sessionId, event.command, cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "bin": {
5
5
  "sessix-server": "./dist/index.js"
6
6
  },
@@ -17,6 +17,15 @@
17
17
  "files": [
18
18
  "dist"
19
19
  ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/ampere1988/Sessix-Server.git"
23
+ },
24
+ "homepage": "https://github.com/ampere1988/Sessix-Server#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/ampere1988/Sessix-Server/issues"
27
+ },
28
+ "license": "MIT",
20
29
  "engines": {
21
30
  "node": ">=22.0.0"
22
31
  },
@@ -1,86 +0,0 @@
1
- import type { ApprovalRequest, ApprovalDecision } from '@sessix/shared';
2
- /** ApprovalProxy 配置 */
3
- interface ApprovalProxyOptions {
4
- port: number;
5
- token: string;
6
- }
7
- /** 外部注入的连接信息回调(用于 /health 端点) */
8
- interface StatusInfo {
9
- connections: number;
10
- activeSessions: number;
11
- }
12
- /**
13
- * 审批代理 HTTP 服务
14
- *
15
- * 接收 Claude Code hook 发来的工具审批请求,通过长轮询机制
16
- * hold 住响应,等待手机端用户做出审批决策后再返回。
17
- */
18
- export declare class ApprovalProxy {
19
- private server;
20
- private token;
21
- private port;
22
- private settingsPath;
23
- /** 待处理的审批请求:requestId -> { resolve, timer, request } */
24
- private pendingApprovals;
25
- /** 审批请求回调(通知外部推送到手机) */
26
- private approvalRequestCallbacks;
27
- /** 获取状态信息的回调(由外部注入) */
28
- private statusInfoProvider;
29
- constructor(options: ApprovalProxyOptions);
30
- /**
31
- * 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
32
- */
33
- static create(options: ApprovalProxyOptions): Promise<ApprovalProxy>;
34
- /** 注册审批请求回调(当有新的审批请求时触发) */
35
- onApprovalRequest(callback: (request: ApprovalRequest) => void): void;
36
- /** 设置状态信息提供者(用于 /health 端点) */
37
- setStatusInfoProvider(provider: () => StatusInfo): void;
38
- /**
39
- * 注入审批结果
40
- *
41
- * 从 pendingApprovals 中取出对应请求,resolve promise,
42
- * 让长轮询的 HTTP 响应返回审批结果给 Claude Code hook。
43
- */
44
- resolveApproval(requestId: string, decision: ApprovalDecision): boolean;
45
- /** 获取当前待处理的审批数量 */
46
- getPendingCount(): number;
47
- /** 检查指定审批请求是否仍在等待用户决策 */
48
- isPending(requestId: string): boolean;
49
- /** 检查工具是否已在 settings.json permissions.allow 中(检查项目级和全局) */
50
- private isToolInClaudeSettings;
51
- /** 将工具写入 settings.json permissions.allow(项目级或全局) */
52
- addToClaudeSettings(projectPath: string | undefined, toolName: string): void;
53
- /** 获取指定会话的所有 pending approval requests(用于 subscribe 重发) */
54
- getPendingRequestsForSession(sessionId: string): ApprovalRequest[];
55
- /**
56
- * 批量允许所有待处理的审批请求(手机端断线时调用)
57
- */
58
- approveAll(reason?: string): void;
59
- /** 优雅关闭 HTTP 服务 */
60
- close(): Promise<void>;
61
- /** 路由请求 */
62
- private handleRequest;
63
- /**
64
- * 核心端点:处理 Claude Code hook 的审批请求
65
- *
66
- * 长轮询实现:
67
- * 1. 解析请求 body
68
- * 2. 创建 ApprovalRequest 对象
69
- * 3. 通知外部(推到手机)
70
- * 4. 创建 Promise 并 hold 住 response
71
- * 5. 等待 resolveApproval() 被调用或超时
72
- */
73
- private handleApprovalHook;
74
- /** 健康检查端点 */
75
- private handleHealth;
76
- /** 返回连接 token(仅本机访问) */
77
- private handleToken;
78
- /** 通知所有注册的审批请求回调 */
79
- private notifyApprovalRequest;
80
- /** 手动解析请求的 JSON body(限制最大 1MB 防止滥用) */
81
- private parseJsonBody;
82
- /** 发送 JSON 响应的辅助方法 */
83
- private sendJson;
84
- }
85
- export {};
86
- //# sourceMappingURL=ApprovalProxy.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ApprovalProxy.d.ts","sourceRoot":"","sources":["../../src/approval/ApprovalProxy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAEvE,uBAAuB;AACvB,UAAU,oBAAoB;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,iCAAiC;AACjC,UAAU,UAAU;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAa;IAC3B,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,IAAI,CAAQ;IACpB,OAAO,CAAC,YAAY,CAAsD;IAE1E,wDAAwD;IACxD,OAAO,CAAC,gBAAgB,CAIpB;IAEJ,wBAAwB;IACxB,OAAO,CAAC,wBAAwB,CAAgD;IAEhF,uBAAuB;IACvB,OAAO,CAAC,kBAAkB,CAAkC;gBAEhD,OAAO,EAAE,oBAAoB;IAazC;;OAEG;WACU,MAAM,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;IAe1E,4BAA4B;IAC5B,iBAAiB,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,IAAI;IAIrE,+BAA+B;IAC/B,qBAAqB,CAAC,QAAQ,EAAE,MAAM,UAAU,GAAG,IAAI;IAIvD;;;;;OAKG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,gBAAgB,GAAG,OAAO;IAkBvE,mBAAmB;IACnB,eAAe,IAAI,MAAM;IAIzB,yBAAyB;IACzB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO;IAIrC,2DAA2D;IAC3D,OAAO,CAAC,sBAAsB;IAsB9B,oDAAoD;IACpD,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAwC5E,2DAA2D;IAC3D,4BAA4B,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE;IAUlE;;OAEG;IACH,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAUjC,mBAAmB;IACnB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BtB,WAAW;IACX,OAAO,CAAC,aAAa;IA2BrB;;;;;;;;;OASG;YACW,kBAAkB;IAyDhC,aAAa;IACb,OAAO,CAAC,YAAY;IASpB,wBAAwB;IACxB,OAAO,CAAC,WAAW;IAenB,oBAAoB;IACpB,OAAO,CAAC,qBAAqB;IAU7B,uCAAuC;IACvC,OAAO,CAAC,aAAa;IAgCrB,sBAAsB;IACtB,OAAO,CAAC,QAAQ;CAQjB"}