memory-journal-mcp 4.3.0 → 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 (109) 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 +14 -12
  11. package/README.md +68 -33
  12. package/SECURITY.md +225 -220
  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 +143 -43
  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 +595 -8
  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/progress-utils.d.ts +18 -3
  55. package/dist/utils/progress-utils.d.ts.map +1 -1
  56. package/dist/utils/progress-utils.js.map +1 -1
  57. package/dist/utils/security-utils.d.ts +91 -0
  58. package/dist/utils/security-utils.d.ts.map +1 -0
  59. package/dist/utils/security-utils.js +184 -0
  60. package/dist/utils/security-utils.js.map +1 -0
  61. package/dist/vector/VectorSearchManager.d.ts +2 -1
  62. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  63. package/dist/vector/VectorSearchManager.js +100 -34
  64. package/dist/vector/VectorSearchManager.js.map +1 -1
  65. package/docker-compose.yml +46 -37
  66. package/mcp-config-example.json +0 -2
  67. package/package.json +21 -14
  68. package/releases/v4.3.1.md +69 -0
  69. package/releases/v4.4.0.md +120 -0
  70. package/server.json +3 -3
  71. package/src/cli.ts +11 -0
  72. package/src/constants/ServerInstructions.ts +70 -26
  73. package/src/constants/icons.ts +7 -0
  74. package/src/database/SqliteAdapter.ts +165 -44
  75. package/src/filtering/ToolFilter.ts +7 -1
  76. package/src/github/GitHubIntegration.ts +588 -8
  77. package/src/handlers/prompts/index.ts +1 -0
  78. package/src/handlers/resources/index.ts +318 -12
  79. package/src/handlers/tools/index.ts +686 -13
  80. package/src/server/McpServer.ts +79 -37
  81. package/src/types/index.ts +98 -0
  82. package/src/utils/logger.ts +10 -1
  83. package/src/utils/progress-utils.ts +17 -6
  84. package/src/utils/security-utils.ts +205 -0
  85. package/src/vector/VectorSearchManager.ts +110 -39
  86. package/tests/constants/icons.test.ts +102 -0
  87. package/tests/constants/server-instructions.test.ts +549 -0
  88. package/tests/database/sqlite-adapter.bench.ts +63 -0
  89. package/tests/database/sqlite-adapter.test.ts +555 -0
  90. package/tests/filtering/tool-filter.test.ts +266 -0
  91. package/tests/github/github-integration.test.ts +1024 -0
  92. package/tests/handlers/github-resource-handlers.test.ts +473 -0
  93. package/tests/handlers/github-tool-handlers.test.ts +556 -0
  94. package/tests/handlers/prompt-handlers.test.ts +91 -0
  95. package/tests/handlers/resource-handlers.test.ts +339 -0
  96. package/tests/handlers/tool-handlers.test.ts +497 -0
  97. package/tests/handlers/vector-tool-handlers.test.ts +238 -0
  98. package/tests/security/sql-injection.test.ts +347 -0
  99. package/tests/server/mcp-server.bench.ts +55 -0
  100. package/tests/server/mcp-server.test.ts +675 -0
  101. package/tests/utils/logger.test.ts +180 -0
  102. package/tests/utils/mcp-logger.test.ts +212 -0
  103. package/tests/utils/progress-utils.test.ts +156 -0
  104. package/tests/utils/security-utils.test.ts +82 -0
  105. package/tests/vector/vector-search-manager.test.ts +335 -0
  106. package/tests/vector/vector-search.bench.ts +53 -0
  107. package/vitest.config.ts +15 -0
  108. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
  109. package/.github/workflows/dependabot-auto-merge.yml +0 -42
package/src/cli.ts CHANGED
@@ -15,37 +15,47 @@ program
15
15
  .version(pkg.version)
16
16
  .option('--transport <type>', 'Transport type: stdio or http', 'stdio')
17
17
  .option('--port <number>', 'HTTP port (for http transport)', '3000')
18
+ .option('--server-host <host>', 'Server bind host for HTTP transport (default: localhost)')
18
19
  .option('--stateless', 'Use stateless HTTP mode (no session management)')
19
20
  .option('--db <path>', 'Database path', './memory_journal.db')
20
21
  .option('--tool-filter <filter>', 'Tool filter string (e.g., "starter", "core,search")')
21
22
  .option('--default-project <number>', 'Default GitHub Project number')
22
23
  .option('--auto-rebuild-index', 'Rebuild vector index on server startup')
24
+ .option('--cors-origin <origin>', 'CORS allowed origin for HTTP transport (default: *)')
23
25
  .option('--log-level <level>', 'Log level: debug, info, warning, error', 'info')
24
26
  .action(
25
27
  async (options: {
26
28
  transport: string
27
29
  port: string
30
+ serverHost?: string
28
31
  stateless?: boolean
29
32
  db: string
30
33
  toolFilter?: string
31
34
  defaultProject: string
32
35
  autoRebuildIndex?: boolean
36
+ corsOrigin?: string
33
37
  logLevel: string
34
38
  }) => {
35
39
  // Set log level
36
40
  logger.setLevel(options.logLevel as 'debug' | 'info' | 'warning' | 'error')
37
41
 
42
+ // Resolve host: CLI flag > env var > default (localhost)
43
+ const host =
44
+ options.serverHost ?? process.env['MCP_HOST'] ?? process.env['HOST'] ?? undefined
45
+
38
46
  logger.info('Starting Memory Journal MCP Server', {
39
47
  module: 'CLI',
40
48
  transport: options.transport,
41
49
  stateless: options.stateless ?? false,
42
50
  db: options.db,
51
+ ...(host ? { host } : {}),
43
52
  })
44
53
 
45
54
  try {
46
55
  await createServer({
47
56
  transport: options.transport as 'stdio' | 'http',
48
57
  port: parseInt(options.port, 10),
58
+ host,
49
59
  statelessHttp: options.stateless === true,
50
60
  dbPath: options.db,
51
61
  toolFilter: options.toolFilter,
@@ -56,6 +66,7 @@ program
56
66
  : undefined,
57
67
  autoRebuildIndex:
58
68
  options.autoRebuildIndex ?? process.env['AUTO_REBUILD_INDEX'] === 'true',
69
+ corsOrigin: options.corsOrigin,
59
70
  })
60
71
  } catch (error) {
61
72
  logger.error('Failed to start server', {
@@ -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',
@@ -9,6 +9,7 @@ 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 { validateDateFormatPattern } from '../utils/security-utils.js'
12
13
  import type {
13
14
  JournalEntry,
14
15
  Tag,
@@ -16,6 +17,8 @@ import type {
16
17
  EntryType,
17
18
  SignificanceType,
18
19
  RelationshipType,
20
+ ImportanceBreakdown,
21
+ ImportanceResult,
19
22
  } from '../types/index.js'
20
23
 
21
24
  // Schema SQL for initialization
@@ -124,6 +127,12 @@ export class SqliteAdapter {
124
127
  private readonly dbPath: string
125
128
  private initialized = false
126
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
+
127
136
  constructor(dbPath: string) {
128
137
  this.dbPath = dbPath
129
138
  }
@@ -163,14 +172,34 @@ export class SqliteAdapter {
163
172
 
164
173
  logger.info('Database opened', { module: 'SqliteAdapter', dbPath: this.dbPath })
165
174
 
166
- // Save after initialization
167
- 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)
168
191
  }
169
192
 
170
193
  /**
171
- * 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.
172
197
  */
173
- private save(): void {
198
+ flushSave(): void {
199
+ if (this.saveTimer !== null) {
200
+ clearTimeout(this.saveTimer)
201
+ this.saveTimer = null
202
+ }
174
203
  if (!this.db) return
175
204
  const data = this.db.export()
176
205
  const buffer = Buffer.from(data)
@@ -178,11 +207,12 @@ export class SqliteAdapter {
178
207
  }
179
208
 
180
209
  /**
181
- * Close database connection
210
+ * Close database connection.
211
+ * Flushes any pending writes immediately before closing.
182
212
  */
183
213
  close(): void {
184
214
  if (this.db) {
185
- this.save()
215
+ this.flushSave()
186
216
  this.db.close()
187
217
  this.db = null
188
218
  }
@@ -263,7 +293,7 @@ export class SqliteAdapter {
263
293
  this.linkTagsToEntry(entryId, tags)
264
294
  }
265
295
 
266
- this.save()
296
+ this.scheduleSave()
267
297
 
268
298
  logger.info('Entry created', {
269
299
  module: 'SqliteAdapter',
@@ -296,30 +326,65 @@ export class SqliteAdapter {
296
326
  return this.rowToEntry(row)
297
327
  }
298
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
+
299
356
  /**
300
357
  * Calculate importance score for an entry (0.0-1.0)
301
358
  *
302
359
  * Formula:
303
- * - significanceWeight (0.30): 1.0 if significanceType set, else 0.0
304
- * - relationshipWeight (0.35): min(relCount / 5, 1.0)
305
- * - causalWeight (0.20): min(causalCount / 3, 1.0)
306
- * - 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.
307
366
  */
308
- calculateImportance(entryId: number): number {
367
+ calculateImportance(entryId: number): ImportanceResult {
309
368
  const db = this.ensureDb()
369
+ const round2 = (n: number): number => Math.round(n * 100) / 100
310
370
 
311
371
  // Get entry data
312
372
  const entryResult = db.exec(
313
373
  `SELECT significance_type, timestamp FROM memory_journal WHERE id = ? AND deleted_at IS NULL`,
314
374
  [entryId]
315
375
  )
316
- 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
+ }
317
382
 
318
383
  const significanceType = entryResult[0]?.values[0]?.[0] as string | null
319
384
  const timestamp = entryResult[0]?.values[0]?.[1] as string
320
385
 
321
386
  // Significance weight: 1.0 if set, else 0.0
322
- const significanceWeight = significanceType ? 1.0 : 0.0
387
+ const significanceRaw = significanceType ? 1.0 : 0.0
323
388
 
324
389
  // Relationship count (total relationships involving this entry)
325
390
  const relResult = db.exec(
@@ -327,7 +392,7 @@ export class SqliteAdapter {
327
392
  [entryId, entryId]
328
393
  )
329
394
  const relCount = (relResult[0]?.values[0]?.[0] as number) ?? 0
330
- const relationshipWeight = Math.min(relCount / 5, 1.0)
395
+ const relationshipsRaw = Math.min(relCount / 5, 1.0)
331
396
 
332
397
  // Causal relationships count
333
398
  const causalResult = db.exec(
@@ -337,23 +402,33 @@ export class SqliteAdapter {
337
402
  [entryId, entryId]
338
403
  )
339
404
  const causalCount = (causalResult[0]?.values[0]?.[0] as number) ?? 0
340
- const causalWeight = Math.min(causalCount / 3, 1.0)
405
+ const causalRaw = Math.min(causalCount / 3, 1.0)
341
406
 
342
407
  // Recency weight: decays over 90 days
343
408
  const entryDate = new Date(timestamp)
344
409
  const now = new Date()
345
410
  const daysSince = Math.floor((now.getTime() - entryDate.getTime()) / (1000 * 60 * 60 * 24))
346
- const recencyWeight = Math.max(0, 1 - daysSince / 90)
411
+ const recencyRaw = Math.max(0, 1 - daysSince / 90)
347
412
 
348
- // Weighted sum
349
- const importance =
350
- significanceWeight * 0.3 +
351
- relationshipWeight * 0.35 +
352
- causalWeight * 0.2 +
353
- recencyWeight * 0.15
413
+ const w = SqliteAdapter.IMPORTANCE_WEIGHTS
354
414
 
355
- // Round to 2 decimal places
356
- return Math.round(importance * 100) / 100
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
+ }
422
+
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 }
357
432
  }
358
433
 
359
434
  /**
@@ -381,6 +456,34 @@ export class SqliteAdapter {
381
456
  )
382
457
  }
383
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
+
384
487
  /**
385
488
  * Update an entry
386
489
  */
@@ -424,7 +527,7 @@ export class SqliteAdapter {
424
527
  this.linkTagsToEntry(id, updates.tags)
425
528
  }
426
529
 
427
- this.save()
530
+ this.scheduleSave()
428
531
 
429
532
  logger.info('Entry updated', {
430
533
  module: 'SqliteAdapter',
@@ -437,17 +540,23 @@ export class SqliteAdapter {
437
540
 
438
541
  /**
439
542
  * Soft delete an entry
543
+ * Returns false if entry does not exist (P154: Proactive Object Existence Verification)
440
544
  */
441
545
  deleteEntry(id: number, permanent = false): boolean {
442
546
  const db = this.ensureDb()
443
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
+
444
553
  if (permanent) {
445
554
  db.run('DELETE FROM memory_journal WHERE id = ?', [id])
446
555
  } else {
447
556
  db.run(`UPDATE memory_journal SET deleted_at = datetime('now') WHERE id = ?`, [id])
448
557
  }
449
558
 
450
- this.save()
559
+ this.scheduleSave()
451
560
  return true
452
561
  }
453
562
 
@@ -609,11 +718,11 @@ export class SqliteAdapter {
609
718
  }
610
719
 
611
720
  /**
612
- * List all tags
721
+ * List all tags with at least one usage
613
722
  */
614
723
  listTags(): Tag[] {
615
724
  const db = this.ensureDb()
616
- 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')
617
726
 
618
727
  if (result.length === 0) return []
619
728
 
@@ -690,7 +799,7 @@ export class SqliteAdapter {
690
799
  db.run('DELETE FROM entry_tags WHERE tag_id = ?', [sourceTagId])
691
800
  db.run('DELETE FROM tags WHERE id = ?', [sourceTagId])
692
801
 
693
- this.save()
802
+ this.scheduleSave()
694
803
 
695
804
  logger.info('Tags merged', {
696
805
  module: 'SqliteAdapter',
@@ -707,6 +816,7 @@ export class SqliteAdapter {
707
816
 
708
817
  /**
709
818
  * Link two entries
819
+ * Throws if either entry does not exist (P154: Proactive Object Existence Verification)
710
820
  */
711
821
  linkEntries(
712
822
  fromEntryId: number,
@@ -716,6 +826,16 @@ export class SqliteAdapter {
716
826
  ): Relationship {
717
827
  const db = this.ensureDb()
718
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
+
719
839
  db.run(
720
840
  `
721
841
  INSERT INTO relationships (from_entry_id, to_entry_id, relationship_type, description)
@@ -727,7 +847,7 @@ export class SqliteAdapter {
727
847
  const result = db.exec('SELECT last_insert_rowid() as id')
728
848
  const id = result[0]?.values[0]?.[0] as number
729
849
 
730
- this.save()
850
+ this.scheduleSave()
731
851
 
732
852
  return {
733
853
  id,
@@ -816,18 +936,8 @@ export class SqliteAdapter {
816
936
  entriesByType[row[0] as string] = row[1] as number
817
937
  }
818
938
 
819
- // By period
820
- let dateFormat: string
821
- switch (groupBy) {
822
- case 'day':
823
- dateFormat = '%Y-%m-%d'
824
- break
825
- case 'month':
826
- dateFormat = '%Y-%m'
827
- break
828
- default:
829
- dateFormat = '%Y-W%W'
830
- }
939
+ // By period - use validated date format pattern (defense-in-depth)
940
+ const dateFormat = validateDateFormatPattern(groupBy)
831
941
 
832
942
  const byPeriodResult = db.exec(`
833
943
  SELECT strftime('${dateFormat}', timestamp) as period, COUNT(*) as count
@@ -1086,7 +1196,7 @@ export class SqliteAdapter {
1086
1196
  const newEntryCount = (newCountResult[0]?.values[0]?.[0] as number) ?? 0
1087
1197
 
1088
1198
  // Save to main database path
1089
- this.save()
1199
+ this.flushSave()
1090
1200
 
1091
1201
  logger.info('Database restored from backup', {
1092
1202
  module: 'SqliteAdapter',
@@ -1204,6 +1314,17 @@ export class SqliteAdapter {
1204
1314
  autoContext: row['auto_context'] as string | null,
1205
1315
  deletedAt: row['deleted_at'] as string | null,
1206
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,
1207
1328
  }
1208
1329
  }
1209
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
  }