memory-journal-mcp 4.4.2 → 5.0.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/.github/workflows/codeql.yml +1 -6
- package/.github/workflows/docker-publish.yml +15 -49
- package/.github/workflows/lint-and-test.yml +1 -1
- package/.github/workflows/secrets-scanning.yml +4 -3
- package/.github/workflows/security-update.yml +3 -3
- package/CHANGELOG.md +213 -0
- package/CONTRIBUTING.md +132 -97
- package/DOCKER_README.md +184 -235
- package/Dockerfile +27 -24
- package/README.md +218 -190
- package/SECURITY.md +27 -35
- package/dist/cli.js +16 -1
- package/dist/cli.js.map +1 -1
- package/dist/constants/ServerInstructions.d.ts +5 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +133 -73
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/constants/icons.d.ts +2 -2
- package/dist/constants/icons.d.ts.map +1 -1
- package/dist/constants/icons.js +7 -6
- package/dist/constants/icons.js.map +1 -1
- package/dist/database/SqliteAdapter.d.ts +37 -24
- package/dist/database/SqliteAdapter.d.ts.map +1 -1
- package/dist/database/SqliteAdapter.js +319 -157
- package/dist/database/SqliteAdapter.js.map +1 -1
- package/dist/database/schema.d.ts +45 -0
- package/dist/database/schema.d.ts.map +1 -0
- package/dist/database/schema.js +92 -0
- package/dist/database/schema.js.map +1 -0
- package/dist/filtering/ToolFilter.d.ts +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +13 -2
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/github/GitHubIntegration.d.ts.map +1 -1
- package/dist/github/GitHubIntegration.js +1 -3
- package/dist/github/GitHubIntegration.js.map +1 -1
- package/dist/handlers/prompts/github.d.ts +12 -0
- package/dist/handlers/prompts/github.d.ts.map +1 -0
- package/dist/handlers/prompts/github.js +178 -0
- package/dist/handlers/prompts/github.js.map +1 -0
- package/dist/handlers/prompts/index.d.ts +23 -2
- package/dist/handlers/prompts/index.d.ts.map +1 -1
- package/dist/handlers/prompts/index.js +7 -432
- package/dist/handlers/prompts/index.js.map +1 -1
- package/dist/handlers/prompts/workflow.d.ts +12 -0
- package/dist/handlers/prompts/workflow.d.ts.map +1 -0
- package/dist/handlers/prompts/workflow.js +277 -0
- package/dist/handlers/prompts/workflow.js.map +1 -0
- package/dist/handlers/resources/core.d.ts +11 -0
- package/dist/handlers/resources/core.d.ts.map +1 -0
- package/dist/handlers/resources/core.js +433 -0
- package/dist/handlers/resources/core.js.map +1 -0
- package/dist/handlers/resources/github.d.ts +11 -0
- package/dist/handlers/resources/github.d.ts.map +1 -0
- package/dist/handlers/resources/github.js +314 -0
- package/dist/handlers/resources/github.js.map +1 -0
- package/dist/handlers/resources/graph.d.ts +11 -0
- package/dist/handlers/resources/graph.d.ts.map +1 -0
- package/dist/handlers/resources/graph.js +204 -0
- package/dist/handlers/resources/graph.js.map +1 -0
- package/dist/handlers/resources/index.d.ts +5 -20
- package/dist/handlers/resources/index.d.ts.map +1 -1
- package/dist/handlers/resources/index.js +16 -1278
- package/dist/handlers/resources/index.js.map +1 -1
- package/dist/handlers/resources/shared.d.ts +60 -0
- package/dist/handlers/resources/shared.d.ts.map +1 -0
- package/dist/handlers/resources/shared.js +49 -0
- package/dist/handlers/resources/shared.js.map +1 -0
- package/dist/handlers/resources/team.d.ts +13 -0
- package/dist/handlers/resources/team.d.ts.map +1 -0
- package/dist/handlers/resources/team.js +119 -0
- package/dist/handlers/resources/team.js.map +1 -0
- package/dist/handlers/resources/templates.d.ts +13 -0
- package/dist/handlers/resources/templates.d.ts.map +1 -0
- package/dist/handlers/resources/templates.js +310 -0
- package/dist/handlers/resources/templates.js.map +1 -0
- package/dist/handlers/tools/admin.d.ts +8 -0
- package/dist/handlers/tools/admin.d.ts.map +1 -0
- package/dist/handlers/tools/admin.js +270 -0
- package/dist/handlers/tools/admin.js.map +1 -0
- package/dist/handlers/tools/analytics.d.ts +8 -0
- package/dist/handlers/tools/analytics.d.ts.map +1 -0
- package/dist/handlers/tools/analytics.js +256 -0
- package/dist/handlers/tools/analytics.js.map +1 -0
- package/dist/handlers/tools/backup.d.ts +8 -0
- package/dist/handlers/tools/backup.d.ts.map +1 -0
- package/dist/handlers/tools/backup.js +224 -0
- package/dist/handlers/tools/backup.js.map +1 -0
- package/dist/handlers/tools/core.d.ts +9 -0
- package/dist/handlers/tools/core.d.ts.map +1 -0
- package/dist/handlers/tools/core.js +326 -0
- package/dist/handlers/tools/core.js.map +1 -0
- package/dist/handlers/tools/export.d.ts +8 -0
- package/dist/handlers/tools/export.d.ts.map +1 -0
- package/dist/handlers/tools/export.js +89 -0
- package/dist/handlers/tools/export.js.map +1 -0
- package/dist/handlers/tools/github/helpers.d.ts +34 -0
- package/dist/handlers/tools/github/helpers.d.ts.map +1 -0
- package/dist/handlers/tools/github/helpers.js +52 -0
- package/dist/handlers/tools/github/helpers.js.map +1 -0
- package/dist/handlers/tools/github/insights-tools.d.ts +8 -0
- package/dist/handlers/tools/github/insights-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/insights-tools.js +104 -0
- package/dist/handlers/tools/github/insights-tools.js.map +1 -0
- package/dist/handlers/tools/github/issue-tools.d.ts +8 -0
- package/dist/handlers/tools/github/issue-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/issue-tools.js +359 -0
- package/dist/handlers/tools/github/issue-tools.js.map +1 -0
- package/dist/handlers/tools/github/kanban-tools.d.ts +8 -0
- package/dist/handlers/tools/github/kanban-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/kanban-tools.js +108 -0
- package/dist/handlers/tools/github/kanban-tools.js.map +1 -0
- package/dist/handlers/tools/github/milestone-tools.d.ts +9 -0
- package/dist/handlers/tools/github/milestone-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/milestone-tools.js +302 -0
- package/dist/handlers/tools/github/milestone-tools.js.map +1 -0
- package/dist/handlers/tools/github/mutation-tools.d.ts +12 -0
- package/dist/handlers/tools/github/mutation-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/mutation-tools.js +15 -0
- package/dist/handlers/tools/github/mutation-tools.js.map +1 -0
- package/dist/handlers/tools/github/read-tools.d.ts +8 -0
- package/dist/handlers/tools/github/read-tools.d.ts.map +1 -0
- package/dist/handlers/tools/github/read-tools.js +260 -0
- package/dist/handlers/tools/github/read-tools.js.map +1 -0
- package/dist/handlers/tools/github/schemas.d.ts +467 -0
- package/dist/handlers/tools/github/schemas.d.ts.map +1 -0
- package/dist/handlers/tools/github/schemas.js +335 -0
- package/dist/handlers/tools/github/schemas.js.map +1 -0
- package/dist/handlers/tools/github.d.ts +14 -0
- package/dist/handlers/tools/github.d.ts.map +1 -0
- package/dist/handlers/tools/github.js +28 -0
- package/dist/handlers/tools/github.js.map +1 -0
- package/dist/handlers/tools/index.d.ts +15 -20
- package/dist/handlers/tools/index.d.ts.map +1 -1
- package/dist/handlers/tools/index.js +117 -2909
- package/dist/handlers/tools/index.js.map +1 -1
- package/dist/handlers/tools/relationships.d.ts +8 -0
- package/dist/handlers/tools/relationships.d.ts.map +1 -0
- package/dist/handlers/tools/relationships.js +308 -0
- package/dist/handlers/tools/relationships.js.map +1 -0
- package/dist/handlers/tools/schemas.d.ts +108 -0
- package/dist/handlers/tools/schemas.d.ts.map +1 -0
- package/dist/handlers/tools/schemas.js +122 -0
- package/dist/handlers/tools/schemas.js.map +1 -0
- package/dist/handlers/tools/search.d.ts +8 -0
- package/dist/handlers/tools/search.d.ts.map +1 -0
- package/dist/handlers/tools/search.js +282 -0
- package/dist/handlers/tools/search.js.map +1 -0
- package/dist/handlers/tools/team.d.ts +11 -0
- package/dist/handlers/tools/team.d.ts.map +1 -0
- package/dist/handlers/tools/team.js +239 -0
- package/dist/handlers/tools/team.js.map +1 -0
- package/dist/server/McpServer.d.ts +4 -0
- package/dist/server/McpServer.d.ts.map +1 -1
- package/dist/server/McpServer.js +48 -297
- package/dist/server/McpServer.js.map +1 -1
- package/dist/server/Scheduler.d.ts +91 -0
- package/dist/server/Scheduler.d.ts.map +1 -0
- package/dist/server/Scheduler.js +201 -0
- package/dist/server/Scheduler.js.map +1 -0
- package/dist/transports/http.d.ts +66 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +519 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/types/entities.d.ts +101 -0
- package/dist/types/entities.d.ts.map +1 -0
- package/dist/types/entities.js +5 -0
- package/dist/types/entities.js.map +1 -0
- package/dist/types/filtering.d.ts +34 -0
- package/dist/types/filtering.d.ts.map +1 -0
- package/dist/types/filtering.js +5 -0
- package/dist/types/filtering.js.map +1 -0
- package/dist/types/github.d.ts +166 -0
- package/dist/types/github.d.ts.map +1 -0
- package/dist/types/github.js +5 -0
- package/dist/types/github.js.map +1 -0
- package/dist/types/index.d.ts +35 -292
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -2
- package/dist/types/index.js.map +1 -1
- package/dist/utils/error-helpers.d.ts +37 -0
- package/dist/utils/error-helpers.d.ts.map +1 -0
- package/dist/utils/error-helpers.js +47 -0
- package/dist/utils/error-helpers.js.map +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +6 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/security-utils.d.ts +0 -21
- package/dist/utils/security-utils.d.ts.map +1 -1
- package/dist/utils/security-utils.js +0 -47
- package/dist/utils/security-utils.js.map +1 -1
- package/dist/vector/VectorSearchManager.d.ts.map +1 -1
- package/dist/vector/VectorSearchManager.js +9 -32
- package/dist/vector/VectorSearchManager.js.map +1 -1
- package/docker-compose.yml +11 -2
- package/hooks/README.md +107 -0
- package/hooks/cursor/hooks.json +10 -0
- package/hooks/cursor/memory-journal.mdc +22 -0
- package/hooks/cursor/session-end.sh +19 -0
- package/hooks/kilo-code/session-end-mode.json +11 -0
- package/hooks/kiro/session-end.md +13 -0
- package/mcp-config-example.json +1 -0
- package/package.json +11 -9
- package/playwright.config.ts +29 -0
- package/releases/v4.5.0.md +116 -0
- package/releases/v5.0.0.md +105 -0
- package/scripts/generate-server-instructions.ts +176 -0
- package/scripts/server-instructions-function-body.ts +77 -0
- package/server.json +3 -3
- package/src/cli.ts +45 -1
- package/src/constants/ServerInstructions.ts +133 -73
- package/src/constants/icons.ts +8 -7
- package/src/constants/server-instructions.md +268 -0
- package/src/database/SqliteAdapter.ts +358 -192
- package/src/database/schema.ts +125 -0
- package/src/filtering/ToolFilter.ts +13 -2
- package/src/github/GitHubIntegration.ts +1 -3
- package/src/handlers/prompts/github.ts +209 -0
- package/src/handlers/prompts/index.ts +10 -499
- package/src/handlers/prompts/workflow.ts +314 -0
- package/src/handlers/resources/core.ts +528 -0
- package/src/handlers/resources/github.ts +358 -0
- package/src/handlers/resources/graph.ts +254 -0
- package/src/handlers/resources/index.ts +23 -1570
- package/src/handlers/resources/shared.ts +103 -0
- package/src/handlers/resources/team.ts +133 -0
- package/src/handlers/resources/templates.ts +374 -0
- package/src/handlers/tools/admin.ts +285 -0
- package/src/handlers/tools/analytics.ts +301 -0
- package/src/handlers/tools/backup.ts +242 -0
- package/src/handlers/tools/core.ts +350 -0
- package/src/handlers/tools/export.ts +115 -0
- package/src/handlers/tools/github/helpers.ts +86 -0
- package/src/handlers/tools/github/insights-tools.ts +119 -0
- package/src/handlers/tools/github/issue-tools.ts +439 -0
- package/src/handlers/tools/github/kanban-tools.ts +134 -0
- package/src/handlers/tools/github/milestone-tools.ts +392 -0
- package/src/handlers/tools/github/mutation-tools.ts +17 -0
- package/src/handlers/tools/github/read-tools.ts +328 -0
- package/src/handlers/tools/github/schemas.ts +369 -0
- package/src/handlers/tools/github.ts +36 -0
- package/src/handlers/tools/index.ts +144 -3325
- package/src/handlers/tools/relationships.ts +358 -0
- package/src/handlers/tools/schemas.ts +132 -0
- package/src/handlers/tools/search.ts +343 -0
- package/src/handlers/tools/team.ts +273 -0
- package/src/server/McpServer.ts +63 -358
- package/src/server/Scheduler.ts +278 -0
- package/src/transports/http.ts +635 -0
- package/src/types/entities.ts +145 -0
- package/src/types/filtering.ts +54 -0
- package/src/types/github.ts +180 -0
- package/src/types/index.ts +67 -375
- package/src/utils/error-helpers.ts +52 -0
- package/src/utils/logger.ts +6 -3
- package/src/utils/security-utils.ts +0 -52
- package/src/vector/VectorSearchManager.ts +9 -33
- package/tests/constants/icons.test.ts +1 -2
- package/tests/constants/server-instructions.test.ts +30 -4
- package/tests/database/sqlite-adapter.test.ts +91 -7
- package/tests/e2e/auth.spec.ts +154 -0
- package/tests/e2e/health.spec.ts +63 -0
- package/tests/e2e/protocols.spec.ts +134 -0
- package/tests/e2e/resources.spec.ts +103 -0
- package/tests/e2e/scheduler.spec.ts +79 -0
- package/tests/e2e/security.spec.ts +91 -0
- package/tests/e2e/sessions.spec.ts +95 -0
- package/tests/e2e/stateless.spec.ts +121 -0
- package/tests/e2e/tools.spec.ts +111 -0
- package/tests/filtering/tool-filter.test.ts +46 -0
- package/tests/handlers/error-path-coverage.test.ts +324 -0
- package/tests/handlers/github-resource-handlers.test.ts +453 -0
- package/tests/handlers/github-tool-handlers.test.ts +899 -0
- package/tests/handlers/prompt-handler-coverage.test.ts +106 -0
- package/tests/handlers/prompt-handlers.test.ts +40 -0
- package/tests/handlers/resource-handler-coverage.test.ts +181 -0
- package/tests/handlers/resource-handlers.test.ts +33 -9
- package/tests/handlers/search-tool-handlers.test.ts +272 -0
- package/tests/handlers/targeted-gap-closure.test.ts +387 -0
- package/tests/handlers/team-resource-handlers.test.ts +156 -0
- package/tests/handlers/team-tool-handlers.test.ts +301 -0
- package/tests/handlers/tool-handler-coverage.test.ts +469 -0
- package/tests/handlers/tool-handlers.test.ts +2 -2
- package/tests/security/sql-injection.test.ts +3 -54
- package/tests/server/mcp-server.test.ts +503 -8
- package/tests/server/scheduler.test.ts +400 -0
- package/tests/transports/http-transport.test.ts +620 -0
- package/tests/vector/vector-search-manager.test.ts +60 -0
- package/vitest.config.ts +4 -1
- package/.memory-journal-team.db +0 -0
- package/.vscode/settings.json +0 -84
|
@@ -9,7 +9,11 @@ import initSqlJs, { type Database } from 'sql.js'
|
|
|
9
9
|
import * as fs from 'node:fs'
|
|
10
10
|
import * as path from 'node:path'
|
|
11
11
|
import { logger } from '../utils/logger.js'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
validateDateFormatPattern,
|
|
14
|
+
sanitizeSearchQuery,
|
|
15
|
+
assertNoPathTraversal,
|
|
16
|
+
} from '../utils/security-utils.js'
|
|
13
17
|
import type {
|
|
14
18
|
JournalEntry,
|
|
15
19
|
Tag,
|
|
@@ -20,104 +24,9 @@ import type {
|
|
|
20
24
|
ImportanceBreakdown,
|
|
21
25
|
ImportanceResult,
|
|
22
26
|
} from '../types/index.js'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-- Main journal entries table
|
|
27
|
-
CREATE TABLE IF NOT EXISTS memory_journal (
|
|
28
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
29
|
-
entry_type TEXT NOT NULL,
|
|
30
|
-
content TEXT NOT NULL,
|
|
31
|
-
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
32
|
-
is_personal INTEGER DEFAULT 1,
|
|
33
|
-
significance_type TEXT,
|
|
34
|
-
auto_context TEXT,
|
|
35
|
-
deleted_at TEXT,
|
|
36
|
-
-- GitHub integration fields
|
|
37
|
-
project_number INTEGER,
|
|
38
|
-
project_owner TEXT,
|
|
39
|
-
issue_number INTEGER,
|
|
40
|
-
issue_url TEXT,
|
|
41
|
-
pr_number INTEGER,
|
|
42
|
-
pr_url TEXT,
|
|
43
|
-
pr_status TEXT,
|
|
44
|
-
workflow_run_id INTEGER,
|
|
45
|
-
workflow_name TEXT,
|
|
46
|
-
workflow_status TEXT
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
-- Tags table
|
|
50
|
-
CREATE TABLE IF NOT EXISTS tags (
|
|
51
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
-
name TEXT UNIQUE NOT NULL,
|
|
53
|
-
usage_count INTEGER DEFAULT 0
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
-- Junction table for entry-tag relationships
|
|
57
|
-
CREATE TABLE IF NOT EXISTS entry_tags (
|
|
58
|
-
entry_id INTEGER NOT NULL,
|
|
59
|
-
tag_id INTEGER NOT NULL,
|
|
60
|
-
PRIMARY KEY (entry_id, tag_id),
|
|
61
|
-
FOREIGN KEY (entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE,
|
|
62
|
-
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
-- Relationships between entries
|
|
66
|
-
CREATE TABLE IF NOT EXISTS relationships (
|
|
67
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
-
from_entry_id INTEGER NOT NULL,
|
|
69
|
-
to_entry_id INTEGER NOT NULL,
|
|
70
|
-
relationship_type TEXT NOT NULL,
|
|
71
|
-
description TEXT,
|
|
72
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
73
|
-
FOREIGN KEY (from_entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE,
|
|
74
|
-
FOREIGN KEY (to_entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
-- Embeddings for vector search (stored as JSON for sql.js compatibility)
|
|
78
|
-
CREATE TABLE IF NOT EXISTS embeddings (
|
|
79
|
-
entry_id INTEGER PRIMARY KEY,
|
|
80
|
-
embedding TEXT NOT NULL,
|
|
81
|
-
model_name TEXT NOT NULL,
|
|
82
|
-
FOREIGN KEY (entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
-- Indexes for performance
|
|
86
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_timestamp ON memory_journal(timestamp);
|
|
87
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_type ON memory_journal(entry_type);
|
|
88
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_personal ON memory_journal(is_personal);
|
|
89
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_deleted ON memory_journal(deleted_at);
|
|
90
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_project ON memory_journal(project_number);
|
|
91
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_issue ON memory_journal(issue_number);
|
|
92
|
-
CREATE INDEX IF NOT EXISTS idx_memory_journal_pr ON memory_journal(pr_number);
|
|
93
|
-
CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name);
|
|
94
|
-
CREATE INDEX IF NOT EXISTS idx_entry_tags_entry ON entry_tags(entry_id);
|
|
95
|
-
CREATE INDEX IF NOT EXISTS idx_entry_tags_tag ON entry_tags(tag_id);
|
|
96
|
-
CREATE INDEX IF NOT EXISTS idx_relationships_from ON relationships(from_entry_id);
|
|
97
|
-
CREATE INDEX IF NOT EXISTS idx_relationships_to ON relationships(to_entry_id);
|
|
98
|
-
`
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Input for creating a new entry
|
|
102
|
-
*/
|
|
103
|
-
export interface CreateEntryInput {
|
|
104
|
-
content: string
|
|
105
|
-
entryType?: EntryType
|
|
106
|
-
tags?: string[]
|
|
107
|
-
isPersonal?: boolean
|
|
108
|
-
significanceType?: SignificanceType
|
|
109
|
-
autoContext?: string
|
|
110
|
-
projectNumber?: number
|
|
111
|
-
projectOwner?: string
|
|
112
|
-
issueNumber?: number
|
|
113
|
-
issueUrl?: string
|
|
114
|
-
prNumber?: number
|
|
115
|
-
prUrl?: string
|
|
116
|
-
prStatus?: 'draft' | 'open' | 'merged' | 'closed'
|
|
117
|
-
workflowRunId?: number
|
|
118
|
-
workflowName?: string
|
|
119
|
-
workflowStatus?: 'queued' | 'in_progress' | 'completed'
|
|
120
|
-
}
|
|
27
|
+
import { SCHEMA_SQL, TEAM_SCHEMA_SQL } from './schema.js'
|
|
28
|
+
export type { CreateEntryInput } from './schema.js'
|
|
29
|
+
import type { CreateEntryInput } from './schema.js'
|
|
121
30
|
|
|
122
31
|
/**
|
|
123
32
|
* SQLite Database Adapter for Memory Journal using sql.js
|
|
@@ -168,6 +77,14 @@ export class SqliteAdapter {
|
|
|
168
77
|
|
|
169
78
|
// Initialize schema
|
|
170
79
|
this.db.run(SCHEMA_SQL)
|
|
80
|
+
|
|
81
|
+
// Migrate existing databases that may lack newer columns
|
|
82
|
+
this.migrateSchema()
|
|
83
|
+
|
|
84
|
+
// Enable foreign key enforcement (SQLite disables by default)
|
|
85
|
+
// Required for ON DELETE CASCADE in entry_tags, relationships, embeddings
|
|
86
|
+
this.db.run('PRAGMA foreign_keys = ON')
|
|
87
|
+
|
|
171
88
|
this.initialized = true
|
|
172
89
|
|
|
173
90
|
logger.info('Database opened', { module: 'SqliteAdapter', dbPath: this.dbPath })
|
|
@@ -176,6 +93,176 @@ export class SqliteAdapter {
|
|
|
176
93
|
this.flushSave()
|
|
177
94
|
}
|
|
178
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Migrate existing databases that may lack newer columns.
|
|
98
|
+
* Required because CREATE TABLE IF NOT EXISTS is a no-op on
|
|
99
|
+
* existing tables — columns added after initial creation are
|
|
100
|
+
* never added. This method checks for each expected column and
|
|
101
|
+
* runs ALTER TABLE as needed.
|
|
102
|
+
* Idempotent — safe to call on databases that already have all columns.
|
|
103
|
+
*/
|
|
104
|
+
private migrateSchema(): void {
|
|
105
|
+
const db = this.ensureDb()
|
|
106
|
+
const tableInfo = db.exec('PRAGMA table_info(memory_journal)')
|
|
107
|
+
const columns = new Set((tableInfo[0]?.values ?? []).map((row) => String(row[1])))
|
|
108
|
+
|
|
109
|
+
const requiredColumns: { name: string; sql: string }[] = [
|
|
110
|
+
{
|
|
111
|
+
name: 'significance_type',
|
|
112
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN significance_type TEXT',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'auto_context',
|
|
116
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN auto_context TEXT',
|
|
117
|
+
},
|
|
118
|
+
{ name: 'deleted_at', sql: 'ALTER TABLE memory_journal ADD COLUMN deleted_at TEXT' },
|
|
119
|
+
{
|
|
120
|
+
name: 'project_number',
|
|
121
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN project_number INTEGER',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'project_owner',
|
|
125
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN project_owner TEXT',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'issue_number',
|
|
129
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN issue_number INTEGER',
|
|
130
|
+
},
|
|
131
|
+
{ name: 'issue_url', sql: 'ALTER TABLE memory_journal ADD COLUMN issue_url TEXT' },
|
|
132
|
+
{ name: 'pr_number', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_number INTEGER' },
|
|
133
|
+
{ name: 'pr_url', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_url TEXT' },
|
|
134
|
+
{ name: 'pr_status', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_status TEXT' },
|
|
135
|
+
{
|
|
136
|
+
name: 'workflow_run_id',
|
|
137
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_run_id INTEGER',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'workflow_name',
|
|
141
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_name TEXT',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'workflow_status',
|
|
145
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_status TEXT',
|
|
146
|
+
},
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
const added: string[] = []
|
|
150
|
+
for (const col of requiredColumns) {
|
|
151
|
+
if (!columns.has(col.name)) {
|
|
152
|
+
db.run(col.sql)
|
|
153
|
+
added.push(col.name)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Fix any tags with NULL usage_count (data repair from legacy DBs)
|
|
158
|
+
db.run('UPDATE tags SET usage_count = 0 WHERE usage_count IS NULL')
|
|
159
|
+
|
|
160
|
+
// Drop legacy FTS5 triggers from Python-era databases.
|
|
161
|
+
// sql.js WASM does not include FTS5; these triggers cause "no such module: fts5"
|
|
162
|
+
// on INSERT/UPDATE/DELETE operations. The TypeScript codebase uses LIKE queries.
|
|
163
|
+
// NOTE: We only drop triggers (regular objects). Dropping FTS5 virtual tables
|
|
164
|
+
// would also require the fts5 module, so we leave the inert shadow tables in place.
|
|
165
|
+
const dropped: string[] = []
|
|
166
|
+
const triggers = db.exec(
|
|
167
|
+
"SELECT name FROM sqlite_master WHERE type = 'trigger' AND sql LIKE '%fts%'"
|
|
168
|
+
)
|
|
169
|
+
// Validate trigger names before interpolating into DDL (defense-in-depth)
|
|
170
|
+
const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/
|
|
171
|
+
for (const row of triggers[0]?.values ?? []) {
|
|
172
|
+
const name = String(row[0])
|
|
173
|
+
if (!SAFE_IDENTIFIER_RE.test(name)) {
|
|
174
|
+
logger.warning('Skipping trigger with unsafe name during migration', {
|
|
175
|
+
module: 'SqliteAdapter',
|
|
176
|
+
triggerName: name,
|
|
177
|
+
})
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
db.run(`DROP TRIGGER IF EXISTS "${name}"`)
|
|
181
|
+
dropped.push(`trigger:${name}`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const changes = [...added.map((c) => `column:${c}`), ...dropped]
|
|
185
|
+
if (changes.length > 0) {
|
|
186
|
+
this.flushSave()
|
|
187
|
+
logger.info('Schema migrated', {
|
|
188
|
+
module: 'SqliteAdapter',
|
|
189
|
+
dbPath: this.dbPath,
|
|
190
|
+
changes,
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Apply additional schema for team databases (adds author column).
|
|
197
|
+
* Also migrates legacy team DBs that may be missing columns from the
|
|
198
|
+
* current main schema (e.g. issue_number, pr_number added after v2).
|
|
199
|
+
* Idempotent — safe to call on databases that already have all columns.
|
|
200
|
+
*/
|
|
201
|
+
applyTeamSchema(): void {
|
|
202
|
+
const db = this.ensureDb()
|
|
203
|
+
const tableInfo = db.exec('PRAGMA table_info(memory_journal)')
|
|
204
|
+
const columns = new Set((tableInfo[0]?.values ?? []).map((row) => String(row[1])))
|
|
205
|
+
|
|
206
|
+
// Columns required by the current schema that legacy team DBs may lack
|
|
207
|
+
const requiredColumns: { name: string; sql: string }[] = [
|
|
208
|
+
{
|
|
209
|
+
name: 'issue_number',
|
|
210
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN issue_number INTEGER',
|
|
211
|
+
},
|
|
212
|
+
{ name: 'issue_url', sql: 'ALTER TABLE memory_journal ADD COLUMN issue_url TEXT' },
|
|
213
|
+
{ name: 'pr_number', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_number INTEGER' },
|
|
214
|
+
{ name: 'pr_url', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_url TEXT' },
|
|
215
|
+
{ name: 'pr_status', sql: 'ALTER TABLE memory_journal ADD COLUMN pr_status TEXT' },
|
|
216
|
+
{
|
|
217
|
+
name: 'workflow_run_id',
|
|
218
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_run_id INTEGER',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'workflow_name',
|
|
222
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_name TEXT',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'workflow_status',
|
|
226
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN workflow_status TEXT',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'project_number',
|
|
230
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN project_number INTEGER',
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'project_owner',
|
|
234
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN project_owner TEXT',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'significance_type',
|
|
238
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN significance_type TEXT',
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'auto_context',
|
|
242
|
+
sql: 'ALTER TABLE memory_journal ADD COLUMN auto_context TEXT',
|
|
243
|
+
},
|
|
244
|
+
{ name: 'deleted_at', sql: 'ALTER TABLE memory_journal ADD COLUMN deleted_at TEXT' },
|
|
245
|
+
{ name: 'author', sql: TEAM_SCHEMA_SQL.trim() },
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
const added: string[] = []
|
|
249
|
+
for (const col of requiredColumns) {
|
|
250
|
+
if (!columns.has(col.name)) {
|
|
251
|
+
db.run(col.sql)
|
|
252
|
+
added.push(col.name)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (added.length > 0) {
|
|
257
|
+
this.flushSave()
|
|
258
|
+
logger.info('Team schema migrated', {
|
|
259
|
+
module: 'SqliteAdapter',
|
|
260
|
+
dbPath: this.dbPath,
|
|
261
|
+
columnsAdded: added,
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
179
266
|
/**
|
|
180
267
|
* Schedule a debounced save to disk.
|
|
181
268
|
* Batches rapid mutations into a single write after SAVE_DEBOUNCE_MS.
|
|
@@ -450,10 +537,7 @@ export class SqliteAdapter {
|
|
|
450
537
|
const result = db.exec(sql, params)
|
|
451
538
|
if (result.length === 0) return []
|
|
452
539
|
|
|
453
|
-
|
|
454
|
-
return (result[0]?.values ?? []).map((values) =>
|
|
455
|
-
this.rowToEntry(this.rowToObject(columns, values))
|
|
456
|
-
)
|
|
540
|
+
return this.rowsToEntries(result[0]?.columns ?? [], result[0]?.values ?? [])
|
|
457
541
|
}
|
|
458
542
|
|
|
459
543
|
/**
|
|
@@ -468,10 +552,7 @@ export class SqliteAdapter {
|
|
|
468
552
|
)
|
|
469
553
|
if (result.length === 0) return []
|
|
470
554
|
|
|
471
|
-
|
|
472
|
-
return (result[0]?.values ?? []).map((values) =>
|
|
473
|
-
this.rowToEntry(this.rowToObject(columns, values))
|
|
474
|
-
)
|
|
555
|
+
return this.rowsToEntries(result[0]?.columns ?? [], result[0]?.values ?? [])
|
|
475
556
|
}
|
|
476
557
|
|
|
477
558
|
/**
|
|
@@ -582,9 +663,9 @@ export class SqliteAdapter {
|
|
|
582
663
|
|
|
583
664
|
let sql = `
|
|
584
665
|
SELECT * FROM memory_journal
|
|
585
|
-
WHERE deleted_at IS NULL AND content LIKE ?
|
|
666
|
+
WHERE deleted_at IS NULL AND content LIKE ? ESCAPE '\\'
|
|
586
667
|
`
|
|
587
|
-
const params: unknown[] = [`%${query}%`]
|
|
668
|
+
const params: unknown[] = [`%${sanitizeSearchQuery(query)}%`]
|
|
588
669
|
|
|
589
670
|
if (isPersonal !== undefined) {
|
|
590
671
|
sql += ` AND is_personal = ?`
|
|
@@ -609,10 +690,7 @@ export class SqliteAdapter {
|
|
|
609
690
|
const result = db.exec(sql, params)
|
|
610
691
|
if (result.length === 0) return []
|
|
611
692
|
|
|
612
|
-
|
|
613
|
-
return (result[0]?.values ?? []).map((values) =>
|
|
614
|
-
this.rowToEntry(this.rowToObject(columns, values))
|
|
615
|
-
)
|
|
693
|
+
return this.rowsToEntries(result[0]?.columns ?? [], result[0]?.values ?? [])
|
|
616
694
|
}
|
|
617
695
|
|
|
618
696
|
/**
|
|
@@ -626,20 +704,35 @@ export class SqliteAdapter {
|
|
|
626
704
|
tags?: string[]
|
|
627
705
|
isPersonal?: boolean
|
|
628
706
|
projectNumber?: number
|
|
707
|
+
limit?: number
|
|
629
708
|
} = {}
|
|
630
709
|
): JournalEntry[] {
|
|
631
710
|
const db = this.ensureDb()
|
|
632
711
|
const { entryType, tags, isPersonal, projectNumber } = options
|
|
633
712
|
|
|
634
|
-
let sql
|
|
635
|
-
SELECT DISTINCT m.* FROM memory_journal m
|
|
636
|
-
LEFT JOIN entry_tags et ON m.id = et.entry_id
|
|
637
|
-
LEFT JOIN tags t ON et.tag_id = t.id
|
|
638
|
-
WHERE m.deleted_at IS NULL
|
|
639
|
-
AND m.timestamp >= ? AND m.timestamp <= ?
|
|
640
|
-
`
|
|
713
|
+
let sql: string
|
|
641
714
|
const params: unknown[] = [startDate, endDate + ' 23:59:59']
|
|
642
715
|
|
|
716
|
+
// Only JOIN tag tables when filtering by tags (avoids DISTINCT overhead)
|
|
717
|
+
if (tags && tags.length > 0) {
|
|
718
|
+
sql = `
|
|
719
|
+
SELECT DISTINCT m.* FROM memory_journal m
|
|
720
|
+
JOIN entry_tags et ON m.id = et.entry_id
|
|
721
|
+
JOIN tags t ON et.tag_id = t.id
|
|
722
|
+
WHERE m.deleted_at IS NULL
|
|
723
|
+
AND m.timestamp >= ? AND m.timestamp <= ?
|
|
724
|
+
`
|
|
725
|
+
const placeholders = tags.map(() => '?').join(',')
|
|
726
|
+
sql += ` AND t.name IN (${placeholders})`
|
|
727
|
+
params.push(...tags)
|
|
728
|
+
} else {
|
|
729
|
+
sql = `
|
|
730
|
+
SELECT * FROM memory_journal m
|
|
731
|
+
WHERE m.deleted_at IS NULL
|
|
732
|
+
AND m.timestamp >= ? AND m.timestamp <= ?
|
|
733
|
+
`
|
|
734
|
+
}
|
|
735
|
+
|
|
643
736
|
if (entryType) {
|
|
644
737
|
sql += ` AND m.entry_type = ?`
|
|
645
738
|
params.push(entryType)
|
|
@@ -652,21 +745,14 @@ export class SqliteAdapter {
|
|
|
652
745
|
sql += ` AND m.project_number = ?`
|
|
653
746
|
params.push(projectNumber)
|
|
654
747
|
}
|
|
655
|
-
if (tags && tags.length > 0) {
|
|
656
|
-
const placeholders = tags.map(() => '?').join(',')
|
|
657
|
-
sql += ` AND t.name IN (${placeholders})`
|
|
658
|
-
params.push(...tags)
|
|
659
|
-
}
|
|
660
748
|
|
|
661
|
-
sql += ` ORDER BY m.timestamp DESC
|
|
749
|
+
sql += ` ORDER BY m.timestamp DESC LIMIT ?`
|
|
750
|
+
params.push(options.limit ?? 500)
|
|
662
751
|
|
|
663
752
|
const result = db.exec(sql, params)
|
|
664
753
|
if (result.length === 0) return []
|
|
665
754
|
|
|
666
|
-
|
|
667
|
-
return (result[0]?.values ?? []).map((values) =>
|
|
668
|
-
this.rowToEntry(this.rowToObject(columns, values))
|
|
669
|
-
)
|
|
755
|
+
return this.rowsToEntries(result[0]?.columns ?? [], result[0]?.values ?? [])
|
|
670
756
|
}
|
|
671
757
|
|
|
672
758
|
// =========================================================================
|
|
@@ -677,25 +763,34 @@ export class SqliteAdapter {
|
|
|
677
763
|
* Get or create tags and link to entry
|
|
678
764
|
*/
|
|
679
765
|
private linkTagsToEntry(entryId: number, tagNames: string[]): void {
|
|
766
|
+
if (tagNames.length === 0) return
|
|
680
767
|
const db = this.ensureDb()
|
|
681
768
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
769
|
+
// Batch: insert all tags in one statement
|
|
770
|
+
const insertPlaceholders = tagNames.map(() => '(?, 0)').join(', ')
|
|
771
|
+
db.run(
|
|
772
|
+
`INSERT OR IGNORE INTO tags (name, usage_count) VALUES ${insertPlaceholders}`,
|
|
773
|
+
tagNames
|
|
774
|
+
)
|
|
685
775
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
776
|
+
// Batch: fetch all tag IDs in one query
|
|
777
|
+
const selectPlaceholders = tagNames.map(() => '?').join(', ')
|
|
778
|
+
const result = db.exec(
|
|
779
|
+
`SELECT id, name FROM tags WHERE name IN (${selectPlaceholders})`,
|
|
780
|
+
tagNames
|
|
781
|
+
)
|
|
689
782
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
783
|
+
const tagIds: number[] = []
|
|
784
|
+
for (const row of result[0]?.values ?? []) {
|
|
785
|
+
const tagId = row[0] as number
|
|
786
|
+
tagIds.push(tagId)
|
|
787
|
+
// Link tag to entry
|
|
788
|
+
db.run('INSERT OR IGNORE INTO entry_tags (entry_id, tag_id) VALUES (?, ?)', [
|
|
789
|
+
entryId,
|
|
790
|
+
tagId,
|
|
791
|
+
])
|
|
792
|
+
// Increment usage
|
|
793
|
+
db.run('UPDATE tags SET usage_count = usage_count + 1 WHERE id = ?', [tagId])
|
|
699
794
|
}
|
|
700
795
|
}
|
|
701
796
|
|
|
@@ -722,7 +817,9 @@ export class SqliteAdapter {
|
|
|
722
817
|
*/
|
|
723
818
|
listTags(): Tag[] {
|
|
724
819
|
const db = this.ensureDb()
|
|
725
|
-
const result = db.exec(
|
|
820
|
+
const result = db.exec(
|
|
821
|
+
'SELECT id, name, COALESCE(usage_count, 0) as usage_count FROM tags WHERE COALESCE(usage_count, 0) > 0 ORDER BY usage_count DESC'
|
|
822
|
+
)
|
|
726
823
|
|
|
727
824
|
if (result.length === 0) return []
|
|
728
825
|
|
|
@@ -918,29 +1015,27 @@ export class SqliteAdapter {
|
|
|
918
1015
|
} {
|
|
919
1016
|
const db = this.ensureDb()
|
|
920
1017
|
|
|
921
|
-
//
|
|
922
|
-
const
|
|
923
|
-
SELECT COUNT(*) as count FROM memory_journal WHERE deleted_at IS NULL
|
|
924
|
-
`)
|
|
925
|
-
const totalEntries = (totalResult[0]?.values[0]?.[0] as number) ?? 0
|
|
926
|
-
|
|
927
|
-
// By type
|
|
928
|
-
const byTypeResult = db.exec(`
|
|
1018
|
+
// Combined query 1: total entries + breakdown by type
|
|
1019
|
+
const combinedResult = db.exec(`
|
|
1020
|
+
SELECT COUNT(*) as count FROM memory_journal WHERE deleted_at IS NULL;
|
|
929
1021
|
SELECT entry_type, COUNT(*) as count
|
|
930
1022
|
FROM memory_journal
|
|
931
1023
|
WHERE deleted_at IS NULL
|
|
932
1024
|
GROUP BY entry_type
|
|
933
1025
|
`)
|
|
1026
|
+
const totalEntries = (combinedResult[0]?.values[0]?.[0] as number) ?? 0
|
|
934
1027
|
const entriesByType: Record<string, number> = {}
|
|
935
|
-
for (const row of
|
|
1028
|
+
for (const row of combinedResult[1]?.values ?? []) {
|
|
936
1029
|
entriesByType[row[0] as string] = row[1] as number
|
|
937
1030
|
}
|
|
938
1031
|
|
|
939
|
-
//
|
|
1032
|
+
// Combined query 2: period breakdown + decision density (using CASE)
|
|
940
1033
|
const dateFormat = validateDateFormatPattern(groupBy)
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1034
|
+
const periodResult = db.exec(`
|
|
1035
|
+
SELECT
|
|
1036
|
+
strftime('${dateFormat}', timestamp) as period,
|
|
1037
|
+
COUNT(*) as total_count,
|
|
1038
|
+
SUM(CASE WHEN significance_type IS NOT NULL THEN 1 ELSE 0 END) as significant_count
|
|
944
1039
|
FROM memory_journal
|
|
945
1040
|
WHERE deleted_at IS NULL
|
|
946
1041
|
GROUP BY period
|
|
@@ -948,32 +1043,27 @@ export class SqliteAdapter {
|
|
|
948
1043
|
LIMIT 52
|
|
949
1044
|
`)
|
|
950
1045
|
|
|
951
|
-
const entriesByPeriod = (
|
|
1046
|
+
const entriesByPeriod = (periodResult[0]?.values ?? []).map((v: unknown[]) => ({
|
|
952
1047
|
period: v[0] as string,
|
|
953
1048
|
count: v[1] as number,
|
|
954
1049
|
}))
|
|
955
1050
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1051
|
+
const decisionDensity = (periodResult[0]?.values ?? [])
|
|
1052
|
+
.filter((v: unknown[]) => (v[2] as number) > 0)
|
|
1053
|
+
.map((v: unknown[]) => ({
|
|
1054
|
+
period: v[0] as string,
|
|
1055
|
+
significantCount: v[2] as number,
|
|
1056
|
+
}))
|
|
959
1057
|
|
|
960
|
-
//
|
|
961
|
-
const
|
|
962
|
-
SELECT
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
LIMIT 52
|
|
1058
|
+
// Combined query 3: relationship counts + causal breakdown
|
|
1059
|
+
const relResult = db.exec(`
|
|
1060
|
+
SELECT COUNT(*) FROM relationships;
|
|
1061
|
+
SELECT relationship_type, COUNT(*) as count
|
|
1062
|
+
FROM relationships
|
|
1063
|
+
WHERE relationship_type IN ('blocked_by', 'resolved', 'caused')
|
|
1064
|
+
GROUP BY relationship_type
|
|
968
1065
|
`)
|
|
969
|
-
const
|
|
970
|
-
period: v[0] as string,
|
|
971
|
-
significantCount: v[1] as number,
|
|
972
|
-
}))
|
|
973
|
-
|
|
974
|
-
// Relationship Complexity: total relationships and avg per entry
|
|
975
|
-
const relCountResult = db.exec(`SELECT COUNT(*) FROM relationships`)
|
|
976
|
-
const totalRelationships = (relCountResult[0]?.values[0]?.[0] as number) ?? 0
|
|
1066
|
+
const totalRelationships = (relResult[0]?.values[0]?.[0] as number) ?? 0
|
|
977
1067
|
const avgPerEntry = totalEntries > 0 ? totalRelationships / totalEntries : 0
|
|
978
1068
|
|
|
979
1069
|
// Activity Trend: week-over-week growth
|
|
@@ -987,14 +1077,8 @@ export class SqliteAdapter {
|
|
|
987
1077
|
: null
|
|
988
1078
|
|
|
989
1079
|
// Causal Metrics: counts for causal relationship types
|
|
990
|
-
const causalResult = db.exec(`
|
|
991
|
-
SELECT relationship_type, COUNT(*) as count
|
|
992
|
-
FROM relationships
|
|
993
|
-
WHERE relationship_type IN ('blocked_by', 'resolved', 'caused')
|
|
994
|
-
GROUP BY relationship_type
|
|
995
|
-
`)
|
|
996
1080
|
const causalMetrics = { blocked_by: 0, resolved: 0, caused: 0 }
|
|
997
|
-
for (const row of
|
|
1081
|
+
for (const row of relResult[1]?.values ?? []) {
|
|
998
1082
|
const relType = row[0] as 'blocked_by' | 'resolved' | 'caused'
|
|
999
1083
|
causalMetrics[relType] = row[1] as number
|
|
1000
1084
|
}
|
|
@@ -1037,6 +1121,11 @@ export class SqliteAdapter {
|
|
|
1037
1121
|
const db = this.ensureDb()
|
|
1038
1122
|
const backupsDir = this.getBackupsDir()
|
|
1039
1123
|
|
|
1124
|
+
// Validate backup name against path traversal before sanitization
|
|
1125
|
+
if (backupName) {
|
|
1126
|
+
assertNoPathTraversal(backupName)
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1040
1129
|
// Ensure backups directory exists
|
|
1041
1130
|
if (!fs.existsSync(backupsDir)) {
|
|
1042
1131
|
fs.mkdirSync(backupsDir, { recursive: true })
|
|
@@ -1118,7 +1207,7 @@ export class SqliteAdapter {
|
|
|
1118
1207
|
deleteOldBackups(keepCount: number): { deleted: string[]; kept: number } {
|
|
1119
1208
|
const backups = this.listBackups() // Already sorted newest-first
|
|
1120
1209
|
|
|
1121
|
-
if (keepCount < 1) {
|
|
1210
|
+
if (keepCount < 1 || Number.isNaN(keepCount)) {
|
|
1122
1211
|
throw new Error('keepCount must be at least 1')
|
|
1123
1212
|
}
|
|
1124
1213
|
|
|
@@ -1155,9 +1244,7 @@ export class SqliteAdapter {
|
|
|
1155
1244
|
newEntryCount: number
|
|
1156
1245
|
}> {
|
|
1157
1246
|
// Validate filename (prevent path traversal)
|
|
1158
|
-
|
|
1159
|
-
throw new Error('Invalid backup filename: path separators not allowed')
|
|
1160
|
-
}
|
|
1247
|
+
assertNoPathTraversal(filename)
|
|
1161
1248
|
|
|
1162
1249
|
const backupsDir = this.getBackupsDir()
|
|
1163
1250
|
const backupPath = path.join(backupsDir, filename)
|
|
@@ -1187,6 +1274,7 @@ export class SqliteAdapter {
|
|
|
1187
1274
|
// Initialize new database from backup
|
|
1188
1275
|
const SQL = await import('sql.js').then((m) => m.default())
|
|
1189
1276
|
this.db = new SQL.Database(backupBuffer)
|
|
1277
|
+
this.db.run('PRAGMA foreign_keys = ON')
|
|
1190
1278
|
this.initialized = true
|
|
1191
1279
|
|
|
1192
1280
|
// Get new entry count
|
|
@@ -1300,7 +1388,9 @@ export class SqliteAdapter {
|
|
|
1300
1388
|
}
|
|
1301
1389
|
|
|
1302
1390
|
/**
|
|
1303
|
-
* Convert database row to JournalEntry
|
|
1391
|
+
* Convert database row to JournalEntry.
|
|
1392
|
+
* Used for single-entry methods (getEntryById, getEntryByIdIncludeDeleted)
|
|
1393
|
+
* where the N+1 overhead of one tag query is negligible.
|
|
1304
1394
|
*/
|
|
1305
1395
|
private rowToEntry(row: Record<string, unknown>): JournalEntry {
|
|
1306
1396
|
const id = row['id'] as number
|
|
@@ -1329,7 +1419,83 @@ export class SqliteAdapter {
|
|
|
1329
1419
|
}
|
|
1330
1420
|
|
|
1331
1421
|
/**
|
|
1332
|
-
*
|
|
1422
|
+
* Convert multiple database rows to JournalEntry[] with batch tag fetching.
|
|
1423
|
+
* Uses a single IN (...) query to fetch all tags for all entries at once,
|
|
1424
|
+
* eliminating the N+1 query pattern of per-row getTagsForEntry calls.
|
|
1425
|
+
*/
|
|
1426
|
+
private rowsToEntries(columns: string[], values: unknown[][]): JournalEntry[] {
|
|
1427
|
+
if (values.length === 0) return []
|
|
1428
|
+
|
|
1429
|
+
// Step 1: Convert all rows to objects
|
|
1430
|
+
const rows = values.map((v) => this.rowToObject(columns, v))
|
|
1431
|
+
const ids = rows.map((r) => r['id'] as number)
|
|
1432
|
+
|
|
1433
|
+
// Step 2: Batch-fetch all tags in one query
|
|
1434
|
+
const tagMap = this.batchGetTagsForEntries(ids)
|
|
1435
|
+
|
|
1436
|
+
// Step 3: Assemble entries using the pre-fetched tag map
|
|
1437
|
+
return rows.map((row) => {
|
|
1438
|
+
const id = row['id'] as number
|
|
1439
|
+
return {
|
|
1440
|
+
id,
|
|
1441
|
+
entryType: row['entry_type'] as EntryType,
|
|
1442
|
+
content: row['content'] as string,
|
|
1443
|
+
timestamp: row['timestamp'] as string,
|
|
1444
|
+
isPersonal: row['is_personal'] === 1,
|
|
1445
|
+
significanceType: row['significance_type'] as SignificanceType,
|
|
1446
|
+
autoContext: row['auto_context'] as string | null,
|
|
1447
|
+
deletedAt: row['deleted_at'] as string | null,
|
|
1448
|
+
tags: tagMap.get(id) ?? [],
|
|
1449
|
+
projectNumber: (row['project_number'] as number | null) ?? null,
|
|
1450
|
+
projectOwner: (row['project_owner'] as string | null) ?? null,
|
|
1451
|
+
issueNumber: (row['issue_number'] as number | null) ?? null,
|
|
1452
|
+
issueUrl: (row['issue_url'] as string | null) ?? null,
|
|
1453
|
+
prNumber: (row['pr_number'] as number | null) ?? null,
|
|
1454
|
+
prUrl: (row['pr_url'] as string | null) ?? null,
|
|
1455
|
+
prStatus: (row['pr_status'] as string | null) ?? null,
|
|
1456
|
+
workflowRunId: (row['workflow_run_id'] as number | null) ?? null,
|
|
1457
|
+
workflowName: (row['workflow_name'] as string | null) ?? null,
|
|
1458
|
+
workflowStatus: (row['workflow_status'] as string | null) ?? null,
|
|
1459
|
+
}
|
|
1460
|
+
})
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Batch-fetch tags for multiple entry IDs in a single query.
|
|
1465
|
+
* Returns a Map<entryId, tagNames[]>.
|
|
1466
|
+
* Eliminates the N+1 query problem for multi-row result sets.
|
|
1467
|
+
*/
|
|
1468
|
+
private batchGetTagsForEntries(ids: number[]): Map<number, string[]> {
|
|
1469
|
+
const tagMap = new Map<number, string[]>()
|
|
1470
|
+
if (ids.length === 0) return tagMap
|
|
1471
|
+
|
|
1472
|
+
const db = this.ensureDb()
|
|
1473
|
+
const placeholders = ids.map(() => '?').join(', ')
|
|
1474
|
+
const result = db.exec(
|
|
1475
|
+
`SELECT et.entry_id, t.name
|
|
1476
|
+
FROM entry_tags et
|
|
1477
|
+
JOIN tags t ON et.tag_id = t.id
|
|
1478
|
+
WHERE et.entry_id IN (${placeholders})`,
|
|
1479
|
+
ids
|
|
1480
|
+
)
|
|
1481
|
+
|
|
1482
|
+
for (const row of result[0]?.values ?? []) {
|
|
1483
|
+
const entryId = row[0] as number
|
|
1484
|
+
const tagName = row[1] as string
|
|
1485
|
+
const existing = tagMap.get(entryId)
|
|
1486
|
+
if (existing) {
|
|
1487
|
+
existing.push(tagName)
|
|
1488
|
+
} else {
|
|
1489
|
+
tagMap.set(entryId, [tagName])
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
return tagMap
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/**
|
|
1497
|
+
* Get raw sql.js Database handle for advanced queries.
|
|
1498
|
+
* @internal Callers MUST use parameterized queries — never concatenate user input into SQL.
|
|
1333
1499
|
*/
|
|
1334
1500
|
getRawDb(): Database {
|
|
1335
1501
|
return this.ensureDb()
|