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
@@ -213,10 +213,18 @@ const RelationshipOutputSchema = z.object({
213
213
  * Schema for get_entry_by_id output (entry with optional relationships).
214
214
  * Handles both success (entry found) and error (entry not found) cases.
215
215
  */
216
+ const ImportanceBreakdownSchema = z.object({
217
+ significance: z.number(),
218
+ relationships: z.number(),
219
+ causal: z.number(),
220
+ recency: z.number(),
221
+ })
222
+
216
223
  const EntryByIdOutputSchema = z.object({
217
224
  entry: EntryOutputSchema.optional(),
218
225
  relationships: z.array(RelationshipOutputSchema).optional(),
219
226
  importance: z.number().nullable().optional(), // Computed importance score (0.0-1.0)
227
+ importanceBreakdown: ImportanceBreakdownSchema.optional(), // Weighted component contributions
220
228
  error: z.string().optional(),
221
229
  })
222
230
 
@@ -362,6 +370,7 @@ const CrossProjectInsightsOutputSchema = z.object({
362
370
  last_entry_date: z.string(),
363
371
  })
364
372
  ),
373
+ inactiveThresholdDays: z.number(), // Cutoff for inactive classification
365
374
  time_distribution: z.array(
366
375
  z.object({
367
376
  project_number: z.number(),
@@ -399,6 +408,7 @@ const DeleteEntryOutputSchema = z.object({
399
408
  success: z.boolean(),
400
409
  entryId: z.number(),
401
410
  permanent: z.boolean(),
411
+ error: z.string().optional(),
402
412
  })
403
413
 
404
414
  /**
@@ -423,6 +433,13 @@ const GitHubIssueOutputSchema = z.object({
423
433
  title: z.string(),
424
434
  url: z.string(),
425
435
  state: z.enum(['OPEN', 'CLOSED']),
436
+ milestone: z
437
+ .object({
438
+ number: z.number(),
439
+ title: z.string(),
440
+ })
441
+ .nullable()
442
+ .optional(),
426
443
  })
427
444
 
428
445
  /**
@@ -593,6 +610,63 @@ const KanbanBoardOutputSchema = z.object({
593
610
  instruction: z.string().optional(),
594
611
  })
595
612
 
613
+ // ============================================================================
614
+ // Phase 3b: Repository Insights Output Schema
615
+ // ============================================================================
616
+
617
+ /**
618
+ * Schema for get_repo_insights output.
619
+ * Uses optional sections to minimize token usage.
620
+ */
621
+ const RepoInsightsOutputSchema = z.object({
622
+ owner: z.string().optional(),
623
+ repo: z.string().optional(),
624
+ section: z.string().optional(),
625
+ stars: z.number().optional(),
626
+ forks: z.number().optional(),
627
+ watchers: z.number().optional(),
628
+ openIssues: z.number().optional(),
629
+ size: z.number().optional(),
630
+ defaultBranch: z.string().optional(),
631
+ traffic: z
632
+ .object({
633
+ clones: z.object({
634
+ total: z.number(),
635
+ unique: z.number(),
636
+ dailyAvg: z.number(),
637
+ }),
638
+ views: z.object({
639
+ total: z.number(),
640
+ unique: z.number(),
641
+ dailyAvg: z.number(),
642
+ }),
643
+ period: z.string(),
644
+ })
645
+ .optional(),
646
+ referrers: z
647
+ .array(
648
+ z.object({
649
+ referrer: z.string(),
650
+ count: z.number(),
651
+ uniques: z.number(),
652
+ })
653
+ )
654
+ .optional(),
655
+ paths: z
656
+ .array(
657
+ z.object({
658
+ path: z.string(),
659
+ title: z.string(),
660
+ count: z.number(),
661
+ uniques: z.number(),
662
+ })
663
+ )
664
+ .optional(),
665
+ error: z.string().optional(),
666
+ requiresUserInput: z.boolean().optional(),
667
+ instruction: z.string().optional(),
668
+ })
669
+
596
670
  // ============================================================================
597
671
  // Phase 4: Backup Tool Output Schemas
598
672
  // ============================================================================
@@ -770,6 +844,93 @@ const CloseGitHubIssueWithEntryOutputSchema = z.object({
770
844
  instruction: z.string().optional(),
771
845
  })
772
846
 
847
+ // ============================================================================
848
+ // GitHub Milestone Output Schemas
849
+ // ============================================================================
850
+
851
+ /**
852
+ * GitHub milestone schema.
853
+ */
854
+ const GitHubMilestoneOutputSchema = z.object({
855
+ number: z.number(),
856
+ title: z.string(),
857
+ description: z.string().nullable(),
858
+ state: z.enum(['open', 'closed']),
859
+ url: z.string(),
860
+ dueOn: z.string().nullable(),
861
+ openIssues: z.number(),
862
+ closedIssues: z.number(),
863
+ completionPercentage: z.number().optional(),
864
+ createdAt: z.string(),
865
+ updatedAt: z.string(),
866
+ creator: z.string().nullable(),
867
+ })
868
+
869
+ /**
870
+ * Schema for get_github_milestones output.
871
+ */
872
+ const GitHubMilestonesListOutputSchema = z.object({
873
+ owner: z.string().optional(),
874
+ repo: z.string().optional(),
875
+ detectedOwner: z.string().nullable().optional(),
876
+ detectedRepo: z.string().nullable().optional(),
877
+ milestones: z.array(GitHubMilestoneOutputSchema).optional(),
878
+ count: z.number().optional(),
879
+ error: z.string().optional(),
880
+ requiresUserInput: z.boolean().optional(),
881
+ instruction: z.string().optional(),
882
+ })
883
+
884
+ /**
885
+ * Schema for get_github_milestone output.
886
+ */
887
+ const GitHubMilestoneResultOutputSchema = z.object({
888
+ milestone: GitHubMilestoneOutputSchema.optional(),
889
+ owner: z.string().optional(),
890
+ repo: z.string().optional(),
891
+ detectedOwner: z.string().nullable().optional(),
892
+ detectedRepo: z.string().nullable().optional(),
893
+ error: z.string().optional(),
894
+ requiresUserInput: z.boolean().optional(),
895
+ instruction: z.string().optional(),
896
+ })
897
+
898
+ /**
899
+ * Schema for create_github_milestone output.
900
+ */
901
+ const CreateMilestoneOutputSchema = z.object({
902
+ success: z.boolean().optional(),
903
+ milestone: GitHubMilestoneOutputSchema.optional(),
904
+ message: z.string().optional(),
905
+ error: z.string().optional(),
906
+ requiresUserInput: z.boolean().optional(),
907
+ instruction: z.string().optional(),
908
+ })
909
+
910
+ /**
911
+ * Schema for update_github_milestone output.
912
+ */
913
+ const UpdateMilestoneOutputSchema = z.object({
914
+ success: z.boolean().optional(),
915
+ milestone: GitHubMilestoneOutputSchema.optional(),
916
+ message: z.string().optional(),
917
+ error: z.string().optional(),
918
+ requiresUserInput: z.boolean().optional(),
919
+ instruction: z.string().optional(),
920
+ })
921
+
922
+ /**
923
+ * Schema for delete_github_milestone output.
924
+ */
925
+ const DeleteMilestoneOutputSchema = z.object({
926
+ success: z.boolean().optional(),
927
+ milestoneNumber: z.number().optional(),
928
+ message: z.string().optional(),
929
+ error: z.string().optional(),
930
+ requiresUserInput: z.boolean().optional(),
931
+ instruction: z.string().optional(),
932
+ })
933
+
773
934
  /**
774
935
  * Schema for cleanup_backups output.
775
936
  */
@@ -915,8 +1076,13 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
915
1076
  if (!entry) {
916
1077
  return Promise.resolve({ error: `Entry ${entry_id} not found` })
917
1078
  }
918
- const importance = db.calculateImportance(entry_id)
919
- const result: Record<string, unknown> = { entry, importance }
1079
+ const { score: importance, breakdown: importanceBreakdown } =
1080
+ db.calculateImportance(entry_id)
1081
+ const result: Record<string, unknown> = {
1082
+ entry,
1083
+ importance,
1084
+ importanceBreakdown,
1085
+ }
920
1086
  if (include_relationships) {
921
1087
  result['relationships'] = db.getRelationships(entry_id)
922
1088
  }
@@ -1166,6 +1332,7 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
1166
1332
  total_entries: 0,
1167
1333
  projects: [],
1168
1334
  inactive_projects: [],
1335
+ inactiveThresholdDays: 7,
1169
1336
  time_distribution: [],
1170
1337
  message: `No projects found with at least ${String(input.min_entries)} entries`,
1171
1338
  })
@@ -1242,6 +1409,7 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
1242
1409
  top_tags: projectTags[p['project_number'] as number] ?? [],
1243
1410
  })),
1244
1411
  inactive_projects: inactiveProjects,
1412
+ inactiveThresholdDays: 7,
1245
1413
  time_distribution: distribution,
1246
1414
  })
1247
1415
  },
@@ -1275,13 +1443,29 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
1275
1443
  })
1276
1444
  }
1277
1445
 
1278
- const relationship = db.linkEntries(
1279
- input.from_entry_id,
1280
- input.to_entry_id,
1281
- input.relationship_type as RelationshipType,
1282
- input.description
1283
- )
1284
- return Promise.resolve({ success: true, relationship })
1446
+ // P154: linkEntries throws for nonexistent entries
1447
+ try {
1448
+ const relationship = db.linkEntries(
1449
+ input.from_entry_id,
1450
+ input.to_entry_id,
1451
+ input.relationship_type as RelationshipType,
1452
+ input.description
1453
+ )
1454
+ return Promise.resolve({ success: true, relationship })
1455
+ } catch (error) {
1456
+ return Promise.resolve({
1457
+ success: false,
1458
+ relationship: {
1459
+ id: 0,
1460
+ fromEntryId: input.from_entry_id,
1461
+ toEntryId: input.to_entry_id,
1462
+ relationshipType: input.relationship_type,
1463
+ description: input.description ?? null,
1464
+ createdAt: '',
1465
+ },
1466
+ message: error instanceof Error ? error.message : 'Unknown error',
1467
+ })
1468
+ }
1285
1469
  },
1286
1470
  },
1287
1471
  {
@@ -1320,6 +1504,19 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
1320
1504
  let entriesResult
1321
1505
 
1322
1506
  if (input.entry_id !== undefined) {
1507
+ // P154: Pre-check entry existence to disambiguate responses
1508
+ const entry = db.getEntryById(input.entry_id)
1509
+ if (!entry) {
1510
+ return Promise.resolve({
1511
+ entry_count: 0,
1512
+ relationship_count: 0,
1513
+ root_entry: input.entry_id,
1514
+ depth: input.depth,
1515
+ mermaid: null,
1516
+ message: `Entry ${String(input.entry_id)} not found`,
1517
+ })
1518
+ }
1519
+
1323
1520
  // Use recursive CTE to get connected entries up to depth
1324
1521
  entriesResult = rawDb.exec(
1325
1522
  `
@@ -1382,8 +1579,12 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
1382
1579
 
1383
1580
  if (!entriesResult[0] || entriesResult[0].values.length === 0) {
1384
1581
  return Promise.resolve({
1385
- message: 'No entries found with relationships matching your criteria',
1582
+ entry_count: 0,
1583
+ relationship_count: 0,
1584
+ root_entry: input.entry_id ?? null,
1585
+ depth: input.depth,
1386
1586
  mermaid: null,
1587
+ message: 'No entries found with relationships matching your criteria',
1387
1588
  })
1388
1589
  }
1389
1590
 
@@ -1573,8 +1774,18 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
1573
1774
  const { entry_id, permanent } = DeleteEntrySchema.parse(params)
1574
1775
  const success = db.deleteEntry(entry_id, permanent)
1575
1776
 
1777
+ // P154: Surface structured error for nonexistent entries
1778
+ if (!success) {
1779
+ return Promise.resolve({
1780
+ success: false,
1781
+ entryId: entry_id,
1782
+ permanent,
1783
+ error: `Entry ${String(entry_id)} not found`,
1784
+ })
1785
+ }
1786
+
1576
1787
  // Remove from vector index (non-critical if fails)
1577
- if (success && vectorManager) {
1788
+ if (vectorManager) {
1578
1789
  vectorManager.removeEntry(entry_id).catch(() => {
1579
1790
  // Non-critical failure, entry already deleted from DB
1580
1791
  })
@@ -2150,6 +2361,10 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
2150
2361
  body: z.string().optional().describe('Issue body/description'),
2151
2362
  labels: z.array(z.string()).optional().describe('Labels to apply'),
2152
2363
  assignees: z.array(z.string()).optional().describe('Users to assign'),
2364
+ milestone_number: z
2365
+ .number()
2366
+ .optional()
2367
+ .describe('Milestone number to assign this issue to'),
2153
2368
  project_number: z
2154
2369
  .number()
2155
2370
  .optional()
@@ -2183,6 +2398,7 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
2183
2398
  body: z.string().optional(),
2184
2399
  labels: z.array(z.string()).optional(),
2185
2400
  assignees: z.array(z.string()).optional(),
2401
+ milestone_number: z.number().optional(),
2186
2402
  project_number: z.number().optional(),
2187
2403
  initial_status: z.string().optional(),
2188
2404
  owner: z.string().optional(),
@@ -2216,7 +2432,8 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
2216
2432
  input.title,
2217
2433
  input.body,
2218
2434
  input.labels,
2219
- input.assignees
2435
+ input.assignees,
2436
+ input.milestone_number
2220
2437
  )
2221
2438
 
2222
2439
  if (!issue) {
@@ -2544,6 +2761,458 @@ function getAllToolDefinitions(context: ToolContext): ToolDefinition[] {
2544
2761
  }
2545
2762
  },
2546
2763
  },
2764
+ // Milestone tools
2765
+ {
2766
+ name: 'get_github_milestones',
2767
+ title: 'List GitHub Milestones',
2768
+ description:
2769
+ 'List GitHub milestones for the repository with completion percentages and due dates.',
2770
+ group: 'github',
2771
+ inputSchema: z.object({
2772
+ state: z
2773
+ .enum(['open', 'closed', 'all'])
2774
+ .optional()
2775
+ .default('open')
2776
+ .describe('Filter by state (default: open)'),
2777
+ limit: z
2778
+ .number()
2779
+ .optional()
2780
+ .default(20)
2781
+ .describe('Max milestones to return (default: 20)'),
2782
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2783
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2784
+ }),
2785
+ outputSchema: GitHubMilestonesListOutputSchema,
2786
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2787
+ handler: async (params: unknown) => {
2788
+ const input = z
2789
+ .object({
2790
+ state: z.enum(['open', 'closed', 'all']).optional().default('open'),
2791
+ limit: z.number().optional().default(20),
2792
+ owner: z.string().optional(),
2793
+ repo: z.string().optional(),
2794
+ })
2795
+ .parse(params)
2796
+
2797
+ if (!github) {
2798
+ return { error: 'GitHub integration not available' }
2799
+ }
2800
+
2801
+ const repoInfo = await github.getRepoInfo()
2802
+ const detectedOwner = repoInfo.owner
2803
+ const detectedRepo = repoInfo.repo
2804
+
2805
+ const owner = input.owner ?? detectedOwner ?? undefined
2806
+ const repo = input.repo ?? detectedRepo ?? undefined
2807
+
2808
+ if (!owner || !repo) {
2809
+ return {
2810
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
2811
+ requiresUserInput: true,
2812
+ detectedOwner,
2813
+ detectedRepo,
2814
+ instruction:
2815
+ 'Ask the user: "What GitHub repository should I list milestones for? Please provide the owner and repo name (e.g., owner/repo)."',
2816
+ }
2817
+ }
2818
+
2819
+ const milestones = await github.getMilestones(owner, repo, input.state, input.limit)
2820
+ const milestonesWithPercentage = milestones.map((ms) => {
2821
+ const total = ms.openIssues + ms.closedIssues
2822
+ const completionPercentage =
2823
+ total > 0 ? Math.round((ms.closedIssues / total) * 100) : 0
2824
+ return { ...ms, completionPercentage }
2825
+ })
2826
+
2827
+ return {
2828
+ milestones: milestonesWithPercentage,
2829
+ count: milestonesWithPercentage.length,
2830
+ owner,
2831
+ repo,
2832
+ detectedOwner,
2833
+ detectedRepo,
2834
+ }
2835
+ },
2836
+ },
2837
+ {
2838
+ name: 'get_github_milestone',
2839
+ title: 'Get GitHub Milestone Details',
2840
+ description:
2841
+ 'Get detailed information about a specific GitHub milestone including progress and linked issue counts.',
2842
+ group: 'github',
2843
+ inputSchema: z.object({
2844
+ milestone_number: z.number().describe('Milestone number'),
2845
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2846
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2847
+ }),
2848
+ outputSchema: GitHubMilestoneResultOutputSchema,
2849
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2850
+ handler: async (params: unknown) => {
2851
+ const input = z
2852
+ .object({
2853
+ milestone_number: z.number(),
2854
+ owner: z.string().optional(),
2855
+ repo: z.string().optional(),
2856
+ })
2857
+ .parse(params)
2858
+
2859
+ if (!github) {
2860
+ return { error: 'GitHub integration not available' }
2861
+ }
2862
+
2863
+ const repoInfo = await github.getRepoInfo()
2864
+ const detectedOwner = repoInfo.owner
2865
+ const detectedRepo = repoInfo.repo
2866
+
2867
+ const owner = input.owner ?? detectedOwner ?? undefined
2868
+ const repo = input.repo ?? detectedRepo ?? undefined
2869
+
2870
+ if (!owner || !repo) {
2871
+ return {
2872
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
2873
+ requiresUserInput: true,
2874
+ detectedOwner,
2875
+ detectedRepo,
2876
+ instruction:
2877
+ 'Ask the user: "What GitHub repository is this milestone from? Please provide the owner and repo name (e.g., owner/repo)."',
2878
+ }
2879
+ }
2880
+
2881
+ const milestone = await github.getMilestone(owner, repo, input.milestone_number)
2882
+ if (!milestone) {
2883
+ return {
2884
+ error: `Milestone #${String(input.milestone_number)} not found`,
2885
+ owner,
2886
+ repo,
2887
+ detectedOwner,
2888
+ detectedRepo,
2889
+ }
2890
+ }
2891
+
2892
+ const total = milestone.openIssues + milestone.closedIssues
2893
+ const completionPercentage =
2894
+ total > 0 ? Math.round((milestone.closedIssues / total) * 100) : 0
2895
+
2896
+ return {
2897
+ milestone: { ...milestone, completionPercentage },
2898
+ owner,
2899
+ repo,
2900
+ detectedOwner,
2901
+ detectedRepo,
2902
+ }
2903
+ },
2904
+ },
2905
+ {
2906
+ name: 'create_github_milestone',
2907
+ title: 'Create GitHub Milestone',
2908
+ description:
2909
+ 'Create a new GitHub milestone for tracking progress toward a project goal.',
2910
+ group: 'github',
2911
+ inputSchema: z.object({
2912
+ title: z.string().min(1).describe('Milestone title'),
2913
+ description: z.string().optional().describe('Milestone description'),
2914
+ due_on: z.string().optional().describe('Due date in YYYY-MM-DD format (optional)'),
2915
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2916
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2917
+ }),
2918
+ outputSchema: CreateMilestoneOutputSchema,
2919
+ annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
2920
+ handler: async (params: unknown) => {
2921
+ const input = z
2922
+ .object({
2923
+ title: z.string().min(1),
2924
+ description: z.string().optional(),
2925
+ due_on: z.string().optional(),
2926
+ owner: z.string().optional(),
2927
+ repo: z.string().optional(),
2928
+ })
2929
+ .parse(params)
2930
+
2931
+ if (!github) {
2932
+ return { error: 'GitHub integration not available' }
2933
+ }
2934
+
2935
+ const repoInfo = await github.getRepoInfo()
2936
+ const owner = input.owner ?? repoInfo.owner ?? undefined
2937
+ const repo = input.repo ?? repoInfo.repo ?? undefined
2938
+
2939
+ if (!owner || !repo) {
2940
+ return {
2941
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
2942
+ requiresUserInput: true,
2943
+ instruction:
2944
+ 'Ask the user: "What GitHub repository should I create the milestone in?"',
2945
+ }
2946
+ }
2947
+
2948
+ // Format due_on to ISO 8601 if provided (GitHub expects YYYY-MM-DDTHH:MM:SSZ)
2949
+ const dueOn = input.due_on ? `${input.due_on}T08:00:00Z` : undefined
2950
+
2951
+ const milestone = await github.createMilestone(
2952
+ owner,
2953
+ repo,
2954
+ input.title,
2955
+ input.description,
2956
+ dueOn
2957
+ )
2958
+
2959
+ if (!milestone) {
2960
+ return {
2961
+ error: 'Failed to create milestone. Check GITHUB_TOKEN permissions.',
2962
+ }
2963
+ }
2964
+
2965
+ return {
2966
+ success: true,
2967
+ milestone: { ...milestone, completionPercentage: 0 },
2968
+ message: `Created milestone #${String(milestone.number)}: ${milestone.title}`,
2969
+ }
2970
+ },
2971
+ },
2972
+ {
2973
+ name: 'update_github_milestone',
2974
+ title: 'Update GitHub Milestone',
2975
+ description:
2976
+ 'Update a GitHub milestone (title, description, due date, or state). Use state "closed" to close a completed milestone.',
2977
+ group: 'github',
2978
+ inputSchema: z.object({
2979
+ milestone_number: z.number().describe('Milestone number to update'),
2980
+ title: z.string().optional().describe('New title'),
2981
+ description: z.string().optional().describe('New description'),
2982
+ due_on: z.string().optional().describe('New due date in YYYY-MM-DD format'),
2983
+ state: z
2984
+ .enum(['open', 'closed'])
2985
+ .optional()
2986
+ .describe('Set to "closed" to close the milestone'),
2987
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2988
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2989
+ }),
2990
+ outputSchema: UpdateMilestoneOutputSchema,
2991
+ annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
2992
+ handler: async (params: unknown) => {
2993
+ const input = z
2994
+ .object({
2995
+ milestone_number: z.number(),
2996
+ title: z.string().optional(),
2997
+ description: z.string().optional(),
2998
+ due_on: z.string().optional(),
2999
+ state: z.enum(['open', 'closed']).optional(),
3000
+ owner: z.string().optional(),
3001
+ repo: z.string().optional(),
3002
+ })
3003
+ .parse(params)
3004
+
3005
+ if (!github) {
3006
+ return { error: 'GitHub integration not available' }
3007
+ }
3008
+
3009
+ const repoInfo = await github.getRepoInfo()
3010
+ const owner = input.owner ?? repoInfo.owner ?? undefined
3011
+ const repo = input.repo ?? repoInfo.repo ?? undefined
3012
+
3013
+ if (!owner || !repo) {
3014
+ return {
3015
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
3016
+ requiresUserInput: true,
3017
+ instruction: 'Ask the user: "What GitHub repository is this milestone in?"',
3018
+ }
3019
+ }
3020
+
3021
+ const dueOn = input.due_on ? `${input.due_on}T08:00:00Z` : undefined
3022
+
3023
+ const milestone = await github.updateMilestone(
3024
+ owner,
3025
+ repo,
3026
+ input.milestone_number,
3027
+ {
3028
+ title: input.title,
3029
+ description: input.description,
3030
+ dueOn,
3031
+ state: input.state,
3032
+ }
3033
+ )
3034
+
3035
+ if (!milestone) {
3036
+ return {
3037
+ error: `Failed to update milestone #${String(input.milestone_number)}. Check that it exists and GITHUB_TOKEN has permissions.`,
3038
+ }
3039
+ }
3040
+
3041
+ const total = milestone.openIssues + milestone.closedIssues
3042
+ const completionPercentage =
3043
+ total > 0 ? Math.round((milestone.closedIssues / total) * 100) : 0
3044
+
3045
+ return {
3046
+ success: true,
3047
+ milestone: { ...milestone, completionPercentage },
3048
+ message: `Updated milestone #${String(milestone.number)}: ${milestone.title}`,
3049
+ }
3050
+ },
3051
+ },
3052
+ {
3053
+ name: 'delete_github_milestone',
3054
+ title: 'Delete GitHub Milestone',
3055
+ description:
3056
+ 'Permanently delete a GitHub milestone. Issues assigned to the milestone will be un-assigned but not deleted.',
3057
+ group: 'github',
3058
+ inputSchema: z.object({
3059
+ milestone_number: z.number().describe('Milestone number to delete'),
3060
+ confirm: z.literal(true).describe('Must be set to true to confirm deletion'),
3061
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
3062
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
3063
+ }),
3064
+ outputSchema: DeleteMilestoneOutputSchema,
3065
+ annotations: {
3066
+ readOnlyHint: false,
3067
+ idempotentHint: false,
3068
+ destructiveHint: true,
3069
+ openWorldHint: true,
3070
+ },
3071
+ handler: async (params: unknown) => {
3072
+ const input = z
3073
+ .object({
3074
+ milestone_number: z.number(),
3075
+ confirm: z.literal(true),
3076
+ owner: z.string().optional(),
3077
+ repo: z.string().optional(),
3078
+ })
3079
+ .parse(params)
3080
+
3081
+ if (!github) {
3082
+ return { error: 'GitHub integration not available' }
3083
+ }
3084
+
3085
+ const repoInfo = await github.getRepoInfo()
3086
+ const owner = input.owner ?? repoInfo.owner ?? undefined
3087
+ const repo = input.repo ?? repoInfo.repo ?? undefined
3088
+
3089
+ if (!owner || !repo) {
3090
+ return {
3091
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
3092
+ requiresUserInput: true,
3093
+ instruction: 'Ask the user: "What GitHub repository is this milestone in?"',
3094
+ }
3095
+ }
3096
+
3097
+ const result = await github.deleteMilestone(owner, repo, input.milestone_number)
3098
+
3099
+ if (!result.success) {
3100
+ return {
3101
+ success: false,
3102
+ milestoneNumber: input.milestone_number,
3103
+ message: `Failed to delete milestone #${String(input.milestone_number)}`,
3104
+ error: result.error ?? undefined,
3105
+ }
3106
+ }
3107
+
3108
+ return {
3109
+ success: true,
3110
+ milestoneNumber: input.milestone_number,
3111
+ message: `Deleted milestone #${String(input.milestone_number)}`,
3112
+ }
3113
+ },
3114
+ },
3115
+ // Repository insights tool
3116
+ {
3117
+ name: 'get_repo_insights',
3118
+ title: 'Repository Insights',
3119
+ description:
3120
+ 'Get repository insights: stars, forks, traffic (clones/views), referrers, and popular paths. Use "sections" to control token usage: stars (~50 tokens), traffic (~100), referrers (~100), paths (~100), or all (~350).',
3121
+ group: 'github',
3122
+ inputSchema: z.object({
3123
+ sections: z
3124
+ .enum(['stars', 'traffic', 'referrers', 'paths', 'all'])
3125
+ .optional()
3126
+ .default('stars')
3127
+ .describe(
3128
+ 'Data section to return (default: stars). Use "all" for full payload.'
3129
+ ),
3130
+ owner: z
3131
+ .string()
3132
+ .optional()
3133
+ .describe('Repository owner - LEAVE EMPTY to auto-detect'),
3134
+ repo: z
3135
+ .string()
3136
+ .optional()
3137
+ .describe('Repository name - LEAVE EMPTY to auto-detect'),
3138
+ }),
3139
+ outputSchema: RepoInsightsOutputSchema,
3140
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
3141
+ handler: async (params: unknown) => {
3142
+ const input = z
3143
+ .object({
3144
+ sections: z
3145
+ .enum(['stars', 'traffic', 'referrers', 'paths', 'all'])
3146
+ .optional()
3147
+ .default('stars'),
3148
+ owner: z.string().optional(),
3149
+ repo: z.string().optional(),
3150
+ })
3151
+ .parse(params)
3152
+
3153
+ if (!github) {
3154
+ return { error: 'GitHub integration not available' }
3155
+ }
3156
+
3157
+ const repoInfo = await github.getRepoInfo()
3158
+ const owner = input.owner ?? repoInfo.owner ?? undefined
3159
+ const repo = input.repo ?? repoInfo.repo ?? undefined
3160
+
3161
+ if (!owner || !repo) {
3162
+ return {
3163
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
3164
+ requiresUserInput: true,
3165
+ instruction:
3166
+ 'Ask the user: "What GitHub repository should I get insights for? Please provide the owner and repo name (e.g., owner/repo)."',
3167
+ }
3168
+ }
3169
+
3170
+ const section = input.sections
3171
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- building response dynamically
3172
+ const result: Record<string, any> = {
3173
+ owner,
3174
+ repo,
3175
+ section,
3176
+ }
3177
+
3178
+ // Stars section (default)
3179
+ if (section === 'stars' || section === 'all') {
3180
+ const stats = await github.getRepoStats(owner, repo)
3181
+ if (stats) {
3182
+ result['stars'] = stats.stars
3183
+ result['forks'] = stats.forks
3184
+ result['watchers'] = stats.watchers
3185
+ result['openIssues'] = stats.openIssues
3186
+ if (section === 'all') {
3187
+ result['size'] = stats.size
3188
+ result['defaultBranch'] = stats.defaultBranch
3189
+ }
3190
+ }
3191
+ }
3192
+
3193
+ // Traffic section
3194
+ if (section === 'traffic' || section === 'all') {
3195
+ const traffic = await github.getTrafficData(owner, repo)
3196
+ if (traffic) {
3197
+ result['traffic'] = traffic
3198
+ }
3199
+ }
3200
+
3201
+ // Referrers section
3202
+ if (section === 'referrers' || section === 'all') {
3203
+ const referrers = await github.getTopReferrers(owner, repo, 5)
3204
+ result['referrers'] = referrers
3205
+ }
3206
+
3207
+ // Paths section
3208
+ if (section === 'paths' || section === 'all') {
3209
+ const paths = await github.getPopularPaths(owner, repo, 5)
3210
+ result['paths'] = paths
3211
+ }
3212
+
3213
+ return result
3214
+ },
3215
+ },
2547
3216
  // Backup tools
2548
3217
  {
2549
3218
  name: 'backup_journal',