@crowley/rag-mcp 1.0.5 → 1.0.6

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 (49) hide show
  1. package/dist/annotations.d.ts +16 -0
  2. package/dist/annotations.js +158 -0
  3. package/dist/context-enrichment.js +7 -0
  4. package/dist/formatters.d.ts +2 -0
  5. package/dist/formatters.js +12 -0
  6. package/dist/index.js +46 -47
  7. package/dist/schemas.d.ts +97 -0
  8. package/dist/schemas.js +128 -0
  9. package/dist/tool-middleware.d.ts +40 -0
  10. package/dist/tool-middleware.js +216 -0
  11. package/dist/tool-registry.js +2 -1
  12. package/dist/tools/advanced.d.ts +2 -2
  13. package/dist/tools/advanced.js +200 -275
  14. package/dist/tools/agents.d.ts +2 -2
  15. package/dist/tools/agents.js +59 -78
  16. package/dist/tools/analytics.d.ts +2 -2
  17. package/dist/tools/analytics.js +170 -210
  18. package/dist/tools/architecture.d.ts +2 -2
  19. package/dist/tools/architecture.js +506 -669
  20. package/dist/tools/ask.d.ts +2 -2
  21. package/dist/tools/ask.js +164 -219
  22. package/dist/tools/cache.d.ts +2 -2
  23. package/dist/tools/cache.js +63 -82
  24. package/dist/tools/clustering.d.ts +2 -2
  25. package/dist/tools/clustering.js +154 -215
  26. package/dist/tools/confluence.d.ts +2 -2
  27. package/dist/tools/confluence.js +80 -116
  28. package/dist/tools/database.d.ts +2 -2
  29. package/dist/tools/database.js +303 -380
  30. package/dist/tools/feedback.d.ts +2 -2
  31. package/dist/tools/feedback.js +143 -184
  32. package/dist/tools/guidelines.d.ts +2 -2
  33. package/dist/tools/guidelines.js +123 -135
  34. package/dist/tools/indexing.d.ts +2 -2
  35. package/dist/tools/indexing.js +100 -108
  36. package/dist/tools/memory.d.ts +2 -2
  37. package/dist/tools/memory.js +299 -485
  38. package/dist/tools/pm.d.ts +2 -2
  39. package/dist/tools/pm.js +367 -615
  40. package/dist/tools/review.d.ts +2 -2
  41. package/dist/tools/review.js +142 -189
  42. package/dist/tools/search.d.ts +2 -2
  43. package/dist/tools/search.js +230 -305
  44. package/dist/tools/session.d.ts +2 -2
  45. package/dist/tools/session.js +288 -345
  46. package/dist/tools/suggestions.d.ts +2 -2
  47. package/dist/tools/suggestions.js +425 -512
  48. package/dist/types.d.ts +19 -2
  49. package/package.json +4 -2
@@ -3,390 +3,333 @@
3
3
  * change tracking, and usage pattern analysis.
4
4
  */
5
5
  import { truncate } from "../formatters.js";
6
+ import { z } from "zod";
7
+ import { TOOL_ANNOTATIONS } from "../annotations.js";
6
8
  /**
7
9
  * Create the session tools module with project-specific descriptions.
8
10
  * Accepts a mutable ctx reference to update activeSessionId on start/end.
9
11
  */
10
12
  export function createSessionTools(projectName, sharedCtx) {
11
- const tools = [
13
+ return [
12
14
  {
13
15
  name: "summarize_context",
14
16
  description: `Summarize the current working context for ${projectName}. Shows recently used tools, active features, recent queries, and suggested next steps.`,
15
- inputSchema: {
16
- type: "object",
17
- properties: {
18
- sessionId: {
19
- type: "string",
20
- description: "Session ID to get context for. If omitted, returns the latest context.",
21
- },
22
- },
17
+ schema: z.object({
18
+ sessionId: z.string().optional().describe("Session ID to get context for. If omitted, returns the latest context."),
19
+ }),
20
+ annotations: TOOL_ANNOTATIONS["summarize_context"],
21
+ handler: async (args, ctx) => {
22
+ const { sessionId } = args;
23
+ const params = sessionId ? `?sessionId=${sessionId}` : "";
24
+ const response = await ctx.api.get(`/api/context/${ctx.projectName}${params}`);
25
+ const data = response.data;
26
+ let result = `**Context Summary for ${ctx.projectName}**\n\n`;
27
+ if (data.recentTools && data.recentTools.length > 0) {
28
+ result += `**Recently Used Tools:**\n`;
29
+ result += data.recentTools
30
+ .map((t) => `- ${t}`)
31
+ .join("\n");
32
+ result += "\n\n";
33
+ }
34
+ if (data.activeFeatures && data.activeFeatures.length > 0) {
35
+ result += `**Active Features:**\n`;
36
+ result += data.activeFeatures
37
+ .map((f) => `- ${f}`)
38
+ .join("\n");
39
+ result += "\n\n";
40
+ }
41
+ if (data.recentQueries && data.recentQueries.length > 0) {
42
+ const queries = data.recentQueries.slice(0, 5);
43
+ result += `**Recent Queries:**\n`;
44
+ result += queries
45
+ .map((q) => `- ${truncate(q, 80)}`)
46
+ .join("\n");
47
+ result += "\n\n";
48
+ }
49
+ if (data.suggestedNextSteps && data.suggestedNextSteps.length > 0) {
50
+ result += `**Suggested Next Steps:**\n`;
51
+ result += data.suggestedNextSteps
52
+ .map((s, i) => `${i + 1}. ${s}`)
53
+ .join("\n");
54
+ result += "\n";
55
+ }
56
+ return result;
23
57
  },
24
58
  },
25
59
  {
26
60
  name: "summarize_changes",
27
61
  description: `Summarize changes made during a session for ${projectName}. Shows what was modified, tools used, and key actions taken.`,
28
- inputSchema: {
29
- type: "object",
30
- properties: {
31
- sessionId: {
32
- type: "string",
33
- description: "Session ID to summarize changes for.",
34
- },
35
- includeCode: {
36
- type: "boolean",
37
- description: "Whether to include code snippets in the summary (default: false).",
38
- default: false,
39
- },
40
- },
41
- required: ["sessionId"],
62
+ schema: z.object({
63
+ sessionId: z.string().describe("Session ID to summarize changes for."),
64
+ includeCode: z.boolean().optional().describe("Whether to include code snippets in the summary (default: false)."),
65
+ }),
66
+ annotations: TOOL_ANNOTATIONS["summarize_changes"],
67
+ handler: async (args, ctx) => {
68
+ const { sessionId, includeCode } = args;
69
+ const params = includeCode ? "?includeCode=true" : "";
70
+ const response = await ctx.api.get(`/api/changes/${ctx.projectName}/${sessionId}${params}`);
71
+ const data = response.data;
72
+ let result = `**Changes Summary for Session ${sessionId}**\n\n`;
73
+ if (data.summary) {
74
+ result += `${data.summary}\n\n`;
75
+ }
76
+ if (data.duration !== undefined) {
77
+ result += `**Duration:** ${data.duration} minutes\n`;
78
+ }
79
+ if (data.toolsUsed && data.toolsUsed.length > 0) {
80
+ result += `**Tools Used:** ${data.toolsUsed.join(", ")}\n`;
81
+ }
82
+ if (data.filesAffected && data.filesAffected.length > 0) {
83
+ const files = data.filesAffected.slice(0, 10);
84
+ result += `\n**Files Affected:**\n`;
85
+ result += files.map((f) => `- ${f}`).join("\n");
86
+ if (data.filesAffected.length > 10) {
87
+ result += `\n- ... and ${data.filesAffected.length - 10} more`;
88
+ }
89
+ result += "\n";
90
+ }
91
+ if (data.keyActions && data.keyActions.length > 0) {
92
+ result += `\n**Key Actions:**\n`;
93
+ result += data.keyActions
94
+ .map((a, i) => `${i + 1}. ${a}`)
95
+ .join("\n");
96
+ result += "\n";
97
+ }
98
+ return result;
42
99
  },
43
100
  },
44
101
  {
45
102
  name: "analyze_usage_patterns",
46
103
  description: `Analyze tool usage patterns for ${projectName}. Shows common workflows, detected patterns, and recommendations for improving productivity.`,
47
- inputSchema: {
48
- type: "object",
49
- properties: {
50
- days: {
51
- type: "number",
52
- description: "Number of days to analyze (default: 7).",
53
- default: 7,
54
- },
55
- },
104
+ schema: z.object({
105
+ days: z.number().optional().describe("Number of days to analyze (default: 7)."),
106
+ }),
107
+ annotations: TOOL_ANNOTATIONS["analyze_usage_patterns"],
108
+ handler: async (args, ctx) => {
109
+ const { days = 7 } = args;
110
+ const response = await ctx.api.get(`/api/patterns/${ctx.projectName}?days=${days}`);
111
+ const data = response.data;
112
+ let result = `**Usage Patterns for ${ctx.projectName}** (last ${days} days)\n\n`;
113
+ if (data.insights && data.insights.length > 0) {
114
+ result += `**Insights:**\n`;
115
+ result += data.insights
116
+ .map((insight) => `- ${insight}`)
117
+ .join("\n");
118
+ result += "\n\n";
119
+ }
120
+ if (data.commonWorkflows && data.commonWorkflows.length > 0) {
121
+ result += `**Common Workflows:**\n`;
122
+ result += data.commonWorkflows
123
+ .map((w) => `- ${w.tools.join(" -> ")} (${w.count}x, ${(w.successRate * 100).toFixed(0)}% success)`)
124
+ .join("\n");
125
+ result += "\n\n";
126
+ }
127
+ if (data.detectedPatterns && data.detectedPatterns.length > 0) {
128
+ result += `**Detected Patterns:**\n`;
129
+ result += data.detectedPatterns
130
+ .map((p) => `- **${p.name}:** ${p.description}\n *Suggestion:* ${p.suggestion}`)
131
+ .join("\n");
132
+ result += "\n\n";
133
+ }
134
+ if (data.recommendations && data.recommendations.length > 0) {
135
+ result += `**Recommendations:**\n`;
136
+ result += data.recommendations
137
+ .map((r, i) => `${i + 1}. ${r}`)
138
+ .join("\n");
139
+ result += "\n";
140
+ }
141
+ return result;
56
142
  },
57
143
  },
58
144
  {
59
145
  name: "get_developer_profile",
60
146
  description: `Get accumulated developer profile for ${projectName}: frequent files, preferred tools, peak hours, common patterns.`,
61
- inputSchema: {
62
- type: "object",
63
- properties: {},
147
+ schema: z.object({}),
148
+ annotations: TOOL_ANNOTATIONS["get_developer_profile"],
149
+ handler: async (_args, ctx) => {
150
+ const response = await ctx.api.get(`/api/developer-profile`, { headers: { "X-Project-Name": ctx.projectName } });
151
+ const p = response.data;
152
+ if (!p.totalToolCalls) {
153
+ return "No usage data yet. Use tools to build your developer profile.";
154
+ }
155
+ let result = `**Developer Profile** (${p.totalSessions} sessions, ${p.totalToolCalls} tool calls)\n\n`;
156
+ if (p.frequentFiles.length > 0) {
157
+ result += "**Frequent Files:**\n";
158
+ result += p.frequentFiles.slice(0, 10).map((f) => `- ${f.file} (${f.count}x)`).join("\n");
159
+ result += "\n\n";
160
+ }
161
+ if (p.preferredTools.length > 0) {
162
+ result += "**Preferred Tools:**\n";
163
+ result += p.preferredTools.slice(0, 8).map((t) => `- ${t.tool}: ${t.count}x (avg ${Math.round(t.avgDurationMs)}ms)`).join("\n");
164
+ result += "\n\n";
165
+ }
166
+ if (p.peakHours.length > 0) {
167
+ result += "**Peak Hours:** ";
168
+ result += p.peakHours.map((h) => `${h.hour}:00 (${h.count})`).join(", ");
169
+ result += "\n\n";
170
+ }
171
+ if (p.commonPatterns.length > 0) {
172
+ result += "**Common Patterns:**\n";
173
+ result += p.commonPatterns.slice(0, 5).map((q) => `- "${truncate(q, 60)}"`).join("\n");
174
+ result += "\n";
175
+ }
176
+ return result;
64
177
  },
65
178
  },
66
179
  {
67
180
  name: "start_session",
68
181
  description: `Start a new working session for ${projectName}. Tracks tool usage, file changes, and learnings throughout the session.`,
69
- inputSchema: {
70
- type: "object",
71
- properties: {
72
- sessionId: {
73
- type: "string",
74
- description: "Custom session ID. If omitted, one will be generated.",
75
- },
76
- initialContext: {
77
- type: "string",
78
- description: "Description of what this session is about (e.g., 'fixing auth bug', 'adding new API endpoint').",
79
- },
80
- resumeFrom: {
81
- type: "string",
82
- description: "Session ID to resume from. Carries over context from the previous session.",
83
- },
84
- },
182
+ schema: z.object({
183
+ sessionId: z.string().optional().describe("Custom session ID. If omitted, one will be generated."),
184
+ initialContext: z.string().optional().describe("Description of what this session is about (e.g., 'fixing auth bug', 'adding new API endpoint')."),
185
+ resumeFrom: z.string().optional().describe("Session ID to resume from. Carries over context from the previous session."),
186
+ }),
187
+ annotations: TOOL_ANNOTATIONS["start_session"],
188
+ handler: async (args, ctx) => {
189
+ const { sessionId, initialContext, resumeFrom } = args;
190
+ const response = await ctx.api.post("/api/session/start", {
191
+ projectName: ctx.projectName,
192
+ sessionId,
193
+ initialContext,
194
+ resumeFrom,
195
+ });
196
+ const data = response.data;
197
+ const session = data.session;
198
+ // Extract fields — API returns { session: { sessionId, startedAt, ... } }
199
+ const sid = session?.sessionId || data.sessionId;
200
+ const started = session?.startedAt || data.started;
201
+ const resumedFrom = session?.metadata?.resumedFrom || data.resumedFrom;
202
+ const initialFiles = session?.currentFiles || data.initialFiles;
203
+ // Update shared context with active session ID
204
+ if (sharedCtx && sid) {
205
+ sharedCtx.activeSessionId = sid;
206
+ }
207
+ let result = `**Session Started**\n\n`;
208
+ result += `- **Session ID:** ${sid}\n`;
209
+ result += `- **Started:** ${started}\n`;
210
+ if (resumedFrom) {
211
+ result += `- **Resumed From:** ${resumedFrom}\n`;
212
+ }
213
+ if (initialFiles && initialFiles.length > 0) {
214
+ result += `\n**Initial Files:**\n`;
215
+ result += initialFiles
216
+ .map((f) => `- ${f}`)
217
+ .join("\n");
218
+ result += "\n";
219
+ }
220
+ // Include prefetch stats if available
221
+ if (session?.metadata?.prefetchStats) {
222
+ const pf = session.metadata.prefetchStats;
223
+ result += `\n**Predictive Prefetch:** ${pf.prefetchedCount ?? 0} resources prefetched\n`;
224
+ }
225
+ // Include briefing if available (Sprint E)
226
+ if (data.briefing) {
227
+ result += `\n**Session Briefing:**\n${data.briefing}\n`;
228
+ }
229
+ return result;
85
230
  },
86
231
  },
87
232
  {
88
233
  name: "get_session_context",
89
234
  description: `Get the current context for an active session in ${projectName}. Shows files being worked on, tools used, and pending learnings.`,
90
- inputSchema: {
91
- type: "object",
92
- properties: {
93
- sessionId: {
94
- type: "string",
95
- description: "Session ID to get context for.",
96
- },
97
- },
98
- required: ["sessionId"],
235
+ schema: z.object({
236
+ sessionId: z.string().describe("Session ID to get context for."),
237
+ }),
238
+ annotations: TOOL_ANNOTATIONS["get_session_context"],
239
+ handler: async (args, ctx) => {
240
+ const { sessionId } = args;
241
+ const response = await ctx.api.get(`/api/session/${sessionId}`);
242
+ const data = response.data;
243
+ let result = `**Session Context**\n\n`;
244
+ result += `- **Session ID:** ${data.sessionId}\n`;
245
+ result += `- **Status:** ${data.status}\n`;
246
+ result += `- **Started At:** ${data.startedAt}\n`;
247
+ result += `- **Last Activity:** ${data.lastActivity}\n`;
248
+ if (data.currentFiles && data.currentFiles.length > 0) {
249
+ const files = data.currentFiles.slice(0, 10);
250
+ result += `\n**Current Files:**\n`;
251
+ result += files.map((f) => `- ${f}`).join("\n");
252
+ if (data.currentFiles.length > 10) {
253
+ result += `\n- ... and ${data.currentFiles.length - 10} more`;
254
+ }
255
+ result += "\n";
256
+ }
257
+ if (data.toolsUsed && data.toolsUsed.length > 0) {
258
+ result += `\n**Tools Used:** ${data.toolsUsed.join(", ")}\n`;
259
+ }
260
+ if (data.activeFeatures && data.activeFeatures.length > 0) {
261
+ result += `\n**Active Features:**\n`;
262
+ result += data.activeFeatures
263
+ .map((f) => `- ${f}`)
264
+ .join("\n");
265
+ result += "\n";
266
+ }
267
+ if (data.pendingLearnings && data.pendingLearnings.length > 0) {
268
+ const learnings = data.pendingLearnings.slice(0, 5);
269
+ result += `\n**Pending Learnings:**\n`;
270
+ result += learnings
271
+ .map((l) => `- ${truncate(l, 80)}`)
272
+ .join("\n");
273
+ if (data.pendingLearnings.length > 5) {
274
+ result += `\n- ... and ${data.pendingLearnings.length - 5} more`;
275
+ }
276
+ result += "\n";
277
+ }
278
+ return result;
99
279
  },
100
280
  },
101
281
  {
102
282
  name: "end_session",
103
283
  description: `End a working session for ${projectName}. Saves a summary and optionally extracts learnings for future sessions.`,
104
- inputSchema: {
105
- type: "object",
106
- properties: {
107
- sessionId: {
108
- type: "string",
109
- description: "Session ID to end.",
110
- },
111
- summary: {
112
- type: "string",
113
- description: "Summary of what was accomplished during the session.",
114
- },
115
- autoSaveLearnings: {
116
- type: "boolean",
117
- description: "Automatically save detected learnings to memory (default: true).",
118
- default: true,
119
- },
120
- feedback: {
121
- type: "string",
122
- description: "Optional feedback about the session (e.g., 'productive', 'too many context switches').",
123
- },
124
- },
125
- required: ["sessionId"],
284
+ schema: z.object({
285
+ sessionId: z.string().describe("Session ID to end."),
286
+ summary: z.string().optional().describe("Summary of what was accomplished during the session."),
287
+ autoSaveLearnings: z.boolean().optional().describe("Automatically save detected learnings to memory (default: true)."),
288
+ feedback: z.string().optional().describe("Optional feedback about the session (e.g., 'productive', 'too many context switches')."),
289
+ }),
290
+ annotations: TOOL_ANNOTATIONS["end_session"],
291
+ handler: async (args, ctx) => {
292
+ const { sessionId, summary, autoSaveLearnings, feedback } = args;
293
+ const response = await ctx.api.post(`/api/session/${sessionId}/end`, {
294
+ summary,
295
+ autoSaveLearnings: autoSaveLearnings !== undefined ? autoSaveLearnings : true,
296
+ feedback,
297
+ });
298
+ const data = response.data;
299
+ // Clear active session ID
300
+ if (sharedCtx) {
301
+ sharedCtx.activeSessionId = undefined;
302
+ }
303
+ let result = `**Session Ended**\n\n`;
304
+ if (data.summary) {
305
+ result += `**Summary:** ${data.summary}\n\n`;
306
+ }
307
+ if (data.duration !== undefined) {
308
+ result += `- **Duration:** ${data.duration} minutes\n`;
309
+ }
310
+ if (data.toolsUsedCount !== undefined) {
311
+ result += `- **Tools Used:** ${data.toolsUsedCount}\n`;
312
+ }
313
+ if (data.filesAffectedCount !== undefined) {
314
+ result += `- **Files Affected:** ${data.filesAffectedCount}\n`;
315
+ }
316
+ if (data.queriesCount !== undefined) {
317
+ result += `- **Queries:** ${data.queriesCount}\n`;
318
+ }
319
+ if (data.learningsSaved !== undefined) {
320
+ result += `- **Learnings Saved:** ${data.learningsSaved}\n`;
321
+ }
322
+ if (data.filesAffected && data.filesAffected.length > 0) {
323
+ const files = data.filesAffected.slice(0, 10);
324
+ result += `\n**Files Affected:**\n`;
325
+ result += files.map((f) => `- ${f}`).join("\n");
326
+ if (data.filesAffected.length > 10) {
327
+ result += `\n- ... and ${data.filesAffected.length - 10} more`;
328
+ }
329
+ result += "\n";
330
+ }
331
+ return result;
126
332
  },
127
333
  },
128
334
  ];
129
- const handlers = {
130
- summarize_context: async (args, ctx) => {
131
- const { sessionId } = args;
132
- const params = sessionId ? `?sessionId=${sessionId}` : "";
133
- const response = await ctx.api.get(`/api/context/${ctx.projectName}${params}`);
134
- const data = response.data;
135
- let result = `**Context Summary for ${ctx.projectName}**\n\n`;
136
- if (data.recentTools && data.recentTools.length > 0) {
137
- result += `**Recently Used Tools:**\n`;
138
- result += data.recentTools
139
- .map((t) => `- ${t}`)
140
- .join("\n");
141
- result += "\n\n";
142
- }
143
- if (data.activeFeatures && data.activeFeatures.length > 0) {
144
- result += `**Active Features:**\n`;
145
- result += data.activeFeatures
146
- .map((f) => `- ${f}`)
147
- .join("\n");
148
- result += "\n\n";
149
- }
150
- if (data.recentQueries && data.recentQueries.length > 0) {
151
- const queries = data.recentQueries.slice(0, 5);
152
- result += `**Recent Queries:**\n`;
153
- result += queries
154
- .map((q) => `- ${truncate(q, 80)}`)
155
- .join("\n");
156
- result += "\n\n";
157
- }
158
- if (data.suggestedNextSteps && data.suggestedNextSteps.length > 0) {
159
- result += `**Suggested Next Steps:**\n`;
160
- result += data.suggestedNextSteps
161
- .map((s, i) => `${i + 1}. ${s}`)
162
- .join("\n");
163
- result += "\n";
164
- }
165
- return result;
166
- },
167
- summarize_changes: async (args, ctx) => {
168
- const { sessionId, includeCode } = args;
169
- const params = includeCode ? "?includeCode=true" : "";
170
- const response = await ctx.api.get(`/api/changes/${ctx.projectName}/${sessionId}${params}`);
171
- const data = response.data;
172
- let result = `**Changes Summary for Session ${sessionId}**\n\n`;
173
- if (data.summary) {
174
- result += `${data.summary}\n\n`;
175
- }
176
- if (data.duration !== undefined) {
177
- result += `**Duration:** ${data.duration} minutes\n`;
178
- }
179
- if (data.toolsUsed && data.toolsUsed.length > 0) {
180
- result += `**Tools Used:** ${data.toolsUsed.join(", ")}\n`;
181
- }
182
- if (data.filesAffected && data.filesAffected.length > 0) {
183
- const files = data.filesAffected.slice(0, 10);
184
- result += `\n**Files Affected:**\n`;
185
- result += files.map((f) => `- ${f}`).join("\n");
186
- if (data.filesAffected.length > 10) {
187
- result += `\n- ... and ${data.filesAffected.length - 10} more`;
188
- }
189
- result += "\n";
190
- }
191
- if (data.keyActions && data.keyActions.length > 0) {
192
- result += `\n**Key Actions:**\n`;
193
- result += data.keyActions
194
- .map((a, i) => `${i + 1}. ${a}`)
195
- .join("\n");
196
- result += "\n";
197
- }
198
- return result;
199
- },
200
- get_developer_profile: async (_args, ctx) => {
201
- const response = await ctx.api.get(`/api/developer-profile`, { headers: { "X-Project-Name": ctx.projectName } });
202
- const p = response.data;
203
- if (!p.totalToolCalls) {
204
- return "No usage data yet. Use tools to build your developer profile.";
205
- }
206
- let result = `**Developer Profile** (${p.totalSessions} sessions, ${p.totalToolCalls} tool calls)\n\n`;
207
- if (p.frequentFiles.length > 0) {
208
- result += "**Frequent Files:**\n";
209
- result += p.frequentFiles.slice(0, 10).map((f) => `- ${f.file} (${f.count}x)`).join("\n");
210
- result += "\n\n";
211
- }
212
- if (p.preferredTools.length > 0) {
213
- result += "**Preferred Tools:**\n";
214
- result += p.preferredTools.slice(0, 8).map((t) => `- ${t.tool}: ${t.count}x (avg ${Math.round(t.avgDurationMs)}ms)`).join("\n");
215
- result += "\n\n";
216
- }
217
- if (p.peakHours.length > 0) {
218
- result += "**Peak Hours:** ";
219
- result += p.peakHours.map((h) => `${h.hour}:00 (${h.count})`).join(", ");
220
- result += "\n\n";
221
- }
222
- if (p.commonPatterns.length > 0) {
223
- result += "**Common Patterns:**\n";
224
- result += p.commonPatterns.slice(0, 5).map((q) => `- "${truncate(q, 60)}"`).join("\n");
225
- result += "\n";
226
- }
227
- return result;
228
- },
229
- analyze_usage_patterns: async (args, ctx) => {
230
- const { days = 7 } = args;
231
- const response = await ctx.api.get(`/api/patterns/${ctx.projectName}?days=${days}`);
232
- const data = response.data;
233
- let result = `**Usage Patterns for ${ctx.projectName}** (last ${days} days)\n\n`;
234
- if (data.insights && data.insights.length > 0) {
235
- result += `**Insights:**\n`;
236
- result += data.insights
237
- .map((insight) => `- ${insight}`)
238
- .join("\n");
239
- result += "\n\n";
240
- }
241
- if (data.commonWorkflows && data.commonWorkflows.length > 0) {
242
- result += `**Common Workflows:**\n`;
243
- result += data.commonWorkflows
244
- .map((w) => `- ${w.tools.join(" -> ")} (${w.count}x, ${(w.successRate * 100).toFixed(0)}% success)`)
245
- .join("\n");
246
- result += "\n\n";
247
- }
248
- if (data.detectedPatterns && data.detectedPatterns.length > 0) {
249
- result += `**Detected Patterns:**\n`;
250
- result += data.detectedPatterns
251
- .map((p) => `- **${p.name}:** ${p.description}\n *Suggestion:* ${p.suggestion}`)
252
- .join("\n");
253
- result += "\n\n";
254
- }
255
- if (data.recommendations && data.recommendations.length > 0) {
256
- result += `**Recommendations:**\n`;
257
- result += data.recommendations
258
- .map((r, i) => `${i + 1}. ${r}`)
259
- .join("\n");
260
- result += "\n";
261
- }
262
- return result;
263
- },
264
- start_session: async (args, ctx) => {
265
- const { sessionId, initialContext, resumeFrom } = args;
266
- const response = await ctx.api.post("/api/session/start", {
267
- projectName: ctx.projectName,
268
- sessionId,
269
- initialContext,
270
- resumeFrom,
271
- });
272
- const data = response.data;
273
- const session = data.session;
274
- // Extract fields — API returns { session: { sessionId, startedAt, ... } }
275
- const sid = session?.sessionId || data.sessionId;
276
- const started = session?.startedAt || data.started;
277
- const resumedFrom = session?.metadata?.resumedFrom || data.resumedFrom;
278
- const initialFiles = session?.currentFiles || data.initialFiles;
279
- // Update shared context with active session ID
280
- if (sharedCtx && sid) {
281
- sharedCtx.activeSessionId = sid;
282
- }
283
- let result = `**Session Started**\n\n`;
284
- result += `- **Session ID:** ${sid}\n`;
285
- result += `- **Started:** ${started}\n`;
286
- if (resumedFrom) {
287
- result += `- **Resumed From:** ${resumedFrom}\n`;
288
- }
289
- if (initialFiles && initialFiles.length > 0) {
290
- result += `\n**Initial Files:**\n`;
291
- result += initialFiles
292
- .map((f) => `- ${f}`)
293
- .join("\n");
294
- result += "\n";
295
- }
296
- // Include prefetch stats if available
297
- if (session?.metadata?.prefetchStats) {
298
- const pf = session.metadata.prefetchStats;
299
- result += `\n**Predictive Prefetch:** ${pf.prefetchedCount ?? 0} resources prefetched\n`;
300
- }
301
- // Include briefing if available (Sprint E)
302
- if (data.briefing) {
303
- result += `\n**Session Briefing:**\n${data.briefing}\n`;
304
- }
305
- return result;
306
- },
307
- get_session_context: async (args, ctx) => {
308
- const { sessionId } = args;
309
- const response = await ctx.api.get(`/api/session/${sessionId}`);
310
- const data = response.data;
311
- let result = `**Session Context**\n\n`;
312
- result += `- **Session ID:** ${data.sessionId}\n`;
313
- result += `- **Status:** ${data.status}\n`;
314
- result += `- **Started At:** ${data.startedAt}\n`;
315
- result += `- **Last Activity:** ${data.lastActivity}\n`;
316
- if (data.currentFiles && data.currentFiles.length > 0) {
317
- const files = data.currentFiles.slice(0, 10);
318
- result += `\n**Current Files:**\n`;
319
- result += files.map((f) => `- ${f}`).join("\n");
320
- if (data.currentFiles.length > 10) {
321
- result += `\n- ... and ${data.currentFiles.length - 10} more`;
322
- }
323
- result += "\n";
324
- }
325
- if (data.toolsUsed && data.toolsUsed.length > 0) {
326
- result += `\n**Tools Used:** ${data.toolsUsed.join(", ")}\n`;
327
- }
328
- if (data.activeFeatures && data.activeFeatures.length > 0) {
329
- result += `\n**Active Features:**\n`;
330
- result += data.activeFeatures
331
- .map((f) => `- ${f}`)
332
- .join("\n");
333
- result += "\n";
334
- }
335
- if (data.pendingLearnings && data.pendingLearnings.length > 0) {
336
- const learnings = data.pendingLearnings.slice(0, 5);
337
- result += `\n**Pending Learnings:**\n`;
338
- result += learnings
339
- .map((l) => `- ${truncate(l, 80)}`)
340
- .join("\n");
341
- if (data.pendingLearnings.length > 5) {
342
- result += `\n- ... and ${data.pendingLearnings.length - 5} more`;
343
- }
344
- result += "\n";
345
- }
346
- return result;
347
- },
348
- end_session: async (args, ctx) => {
349
- const { sessionId, summary, autoSaveLearnings, feedback } = args;
350
- const response = await ctx.api.post(`/api/session/${sessionId}/end`, {
351
- summary,
352
- autoSaveLearnings: autoSaveLearnings !== undefined ? autoSaveLearnings : true,
353
- feedback,
354
- });
355
- const data = response.data;
356
- // Clear active session ID
357
- if (sharedCtx) {
358
- sharedCtx.activeSessionId = undefined;
359
- }
360
- let result = `**Session Ended**\n\n`;
361
- if (data.summary) {
362
- result += `**Summary:** ${data.summary}\n\n`;
363
- }
364
- if (data.duration !== undefined) {
365
- result += `- **Duration:** ${data.duration} minutes\n`;
366
- }
367
- if (data.toolsUsedCount !== undefined) {
368
- result += `- **Tools Used:** ${data.toolsUsedCount}\n`;
369
- }
370
- if (data.filesAffectedCount !== undefined) {
371
- result += `- **Files Affected:** ${data.filesAffectedCount}\n`;
372
- }
373
- if (data.queriesCount !== undefined) {
374
- result += `- **Queries:** ${data.queriesCount}\n`;
375
- }
376
- if (data.learningsSaved !== undefined) {
377
- result += `- **Learnings Saved:** ${data.learningsSaved}\n`;
378
- }
379
- if (data.filesAffected && data.filesAffected.length > 0) {
380
- const files = data.filesAffected.slice(0, 10);
381
- result += `\n**Files Affected:**\n`;
382
- result += files.map((f) => `- ${f}`).join("\n");
383
- if (data.filesAffected.length > 10) {
384
- result += `\n- ... and ${data.filesAffected.length - 10} more`;
385
- }
386
- result += "\n";
387
- }
388
- return result;
389
- },
390
- };
391
- return { tools, handlers };
392
335
  }