@cloudcli-ai/cloudcli 1.30.0 → 1.31.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.
Files changed (238) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/index-B37ufcoi.js +1262 -0
  3. package/dist/assets/{index-DI22MZMb.css → index-DF8TTVHl.css} +1 -1
  4. package/dist/index.html +2 -2
  5. package/dist-server/server/index.js +115 -955
  6. package/dist-server/server/index.js.map +1 -1
  7. package/dist-server/server/middleware/auth.js +1 -1
  8. package/dist-server/server/middleware/auth.js.map +1 -1
  9. package/dist-server/server/modules/database/connection.js +125 -0
  10. package/dist-server/server/modules/database/connection.js.map +1 -0
  11. package/dist-server/server/modules/database/index.js +13 -0
  12. package/dist-server/server/modules/database/index.js.map +1 -0
  13. package/dist-server/server/modules/database/init-db.js +18 -0
  14. package/dist-server/server/modules/database/init-db.js.map +1 -0
  15. package/dist-server/server/modules/database/migrations.js +370 -0
  16. package/dist-server/server/modules/database/migrations.js.map +1 -0
  17. package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
  18. package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
  19. package/dist-server/server/modules/database/repositories/app-config.js +47 -0
  20. package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
  21. package/dist-server/server/modules/database/repositories/credentials.js +68 -0
  22. package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
  23. package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
  24. package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
  25. package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
  26. package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
  27. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +64 -0
  28. package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
  29. package/dist-server/server/modules/database/repositories/projects.db.js +160 -0
  30. package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
  31. package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
  32. package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
  33. package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
  34. package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
  35. package/dist-server/server/modules/database/repositories/sessions.db.js +109 -0
  36. package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
  37. package/dist-server/server/modules/database/repositories/users.js +88 -0
  38. package/dist-server/server/modules/database/repositories/users.js.map +1 -0
  39. package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
  40. package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
  41. package/dist-server/server/modules/database/schema.js +143 -0
  42. package/dist-server/server/modules/database/schema.js.map +1 -0
  43. package/dist-server/server/modules/projects/index.js +4 -0
  44. package/dist-server/server/modules/projects/index.js.map +1 -0
  45. package/dist-server/server/modules/projects/projects.routes.js +188 -0
  46. package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
  47. package/dist-server/server/modules/projects/services/project-clone.service.js +219 -0
  48. package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
  49. package/dist-server/server/modules/projects/services/project-delete.service.js +65 -0
  50. package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
  51. package/dist-server/server/modules/projects/services/project-management.service.js +93 -0
  52. package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
  53. package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
  54. package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
  55. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
  56. package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
  57. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +169 -0
  58. package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
  59. package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
  60. package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
  61. package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
  62. package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
  63. package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
  64. package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
  65. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
  66. package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
  67. package/dist-server/server/modules/providers/index.js +4 -0
  68. package/dist-server/server/modules/providers/index.js.map +1 -0
  69. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +63 -0
  70. package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
  71. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +144 -7
  72. package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
  73. package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
  74. package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
  75. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +71 -0
  76. package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
  77. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +225 -13
  78. package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
  79. package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
  80. package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
  81. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +127 -0
  82. package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
  83. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
  84. package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
  85. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +296 -0
  86. package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
  87. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +277 -10
  88. package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
  89. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
  90. package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
  91. package/dist-server/server/modules/providers/provider.routes.js +172 -0
  92. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  93. package/dist-server/server/modules/providers/services/session-conversations-search.service.js +868 -0
  94. package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
  95. package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
  96. package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
  97. package/dist-server/server/modules/providers/services/sessions-watcher.service.js +234 -0
  98. package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
  99. package/dist-server/server/modules/providers/services/sessions.service.js +78 -3
  100. package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
  101. package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
  102. package/dist-server/server/modules/websocket/index.js +3 -0
  103. package/dist-server/server/modules/websocket/index.js.map +1 -0
  104. package/dist-server/server/modules/websocket/services/chat-websocket.service.js +188 -0
  105. package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
  106. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
  107. package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
  108. package/dist-server/server/modules/websocket/services/shell-websocket.service.js +334 -0
  109. package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
  110. package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
  111. package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
  112. package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
  113. package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
  114. package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
  115. package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
  116. package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
  117. package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
  118. package/dist-server/server/routes/agent.js +10 -17
  119. package/dist-server/server/routes/agent.js.map +1 -1
  120. package/dist-server/server/routes/auth.js +3 -1
  121. package/dist-server/server/routes/auth.js.map +1 -1
  122. package/dist-server/server/routes/commands.js +1 -1
  123. package/dist-server/server/routes/commands.js.map +1 -1
  124. package/dist-server/server/routes/gemini.js +2 -2
  125. package/dist-server/server/routes/gemini.js.map +1 -1
  126. package/dist-server/server/routes/git.js +33 -29
  127. package/dist-server/server/routes/git.js.map +1 -1
  128. package/dist-server/server/routes/settings.js +1 -1
  129. package/dist-server/server/routes/settings.js.map +1 -1
  130. package/dist-server/server/routes/taskmaster.js +97 -110
  131. package/dist-server/server/routes/taskmaster.js.map +1 -1
  132. package/dist-server/server/routes/user.js +1 -1
  133. package/dist-server/server/routes/user.js.map +1 -1
  134. package/dist-server/server/services/notification-orchestrator.js +2 -2
  135. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  136. package/dist-server/server/services/vapid-keys.js +2 -1
  137. package/dist-server/server/services/vapid-keys.js.map +1 -1
  138. package/dist-server/server/shared/types.js +0 -1
  139. package/dist-server/server/shared/types.js.map +1 -1
  140. package/dist-server/server/shared/utils.js +408 -7
  141. package/dist-server/server/shared/utils.js.map +1 -1
  142. package/dist-server/server/utils/taskmaster-websocket.js +18 -12
  143. package/dist-server/server/utils/taskmaster-websocket.js.map +1 -1
  144. package/dist-server/shared/modelConstants.js +2 -1
  145. package/dist-server/shared/modelConstants.js.map +1 -1
  146. package/package.json +3 -1
  147. package/server/index.js +146 -1012
  148. package/server/middleware/auth.js +1 -1
  149. package/server/modules/database/connection.ts +143 -0
  150. package/server/modules/database/index.ts +12 -0
  151. package/server/modules/database/init-db.ts +17 -0
  152. package/server/modules/database/migrations.ts +442 -0
  153. package/server/modules/database/repositories/api-keys.ts +119 -0
  154. package/server/modules/database/repositories/app-config.ts +53 -0
  155. package/server/modules/database/repositories/credentials.ts +106 -0
  156. package/server/modules/database/repositories/github-tokens.ts +100 -0
  157. package/server/modules/database/repositories/notification-preferences.ts +103 -0
  158. package/server/modules/database/repositories/projects.db.integration.test.ts +72 -0
  159. package/server/modules/database/repositories/projects.db.ts +183 -0
  160. package/server/modules/database/repositories/push-subscriptions.ts +80 -0
  161. package/server/modules/database/repositories/scan-state.db.ts +42 -0
  162. package/server/modules/database/repositories/sessions.db.ts +174 -0
  163. package/server/modules/database/repositories/users.ts +140 -0
  164. package/server/modules/database/repositories/vapid-keys.ts +57 -0
  165. package/server/modules/database/schema.ts +152 -0
  166. package/server/modules/projects/index.ts +6 -0
  167. package/server/modules/projects/projects.routes.ts +247 -0
  168. package/server/modules/projects/services/project-clone.service.ts +321 -0
  169. package/server/modules/projects/services/project-delete.service.ts +75 -0
  170. package/server/modules/projects/services/project-management.service.ts +150 -0
  171. package/server/modules/projects/services/project-star.service.ts +78 -0
  172. package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
  173. package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +285 -0
  174. package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
  175. package/server/modules/projects/tests/project-management.service.test.ts +117 -0
  176. package/server/modules/projects/tests/project-star.service.test.ts +123 -0
  177. package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
  178. package/server/modules/providers/index.ts +4 -0
  179. package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +110 -0
  180. package/server/modules/providers/list/claude/claude-sessions.provider.ts +182 -13
  181. package/server/modules/providers/list/claude/claude.provider.ts +3 -1
  182. package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +119 -0
  183. package/server/modules/providers/list/codex/codex-sessions.provider.ts +262 -17
  184. package/server/modules/providers/list/codex/codex.provider.ts +3 -1
  185. package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +176 -0
  186. package/server/modules/providers/list/cursor/cursor.provider.ts +3 -1
  187. package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +401 -0
  188. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +327 -13
  189. package/server/modules/providers/list/gemini/gemini.provider.ts +3 -1
  190. package/server/modules/providers/provider.routes.ts +220 -12
  191. package/server/modules/providers/services/session-conversations-search.service.ts +1167 -0
  192. package/server/modules/providers/services/session-synchronizer.service.ts +74 -0
  193. package/server/modules/providers/services/sessions-watcher.service.ts +283 -0
  194. package/server/modules/providers/services/sessions.service.ts +89 -4
  195. package/server/modules/providers/shared/base/abstract.provider.ts +8 -1
  196. package/server/modules/websocket/README.md +267 -0
  197. package/server/modules/websocket/index.ts +2 -0
  198. package/server/modules/websocket/services/chat-websocket.service.ts +271 -0
  199. package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
  200. package/server/modules/websocket/services/shell-websocket.service.ts +453 -0
  201. package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
  202. package/server/modules/websocket/services/websocket-server.service.ts +58 -0
  203. package/server/modules/websocket/services/websocket-state.service.ts +16 -0
  204. package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
  205. package/server/routes/agent.js +11 -16
  206. package/server/routes/auth.js +4 -2
  207. package/server/routes/commands.js +1 -1
  208. package/server/routes/gemini.js +3 -2
  209. package/server/routes/git.js +33 -28
  210. package/server/routes/settings.js +1 -1
  211. package/server/routes/taskmaster.js +108 -111
  212. package/server/routes/user.js +1 -1
  213. package/server/services/notification-orchestrator.js +3 -2
  214. package/server/services/vapid-keys.js +2 -1
  215. package/server/shared/interfaces.ts +39 -1
  216. package/server/shared/types.ts +185 -30
  217. package/server/shared/utils.ts +485 -9
  218. package/server/utils/taskmaster-websocket.js +19 -13
  219. package/shared/modelConstants.js +2 -1
  220. package/dist/assets/index-RX-W_g_Q.js +0 -1262
  221. package/dist-server/server/database/db.js +0 -535
  222. package/dist-server/server/database/db.js.map +0 -1
  223. package/dist-server/server/database/schema.js +0 -97
  224. package/dist-server/server/database/schema.js.map +0 -1
  225. package/dist-server/server/projects.js +0 -2325
  226. package/dist-server/server/projects.js.map +0 -1
  227. package/dist-server/server/routes/codex.js +0 -18
  228. package/dist-server/server/routes/codex.js.map +0 -1
  229. package/dist-server/server/routes/messages.js +0 -56
  230. package/dist-server/server/routes/messages.js.map +0 -1
  231. package/dist-server/server/routes/projects.js +0 -505
  232. package/dist-server/server/routes/projects.js.map +0 -1
  233. package/server/database/db.js +0 -593
  234. package/server/database/schema.js +0 -102
  235. package/server/projects.js +0 -2555
  236. package/server/routes/codex.js +0 -19
  237. package/server/routes/messages.js +0 -61
  238. package/server/routes/projects.js +0 -548
@@ -0,0 +1,117 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { createProject } from '@/modules/projects/services/project-management.service.js';
5
+ import { AppError } from '@/shared/utils.js';
6
+
7
+ const projectRow = {
8
+ project_id: 'project-1',
9
+ project_path: '/workspace/my-project',
10
+ custom_project_name: 'my-project',
11
+ isStarred: 0,
12
+ isArchived: 0,
13
+ };
14
+
15
+ test('createProject throws when project path is missing', async () => {
16
+ await assert.rejects(
17
+ async () => createProject({ projectPath: '' }),
18
+ (error: unknown) => {
19
+ assert.ok(error instanceof AppError);
20
+ assert.equal(error.code, 'PROJECT_PATH_REQUIRED');
21
+ assert.equal(error.statusCode, 400);
22
+ return true;
23
+ },
24
+ );
25
+ });
26
+
27
+ test('createProject throws when path validation fails', async () => {
28
+ await assert.rejects(
29
+ async () =>
30
+ createProject(
31
+ { projectPath: '/invalid/path' },
32
+ {
33
+ validatePath: async () => ({ valid: false, error: 'blocked path' }),
34
+ ensureWorkspaceDirectory: async () => undefined,
35
+ persistProjectPath: () => ({ outcome: 'created', project: projectRow }),
36
+ getProjectByPath: () => projectRow,
37
+ },
38
+ ),
39
+ (error: unknown) => {
40
+ assert.ok(error instanceof AppError);
41
+ assert.equal(error.code, 'INVALID_PROJECT_PATH');
42
+ assert.equal(error.statusCode, 400);
43
+ assert.equal(error.details, 'blocked path');
44
+ return true;
45
+ },
46
+ );
47
+ });
48
+
49
+ test('createProject throws conflict when active project path already exists', async () => {
50
+ await assert.rejects(
51
+ async () =>
52
+ createProject(
53
+ { projectPath: '/workspace/my-project' },
54
+ {
55
+ validatePath: async () => ({ valid: true, resolvedPath: '/workspace/my-project' }),
56
+ ensureWorkspaceDirectory: async () => undefined,
57
+ persistProjectPath: () => ({ outcome: 'active_conflict', project: projectRow }),
58
+ getProjectByPath: () => projectRow,
59
+ },
60
+ ),
61
+ (error: unknown) => {
62
+ assert.ok(error instanceof AppError);
63
+ assert.equal(error.code, 'PROJECT_ALREADY_EXISTS');
64
+ assert.equal(error.statusCode, 409);
65
+ assert.equal(error.details, 'Project path already exists: /workspace/my-project');
66
+ return true;
67
+ },
68
+ );
69
+ });
70
+
71
+ test('createProject falls back to directory name when custom name is not provided', async () => {
72
+ let capturedCustomName: string | null = null;
73
+
74
+ const result = await createProject(
75
+ { projectPath: '/workspace/my-project', customName: '' },
76
+ {
77
+ validatePath: async () => ({ valid: true, resolvedPath: '/workspace/my-project' }),
78
+ ensureWorkspaceDirectory: async () => undefined,
79
+ persistProjectPath: (_projectPath, customName) => {
80
+ capturedCustomName = customName;
81
+ return {
82
+ outcome: 'created',
83
+ project: {
84
+ ...projectRow,
85
+ custom_project_name: customName,
86
+ },
87
+ };
88
+ },
89
+ getProjectByPath: () => projectRow,
90
+ },
91
+ );
92
+
93
+ assert.equal(capturedCustomName, 'my-project');
94
+ assert.equal(result.outcome, 'created');
95
+ assert.equal(result.project.displayName, 'my-project');
96
+ });
97
+
98
+ test('createProject returns archived reuse outcome when archived row is reused', async () => {
99
+ const result = await createProject(
100
+ { projectPath: '/workspace/my-project' },
101
+ {
102
+ validatePath: async () => ({ valid: true, resolvedPath: '/workspace/my-project' }),
103
+ ensureWorkspaceDirectory: async () => undefined,
104
+ persistProjectPath: () => ({
105
+ outcome: 'reactivated_archived',
106
+ project: {
107
+ ...projectRow,
108
+ isArchived: 1,
109
+ },
110
+ }),
111
+ getProjectByPath: () => projectRow,
112
+ },
113
+ );
114
+
115
+ assert.equal(result.outcome, 'reactivated_archived');
116
+ assert.equal(result.project.isArchived, true);
117
+ });
@@ -0,0 +1,123 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import { projectsDb } from '@/modules/database/index.js';
5
+ import { applyLegacyStarredProjectIds, toggleProjectStar } from '@/modules/projects/services/project-star.service.js';
6
+ import { AppError } from '@/shared/utils.js';
7
+
8
+ type ProjectRow = {
9
+ project_id: string;
10
+ project_path: string;
11
+ custom_project_name: string | null;
12
+ isStarred: number;
13
+ isArchived: number;
14
+ };
15
+
16
+ test('toggleProjectStar throws when projectId is missing', () => {
17
+ assert.throws(
18
+ () => toggleProjectStar(' '),
19
+ (error: unknown) =>
20
+ error instanceof AppError
21
+ && error.code === 'PROJECT_ID_REQUIRED'
22
+ && error.statusCode === 400,
23
+ );
24
+ });
25
+
26
+ test('toggleProjectStar throws when project does not exist', () => {
27
+ const originalGetProjectById = projectsDb.getProjectById;
28
+ try {
29
+ projectsDb.getProjectById = () => null;
30
+ assert.throws(
31
+ () => toggleProjectStar('project-1'),
32
+ (error: unknown) =>
33
+ error instanceof AppError
34
+ && error.code === 'PROJECT_NOT_FOUND'
35
+ && error.statusCode === 404,
36
+ );
37
+ } finally {
38
+ projectsDb.getProjectById = originalGetProjectById;
39
+ }
40
+ });
41
+
42
+ test('toggleProjectStar flips star state and persists it', () => {
43
+ const originalGetProjectById = projectsDb.getProjectById;
44
+ const originalUpdateProjectIsStarredById = projectsDb.updateProjectIsStarredById;
45
+
46
+ let capturedProjectId = '';
47
+ let capturedState = false;
48
+
49
+ try {
50
+ projectsDb.getProjectById = () =>
51
+ ({
52
+ project_id: 'project-1',
53
+ project_path: '/workspace/project-1',
54
+ custom_project_name: 'project-1',
55
+ isStarred: 0,
56
+ isArchived: 0,
57
+ }) as ProjectRow;
58
+ projectsDb.updateProjectIsStarredById = (projectId: string, isStarred: boolean) => {
59
+ capturedProjectId = projectId;
60
+ capturedState = isStarred;
61
+ };
62
+
63
+ const result = toggleProjectStar('project-1');
64
+
65
+ assert.equal(result.isStarred, true);
66
+ assert.equal(capturedProjectId, 'project-1');
67
+ assert.equal(capturedState, true);
68
+ } finally {
69
+ projectsDb.getProjectById = originalGetProjectById;
70
+ projectsDb.updateProjectIsStarredById = originalUpdateProjectIsStarredById;
71
+ }
72
+ });
73
+
74
+ test('applyLegacyStarredProjectIds stars only valid, unstarred projects', () => {
75
+ const originalGetProjectById = projectsDb.getProjectById;
76
+ const originalUpdateProjectIsStarredById = projectsDb.updateProjectIsStarredById;
77
+
78
+ const updatedProjectIds: string[] = [];
79
+
80
+ try {
81
+ projectsDb.getProjectById = (projectId: string) => {
82
+ if (projectId === 'project-a') {
83
+ return {
84
+ project_id: 'project-a',
85
+ project_path: '/workspace/project-a',
86
+ custom_project_name: 'A',
87
+ isStarred: 0,
88
+ isArchived: 0,
89
+ } as ProjectRow;
90
+ }
91
+
92
+ if (projectId === 'project-b') {
93
+ return {
94
+ project_id: 'project-b',
95
+ project_path: '/workspace/project-b',
96
+ custom_project_name: 'B',
97
+ isStarred: 1,
98
+ isArchived: 0,
99
+ } as ProjectRow;
100
+ }
101
+
102
+ return null;
103
+ };
104
+ projectsDb.updateProjectIsStarredById = (projectId: string) => {
105
+ updatedProjectIds.push(projectId);
106
+ };
107
+
108
+ const result = applyLegacyStarredProjectIds([
109
+ 'project-a',
110
+ 'project-b',
111
+ 'missing-project',
112
+ 'project-a',
113
+ '',
114
+ ' ',
115
+ ]);
116
+
117
+ assert.equal(result.updated, 1);
118
+ assert.deepEqual(updatedProjectIds, ['project-a']);
119
+ } finally {
120
+ projectsDb.getProjectById = originalGetProjectById;
121
+ projectsDb.updateProjectIsStarredById = originalUpdateProjectIsStarredById;
122
+ }
123
+ });
@@ -0,0 +1,105 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ getProjectTaskMaster,
6
+ getProjectTaskMasterById,
7
+ } from '@/modules/projects/services/projects-has-taskmaster.service.js';
8
+ import { AppError } from '@/shared/utils.js';
9
+
10
+ test('getProjectTaskMasterById returns null when project path is missing', async () => {
11
+ const result = await getProjectTaskMasterById('project-1', {
12
+ resolveProjectPathById: () => null,
13
+ detectTaskMasterFolder: async () => {
14
+ throw new Error('detectTaskMasterFolder should not be called when path is missing');
15
+ },
16
+ });
17
+
18
+ assert.equal(result, null);
19
+ });
20
+
21
+ test('getProjectTaskMasterById returns configured status when taskmaster exists with essential files', async () => {
22
+ const result = await getProjectTaskMasterById('project-1', {
23
+ resolveProjectPathById: () => '/workspace/project-1',
24
+ detectTaskMasterFolder: async () => ({
25
+ hasTaskmaster: true,
26
+ hasEssentialFiles: true,
27
+ metadata: {
28
+ taskCount: 3,
29
+ subtaskCount: 0,
30
+ completed: 1,
31
+ pending: 2,
32
+ inProgress: 0,
33
+ review: 0,
34
+ completionPercentage: 33,
35
+ lastModified: '2026-01-01T00:00:00.000Z',
36
+ },
37
+ }),
38
+ });
39
+
40
+ assert.ok(result);
41
+ assert.equal(result.projectId, 'project-1');
42
+ assert.equal(result.projectPath, '/workspace/project-1');
43
+ assert.equal(result.taskmaster.hasTaskmaster, true);
44
+ assert.equal(result.taskmaster.hasEssentialFiles, true);
45
+ assert.equal(result.taskmaster.status, 'configured');
46
+ assert.deepEqual(result.taskmaster.metadata, {
47
+ taskCount: 3,
48
+ subtaskCount: 0,
49
+ completed: 1,
50
+ pending: 2,
51
+ inProgress: 0,
52
+ review: 0,
53
+ completionPercentage: 33,
54
+ lastModified: '2026-01-01T00:00:00.000Z',
55
+ });
56
+ });
57
+
58
+ test('getProjectTaskMasterById returns not-configured status when taskmaster is missing', async () => {
59
+ const result = await getProjectTaskMasterById('project-1', {
60
+ resolveProjectPathById: () => '/workspace/project-1',
61
+ detectTaskMasterFolder: async () => ({
62
+ hasTaskmaster: false,
63
+ }),
64
+ });
65
+
66
+ assert.ok(result);
67
+ assert.equal(result.taskmaster.hasTaskmaster, false);
68
+ assert.equal(result.taskmaster.hasEssentialFiles, false);
69
+ assert.equal(result.taskmaster.status, 'not-configured');
70
+ assert.equal(result.taskmaster.metadata, null);
71
+ });
72
+
73
+ test('getProjectTaskMaster throws when project id is missing', async () => {
74
+ await assert.rejects(
75
+ async () =>
76
+ getProjectTaskMaster('', async () => ({
77
+ projectId: 'project-1',
78
+ projectPath: '/workspace/project-1',
79
+ taskmaster: {
80
+ hasTaskmaster: true,
81
+ hasEssentialFiles: true,
82
+ metadata: null,
83
+ status: 'configured',
84
+ },
85
+ })),
86
+ (error: unknown) => {
87
+ assert.ok(error instanceof AppError);
88
+ assert.equal(error.code, 'PROJECT_ID_REQUIRED');
89
+ assert.equal(error.statusCode, 400);
90
+ return true;
91
+ },
92
+ );
93
+ });
94
+
95
+ test('getProjectTaskMaster throws when project does not exist', async () => {
96
+ await assert.rejects(
97
+ async () => getProjectTaskMaster('project-that-does-not-exist', async () => null),
98
+ (error: unknown) => {
99
+ assert.ok(error instanceof AppError);
100
+ assert.equal(error.code, 'PROJECT_NOT_FOUND');
101
+ assert.equal(error.statusCode, 404);
102
+ return true;
103
+ },
104
+ );
105
+ });
@@ -0,0 +1,4 @@
1
+ export { sessionSynchronizerService } from './services/session-synchronizer.service.js';
2
+
3
+ export { initializeSessionsWatcher } from './services/sessions-watcher.service.js';
4
+ export { closeSessionsWatcher } from './services/sessions-watcher.service.js';
@@ -0,0 +1,110 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ import { sessionsDb } from '@/modules/database/index.js';
5
+ import {
6
+ buildLookupMap,
7
+ extractFirstValidJsonlData,
8
+ findFilesRecursivelyCreatedAfter,
9
+ normalizeSessionName,
10
+ readFileTimestamps,
11
+ } from '@/shared/utils.js';
12
+ import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js';
13
+
14
+ type ParsedSession = {
15
+ sessionId: string;
16
+ projectPath: string;
17
+ sessionName?: string;
18
+ };
19
+
20
+ /**
21
+ * Session indexer for Claude transcript artifacts.
22
+ */
23
+ export class ClaudeSessionSynchronizer implements IProviderSessionSynchronizer {
24
+ private readonly provider = 'claude' as const;
25
+ private readonly claudeHome = path.join(os.homedir(), '.claude');
26
+
27
+ /**
28
+ * Scans ~/.claude/projects and upserts discovered sessions into DB.
29
+ */
30
+ async synchronize(since?: Date): Promise<number> {
31
+ const nameMap = await buildLookupMap(path.join(this.claudeHome, 'history.jsonl'), 'sessionId', 'display');
32
+ const files = await findFilesRecursivelyCreatedAfter(
33
+ path.join(this.claudeHome, 'projects'),
34
+ '.jsonl',
35
+ since ?? null
36
+ );
37
+
38
+ let processed = 0;
39
+ for (const filePath of files) {
40
+ const parsed = await this.processSessionFile(filePath, nameMap);
41
+ if (!parsed) {
42
+ continue;
43
+ }
44
+
45
+ const timestamps = await readFileTimestamps(filePath);
46
+ sessionsDb.createSession(
47
+ parsed.sessionId,
48
+ this.provider,
49
+ parsed.projectPath,
50
+ parsed.sessionName,
51
+ timestamps.createdAt,
52
+ timestamps.updatedAt,
53
+ filePath
54
+ );
55
+ processed += 1;
56
+ }
57
+
58
+ return processed;
59
+ }
60
+
61
+ /**
62
+ * Parses and upserts one Claude session JSONL file.
63
+ */
64
+ async synchronizeFile(filePath: string): Promise<string | null> {
65
+ if (!filePath.endsWith('.jsonl')) {
66
+ return null;
67
+ }
68
+
69
+ const nameMap = await buildLookupMap(path.join(this.claudeHome, 'history.jsonl'), 'sessionId', 'display');
70
+ const parsed = await this.processSessionFile(filePath, nameMap);
71
+ if (!parsed) {
72
+ return null;
73
+ }
74
+
75
+ const timestamps = await readFileTimestamps(filePath);
76
+ return sessionsDb.createSession(
77
+ parsed.sessionId,
78
+ this.provider,
79
+ parsed.projectPath,
80
+ parsed.sessionName,
81
+ timestamps.createdAt,
82
+ timestamps.updatedAt,
83
+ filePath
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Extracts session metadata from one Claude JSONL session file.
89
+ */
90
+ private async processSessionFile(
91
+ filePath: string,
92
+ nameMap: Map<string, string>
93
+ ): Promise<ParsedSession | null> {
94
+ return extractFirstValidJsonlData(filePath, (rawData) => {
95
+ const data = rawData as Record<string, unknown>;
96
+ const sessionId = typeof data.sessionId === 'string' ? data.sessionId : undefined;
97
+ const projectPath = typeof data.cwd === 'string' ? data.cwd : undefined;
98
+
99
+ if (!sessionId || !projectPath) {
100
+ return null;
101
+ }
102
+
103
+ return {
104
+ sessionId,
105
+ projectPath,
106
+ sessionName: normalizeSessionName(nameMap.get(sessionId), 'Untitled Claude Session'),
107
+ };
108
+ });
109
+ }
110
+ }
@@ -1,7 +1,12 @@
1
- import { getSessionMessages } from '@/projects.js';
1
+ import fs from 'node:fs';
2
+ import fsp from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import readline from 'node:readline';
5
+
2
6
  import type { IProviderSessions } from '@/shared/interfaces.js';
3
7
  import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
4
8
  import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
9
+ import { sessionsDb } from '@/modules/database/index.js';
5
10
 
6
11
  const PROVIDER = 'claude';
7
12
 
@@ -15,17 +20,184 @@ type ClaudeToolResult = {
15
20
  type ClaudeHistoryResult =
16
21
  | AnyRecord[]
17
22
  | {
18
- messages?: AnyRecord[];
19
- total?: number;
20
- hasMore?: boolean;
21
- };
23
+ messages?: AnyRecord[];
24
+ total?: number;
25
+ hasMore?: boolean;
26
+ };
27
+
28
+ type ClaudeHistoryMessagesResult =
29
+ | AnyRecord[]
30
+ | {
31
+ messages: AnyRecord[];
32
+ total: number;
33
+ hasMore: boolean;
34
+ offset?: number;
35
+ limit?: number | null;
36
+ };
37
+
38
+ async function parseAgentTools(filePath: string): Promise<AnyRecord[]> {
39
+ const tools: AnyRecord[] = [];
40
+
41
+ try {
42
+ const fileStream = fs.createReadStream(filePath);
43
+ const rl = readline.createInterface({
44
+ input: fileStream,
45
+ crlfDelay: Infinity,
46
+ });
47
+
48
+ for await (const line of rl) {
49
+ if (!line.trim()) {
50
+ continue;
51
+ }
22
52
 
23
- const loadClaudeSessionMessages = getSessionMessages as unknown as (
24
- projectName: string,
53
+ try {
54
+ const entry = JSON.parse(line) as AnyRecord;
55
+
56
+ if (entry.message?.role === 'assistant' && Array.isArray(entry.message?.content)) {
57
+ for (const part of entry.message.content as AnyRecord[]) {
58
+ if (part.type === 'tool_use') {
59
+ tools.push({
60
+ toolId: part.id,
61
+ toolName: part.name,
62
+ toolInput: part.input,
63
+ timestamp: entry.timestamp,
64
+ });
65
+ }
66
+ }
67
+ }
68
+
69
+ if (entry.message?.role === 'user' && Array.isArray(entry.message?.content)) {
70
+ for (const part of entry.message.content as AnyRecord[]) {
71
+ if (part.type !== 'tool_result') {
72
+ continue;
73
+ }
74
+
75
+ const tool = tools.find((candidate) => candidate.toolId === part.tool_use_id);
76
+ if (!tool) {
77
+ continue;
78
+ }
79
+
80
+ tool.toolResult = {
81
+ content: typeof part.content === 'string'
82
+ ? part.content
83
+ : Array.isArray(part.content)
84
+ ? part.content
85
+ .map((contentPart: AnyRecord) => contentPart?.text || '')
86
+ .join('\n')
87
+ : JSON.stringify(part.content),
88
+ isError: Boolean(part.is_error),
89
+ };
90
+ }
91
+ }
92
+ } catch {
93
+ // Skip malformed lines that can happen during concurrent writes.
94
+ }
95
+ }
96
+ } catch (error) {
97
+ const message = error instanceof Error ? error.message : String(error);
98
+ console.warn(`Error parsing agent file ${filePath}:`, message);
99
+ }
100
+
101
+ return tools;
102
+ }
103
+
104
+ async function getSessionMessages(
25
105
  sessionId: string,
26
106
  limit: number | null,
27
107
  offset: number,
28
- ) => Promise<ClaudeHistoryResult>;
108
+ ): Promise<ClaudeHistoryMessagesResult> {
109
+ try {
110
+ const jsonLPath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
111
+
112
+ if (!jsonLPath) {
113
+ return { messages: [], total: 0, hasMore: false };
114
+ }
115
+
116
+ const projectDir = path.dirname(jsonLPath);
117
+ const files = await fsp.readdir(projectDir);
118
+ const agentFiles = files.filter((file) => file.endsWith('.jsonl') && file.startsWith('agent-'));
119
+
120
+ const messages: AnyRecord[] = [];
121
+ const agentToolsCache = new Map<string, AnyRecord[]>();
122
+
123
+ const fileStream = fs.createReadStream(jsonLPath);
124
+ const rl = readline.createInterface({
125
+ input: fileStream,
126
+ crlfDelay: Infinity,
127
+ });
128
+
129
+ for await (const line of rl) {
130
+ if (!line.trim()) {
131
+ continue;
132
+ }
133
+
134
+ try {
135
+ const entry = JSON.parse(line) as AnyRecord;
136
+ if (entry.sessionId === sessionId) {
137
+ messages.push(entry);
138
+ }
139
+ } catch {
140
+ // Skip malformed JSONL lines that can happen during concurrent writes.
141
+ }
142
+ }
143
+
144
+ const agentIds = new Set<string>();
145
+ for (const message of messages) {
146
+ const agentId = message.toolUseResult?.agentId;
147
+ if (agentId) {
148
+ agentIds.add(String(agentId));
149
+ }
150
+ }
151
+
152
+ for (const agentId of agentIds) {
153
+ const agentFileName = `agent-${agentId}.jsonl`;
154
+ if (!agentFiles.includes(agentFileName)) {
155
+ continue;
156
+ }
157
+
158
+ const agentFilePath = path.join(projectDir, agentFileName);
159
+ const tools = await parseAgentTools(agentFilePath);
160
+ agentToolsCache.set(agentId, tools);
161
+ }
162
+
163
+ for (const message of messages) {
164
+ const agentId = message.toolUseResult?.agentId;
165
+ if (!agentId) {
166
+ continue;
167
+ }
168
+
169
+ const agentTools = agentToolsCache.get(String(agentId));
170
+ if (agentTools && agentTools.length > 0) {
171
+ message.subagentTools = agentTools;
172
+ }
173
+ }
174
+
175
+ const sortedMessages = messages.sort(
176
+ (a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime(),
177
+ );
178
+ const total = sortedMessages.length;
179
+
180
+ if (limit === null) {
181
+ return sortedMessages;
182
+ }
183
+
184
+ const startIndex = Math.max(0, total - offset - limit);
185
+ const endIndex = total - offset;
186
+ const paginatedMessages = sortedMessages.slice(startIndex, endIndex);
187
+ const hasMore = startIndex > 0;
188
+
189
+ return {
190
+ messages: paginatedMessages,
191
+ total,
192
+ hasMore,
193
+ offset,
194
+ limit,
195
+ };
196
+ } catch (error) {
197
+ console.error(`Error reading messages for session ${sessionId}:`, error);
198
+ return limit === null ? [] : { messages: [], total: 0, hasMore: false };
199
+ }
200
+ }
29
201
 
30
202
  /**
31
203
  * Claude writes internal command and system reminder entries into history.
@@ -238,14 +410,11 @@ export class ClaudeSessionsProvider implements IProviderSessions {
238
410
  sessionId: string,
239
411
  options: FetchHistoryOptions = {},
240
412
  ): Promise<FetchHistoryResult> {
241
- const { projectName, limit = null, offset = 0 } = options;
242
- if (!projectName) {
243
- return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
244
- }
413
+ const { limit = null, offset = 0 } = options;
245
414
 
246
415
  let result: ClaudeHistoryResult;
247
416
  try {
248
- result = await loadClaudeSessionMessages(projectName, sessionId, limit, offset);
417
+ result = await getSessionMessages(sessionId, limit, offset);
249
418
  } catch (error) {
250
419
  const message = error instanceof Error ? error.message : String(error);
251
420
  console.warn(`[ClaudeProvider] Failed to load session ${sessionId}:`, message);
@@ -1,13 +1,15 @@
1
1
  import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
2
2
  import { ClaudeProviderAuth } from '@/modules/providers/list/claude/claude-auth.provider.js';
3
3
  import { ClaudeMcpProvider } from '@/modules/providers/list/claude/claude-mcp.provider.js';
4
+ import { ClaudeSessionSynchronizer } from '@/modules/providers/list/claude/claude-session-synchronizer.provider.js';
4
5
  import { ClaudeSessionsProvider } from '@/modules/providers/list/claude/claude-sessions.provider.js';
5
- import type { IProviderAuth, IProviderSessions } from '@/shared/interfaces.js';
6
+ import type { IProviderAuth, IProviderSessionSynchronizer, IProviderSessions } from '@/shared/interfaces.js';
6
7
 
7
8
  export class ClaudeProvider extends AbstractProvider {
8
9
  readonly mcp = new ClaudeMcpProvider();
9
10
  readonly auth: IProviderAuth = new ClaudeProviderAuth();
10
11
  readonly sessions: IProviderSessions = new ClaudeSessionsProvider();
12
+ readonly sessionSynchronizer: IProviderSessionSynchronizer = new ClaudeSessionSynchronizer();
11
13
 
12
14
  constructor() {
13
15
  super('claude');