@assistkick/create 1.10.0 → 1.12.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 (209) 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 +390 -22
  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 +129 -8
  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/migrate.ts +82 -0
  159. package/templates/assistkick-product-system/packages/shared/db/migrations/0000_outgoing_ultron.sql +277 -0
  160. package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
  161. package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
  162. package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
  163. package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
  164. package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
  165. package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
  166. package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
  167. package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
  168. package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
  169. package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
  170. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0000_snapshot.json +972 -22
  171. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
  172. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
  173. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
  174. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
  175. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
  176. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
  177. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
  178. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
  179. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
  180. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
  181. package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +2 -100
  182. package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
  183. package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
  184. package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
  185. package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
  186. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
  187. package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
  188. package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
  189. package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
  190. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
  191. package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
  192. package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
  193. package/templates/assistkick-product-system/packages/shared/package.json +2 -1
  194. package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
  195. package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
  196. package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
  197. package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
  198. package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
  199. package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
  200. package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
  201. package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
  202. package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
  203. package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
  204. package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
  205. package/templates/skills/assistkick-debugger/SKILL.md +23 -23
  206. package/templates/skills/assistkick-developer/SKILL.md +59 -63
  207. package/templates/skills/assistkick-interview/SKILL.md +26 -26
  208. package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
  209. package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Chat file serve route — serves uploaded chat attachment files with caching.
3
+ *
4
+ * GET /api/chat-files/:projectId/*
5
+ * Serves the file at .chat-uploads/<path> within the project workspace.
6
+ * Returns appropriate content-type and caching headers.
7
+ */
8
+
9
+ import { Router } from 'express';
10
+ import { join, resolve, extname } from 'node:path';
11
+ import { existsSync, createReadStream } from 'node:fs';
12
+ import { stat } from 'node:fs/promises';
13
+ import type { ProjectWorkspaceService } from '../services/project_workspace_service.js';
14
+
15
+ interface ChatFilesRoutesDeps {
16
+ workspaceService: ProjectWorkspaceService;
17
+ log: (tag: string, ...args: unknown[]) => void;
18
+ }
19
+
20
+ /** Map file extensions to MIME types for common image/file formats. */
21
+ const MIME_TYPES: Record<string, string> = {
22
+ '.png': 'image/png',
23
+ '.jpg': 'image/jpeg',
24
+ '.jpeg': 'image/jpeg',
25
+ '.gif': 'image/gif',
26
+ '.webp': 'image/webp',
27
+ '.svg': 'image/svg+xml',
28
+ '.bmp': 'image/bmp',
29
+ '.pdf': 'application/pdf',
30
+ '.txt': 'text/plain',
31
+ '.json': 'application/json',
32
+ '.csv': 'text/csv',
33
+ '.md': 'text/markdown',
34
+ };
35
+
36
+ export const createChatFilesRoutes = ({ workspaceService, log }: ChatFilesRoutesDeps): Router => {
37
+ const router: Router = Router();
38
+
39
+ // GET /api/chat-files/:projectId/:fileName
40
+ // Files are always in .chat-uploads/ — the fileName is the timestamped unique name
41
+ router.get('/:projectId/:fileName', async (req, res) => {
42
+ const { projectId, fileName } = req.params;
43
+ const filePath = `.chat-uploads/${fileName}`;
44
+
45
+ if (!projectId || !fileName) {
46
+ res.status(400).json({ error: 'projectId and fileName are required' });
47
+ return;
48
+ }
49
+
50
+ // Reject path traversal in the filename
51
+ if (fileName.includes('/') || fileName.includes('\\') || fileName === '..' || fileName === '.') {
52
+ res.status(400).json({ error: 'Invalid file name' });
53
+ return;
54
+ }
55
+
56
+ const wsPath = workspaceService.getWorkspacePath(projectId);
57
+ if (!existsSync(wsPath)) {
58
+ res.status(404).json({ error: 'Project workspace not found' });
59
+ return;
60
+ }
61
+
62
+ const absolutePath = join(wsPath, filePath);
63
+
64
+ // Validate no path traversal
65
+ const resolved = resolve(absolutePath);
66
+ if (!resolved.startsWith(resolve(wsPath))) {
67
+ res.status(400).json({ error: 'Invalid file path' });
68
+ return;
69
+ }
70
+
71
+ if (!existsSync(absolutePath)) {
72
+ res.status(404).json({ error: 'File not found' });
73
+ return;
74
+ }
75
+
76
+ try {
77
+ const fileStat = await stat(absolutePath);
78
+ const ext = extname(absolutePath).toLowerCase();
79
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
80
+
81
+ // Immutable cache: uploaded files have timestamped unique names so they never change
82
+ res.setHeader('Content-Type', contentType);
83
+ res.setHeader('Content-Length', fileStat.size);
84
+ res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
85
+ res.setHeader('ETag', `"${fileStat.mtimeMs.toString(36)}-${fileStat.size.toString(36)}"`);
86
+
87
+ const stream = createReadStream(resolved);
88
+ stream.pipe(res);
89
+ } catch (err: unknown) {
90
+ const msg = err instanceof Error ? err.message : 'Unknown error';
91
+ log('CHAT_FILES', `Failed to serve file: ${msg}`);
92
+ res.status(500).json({ error: `Failed to serve file: ${msg}` });
93
+ }
94
+ });
95
+
96
+ return router;
97
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Chat permission routes — HTTP endpoints used by the MCP permission server.
3
+ *
4
+ * POST /api/chat/permission/request — MCP server calls this when Claude CLI
5
+ * needs permission. The request is held open until the user responds via WebSocket.
6
+ *
7
+ * GET /api/chat/permission/rules?projectId=... — list "always allow" rules
8
+ * POST /api/chat/permission/rules — add an "always allow" rule
9
+ * DELETE /api/chat/permission/rules — remove an "always allow" rule
10
+ */
11
+
12
+ import { Router } from 'express';
13
+ import type { PermissionService } from '../services/permission_service.js';
14
+
15
+ export interface ChatPermissionRoutesDeps {
16
+ permissionService: PermissionService;
17
+ log: (tag: string, ...args: unknown[]) => void;
18
+ }
19
+
20
+ export const createChatPermissionRoutes = ({ permissionService, log }: ChatPermissionRoutesDeps): Router => {
21
+ const router = Router();
22
+
23
+ /**
24
+ * POST /request — called by the MCP permission server.
25
+ * Long-polls until the user responds or times out.
26
+ */
27
+ router.post('/request', async (req, res) => {
28
+ const { claudeSessionId, projectId, toolName, input } = req.body;
29
+
30
+ if (!claudeSessionId || !projectId || !toolName) {
31
+ res.status(400).json({ error: 'Missing required fields: claudeSessionId, projectId, toolName' });
32
+ return;
33
+ }
34
+
35
+ try {
36
+ const response = await permissionService.requestPermission(
37
+ projectId,
38
+ claudeSessionId,
39
+ toolName,
40
+ input || {},
41
+ );
42
+
43
+ res.json(response);
44
+ } catch (err: unknown) {
45
+ const message = err instanceof Error ? err.message : 'Unknown error';
46
+ log('PERM_ROUTE', `Permission request failed: ${message}`);
47
+ res.status(408).json({ error: message });
48
+ }
49
+ });
50
+
51
+ /**
52
+ * GET /rules?projectId=... — list "always allow" rules for a project.
53
+ */
54
+ router.get('/rules', async (req, res) => {
55
+ const projectId = req.query.projectId as string;
56
+ if (!projectId) {
57
+ res.status(400).json({ error: 'Missing projectId query parameter' });
58
+ return;
59
+ }
60
+
61
+ const rules = await permissionService.getAlwaysRules(projectId);
62
+ res.json({ rules });
63
+ });
64
+
65
+ /**
66
+ * POST /rules — add an "always allow" rule.
67
+ */
68
+ router.post('/rules', async (req, res) => {
69
+ const { projectId, toolName } = req.body;
70
+ if (!projectId || !toolName) {
71
+ res.status(400).json({ error: 'Missing required fields: projectId, toolName' });
72
+ return;
73
+ }
74
+
75
+ await permissionService.addAlwaysRule(projectId, toolName);
76
+ res.json({ ok: true });
77
+ });
78
+
79
+ /**
80
+ * DELETE /rules — remove an "always allow" rule.
81
+ */
82
+ router.delete('/rules', async (req, res) => {
83
+ const { projectId, toolName } = req.body;
84
+ if (!projectId || !toolName) {
85
+ res.status(400).json({ error: 'Missing required fields: projectId, toolName' });
86
+ return;
87
+ }
88
+
89
+ await permissionService.removeAlwaysRule(projectId, toolName);
90
+ res.json({ ok: true });
91
+ });
92
+
93
+ return router;
94
+ };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Chat session routes — REST API for managing Chat v2 sessions.
3
+ * GET /api/chat-sessions?projectId=xxx — list sessions for a project
4
+ * POST /api/chat-sessions — create a new session
5
+ * PATCH /api/chat-sessions/:id — rename a session
6
+ * DELETE /api/chat-sessions/:id — delete a session
7
+ * GET /api/chat-sessions/:id/messages — load message history for a session
8
+ *
9
+ * All routes require authentication.
10
+ */
11
+
12
+ import { Router } from 'express';
13
+ import type { ChatSessionService } from '../services/chat_session_service.js';
14
+ import type { ChatMessageRepository } from '../services/chat_message_repository.js';
15
+
16
+ interface ChatSessionRoutesDeps {
17
+ chatSessionService: ChatSessionService;
18
+ chatMessageRepository: ChatMessageRepository;
19
+ log: (tag: string, ...args: unknown[]) => void;
20
+ }
21
+
22
+ export const createChatSessionRoutes = ({ chatSessionService, chatMessageRepository, log }: ChatSessionRoutesDeps): Router => {
23
+ const router: Router = Router();
24
+
25
+ // GET /api/chat-sessions?projectId=xxx
26
+ router.get('/', async (req, res) => {
27
+ const { projectId } = req.query;
28
+ if (!projectId || typeof projectId !== 'string') {
29
+ res.status(400).json({ error: 'projectId query parameter is required' });
30
+ return;
31
+ }
32
+ const sessions = await chatSessionService.listSessions(projectId);
33
+ res.json({ sessions });
34
+ });
35
+
36
+ // POST /api/chat-sessions
37
+ router.post('/', async (req, res) => {
38
+ const { projectId } = req.body;
39
+ if (!projectId || typeof projectId !== 'string') {
40
+ res.status(400).json({ error: 'projectId is required' });
41
+ return;
42
+ }
43
+
44
+ const session = await chatSessionService.createSession(projectId.trim());
45
+ log('CHAT_SESSION', `Created session "${session.name}" for project ${projectId}`);
46
+ res.status(201).json({ session });
47
+ });
48
+
49
+ // PATCH /api/chat-sessions/:id
50
+ router.patch('/:id', async (req, res) => {
51
+ const { id } = req.params;
52
+ const { name } = req.body;
53
+ if (!name || typeof name !== 'string') {
54
+ res.status(400).json({ error: 'name is required' });
55
+ return;
56
+ }
57
+
58
+ const session = await chatSessionService.renameSession(id, name.trim());
59
+ if (!session) {
60
+ res.status(404).json({ error: 'Session not found' });
61
+ return;
62
+ }
63
+ res.json({ session });
64
+ });
65
+
66
+ // DELETE /api/chat-sessions/:id
67
+ router.delete('/:id', async (req, res) => {
68
+ const { id } = req.params;
69
+ await chatMessageRepository.deleteBySessionId(id);
70
+ const deleted = await chatSessionService.deleteSession(id);
71
+ if (!deleted) {
72
+ res.status(404).json({ error: 'Session not found' });
73
+ return;
74
+ }
75
+ res.json({ ok: true });
76
+ });
77
+
78
+ // GET /api/chat-sessions/:id/messages
79
+ router.get('/:id/messages', async (req, res) => {
80
+ const { id } = req.params;
81
+ const messages = await chatMessageRepository.getMessages(id);
82
+ res.json({ messages });
83
+ });
84
+
85
+ // POST /api/chat-sessions/:id/compact-continue
86
+ // Clean messages (strip tool_use/tool_result), create a new session with continuation context.
87
+ router.post('/:id/compact-continue', async (req, res) => {
88
+ const { id } = req.params;
89
+
90
+ const oldSession = await chatSessionService.getSession(id);
91
+ if (!oldSession) {
92
+ res.status(404).json({ error: 'Session not found' });
93
+ return;
94
+ }
95
+
96
+ // Read all messages from the old session
97
+ const messages = await chatMessageRepository.getMessages(id);
98
+
99
+ // Extract text content and collect file paths from tool_use blocks
100
+ const cleanedExchanges: Array<{ role: string; text: string }> = [];
101
+ const touchedFiles = new Set<string>();
102
+
103
+ for (const msg of messages) {
104
+ let blocks: Array<Record<string, unknown>>;
105
+ try {
106
+ blocks = JSON.parse(msg.content);
107
+ } catch {
108
+ continue;
109
+ }
110
+
111
+ const textParts: string[] = [];
112
+ for (const block of blocks) {
113
+ if (block.type === 'text' && typeof block.text === 'string') {
114
+ const text = (block.text as string).trim();
115
+ if (text) textParts.push(text);
116
+ }
117
+
118
+ // Extract file paths from tool_use blocks
119
+ if (block.type === 'tool_use') {
120
+ const input = block.input as Record<string, unknown> | undefined;
121
+ if (!input) continue;
122
+ const toolName = block.name as string | undefined;
123
+
124
+ // Read, Write, Edit, MultiEdit — file_path
125
+ if (input.file_path && typeof input.file_path === 'string') {
126
+ touchedFiles.add(input.file_path);
127
+ }
128
+ // Glob — pattern + path
129
+ if (toolName === 'Glob' && typeof input.pattern === 'string') {
130
+ // Not a specific file, but record the search path if given
131
+ if (typeof input.path === 'string') touchedFiles.add(input.path);
132
+ }
133
+ // Grep — path
134
+ if (toolName === 'Grep' && typeof input.path === 'string') {
135
+ touchedFiles.add(input.path);
136
+ }
137
+ // Bash — try to extract file paths from the command
138
+ if (toolName === 'Bash' && typeof input.command === 'string') {
139
+ const fileMatches = (input.command as string).match(/(?:^|\s)(\/[\w/.~-]+)/g);
140
+ if (fileMatches) {
141
+ for (const m of fileMatches) {
142
+ const p = m.trim();
143
+ // Only include paths that look like actual files (have an extension or known dirs)
144
+ if (p.includes('.') || p.includes('/src/') || p.includes('/packages/')) {
145
+ touchedFiles.add(p);
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ if (textParts.length > 0) {
154
+ cleanedExchanges.push({ role: msg.role, text: textParts.join('\n\n') });
155
+ }
156
+ }
157
+
158
+ // Build the continuation summary with conversation + touched files
159
+ const conversationLines = cleanedExchanges.map(
160
+ (e) => `**${e.role === 'user' ? 'User' : 'Assistant'}**: ${e.text}`,
161
+ );
162
+
163
+ const parts: string[] = [
164
+ `We are continuing a previous conversation. Here is the summary of what was discussed:\n\n` +
165
+ conversationLines.join('\n\n'),
166
+ ];
167
+
168
+ if (touchedFiles.size > 0) {
169
+ const sortedFiles = [...touchedFiles].sort();
170
+ parts.push(
171
+ `\n\n## Files touched in the previous conversation\n\n` +
172
+ sortedFiles.map((f) => `- ${f}`).join('\n'),
173
+ );
174
+ }
175
+
176
+ const continuationPrompt = parts.join('');
177
+
178
+ // Create the new session with the continuation context stored
179
+ const newSession = await chatSessionService.createSession(oldSession.projectId, continuationPrompt);
180
+
181
+ log('CHAT_SESSION', `Compact-continue: old=${id} → new=${newSession.id}, ${cleanedExchanges.length} exchanges`);
182
+
183
+ res.status(201).json({
184
+ session: newSession,
185
+ });
186
+ });
187
+
188
+ return router;
189
+ };
@@ -0,0 +1,131 @@
1
+ import { describe, it, beforeEach, afterEach, mock } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdtemp, rm, readFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { existsSync } from 'node:fs';
7
+ import { createChatUploadRoutes } from './chat_upload.ts';
8
+ import express from 'express';
9
+ import type { Server } from 'node:http';
10
+
11
+ const createTestApp = (workspacePath: string) => {
12
+ const workspaceService = {
13
+ getWorkspacePath: (_projectId: string) => workspacePath,
14
+ } as any;
15
+ const log = mock.fn();
16
+ const app = express();
17
+ app.use(express.json({ limit: '15mb' }));
18
+ const routes = createChatUploadRoutes({ workspaceService, log });
19
+ app.use('/api/chat-upload', routes);
20
+ return { app, log };
21
+ };
22
+
23
+ const request = (server: Server) => {
24
+ const addr = server.address() as { port: number };
25
+ const base = `http://127.0.0.1:${addr.port}`;
26
+ return {
27
+ post: async (path: string, body: any) => {
28
+ const res = await fetch(`${base}${path}`, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify(body),
32
+ });
33
+ return { status: res.status, body: await res.json() };
34
+ },
35
+ };
36
+ };
37
+
38
+ describe('chat_upload route', () => {
39
+ let tmpDir: string;
40
+ let server: Server;
41
+ let req: ReturnType<typeof request>;
42
+
43
+ beforeEach(async () => {
44
+ tmpDir = await mkdtemp(join(tmpdir(), 'chat-upload-test-'));
45
+ const { app } = createTestApp(tmpDir);
46
+ server = await new Promise<Server>((resolve) => {
47
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
48
+ });
49
+ req = request(server);
50
+ });
51
+
52
+ afterEach(async () => {
53
+ server.close();
54
+ await rm(tmpDir, { recursive: true, force: true });
55
+ });
56
+
57
+ it('uploads a file and returns its relative path', async () => {
58
+ const content = Buffer.from('hello world').toString('base64');
59
+ const res = await req.post('/api/chat-upload', {
60
+ projectId: 'proj_test',
61
+ fileName: 'test.txt',
62
+ content,
63
+ });
64
+
65
+ assert.equal(res.status, 200);
66
+ assert.ok(res.body.path.startsWith('.chat-uploads/'));
67
+ assert.ok(res.body.path.endsWith('_test.txt'));
68
+
69
+ // Verify file was actually written
70
+ const filePath = join(tmpDir, res.body.path);
71
+ assert.ok(existsSync(filePath));
72
+ const fileContent = await readFile(filePath, 'utf-8');
73
+ assert.equal(fileContent, 'hello world');
74
+ });
75
+
76
+ it('rejects missing fields', async () => {
77
+ const res = await req.post('/api/chat-upload', {
78
+ projectId: 'proj_test',
79
+ });
80
+ assert.equal(res.status, 400);
81
+ assert.ok(res.body.error.includes('required'));
82
+ });
83
+
84
+ it('sanitizes filenames with special characters', async () => {
85
+ const content = Buffer.from('data').toString('base64');
86
+ const res = await req.post('/api/chat-upload', {
87
+ projectId: 'proj_test',
88
+ fileName: '../../../etc/passwd',
89
+ content,
90
+ });
91
+
92
+ assert.equal(res.status, 200);
93
+ // The path should not contain directory traversal
94
+ assert.ok(!res.body.path.includes('..'));
95
+ assert.ok(res.body.path.startsWith('.chat-uploads/'));
96
+ });
97
+
98
+ it('rejects when workspace does not exist', async () => {
99
+ // Create a new app pointing to nonexistent workspace
100
+ const { app: badApp } = createTestApp('/nonexistent/path');
101
+ const badServer = await new Promise<Server>((resolve) => {
102
+ const s = badApp.listen(0, '127.0.0.1', () => resolve(s));
103
+ });
104
+ const badReq = request(badServer);
105
+
106
+ const content = Buffer.from('data').toString('base64');
107
+ const res = await badReq.post('/api/chat-upload', {
108
+ projectId: 'proj_test',
109
+ fileName: 'test.txt',
110
+ content,
111
+ });
112
+
113
+ assert.equal(res.status, 404);
114
+ assert.ok(res.body.error.includes('not found'));
115
+ badServer.close();
116
+ });
117
+
118
+ it('creates the .chat-uploads directory if it does not exist', async () => {
119
+ const uploadsDir = join(tmpDir, '.chat-uploads');
120
+ assert.ok(!existsSync(uploadsDir));
121
+
122
+ const content = Buffer.from('test').toString('base64');
123
+ await req.post('/api/chat-upload', {
124
+ projectId: 'proj_test',
125
+ fileName: 'new.txt',
126
+ content,
127
+ });
128
+
129
+ assert.ok(existsSync(uploadsDir));
130
+ });
131
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Chat file upload route — accepts files for chat attachments.
3
+ *
4
+ * POST /api/chat-upload
5
+ * Body: { projectId, fileName, content (base64) }
6
+ * Response: { path (relative workspace path) }
7
+ *
8
+ * Files are saved to .chat-uploads/<timestamp>_<fileName> in the project workspace.
9
+ * The returned path is relative to the workspace root, suitable for
10
+ * inclusion in CLI prompts where Claude can read them.
11
+ */
12
+
13
+ import { Router } from 'express';
14
+ import { join, resolve, basename } from 'node:path';
15
+ import { existsSync } from 'node:fs';
16
+ import { mkdir, writeFile } from 'node:fs/promises';
17
+ import type { ProjectWorkspaceService } from '../services/project_workspace_service.js';
18
+
19
+ interface ChatUploadRoutesDeps {
20
+ workspaceService: ProjectWorkspaceService;
21
+ log: (tag: string, ...args: unknown[]) => void;
22
+ }
23
+
24
+ /** Sanitize a filename — remove path separators and control characters. */
25
+ const sanitizeFileName = (name: string): string => {
26
+ return basename(name).replace(/[^\w.\-]/g, '_').slice(0, 200);
27
+ };
28
+
29
+ const UPLOAD_DIR = '.chat-uploads';
30
+
31
+ /** Max file size: 10 MB (base64 encoded ≈ 13.3 MB in body) */
32
+ const MAX_FILE_SIZE = 10 * 1024 * 1024;
33
+
34
+ export const createChatUploadRoutes = ({ workspaceService, log }: ChatUploadRoutesDeps): Router => {
35
+ const router: Router = Router();
36
+
37
+ router.post('/', async (req, res) => {
38
+ const { projectId, fileName, content } = req.body;
39
+
40
+ if (!projectId || !fileName || !content) {
41
+ res.status(400).json({ error: 'projectId, fileName, and content are required' });
42
+ return;
43
+ }
44
+
45
+ if (typeof content !== 'string') {
46
+ res.status(400).json({ error: 'content must be a base64 string' });
47
+ return;
48
+ }
49
+
50
+ // Decode and check size
51
+ const buffer = Buffer.from(content, 'base64');
52
+ if (buffer.length > MAX_FILE_SIZE) {
53
+ res.status(413).json({ error: `File too large. Maximum size is ${MAX_FILE_SIZE / (1024 * 1024)} MB` });
54
+ return;
55
+ }
56
+
57
+ const wsPath = workspaceService.getWorkspacePath(projectId);
58
+ if (!existsSync(wsPath)) {
59
+ res.status(404).json({ error: 'Project workspace not found' });
60
+ return;
61
+ }
62
+
63
+ try {
64
+ const uploadDir = join(wsPath, UPLOAD_DIR);
65
+ if (!existsSync(uploadDir)) {
66
+ await mkdir(uploadDir, { recursive: true });
67
+ }
68
+
69
+ const safeName = sanitizeFileName(fileName);
70
+ const uniqueName = `${Date.now()}_${safeName}`;
71
+ const filePath = join(uploadDir, uniqueName);
72
+
73
+ // Validate no path traversal
74
+ const resolved = resolve(filePath);
75
+ if (!resolved.startsWith(resolve(wsPath))) {
76
+ res.status(400).json({ error: 'Invalid file path' });
77
+ return;
78
+ }
79
+
80
+ await writeFile(filePath, buffer);
81
+
82
+ const relativePath = `${UPLOAD_DIR}/${uniqueName}`;
83
+ log('CHAT_UPLOAD', `Saved ${relativePath} (${buffer.length} bytes) for project ${projectId}`);
84
+
85
+ res.json({ path: relativePath });
86
+ } catch (err: unknown) {
87
+ const msg = err instanceof Error ? err.message : 'Unknown error';
88
+ log('CHAT_UPLOAD', `Upload failed: ${msg}`);
89
+ res.status(500).json({ error: `Upload failed: ${msg}` });
90
+ }
91
+ });
92
+
93
+ return router;
94
+ };
@@ -109,15 +109,24 @@ describe('File Routes', () => {
109
109
  assert.equal(readmeEntry.type, 'file');
110
110
  });
111
111
 
112
- it('skips hidden files and directories', async () => {
112
+ it('excludes only .git directory, shows other dotfiles', async () => {
113
113
  await mkdir(join(tmpDir, '.git'));
114
+ await mkdir(join(tmpDir, '.claude'));
114
115
  await writeFile(join(tmpDir, '.gitignore'), 'node_modules');
116
+ await writeFile(join(tmpDir, '.env'), 'SECRET=123');
115
117
  await writeFile(join(tmpDir, 'visible.ts'), 'export {}');
116
118
 
117
119
  const { status, body } = await http.get('/api/projects/proj_1/files');
118
120
  assert.equal(status, 200);
119
- assert.equal(body.tree.length, 1);
120
- assert.equal(body.tree[0].name, 'visible.ts');
121
+ const names = body.tree.map((e: any) => e.name);
122
+ // .git must be excluded
123
+ assert.ok(!names.includes('.git'), '.git should be excluded');
124
+ // Other dotfiles must be present
125
+ assert.ok(names.includes('.claude'), '.claude directory should be shown');
126
+ assert.ok(names.includes('.gitignore'), '.gitignore should be shown');
127
+ assert.ok(names.includes('.env'), '.env should be shown');
128
+ assert.ok(names.includes('visible.ts'), 'visible.ts should be shown');
129
+ assert.equal(body.tree.length, 4);
121
130
  });
122
131
  });
123
132
 
@@ -111,8 +111,8 @@ const buildTree = async (dirPath: string, basePath: string): Promise<TreeEntry[]
111
111
  const tree: TreeEntry[] = [];
112
112
 
113
113
  for (const entry of entries) {
114
- // Skip hidden files/directories (like .git)
115
- if (entry.name.startsWith('.')) continue;
114
+ // Only skip .git directory — all other dotfiles are shown
115
+ if (entry.name === '.git') continue;
116
116
 
117
117
  const fullPath = join(dirPath, entry.name);
118
118
  const relPath = relative(basePath, fullPath);