@assistkick/create 1.9.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/src/scaffolder.d.ts +12 -1
  2. package/dist/src/scaffolder.js +40 -3
  3. package/dist/src/scaffolder.js.map +1 -1
  4. package/package.json +1 -1
  5. package/templates/assistkick-product-system/package.json +1 -1
  6. package/templates/assistkick-product-system/packages/backend/package.json +1 -0
  7. package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
  8. package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
  9. package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
  10. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
  11. package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
  12. package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
  13. package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
  14. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
  15. package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
  16. package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
  17. package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
  18. package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +391 -23
  19. package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
  20. package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
  21. package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
  22. package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
  23. package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
  24. package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
  25. package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
  26. package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
  27. package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
  28. package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
  29. package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
  30. package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
  31. package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
  32. package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
  33. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
  34. package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
  35. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
  36. package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
  37. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
  38. package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
  39. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
  40. package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
  41. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
  42. package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
  43. package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
  44. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
  45. package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
  46. package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
  47. package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
  48. package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
  49. package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
  50. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
  51. package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
  52. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
  53. package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
  54. package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
  55. package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
  56. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
  57. package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
  58. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
  59. package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
  60. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
  61. package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +149 -14
  62. package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
  63. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
  64. package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
  65. package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
  66. package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
  67. package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
  68. package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
  69. package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
  70. package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
  71. package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
  72. package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
  73. package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
  74. package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
  75. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
  76. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
  77. package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
  78. package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
  79. package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
  80. package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
  81. package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
  82. package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
  83. package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
  84. package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
  85. package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
  86. package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
  87. package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
  88. package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
  89. package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
  90. package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
  91. package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
  92. package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
  93. package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
  94. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
  95. package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
  96. package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
  97. package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
  98. package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
  99. package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
  100. package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
  101. package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
  102. package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
  103. package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
  104. package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
  105. package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
  106. package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
  107. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
  108. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
  109. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
  110. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
  111. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
  112. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
  113. package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
  114. package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
  115. package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
  116. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
  117. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
  118. package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
  119. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
  120. package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
  121. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
  122. package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
  123. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
  124. package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
  125. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
  126. package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
  127. package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
  128. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
  129. package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
  130. package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
  131. package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
  132. package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
  133. package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
  134. package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
  135. package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
  136. package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
  137. package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
  138. package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
  139. package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
  140. package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
  141. package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
  142. package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
  143. package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
  144. package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
  145. package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
  146. package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
  147. package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
  148. package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
  149. package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
  150. package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
  151. package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
  152. package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
  153. package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
  154. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
  155. package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
  156. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
  157. package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
  158. package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
  159. package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
  160. package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
  161. package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
  162. package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
  163. package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
  164. package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
  165. package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
  166. package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
  167. package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
  168. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
  169. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
  170. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
  171. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
  172. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
  173. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
  174. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
  175. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
  176. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
  177. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
  178. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
  179. package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
  180. package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
  181. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
  182. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
  183. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
  184. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
  185. package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
  186. package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
  187. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
  188. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
  189. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
  190. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  191. package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
  192. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
  193. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
  194. package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
  195. package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
  196. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
  197. package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
  198. package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
  199. package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
  200. package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
  201. package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
  202. package/templates/skills/assistkick-debugger/SKILL.md +23 -23
  203. package/templates/skills/assistkick-developer/SKILL.md +59 -63
  204. package/templates/skills/assistkick-interview/SKILL.md +26 -26
  205. package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
  206. package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
@@ -232,7 +232,7 @@ export const runCoherenceReview = async (projectId?: string) => {
232
232
  await writeCoherenceData(coherenceData, projectId);
233
233
  log('COHERENCE', `=== Coherence review completed — ${sorted.length} proposals ===`);
234
234
  } catch (err: any) {
235
- log('COHERENCE', `Review FAILED: ${err.message}`);
235
+ log('COHERENCE', `Review FAILED:`, err.stack || err.message);
236
236
  if (collectedProposals.length > 0) {
237
237
  const sorted = sortProposals(collectedProposals);
238
238
  for (const p of sorted) {
@@ -287,7 +287,7 @@ export const executeProposalAction = async (proposal: any, coherenceData: any, p
287
287
  ], dataDir);
288
288
  log('COHERENCE', `Applied suggested edge: ${fromId} --${edge.relation}--> ${toId}`);
289
289
  } catch (edgeErr: any) {
290
- log('COHERENCE', `Failed to apply suggested edge: ${edgeErr.message}`);
290
+ log('COHERENCE', `Failed to apply suggested edge:`, edgeErr.stack || edgeErr.message);
291
291
  }
292
292
  }
293
293
  proposal.created_node_id = newNodeId;
@@ -309,7 +309,7 @@ export const executeProposalAction = async (proposal: any, coherenceData: any, p
309
309
  await removeEdgeDb(edge.from, edge.relation, edge.to);
310
310
  log('COHERENCE', `Removed edge for deprecated node: ${edge.from} --${edge.relation}--> ${edge.to}`);
311
311
  } catch (edgeErr: any) {
312
- log('COHERENCE', `Failed to remove edge: ${edgeErr.message}`);
312
+ log('COHERENCE', `Failed to remove edge:`, edgeErr.stack || edgeErr.message);
313
313
  }
314
314
  }
315
315
  const db = getDb();
@@ -0,0 +1,85 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { DevCommandDetector } from './dev_command_detector.ts';
4
+ import { writeFile, mkdir, rm } from 'node:fs/promises';
5
+ import { join } from 'node:path';
6
+ import { tmpdir } from 'node:os';
7
+ import { randomBytes } from 'node:crypto';
8
+
9
+ const createTmpDir = async (): Promise<string> => {
10
+ const dir = join(tmpdir(), `dev-detect-test-${randomBytes(4).toString('hex')}`);
11
+ await mkdir(dir, { recursive: true });
12
+ return dir;
13
+ };
14
+
15
+ describe('DevCommandDetector', () => {
16
+ let detector: DevCommandDetector;
17
+ let tmpDir: string;
18
+
19
+ beforeEach(async () => {
20
+ detector = new DevCommandDetector({ log: () => {} });
21
+ tmpDir = await createTmpDir();
22
+ });
23
+
24
+ it('detects "dev" script as highest priority', async () => {
25
+ await writeFile(join(tmpDir, 'package.json'), JSON.stringify({
26
+ scripts: { start: 'node server.js', dev: 'vite', serve: 'serve dist' },
27
+ }));
28
+
29
+ const result = await detector.detect(tmpDir);
30
+ assert.ok(result);
31
+ assert.equal(result.scriptName, 'dev');
32
+ assert.equal(result.command, 'vite');
33
+ await rm(tmpDir, { recursive: true });
34
+ });
35
+
36
+ it('falls back to "start" when "dev" is absent', async () => {
37
+ await writeFile(join(tmpDir, 'package.json'), JSON.stringify({
38
+ scripts: { start: 'node server.js', serve: 'serve dist' },
39
+ }));
40
+
41
+ const result = await detector.detect(tmpDir);
42
+ assert.ok(result);
43
+ assert.equal(result.scriptName, 'start');
44
+ assert.equal(result.command, 'node server.js');
45
+ await rm(tmpDir, { recursive: true });
46
+ });
47
+
48
+ it('returns null when no matching scripts exist', async () => {
49
+ await writeFile(join(tmpDir, 'package.json'), JSON.stringify({
50
+ scripts: { build: 'tsc', lint: 'eslint .' },
51
+ }));
52
+
53
+ const result = await detector.detect(tmpDir);
54
+ assert.equal(result, null);
55
+ await rm(tmpDir, { recursive: true });
56
+ });
57
+
58
+ it('returns null when no scripts field exists', async () => {
59
+ await writeFile(join(tmpDir, 'package.json'), JSON.stringify({
60
+ name: 'no-scripts',
61
+ }));
62
+
63
+ const result = await detector.detect(tmpDir);
64
+ assert.equal(result, null);
65
+ await rm(tmpDir, { recursive: true });
66
+ });
67
+
68
+ it('returns null when package.json does not exist', async () => {
69
+ const result = await detector.detect(join(tmpDir, 'nonexistent'));
70
+ assert.equal(result, null);
71
+ await rm(tmpDir, { recursive: true });
72
+ });
73
+
74
+ it('detects "serve" script', async () => {
75
+ await writeFile(join(tmpDir, 'package.json'), JSON.stringify({
76
+ scripts: { serve: 'vite preview' },
77
+ }));
78
+
79
+ const result = await detector.detect(tmpDir);
80
+ assert.ok(result);
81
+ assert.equal(result.scriptName, 'serve');
82
+ assert.equal(result.command, 'vite preview');
83
+ await rm(tmpDir, { recursive: true });
84
+ });
85
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * DevCommandDetector — auto-detects the dev server command from a project's package.json.
3
+ * Scans the "scripts" field for common dev server script names and returns the command.
4
+ */
5
+
6
+ import { readFile } from 'node:fs/promises';
7
+ import { join } from 'node:path';
8
+
9
+ /** Priority-ordered list of script names that typically start a dev server. */
10
+ const DEV_SCRIPT_NAMES = ['dev', 'start', 'serve', 'develop', 'preview'];
11
+
12
+ interface DevCommandDetectorDeps {
13
+ log: (tag: string, ...args: unknown[]) => void;
14
+ }
15
+
16
+ export interface DetectedCommand {
17
+ scriptName: string;
18
+ command: string;
19
+ }
20
+
21
+ export class DevCommandDetector {
22
+ private readonly log: DevCommandDetectorDeps['log'];
23
+
24
+ constructor({ log }: DevCommandDetectorDeps) {
25
+ this.log = log;
26
+ }
27
+
28
+ /**
29
+ * Detect the dev server command from a project directory's package.json.
30
+ * Returns the first matching script from the priority list, or null if none found.
31
+ */
32
+ detect = async (projectDir: string): Promise<DetectedCommand | null> => {
33
+ const packageJsonPath = join(projectDir, 'package.json');
34
+
35
+ try {
36
+ const raw = await readFile(packageJsonPath, 'utf-8');
37
+ const pkg = JSON.parse(raw);
38
+ const scripts: Record<string, string> = pkg.scripts || {};
39
+
40
+ for (const name of DEV_SCRIPT_NAMES) {
41
+ if (scripts[name]) {
42
+ this.log('DEV_DETECT', `Found script "${name}": ${scripts[name]} in ${projectDir}`);
43
+ return { scriptName: name, command: scripts[name] };
44
+ }
45
+ }
46
+
47
+ this.log('DEV_DETECT', `No dev script found in ${packageJsonPath}`);
48
+ return null;
49
+ } catch (err: any) {
50
+ this.log('DEV_DETECT', `Failed to read ${packageJsonPath}: ${err.message}`);
51
+ return null;
52
+ }
53
+ };
54
+ }
@@ -9,27 +9,30 @@ import { Resend } from 'resend';
9
9
  interface EmailServiceDeps {
10
10
  apiKey: string;
11
11
  fromAddress: string;
12
+ log: (tag: string, ...args: any[]) => void;
12
13
  }
13
14
 
14
15
  export class EmailService {
15
16
  private readonly client: Resend | null;
16
17
  private readonly fromAddress: string;
18
+ private readonly log: (tag: string, ...args: any[]) => void;
17
19
 
18
- constructor({ apiKey, fromAddress }: EmailServiceDeps) {
20
+ constructor({ apiKey, fromAddress, log }: EmailServiceDeps) {
19
21
  this.client = apiKey ? new Resend(apiKey) : null;
20
22
  this.fromAddress = fromAddress;
23
+ this.log = log;
21
24
  if (!apiKey) {
22
- console.warn('[EMAIL] RESEND_API_KEY not set — email sending disabled');
25
+ log('EMAIL', 'RESEND_API_KEY not set — email sending disabled');
23
26
  }
24
27
  }
25
28
 
26
29
  sendPasswordResetEmail = async (to: string, resetUrl: string): Promise<void> => {
27
30
  if (!this.client) {
28
- console.warn(`[EMAIL] Would send password reset to ${to}: ${resetUrl}`);
31
+ this.log('EMAIL', `Would send password reset to ${to}: ${resetUrl}`);
29
32
  return;
30
33
  }
31
34
 
32
- console.log(`[EMAIL] Sending password reset to ${to} from ${this.fromAddress}`);
35
+ this.log('EMAIL', `Sending password reset to ${to} from ${this.fromAddress}`);
33
36
  const { data, error } = await this.client.emails.send({
34
37
  from: this.fromAddress,
35
38
  to,
@@ -42,19 +45,19 @@ export class EmailService {
42
45
  });
43
46
 
44
47
  if (error) {
45
- console.error(`[EMAIL] Resend API error:`, error);
48
+ this.log('EMAIL', `Resend API error: ${error.message}`, error);
46
49
  throw new Error(`Resend API error: ${error.message}`);
47
50
  }
48
- console.log(`[EMAIL] Password reset sent successfully, id=${data?.id}`);
51
+ this.log('EMAIL', `Password reset sent successfully, id=${data?.id}`);
49
52
  };
50
53
 
51
54
  sendInvitationEmail = async (to: string, invitationUrl: string): Promise<void> => {
52
55
  if (!this.client) {
53
- console.warn(`[EMAIL] Would send invitation to ${to}: ${invitationUrl}`);
56
+ this.log('EMAIL', `Would send invitation to ${to}: ${invitationUrl}`);
54
57
  return;
55
58
  }
56
59
 
57
- console.log(`[EMAIL] Sending invitation to ${to} from ${this.fromAddress}`);
60
+ this.log('EMAIL', `Sending invitation to ${to} from ${this.fromAddress}`);
58
61
  const { data, error } = await this.client.emails.send({
59
62
  from: this.fromAddress,
60
63
  to,
@@ -67,9 +70,9 @@ export class EmailService {
67
70
  });
68
71
 
69
72
  if (error) {
70
- console.error(`[EMAIL] Resend API error:`, error);
73
+ this.log('EMAIL', `Resend API error: ${error.message}`, error);
71
74
  throw new Error(`Resend API error: ${error.message}`);
72
75
  }
73
- console.log(`[EMAIL] Invitation sent successfully, id=${data?.id}`);
76
+ this.log('EMAIL', `Invitation sent successfully, id=${data?.id}`);
74
77
  };
75
78
  }
@@ -34,7 +34,7 @@ const DATA_DIR = SHARED_DIR;
34
34
  const PROJECT_ROOT = join(SKILL_ROOT, '..');
35
35
  const SKILLS_DIR = join(PROJECT_ROOT, '.claude', 'skills');
36
36
  const WORKSPACES_DIR = process.env.WORKSPACES_DIR || join(PROJECT_ROOT, 'workspaces');
37
- const WORKTREES_DIR = join(PROJECT_ROOT, '.worktrees');
37
+ // WORKTREES_DIR is no longer used — worktrees are per-project inside workspacesDir
38
38
  const DEVELOPER_SKILL_PATH = join(SKILLS_DIR, 'product-developer', 'SKILL.md');
39
39
  const REVIEWER_SKILL_PATH = join(SKILLS_DIR, 'product-code-reviewer', 'SKILL.md');
40
40
  const DEBUGGER_SKILL_PATH = join(SKILLS_DIR, 'product-debugger', 'SKILL.md');
@@ -46,6 +46,13 @@ export const log = (tag: string, ...args: any[]) => {
46
46
  console.log(`[${ts}] [${tag}]`, ...args);
47
47
  };
48
48
 
49
+ /** Log an error with its stack trace for debugging. */
50
+ export const logError = (tag: string, msg: string, err: unknown) => {
51
+ const ts = new Date().toISOString().slice(11, 23);
52
+ const stack = err instanceof Error ? err.stack : String(err);
53
+ console.error(`[${ts}] [${tag}] ERROR ${msg}`, stack);
54
+ };
55
+
49
56
  export let claudeService: any;
50
57
  export let workflowEngine: WorkflowEngine;
51
58
  export let orchestrator: WorkflowOrchestrator;
@@ -61,7 +68,7 @@ export let videoRenderService: VideoRenderService;
61
68
  export const paths = {
62
69
  projectRoot: PROJECT_ROOT,
63
70
  workspacesDir: WORKSPACES_DIR,
64
- worktreesDir: WORKTREES_DIR,
71
+ worktreesDir: '', // deprecated — worktrees are per-project inside workspacesDir
65
72
  skillsDir: SKILLS_DIR,
66
73
  toolsDir: TOOLS_DIR,
67
74
  dataDir: DATA_DIR,
@@ -74,7 +81,7 @@ export const paths = {
74
81
 
75
82
  export const initServices = (verbose: boolean) => {
76
83
  claudeService = createClaudeService({ verbose, log });
77
- const gitWorkflow = new GitWorkflow({ claudeService, projectRoot: PROJECT_ROOT, worktreesDir: WORKTREES_DIR, log });
84
+ const gitWorkflow = new GitWorkflow({ claudeService, workspacesDir: WORKSPACES_DIR, log });
78
85
 
79
86
  // New services for project git repo integration
80
87
  githubAppService = new GitHubAppService({ log });
@@ -96,6 +103,7 @@ export const initServices = (verbose: boolean) => {
96
103
  kanban: { getKanbanEntry, saveKanbanEntry },
97
104
  claudeService,
98
105
  gitWorkflow,
106
+ skillsDir: SKILLS_DIR,
99
107
  bundleService,
100
108
  ttsService,
101
109
  videoRenderService,
@@ -89,7 +89,7 @@ export class InvitationService {
89
89
  await this.emailService.sendInvitationEmail(normalizedEmail, invitationUrl);
90
90
  this.log('INVITE', `Invitation sent to ${normalizedEmail}`);
91
91
  } catch (err: any) {
92
- this.log('INVITE', `Failed to send invitation email: ${err.message}`);
92
+ this.log('INVITE', `Failed to send invitation email:`, err.stack || err.message);
93
93
  throw new Error('Failed to send invitation email');
94
94
  }
95
95
  };
@@ -73,7 +73,7 @@ export class PasswordResetService {
73
73
  await this.emailService.sendPasswordResetEmail(user.email, resetUrl);
74
74
  this.log('RESET', `Reset email sent to ${user.email}`);
75
75
  } catch (err: any) {
76
- this.log('RESET', `Failed to send reset email: ${err.message}`);
76
+ this.log('RESET', `Failed to send reset email:`, err.stack || err.message);
77
77
  throw new Error('Failed to send reset email');
78
78
  }
79
79
  };
@@ -0,0 +1,243 @@
1
+ import { describe, it, mock, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { PermissionService, type PermissionRequest } from './permission_service.ts';
4
+
5
+ /** Wait for microtasks (async operations) to complete */
6
+ const tick = () => new Promise(resolve => setTimeout(resolve, 5));
7
+
8
+ /**
9
+ * Create a mock database that simulates Drizzle ORM's chainable API.
10
+ */
11
+ const createMockDb = (alwaysRules: Array<{ toolName: string }> = []) => {
12
+ const insertedRows: unknown[] = [];
13
+
14
+ return {
15
+ select: mock.fn((..._args: unknown[]) => ({
16
+ from: mock.fn(() => ({
17
+ where: mock.fn(() => ({
18
+ limit: mock.fn(() => alwaysRules),
19
+ })),
20
+ })),
21
+ })),
22
+ insert: mock.fn(() => ({
23
+ values: mock.fn(async (val: unknown) => {
24
+ insertedRows.push(val);
25
+ }),
26
+ })),
27
+ delete: mock.fn(() => ({
28
+ where: mock.fn(async () => {}),
29
+ })),
30
+ _insertedRows: insertedRows,
31
+ };
32
+ };
33
+
34
+ describe('PermissionService', () => {
35
+ let service: PermissionService;
36
+ let mockDb: ReturnType<typeof createMockDb>;
37
+ let logMock: ReturnType<typeof mock.fn>;
38
+
39
+ beforeEach(() => {
40
+ logMock = mock.fn();
41
+ mockDb = createMockDb();
42
+
43
+ service = new PermissionService({
44
+ getDb: () => mockDb,
45
+ log: logMock,
46
+ });
47
+ });
48
+
49
+ describe('isToolAllowed', () => {
50
+ it('returns false for unknown tool with empty DB', async () => {
51
+ const result = await service.isToolAllowed('proj_1', 'session_1', 'Read');
52
+ assert.equal(result, false);
53
+ });
54
+
55
+ it('returns true when tool has an "always" rule in DB', async () => {
56
+ const dbWithRules = createMockDb([{ toolName: 'Read' }]);
57
+ const svc = new PermissionService({ getDb: () => dbWithRules, log: logMock });
58
+ const result = await svc.isToolAllowed('proj_1', 'session_1', 'Read');
59
+ assert.equal(result, true);
60
+ });
61
+ });
62
+
63
+ describe('requestPermission', () => {
64
+ it('creates a pending request and notifies handler', async () => {
65
+ let receivedRequest: PermissionRequest | null = null;
66
+ service.setPermissionRequestHandler((req) => {
67
+ receivedRequest = req;
68
+ });
69
+
70
+ // Start the request — don't await (it blocks until resolved)
71
+ const promise = service.requestPermission('proj_1', 'session_1', 'Read', { file_path: '/test.ts' });
72
+
73
+ // Wait for isToolAllowed async check to complete
74
+ await tick();
75
+
76
+ // Handler should have been notified
77
+ assert.ok(receivedRequest !== null);
78
+ assert.equal(receivedRequest!.toolName, 'Read');
79
+ assert.equal(receivedRequest!.claudeSessionId, 'session_1');
80
+ assert.ok(receivedRequest!.requestId.startsWith('perm_'));
81
+
82
+ // Resolve the permission
83
+ const resolved = await service.resolvePermission(receivedRequest!.requestId, 'proj_1', 'allow_once');
84
+ assert.equal(resolved, true);
85
+
86
+ const response = await promise;
87
+ assert.equal(response.decision, 'allow_once');
88
+ });
89
+
90
+ it('rejects when no handler is registered', async () => {
91
+ await assert.rejects(
92
+ () => service.requestPermission('proj_1', 'session_1', 'Read', {}),
93
+ { message: 'No permission request handler registered' },
94
+ );
95
+ });
96
+
97
+ it('resolves immediately when tool is already allowed in DB', async () => {
98
+ const dbWithRules = createMockDb([{ toolName: 'Read' }]);
99
+ const svc = new PermissionService({ getDb: () => dbWithRules, log: logMock });
100
+
101
+ const response = await svc.requestPermission('proj_1', 'session_1', 'Read', {});
102
+ assert.equal(response.decision, 'allow_always');
103
+ });
104
+ });
105
+
106
+ describe('resolvePermission', () => {
107
+ it('returns false for unknown requestId', async () => {
108
+ const result = await service.resolvePermission('unknown', 'proj_1', 'allow_once');
109
+ assert.equal(result, false);
110
+ });
111
+
112
+ it('persists "allow_always" to database', async () => {
113
+ let receivedRequest: PermissionRequest | null = null;
114
+ service.setPermissionRequestHandler((req) => {
115
+ receivedRequest = req;
116
+ });
117
+
118
+ const promise = service.requestPermission('proj_1', 'session_1', 'Read', {});
119
+ await tick();
120
+
121
+ // Resolve with "allow_always"
122
+ await service.resolvePermission(receivedRequest!.requestId, 'proj_1', 'allow_always');
123
+ await promise;
124
+
125
+ // Should have called insert for the "always" rule
126
+ assert.ok(mockDb.insert.mock.calls.length > 0);
127
+ });
128
+
129
+ it('stores session rule in memory for "allow_session"', async () => {
130
+ let receivedRequest: PermissionRequest | null = null;
131
+ service.setPermissionRequestHandler((req) => {
132
+ receivedRequest = req;
133
+ });
134
+
135
+ const promise = service.requestPermission('proj_1', 'session_1', 'Read', {});
136
+ await tick();
137
+
138
+ await service.resolvePermission(receivedRequest!.requestId, 'proj_1', 'allow_session');
139
+ await promise;
140
+
141
+ // Now the same tool should be allowed for this session (in-memory)
142
+ // isToolAllowed checks session permissions first
143
+ const allowed = await service.isToolAllowed('proj_1', 'session_1', 'Read');
144
+ assert.equal(allowed, true);
145
+ });
146
+ });
147
+
148
+ describe('cancelPendingRequests', () => {
149
+ it('cancels all pending requests for a session', async () => {
150
+ service.setPermissionRequestHandler(() => {});
151
+
152
+ const promise = service.requestPermission('proj_1', 'session_1', 'Read', {});
153
+ await tick();
154
+
155
+ // Cancel the request
156
+ service.cancelPendingRequests('session_1');
157
+
158
+ // The promise should reject
159
+ await assert.rejects(() => promise, { message: 'Permission request cancelled — session ended' });
160
+ });
161
+
162
+ it('does not cancel requests for other sessions', async () => {
163
+ service.setPermissionRequestHandler(() => {});
164
+
165
+ const promise = service.requestPermission('proj_1', 'session_1', 'Read', {});
166
+ await tick();
167
+
168
+ // Cancel requests for session_2 — should not affect session_1
169
+ service.cancelPendingRequests('session_2');
170
+
171
+ assert.equal(service.getPendingCount(), 1);
172
+
173
+ // Clean up — cancel session_1 too
174
+ service.cancelPendingRequests('session_1');
175
+ await promise.catch(() => {}); // Ignore rejection
176
+ });
177
+ });
178
+
179
+ describe('clearSessionPermissions', () => {
180
+ it('clears session-scoped permissions', async () => {
181
+ // Add a session rule first
182
+ let receivedRequest: PermissionRequest | null = null;
183
+ service.setPermissionRequestHandler((req) => {
184
+ receivedRequest = req;
185
+ });
186
+
187
+ const promise = service.requestPermission('proj_1', 'session_1', 'Read', {});
188
+ await tick();
189
+
190
+ await service.resolvePermission(receivedRequest!.requestId, 'proj_1', 'allow_session');
191
+ await promise;
192
+
193
+ // Tool should be allowed
194
+ let allowed = await service.isToolAllowed('proj_1', 'session_1', 'Read');
195
+ assert.equal(allowed, true);
196
+
197
+ // Clear session permissions
198
+ service.clearSessionPermissions('session_1');
199
+
200
+ // Tool should no longer be allowed (DB has no rules)
201
+ allowed = await service.isToolAllowed('proj_1', 'session_1', 'Read');
202
+ assert.equal(allowed, false);
203
+ });
204
+ });
205
+
206
+ describe('getAlwaysRules', () => {
207
+ it('returns tool names from the database', async () => {
208
+ const dbWithRules = createMockDb();
209
+ // Override select to return rules without the limit chain
210
+ dbWithRules.select = mock.fn(() => ({
211
+ from: mock.fn(() => ({
212
+ where: mock.fn(() => [
213
+ { toolName: 'Read' },
214
+ { toolName: 'Glob' },
215
+ ]),
216
+ })),
217
+ }));
218
+
219
+ const svc = new PermissionService({ getDb: () => dbWithRules, log: logMock });
220
+ const rules = await svc.getAlwaysRules('proj_1');
221
+ assert.deepEqual(rules, ['Read', 'Glob']);
222
+ });
223
+ });
224
+
225
+ describe('getPendingCount', () => {
226
+ it('returns 0 when no pending requests', () => {
227
+ assert.equal(service.getPendingCount(), 0);
228
+ });
229
+
230
+ it('returns correct count with pending requests', async () => {
231
+ service.setPermissionRequestHandler(() => {});
232
+
233
+ const promise = service.requestPermission('proj_1', 'session_1', 'Read', {});
234
+ await tick();
235
+
236
+ assert.equal(service.getPendingCount(), 1);
237
+
238
+ // Clean up — cancel and await the rejection
239
+ service.cancelPendingRequests('session_1');
240
+ await promise.catch(() => {});
241
+ });
242
+ });
243
+ });