@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,248 @@
1
+ import { access, readFile, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { projectsDb } from '@/modules/database/index.js';
5
+ import { AppError } from '@/shared/utils.js';
6
+
7
+ type TaskMasterTask = {
8
+ status?: string;
9
+ subtasks?: Array<{
10
+ status?: string;
11
+ }>;
12
+ };
13
+
14
+ type TaskMasterMetadata =
15
+ | {
16
+ taskCount: number;
17
+ subtaskCount: number;
18
+ completed: number;
19
+ pending: number;
20
+ inProgress: number;
21
+ review: number;
22
+ completionPercentage: number;
23
+ lastModified: string;
24
+ }
25
+ | {
26
+ error: string;
27
+ }
28
+ | null;
29
+
30
+ type TaskMasterDetectionResult = {
31
+ hasTaskmaster: boolean;
32
+ hasEssentialFiles?: boolean;
33
+ files?: Record<string, boolean>;
34
+ metadata?: TaskMasterMetadata;
35
+ path?: string;
36
+ reason?: string;
37
+ };
38
+
39
+ type NormalizedTaskMasterInfo = {
40
+ hasTaskmaster: boolean;
41
+ hasEssentialFiles: boolean;
42
+ metadata: TaskMasterMetadata;
43
+ status: 'configured' | 'not-configured';
44
+ };
45
+
46
+ type GetProjectTaskMasterByIdResult = {
47
+ projectId: string;
48
+ projectPath: string;
49
+ taskmaster: NormalizedTaskMasterInfo;
50
+ };
51
+
52
+ type GetProjectTaskMasterDependencies = {
53
+ resolveProjectPathById: (projectId: string) => string | null;
54
+ detectTaskMasterFolder: (projectPath: string) => Promise<TaskMasterDetectionResult>;
55
+ };
56
+
57
+ type GetProjectTaskMasterResolver = (projectId: string) => Promise<GetProjectTaskMasterByIdResult | null>;
58
+
59
+ function extractTasksFromJson(tasksData: unknown): TaskMasterTask[] {
60
+ if (!tasksData || typeof tasksData !== 'object') {
61
+ return [];
62
+ }
63
+
64
+ const legacyTasks = (tasksData as { tasks?: unknown }).tasks;
65
+ if (Array.isArray(legacyTasks)) {
66
+ return legacyTasks as TaskMasterTask[];
67
+ }
68
+
69
+ const taggedTaskCollections: TaskMasterTask[] = [];
70
+ for (const tagValue of Object.values(tasksData)) {
71
+ if (!tagValue || typeof tagValue !== 'object') {
72
+ continue;
73
+ }
74
+
75
+ const tagTasks = (tagValue as { tasks?: unknown }).tasks;
76
+ if (Array.isArray(tagTasks)) {
77
+ taggedTaskCollections.push(...(tagTasks as TaskMasterTask[]));
78
+ }
79
+ }
80
+
81
+ return taggedTaskCollections;
82
+ }
83
+
84
+ async function detectTaskMasterFolder(projectPath: string): Promise<TaskMasterDetectionResult> {
85
+ try {
86
+ const taskMasterPath = path.join(projectPath, '.taskmaster');
87
+
88
+ try {
89
+ const taskMasterStats = await stat(taskMasterPath);
90
+ if (!taskMasterStats.isDirectory()) {
91
+ return {
92
+ hasTaskmaster: false,
93
+ reason: '.taskmaster exists but is not a directory',
94
+ };
95
+ }
96
+ } catch (error) {
97
+ const fileError = error as NodeJS.ErrnoException;
98
+ if (fileError.code === 'ENOENT') {
99
+ return {
100
+ hasTaskmaster: false,
101
+ reason: '.taskmaster directory not found',
102
+ };
103
+ }
104
+
105
+ throw fileError;
106
+ }
107
+
108
+ const keyFiles = ['tasks/tasks.json', 'config.json'];
109
+ const fileStatus: Record<string, boolean> = {};
110
+ let hasEssentialFiles = true;
111
+
112
+ for (const fileName of keyFiles) {
113
+ const absoluteFilePath = path.join(taskMasterPath, fileName);
114
+ try {
115
+ await access(absoluteFilePath);
116
+ fileStatus[fileName] = true;
117
+ } catch {
118
+ fileStatus[fileName] = false;
119
+ if (fileName === 'tasks/tasks.json') {
120
+ hasEssentialFiles = false;
121
+ }
122
+ }
123
+ }
124
+
125
+ let taskMetadata: TaskMasterMetadata = null;
126
+ if (fileStatus['tasks/tasks.json']) {
127
+ const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
128
+ try {
129
+ const tasksContent = await readFile(tasksPath, 'utf8');
130
+ const parsedTasksJson = JSON.parse(tasksContent) as unknown;
131
+ const tasks = extractTasksFromJson(parsedTasksJson);
132
+
133
+ const stats = tasks.reduce(
134
+ (accumulator, currentTask) => {
135
+ accumulator.total += 1;
136
+ const normalizedTaskStatus = currentTask.status || 'pending';
137
+ accumulator.byStatus[normalizedTaskStatus] = (accumulator.byStatus[normalizedTaskStatus] || 0) + 1;
138
+
139
+ if (Array.isArray(currentTask.subtasks)) {
140
+ for (const subtask of currentTask.subtasks) {
141
+ accumulator.subtotalTasks += 1;
142
+ const normalizedSubtaskStatus = subtask.status || 'pending';
143
+ accumulator.subtaskByStatus[normalizedSubtaskStatus] =
144
+ (accumulator.subtaskByStatus[normalizedSubtaskStatus] || 0) + 1;
145
+ }
146
+ }
147
+
148
+ return accumulator;
149
+ },
150
+ {
151
+ total: 0,
152
+ subtotalTasks: 0,
153
+ byStatus: {} as Record<string, number>,
154
+ subtaskByStatus: {} as Record<string, number>,
155
+ },
156
+ );
157
+
158
+ const tasksStat = await stat(tasksPath);
159
+ taskMetadata = {
160
+ taskCount: stats.total,
161
+ subtaskCount: stats.subtotalTasks,
162
+ completed: stats.byStatus.done || 0,
163
+ pending: stats.byStatus.pending || 0,
164
+ inProgress: stats.byStatus['in-progress'] || 0,
165
+ review: stats.byStatus.review || 0,
166
+ completionPercentage: stats.total > 0 ? Math.round(((stats.byStatus.done || 0) / stats.total) * 100) : 0,
167
+ lastModified: tasksStat.mtime.toISOString(),
168
+ };
169
+ } catch (parseError) {
170
+ console.warn('Failed to parse tasks.json:', (parseError as Error).message);
171
+ taskMetadata = {
172
+ error: 'Failed to parse tasks.json',
173
+ };
174
+ }
175
+ }
176
+
177
+ return {
178
+ hasTaskmaster: true,
179
+ hasEssentialFiles,
180
+ files: fileStatus,
181
+ metadata: taskMetadata,
182
+ path: taskMasterPath,
183
+ };
184
+ } catch (error) {
185
+ console.error('Error detecting TaskMaster folder:', error);
186
+ return {
187
+ hasTaskmaster: false,
188
+ reason: `Error checking directory: ${(error as Error).message}`,
189
+ };
190
+ }
191
+ }
192
+
193
+ function normalizeTaskMasterInfo(taskMasterResult: TaskMasterDetectionResult | null = null): NormalizedTaskMasterInfo {
194
+ const hasTaskmaster = Boolean(taskMasterResult?.hasTaskmaster);
195
+ const hasEssentialFiles = Boolean(taskMasterResult?.hasEssentialFiles);
196
+
197
+ return {
198
+ hasTaskmaster,
199
+ hasEssentialFiles,
200
+ metadata: taskMasterResult?.metadata ?? null,
201
+ status: hasTaskmaster && hasEssentialFiles ? 'configured' : 'not-configured',
202
+ };
203
+ }
204
+
205
+ const defaultDependencies: GetProjectTaskMasterDependencies = {
206
+ resolveProjectPathById: (projectId: string): string | null => projectsDb.getProjectPathById(projectId),
207
+ detectTaskMasterFolder,
208
+ };
209
+
210
+ export async function getProjectTaskMasterById(
211
+ projectId: string,
212
+ dependencies: GetProjectTaskMasterDependencies = defaultDependencies,
213
+ ): Promise<GetProjectTaskMasterByIdResult | null> {
214
+ const projectPath = dependencies.resolveProjectPathById(projectId);
215
+ if (!projectPath) {
216
+ return null;
217
+ }
218
+
219
+ const taskMasterResult = await dependencies.detectTaskMasterFolder(projectPath);
220
+ return {
221
+ projectId,
222
+ projectPath,
223
+ taskmaster: normalizeTaskMasterInfo(taskMasterResult),
224
+ };
225
+ }
226
+
227
+ export async function getProjectTaskMaster(
228
+ projectId: string,
229
+ resolveById: GetProjectTaskMasterResolver = getProjectTaskMasterById,
230
+ ): Promise<GetProjectTaskMasterByIdResult> {
231
+ const normalizedProjectId = projectId.trim();
232
+ if (!normalizedProjectId) {
233
+ throw new AppError('projectId is required', {
234
+ code: 'PROJECT_ID_REQUIRED',
235
+ statusCode: 400,
236
+ });
237
+ }
238
+
239
+ const taskMasterDetails = await resolveById(normalizedProjectId);
240
+ if (!taskMasterDetails) {
241
+ throw new AppError('Project not found', {
242
+ code: 'PROJECT_NOT_FOUND',
243
+ statusCode: 404,
244
+ });
245
+ }
246
+
247
+ return taskMasterDetails;
248
+ }
@@ -0,0 +1,285 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+
4
+ import { projectsDb, sessionsDb } from '@/modules/database/index.js';
5
+ import { sessionSynchronizerService } from '@/modules/providers/index.js';
6
+ import { WS_OPEN_STATE, connectedClients } from '@/modules/websocket/index.js';
7
+ import type { RealtimeClientConnection } from '@/shared/types.js';
8
+ import { AppError } from '@/shared/utils.js';
9
+
10
+ type SessionSummary = {
11
+ id: string;
12
+ summary: string;
13
+ messageCount: number;
14
+ lastActivity: string;
15
+ };
16
+
17
+ type SessionsByProvider = Record<'claude' | 'cursor' | 'codex' | 'gemini', SessionSummary[]>;
18
+
19
+ type SessionRepositoryRow = {
20
+ provider: string;
21
+ session_id: string;
22
+ custom_name?: string | null;
23
+ updated_at?: string | null;
24
+ created_at?: string | null;
25
+ };
26
+
27
+ export type ProjectListItem = {
28
+ projectId: string;
29
+ path: string;
30
+ displayName: string;
31
+ fullPath: string;
32
+ isStarred: boolean;
33
+ sessions: SessionSummary[];
34
+ cursorSessions: SessionSummary[];
35
+ codexSessions: SessionSummary[];
36
+ geminiSessions: SessionSummary[];
37
+ sessionMeta: {
38
+ hasMore: boolean;
39
+ total: number;
40
+ };
41
+ };
42
+
43
+ type ProgressUpdate = {
44
+ phase: 'loading' | 'complete';
45
+ current: number;
46
+ total: number;
47
+ currentProject?: string;
48
+ };
49
+
50
+ type GetProjectsWithSessionsOptions = {
51
+ skipSynchronization?: boolean;
52
+ sessionsLimit?: number;
53
+ sessionsOffset?: number;
54
+ };
55
+
56
+ type SessionPaginationOptions = {
57
+ limit?: number;
58
+ offset?: number;
59
+ };
60
+
61
+ type ProjectSessionsPageResult = {
62
+ sessionsByProvider: SessionsByProvider;
63
+ total: number;
64
+ hasMore: boolean;
65
+ };
66
+
67
+ export type ProjectSessionsPageApiView = {
68
+ projectId: string;
69
+ sessions: SessionSummary[];
70
+ cursorSessions: SessionSummary[];
71
+ codexSessions: SessionSummary[];
72
+ geminiSessions: SessionSummary[];
73
+ sessionMeta: {
74
+ hasMore: boolean;
75
+ total: number;
76
+ };
77
+ };
78
+
79
+ const DEFAULT_PROJECT_SESSIONS_PAGE_SIZE = 20;
80
+ const MAX_PROJECT_SESSIONS_PAGE_SIZE = 200;
81
+
82
+ /**
83
+ * Generate better display name from path.
84
+ */
85
+ export async function generateDisplayName(projectName: string, actualProjectDir: string | null = null): Promise<string> {
86
+ // Use actual project directory if provided, otherwise decode from project name.
87
+ const projectPath = actualProjectDir || projectName.replace(/-/g, '/');
88
+
89
+ // Try to read package.json from the project path.
90
+ try {
91
+ const packageJsonPath = path.join(projectPath, 'package.json');
92
+ const packageData = await fs.readFile(packageJsonPath, 'utf8');
93
+ const packageJson = JSON.parse(packageData) as { name?: string };
94
+
95
+ // Return the name from package.json if it exists.
96
+ if (packageJson.name) {
97
+ return packageJson.name;
98
+ }
99
+ } catch {
100
+ // Fall back to path-based naming if package.json doesn't exist or can't be read.
101
+ }
102
+
103
+ // If it starts with /, it's an absolute path.
104
+ if (projectPath.startsWith('/')) {
105
+ const parts = projectPath.split('/').filter(Boolean);
106
+ // Return only the last folder name.
107
+ return parts[parts.length - 1] || projectPath;
108
+ }
109
+
110
+ return projectPath;
111
+ }
112
+
113
+ function normalizeSessionPagination(options: SessionPaginationOptions = {}): { limit: number; offset: number } {
114
+ const rawLimit = Number.isFinite(options.limit) ? Math.floor(Number(options.limit)) : DEFAULT_PROJECT_SESSIONS_PAGE_SIZE;
115
+ const rawOffset = Number.isFinite(options.offset) ? Math.floor(Number(options.offset)) : 0;
116
+
117
+ return {
118
+ limit: Math.min(Math.max(1, rawLimit), MAX_PROJECT_SESSIONS_PAGE_SIZE),
119
+ offset: Math.max(0, rawOffset),
120
+ };
121
+ }
122
+
123
+ function mapSessionRowToSummary(row: SessionRepositoryRow): SessionSummary {
124
+ return {
125
+ id: row.session_id,
126
+ summary: row.custom_name || '',
127
+ messageCount: 0,
128
+ lastActivity: row.updated_at ?? row.created_at ?? new Date().toISOString(),
129
+ };
130
+ }
131
+
132
+ function bucketSessionRowsByProvider(rows: SessionRepositoryRow[]): SessionsByProvider {
133
+ const byProvider: SessionsByProvider = {
134
+ claude: [],
135
+ cursor: [],
136
+ codex: [],
137
+ gemini: [],
138
+ };
139
+
140
+ for (const row of rows) {
141
+ const provider = row.provider as keyof SessionsByProvider;
142
+ const bucket = byProvider[provider];
143
+ if (!bucket) {
144
+ continue;
145
+ }
146
+
147
+ bucket.push(mapSessionRowToSummary(row));
148
+ }
149
+
150
+ return byProvider;
151
+ }
152
+
153
+ /**
154
+ * Reads one paginated project session slice from the DB and groups rows by provider.
155
+ */
156
+ function readProjectSessionsPageByPath(
157
+ projectPath: string,
158
+ options: SessionPaginationOptions = {},
159
+ ): ProjectSessionsPageResult {
160
+ const pagination = normalizeSessionPagination(options);
161
+ const rows = sessionsDb.getSessionsByProjectPathPage(
162
+ projectPath,
163
+ pagination.limit,
164
+ pagination.offset,
165
+ ) as SessionRepositoryRow[];
166
+ const total = sessionsDb.countSessionsByProjectPath(projectPath);
167
+
168
+ return {
169
+ sessionsByProvider: bucketSessionRowsByProvider(rows),
170
+ total,
171
+ hasMore: pagination.offset + rows.length < total,
172
+ };
173
+ }
174
+
175
+ // Broadcast progress to all connected WebSocket clients
176
+ function broadcastProgress(progress: ProgressUpdate) {
177
+ const message = JSON.stringify({
178
+ type: 'loading_progress',
179
+ ...progress,
180
+ });
181
+
182
+ connectedClients.forEach((client: RealtimeClientConnection) => {
183
+ if (client.readyState === WS_OPEN_STATE) {
184
+ client.send(message);
185
+ }
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Reads all projects from DB and returns provider-bucketed session summaries.
191
+ */
192
+ export async function getProjectsWithSessions(
193
+ options: GetProjectsWithSessionsOptions = {}
194
+ ): Promise<ProjectListItem[]> {
195
+ if (!options.skipSynchronization) {
196
+ await sessionSynchronizerService.synchronizeSessions();
197
+ }
198
+
199
+ const projectRows = projectsDb.getProjectPaths() as Array<{
200
+ project_id: string;
201
+ project_path: string;
202
+ custom_project_name?: string | null;
203
+ isStarred?: number;
204
+ }>;
205
+ const totalProjects = projectRows.length;
206
+ const projects: ProjectListItem[] = [];
207
+ let processedProjects = 0;
208
+
209
+ for (const row of projectRows) {
210
+ processedProjects += 1;
211
+
212
+ const projectId = row.project_id;
213
+ const projectPath = row.project_path;
214
+
215
+ broadcastProgress({
216
+ phase: 'loading',
217
+ current: processedProjects,
218
+ total: totalProjects,
219
+ currentProject: projectPath,
220
+ });
221
+
222
+ const displayName =
223
+ row.custom_project_name && row.custom_project_name.trim().length > 0
224
+ ? row.custom_project_name
225
+ : await generateDisplayName(path.basename(projectPath) || projectPath, projectPath);
226
+
227
+ const sessionsPage = readProjectSessionsPageByPath(projectPath, {
228
+ limit: options.sessionsLimit,
229
+ offset: options.sessionsOffset,
230
+ });
231
+
232
+ projects.push({
233
+ projectId,
234
+ path: projectPath,
235
+ displayName,
236
+ fullPath: projectPath,
237
+ isStarred: Boolean(row.isStarred),
238
+ sessions: sessionsPage.sessionsByProvider.claude,
239
+ cursorSessions: sessionsPage.sessionsByProvider.cursor,
240
+ codexSessions: sessionsPage.sessionsByProvider.codex,
241
+ geminiSessions: sessionsPage.sessionsByProvider.gemini,
242
+ sessionMeta: {
243
+ hasMore: sessionsPage.hasMore,
244
+ total: sessionsPage.total,
245
+ },
246
+ });
247
+ }
248
+
249
+ broadcastProgress({
250
+ phase: 'complete',
251
+ current: totalProjects,
252
+ total: totalProjects,
253
+ });
254
+
255
+ return projects;
256
+ }
257
+
258
+ /**
259
+ * Loads one paginated session slice for a specific project id.
260
+ */
261
+ export async function getProjectSessionsPage(
262
+ projectId: string,
263
+ options: SessionPaginationOptions = {},
264
+ ): Promise<ProjectSessionsPageApiView> {
265
+ const projectRow = projectsDb.getProjectById(projectId);
266
+ if (!projectRow) {
267
+ throw new AppError(`Project "${projectId}" was not found.`, {
268
+ code: 'PROJECT_NOT_FOUND',
269
+ statusCode: 404,
270
+ });
271
+ }
272
+
273
+ const sessionsPage = readProjectSessionsPageByPath(projectRow.project_path, options);
274
+ return {
275
+ projectId: projectRow.project_id,
276
+ sessions: sessionsPage.sessionsByProvider.claude,
277
+ cursorSessions: sessionsPage.sessionsByProvider.cursor,
278
+ codexSessions: sessionsPage.sessionsByProvider.codex,
279
+ geminiSessions: sessionsPage.sessionsByProvider.gemini,
280
+ sessionMeta: {
281
+ hasMore: sessionsPage.hasMore,
282
+ total: sessionsPage.total,
283
+ },
284
+ };
285
+ }
@@ -0,0 +1,183 @@
1
+ import assert from 'node:assert/strict';
2
+ import { EventEmitter } from 'node:events';
3
+ import path from 'node:path';
4
+ import { PassThrough } from 'node:stream';
5
+ import test from 'node:test';
6
+
7
+ import { startCloneProject } from '@/modules/projects/services/project-clone.service.js';
8
+ import { AppError } from '@/shared/utils.js';
9
+
10
+ type TestDependencies = Parameters<typeof startCloneProject>[2];
11
+
12
+ function buildDependencies(overrides: Partial<NonNullable<TestDependencies>> = {}): NonNullable<TestDependencies> {
13
+ return {
14
+ validatePath: async () => ({ valid: true, resolvedPath: '/workspace/root' }),
15
+ ensureDirectory: async () => undefined,
16
+ pathExists: async () => false,
17
+ removePath: async () => undefined,
18
+ getGithubTokenById: async () => ({ github_token: 'token-value' }),
19
+ spawnGitClone: () => {
20
+ throw new Error('spawnGitClone should be overridden in this test');
21
+ },
22
+ registerProject: async () => ({ project: { projectId: 'project-1' } }),
23
+ logError: () => undefined,
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ function createMockGitProcess() {
29
+ const emitter = new EventEmitter() as EventEmitter & {
30
+ stdout: PassThrough;
31
+ stderr: PassThrough;
32
+ kill: () => void;
33
+ };
34
+
35
+ emitter.stdout = new PassThrough();
36
+ emitter.stderr = new PassThrough();
37
+ emitter.kill = () => {
38
+ emitter.emit('close', null);
39
+ };
40
+
41
+ return emitter;
42
+ }
43
+
44
+ test('startCloneProject rejects when workspace path is missing', async () => {
45
+ await assert.rejects(
46
+ async () =>
47
+ startCloneProject(
48
+ {
49
+ workspacePath: '',
50
+ githubUrl: 'https://github.com/example/repo',
51
+ userId: 1,
52
+ },
53
+ {
54
+ onProgress: () => undefined,
55
+ onComplete: () => undefined,
56
+ },
57
+ buildDependencies(),
58
+ ),
59
+ (error: unknown) => {
60
+ assert.ok(error instanceof AppError);
61
+ assert.equal(error.code, 'WORKSPACE_PATH_REQUIRED');
62
+ return true;
63
+ },
64
+ );
65
+ });
66
+
67
+ test('startCloneProject rejects when github URL is missing', async () => {
68
+ await assert.rejects(
69
+ async () =>
70
+ startCloneProject(
71
+ {
72
+ workspacePath: '/workspace/root',
73
+ githubUrl: '',
74
+ userId: 1,
75
+ },
76
+ {
77
+ onProgress: () => undefined,
78
+ onComplete: () => undefined,
79
+ },
80
+ buildDependencies(),
81
+ ),
82
+ (error: unknown) => {
83
+ assert.ok(error instanceof AppError);
84
+ assert.equal(error.code, 'GITHUB_URL_REQUIRED');
85
+ return true;
86
+ },
87
+ );
88
+ });
89
+
90
+ test('startCloneProject rejects github URL values that begin with option prefixes', async () => {
91
+ await assert.rejects(
92
+ async () =>
93
+ startCloneProject(
94
+ {
95
+ workspacePath: '/workspace/root',
96
+ githubUrl: '--upload-pack=malicious',
97
+ userId: 1,
98
+ },
99
+ {
100
+ onProgress: () => undefined,
101
+ onComplete: () => undefined,
102
+ },
103
+ buildDependencies(),
104
+ ),
105
+ (error: unknown) => {
106
+ assert.ok(error instanceof AppError);
107
+ assert.equal(error.code, 'INVALID_GITHUB_URL');
108
+ return true;
109
+ },
110
+ );
111
+ });
112
+
113
+ test('startCloneProject rejects when selected github token does not exist', async () => {
114
+ await assert.rejects(
115
+ async () =>
116
+ startCloneProject(
117
+ {
118
+ workspacePath: '/workspace/root',
119
+ githubUrl: 'https://github.com/example/repo',
120
+ githubTokenId: 12,
121
+ userId: 1,
122
+ },
123
+ {
124
+ onProgress: () => undefined,
125
+ onComplete: () => undefined,
126
+ },
127
+ buildDependencies({
128
+ getGithubTokenById: async () => null,
129
+ }),
130
+ ),
131
+ (error: unknown) => {
132
+ assert.ok(error instanceof AppError);
133
+ assert.equal(error.code, 'GITHUB_TOKEN_NOT_FOUND');
134
+ return true;
135
+ },
136
+ );
137
+ });
138
+
139
+ test('startCloneProject completes and emits complete payload when git exits successfully', async () => {
140
+ const gitProcess = createMockGitProcess();
141
+ const progressMessages: string[] = [];
142
+ let completePayload: { project: Record<string, unknown>; message: string } | null = null;
143
+ let capturedProjectPath = '';
144
+ let capturedCustomName = '';
145
+
146
+ const operation = await startCloneProject(
147
+ {
148
+ workspacePath: '/workspace/root',
149
+ githubUrl: 'https://github.com/example/repo.git',
150
+ userId: 1,
151
+ },
152
+ {
153
+ onProgress: (message) => {
154
+ progressMessages.push(message);
155
+ },
156
+ onComplete: (payload: { project: Record<string, unknown>; message: string }) => {
157
+ completePayload = payload;
158
+ },
159
+ },
160
+ buildDependencies({
161
+ spawnGitClone: () => gitProcess as any,
162
+ registerProject: async (projectPath, customName) => {
163
+ capturedProjectPath = projectPath;
164
+ capturedCustomName = customName;
165
+ return { project: { projectId: 'project-1', path: projectPath } };
166
+ },
167
+ }),
168
+ );
169
+
170
+ gitProcess.emit('close', 0);
171
+ await operation.waitForCompletion;
172
+
173
+ assert.ok(progressMessages.some((message) => message.includes("Cloning into 'repo'")));
174
+ assert.equal(capturedCustomName, 'repo');
175
+ assert.equal(path.basename(capturedProjectPath), 'repo');
176
+ assert.notEqual(completePayload, null);
177
+ const resolvedCompletePayload = completePayload as unknown as {
178
+ project: Record<string, unknown>;
179
+ message: string;
180
+ };
181
+ assert.equal(resolvedCompletePayload.message, 'Repository cloned successfully');
182
+ assert.equal((resolvedCompletePayload.project.projectId as string) || '', 'project-1');
183
+ });