@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.
- package/dist/src/scaffolder.d.ts +12 -1
- package/dist/src/scaffolder.js +40 -3
- package/dist/src/scaffolder.js.map +1 -1
- package/package.json +1 -1
- package/templates/assistkick-product-system/package.json +1 -1
- package/templates/assistkick-product-system/packages/backend/package.json +1 -0
- package/templates/assistkick-product-system/packages/backend/src/mcp/permission_mcp_server.ts +196 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/agents.ts +31 -7
- package/templates/assistkick-product-system/packages/backend/src/routes/auth.ts +15 -12
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.test.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_files.ts +97 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_permission.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_sessions.ts +189 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.test.ts +131 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/chat_upload.ts +94 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/files.test.ts +12 -3
- package/templates/assistkick-product-system/packages/backend/src/routes/files.ts +2 -2
- package/templates/assistkick-product-system/packages/backend/src/routes/git.ts +391 -23
- package/templates/assistkick-product-system/packages/backend/src/routes/git_branches.test.ts +306 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/git_connect.test.ts +133 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/pipeline.ts +66 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/preview.ts +204 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.test.ts +205 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/projects.ts +37 -9
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.test.ts +139 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/skills.ts +95 -0
- package/templates/assistkick-product-system/packages/backend/src/routes/terminal.ts +5 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/users.ts +4 -4
- package/templates/assistkick-product-system/packages/backend/src/routes/video.ts +8 -8
- package/templates/assistkick-product-system/packages/backend/src/routes/workflow_groups.ts +5 -5
- package/templates/assistkick-product-system/packages/backend/src/routes/workflows.ts +6 -6
- package/templates/assistkick-product-system/packages/backend/src/server.ts +107 -27
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.test.ts +105 -203
- package/templates/assistkick-product-system/packages/backend/src/services/agent_service.ts +76 -266
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.test.ts +427 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_cli_bridge.ts +345 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.test.ts +170 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_message_repository.ts +106 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.test.ts +217 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_session_service.ts +188 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.test.ts +1243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/chat_ws_handler.ts +894 -0
- package/templates/assistkick-product-system/packages/backend/src/services/coherence-review.ts +3 -3
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.test.ts +85 -0
- package/templates/assistkick-product-system/packages/backend/src/services/dev_command_detector.ts +54 -0
- package/templates/assistkick-product-system/packages/backend/src/services/email_service.ts +13 -10
- package/templates/assistkick-product-system/packages/backend/src/services/init.ts +11 -3
- package/templates/assistkick-product-system/packages/backend/src/services/invitation_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/password_reset_service.ts +1 -1
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.test.ts +243 -0
- package/templates/assistkick-product-system/packages/backend/src/services/permission_service.ts +259 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.test.ts +172 -0
- package/templates/assistkick-product-system/packages/backend/src/services/preview_server_manager.ts +225 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.test.ts +29 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_service.ts +17 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.test.ts +255 -0
- package/templates/assistkick-product-system/packages/backend/src/services/project_workspace_service.ts +300 -25
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.test.ts +44 -0
- package/templates/assistkick-product-system/packages/backend/src/services/pty_session_manager.ts +62 -7
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts +77 -6
- package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts +149 -14
- package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts +2 -1
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.test.ts +45 -0
- package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts +157 -0
- package/templates/assistkick-product-system/packages/backend/src/services/tts_service.ts +4 -3
- package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts +3 -3
- package/templates/assistkick-product-system/packages/frontend/package.json +5 -0
- package/templates/assistkick-product-system/packages/frontend/src/App.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/api/client.ts +336 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/AgentsView.tsx +192 -12
- package/templates/assistkick-product-system/packages/frontend/src/components/AttachmentPreviewList.tsx +98 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/AutocompleteDropdown.tsx +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatAttachButton.tsx +56 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatDropZone.tsx +80 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageBubble.tsx +155 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageContent.tsx +182 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatMessageInput.tsx +233 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatSessionSidebar.tsx +218 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatStopButton.tsx +32 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatTodoSidebar.tsx +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ChatView.tsx +842 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/CommitMessageModal.tsx +82 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/DiagramOverlay.tsx +160 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/EditorTabBar.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTree.tsx +9 -10
- package/templates/assistkick-product-system/packages/frontend/src/components/FileTreeInlineInput.tsx +5 -5
- package/templates/assistkick-product-system/packages/frontend/src/components/FilesView.tsx +112 -41
- package/templates/assistkick-product-system/packages/frontend/src/components/GraphLegend.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/HighlightedText.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ImageLightbox.tsx +192 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/KanbanView.tsx +2 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/MentionPill.tsx +33 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/MermaidBlock.tsx +148 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionDialog.tsx +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/PermissionModeSelector.tsx +229 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ProjectSelector.tsx +249 -83
- package/templates/assistkick-product-system/packages/frontend/src/components/QueuedMessageBubble.tsx +38 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/SidePanel.tsx +212 -117
- package/templates/assistkick-product-system/packages/frontend/src/components/SystemPromptAccordion.tsx +48 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TaskIcon.tsx +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/TerminalView.tsx +25 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolDiffView.tsx +114 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolResultCard.tsx +87 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/ToolUseCard.tsx +149 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/Toolbar.tsx +25 -8
- package/templates/assistkick-product-system/packages/frontend/src/components/UnifiedGitWidget.tsx +722 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/GroupNode.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/NodePalette.tsx +2 -1
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/ProgrammableNode.tsx +178 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowCanvas.tsx +3 -0
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/WorkflowMonitorModal.tsx +103 -9
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/monitor_nodes.tsx +26 -2
- package/templates/assistkick-product-system/packages/frontend/src/components/workflow/workflow_types.ts +42 -1
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useDocumentTitle.ts +11 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/useProjects.ts +1 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_chat_stream.ts +826 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_file_tree_cache.ts +69 -0
- package/templates/assistkick-product-system/packages/frontend/src/hooks/use_mention_autocomplete.ts +284 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.test.ts +183 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/attachment_manager.ts +150 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.test.ts +305 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/chat_message_helpers.ts +113 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.test.ts +157 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/context_usage_helpers.ts +95 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.test.ts +65 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/mermaid_helpers.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/message_queue.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.test.ts +124 -0
- package/templates/assistkick-product-system/packages/frontend/src/lib/tool_use_summary.ts +112 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/AgentsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/ChatRoute.tsx +8 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/CoherenceRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/DashboardLayout.tsx +0 -4
- package/templates/assistkick-product-system/packages/frontend/src/routes/DesignSystemRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/FilesRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/GraphRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/KanbanRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/TerminalRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/UsersRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/VideographyRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/WorkflowsRoute.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/accept_invitation.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/forgot_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/login.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/register.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/routes/reset_password.tsx +2 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useAttachmentStore.ts +66 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useChatSessionStore.ts +107 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useMessageQueueStore.ts +110 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/usePreviewStore.ts +78 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useProjectStore.ts +7 -0
- package/templates/assistkick-product-system/packages/frontend/src/stores/useSidePanelStore.ts +6 -1
- package/templates/assistkick-product-system/packages/frontend/src/styles/index.css +30 -357
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.test.ts +115 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/parse_node_markdown.ts +91 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.test.ts +30 -0
- package/templates/assistkick-product-system/packages/frontend/src/utils/preview_utils.ts +3 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0015_magenta_jazinda.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0016_giant_xorn.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0017_sloppy_mentor.sql +6 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0018_vengeful_kabuki.sql +9 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0019_careful_sentinels.sql +8 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0020_clever_spot.sql +27 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0021_graceful_hex.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0022_short_kingpin.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0023_ambiguous_sharon_carter.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/0024_fat_unus.sql +1 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0015_snapshot.json +1552 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0016_snapshot.json +1560 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0017_snapshot.json +1598 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0018_snapshot.json +1657 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0019_snapshot.json +1709 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0020_snapshot.json +1733 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0021_snapshot.json +1740 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0022_snapshot.json +1755 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0023_snapshot.json +1762 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/0024_snapshot.json +1769 -0
- package/templates/assistkick-product-system/packages/shared/db/migrations/meta/_journal.json +70 -0
- package/templates/assistkick-product-system/packages/shared/db/schema.ts +40 -1
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.test.ts +236 -0
- package/templates/assistkick-product-system/packages/shared/lib/claude-service.ts +46 -5
- package/templates/assistkick-product-system/packages/shared/lib/git_workflow.ts +65 -39
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.test.ts +173 -0
- package/templates/assistkick-product-system/packages/shared/lib/programmable_node_executor.ts +213 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.test.ts +70 -0
- package/templates/assistkick-product-system/packages/shared/lib/validator.ts +17 -1
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.test.ts +803 -27
- package/templates/assistkick-product-system/packages/shared/lib/workflow_engine.ts +502 -68
- package/templates/assistkick-product-system/packages/shared/lib/workflow_orchestrator.ts +4 -4
- package/templates/assistkick-product-system/packages/shared/package.json +2 -1
- package/templates/assistkick-product-system/packages/shared/test_fixtures/hanging_stream.mjs +46 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.test.ts +44 -0
- package/templates/assistkick-product-system/packages/shared/tools/add_node.ts +7 -0
- package/templates/assistkick-product-system/packages/shared/tools/remove_node.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/resolve_question.ts +2 -1
- package/templates/assistkick-product-system/packages/shared/tools/update_node.ts +2 -1
- package/templates/assistkick-product-system/tests/message_queue.test.ts +178 -0
- package/templates/assistkick-product-system/tests/message_queue_per_session.test.ts +143 -0
- package/templates/skills/assistkick-bootstrap/SKILL.md +26 -26
- package/templates/skills/assistkick-code-reviewer/SKILL.md +45 -46
- package/templates/skills/assistkick-db-explorer/SKILL.md +13 -13
- package/templates/skills/assistkick-debugger/SKILL.md +23 -23
- package/templates/skills/assistkick-developer/SKILL.md +59 -63
- package/templates/skills/assistkick-interview/SKILL.md +26 -26
- package/templates/skills/assistkick-video-composition-agent/SKILL.md +231 -0
- package/templates/skills/assistkick-video-script-writer/SKILL.md +136 -0
package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.test.ts
CHANGED
|
@@ -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'), /
|
|
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
|
|
113
|
+
it('writes a temp file converting PKCS8 to OpenSSH format', async () => {
|
|
112
114
|
const service = createService();
|
|
113
|
-
const
|
|
115
|
+
const keyPair = service.generateKeyPair();
|
|
114
116
|
|
|
115
|
-
const filepath = await service.writeTempKeyFile(
|
|
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.
|
|
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();
|
package/templates/assistkick-product-system/packages/backend/src/services/ssh_key_service.ts
CHANGED
|
@@ -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)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
throw new Error(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
146
|
-
return Buffer.from(key, 'hex').length === 32;
|
|
281
|
+
return !!key && key.length >= 32;
|
|
147
282
|
};
|
|
148
283
|
}
|
package/templates/assistkick-product-system/packages/backend/src/services/terminal_ws_handler.ts
CHANGED
|
@@ -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
|
+
});
|
package/templates/assistkick-product-system/packages/backend/src/services/title_generator_service.ts
ADDED
|
@@ -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
|
-
|
|
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
|
});
|
package/templates/assistkick-product-system/packages/backend/src/services/video_render_service.ts
CHANGED
|
@@ -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
|
|
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}
|
|
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
|
|
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 />} />
|