@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
@@ -2,7 +2,9 @@ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { SshKeyService } from './ssh_key_service.ts';
4
4
  import { existsSync } from 'node:fs';
5
- import { readFile } from 'node:fs/promises';
5
+ import { readFile, rm, mkdtemp } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { join } from 'node:path';
6
8
 
7
9
  // 32-byte key as 64-char hex string
8
10
  const TEST_ENCRYPTION_KEY = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2';
@@ -91,7 +93,7 @@ describe('SshKeyService', () => {
91
93
  process.env.ENCRYPTION_KEY = 'tooshort';
92
94
  const service = createService();
93
95
 
94
- assert.throws(() => service.encrypt('test'), /64-character hex/);
96
+ assert.throws(() => service.encrypt('test'), /at least 32 characters/);
95
97
  });
96
98
 
97
99
  it('fails to decrypt with tampered ciphertext', () => {
@@ -108,21 +110,38 @@ describe('SshKeyService', () => {
108
110
  });
109
111
 
110
112
  describe('writeTempKeyFile / removeTempKeyFile', () => {
111
- it('writes a temp file with correct content and permissions', async () => {
113
+ it('writes a temp file converting PKCS8 to OpenSSH format', async () => {
112
114
  const service = createService();
113
- const content = '-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----';
115
+ const keyPair = service.generateKeyPair();
114
116
 
115
- const filepath = await service.writeTempKeyFile(content);
117
+ const filepath = await service.writeTempKeyFile(keyPair.privateKey);
116
118
 
117
119
  assert.ok(existsSync(filepath));
118
120
  const written = await readFile(filepath, 'utf8');
119
- assert.equal(written, content);
121
+ assert.ok(written.includes('-----BEGIN OPENSSH PRIVATE KEY-----'));
122
+ assert.ok(written.includes('-----END OPENSSH PRIVATE KEY-----'));
120
123
 
121
124
  // Clean up
122
125
  await service.removeTempKeyFile(filepath);
123
126
  assert.ok(!existsSync(filepath));
124
127
  });
125
128
 
129
+ it('passes through keys already in OpenSSH format', async () => {
130
+ const service = createService();
131
+ const keyPair = service.generateKeyPair();
132
+
133
+ // Write once to get OpenSSH format, then write again
134
+ const firstPath = await service.writeTempKeyFile(keyPair.privateKey);
135
+ const opensshKey = await readFile(firstPath, 'utf8');
136
+ await service.removeTempKeyFile(firstPath);
137
+
138
+ const secondPath = await service.writeTempKeyFile(opensshKey);
139
+ const written = await readFile(secondPath, 'utf8');
140
+ assert.equal(written, opensshKey);
141
+
142
+ await service.removeTempKeyFile(secondPath);
143
+ });
144
+
126
145
  it('removeTempKeyFile does not throw for non-existent file', async () => {
127
146
  const service = createService();
128
147
  // Should not throw
@@ -141,6 +160,58 @@ describe('SshKeyService', () => {
141
160
  });
142
161
  });
143
162
 
163
+ describe('installKeyForCli', () => {
164
+ it('writes private key, public key, and ssh config to a temp HOME/.ssh/', async () => {
165
+ const service = createService();
166
+ const keyPair = service.generateKeyPair();
167
+
168
+ // Use a temp dir as fake HOME so we don't clobber real ~/.ssh
169
+ const fakeHome = await mkdtemp(join(tmpdir(), 'ssh-test-'));
170
+ const origHome = process.env.HOME;
171
+ process.env.HOME = fakeHome;
172
+
173
+ try {
174
+ await service.installKeyForCli(keyPair);
175
+
176
+ const sshDir = join(fakeHome, '.ssh');
177
+ const privKey = await readFile(join(sshDir, 'ssh_id_ed25519'), 'utf8');
178
+ const pubKey = await readFile(join(sshDir, 'ssh_id_ed25519.pub'), 'utf8');
179
+ const config = await readFile(join(sshDir, 'config'), 'utf8');
180
+
181
+ assert.ok(privKey.includes('BEGIN OPENSSH PRIVATE KEY'));
182
+ assert.ok(pubKey.startsWith('ssh-ed25519 '));
183
+ assert.ok(pubKey.includes('assistkick-deploy'));
184
+ assert.ok(config.includes('Host github.com'));
185
+ assert.ok(config.includes('ssh_id_ed25519'));
186
+ } finally {
187
+ process.env.HOME = origHome;
188
+ await rm(fakeHome, { recursive: true });
189
+ }
190
+ });
191
+
192
+ it('replaces existing github.com block in ssh config', async () => {
193
+ const service = createService();
194
+ const keyPair = service.generateKeyPair();
195
+
196
+ const fakeHome = await mkdtemp(join(tmpdir(), 'ssh-test-'));
197
+ const origHome = process.env.HOME;
198
+ process.env.HOME = fakeHome;
199
+
200
+ try {
201
+ // Install twice — second should replace, not duplicate
202
+ await service.installKeyForCli(keyPair);
203
+ await service.installKeyForCli(keyPair);
204
+
205
+ const config = await readFile(join(fakeHome, '.ssh', 'config'), 'utf8');
206
+ const matches = config.match(/Host github\.com/g);
207
+ assert.equal(matches?.length, 1, 'should have exactly one Host github.com block');
208
+ } finally {
209
+ process.env.HOME = origHome;
210
+ await rm(fakeHome, { recursive: true });
211
+ }
212
+ });
213
+ });
214
+
144
215
  describe('isConfigured', () => {
145
216
  it('returns true when ENCRYPTION_KEY is valid', () => {
146
217
  const service = createService();
@@ -3,9 +3,9 @@
3
3
  * with AES-256-GCM, and manages temporary key files for git operations.
4
4
  */
5
5
 
6
- import { generateKeyPairSync, createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
7
- import { writeFile, unlink, chmod } from 'node:fs/promises';
8
- import { tmpdir } from 'node:os';
6
+ import { generateKeyPairSync, createCipheriv, createDecipheriv, randomBytes, createPrivateKey, createPublicKey } from 'node:crypto';
7
+ import { writeFile, unlink, chmod, mkdir, readFile } from 'node:fs/promises';
8
+ import { tmpdir, homedir } from 'node:os';
9
9
  import { join } from 'node:path';
10
10
 
11
11
  interface SshKeyServiceDeps {
@@ -30,15 +30,29 @@ export class SshKeyService {
30
30
  this.log = log;
31
31
  }
32
32
 
33
+ static readonly KEY_HELP = 'Set ENCRYPTION_KEY in your .env file (any string with at least 32 characters). Generate one with: openssl rand -hex 32';
34
+
35
+ /** Parse ENCRYPTION_KEY: try hex first, then base64, then use raw UTF-8 bytes via SHA-256. */
33
36
  private getEncryptionKey = (): Buffer => {
34
37
  const key = process.env.ENCRYPTION_KEY;
35
- if (!key) throw new Error('ENCRYPTION_KEY environment variable is not set');
36
- // Expect a 64-char hex string (32 bytes)
37
- const buf = Buffer.from(key, 'hex');
38
- if (buf.length !== 32) {
39
- throw new Error('ENCRYPTION_KEY must be a 64-character hex string (32 bytes for AES-256)');
38
+ if (!key) {
39
+ throw new Error(`ENCRYPTION_KEY environment variable is not set. ${SshKeyService.KEY_HELP}`);
40
+ }
41
+ if (key.length < 32) {
42
+ throw new Error(`ENCRYPTION_KEY must be at least 32 characters. ${SshKeyService.KEY_HELP}`);
43
+ }
44
+ // Try hex (64-char hex = 32 bytes)
45
+ if (/^[0-9a-fA-F]{64}$/.test(key)) {
46
+ return Buffer.from(key, 'hex');
40
47
  }
41
- return buf;
48
+ // Try base64 (44 chars with padding = 32 bytes)
49
+ if (/^[A-Za-z0-9+/]{43}=?$/.test(key)) {
50
+ const buf = Buffer.from(key, 'base64');
51
+ if (buf.length === 32) return buf;
52
+ }
53
+ // Fallback: SHA-256 hash of the raw string to get exactly 32 bytes
54
+ const { createHash } = require('node:crypto');
55
+ return createHash('sha256').update(key).digest();
42
56
  };
43
57
 
44
58
  /** Generate an ED25519 SSH keypair. */
@@ -113,12 +127,80 @@ export class SshKeyService {
113
127
  return plaintext;
114
128
  };
115
129
 
130
+ /**
131
+ * Convert a PKCS8 PEM private key to OpenSSH format.
132
+ * Many OpenSSH builds can't read PKCS8 PEM for ED25519, so we convert
133
+ * to the native OpenSSH private key format that all versions support.
134
+ */
135
+ private pkcs8ToOpenSsh = (pkcs8Pem: string): string => {
136
+ const privKeyObj = createPrivateKey(pkcs8Pem);
137
+ const pubKeyObj = createPublicKey(privKeyObj);
138
+
139
+ const privDer = privKeyObj.export({ type: 'pkcs8', format: 'der' });
140
+ const pubDer = pubKeyObj.export({ type: 'spki', format: 'der' });
141
+
142
+ // ED25519 PKCS8 DER: seed (32 bytes) at offset 16
143
+ // ED25519 SPKI DER: public key (32 bytes) at offset 12
144
+ const seed = Buffer.from(privDer.subarray(16, 48));
145
+ const rawPubKey = Buffer.from(pubDer.subarray(12, 44));
146
+
147
+ const sshStr = (buf: Buffer): Buffer => {
148
+ const len = Buffer.alloc(4);
149
+ len.writeUInt32BE(buf.length);
150
+ return Buffer.concat([len, buf]);
151
+ };
152
+ const uint32 = (n: number): Buffer => {
153
+ const b = Buffer.alloc(4);
154
+ b.writeUInt32BE(n);
155
+ return b;
156
+ };
157
+
158
+ const keyType = Buffer.from('ssh-ed25519');
159
+ const pubBlob = Buffer.concat([sshStr(keyType), sshStr(rawPubKey)]);
160
+
161
+ const checkBytes = randomBytes(4);
162
+ const privSection = Buffer.concat([
163
+ checkBytes,
164
+ checkBytes,
165
+ sshStr(keyType),
166
+ sshStr(rawPubKey),
167
+ sshStr(Buffer.concat([seed, rawPubKey])), // 64-byte private key: seed + pubkey
168
+ sshStr(Buffer.alloc(0)), // empty comment
169
+ ]);
170
+
171
+ // Pad to 8-byte block alignment (1, 2, 3, ...)
172
+ const padNeeded = (8 - (privSection.length % 8)) % 8;
173
+ const padding = Buffer.alloc(padNeeded);
174
+ for (let i = 0; i < padNeeded; i++) padding[i] = i + 1;
175
+
176
+ const blob = Buffer.concat([
177
+ Buffer.from('openssh-key-v1\0'),
178
+ sshStr(Buffer.from('none')), // cipher
179
+ sshStr(Buffer.from('none')), // kdf
180
+ sshStr(Buffer.alloc(0)), // kdf options
181
+ uint32(1), // number of keys
182
+ sshStr(pubBlob),
183
+ sshStr(Buffer.concat([privSection, padding])),
184
+ ]);
185
+
186
+ const b64 = blob.toString('base64');
187
+ const lines = b64.match(/.{1,70}/g) || [];
188
+ return `-----BEGIN OPENSSH PRIVATE KEY-----\n${lines.join('\n')}\n-----END OPENSSH PRIVATE KEY-----\n`;
189
+ };
190
+
191
+ /** Ensure a private key is in OpenSSH format (convert from PKCS8 if needed). */
192
+ private ensureOpenSshFormat = (privateKeyPem: string): string => {
193
+ if (privateKeyPem.includes('BEGIN OPENSSH PRIVATE KEY')) return privateKeyPem;
194
+ return this.pkcs8ToOpenSsh(privateKeyPem);
195
+ };
196
+
116
197
  /** Write an SSH private key to a temporary file with restricted permissions. Returns the file path. */
117
198
  writeTempKeyFile = async (privateKeyPem: string): Promise<string> => {
199
+ const opensshKey = this.ensureOpenSshFormat(privateKeyPem);
118
200
  const filename = `assistkick_ssh_${randomBytes(8).toString('hex')}`;
119
201
  const filepath = join(tmpdir(), filename);
120
202
 
121
- await writeFile(filepath, privateKeyPem, { mode: 0o600 });
203
+ await writeFile(filepath, opensshKey, { mode: 0o600 });
122
204
  await chmod(filepath, 0o600);
123
205
  this.log('SSH', `Temporary key file created: ${filepath}`);
124
206
  return filepath;
@@ -129,8 +211,8 @@ export class SshKeyService {
129
211
  try {
130
212
  await unlink(filepath);
131
213
  this.log('SSH', `Temporary key file removed: ${filepath}`);
132
- } catch {
133
- this.log('SSH', `Failed to remove temp key file (non-fatal): ${filepath}`);
214
+ } catch (err: any) {
215
+ this.log('SSH', `Failed to remove temp key file (non-fatal): ${filepath}`, err.message);
134
216
  }
135
217
  };
136
218
 
@@ -139,10 +221,63 @@ export class SshKeyService {
139
221
  return `ssh -i ${keyFilePath} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`;
140
222
  };
141
223
 
224
+ /**
225
+ * Install an SSH keypair to ~/.ssh/ so the CLI git can use the same key
226
+ * as the app. Writes the private key, public key, and ensures ~/.ssh/config
227
+ * routes github.com through the installed key.
228
+ */
229
+ installKeyForCli = async (keyPair: SshKeyPair): Promise<void> => {
230
+ const sshDir = join(homedir(), '.ssh');
231
+ const privPath = join(sshDir, 'ssh_id_ed25519');
232
+ const pubPath = join(sshDir, 'ssh_id_ed25519.pub');
233
+ const configPath = join(sshDir, 'config');
234
+
235
+ await mkdir(sshDir, { recursive: true, mode: 0o700 });
236
+
237
+ // Write private key in OpenSSH format
238
+ const opensshKey = this.ensureOpenSshFormat(keyPair.privateKey);
239
+ await writeFile(privPath, opensshKey, { mode: 0o600 });
240
+ await chmod(privPath, 0o600);
241
+
242
+ // Write public key with comment
243
+ await writeFile(pubPath, `${keyPair.publicKey} assistkick-deploy\n`, { mode: 0o644 });
244
+
245
+ // Ensure ~/.ssh/config has a github.com Host entry pointing to our key
246
+ const hostBlock = [
247
+ 'Host github.com',
248
+ ' HostName github.com',
249
+ ' User git',
250
+ ` IdentityFile ${privPath}`,
251
+ ' StrictHostKeyChecking no',
252
+ ].join('\n');
253
+
254
+ let existingConfig = '';
255
+ try {
256
+ existingConfig = await readFile(configPath, 'utf8');
257
+ } catch {
258
+ // No config file yet
259
+ }
260
+
261
+ if (existingConfig.includes('Host github.com')) {
262
+ // Replace existing github.com block (everything from "Host github.com" to next "Host " or EOF)
263
+ const replaced = existingConfig.replace(
264
+ /Host github\.com[\s\S]*?(?=\nHost |\s*$)/,
265
+ hostBlock,
266
+ );
267
+ await writeFile(configPath, replaced, { mode: 0o600 });
268
+ } else {
269
+ const newConfig = existingConfig.trim()
270
+ ? `${existingConfig.trimEnd()}\n\n${hostBlock}\n`
271
+ : `${hostBlock}\n`;
272
+ await writeFile(configPath, newConfig, { mode: 0o600 });
273
+ }
274
+
275
+ this.log('SSH', `Installed SSH key for CLI use at ${privPath}`);
276
+ };
277
+
142
278
  /** Check whether ENCRYPTION_KEY is configured. */
143
279
  isConfigured = (): boolean => {
144
280
  const key = process.env.ENCRYPTION_KEY;
145
- if (!key) return false;
146
- return Buffer.from(key, 'hex').length === 32;
281
+ return !!key && key.length >= 32;
147
282
  };
148
283
  }
@@ -128,7 +128,8 @@ export class TerminalWsHandler {
128
128
  let msg: TerminalMessage;
129
129
  try {
130
130
  msg = JSON.parse(typeof raw === 'string' ? raw : raw.toString());
131
- } catch {
131
+ } catch (err: any) {
132
+ this.log('TERMINAL', `Failed to parse WS message: ${err.message}`);
132
133
  return;
133
134
  }
134
135
 
@@ -0,0 +1,45 @@
1
+ import { describe, it, mock, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { TitleGeneratorService } from './title_generator_service.ts';
4
+
5
+ describe('TitleGeneratorService', () => {
6
+ let service: TitleGeneratorService;
7
+ const log = mock.fn();
8
+
9
+ beforeEach(() => {
10
+ log.mock.resetCalls();
11
+ service = new TitleGeneratorService({ log });
12
+ });
13
+
14
+ describe('isDefaultName', () => {
15
+ it('returns true for default timestamp names', () => {
16
+ assert.equal(service.isDefaultName('Chat 17/03/26 14:30'), true);
17
+ assert.equal(service.isDefaultName('Chat 01/01/26 00:00'), true);
18
+ assert.equal(service.isDefaultName('Chat 31/12/25 23:59'), true);
19
+ });
20
+
21
+ it('returns false for custom names', () => {
22
+ assert.equal(service.isDefaultName('My Custom Chat'), false);
23
+ assert.equal(service.isDefaultName('Debugging Auth Issue'), false);
24
+ assert.equal(service.isDefaultName('Chat about React'), false);
25
+ });
26
+
27
+ it('returns false for names that partially match the pattern', () => {
28
+ assert.equal(service.isDefaultName('Chat 17/03/26 14:30 extra'), false);
29
+ assert.equal(service.isDefaultName('Chat 17/03/26'), false);
30
+ assert.equal(service.isDefaultName('chat 17/03/26 14:30'), false);
31
+ });
32
+ });
33
+
34
+ describe('generateTitle', () => {
35
+ it('completes without hanging and returns a string or null', async () => {
36
+ // With stdio: ['ignore', ...] the CLI no longer blocks on piped stdin.
37
+ // It either returns a valid title or null if the CLI is unavailable.
38
+ const result = await service.generateTitle('hello', '[]');
39
+ assert.ok(
40
+ result === null || typeof result === 'string',
41
+ `Expected null or string, got ${typeof result}: ${result}`,
42
+ );
43
+ });
44
+ });
45
+ });
@@ -0,0 +1,157 @@
1
+ /**
2
+ * TitleGeneratorService — generates short chat session titles using Claude Haiku.
3
+ *
4
+ * Spawns the Claude CLI in one-shot mode with claude-haiku-4-5 to produce
5
+ * a 3-10 word title from the conversation's first user message and assistant reply.
6
+ * Runs fire-and-forget so it never blocks the main chat stream.
7
+ */
8
+
9
+ import { spawn } from 'node:child_process';
10
+
11
+ const DEFAULT_NAME_REGEX = /^Chat \d{2}\/\d{2}\/\d{2} \d{2}:\d{2}$/;
12
+
13
+ interface TitleGeneratorServiceDeps {
14
+ log: (tag: string, ...args: unknown[]) => void;
15
+ }
16
+
17
+ export class TitleGeneratorService {
18
+ private readonly log: TitleGeneratorServiceDeps['log'];
19
+
20
+ constructor({ log }: TitleGeneratorServiceDeps) {
21
+ this.log = log;
22
+ }
23
+
24
+ /**
25
+ * Check whether a session name is still the default timestamp-based name.
26
+ */
27
+ isDefaultName = (name: string): boolean => {
28
+ return DEFAULT_NAME_REGEX.test(name);
29
+ };
30
+
31
+ /**
32
+ * Generate a short title for a chat session based on conversation content.
33
+ * Returns the generated title (3-10 words), or null if generation fails.
34
+ */
35
+ generateTitle = async (userMessage: string, assistantContent: string): Promise<string | null> => {
36
+ const prompt = this.buildPrompt(userMessage, assistantContent);
37
+
38
+ try {
39
+ const title = await this.callCli(prompt);
40
+ if (!title) return null;
41
+
42
+ const cleaned = this.cleanTitle(title);
43
+ if (!cleaned) return null;
44
+
45
+ this.log('TITLE_GEN', `Generated title: "${cleaned}"`);
46
+ return cleaned;
47
+ } catch (err: unknown) {
48
+ const msg = err instanceof Error ? err.message : 'Unknown error';
49
+ this.log('TITLE_GEN', `Title generation failed: ${msg}`);
50
+ return null;
51
+ }
52
+ };
53
+
54
+ /**
55
+ * Build the prompt for title generation.
56
+ */
57
+ private buildPrompt = (userMessage: string, assistantContent: string): string => {
58
+ // Extract text from assistant content blocks
59
+ let assistantText = '';
60
+ try {
61
+ const blocks = JSON.parse(assistantContent) as Array<Record<string, unknown>>;
62
+ assistantText = blocks
63
+ .filter(b => b.type === 'text' && typeof b.text === 'string')
64
+ .map(b => b.text as string)
65
+ .join(' ');
66
+ } catch {
67
+ assistantText = '';
68
+ }
69
+
70
+ // Truncate to keep the prompt small
71
+ const maxLen = 500;
72
+ const truncUser = userMessage.length > maxLen ? userMessage.slice(0, maxLen) + '...' : userMessage;
73
+ const truncAssistant = assistantText.length > maxLen ? assistantText.slice(0, maxLen) + '...' : assistantText;
74
+
75
+ return [
76
+ 'Generate a short title (3-10 words) for this chat conversation.',
77
+ 'The title should summarize the main topic or intent.',
78
+ 'Reply with ONLY the title text, no quotes, no punctuation at the end, no explanation.',
79
+ '',
80
+ `User: ${truncUser}`,
81
+ '',
82
+ truncAssistant ? `Assistant: ${truncAssistant}` : '',
83
+ ].filter(Boolean).join('\n');
84
+ };
85
+
86
+ /**
87
+ * Call Claude CLI in one-shot mode with claude-haiku-4-5.
88
+ */
89
+ private callCli = (prompt: string): Promise<string | null> => {
90
+ return new Promise((resolve) => {
91
+ const args = [
92
+ '-p', prompt,
93
+ '--model', 'claude-haiku-4-5',
94
+ '--output-format', 'text',
95
+ '--max-turns', '1',
96
+ '--dangerously-skip-permissions',
97
+ ];
98
+
99
+ const child = spawn('claude', args, {
100
+ stdio: ['ignore', 'pipe', 'pipe'],
101
+ env: { ...process.env },
102
+ timeout: 15000,
103
+ });
104
+
105
+ let stdout = '';
106
+ let stderr = '';
107
+
108
+ child.stdout.on('data', (data: Buffer) => {
109
+ stdout += data.toString();
110
+ });
111
+
112
+ child.stderr.on('data', (data: Buffer) => {
113
+ stderr += data.toString();
114
+ });
115
+
116
+ child.on('close', (code) => {
117
+ if (code === 0 && stdout.trim()) {
118
+ resolve(stdout.trim());
119
+ } else {
120
+ if (stderr) {
121
+ this.log('TITLE_GEN', `CLI stderr: ${stderr.trim()}`);
122
+ }
123
+ resolve(null);
124
+ }
125
+ });
126
+
127
+ child.on('error', (err) => {
128
+ this.log('TITLE_GEN', `CLI spawn error: ${err.message}`);
129
+ resolve(null);
130
+ });
131
+ });
132
+ };
133
+
134
+ /**
135
+ * Clean and validate the generated title.
136
+ */
137
+ private cleanTitle = (raw: string): string | null => {
138
+ // Remove quotes, trailing punctuation
139
+ let title = raw
140
+ .replace(/^["']+|["']+$/g, '')
141
+ .replace(/[.!?]+$/, '')
142
+ .trim();
143
+
144
+ // Validate word count (3-10 words)
145
+ const words = title.split(/\s+/).filter(Boolean);
146
+ if (words.length < 2 || words.length > 15) {
147
+ // Be lenient but reject obviously bad titles
148
+ if (words.length === 0) return null;
149
+ // Truncate to 10 words if too long
150
+ if (words.length > 10) {
151
+ title = words.slice(0, 10).join(' ');
152
+ }
153
+ }
154
+
155
+ return title || null;
156
+ };
157
+ }
@@ -111,8 +111,8 @@ export class TtsService {
111
111
  reject(new Error(parsed.error));
112
112
  return;
113
113
  }
114
- } catch {
115
- // Not JSON, use the error message
114
+ } catch (parseErr: any) {
115
+ this.log('TTS', `Failed to parse error output as JSON: ${parseErr.message}`);
116
116
  }
117
117
  reject(new Error(`TTS process failed: ${error.message}`));
118
118
  return;
@@ -125,7 +125,8 @@ export class TtsService {
125
125
  return;
126
126
  }
127
127
  resolve(parsed as TtsResult);
128
- } catch {
128
+ } catch (parseErr: any) {
129
+ this.log('TTS', `Failed to parse TTS output: ${parseErr.message}`);
129
130
  reject(new Error(`Failed to parse TTS output: ${stdout}`));
130
131
  }
131
132
  });
@@ -101,7 +101,7 @@ export class VideoRenderService {
101
101
 
102
102
  // Start rendering asynchronously — don't await
103
103
  this.executeRender(id, request).catch((err) => {
104
- this.log('RENDER', `Render ${id} async error: ${err.message}`);
104
+ this.log('RENDER', `Render ${id} async error:`, err.stack || err.message);
105
105
  });
106
106
 
107
107
  return this.getStatus(id) as Promise<RenderStatus>;
@@ -166,7 +166,7 @@ export class VideoRenderService {
166
166
  unlinkSync(row.filePath);
167
167
  this.log('RENDER', `Deleted file for render ${renderId}: ${row.filePath}`);
168
168
  } catch (err: any) {
169
- this.log('RENDER', `Failed to delete file for render ${renderId}: ${err.message}`);
169
+ this.log('RENDER', `Failed to delete file for render ${renderId}:`, err.stack || err.message);
170
170
  }
171
171
  }
172
172
 
@@ -244,7 +244,7 @@ export class VideoRenderService {
244
244
 
245
245
  this.log('RENDER', `Render ${renderId} complete: ${outputPath} (${stat.size} bytes)`);
246
246
  } catch (err: any) {
247
- this.log('RENDER', `Render ${renderId} failed: ${err.message}`);
247
+ this.log('RENDER', `Render ${renderId} failed:`, err.stack || err.message);
248
248
 
249
249
  await db.update(videoRenders)
250
250
  .set({
@@ -21,9 +21,13 @@
21
21
  "d3": "^7.9.0",
22
22
  "lucide-react": "^0.577.0",
23
23
  "marked": "^15.0.0",
24
+ "mermaid": "^11.13.0",
24
25
  "react": "^19.1.0",
25
26
  "react-dom": "^19.1.0",
27
+ "react-markdown": "^10.1.0",
26
28
  "react-router-dom": "^7.6.0",
29
+ "react-syntax-highlighter": "^16.1.1",
30
+ "remark-gfm": "^4.0.1",
27
31
  "remotion": "^4.0.434",
28
32
  "zustand": "^5.0.11"
29
33
  },
@@ -32,6 +36,7 @@
32
36
  "@types/d3": "^7.4.3",
33
37
  "@types/react": "^19.1.0",
34
38
  "@types/react-dom": "^19.1.0",
39
+ "@types/react-syntax-highlighter": "^15.5.13",
35
40
  "@vitejs/plugin-react": "^4.5.2",
36
41
  "tailwindcss": "^4.2.1",
37
42
  "typescript": "^5.9.3",
@@ -17,6 +17,7 @@ import { AgentsRoute } from './routes/AgentsRoute';
17
17
  import { WorkflowsRoute } from './routes/WorkflowsRoute';
18
18
  import { FilesRoute } from './routes/FilesRoute';
19
19
  import { VideographyRoute } from './routes/VideographyRoute';
20
+ import { ChatRoute } from './routes/ChatRoute';
20
21
 
21
22
  export function App() {
22
23
  return (
@@ -34,6 +35,7 @@ export function App() {
34
35
  <Route path="/coherence" element={<CoherenceRoute />} />
35
36
  <Route path="/users" element={<UsersRoute />} />
36
37
  <Route path="/terminal" element={<TerminalRoute />} />
38
+ <Route path="/chat" element={<ChatRoute />} />
37
39
  <Route path="/agents" element={<AgentsRoute />} />
38
40
  <Route path="/workflows" element={<WorkflowsRoute />} />
39
41
  <Route path="/files" element={<FilesRoute />} />