@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
@@ -0,0 +1,345 @@
1
+ /**
2
+ * ChatCliBridge — spawns Claude Code CLI in non-interactive mode for Chat v2.
3
+ *
4
+ * Each user message spawns a new CLI invocation:
5
+ * claude -p - --output-format stream-json --verbose --include-partial-messages
6
+ * --append-system-prompt "We are working on project-id ${projectId}"
7
+ *
8
+ * New sessions use --session-id <uuid>, subsequent messages use --resume <uuid>.
9
+ * Permission mode controls: --dangerously-skip-permissions, --allowedTools, or --permission-prompt-tool.
10
+ * When permission mode is 'prompt', a temporary MCP config file is generated
11
+ * pointing to the permission MCP server, and --mcp-config is passed to the CLI.
12
+ * Emits parsed stream-json events via onEvent callback for downstream WebSocket forwarding.
13
+ */
14
+
15
+ import { spawn, type ChildProcess } from 'node:child_process';
16
+ import { join } from 'node:path';
17
+ import { existsSync, mkdirSync } from 'node:fs';
18
+
19
+ export type PermissionMode = 'skip' | 'allowed_tools';
20
+
21
+ export interface ChatCliBridgeDeps {
22
+ workspacesDir: string;
23
+ log: (tag: string, ...args: unknown[]) => void;
24
+ }
25
+
26
+ export interface SpawnOptions {
27
+ projectId: string;
28
+ message: string;
29
+ claudeSessionId: string;
30
+ isNewSession: boolean;
31
+ permissionMode: PermissionMode;
32
+ allowedTools?: string[];
33
+ /** Compacted conversation context to prepend to the system prompt */
34
+ continuationContext?: string;
35
+ onEvent: (event: Record<string, unknown>) => void;
36
+ }
37
+
38
+ export interface SpawnCompletion {
39
+ exitCode: number | null;
40
+ claudeSessionId: string;
41
+ sessionId: string | null;
42
+ /** Captured stderr lines (if any) — useful for diagnosing non-zero exit codes */
43
+ stderr: string | null;
44
+ /** Non-JSON lines from stdout (if any) — CLI errors often appear here */
45
+ stdoutNonJsonLines: string | null;
46
+ }
47
+
48
+ export interface SpawnResult {
49
+ childProcess: ChildProcess;
50
+ completion: Promise<SpawnCompletion>;
51
+ /** The composed system prompt text (only set when isNewSession is true). */
52
+ systemPrompt: string | null;
53
+ }
54
+
55
+ export class ChatCliBridge {
56
+ private readonly workspacesDir: string;
57
+ private readonly log: ChatCliBridgeDeps['log'];
58
+ private readonly activeProcesses = new Map<string, ChildProcess>();
59
+
60
+ constructor({ workspacesDir, log }: ChatCliBridgeDeps) {
61
+ this.workspacesDir = workspacesDir;
62
+ this.log = log;
63
+ }
64
+
65
+ /**
66
+ * Resolve the workspace directory for a project.
67
+ * The CLI runs with cwd set to this path.
68
+ */
69
+ getWorkspaceCwd = (projectId: string): string => {
70
+ return join(this.workspacesDir, projectId);
71
+ };
72
+
73
+ /**
74
+ * Build the CLI arguments array from spawn options.
75
+ * Returns both the args array and the composed system prompt text (if any).
76
+ */
77
+ buildArgs = (options: SpawnOptions): { args: string[]; systemPrompt: string | null } => {
78
+ const { projectId, claudeSessionId, isNewSession, permissionMode, allowedTools, continuationContext } = options;
79
+ const args: string[] = [];
80
+ let systemPrompt: string | null = null;
81
+
82
+ // Permission mode
83
+ if (permissionMode === 'skip') {
84
+ args.push('--dangerously-skip-permissions');
85
+ } else if (permissionMode === 'allowed_tools' && allowedTools && allowedTools.length > 0) {
86
+ for (const tool of allowedTools) {
87
+ args.push('--allowedTools', tool);
88
+ }
89
+ }
90
+
91
+ // Prompt via stdin
92
+ args.push('-p', '-');
93
+
94
+ // Session management
95
+ if (isNewSession) {
96
+ args.push('--session-id', claudeSessionId);
97
+ } else {
98
+ args.push('--resume', claudeSessionId);
99
+ }
100
+
101
+ // Output format
102
+ args.push('--output-format', 'stream-json');
103
+ args.push('--verbose');
104
+ args.push('--include-partial-messages');
105
+
106
+ // Project context system prompt (only on first message; resumed sessions already have it)
107
+ if (isNewSession) {
108
+ const systemParts: string[] = [];
109
+
110
+ // Prepend compacted conversation context when continuing from a previous session
111
+ if (continuationContext) {
112
+ systemParts.push(continuationContext);
113
+ }
114
+
115
+ systemParts.push(`We are working on project-id ${projectId}
116
+ You are only allowed to read/write/edit/delete (any work with) files in the /workspaces/${projectId} directory.
117
+
118
+ ## Git Repository Structure
119
+ Each project workspace (workspaces/${projectId}/) has its OWN independent git repo
120
+ When committing and pushing changes, always use the workspace git repo (your cwd), not the top-level repo.
121
+ The skill files (.claude/skills/) and project code are tracked in the workspace repo.
122
+
123
+ ## Skills
124
+ Project skills (e.g. assistkick-interview, assistkick-developer, etc.) are defined in .claude/skills/ within this workspace.
125
+ To invoke a skill, always use the Skill tool (e.g. Skill with skill: "assistkick-interview"). Do NOT manually read or execute skill files — the Skill tool handles loading and execution.
126
+ Only edit code and project files — never modify the skill definition files in .claude/skills/.`);
127
+
128
+ systemPrompt = systemParts.join('\n\n');
129
+ args.push('--append-system-prompt', systemPrompt);
130
+ }
131
+
132
+ return { args, systemPrompt };
133
+ };
134
+
135
+ /**
136
+ * Build environment variables for the CLI subprocess.
137
+ * Ensures common bin paths are available.
138
+ */
139
+ buildEnv = (): Record<string, string> => {
140
+ const env = { ...process.env } as Record<string, string>;
141
+ const home = env.HOME || '/root';
142
+ const extraPaths = [
143
+ `${home}/.local/bin`,
144
+ `${home}/.npm-global/bin`,
145
+ `${home}/.nvm/current/bin`,
146
+ '/usr/local/bin',
147
+ '/opt/homebrew/bin',
148
+ ];
149
+ const existing = env.PATH || '/usr/bin:/bin';
150
+ const missing = extraPaths.filter(p => !existing.split(':').includes(p));
151
+ if (missing.length) {
152
+ env.PATH = [...missing, existing].join(':');
153
+ }
154
+ return env;
155
+ };
156
+
157
+ /**
158
+ * Spawn a Claude CLI process for a chat message.
159
+ * Returns a handle to the child process and a promise that resolves on completion.
160
+ */
161
+ spawn = (options: SpawnOptions): SpawnResult => {
162
+ const { projectId, message, claudeSessionId, isNewSession, onEvent } = options;
163
+ const cwd = this.getWorkspaceCwd(projectId);
164
+ const { args, systemPrompt } = this.buildArgs(options);
165
+
166
+ // Ensure workspace directory exists (Docker volumes may not have project subdirs)
167
+ if (!existsSync(cwd)) {
168
+ mkdirSync(cwd, { recursive: true });
169
+ }
170
+
171
+ const mode = isNewSession ? 'new' : 'resume';
172
+ this.log('CHAT_CLI', `Spawning claude (${mode}) for project ${projectId}, session ${claudeSessionId}`);
173
+ this.log('CHAT_CLI', `cwd: ${cwd} (exists: ${existsSync(cwd)}), args: claude ${args.join(' ')}`);
174
+
175
+ const child = spawn('claude', args, {
176
+ cwd,
177
+ env: this.buildEnv(),
178
+ stdio: ['pipe', 'pipe', 'pipe'],
179
+ });
180
+
181
+ // Track active process by session
182
+ this.activeProcesses.set(claudeSessionId, child);
183
+
184
+ const completion = new Promise<SpawnCompletion>((resolve, reject) => {
185
+ let lineBuf = '';
186
+ let capturedSessionId: string | null = null;
187
+ let stdoutReceived = false;
188
+ let stderrLines: string[] = [];
189
+ let stdoutNonJsonLines: string[] = [];
190
+
191
+ child.stdout!.on('data', (chunk: Buffer) => {
192
+ stdoutReceived = true;
193
+ lineBuf += chunk.toString();
194
+ const lines = lineBuf.split('\n');
195
+ lineBuf = lines.pop()!;
196
+
197
+ for (const line of lines) {
198
+ const trimmed = line.trim();
199
+ if (!trimmed) continue;
200
+ this.processLine(trimmed, onEvent, (sid) => {
201
+ capturedSessionId = sid;
202
+ }, (nonJsonLine) => {
203
+ stdoutNonJsonLines.push(nonJsonLine);
204
+ });
205
+ }
206
+ });
207
+
208
+ child.stderr!.on('data', (chunk: Buffer) => {
209
+ const text = chunk.toString();
210
+ text.split('\n').forEach(line => {
211
+ if (line.trim()) {
212
+ stderrLines.push(line.trim());
213
+ this.log('CHAT_CLI', `[stderr] ${line.trim()}`);
214
+ }
215
+ });
216
+ });
217
+
218
+ // Write the message to stdin and close
219
+ child.stdin!.write(message);
220
+ child.stdin!.end();
221
+ this.log('CHAT_CLI', `Message written to stdin (${message.length} chars)`);
222
+
223
+ child.on('close', (code) => {
224
+ // Process any remaining data in the line buffer
225
+ if (lineBuf.trim()) {
226
+ this.processLine(lineBuf.trim(), onEvent, (sid) => {
227
+ capturedSessionId = sid;
228
+ }, (nonJsonLine) => {
229
+ stdoutNonJsonLines.push(nonJsonLine);
230
+ });
231
+ }
232
+
233
+ this.activeProcesses.delete(claudeSessionId);
234
+
235
+ if (code !== 0) {
236
+ this.log('CHAT_CLI', `Process FAILED with exit code ${code} for session ${claudeSessionId}`);
237
+ if (stderrLines.length > 0) {
238
+ this.log('CHAT_CLI', `[stderr summary] ${stderrLines.join(' | ')}`);
239
+ }
240
+ if (stdoutNonJsonLines.length > 0) {
241
+ this.log('CHAT_CLI', `[stdout non-json summary] ${stdoutNonJsonLines.join(' | ')}`);
242
+ }
243
+ if (!stdoutReceived) {
244
+ this.log('CHAT_CLI', `No stdout received — CLI may have crashed or failed to start`);
245
+ }
246
+ } else {
247
+ this.log('CHAT_CLI', `Process exited successfully for session ${claudeSessionId}`);
248
+ }
249
+
250
+ resolve({
251
+ exitCode: code,
252
+ claudeSessionId,
253
+ sessionId: capturedSessionId,
254
+ stderr: stderrLines.length > 0 ? stderrLines.join('\n') : null,
255
+ stdoutNonJsonLines: stdoutNonJsonLines.length > 0 ? stdoutNonJsonLines.join('\n') : null,
256
+ });
257
+ });
258
+
259
+ child.on('error', (err) => {
260
+ this.activeProcesses.delete(claudeSessionId);
261
+ this.log('CHAT_CLI', `Process spawn error for session ${claudeSessionId}: ${err.message} (${(err as NodeJS.ErrnoException).code || 'unknown'})`);
262
+ reject(err);
263
+ });
264
+ });
265
+
266
+ return { childProcess: child, completion, systemPrompt };
267
+ };
268
+
269
+ /**
270
+ * Kill an active CLI process by session ID.
271
+ * Returns true if a process was found and killed.
272
+ */
273
+ kill = (claudeSessionId: string): boolean => {
274
+ const child = this.activeProcesses.get(claudeSessionId);
275
+ if (!child) return false;
276
+
277
+ this.log('CHAT_CLI', `Killing process for session ${claudeSessionId}`);
278
+ child.kill('SIGTERM');
279
+
280
+ // Force kill after 5 seconds if still alive
281
+ const forceKillTimer = setTimeout(() => {
282
+ if (!child.killed) {
283
+ this.log('CHAT_CLI', `Force killing process for session ${claudeSessionId}`);
284
+ child.kill('SIGKILL');
285
+ }
286
+ }, 5000);
287
+
288
+ child.on('close', () => {
289
+ clearTimeout(forceKillTimer);
290
+ });
291
+
292
+ return true;
293
+ };
294
+
295
+ /**
296
+ * Kill all active CLI processes (for server shutdown).
297
+ */
298
+ killAll = (): void => {
299
+ for (const [sessionId, child] of this.activeProcesses) {
300
+ this.log('CHAT_CLI', `Killing process for session ${sessionId} (shutdown)`);
301
+ child.kill('SIGTERM');
302
+ }
303
+ this.activeProcesses.clear();
304
+ };
305
+
306
+ /**
307
+ * Check if a process is currently active for a session.
308
+ */
309
+ isActive = (claudeSessionId: string): boolean => {
310
+ return this.activeProcesses.has(claudeSessionId);
311
+ };
312
+
313
+ /**
314
+ * Get the number of currently active processes.
315
+ */
316
+ getActiveCount = (): number => {
317
+ return this.activeProcesses.size;
318
+ };
319
+
320
+ /**
321
+ * Parse a single line of stream-json output and emit events.
322
+ * Also captures session_id from result events.
323
+ */
324
+ private processLine = (
325
+ line: string,
326
+ onEvent: (event: Record<string, unknown>) => void,
327
+ onSessionId: (sessionId: string) => void,
328
+ onNonJsonLine?: (line: string) => void,
329
+ ): void => {
330
+ try {
331
+ const event = JSON.parse(line) as Record<string, unknown>;
332
+ onEvent(event);
333
+
334
+ // Capture session_id from result event
335
+ if (event.type === 'result' && typeof event.session_id === 'string') {
336
+ onSessionId(event.session_id);
337
+ }
338
+ } catch {
339
+ // Not valid JSON — log and capture it as it may contain useful error info
340
+ const truncated = line.slice(0, 500);
341
+ this.log('CHAT_CLI', `[stdout non-json] ${truncated}`);
342
+ onNonJsonLine?.(truncated);
343
+ }
344
+ };
345
+ }
@@ -0,0 +1,170 @@
1
+ import { describe, it, mock, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { ChatMessageRepository } from './chat_message_repository.ts';
4
+
5
+ const createMockDb = () => {
6
+ let selectResults: any[][] = [];
7
+ let selectCallIndex = 0;
8
+
9
+ const db = {
10
+ select: mock.fn(() => ({
11
+ from: mock.fn(() => ({
12
+ where: mock.fn(() => {
13
+ const result = selectResults[selectCallIndex] || [];
14
+ selectCallIndex++;
15
+ return Object.assign(result, {
16
+ orderBy: mock.fn(() => result),
17
+ });
18
+ }),
19
+ })),
20
+ })),
21
+ insert: mock.fn(() => ({
22
+ values: mock.fn(() => Promise.resolve()),
23
+ })),
24
+ delete: mock.fn(() => ({
25
+ where: mock.fn(() => Promise.resolve({ changes: 3 })),
26
+ })),
27
+ _setSelectResults: (results: any[][]) => {
28
+ selectResults = results;
29
+ selectCallIndex = 0;
30
+ },
31
+ };
32
+
33
+ return db;
34
+ };
35
+
36
+ const createRepository = (db: any) => {
37
+ return new ChatMessageRepository({
38
+ getDb: () => db,
39
+ log: mock.fn(),
40
+ });
41
+ };
42
+
43
+ describe('ChatMessageRepository', () => {
44
+ let db: ReturnType<typeof createMockDb>;
45
+ let repo: ChatMessageRepository;
46
+
47
+ beforeEach(() => {
48
+ db = createMockDb();
49
+ repo = createRepository(db);
50
+ });
51
+
52
+ describe('generateId', () => {
53
+ it('returns an id with cmsg_ prefix', () => {
54
+ const id = repo.generateId();
55
+ assert.ok(id.startsWith('cmsg_'));
56
+ assert.ok(id.length > 5);
57
+ });
58
+
59
+ it('generates unique ids', () => {
60
+ const id1 = repo.generateId();
61
+ const id2 = repo.generateId();
62
+ assert.notEqual(id1, id2);
63
+ });
64
+ });
65
+
66
+ describe('getNextOrderIndex', () => {
67
+ it('returns 0 when no messages exist', async () => {
68
+ db._setSelectResults([[{ maxIndex: null }]]);
69
+ const index = await repo.getNextOrderIndex('csess_abc');
70
+ assert.equal(index, 0);
71
+ });
72
+
73
+ it('returns max + 1 when messages exist', async () => {
74
+ db._setSelectResults([[{ maxIndex: 5 }]]);
75
+ const index = await repo.getNextOrderIndex('csess_abc');
76
+ assert.equal(index, 6);
77
+ });
78
+
79
+ it('returns 1 when max is 0', async () => {
80
+ db._setSelectResults([[{ maxIndex: 0 }]]);
81
+ const index = await repo.getNextOrderIndex('csess_abc');
82
+ assert.equal(index, 1);
83
+ });
84
+ });
85
+
86
+ describe('saveMessage', () => {
87
+ it('inserts a message and returns the saved row', async () => {
88
+ const row = await repo.saveMessage(
89
+ 'csess_abc',
90
+ 'user',
91
+ JSON.stringify([{ type: 'text', text: 'Hello' }]),
92
+ 0,
93
+ );
94
+
95
+ assert.ok(row.id.startsWith('cmsg_'));
96
+ assert.equal(row.sessionId, 'csess_abc');
97
+ assert.equal(row.role, 'user');
98
+ assert.equal(row.orderIndex, 0);
99
+ assert.ok(row.createdAt);
100
+
101
+ const content = JSON.parse(row.content);
102
+ assert.equal(content[0].type, 'text');
103
+ assert.equal(content[0].text, 'Hello');
104
+
105
+ assert.equal(db.insert.mock.calls.length, 1);
106
+ });
107
+
108
+ it('saves assistant messages with complex content', async () => {
109
+ const content = JSON.stringify([
110
+ { type: 'text', text: 'Here is the code:' },
111
+ { type: 'tool_use', id: 'tu_1', name: 'Write', input: { path: '/test.ts' } },
112
+ ]);
113
+
114
+ const row = await repo.saveMessage('csess_abc', 'assistant', content, 1);
115
+
116
+ assert.equal(row.role, 'assistant');
117
+ assert.equal(row.orderIndex, 1);
118
+
119
+ const parsed = JSON.parse(row.content);
120
+ assert.equal(parsed.length, 2);
121
+ assert.equal(parsed[0].type, 'text');
122
+ assert.equal(parsed[1].type, 'tool_use');
123
+ });
124
+ });
125
+
126
+ describe('getMessages', () => {
127
+ it('returns messages for a session ordered by index', async () => {
128
+ const mockMessages = [
129
+ {
130
+ id: 'cmsg_001',
131
+ sessionId: 'csess_abc',
132
+ role: 'user',
133
+ content: JSON.stringify([{ type: 'text', text: 'Hello' }]),
134
+ orderIndex: 0,
135
+ createdAt: '2026-03-11T14:00:00.000Z',
136
+ },
137
+ {
138
+ id: 'cmsg_002',
139
+ sessionId: 'csess_abc',
140
+ role: 'assistant',
141
+ content: JSON.stringify([{ type: 'text', text: 'Hi there!' }]),
142
+ orderIndex: 1,
143
+ createdAt: '2026-03-11T14:00:01.000Z',
144
+ },
145
+ ];
146
+ db._setSelectResults([mockMessages]);
147
+
148
+ const messages = await repo.getMessages('csess_abc');
149
+ assert.equal(messages.length, 2);
150
+ assert.equal(messages[0].role, 'user');
151
+ assert.equal(messages[1].role, 'assistant');
152
+ assert.equal(messages[0].orderIndex, 0);
153
+ assert.equal(messages[1].orderIndex, 1);
154
+ });
155
+
156
+ it('returns empty array when no messages exist', async () => {
157
+ db._setSelectResults([[]]);
158
+ const messages = await repo.getMessages('csess_nonexistent');
159
+ assert.equal(messages.length, 0);
160
+ });
161
+ });
162
+
163
+ describe('deleteBySessionId', () => {
164
+ it('deletes all messages for a session and returns count', async () => {
165
+ const count = await repo.deleteBySessionId('csess_abc');
166
+ assert.equal(count, 3);
167
+ assert.equal(db.delete.mock.calls.length, 1);
168
+ });
169
+ });
170
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * ChatMessageRepository — persists chat messages in the database.
3
+ * Each message belongs to a chat session and stores role, content blocks
4
+ * (as JSON), an ordering index, and a timestamp.
5
+ */
6
+
7
+ import { randomBytes } from 'node:crypto';
8
+ import { eq, asc, max } from 'drizzle-orm';
9
+ import { chatMessages } from '@assistkick/shared/db/schema.js';
10
+
11
+ interface ChatMessageRepositoryDeps {
12
+ getDb: () => any;
13
+ log: (tag: string, ...args: unknown[]) => void;
14
+ }
15
+
16
+ export interface ChatMessageRow {
17
+ id: string;
18
+ sessionId: string;
19
+ role: string;
20
+ content: string;
21
+ orderIndex: number;
22
+ createdAt: string;
23
+ }
24
+
25
+ export class ChatMessageRepository {
26
+ private readonly getDb: ChatMessageRepositoryDeps['getDb'];
27
+ private readonly log: ChatMessageRepositoryDeps['log'];
28
+
29
+ constructor({ getDb, log }: ChatMessageRepositoryDeps) {
30
+ this.getDb = getDb;
31
+ this.log = log;
32
+ }
33
+
34
+ generateId = (): string => {
35
+ const hex = randomBytes(4).toString('hex');
36
+ return `cmsg_${hex}`;
37
+ };
38
+
39
+ /** Get the next available order index for a session. */
40
+ getNextOrderIndex = async (sessionId: string): Promise<number> => {
41
+ const db = this.getDb();
42
+ const result = await db
43
+ .select({ maxIndex: max(chatMessages.orderIndex) })
44
+ .from(chatMessages)
45
+ .where(eq(chatMessages.sessionId, sessionId));
46
+ const current = result[0]?.maxIndex;
47
+ return current != null ? current + 1 : 0;
48
+ };
49
+
50
+ /** Save a single message. Returns the saved row. */
51
+ saveMessage = async (
52
+ sessionId: string,
53
+ role: string,
54
+ content: string,
55
+ orderIndex: number,
56
+ ): Promise<ChatMessageRow> => {
57
+ const id = this.generateId();
58
+ const createdAt = new Date().toISOString();
59
+
60
+ const db = this.getDb();
61
+ await db.insert(chatMessages).values({
62
+ id,
63
+ sessionId,
64
+ role,
65
+ content,
66
+ orderIndex,
67
+ createdAt,
68
+ });
69
+
70
+ this.log('CHAT_MSG', `Saved ${role} message ${id} at index ${orderIndex} for session ${sessionId}`);
71
+
72
+ return { id, sessionId, role, content, orderIndex, createdAt };
73
+ };
74
+
75
+ /** Load all messages for a session, ordered by orderIndex ascending. */
76
+ getMessages = async (sessionId: string): Promise<ChatMessageRow[]> => {
77
+ const db = this.getDb();
78
+ const rows = await db
79
+ .select()
80
+ .from(chatMessages)
81
+ .where(eq(chatMessages.sessionId, sessionId))
82
+ .orderBy(asc(chatMessages.orderIndex));
83
+ return rows as ChatMessageRow[];
84
+ };
85
+
86
+ /** Update the content of an existing message (used to finalize partial assistant responses). */
87
+ updateMessageContent = async (id: string, content: string): Promise<void> => {
88
+ const db = this.getDb();
89
+ await db
90
+ .update(chatMessages)
91
+ .set({ content })
92
+ .where(eq(chatMessages.id, id));
93
+ this.log('CHAT_MSG', `Updated content for message ${id}`);
94
+ };
95
+
96
+ /** Delete all messages for a session (used when session is deleted). */
97
+ deleteBySessionId = async (sessionId: string): Promise<number> => {
98
+ const db = this.getDb();
99
+ const result = await db
100
+ .delete(chatMessages)
101
+ .where(eq(chatMessages.sessionId, sessionId));
102
+ const count = result.changes ?? 0;
103
+ this.log('CHAT_MSG', `Deleted ${count} messages for session ${sessionId}`);
104
+ return count;
105
+ };
106
+ }