@recall_v3/mcp-server 0.1.0 → 3.0.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.
@@ -2,18 +2,22 @@
2
2
  * saveSession Tool Implementation
3
3
  *
4
4
  * Saves a summary of what was accomplished in this coding session.
5
- * Updates the team memory files via the Recall API.
5
+ * Encrypts content with team key and sends to Recall v3 API.
6
6
  */
7
7
 
8
8
  import { RecallApiClient, AuthenticationError, RecallApiError } from '../api/client.js';
9
9
  import { getApiBaseUrl, getApiToken, getTeamKey, setTeamKey } from '../config/index.js';
10
- import { encryptContent } from '../crypto/index.js';
10
+ import { encryptForApi } from '../crypto/index.js';
11
11
  import { successResponse, errorResponse, type ToolResponse } from './types.js';
12
12
  import { resolveProjectPath, getRepoInfo } from './utils.js';
13
- import type { SaveSessionRequest } from '@recall_v3/shared';
14
13
 
15
14
  export interface SaveSessionArgs {
16
- summary: string;
15
+ /** Pre-written summary (if transcript not provided) */
16
+ summary?: string;
17
+ /** Raw conversation transcript - will be summarized by AI */
18
+ transcript?: string;
19
+ /** Git repository path (defaults to cwd if not provided) */
20
+ projectPath?: string;
17
21
  decisions?: Array<{
18
22
  what: string;
19
23
  why: string;
@@ -24,17 +28,172 @@ export interface SaveSessionArgs {
24
28
  blockers?: string;
25
29
  }
26
30
 
31
+ /**
32
+ * Request body for the summarize API (matches actual API, not shared types)
33
+ */
34
+ interface SummarizeApiRequest {
35
+ transcript: string;
36
+ repo_name?: string;
37
+ developer?: string;
38
+ tool?: 'claude-code' | 'cursor' | 'copilot' | 'other';
39
+ tier?: 'team' | 'pro' | 'enterprise';
40
+ started_at?: string;
41
+ ended_at?: string;
42
+ }
43
+
44
+ /**
45
+ * Response from the summarize API (matches actual API response)
46
+ */
47
+ interface SummarizeApiResponse {
48
+ session_title: string;
49
+ summary: string;
50
+ detailed_summary?: string;
51
+ status: string;
52
+ tldr: {
53
+ summary: string;
54
+ status: string;
55
+ decisions: string[];
56
+ mistakes: string[];
57
+ tags: string[];
58
+ files_changed: string[];
59
+ };
60
+ decisions: Array<{
61
+ title: string;
62
+ what: string;
63
+ why: string;
64
+ alternatives: Array<{ option: string; rejected_because: string }>;
65
+ confidence: string;
66
+ }>;
67
+ failures: Array<{
68
+ title: string;
69
+ what_tried: string;
70
+ what_happened: string;
71
+ root_cause: string;
72
+ resolution: string;
73
+ }>;
74
+ lessons: Array<{
75
+ title: string;
76
+ lesson: string;
77
+ when_applies: string;
78
+ }>;
79
+ quality_metadata: {
80
+ understandingScore: number;
81
+ accuracyScore: number;
82
+ attempts: number;
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Build the full session content to encrypt
88
+ */
89
+ function buildSessionContent(args: SaveSessionArgs): string {
90
+ const parts: string[] = [];
91
+
92
+ parts.push('## Summary');
93
+ parts.push((args.summary || 'Session saved').trim());
94
+ parts.push('');
95
+
96
+ if (args.decisions && args.decisions.length > 0) {
97
+ parts.push('## Decisions');
98
+ for (const decision of args.decisions) {
99
+ parts.push(`- **${decision.what}**: ${decision.why}`);
100
+ }
101
+ parts.push('');
102
+ }
103
+
104
+ if (args.mistakes && args.mistakes.length > 0) {
105
+ parts.push('## Mistakes / Gotchas');
106
+ for (const mistake of args.mistakes) {
107
+ parts.push(`- ${mistake}`);
108
+ }
109
+ parts.push('');
110
+ }
111
+
112
+ if (args.filesChanged && args.filesChanged.length > 0) {
113
+ parts.push('## Files Changed');
114
+ for (const file of args.filesChanged) {
115
+ parts.push(`- ${file}`);
116
+ }
117
+ parts.push('');
118
+ }
119
+
120
+ if (args.nextSteps) {
121
+ parts.push('## Next Steps');
122
+ parts.push(args.nextSteps.trim());
123
+ parts.push('');
124
+ }
125
+
126
+ if (args.blockers) {
127
+ parts.push('## Blockers');
128
+ parts.push(args.blockers.trim());
129
+ parts.push('');
130
+ }
131
+
132
+ return parts.join('\n');
133
+ }
134
+
135
+ /**
136
+ * Build TLDR metadata for the session
137
+ */
138
+ function buildTldr(args: SaveSessionArgs): {
139
+ summary: string;
140
+ status: 'complete' | 'in-progress' | 'blocked';
141
+ decisions?: string[];
142
+ mistakes?: string[];
143
+ tags?: string[];
144
+ files_changed?: string[];
145
+ blockers?: string | null;
146
+ next_steps?: string | null;
147
+ } {
148
+ const tldr: {
149
+ summary: string;
150
+ status: 'complete' | 'in-progress' | 'blocked';
151
+ decisions?: string[];
152
+ mistakes?: string[];
153
+ tags?: string[];
154
+ files_changed?: string[];
155
+ blockers?: string | null;
156
+ next_steps?: string | null;
157
+ } = {
158
+ summary: (args.summary || 'Session saved').trim().substring(0, 200),
159
+ status: args.blockers ? 'blocked' : 'complete',
160
+ };
161
+
162
+ if (args.decisions && args.decisions.length > 0) {
163
+ tldr.decisions = args.decisions.map(d => d.what);
164
+ }
165
+
166
+ if (args.mistakes && args.mistakes.length > 0) {
167
+ tldr.mistakes = args.mistakes;
168
+ }
169
+
170
+ if (args.filesChanged && args.filesChanged.length > 0) {
171
+ tldr.files_changed = args.filesChanged;
172
+ }
173
+
174
+ if (args.nextSteps) {
175
+ tldr.next_steps = args.nextSteps.trim();
176
+ }
177
+
178
+ if (args.blockers) {
179
+ tldr.blockers = args.blockers.trim();
180
+ }
181
+
182
+ return tldr;
183
+ }
184
+
27
185
  /**
28
186
  * Execute the saveSession tool
29
187
  *
30
- * @param args - Tool arguments with session summary and metadata
188
+ * @param args - Tool arguments with session summary/transcript and metadata
31
189
  * @returns MCP tool response with save confirmation
32
190
  */
33
191
  export async function saveSession(args: SaveSessionArgs): Promise<ToolResponse> {
34
192
  try {
35
- // Validate required fields
36
- if (!args.summary || args.summary.trim().length === 0) {
37
- return errorResponse('Summary is required. Please provide a description of what was accomplished.');
193
+ // Validate required fields - need either summary or transcript
194
+ if ((!args.summary || args.summary.trim().length === 0) &&
195
+ (!args.transcript || args.transcript.trim().length === 0)) {
196
+ return errorResponse('Either summary or transcript is required. Please provide a description of what was accomplished or the session transcript.');
38
197
  }
39
198
 
40
199
  // Get API token
@@ -45,15 +204,16 @@ export async function saveSession(args: SaveSessionArgs): Promise<ToolResponse>
45
204
  );
46
205
  }
47
206
 
48
- // Resolve project path (use cwd since projectPath is not in args)
49
- const projectPath = resolveProjectPath(undefined);
207
+ // Resolve project path (use args.projectPath if provided, otherwise cwd)
208
+ const projectPath = resolveProjectPath(args.projectPath);
50
209
 
51
210
  // Get repo info from git
52
211
  const repoInfo = await getRepoInfo(projectPath);
53
212
  if (!repoInfo) {
54
213
  return errorResponse(
55
214
  `Could not determine repository info for: ${projectPath}\n` +
56
- 'Make sure this is a git repository with a remote origin.'
215
+ 'Make sure this is a git repository with a remote origin.\n' +
216
+ 'If running from Claude Code hooks, use --project-path to specify the repo path.'
57
217
  );
58
218
  }
59
219
 
@@ -74,64 +234,136 @@ export async function saveSession(args: SaveSessionArgs): Promise<ToolResponse>
74
234
  setTeamKey(teamId, teamKey);
75
235
  }
76
236
 
77
- // Build the session request
78
- const sessionRequest: SaveSessionRequest = {
79
- summary: args.summary.trim(),
80
- };
237
+ // If transcript provided, call summarize API to generate structured summary
238
+ let summarizedArgs = { ...args };
239
+ let aiSummary: SummarizeApiResponse | null = null;
81
240
 
82
- if (args.decisions && args.decisions.length > 0) {
83
- sessionRequest.decisions = args.decisions;
84
- }
241
+ if (args.transcript && args.transcript.trim().length >= 50) {
242
+ try {
243
+ // Build the API request
244
+ const now = new Date().toISOString();
245
+ const summarizeRequest: SummarizeApiRequest = {
246
+ transcript: args.transcript,
247
+ repo_name: repoInfo.fullName,
248
+ tool: 'claude-code',
249
+ started_at: now,
250
+ ended_at: now,
251
+ };
85
252
 
86
- if (args.mistakes && args.mistakes.length > 0) {
87
- sessionRequest.mistakes = args.mistakes;
88
- }
253
+ // Call the summarize API using the raw method to avoid type mismatch
254
+ aiSummary = await client.summarizeRaw<SummarizeApiRequest, SummarizeApiResponse>(summarizeRequest);
89
255
 
90
- if (args.filesChanged && args.filesChanged.length > 0) {
91
- sessionRequest.filesChanged = args.filesChanged;
256
+ // Update args with AI-generated content
257
+ summarizedArgs = {
258
+ ...args,
259
+ summary: aiSummary.tldr.summary || aiSummary.summary,
260
+ decisions: aiSummary.decisions.map(d => ({
261
+ what: d.what,
262
+ why: d.why,
263
+ })),
264
+ mistakes: aiSummary.tldr.mistakes,
265
+ filesChanged: aiSummary.tldr.files_changed,
266
+ nextSteps: undefined, // Could extract from lessons
267
+ blockers: aiSummary.tldr.status === 'blocked' ? 'Session blocked' : undefined,
268
+ };
269
+ } catch (error) {
270
+ // If summarization fails, fall back to provided summary or placeholder
271
+ console.error('[Recall] AI summarization failed, using fallback:', error);
272
+ if (!args.summary || args.summary.trim().length === 0) {
273
+ summarizedArgs.summary = 'Session saved (AI summarization unavailable)';
274
+ }
275
+ }
276
+ } else if (!args.summary || args.summary.trim().length === 0) {
277
+ // No transcript and no summary - this shouldn't happen due to validation
278
+ summarizedArgs.summary = 'Session saved';
92
279
  }
93
280
 
94
- if (args.nextSteps && args.nextSteps.trim().length > 0) {
95
- sessionRequest.nextSteps = args.nextSteps.trim();
96
- }
281
+ // Build the session content to encrypt
282
+ const sessionContent = buildSessionContent(summarizedArgs);
97
283
 
98
- if (args.blockers && args.blockers.trim().length > 0) {
99
- sessionRequest.blockers = args.blockers.trim();
100
- }
284
+ // Encrypt the content with team key
285
+ const encryptedContent = encryptForApi(sessionContent, teamKey);
101
286
 
102
- // Save session via API
103
- const response = await client.saveSession(repoId, sessionRequest);
287
+ // Build TLDR metadata (use summarized args if available)
288
+ const tldr = buildTldr(summarizedArgs);
104
289
 
105
- // Build success message
106
- const parts: string[] = [
107
- `Session saved successfully.`,
108
- ``,
109
- `Session ID: ${response.sessionId}`,
110
- `Repository: ${repoInfo.fullName}`,
111
- ];
290
+ // Create timestamps (session is "now")
291
+ const now = new Date().toISOString();
292
+
293
+ // Build the API request body (matches CreateSessionRequestBody in API)
294
+ const requestBody = {
295
+ encrypted_content: encryptedContent,
296
+ tldr,
297
+ started_at: now,
298
+ ended_at: now,
299
+ tool: 'claude-code' as const,
300
+ };
301
+
302
+ // Save session via API
303
+ const response = await client.saveSessionV3(repoId, requestBody);
112
304
 
113
- if (args.decisions && args.decisions.length > 0) {
114
- parts.push(`Decisions logged: ${args.decisions.length}`);
305
+ // Build a clean, informative success message
306
+ const parts: string[] = [];
307
+ parts.push(`Session saved to ${repoInfo.fullName}`);
308
+ if (aiSummary) {
309
+ parts.push(` (AI summarized)`);
115
310
  }
311
+ parts.push(``);
312
+ parts.push(` Saved:`);
313
+
314
+ // Summary (truncate if too long)
315
+ const summaryText = summarizedArgs.summary || 'Session saved';
316
+ const summaryPreview = summaryText.trim().length > 80
317
+ ? summaryText.trim().substring(0, 77) + '...'
318
+ : summaryText.trim();
319
+ parts.push(` - Summary: ${summaryPreview}`);
116
320
 
117
- if (args.mistakes && args.mistakes.length > 0) {
118
- parts.push(`Mistakes documented: ${args.mistakes.length}`);
321
+ // Key decisions (show first one if any)
322
+ if (summarizedArgs.decisions && summarizedArgs.decisions.length > 0) {
323
+ const firstDecision = summarizedArgs.decisions[0].what;
324
+ if (summarizedArgs.decisions.length === 1) {
325
+ parts.push(` - Decision: ${firstDecision}`);
326
+ } else {
327
+ parts.push(` - Decisions: ${firstDecision} (+${summarizedArgs.decisions.length - 1} more)`);
328
+ }
119
329
  }
120
330
 
121
- if (args.filesChanged && args.filesChanged.length > 0) {
122
- parts.push(`Files tracked: ${args.filesChanged.length}`);
331
+ // Blockers (if any)
332
+ if (summarizedArgs.blockers) {
333
+ const blockerPreview = summarizedArgs.blockers.trim().length > 60
334
+ ? summarizedArgs.blockers.trim().substring(0, 57) + '...'
335
+ : summarizedArgs.blockers.trim();
336
+ parts.push(` - Blocker: ${blockerPreview}`);
123
337
  }
124
338
 
125
- if (args.nextSteps) {
126
- parts.push(`Next steps: ${args.nextSteps}`);
339
+ // Next steps (if any)
340
+ if (summarizedArgs.nextSteps) {
341
+ const nextPreview = summarizedArgs.nextSteps.trim().length > 60
342
+ ? summarizedArgs.nextSteps.trim().substring(0, 57) + '...'
343
+ : summarizedArgs.nextSteps.trim();
344
+ parts.push(` - Next: ${nextPreview}`);
127
345
  }
128
346
 
129
- if (args.blockers) {
130
- parts.push(`Blockers: ${args.blockers}`);
347
+ // Files changed (count only if many)
348
+ if (summarizedArgs.filesChanged && summarizedArgs.filesChanged.length > 0) {
349
+ if (summarizedArgs.filesChanged.length <= 3) {
350
+ parts.push(` - Files: ${summarizedArgs.filesChanged.join(', ')}`);
351
+ } else {
352
+ parts.push(` - Files: ${summarizedArgs.filesChanged.length} files changed`);
353
+ }
131
354
  }
132
355
 
133
- parts.push(``);
134
- parts.push(`Team memory has been updated.`);
356
+ // Footer
357
+ parts.push(` - Updated team memory for next session`);
358
+
359
+ // Show warnings if any (non-critical issues that didn't prevent save)
360
+ if (response.warnings && response.warnings.length > 0) {
361
+ parts.push(``);
362
+ parts.push(` Warnings:`);
363
+ for (const warning of response.warnings) {
364
+ parts.push(` ⚠️ ${warning}`);
365
+ }
366
+ }
135
367
 
136
368
  return successResponse(parts.join('\n'));
137
369