@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.
@@ -3,38 +3,107 @@
3
3
  * saveSession Tool Implementation
4
4
  *
5
5
  * Saves a summary of what was accomplished in this coding session.
6
- * Updates the team memory files via the Recall API.
6
+ * Encrypts content with team key and sends to Recall v3 API.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.saveSession = saveSession;
10
10
  const client_js_1 = require("../api/client.js");
11
11
  const index_js_1 = require("../config/index.js");
12
+ const index_js_2 = require("../crypto/index.js");
12
13
  const types_js_1 = require("./types.js");
13
14
  const utils_js_1 = require("./utils.js");
15
+ /**
16
+ * Build the full session content to encrypt
17
+ */
18
+ function buildSessionContent(args) {
19
+ const parts = [];
20
+ parts.push('## Summary');
21
+ parts.push((args.summary || 'Session saved').trim());
22
+ parts.push('');
23
+ if (args.decisions && args.decisions.length > 0) {
24
+ parts.push('## Decisions');
25
+ for (const decision of args.decisions) {
26
+ parts.push(`- **${decision.what}**: ${decision.why}`);
27
+ }
28
+ parts.push('');
29
+ }
30
+ if (args.mistakes && args.mistakes.length > 0) {
31
+ parts.push('## Mistakes / Gotchas');
32
+ for (const mistake of args.mistakes) {
33
+ parts.push(`- ${mistake}`);
34
+ }
35
+ parts.push('');
36
+ }
37
+ if (args.filesChanged && args.filesChanged.length > 0) {
38
+ parts.push('## Files Changed');
39
+ for (const file of args.filesChanged) {
40
+ parts.push(`- ${file}`);
41
+ }
42
+ parts.push('');
43
+ }
44
+ if (args.nextSteps) {
45
+ parts.push('## Next Steps');
46
+ parts.push(args.nextSteps.trim());
47
+ parts.push('');
48
+ }
49
+ if (args.blockers) {
50
+ parts.push('## Blockers');
51
+ parts.push(args.blockers.trim());
52
+ parts.push('');
53
+ }
54
+ return parts.join('\n');
55
+ }
56
+ /**
57
+ * Build TLDR metadata for the session
58
+ */
59
+ function buildTldr(args) {
60
+ const tldr = {
61
+ summary: (args.summary || 'Session saved').trim().substring(0, 200),
62
+ status: args.blockers ? 'blocked' : 'complete',
63
+ };
64
+ if (args.decisions && args.decisions.length > 0) {
65
+ tldr.decisions = args.decisions.map(d => d.what);
66
+ }
67
+ if (args.mistakes && args.mistakes.length > 0) {
68
+ tldr.mistakes = args.mistakes;
69
+ }
70
+ if (args.filesChanged && args.filesChanged.length > 0) {
71
+ tldr.files_changed = args.filesChanged;
72
+ }
73
+ if (args.nextSteps) {
74
+ tldr.next_steps = args.nextSteps.trim();
75
+ }
76
+ if (args.blockers) {
77
+ tldr.blockers = args.blockers.trim();
78
+ }
79
+ return tldr;
80
+ }
14
81
  /**
15
82
  * Execute the saveSession tool
16
83
  *
17
- * @param args - Tool arguments with session summary and metadata
84
+ * @param args - Tool arguments with session summary/transcript and metadata
18
85
  * @returns MCP tool response with save confirmation
19
86
  */
20
87
  async function saveSession(args) {
21
88
  try {
22
- // Validate required fields
23
- if (!args.summary || args.summary.trim().length === 0) {
24
- return (0, types_js_1.errorResponse)('Summary is required. Please provide a description of what was accomplished.');
89
+ // Validate required fields - need either summary or transcript
90
+ if ((!args.summary || args.summary.trim().length === 0) &&
91
+ (!args.transcript || args.transcript.trim().length === 0)) {
92
+ return (0, types_js_1.errorResponse)('Either summary or transcript is required. Please provide a description of what was accomplished or the session transcript.');
25
93
  }
26
94
  // Get API token
27
95
  const token = (0, index_js_1.getApiToken)();
28
96
  if (!token) {
29
97
  return (0, types_js_1.errorResponse)('Not authenticated. Run `recall auth` to connect your account, or set RECALL_API_TOKEN environment variable.');
30
98
  }
31
- // Resolve project path (use cwd since projectPath is not in args)
32
- const projectPath = (0, utils_js_1.resolveProjectPath)(undefined);
99
+ // Resolve project path (use args.projectPath if provided, otherwise cwd)
100
+ const projectPath = (0, utils_js_1.resolveProjectPath)(args.projectPath);
33
101
  // Get repo info from git
34
102
  const repoInfo = await (0, utils_js_1.getRepoInfo)(projectPath);
35
103
  if (!repoInfo) {
36
104
  return (0, types_js_1.errorResponse)(`Could not determine repository info for: ${projectPath}\n` +
37
- 'Make sure this is a git repository with a remote origin.');
105
+ 'Make sure this is a git repository with a remote origin.\n' +
106
+ 'If running from Claude Code hooks, use --project-path to specify the repo path.');
38
107
  }
39
108
  // Create API client
40
109
  const client = new client_js_1.RecallApiClient({
@@ -50,51 +119,123 @@ async function saveSession(args) {
50
119
  teamKey = keyResponse.encryptionKey;
51
120
  (0, index_js_1.setTeamKey)(teamId, teamKey);
52
121
  }
53
- // Build the session request
54
- const sessionRequest = {
55
- summary: args.summary.trim(),
56
- };
57
- if (args.decisions && args.decisions.length > 0) {
58
- sessionRequest.decisions = args.decisions;
59
- }
60
- if (args.mistakes && args.mistakes.length > 0) {
61
- sessionRequest.mistakes = args.mistakes;
62
- }
63
- if (args.filesChanged && args.filesChanged.length > 0) {
64
- sessionRequest.filesChanged = args.filesChanged;
65
- }
66
- if (args.nextSteps && args.nextSteps.trim().length > 0) {
67
- sessionRequest.nextSteps = args.nextSteps.trim();
122
+ // If transcript provided, call summarize API to generate structured summary
123
+ let summarizedArgs = { ...args };
124
+ let aiSummary = null;
125
+ if (args.transcript && args.transcript.trim().length >= 50) {
126
+ try {
127
+ // Build the API request
128
+ const now = new Date().toISOString();
129
+ const summarizeRequest = {
130
+ transcript: args.transcript,
131
+ repo_name: repoInfo.fullName,
132
+ tool: 'claude-code',
133
+ started_at: now,
134
+ ended_at: now,
135
+ };
136
+ // Call the summarize API using the raw method to avoid type mismatch
137
+ aiSummary = await client.summarizeRaw(summarizeRequest);
138
+ // Update args with AI-generated content
139
+ summarizedArgs = {
140
+ ...args,
141
+ summary: aiSummary.tldr.summary || aiSummary.summary,
142
+ decisions: aiSummary.decisions.map(d => ({
143
+ what: d.what,
144
+ why: d.why,
145
+ })),
146
+ mistakes: aiSummary.tldr.mistakes,
147
+ filesChanged: aiSummary.tldr.files_changed,
148
+ nextSteps: undefined, // Could extract from lessons
149
+ blockers: aiSummary.tldr.status === 'blocked' ? 'Session blocked' : undefined,
150
+ };
151
+ }
152
+ catch (error) {
153
+ // If summarization fails, fall back to provided summary or placeholder
154
+ console.error('[Recall] AI summarization failed, using fallback:', error);
155
+ if (!args.summary || args.summary.trim().length === 0) {
156
+ summarizedArgs.summary = 'Session saved (AI summarization unavailable)';
157
+ }
158
+ }
68
159
  }
69
- if (args.blockers && args.blockers.trim().length > 0) {
70
- sessionRequest.blockers = args.blockers.trim();
160
+ else if (!args.summary || args.summary.trim().length === 0) {
161
+ // No transcript and no summary - this shouldn't happen due to validation
162
+ summarizedArgs.summary = 'Session saved';
71
163
  }
164
+ // Build the session content to encrypt
165
+ const sessionContent = buildSessionContent(summarizedArgs);
166
+ // Encrypt the content with team key
167
+ const encryptedContent = (0, index_js_2.encryptForApi)(sessionContent, teamKey);
168
+ // Build TLDR metadata (use summarized args if available)
169
+ const tldr = buildTldr(summarizedArgs);
170
+ // Create timestamps (session is "now")
171
+ const now = new Date().toISOString();
172
+ // Build the API request body (matches CreateSessionRequestBody in API)
173
+ const requestBody = {
174
+ encrypted_content: encryptedContent,
175
+ tldr,
176
+ started_at: now,
177
+ ended_at: now,
178
+ tool: 'claude-code',
179
+ };
72
180
  // Save session via API
73
- const response = await client.saveSession(repoId, sessionRequest);
74
- // Build success message
75
- const parts = [
76
- `Session saved successfully.`,
77
- ``,
78
- `Session ID: ${response.sessionId}`,
79
- `Repository: ${repoInfo.fullName}`,
80
- ];
81
- if (args.decisions && args.decisions.length > 0) {
82
- parts.push(`Decisions logged: ${args.decisions.length}`);
181
+ const response = await client.saveSessionV3(repoId, requestBody);
182
+ // Build a clean, informative success message
183
+ const parts = [];
184
+ parts.push(`Session saved to ${repoInfo.fullName}`);
185
+ if (aiSummary) {
186
+ parts.push(` (AI summarized)`);
83
187
  }
84
- if (args.mistakes && args.mistakes.length > 0) {
85
- parts.push(`Mistakes documented: ${args.mistakes.length}`);
188
+ parts.push(``);
189
+ parts.push(` Saved:`);
190
+ // Summary (truncate if too long)
191
+ const summaryText = summarizedArgs.summary || 'Session saved';
192
+ const summaryPreview = summaryText.trim().length > 80
193
+ ? summaryText.trim().substring(0, 77) + '...'
194
+ : summaryText.trim();
195
+ parts.push(` - Summary: ${summaryPreview}`);
196
+ // Key decisions (show first one if any)
197
+ if (summarizedArgs.decisions && summarizedArgs.decisions.length > 0) {
198
+ const firstDecision = summarizedArgs.decisions[0].what;
199
+ if (summarizedArgs.decisions.length === 1) {
200
+ parts.push(` - Decision: ${firstDecision}`);
201
+ }
202
+ else {
203
+ parts.push(` - Decisions: ${firstDecision} (+${summarizedArgs.decisions.length - 1} more)`);
204
+ }
205
+ }
206
+ // Blockers (if any)
207
+ if (summarizedArgs.blockers) {
208
+ const blockerPreview = summarizedArgs.blockers.trim().length > 60
209
+ ? summarizedArgs.blockers.trim().substring(0, 57) + '...'
210
+ : summarizedArgs.blockers.trim();
211
+ parts.push(` - Blocker: ${blockerPreview}`);
86
212
  }
87
- if (args.filesChanged && args.filesChanged.length > 0) {
88
- parts.push(`Files tracked: ${args.filesChanged.length}`);
213
+ // Next steps (if any)
214
+ if (summarizedArgs.nextSteps) {
215
+ const nextPreview = summarizedArgs.nextSteps.trim().length > 60
216
+ ? summarizedArgs.nextSteps.trim().substring(0, 57) + '...'
217
+ : summarizedArgs.nextSteps.trim();
218
+ parts.push(` - Next: ${nextPreview}`);
89
219
  }
90
- if (args.nextSteps) {
91
- parts.push(`Next steps: ${args.nextSteps}`);
220
+ // Files changed (count only if many)
221
+ if (summarizedArgs.filesChanged && summarizedArgs.filesChanged.length > 0) {
222
+ if (summarizedArgs.filesChanged.length <= 3) {
223
+ parts.push(` - Files: ${summarizedArgs.filesChanged.join(', ')}`);
224
+ }
225
+ else {
226
+ parts.push(` - Files: ${summarizedArgs.filesChanged.length} files changed`);
227
+ }
92
228
  }
93
- if (args.blockers) {
94
- parts.push(`Blockers: ${args.blockers}`);
229
+ // Footer
230
+ parts.push(` - Updated team memory for next session`);
231
+ // Show warnings if any (non-critical issues that didn't prevent save)
232
+ if (response.warnings && response.warnings.length > 0) {
233
+ parts.push(``);
234
+ parts.push(` Warnings:`);
235
+ for (const warning of response.warnings) {
236
+ parts.push(` ⚠️ ${warning}`);
237
+ }
95
238
  }
96
- parts.push(``);
97
- parts.push(`Team memory has been updated.`);
98
239
  return (0, types_js_1.successResponse)(parts.join('\n'));
99
240
  }
100
241
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recall_v3/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "Recall MCP Server - Team memory for AI coding assistants",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
package/src/api/client.ts CHANGED
@@ -94,16 +94,18 @@ export class RecallApiClient {
94
94
  private async request<T>(
95
95
  method: 'GET' | 'POST' | 'PUT' | 'DELETE',
96
96
  path: string,
97
- body?: unknown
97
+ body?: unknown,
98
+ options?: { timeout?: number }
98
99
  ): Promise<T> {
99
100
  const url = `${this.baseUrl}${path}`;
101
+ const timeout = options?.timeout ?? this.timeout;
100
102
 
101
103
  let lastError: Error | undefined;
102
104
 
103
105
  for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
104
106
  try {
105
107
  const controller = new AbortController();
106
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
108
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
107
109
 
108
110
  try {
109
111
  const response = await fetch(url, {
@@ -139,13 +141,20 @@ export class RecallApiClient {
139
141
  }
140
142
 
141
143
  // Parse successful response
142
- const data = await response.json() as ApiResponse<T>;
143
-
144
- if (!data.success && data.error) {
145
- throw new RecallApiError(data.error.message, data.error.code, response.status, false);
144
+ // API may return { success: true, data: T } or T directly
145
+ const rawData = await response.json();
146
+
147
+ // Check if it's a wrapped response
148
+ if (rawData && typeof rawData === 'object' && 'success' in rawData) {
149
+ const wrapped = rawData as ApiResponse<T>;
150
+ if (!wrapped.success && wrapped.error) {
151
+ throw new RecallApiError(wrapped.error.message, wrapped.error.code, response.status, false);
152
+ }
153
+ return wrapped.data as T;
146
154
  }
147
155
 
148
- return data.data as T;
156
+ // Direct response (not wrapped)
157
+ return rawData as T;
149
158
  } finally {
150
159
  clearTimeout(timeoutId);
151
160
  }
@@ -218,31 +227,141 @@ export class RecallApiClient {
218
227
  * Called by saveSession tool after reading JSONL from disk
219
228
  */
220
229
  async summarize(request: SummarizeRequest): Promise<SummarizeResponse> {
221
- return this.request<SummarizeResponse>('POST', '/api/v1/summarize', request);
230
+ return this.request<SummarizeResponse>('POST', '/v1/summarize', request);
231
+ }
232
+
233
+ /**
234
+ * Summarize a session transcript using the actual API format
235
+ * This bypasses the strict shared types which don't match the real API
236
+ * Uses 2 minute timeout since AI summarization can take time
237
+ */
238
+ async summarizeRaw<TRequest, TResponse>(request: TRequest): Promise<TResponse> {
239
+ return this.request<TResponse>('POST', '/v1/summarize', request, { timeout: 120000 });
222
240
  }
223
241
 
224
242
  /**
225
243
  * Save a session with manual input (not from transcript)
226
244
  * Used when user provides summary directly via MCP tool
245
+ * @deprecated Use saveSessionV3 for proper encrypted format
227
246
  */
228
247
  async saveSession(repoId: string, data: SaveSessionRequest): Promise<SaveSessionResponse> {
229
- return this.request<SaveSessionResponse>('POST', `/api/v1/repos/${repoId}/sessions`, data);
248
+ return this.request<SaveSessionResponse>('POST', `/v1/repos/${repoId}/sessions`, data);
249
+ }
250
+
251
+ /**
252
+ * Save a session with encrypted content (v3 format)
253
+ * This is the correct format for the v3 API
254
+ */
255
+ async saveSessionV3(repoId: string, data: {
256
+ encrypted_content: string;
257
+ tldr: {
258
+ summary: string;
259
+ status: 'complete' | 'in-progress' | 'blocked';
260
+ decisions?: string[];
261
+ mistakes?: string[];
262
+ tags?: string[];
263
+ files_changed?: string[];
264
+ blockers?: string | null;
265
+ next_steps?: string | null;
266
+ };
267
+ started_at: string;
268
+ ended_at: string;
269
+ tool?: string;
270
+ }): Promise<{ id: string; created_at: string; warnings?: string[] }> {
271
+ return this.request<{ id: string; created_at: string; warnings?: string[] }>('POST', `/v1/repos/${repoId}/sessions`, data);
230
272
  }
231
273
 
232
274
  /**
233
275
  * Get context for a repository (context.md)
234
276
  * Returns the distilled team brain for this repo
277
+ * @deprecated Use getContextV3 for proper v3 API format
235
278
  */
236
279
  async getContext(repoId: string): Promise<GetContextResponse> {
237
- return this.request<GetContextResponse>('GET', `/api/v1/repos/${repoId}/context`);
280
+ return this.request<GetContextResponse>('GET', `/v1/repos/${repoId}/context`);
281
+ }
282
+
283
+ /**
284
+ * Get context for a repository (v3 format)
285
+ * Returns sessions with encrypted content for client-side decryption
286
+ */
287
+ async getContextV3(repoId: string): Promise<{
288
+ sessions: Array<{
289
+ id: string;
290
+ encrypted_content: string;
291
+ started_at: string;
292
+ ended_at: string;
293
+ status: string;
294
+ tldr_summary: string | null;
295
+ user: {
296
+ id: string;
297
+ name: string | null;
298
+ github_username: string | null;
299
+ avatar_url: string | null;
300
+ };
301
+ }>;
302
+ cached_context: string;
303
+ updated_at: string;
304
+ session_count: number;
305
+ tier: string;
306
+ }> {
307
+ return this.request('GET', `/v1/repos/${repoId}/context`);
238
308
  }
239
309
 
240
310
  /**
241
311
  * Get history for a repository (context.md + history.md)
242
312
  * Returns more detail than getContext
313
+ * @deprecated Use getHistoryV3 for proper v3 API format
243
314
  */
244
315
  async getHistory(repoId: string): Promise<GetHistoryResponse> {
245
- return this.request<GetHistoryResponse>('GET', `/api/v1/repos/${repoId}/history`);
316
+ return this.request<GetHistoryResponse>('GET', `/v1/repos/${repoId}/history`);
317
+ }
318
+
319
+ /**
320
+ * Get history for a repository (v3 format)
321
+ * Returns sessions, decisions, and mistakes with encrypted content
322
+ */
323
+ async getHistoryV3(repoId: string, days: number = 30): Promise<{
324
+ sessions: Array<{
325
+ id: string;
326
+ encrypted_content: string;
327
+ started_at: string;
328
+ ended_at: string;
329
+ status: string;
330
+ tldr_summary: string | null;
331
+ user: {
332
+ id: string;
333
+ name: string | null;
334
+ github_username: string | null;
335
+ avatar_url: string | null;
336
+ };
337
+ }>;
338
+ decisions: Array<{
339
+ id: string;
340
+ title: string;
341
+ encrypted_content: string;
342
+ created_at: string;
343
+ user: {
344
+ id: string;
345
+ name: string | null;
346
+ github_username: string | null;
347
+ avatar_url: string | null;
348
+ };
349
+ }>;
350
+ mistakes: Array<{
351
+ id: string;
352
+ title: string;
353
+ encrypted_content: string;
354
+ created_at: string;
355
+ user: {
356
+ id: string;
357
+ name: string | null;
358
+ github_username: string | null;
359
+ avatar_url: string | null;
360
+ };
361
+ }>;
362
+ token_warning?: string;
363
+ }> {
364
+ return this.request('GET', `/v1/repos/${repoId}/history?days=${days}`);
246
365
  }
247
366
 
248
367
  /**
@@ -250,14 +369,14 @@ export class RecallApiClient {
250
369
  * WARNING: Can be very large, uses many tokens
251
370
  */
252
371
  async getTranscripts(repoId: string): Promise<GetTranscriptsResponse> {
253
- return this.request<GetTranscriptsResponse>('GET', `/api/v1/repos/${repoId}/transcripts`);
372
+ return this.request<GetTranscriptsResponse>('GET', `/v1/repos/${repoId}/transcripts`);
254
373
  }
255
374
 
256
375
  /**
257
376
  * Log a decision for a repository
258
377
  */
259
378
  async logDecision(repoId: string, data: LogDecisionRequest): Promise<LogDecisionResponse> {
260
- return this.request<LogDecisionResponse>('POST', `/api/v1/repos/${repoId}/decisions`, data);
379
+ return this.request<LogDecisionResponse>('POST', `/v1/repos/${repoId}/decisions`, data);
261
380
  }
262
381
 
263
382
  /**
@@ -265,21 +384,39 @@ export class RecallApiClient {
265
384
  * Used to decrypt content locally
266
385
  */
267
386
  async getTeamKey(teamId: string): Promise<TeamKeyResponse> {
268
- return this.request<TeamKeyResponse>('GET', `/api/v1/teams/${teamId}/key`);
387
+ // API returns { hasAccess, key, keyVersion, teamId, teamName, teamSlug, tier }
388
+ // Map to TeamKeyResponse type
389
+ const response = await this.request<{
390
+ hasAccess: boolean;
391
+ key: string;
392
+ keyVersion: number;
393
+ teamId: string;
394
+ teamName: string;
395
+ teamSlug: string;
396
+ tier: string;
397
+ }>('GET', '/v1/keys/team');
398
+
399
+ return {
400
+ teamId: response.teamId,
401
+ encryptionKey: response.key,
402
+ keyVersion: response.keyVersion,
403
+ teamName: response.teamName,
404
+ tier: response.tier as 'team' | 'pro' | 'enterprise',
405
+ };
269
406
  }
270
407
 
271
408
  /**
272
409
  * List teams the user belongs to
273
410
  */
274
411
  async listTeams(): Promise<ListTeamsResponse> {
275
- return this.request<ListTeamsResponse>('GET', '/api/v1/teams');
412
+ return this.request<ListTeamsResponse>('GET', '/v1/teams');
276
413
  }
277
414
 
278
415
  /**
279
416
  * Get authentication status
280
417
  */
281
418
  async getStatus(): Promise<RecallStatusResponse> {
282
- return this.request<RecallStatusResponse>('GET', '/api/v1/status');
419
+ return this.request<RecallStatusResponse>('GET', '/v1/status');
283
420
  }
284
421
 
285
422
  /**
@@ -287,7 +424,7 @@ export class RecallApiClient {
287
424
  * Returns the repo ID for subsequent API calls
288
425
  */
289
426
  async resolveRepo(fullName: string, defaultBranch?: string): Promise<{ repoId: string; teamId: string }> {
290
- return this.request<{ repoId: string; teamId: string }>('POST', '/api/v1/repos/resolve', {
427
+ return this.request<{ repoId: string; teamId: string }>('POST', '/v1/repos/resolve', {
291
428
  fullName,
292
429
  defaultBranch,
293
430
  });