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
@@ -168,10 +168,17 @@ const RelationshipOutputSchema = z.object({
168
168
  * Schema for get_entry_by_id output (entry with optional relationships).
169
169
  * Handles both success (entry found) and error (entry not found) cases.
170
170
  */
171
+ const ImportanceBreakdownSchema = z.object({
172
+ significance: z.number(),
173
+ relationships: z.number(),
174
+ causal: z.number(),
175
+ recency: z.number(),
176
+ });
171
177
  const EntryByIdOutputSchema = z.object({
172
178
  entry: EntryOutputSchema.optional(),
173
179
  relationships: z.array(RelationshipOutputSchema).optional(),
174
180
  importance: z.number().nullable().optional(), // Computed importance score (0.0-1.0)
181
+ importanceBreakdown: ImportanceBreakdownSchema.optional(), // Weighted component contributions
175
182
  error: z.string().optional(),
176
183
  });
177
184
  /**
@@ -300,6 +307,7 @@ const CrossProjectInsightsOutputSchema = z.object({
300
307
  project_number: z.number(),
301
308
  last_entry_date: z.string(),
302
309
  })),
310
+ inactiveThresholdDays: z.number(), // Cutoff for inactive classification
303
311
  time_distribution: z.array(z.object({
304
312
  project_number: z.number(),
305
313
  percentage: z.string(),
@@ -331,6 +339,7 @@ const DeleteEntryOutputSchema = z.object({
331
339
  success: z.boolean(),
332
340
  entryId: z.number(),
333
341
  permanent: z.boolean(),
342
+ error: z.string().optional(),
334
343
  });
335
344
  /**
336
345
  * Schema for link_entries output.
@@ -352,6 +361,13 @@ const GitHubIssueOutputSchema = z.object({
352
361
  title: z.string(),
353
362
  url: z.string(),
354
363
  state: z.enum(['OPEN', 'CLOSED']),
364
+ milestone: z
365
+ .object({
366
+ number: z.number(),
367
+ title: z.string(),
368
+ })
369
+ .nullable()
370
+ .optional(),
355
371
  });
356
372
  /**
357
373
  * GitHub issue details schema (extended).
@@ -510,6 +526,57 @@ const KanbanBoardOutputSchema = z.object({
510
526
  instruction: z.string().optional(),
511
527
  });
512
528
  // ============================================================================
529
+ // Phase 3b: Repository Insights Output Schema
530
+ // ============================================================================
531
+ /**
532
+ * Schema for get_repo_insights output.
533
+ * Uses optional sections to minimize token usage.
534
+ */
535
+ const RepoInsightsOutputSchema = z.object({
536
+ owner: z.string().optional(),
537
+ repo: z.string().optional(),
538
+ section: z.string().optional(),
539
+ stars: z.number().optional(),
540
+ forks: z.number().optional(),
541
+ watchers: z.number().optional(),
542
+ openIssues: z.number().optional(),
543
+ size: z.number().optional(),
544
+ defaultBranch: z.string().optional(),
545
+ traffic: z
546
+ .object({
547
+ clones: z.object({
548
+ total: z.number(),
549
+ unique: z.number(),
550
+ dailyAvg: z.number(),
551
+ }),
552
+ views: z.object({
553
+ total: z.number(),
554
+ unique: z.number(),
555
+ dailyAvg: z.number(),
556
+ }),
557
+ period: z.string(),
558
+ })
559
+ .optional(),
560
+ referrers: z
561
+ .array(z.object({
562
+ referrer: z.string(),
563
+ count: z.number(),
564
+ uniques: z.number(),
565
+ }))
566
+ .optional(),
567
+ paths: z
568
+ .array(z.object({
569
+ path: z.string(),
570
+ title: z.string(),
571
+ count: z.number(),
572
+ uniques: z.number(),
573
+ }))
574
+ .optional(),
575
+ error: z.string().optional(),
576
+ requiresUserInput: z.boolean().optional(),
577
+ instruction: z.string().optional(),
578
+ });
579
+ // ============================================================================
513
580
  // Phase 4: Backup Tool Output Schemas
514
581
  // ============================================================================
515
582
  /**
@@ -673,6 +740,86 @@ const CloseGitHubIssueWithEntryOutputSchema = z.object({
673
740
  requiresUserInput: z.boolean().optional(),
674
741
  instruction: z.string().optional(),
675
742
  });
743
+ // ============================================================================
744
+ // GitHub Milestone Output Schemas
745
+ // ============================================================================
746
+ /**
747
+ * GitHub milestone schema.
748
+ */
749
+ const GitHubMilestoneOutputSchema = z.object({
750
+ number: z.number(),
751
+ title: z.string(),
752
+ description: z.string().nullable(),
753
+ state: z.enum(['open', 'closed']),
754
+ url: z.string(),
755
+ dueOn: z.string().nullable(),
756
+ openIssues: z.number(),
757
+ closedIssues: z.number(),
758
+ completionPercentage: z.number().optional(),
759
+ createdAt: z.string(),
760
+ updatedAt: z.string(),
761
+ creator: z.string().nullable(),
762
+ });
763
+ /**
764
+ * Schema for get_github_milestones output.
765
+ */
766
+ const GitHubMilestonesListOutputSchema = z.object({
767
+ owner: z.string().optional(),
768
+ repo: z.string().optional(),
769
+ detectedOwner: z.string().nullable().optional(),
770
+ detectedRepo: z.string().nullable().optional(),
771
+ milestones: z.array(GitHubMilestoneOutputSchema).optional(),
772
+ count: z.number().optional(),
773
+ error: z.string().optional(),
774
+ requiresUserInput: z.boolean().optional(),
775
+ instruction: z.string().optional(),
776
+ });
777
+ /**
778
+ * Schema for get_github_milestone output.
779
+ */
780
+ const GitHubMilestoneResultOutputSchema = z.object({
781
+ milestone: GitHubMilestoneOutputSchema.optional(),
782
+ owner: z.string().optional(),
783
+ repo: z.string().optional(),
784
+ detectedOwner: z.string().nullable().optional(),
785
+ detectedRepo: z.string().nullable().optional(),
786
+ error: z.string().optional(),
787
+ requiresUserInput: z.boolean().optional(),
788
+ instruction: z.string().optional(),
789
+ });
790
+ /**
791
+ * Schema for create_github_milestone output.
792
+ */
793
+ const CreateMilestoneOutputSchema = z.object({
794
+ success: z.boolean().optional(),
795
+ milestone: GitHubMilestoneOutputSchema.optional(),
796
+ message: z.string().optional(),
797
+ error: z.string().optional(),
798
+ requiresUserInput: z.boolean().optional(),
799
+ instruction: z.string().optional(),
800
+ });
801
+ /**
802
+ * Schema for update_github_milestone output.
803
+ */
804
+ const UpdateMilestoneOutputSchema = z.object({
805
+ success: z.boolean().optional(),
806
+ milestone: GitHubMilestoneOutputSchema.optional(),
807
+ message: z.string().optional(),
808
+ error: z.string().optional(),
809
+ requiresUserInput: z.boolean().optional(),
810
+ instruction: z.string().optional(),
811
+ });
812
+ /**
813
+ * Schema for delete_github_milestone output.
814
+ */
815
+ const DeleteMilestoneOutputSchema = z.object({
816
+ success: z.boolean().optional(),
817
+ milestoneNumber: z.number().optional(),
818
+ message: z.string().optional(),
819
+ error: z.string().optional(),
820
+ requiresUserInput: z.boolean().optional(),
821
+ instruction: z.string().optional(),
822
+ });
676
823
  /**
677
824
  * Schema for cleanup_backups output.
678
825
  */
@@ -791,8 +938,12 @@ function getAllToolDefinitions(context) {
791
938
  if (!entry) {
792
939
  return Promise.resolve({ error: `Entry ${entry_id} not found` });
793
940
  }
794
- const importance = db.calculateImportance(entry_id);
795
- const result = { entry, importance };
941
+ const { score: importance, breakdown: importanceBreakdown } = db.calculateImportance(entry_id);
942
+ const result = {
943
+ entry,
944
+ importance,
945
+ importanceBreakdown,
946
+ };
796
947
  if (include_relationships) {
797
948
  result['relationships'] = db.getRelationships(entry_id);
798
949
  }
@@ -1018,6 +1169,7 @@ function getAllToolDefinitions(context) {
1018
1169
  total_entries: 0,
1019
1170
  projects: [],
1020
1171
  inactive_projects: [],
1172
+ inactiveThresholdDays: 7,
1021
1173
  time_distribution: [],
1022
1174
  message: `No projects found with at least ${String(input.min_entries)} entries`,
1023
1175
  });
@@ -1078,6 +1230,7 @@ function getAllToolDefinitions(context) {
1078
1230
  top_tags: projectTags[p['project_number']] ?? [],
1079
1231
  })),
1080
1232
  inactive_projects: inactiveProjects,
1233
+ inactiveThresholdDays: 7,
1081
1234
  time_distribution: distribution,
1082
1235
  });
1083
1236
  },
@@ -1105,8 +1258,25 @@ function getAllToolDefinitions(context) {
1105
1258
  message: 'Relationship already exists',
1106
1259
  });
1107
1260
  }
1108
- const relationship = db.linkEntries(input.from_entry_id, input.to_entry_id, input.relationship_type, input.description);
1109
- return Promise.resolve({ success: true, relationship });
1261
+ // P154: linkEntries throws for nonexistent entries
1262
+ try {
1263
+ const relationship = db.linkEntries(input.from_entry_id, input.to_entry_id, input.relationship_type, input.description);
1264
+ return Promise.resolve({ success: true, relationship });
1265
+ }
1266
+ catch (error) {
1267
+ return Promise.resolve({
1268
+ success: false,
1269
+ relationship: {
1270
+ id: 0,
1271
+ fromEntryId: input.from_entry_id,
1272
+ toEntryId: input.to_entry_id,
1273
+ relationshipType: input.relationship_type,
1274
+ description: input.description ?? null,
1275
+ createdAt: '',
1276
+ },
1277
+ message: error instanceof Error ? error.message : 'Unknown error',
1278
+ });
1279
+ }
1110
1280
  },
1111
1281
  },
1112
1282
  {
@@ -1143,6 +1313,18 @@ function getAllToolDefinitions(context) {
1143
1313
  const rawDb = db.getRawDb();
1144
1314
  let entriesResult;
1145
1315
  if (input.entry_id !== undefined) {
1316
+ // P154: Pre-check entry existence to disambiguate responses
1317
+ const entry = db.getEntryById(input.entry_id);
1318
+ if (!entry) {
1319
+ return Promise.resolve({
1320
+ entry_count: 0,
1321
+ relationship_count: 0,
1322
+ root_entry: input.entry_id,
1323
+ depth: input.depth,
1324
+ mermaid: null,
1325
+ message: `Entry ${String(input.entry_id)} not found`,
1326
+ });
1327
+ }
1146
1328
  // Use recursive CTE to get connected entries up to depth
1147
1329
  entriesResult = rawDb.exec(`
1148
1330
  WITH RECURSIVE connected_entries(id, distance) AS (
@@ -1197,8 +1379,12 @@ function getAllToolDefinitions(context) {
1197
1379
  }
1198
1380
  if (!entriesResult[0] || entriesResult[0].values.length === 0) {
1199
1381
  return Promise.resolve({
1200
- message: 'No entries found with relationships matching your criteria',
1382
+ entry_count: 0,
1383
+ relationship_count: 0,
1384
+ root_entry: input.entry_id ?? null,
1385
+ depth: input.depth,
1201
1386
  mermaid: null,
1387
+ message: 'No entries found with relationships matching your criteria',
1202
1388
  });
1203
1389
  }
1204
1390
  // Build entries map
@@ -1356,8 +1542,17 @@ function getAllToolDefinitions(context) {
1356
1542
  handler: (params) => {
1357
1543
  const { entry_id, permanent } = DeleteEntrySchema.parse(params);
1358
1544
  const success = db.deleteEntry(entry_id, permanent);
1545
+ // P154: Surface structured error for nonexistent entries
1546
+ if (!success) {
1547
+ return Promise.resolve({
1548
+ success: false,
1549
+ entryId: entry_id,
1550
+ permanent,
1551
+ error: `Entry ${String(entry_id)} not found`,
1552
+ });
1553
+ }
1359
1554
  // Remove from vector index (non-critical if fails)
1360
- if (success && vectorManager) {
1555
+ if (vectorManager) {
1361
1556
  vectorManager.removeEntry(entry_id).catch(() => {
1362
1557
  // Non-critical failure, entry already deleted from DB
1363
1558
  });
@@ -1862,6 +2057,10 @@ function getAllToolDefinitions(context) {
1862
2057
  body: z.string().optional().describe('Issue body/description'),
1863
2058
  labels: z.array(z.string()).optional().describe('Labels to apply'),
1864
2059
  assignees: z.array(z.string()).optional().describe('Users to assign'),
2060
+ milestone_number: z
2061
+ .number()
2062
+ .optional()
2063
+ .describe('Milestone number to assign this issue to'),
1865
2064
  project_number: z
1866
2065
  .number()
1867
2066
  .optional()
@@ -1893,6 +2092,7 @@ function getAllToolDefinitions(context) {
1893
2092
  body: z.string().optional(),
1894
2093
  labels: z.array(z.string()).optional(),
1895
2094
  assignees: z.array(z.string()).optional(),
2095
+ milestone_number: z.number().optional(),
1896
2096
  project_number: z.number().optional(),
1897
2097
  initial_status: z.string().optional(),
1898
2098
  owner: z.string().optional(),
@@ -1916,7 +2116,7 @@ function getAllToolDefinitions(context) {
1916
2116
  };
1917
2117
  }
1918
2118
  // Create the GitHub issue
1919
- const issue = await github.createIssue(owner, repo, input.title, input.body, input.labels, input.assignees);
2119
+ const issue = await github.createIssue(owner, repo, input.title, input.body, input.labels, input.assignees, input.milestone_number);
1920
2120
  if (!issue) {
1921
2121
  return {
1922
2122
  error: 'Failed to create GitHub issue. Check GITHUB_TOKEN permissions.',
@@ -2200,6 +2400,389 @@ function getAllToolDefinitions(context) {
2200
2400
  };
2201
2401
  },
2202
2402
  },
2403
+ // Milestone tools
2404
+ {
2405
+ name: 'get_github_milestones',
2406
+ title: 'List GitHub Milestones',
2407
+ description: 'List GitHub milestones for the repository with completion percentages and due dates.',
2408
+ group: 'github',
2409
+ inputSchema: z.object({
2410
+ state: z
2411
+ .enum(['open', 'closed', 'all'])
2412
+ .optional()
2413
+ .default('open')
2414
+ .describe('Filter by state (default: open)'),
2415
+ limit: z
2416
+ .number()
2417
+ .optional()
2418
+ .default(20)
2419
+ .describe('Max milestones to return (default: 20)'),
2420
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2421
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2422
+ }),
2423
+ outputSchema: GitHubMilestonesListOutputSchema,
2424
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2425
+ handler: async (params) => {
2426
+ const input = z
2427
+ .object({
2428
+ state: z.enum(['open', 'closed', 'all']).optional().default('open'),
2429
+ limit: z.number().optional().default(20),
2430
+ owner: z.string().optional(),
2431
+ repo: z.string().optional(),
2432
+ })
2433
+ .parse(params);
2434
+ if (!github) {
2435
+ return { error: 'GitHub integration not available' };
2436
+ }
2437
+ const repoInfo = await github.getRepoInfo();
2438
+ const detectedOwner = repoInfo.owner;
2439
+ const detectedRepo = repoInfo.repo;
2440
+ const owner = input.owner ?? detectedOwner ?? undefined;
2441
+ const repo = input.repo ?? detectedRepo ?? undefined;
2442
+ if (!owner || !repo) {
2443
+ return {
2444
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
2445
+ requiresUserInput: true,
2446
+ detectedOwner,
2447
+ detectedRepo,
2448
+ instruction: 'Ask the user: "What GitHub repository should I list milestones for? Please provide the owner and repo name (e.g., owner/repo)."',
2449
+ };
2450
+ }
2451
+ const milestones = await github.getMilestones(owner, repo, input.state, input.limit);
2452
+ const milestonesWithPercentage = milestones.map((ms) => {
2453
+ const total = ms.openIssues + ms.closedIssues;
2454
+ const completionPercentage = total > 0 ? Math.round((ms.closedIssues / total) * 100) : 0;
2455
+ return { ...ms, completionPercentage };
2456
+ });
2457
+ return {
2458
+ milestones: milestonesWithPercentage,
2459
+ count: milestonesWithPercentage.length,
2460
+ owner,
2461
+ repo,
2462
+ detectedOwner,
2463
+ detectedRepo,
2464
+ };
2465
+ },
2466
+ },
2467
+ {
2468
+ name: 'get_github_milestone',
2469
+ title: 'Get GitHub Milestone Details',
2470
+ description: 'Get detailed information about a specific GitHub milestone including progress and linked issue counts.',
2471
+ group: 'github',
2472
+ inputSchema: z.object({
2473
+ milestone_number: z.number().describe('Milestone number'),
2474
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2475
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect from git'),
2476
+ }),
2477
+ outputSchema: GitHubMilestoneResultOutputSchema,
2478
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2479
+ handler: async (params) => {
2480
+ const input = z
2481
+ .object({
2482
+ milestone_number: z.number(),
2483
+ owner: z.string().optional(),
2484
+ repo: z.string().optional(),
2485
+ })
2486
+ .parse(params);
2487
+ if (!github) {
2488
+ return { error: 'GitHub integration not available' };
2489
+ }
2490
+ const repoInfo = await github.getRepoInfo();
2491
+ const detectedOwner = repoInfo.owner;
2492
+ const detectedRepo = repoInfo.repo;
2493
+ const owner = input.owner ?? detectedOwner ?? undefined;
2494
+ const repo = input.repo ?? detectedRepo ?? undefined;
2495
+ if (!owner || !repo) {
2496
+ return {
2497
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
2498
+ requiresUserInput: true,
2499
+ detectedOwner,
2500
+ detectedRepo,
2501
+ instruction: 'Ask the user: "What GitHub repository is this milestone from? Please provide the owner and repo name (e.g., owner/repo)."',
2502
+ };
2503
+ }
2504
+ const milestone = await github.getMilestone(owner, repo, input.milestone_number);
2505
+ if (!milestone) {
2506
+ return {
2507
+ error: `Milestone #${String(input.milestone_number)} not found`,
2508
+ owner,
2509
+ repo,
2510
+ detectedOwner,
2511
+ detectedRepo,
2512
+ };
2513
+ }
2514
+ const total = milestone.openIssues + milestone.closedIssues;
2515
+ const completionPercentage = total > 0 ? Math.round((milestone.closedIssues / total) * 100) : 0;
2516
+ return {
2517
+ milestone: { ...milestone, completionPercentage },
2518
+ owner,
2519
+ repo,
2520
+ detectedOwner,
2521
+ detectedRepo,
2522
+ };
2523
+ },
2524
+ },
2525
+ {
2526
+ name: 'create_github_milestone',
2527
+ title: 'Create GitHub Milestone',
2528
+ description: 'Create a new GitHub milestone for tracking progress toward a project goal.',
2529
+ group: 'github',
2530
+ inputSchema: z.object({
2531
+ title: z.string().min(1).describe('Milestone title'),
2532
+ description: z.string().optional().describe('Milestone description'),
2533
+ due_on: z.string().optional().describe('Due date in YYYY-MM-DD format (optional)'),
2534
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2535
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2536
+ }),
2537
+ outputSchema: CreateMilestoneOutputSchema,
2538
+ annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
2539
+ handler: async (params) => {
2540
+ const input = z
2541
+ .object({
2542
+ title: z.string().min(1),
2543
+ description: z.string().optional(),
2544
+ due_on: z.string().optional(),
2545
+ owner: z.string().optional(),
2546
+ repo: z.string().optional(),
2547
+ })
2548
+ .parse(params);
2549
+ if (!github) {
2550
+ return { error: 'GitHub integration not available' };
2551
+ }
2552
+ const repoInfo = await github.getRepoInfo();
2553
+ const owner = input.owner ?? repoInfo.owner ?? undefined;
2554
+ const repo = input.repo ?? repoInfo.repo ?? undefined;
2555
+ if (!owner || !repo) {
2556
+ return {
2557
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
2558
+ requiresUserInput: true,
2559
+ instruction: 'Ask the user: "What GitHub repository should I create the milestone in?"',
2560
+ };
2561
+ }
2562
+ // Format due_on to ISO 8601 if provided (GitHub expects YYYY-MM-DDTHH:MM:SSZ)
2563
+ const dueOn = input.due_on ? `${input.due_on}T08:00:00Z` : undefined;
2564
+ const milestone = await github.createMilestone(owner, repo, input.title, input.description, dueOn);
2565
+ if (!milestone) {
2566
+ return {
2567
+ error: 'Failed to create milestone. Check GITHUB_TOKEN permissions.',
2568
+ };
2569
+ }
2570
+ return {
2571
+ success: true,
2572
+ milestone: { ...milestone, completionPercentage: 0 },
2573
+ message: `Created milestone #${String(milestone.number)}: ${milestone.title}`,
2574
+ };
2575
+ },
2576
+ },
2577
+ {
2578
+ name: 'update_github_milestone',
2579
+ title: 'Update GitHub Milestone',
2580
+ description: 'Update a GitHub milestone (title, description, due date, or state). Use state "closed" to close a completed milestone.',
2581
+ group: 'github',
2582
+ inputSchema: z.object({
2583
+ milestone_number: z.number().describe('Milestone number to update'),
2584
+ title: z.string().optional().describe('New title'),
2585
+ description: z.string().optional().describe('New description'),
2586
+ due_on: z.string().optional().describe('New due date in YYYY-MM-DD format'),
2587
+ state: z
2588
+ .enum(['open', 'closed'])
2589
+ .optional()
2590
+ .describe('Set to "closed" to close the milestone'),
2591
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2592
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2593
+ }),
2594
+ outputSchema: UpdateMilestoneOutputSchema,
2595
+ annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: true },
2596
+ handler: async (params) => {
2597
+ const input = z
2598
+ .object({
2599
+ milestone_number: z.number(),
2600
+ title: z.string().optional(),
2601
+ description: z.string().optional(),
2602
+ due_on: z.string().optional(),
2603
+ state: z.enum(['open', 'closed']).optional(),
2604
+ owner: z.string().optional(),
2605
+ repo: z.string().optional(),
2606
+ })
2607
+ .parse(params);
2608
+ if (!github) {
2609
+ return { error: 'GitHub integration not available' };
2610
+ }
2611
+ const repoInfo = await github.getRepoInfo();
2612
+ const owner = input.owner ?? repoInfo.owner ?? undefined;
2613
+ const repo = input.repo ?? repoInfo.repo ?? undefined;
2614
+ if (!owner || !repo) {
2615
+ return {
2616
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
2617
+ requiresUserInput: true,
2618
+ instruction: 'Ask the user: "What GitHub repository is this milestone in?"',
2619
+ };
2620
+ }
2621
+ const dueOn = input.due_on ? `${input.due_on}T08:00:00Z` : undefined;
2622
+ const milestone = await github.updateMilestone(owner, repo, input.milestone_number, {
2623
+ title: input.title,
2624
+ description: input.description,
2625
+ dueOn,
2626
+ state: input.state,
2627
+ });
2628
+ if (!milestone) {
2629
+ return {
2630
+ error: `Failed to update milestone #${String(input.milestone_number)}. Check that it exists and GITHUB_TOKEN has permissions.`,
2631
+ };
2632
+ }
2633
+ const total = milestone.openIssues + milestone.closedIssues;
2634
+ const completionPercentage = total > 0 ? Math.round((milestone.closedIssues / total) * 100) : 0;
2635
+ return {
2636
+ success: true,
2637
+ milestone: { ...milestone, completionPercentage },
2638
+ message: `Updated milestone #${String(milestone.number)}: ${milestone.title}`,
2639
+ };
2640
+ },
2641
+ },
2642
+ {
2643
+ name: 'delete_github_milestone',
2644
+ title: 'Delete GitHub Milestone',
2645
+ description: 'Permanently delete a GitHub milestone. Issues assigned to the milestone will be un-assigned but not deleted.',
2646
+ group: 'github',
2647
+ inputSchema: z.object({
2648
+ milestone_number: z.number().describe('Milestone number to delete'),
2649
+ confirm: z.literal(true).describe('Must be set to true to confirm deletion'),
2650
+ owner: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2651
+ repo: z.string().optional().describe('LEAVE EMPTY to auto-detect'),
2652
+ }),
2653
+ outputSchema: DeleteMilestoneOutputSchema,
2654
+ annotations: {
2655
+ readOnlyHint: false,
2656
+ idempotentHint: false,
2657
+ destructiveHint: true,
2658
+ openWorldHint: true,
2659
+ },
2660
+ handler: async (params) => {
2661
+ const input = z
2662
+ .object({
2663
+ milestone_number: z.number(),
2664
+ confirm: z.literal(true),
2665
+ owner: z.string().optional(),
2666
+ repo: z.string().optional(),
2667
+ })
2668
+ .parse(params);
2669
+ if (!github) {
2670
+ return { error: 'GitHub integration not available' };
2671
+ }
2672
+ const repoInfo = await github.getRepoInfo();
2673
+ const owner = input.owner ?? repoInfo.owner ?? undefined;
2674
+ const repo = input.repo ?? repoInfo.repo ?? undefined;
2675
+ if (!owner || !repo) {
2676
+ return {
2677
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS.',
2678
+ requiresUserInput: true,
2679
+ instruction: 'Ask the user: "What GitHub repository is this milestone in?"',
2680
+ };
2681
+ }
2682
+ const result = await github.deleteMilestone(owner, repo, input.milestone_number);
2683
+ if (!result.success) {
2684
+ return {
2685
+ success: false,
2686
+ milestoneNumber: input.milestone_number,
2687
+ message: `Failed to delete milestone #${String(input.milestone_number)}`,
2688
+ error: result.error ?? undefined,
2689
+ };
2690
+ }
2691
+ return {
2692
+ success: true,
2693
+ milestoneNumber: input.milestone_number,
2694
+ message: `Deleted milestone #${String(input.milestone_number)}`,
2695
+ };
2696
+ },
2697
+ },
2698
+ // Repository insights tool
2699
+ {
2700
+ name: 'get_repo_insights',
2701
+ title: 'Repository Insights',
2702
+ description: '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).',
2703
+ group: 'github',
2704
+ inputSchema: z.object({
2705
+ sections: z
2706
+ .enum(['stars', 'traffic', 'referrers', 'paths', 'all'])
2707
+ .optional()
2708
+ .default('stars')
2709
+ .describe('Data section to return (default: stars). Use "all" for full payload.'),
2710
+ owner: z
2711
+ .string()
2712
+ .optional()
2713
+ .describe('Repository owner - LEAVE EMPTY to auto-detect'),
2714
+ repo: z
2715
+ .string()
2716
+ .optional()
2717
+ .describe('Repository name - LEAVE EMPTY to auto-detect'),
2718
+ }),
2719
+ outputSchema: RepoInsightsOutputSchema,
2720
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
2721
+ handler: async (params) => {
2722
+ const input = z
2723
+ .object({
2724
+ sections: z
2725
+ .enum(['stars', 'traffic', 'referrers', 'paths', 'all'])
2726
+ .optional()
2727
+ .default('stars'),
2728
+ owner: z.string().optional(),
2729
+ repo: z.string().optional(),
2730
+ })
2731
+ .parse(params);
2732
+ if (!github) {
2733
+ return { error: 'GitHub integration not available' };
2734
+ }
2735
+ const repoInfo = await github.getRepoInfo();
2736
+ const owner = input.owner ?? repoInfo.owner ?? undefined;
2737
+ const repo = input.repo ?? repoInfo.repo ?? undefined;
2738
+ if (!owner || !repo) {
2739
+ return {
2740
+ error: 'STOP: Could not auto-detect repository. DO NOT GUESS. You MUST ask the user to provide the GitHub owner and repository name.',
2741
+ requiresUserInput: true,
2742
+ instruction: 'Ask the user: "What GitHub repository should I get insights for? Please provide the owner and repo name (e.g., owner/repo)."',
2743
+ };
2744
+ }
2745
+ const section = input.sections;
2746
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- building response dynamically
2747
+ const result = {
2748
+ owner,
2749
+ repo,
2750
+ section,
2751
+ };
2752
+ // Stars section (default)
2753
+ if (section === 'stars' || section === 'all') {
2754
+ const stats = await github.getRepoStats(owner, repo);
2755
+ if (stats) {
2756
+ result['stars'] = stats.stars;
2757
+ result['forks'] = stats.forks;
2758
+ result['watchers'] = stats.watchers;
2759
+ result['openIssues'] = stats.openIssues;
2760
+ if (section === 'all') {
2761
+ result['size'] = stats.size;
2762
+ result['defaultBranch'] = stats.defaultBranch;
2763
+ }
2764
+ }
2765
+ }
2766
+ // Traffic section
2767
+ if (section === 'traffic' || section === 'all') {
2768
+ const traffic = await github.getTrafficData(owner, repo);
2769
+ if (traffic) {
2770
+ result['traffic'] = traffic;
2771
+ }
2772
+ }
2773
+ // Referrers section
2774
+ if (section === 'referrers' || section === 'all') {
2775
+ const referrers = await github.getTopReferrers(owner, repo, 5);
2776
+ result['referrers'] = referrers;
2777
+ }
2778
+ // Paths section
2779
+ if (section === 'paths' || section === 'all') {
2780
+ const paths = await github.getPopularPaths(owner, repo, 5);
2781
+ result['paths'] = paths;
2782
+ }
2783
+ return result;
2784
+ },
2785
+ },
2203
2786
  // Backup tools
2204
2787
  {
2205
2788
  name: 'backup_journal',