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