memory-journal-mcp 4.3.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.dockerignore +131 -122
  2. package/.gitattributes +29 -0
  3. package/.github/workflows/docker-publish.yml +1 -1
  4. package/.github/workflows/lint-and-test.yml +1 -2
  5. package/.github/workflows/secrets-scanning.yml +0 -1
  6. package/.github/workflows/security-update.yml +6 -6
  7. package/.vscode/settings.json +17 -15
  8. package/CHANGELOG.md +1065 -11
  9. package/DOCKER_README.md +51 -33
  10. package/Dockerfile +12 -11
  11. package/README.md +68 -33
  12. package/SECURITY.md +225 -158
  13. package/dist/cli.js +7 -0
  14. package/dist/cli.js.map +1 -1
  15. package/dist/constants/ServerInstructions.d.ts +1 -1
  16. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  17. package/dist/constants/ServerInstructions.js +70 -26
  18. package/dist/constants/ServerInstructions.js.map +1 -1
  19. package/dist/constants/icons.d.ts +2 -0
  20. package/dist/constants/icons.d.ts.map +1 -1
  21. package/dist/constants/icons.js +6 -0
  22. package/dist/constants/icons.js.map +1 -1
  23. package/dist/database/SqliteAdapter.d.ts +51 -10
  24. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  25. package/dist/database/SqliteAdapter.js +140 -31
  26. package/dist/database/SqliteAdapter.js.map +1 -1
  27. package/dist/filtering/ToolFilter.d.ts +1 -1
  28. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  29. package/dist/filtering/ToolFilter.js +7 -1
  30. package/dist/filtering/ToolFilter.js.map +1 -1
  31. package/dist/github/GitHubIntegration.d.ts +74 -2
  32. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  33. package/dist/github/GitHubIntegration.js +508 -7
  34. package/dist/github/GitHubIntegration.js.map +1 -1
  35. package/dist/handlers/prompts/index.js +1 -0
  36. package/dist/handlers/prompts/index.js.map +1 -1
  37. package/dist/handlers/resources/index.d.ts.map +1 -1
  38. package/dist/handlers/resources/index.js +257 -13
  39. package/dist/handlers/resources/index.js.map +1 -1
  40. package/dist/handlers/tools/index.d.ts.map +1 -1
  41. package/dist/handlers/tools/index.js +590 -7
  42. package/dist/handlers/tools/index.js.map +1 -1
  43. package/dist/server/McpServer.d.ts +2 -0
  44. package/dist/server/McpServer.d.ts.map +1 -1
  45. package/dist/server/McpServer.js +69 -26
  46. package/dist/server/McpServer.js.map +1 -1
  47. package/dist/types/index.d.ts +97 -0
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/utils/logger.d.ts +1 -0
  51. package/dist/utils/logger.d.ts.map +1 -1
  52. package/dist/utils/logger.js +8 -1
  53. package/dist/utils/logger.js.map +1 -1
  54. package/dist/utils/security-utils.d.ts +8 -0
  55. package/dist/utils/security-utils.d.ts.map +1 -1
  56. package/dist/utils/security-utils.js +32 -0
  57. package/dist/utils/security-utils.js.map +1 -1
  58. package/dist/vector/VectorSearchManager.d.ts +2 -1
  59. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  60. package/dist/vector/VectorSearchManager.js +100 -34
  61. package/dist/vector/VectorSearchManager.js.map +1 -1
  62. package/docker-compose.yml +46 -37
  63. package/mcp-config-example.json +0 -2
  64. package/package.json +12 -11
  65. package/releases/v4.3.1.md +69 -0
  66. package/releases/v4.4.0.md +120 -0
  67. package/server.json +3 -3
  68. package/src/cli.ts +11 -0
  69. package/src/constants/ServerInstructions.ts +70 -26
  70. package/src/constants/icons.ts +7 -0
  71. package/src/database/SqliteAdapter.ts +162 -32
  72. package/src/filtering/ToolFilter.ts +7 -1
  73. package/src/github/GitHubIntegration.ts +588 -8
  74. package/src/handlers/prompts/index.ts +1 -0
  75. package/src/handlers/resources/index.ts +318 -12
  76. package/src/handlers/tools/index.ts +681 -12
  77. package/src/server/McpServer.ts +79 -37
  78. package/src/types/index.ts +98 -0
  79. package/src/utils/logger.ts +10 -1
  80. package/src/utils/security-utils.ts +35 -0
  81. package/src/vector/VectorSearchManager.ts +110 -39
  82. package/tests/constants/icons.test.ts +102 -0
  83. package/tests/constants/server-instructions.test.ts +549 -0
  84. package/tests/database/sqlite-adapter.bench.ts +63 -0
  85. package/tests/database/sqlite-adapter.test.ts +555 -0
  86. package/tests/filtering/tool-filter.test.ts +266 -0
  87. package/tests/github/github-integration.test.ts +1024 -0
  88. package/tests/handlers/github-resource-handlers.test.ts +473 -0
  89. package/tests/handlers/github-tool-handlers.test.ts +556 -0
  90. package/tests/handlers/prompt-handlers.test.ts +91 -0
  91. package/tests/handlers/resource-handlers.test.ts +339 -0
  92. package/tests/handlers/tool-handlers.test.ts +497 -0
  93. package/tests/handlers/vector-tool-handlers.test.ts +238 -0
  94. package/tests/server/mcp-server.bench.ts +55 -0
  95. package/tests/server/mcp-server.test.ts +675 -0
  96. package/tests/utils/logger.test.ts +180 -0
  97. package/tests/utils/mcp-logger.test.ts +212 -0
  98. package/tests/utils/progress-utils.test.ts +156 -0
  99. package/tests/utils/security-utils.test.ts +82 -0
  100. package/tests/vector/vector-search-manager.test.ts +335 -0
  101. package/tests/vector/vector-search.bench.ts +53 -0
  102. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
  103. package/.github/workflows/dependabot-auto-merge.yml +0 -42
@@ -4,7 +4,7 @@
4
4
  * These instructions are automatically sent to MCP clients during initialization,
5
5
  * providing guidance for AI agents on tool usage.
6
6
  *
7
- * v3.1.6: Optimized for token efficiency with tiered instruction levels.
7
+ * Unreleased: Optimized for token efficiency with tiered instruction levels.
8
8
  */
9
9
 
10
10
  import type { ToolGroup } from '../types/index.js'
@@ -80,6 +80,7 @@ const GITHUB_INSTRUCTIONS = `
80
80
  - After closing issue/merging PR → create summary entry with learnings
81
81
  - CI failures → \`actions-failure-digest\` prompt or \`memory://actions/recent\`
82
82
  - Kanban: \`get_kanban_board(project_number)\` → \`move_kanban_item\` → document completion
83
+ - Milestones: \`get_github_milestones\` → track project progress, \`memory://github/milestones\`
83
84
  - GitHub tools auto-detect owner/repo from git context; specify explicitly if null
84
85
  `
85
86
 
@@ -122,10 +123,10 @@ const TOOL_PARAMETER_REFERENCE = `
122
123
  ### Entry Operations
123
124
  | Tool | Required Parameters | Optional Parameters |
124
125
  |------|---------------------|---------------------|
125
- | \`create_entry\` | \`content\` (string) | \`entry_type\`, \`tags\` (array), \`is_personal\` (bool) |
126
+ | \`create_entry\` | \`content\` (string) | \`entry_type\`, \`tags\` (array), \`is_personal\`, \`significance_type\`, \`share_with_team\`, \`auto_context\`, \`issue_number\`, \`issue_url\`, \`pr_number\`, \`pr_url\`, \`pr_status\`, \`project_number\`, \`project_owner\`, \`workflow_run_id\`, \`workflow_name\`, \`workflow_status\` |
126
127
  | \`create_entry_minimal\` | \`content\` (string) | none |
127
- | \`get_entry_by_id\` | \`entry_id\` (number) | none |
128
- | \`get_recent_entries\` | none | \`limit\` (default 10), \`is_personal\` (bool) |
128
+ | \`get_entry_by_id\` | \`entry_id\` (number) | \`include_relationships\` (bool, default true) |
129
+ | \`get_recent_entries\` | none | \`limit\` (default 5), \`is_personal\` (bool) |
129
130
  | \`update_entry\` | \`entry_id\` (number) | \`content\`, \`tags\`, \`entry_type\`, \`is_personal\` |
130
131
  | \`delete_entry\` | \`entry_id\` (number) | \`permanent\` (bool, default false) |
131
132
  | \`list_tags\` | none | none |
@@ -133,33 +134,40 @@ const TOOL_PARAMETER_REFERENCE = `
133
134
  ### Search Tools
134
135
  | Tool | Required Parameters | Optional Parameters |
135
136
  |------|---------------------|---------------------|
136
- | \`search_entries\` | \`query\` (string) | \`limit\`, \`entry_type\`, \`tags\` |
137
- | \`search_by_date_range\` | \`start_date\`, \`end_date\` (YYYY-MM-DD) | \`tags\`, \`entry_type\` |
138
- | \`semantic_search\` | \`query\` (string) | \`limit\`, \`similarity_threshold\` (default 0.3) |
137
+ | \`search_entries\` | none | \`query\`, \`limit\`, \`is_personal\`, \`issue_number\`, \`pr_number\`, \`pr_status\`, \`project_number\`, \`workflow_run_id\` |
138
+ | \`search_by_date_range\` | \`start_date\`, \`end_date\` (YYYY-MM-DD) | \`tags\`, \`entry_type\`, \`is_personal\`, \`issue_number\`, \`pr_number\`, \`project_number\`, \`workflow_run_id\` |
139
+ | \`semantic_search\` | \`query\` (string) | \`limit\`, \`similarity_threshold\` (default 0.25), \`is_personal\`, \`hint_on_empty\` (bool, default true) |
139
140
  | \`get_vector_index_stats\` | none | none |
140
141
 
141
142
  ### Relationship Tools
142
143
  | Tool | Required Parameters | Notes |
143
144
  |------|---------------------|-------|
144
- | \`link_entries\` | \`from_entry_id\`, \`to_entry_id\` (numbers) | Types: \`evolves_from\`, \`references\`, \`implements\`, \`clarifies\`, \`response_to\`, \`blocked_by\`, \`resolved\`, \`caused\` |
145
- | \`visualize_relationships\` | \`entry_id\` (number) | Optional \`depth\` (default 2). Returns Mermaid diagram. |
145
+ | \`link_entries\` | \`from_entry_id\`, \`to_entry_id\` (numbers) | Types: \`evolves_from\`, \`references\`, \`implements\`, \`clarifies\`, \`response_to\`, \`blocked_by\`, \`resolved\`, \`caused\`. Optional \`description\`. |
146
+ | \`visualize_relationships\` | none | Optional \`entry_id\`, \`tags\` (array), \`depth\` (1-3, default 2), \`limit\` (default 20). Returns Mermaid diagram. |
146
147
 
147
148
  ### GitHub Tools
148
149
  | Tool | Required Parameters | Notes |
149
150
  |------|---------------------|-------|
150
- | \`get_github_context\` | none | Returns repo info, open issues/PRs |
151
- | \`get_github_issues\` | none | Optional \`state\` (open/closed/all), \`limit\` |
152
- | \`get_github_prs\` | none | Optional \`state\`, \`limit\` |
153
- | \`get_github_issue\` | \`issue_number\` (number) | Fetches single issue details |
154
- | \`get_github_pr\` | \`pr_number\` (number) | Fetches single PR details |
151
+ | \`get_github_context\` | none | Returns repo info, open issues/PRs. Only counts OPEN items. |
152
+ | \`get_github_issues\` | none | Optional \`state\` (open/closed/all), \`limit\`, \`owner\`, \`repo\` |
153
+ | \`get_github_prs\` | none | Optional \`state\`, \`limit\`, \`owner\`, \`repo\` |
154
+ | \`get_github_issue\` | \`issue_number\` (number) | Optional \`owner\`, \`repo\`. Fetches single issue details. |
155
+ | \`get_github_pr\` | \`pr_number\` (number) | Optional \`owner\`, \`repo\`. Fetches single PR details. |
156
+ | \`get_repo_insights\` | none | Optional \`sections\` (stars/traffic/referrers/paths/all, default "stars"), \`owner\`, \`repo\`. Requires push access for traffic. |
155
157
 
156
158
  GitHub tools auto-detect owner/repo from GITHUB_REPO_PATH. If \`detectedOwner\`/\`detectedRepo\` are null in response, specify \`owner\` and \`repo\` parameters explicitly.
157
159
 
160
+ ### Issue Lifecycle Tools
161
+ | Tool | Required Parameters | Notes |
162
+ |------|---------------------|-------|
163
+ | \`create_github_issue_with_entry\` | \`title\` (string) | Optional \`body\`, \`labels\` (array), \`assignees\` (array), \`project_number\`, \`initial_status\`, \`milestone_number\`, \`entry_content\`, \`tags\`, \`owner\`, \`repo\` |
164
+ | \`close_github_issue_with_entry\` | \`issue_number\` (number) | Optional \`comment\`, \`resolution_notes\`, \`tags\`, \`move_to_done\` (bool), \`project_number\`, \`owner\`, \`repo\` |
165
+
158
166
  ### Kanban Tools (GitHub Projects v2)
159
167
  | Tool | Required Parameters | Notes |
160
168
  |------|---------------------|-------|
161
- | \`get_kanban_board\` | \`project_number\` (number) | Returns columns with items grouped by Status |
162
- | \`move_kanban_item\` | \`project_number\`, \`item_id\` (string), \`target_status\` (string) | \`item_id\` is the GraphQL node ID from board items |
169
+ | \`get_kanban_board\` | \`project_number\` (number) | Optional \`owner\`. Returns columns with items grouped by Status |
170
+ | \`move_kanban_item\` | \`project_number\`, \`item_id\` (string), \`target_status\` (string) | Optional \`owner\`. \`item_id\` is the GraphQL node ID from board items. Status matching is case-insensitive. |
163
171
 
164
172
  **Finding the right project**: User may have multiple projects. Use \`get_kanban_board\` with different project numbers (1, 2, 3...) to find the correct one by checking \`projectTitle\`.
165
173
 
@@ -176,33 +184,53 @@ Kanban resources:
176
184
  - \`memory://kanban/{project_number}\` - JSON board data
177
185
  - \`memory://kanban/{project_number}/diagram\` - Mermaid visualization
178
186
 
187
+ ### Milestone Tools
188
+ | Tool | Required Parameters | Notes |
189
+ |------|---------------------|-------|
190
+ | \`get_github_milestones\` | none | Optional \`state\` (open/closed/all), \`limit\`, \`owner\`, \`repo\` |
191
+ | \`get_github_milestone\` | \`milestone_number\` (number) | Optional \`owner\`, \`repo\`. Single milestone with completion %. |
192
+ | \`create_github_milestone\` | \`title\` (string) | Optional \`description\`, \`due_on\` (YYYY-MM-DD), \`owner\`, \`repo\` |
193
+ | \`update_github_milestone\` | \`milestone_number\` (number) | Optional \`title\`, \`description\`, \`due_on\`, \`state\` (open/closed), \`owner\`, \`repo\` |
194
+ | \`delete_github_milestone\` | \`milestone_number\`, \`confirm: true\` | Optional \`owner\`, \`repo\`. Permanent deletion. |
195
+
196
+ Milestone resources:
197
+ - \`memory://github/milestones\` - Open milestones with completion %
198
+ - \`memory://milestones/{number}\` - Single milestone detail
199
+
179
200
  ### Admin Tools
180
201
  | Tool | Required Parameters | Notes |
181
202
  |------|---------------------|-------|
182
- | \`backup_journal\` | none | Optional \`backup_name\` |
203
+ | \`backup_journal\` | none | Optional \`name\` (custom backup name) |
183
204
  | \`list_backups\` | none | Returns available backup files |
184
- | \`restore_backup\` | \`backup_filename\`, \`confirm: true\` | Creates auto-backup before restore |
205
+ | \`cleanup_backups\` | none | Optional \`keep_count\` (default 5). Deletes old backups, keeps N most recent. |
206
+ | \`restore_backup\` | \`filename\`, \`confirm: true\` | Creates auto-backup before restore |
185
207
  | \`add_to_vector_index\` | \`entry_id\` (single number) | Indexes one entry for semantic search |
186
208
  | \`rebuild_vector_index\` | none | Re-indexes all entries |
187
209
 
188
210
  ### Export Tools
189
211
  | Tool | Required Parameters | Notes |
190
212
  |------|---------------------|-------|
191
- | \`export_entries\` | none | Optional \`format\` (json/markdown), \`limit\`, \`tags\` |
213
+ | \`export_entries\` | none | Optional \`format\` (json/markdown), \`limit\` (default 100), \`tags\`, \`start_date\`, \`end_date\`, \`entry_types\` |
192
214
 
193
215
  ## Entry Types
194
216
  Valid values for \`entry_type\` parameter:
195
217
  - \`personal_reflection\` (default) - Personal thoughts and notes
196
- - \`technical_note\` - Technical documentation
218
+ - \`project_decision\` - Architectural and team decisions
219
+ - \`technical_achievement\` - Milestones and breakthroughs
197
220
  - \`bug_fix\` - Bug fixes and resolutions
198
- - \`progress_update\` - Project progress updates
221
+ - \`feature_implementation\` - Feature builds and rollouts
199
222
  - \`code_review\` - Code review notes
200
- - \`deployment\` - Deployment records
201
- - \`technical_achievement\` - Milestones and breakthroughs
223
+ - \`meeting_notes\` - Meeting summaries
224
+ - \`learning\` - Learning and research insights
225
+ - \`research\` - Research and exploration
226
+ - \`planning\` - Planning sessions and roadmaps (\`create_github_issue_with_entry\` uses this type)
227
+ - \`retrospective\` - Sprint and project retrospectives
228
+ - \`standup\` - Daily standup notes
229
+ - \`other\` - Miscellaneous
202
230
 
203
231
  ## Field Notes
204
232
  - **\`autoContext\`**: Reserved for future automatic context capture. Currently always \`null\`.
205
- - **\`memory://tags\` vs \`list_tags\`**: Resource includes \`id\`, \`name\`, \`count\`; tool returns only \`name\`, \`count\`.
233
+ - **\`memory://tags\` vs \`list_tags\`**: Resource includes \`id\`, \`name\`, \`count\`; tool returns only \`name\`, \`count\`. Neither returns orphan tags with zero usage.
206
234
  - **Tag naming**: Use lowercase with dashes (e.g., \`bug-fix\`, \`phase-2\`). Use \`merge_tags\` to consolidate duplicates (e.g., merge \`phase2\` into \`phase-2\`).
207
235
  - **\`merge_tags\` behavior**: Only updates non-deleted entries. Deleted entries retain their original tags.
208
236
  - **\`prStatus\` in entries**: Reflects PR state at entry creation time, not current state. Use \`get_github_pr\` for live status.
@@ -211,19 +239,35 @@ Valid values for \`entry_type\` parameter:
211
239
  - **\`semantic_search\` thresholds**: Default similarity threshold is 0.25. For broader matches, try 0.15-0.2. Higher values (0.4+) return only very close semantic matches.
212
240
  - **Causal relationship types**: Use \`blocked_by\` (A was blocked by B), \`resolved\` (A resolved B), \`caused\` (A caused B) for decision tracing and failure analysis. Visualizations use distinct arrow styles for causal types.
213
241
  - **Enhanced analytics**: \`get_statistics\` returns \`decisionDensity\` (significant entries per period), \`relationshipComplexity\` (avg relationships per entry), \`activityTrend\` (period-over-period growth %), and \`causalMetrics\` (counts for blocked_by/resolved/caused).
214
- - **Importance scores**: \`get_entry_by_id\` returns an \`importance\` score (0.0-1.0) based on: significance type (30%), relationship count (35%), causal relationships (20%), and recency (15%). \`memory://significant\` sorts entries by importance.
242
+ - **Importance scores**: \`get_entry_by_id\` returns \`importance\` (0.0-1.0) and \`importanceBreakdown\` showing weighted components: significance (30%), relationships (35%), causal (20%), recency (15%). \`memory://significant\` sorts entries by importance.
243
+ - **\`inactiveThresholdDays\`**: \`get_cross_project_insights\` includes \`inactiveThresholdDays: 7\` in output, documenting the inactive project classification cutoff.
244
+ - **GitHub metadata in entries**: Entry output includes 10 GitHub fields (\`issueNumber\`, \`issueUrl\`, \`prNumber\`, \`prUrl\`, \`prStatus\`, \`projectNumber\`, \`projectOwner\`, \`workflowRunId\`, \`workflowName\`, \`workflowStatus\`) in all tool responses.
245
+ - **\`delete_entry\` on soft-deleted**: \`delete_entry(id, permanent: true)\` works on previously soft-deleted entries. Returns \`success: false\` for nonexistent entries.
215
246
 
216
247
  ## Key Resources
217
248
  | URI | Description |
218
249
  |-----|-------------|
219
250
  | \`memory://health\` | Server health, DB stats, tool filter status |
220
251
  | \`memory://briefing\` | Session context with userMessage to show user |
252
+ | \`memory://instructions\` | Full server instructions and tool reference |
221
253
  | \`memory://statistics\` | Entry counts by type and period |
222
254
  | \`memory://recent\` | 10 most recent entries |
223
255
  | \`memory://tags\` | All tags with usage counts |
224
- | \`memory://significant\` | Entries marked as milestones/breakthroughs |
256
+ | \`memory://significant\` | Entries sorted by importance score |
225
257
  | \`memory://graph/recent\` | Mermaid diagram of recent relationships |
258
+ | \`memory://graph/actions\` | CI/CD narrative graph |
259
+ | \`memory://actions/recent\` | Recent workflow runs |
260
+ | \`memory://team/recent\` | Team-shared entries |
261
+ | \`memory://github/status\` | GitHub repo overview (CI, issues, PRs, milestones) |
262
+ | \`memory://github/milestones\` | Open milestones with completion % |
263
+ | \`memory://github/insights\` | Stars, forks, and 14-day traffic summary |
226
264
  | \`memory://kanban/{n}\` | Kanban board for project number n |
265
+ | \`memory://kanban/{n}/diagram\` | Mermaid Kanban visualization |
266
+ | \`memory://milestones/{n}\` | Single milestone detail + progress |
267
+ | \`memory://projects/{n}/timeline\` | Project entries timeline |
268
+ | \`memory://issues/{n}/entries\` | Entries linked to issue n |
269
+ | \`memory://prs/{n}/entries\` | Entries linked to PR n |
270
+ | \`memory://prs/{n}/timeline\` | PR lifecycle and linked entries |
227
271
  `
228
272
 
229
273
  /**
@@ -143,6 +143,13 @@ export const ICON_PR: McpIcon = {
143
143
  sizes: ['24x24'],
144
144
  }
145
145
 
146
+ /** Flag for milestone tracking */
147
+ export const ICON_MILESTONE: McpIcon = {
148
+ src: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"%3E%3Cpath d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/%3E%3Cline x1="4" y1="22" x2="4" y2="15"/%3E%3C/svg%3E',
149
+ mimeType: 'image/svg+xml',
150
+ sizes: ['24x24'],
151
+ }
152
+
146
153
  /** Message bubble for prompts */
147
154
  export const ICON_PROMPT: McpIcon = {
148
155
  src: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"%3E%3Cpath d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/%3E%3C/svg%3E',
@@ -17,6 +17,8 @@ import type {
17
17
  EntryType,
18
18
  SignificanceType,
19
19
  RelationshipType,
20
+ ImportanceBreakdown,
21
+ ImportanceResult,
20
22
  } from '../types/index.js'
21
23
 
22
24
  // Schema SQL for initialization
@@ -125,6 +127,12 @@ export class SqliteAdapter {
125
127
  private readonly dbPath: string
126
128
  private initialized = false
127
129
 
130
+ /** Timer handle for debounced save */
131
+ private saveTimer: ReturnType<typeof setTimeout> | null = null
132
+
133
+ /** Debounce interval for batching disk writes (ms) */
134
+ private static readonly SAVE_DEBOUNCE_MS = 500
135
+
128
136
  constructor(dbPath: string) {
129
137
  this.dbPath = dbPath
130
138
  }
@@ -164,14 +172,34 @@ export class SqliteAdapter {
164
172
 
165
173
  logger.info('Database opened', { module: 'SqliteAdapter', dbPath: this.dbPath })
166
174
 
167
- // Save after initialization
168
- this.save()
175
+ // Immediate flush after initialization to persist schema
176
+ this.flushSave()
177
+ }
178
+
179
+ /**
180
+ * Schedule a debounced save to disk.
181
+ * Batches rapid mutations into a single write after SAVE_DEBOUNCE_MS.
182
+ * Used by all mutation methods (createEntry, updateEntry, etc.).
183
+ */
184
+ private scheduleSave(): void {
185
+ if (this.saveTimer !== null) {
186
+ clearTimeout(this.saveTimer)
187
+ }
188
+ this.saveTimer = setTimeout(() => {
189
+ this.flushSave()
190
+ }, SqliteAdapter.SAVE_DEBOUNCE_MS)
169
191
  }
170
192
 
171
193
  /**
172
- * Save database to disk
194
+ * Immediately flush the database to disk (synchronous).
195
+ * Cancels any pending debounced save.
196
+ * Used by close() and initialize() for guaranteed persistence.
173
197
  */
174
- private save(): void {
198
+ flushSave(): void {
199
+ if (this.saveTimer !== null) {
200
+ clearTimeout(this.saveTimer)
201
+ this.saveTimer = null
202
+ }
175
203
  if (!this.db) return
176
204
  const data = this.db.export()
177
205
  const buffer = Buffer.from(data)
@@ -179,11 +207,12 @@ export class SqliteAdapter {
179
207
  }
180
208
 
181
209
  /**
182
- * Close database connection
210
+ * Close database connection.
211
+ * Flushes any pending writes immediately before closing.
183
212
  */
184
213
  close(): void {
185
214
  if (this.db) {
186
- this.save()
215
+ this.flushSave()
187
216
  this.db.close()
188
217
  this.db = null
189
218
  }
@@ -264,7 +293,7 @@ export class SqliteAdapter {
264
293
  this.linkTagsToEntry(entryId, tags)
265
294
  }
266
295
 
267
- this.save()
296
+ this.scheduleSave()
268
297
 
269
298
  logger.info('Entry created', {
270
299
  module: 'SqliteAdapter',
@@ -297,30 +326,65 @@ export class SqliteAdapter {
297
326
  return this.rowToEntry(row)
298
327
  }
299
328
 
329
+ /**
330
+ * Get entry by ID, including soft-deleted entries.
331
+ * Used for permanent deletion of previously soft-deleted entries.
332
+ */
333
+ getEntryByIdIncludeDeleted(id: number): JournalEntry | null {
334
+ const db = this.ensureDb()
335
+ const result = db.exec(`SELECT * FROM memory_journal WHERE id = ?`, [id])
336
+
337
+ if (result.length === 0 || result[0]?.values.length === 0) return null
338
+
339
+ const columns = result[0]?.columns ?? []
340
+ const values = result[0]?.values[0] ?? []
341
+ const row = this.rowToObject(columns, values)
342
+
343
+ return this.rowToEntry(row)
344
+ }
345
+
346
+ /**
347
+ * Importance score result with scoring breakdown
348
+ */
349
+ static readonly IMPORTANCE_WEIGHTS = {
350
+ significance: 0.3,
351
+ relationships: 0.35,
352
+ causal: 0.2,
353
+ recency: 0.15,
354
+ } as const
355
+
300
356
  /**
301
357
  * Calculate importance score for an entry (0.0-1.0)
302
358
  *
303
359
  * Formula:
304
- * - significanceWeight (0.30): 1.0 if significanceType set, else 0.0
305
- * - relationshipWeight (0.35): min(relCount / 5, 1.0)
306
- * - causalWeight (0.20): min(causalCount / 3, 1.0)
307
- * - recencyWeight (0.15): max(0, 1 - daysSince / 90)
360
+ * - significance (0.30): 1.0 if significanceType set, else 0.0
361
+ * - relationships (0.35): min(relCount / 5, 1.0)
362
+ * - causal (0.20): min(causalCount / 3, 1.0)
363
+ * - recency (0.15): max(0, 1 - daysSince / 90)
364
+ *
365
+ * Returns ImportanceResult with score and component breakdown.
308
366
  */
309
- calculateImportance(entryId: number): number {
367
+ calculateImportance(entryId: number): ImportanceResult {
310
368
  const db = this.ensureDb()
369
+ const round2 = (n: number): number => Math.round(n * 100) / 100
311
370
 
312
371
  // Get entry data
313
372
  const entryResult = db.exec(
314
373
  `SELECT significance_type, timestamp FROM memory_journal WHERE id = ? AND deleted_at IS NULL`,
315
374
  [entryId]
316
375
  )
317
- if (entryResult.length === 0 || entryResult[0]?.values.length === 0) return 0
376
+ if (entryResult.length === 0 || entryResult[0]?.values.length === 0) {
377
+ return {
378
+ score: 0,
379
+ breakdown: { significance: 0, relationships: 0, causal: 0, recency: 0 },
380
+ }
381
+ }
318
382
 
319
383
  const significanceType = entryResult[0]?.values[0]?.[0] as string | null
320
384
  const timestamp = entryResult[0]?.values[0]?.[1] as string
321
385
 
322
386
  // Significance weight: 1.0 if set, else 0.0
323
- const significanceWeight = significanceType ? 1.0 : 0.0
387
+ const significanceRaw = significanceType ? 1.0 : 0.0
324
388
 
325
389
  // Relationship count (total relationships involving this entry)
326
390
  const relResult = db.exec(
@@ -328,7 +392,7 @@ export class SqliteAdapter {
328
392
  [entryId, entryId]
329
393
  )
330
394
  const relCount = (relResult[0]?.values[0]?.[0] as number) ?? 0
331
- const relationshipWeight = Math.min(relCount / 5, 1.0)
395
+ const relationshipsRaw = Math.min(relCount / 5, 1.0)
332
396
 
333
397
  // Causal relationships count
334
398
  const causalResult = db.exec(
@@ -338,23 +402,33 @@ export class SqliteAdapter {
338
402
  [entryId, entryId]
339
403
  )
340
404
  const causalCount = (causalResult[0]?.values[0]?.[0] as number) ?? 0
341
- const causalWeight = Math.min(causalCount / 3, 1.0)
405
+ const causalRaw = Math.min(causalCount / 3, 1.0)
342
406
 
343
407
  // Recency weight: decays over 90 days
344
408
  const entryDate = new Date(timestamp)
345
409
  const now = new Date()
346
410
  const daysSince = Math.floor((now.getTime() - entryDate.getTime()) / (1000 * 60 * 60 * 24))
347
- const recencyWeight = Math.max(0, 1 - daysSince / 90)
411
+ const recencyRaw = Math.max(0, 1 - daysSince / 90)
412
+
413
+ const w = SqliteAdapter.IMPORTANCE_WEIGHTS
348
414
 
349
- // Weighted sum
350
- const importance =
351
- significanceWeight * 0.3 +
352
- relationshipWeight * 0.35 +
353
- causalWeight * 0.2 +
354
- recencyWeight * 0.15
415
+ // Weighted contributions
416
+ const breakdown: ImportanceBreakdown = {
417
+ significance: round2(significanceRaw * w.significance),
418
+ relationships: round2(relationshipsRaw * w.relationships),
419
+ causal: round2(causalRaw * w.causal),
420
+ recency: round2(recencyRaw * w.recency),
421
+ }
355
422
 
356
- // Round to 2 decimal places
357
- return Math.round(importance * 100) / 100
423
+ // Total score
424
+ const score = round2(
425
+ significanceRaw * w.significance +
426
+ relationshipsRaw * w.relationships +
427
+ causalRaw * w.causal +
428
+ recencyRaw * w.recency
429
+ )
430
+
431
+ return { score, breakdown }
358
432
  }
359
433
 
360
434
  /**
@@ -382,6 +456,34 @@ export class SqliteAdapter {
382
456
  )
383
457
  }
384
458
 
459
+ /**
460
+ * Get a page of active entries for batch processing (e.g., vector index rebuild).
461
+ * Returns entries ordered by ID ascending for deterministic pagination.
462
+ */
463
+ getEntriesPage(offset: number, limit: number): JournalEntry[] {
464
+ const db = this.ensureDb()
465
+ const result = db.exec(
466
+ `SELECT * FROM memory_journal WHERE deleted_at IS NULL ORDER BY id ASC LIMIT ? OFFSET ?`,
467
+ [limit, offset]
468
+ )
469
+ if (result.length === 0) return []
470
+
471
+ const columns = result[0]?.columns ?? []
472
+ return (result[0]?.values ?? []).map((values) =>
473
+ this.rowToEntry(this.rowToObject(columns, values))
474
+ )
475
+ }
476
+
477
+ /**
478
+ * Get total count of active (non-deleted) entries.
479
+ * Used for progress reporting and pagination bounds.
480
+ */
481
+ getActiveEntryCount(): number {
482
+ const db = this.ensureDb()
483
+ const result = db.exec(`SELECT COUNT(*) FROM memory_journal WHERE deleted_at IS NULL`)
484
+ return (result[0]?.values[0]?.[0] as number) ?? 0
485
+ }
486
+
385
487
  /**
386
488
  * Update an entry
387
489
  */
@@ -425,7 +527,7 @@ export class SqliteAdapter {
425
527
  this.linkTagsToEntry(id, updates.tags)
426
528
  }
427
529
 
428
- this.save()
530
+ this.scheduleSave()
429
531
 
430
532
  logger.info('Entry updated', {
431
533
  module: 'SqliteAdapter',
@@ -438,17 +540,23 @@ export class SqliteAdapter {
438
540
 
439
541
  /**
440
542
  * Soft delete an entry
543
+ * Returns false if entry does not exist (P154: Proactive Object Existence Verification)
441
544
  */
442
545
  deleteEntry(id: number, permanent = false): boolean {
443
546
  const db = this.ensureDb()
444
547
 
548
+ // P154: Pre-check entry existence before mutation
549
+ // For permanent deletion, also look through soft-deleted entries
550
+ const entry = permanent ? this.getEntryByIdIncludeDeleted(id) : this.getEntryById(id)
551
+ if (!entry) return false
552
+
445
553
  if (permanent) {
446
554
  db.run('DELETE FROM memory_journal WHERE id = ?', [id])
447
555
  } else {
448
556
  db.run(`UPDATE memory_journal SET deleted_at = datetime('now') WHERE id = ?`, [id])
449
557
  }
450
558
 
451
- this.save()
559
+ this.scheduleSave()
452
560
  return true
453
561
  }
454
562
 
@@ -610,11 +718,11 @@ export class SqliteAdapter {
610
718
  }
611
719
 
612
720
  /**
613
- * List all tags
721
+ * List all tags with at least one usage
614
722
  */
615
723
  listTags(): Tag[] {
616
724
  const db = this.ensureDb()
617
- const result = db.exec('SELECT * FROM tags ORDER BY usage_count DESC')
725
+ const result = db.exec('SELECT * FROM tags WHERE usage_count > 0 ORDER BY usage_count DESC')
618
726
 
619
727
  if (result.length === 0) return []
620
728
 
@@ -691,7 +799,7 @@ export class SqliteAdapter {
691
799
  db.run('DELETE FROM entry_tags WHERE tag_id = ?', [sourceTagId])
692
800
  db.run('DELETE FROM tags WHERE id = ?', [sourceTagId])
693
801
 
694
- this.save()
802
+ this.scheduleSave()
695
803
 
696
804
  logger.info('Tags merged', {
697
805
  module: 'SqliteAdapter',
@@ -708,6 +816,7 @@ export class SqliteAdapter {
708
816
 
709
817
  /**
710
818
  * Link two entries
819
+ * Throws if either entry does not exist (P154: Proactive Object Existence Verification)
711
820
  */
712
821
  linkEntries(
713
822
  fromEntryId: number,
@@ -717,6 +826,16 @@ export class SqliteAdapter {
717
826
  ): Relationship {
718
827
  const db = this.ensureDb()
719
828
 
829
+ // P154: Pre-check both entries exist before creating relationship
830
+ const fromEntry = this.getEntryById(fromEntryId)
831
+ if (!fromEntry) {
832
+ throw new Error(`Source entry ${String(fromEntryId)} not found`)
833
+ }
834
+ const toEntry = this.getEntryById(toEntryId)
835
+ if (!toEntry) {
836
+ throw new Error(`Target entry ${String(toEntryId)} not found`)
837
+ }
838
+
720
839
  db.run(
721
840
  `
722
841
  INSERT INTO relationships (from_entry_id, to_entry_id, relationship_type, description)
@@ -728,7 +847,7 @@ export class SqliteAdapter {
728
847
  const result = db.exec('SELECT last_insert_rowid() as id')
729
848
  const id = result[0]?.values[0]?.[0] as number
730
849
 
731
- this.save()
850
+ this.scheduleSave()
732
851
 
733
852
  return {
734
853
  id,
@@ -1077,7 +1196,7 @@ export class SqliteAdapter {
1077
1196
  const newEntryCount = (newCountResult[0]?.values[0]?.[0] as number) ?? 0
1078
1197
 
1079
1198
  // Save to main database path
1080
- this.save()
1199
+ this.flushSave()
1081
1200
 
1082
1201
  logger.info('Database restored from backup', {
1083
1202
  module: 'SqliteAdapter',
@@ -1195,6 +1314,17 @@ export class SqliteAdapter {
1195
1314
  autoContext: row['auto_context'] as string | null,
1196
1315
  deletedAt: row['deleted_at'] as string | null,
1197
1316
  tags: this.getTagsForEntry(id),
1317
+ // GitHub integration fields
1318
+ projectNumber: (row['project_number'] as number | null) ?? null,
1319
+ projectOwner: (row['project_owner'] as string | null) ?? null,
1320
+ issueNumber: (row['issue_number'] as number | null) ?? null,
1321
+ issueUrl: (row['issue_url'] as string | null) ?? null,
1322
+ prNumber: (row['pr_number'] as number | null) ?? null,
1323
+ prUrl: (row['pr_url'] as string | null) ?? null,
1324
+ prStatus: (row['pr_status'] as string | null) ?? null,
1325
+ workflowRunId: (row['workflow_run_id'] as number | null) ?? null,
1326
+ workflowName: (row['workflow_name'] as string | null) ?? null,
1327
+ workflowStatus: (row['workflow_status'] as string | null) ?? null,
1198
1328
  }
1199
1329
  }
1200
1330
 
@@ -13,7 +13,7 @@ export type { ToolFilterConfig } from '../types/index.js'
13
13
  /**
14
14
  * Tool group definitions mapping group names to tool names
15
15
  *
16
- * All 33 tools are categorized here for filtering support.
16
+ * All 39 tools are categorized here for filtering support.
17
17
  */
18
18
  export const TOOL_GROUPS: Record<ToolGroup, string[]> = {
19
19
  core: [
@@ -45,6 +45,12 @@ export const TOOL_GROUPS: Record<ToolGroup, string[]> = {
45
45
  'move_kanban_item',
46
46
  'create_github_issue_with_entry',
47
47
  'close_github_issue_with_entry',
48
+ 'get_github_milestones',
49
+ 'get_github_milestone',
50
+ 'create_github_milestone',
51
+ 'update_github_milestone',
52
+ 'delete_github_milestone',
53
+ 'get_repo_insights',
48
54
  ],
49
55
  backup: ['backup_journal', 'list_backups', 'restore_backup', 'cleanup_backups'],
50
56
  }