@recall_v3/mcp-server 0.1.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 (50) hide show
  1. package/dist/api/client.d.ts +111 -0
  2. package/dist/api/client.d.ts.map +1 -0
  3. package/dist/api/client.js +244 -0
  4. package/dist/config/index.d.ts +89 -0
  5. package/dist/config/index.d.ts.map +1 -0
  6. package/dist/config/index.js +256 -0
  7. package/dist/crypto/index.d.ts +56 -0
  8. package/dist/crypto/index.d.ts.map +1 -0
  9. package/dist/crypto/index.js +224 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +189 -0
  13. package/dist/tools/getContext.d.ts +18 -0
  14. package/dist/tools/getContext.d.ts.map +1 -0
  15. package/dist/tools/getContext.js +87 -0
  16. package/dist/tools/getHistory.d.ts +18 -0
  17. package/dist/tools/getHistory.d.ts.map +1 -0
  18. package/dist/tools/getHistory.js +97 -0
  19. package/dist/tools/getTranscripts.d.ts +19 -0
  20. package/dist/tools/getTranscripts.d.ts.map +1 -0
  21. package/dist/tools/getTranscripts.js +129 -0
  22. package/dist/tools/index.d.ts +13 -0
  23. package/dist/tools/index.d.ts.map +1 -0
  24. package/dist/tools/index.js +37 -0
  25. package/dist/tools/logDecision.d.ts +19 -0
  26. package/dist/tools/logDecision.d.ts.map +1 -0
  27. package/dist/tools/logDecision.js +92 -0
  28. package/dist/tools/saveSession.d.ts +26 -0
  29. package/dist/tools/saveSession.d.ts.map +1 -0
  30. package/dist/tools/saveSession.js +115 -0
  31. package/dist/tools/types.d.ts +32 -0
  32. package/dist/tools/types.d.ts.map +1 -0
  33. package/dist/tools/types.js +33 -0
  34. package/dist/tools/utils.d.ts +52 -0
  35. package/dist/tools/utils.d.ts.map +1 -0
  36. package/dist/tools/utils.js +238 -0
  37. package/package.json +46 -0
  38. package/src/api/client.ts +295 -0
  39. package/src/config/index.ts +247 -0
  40. package/src/crypto/index.ts +207 -0
  41. package/src/index.ts +232 -0
  42. package/src/tools/getContext.ts +106 -0
  43. package/src/tools/getHistory.ts +118 -0
  44. package/src/tools/getTranscripts.ts +150 -0
  45. package/src/tools/index.ts +13 -0
  46. package/src/tools/logDecision.ts +118 -0
  47. package/src/tools/saveSession.ts +159 -0
  48. package/src/tools/types.ts +47 -0
  49. package/src/tools/utils.ts +226 -0
  50. package/tsconfig.json +14 -0
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Recall Encryption Utilities
3
+ *
4
+ * AES-256-GCM encryption/decryption compatible with Web Crypto API.
5
+ * Uses Node.js crypto module for server-side operations.
6
+ */
7
+
8
+ import * as crypto from 'node:crypto';
9
+ import type { EncryptedPayload } from '@recall_v3/shared';
10
+
11
+ // AES-256-GCM constants
12
+ const ALGORITHM = 'aes-256-gcm';
13
+ const IV_LENGTH = 12; // 96 bits for GCM
14
+ const TAG_LENGTH = 16; // 128 bits for GCM auth tag
15
+
16
+ /**
17
+ * Generate a cryptographically secure random IV
18
+ */
19
+ export function generateIV(): Uint8Array {
20
+ return crypto.randomBytes(IV_LENGTH);
21
+ }
22
+
23
+ /**
24
+ * Generate a new AES-256 encryption key
25
+ * Returns base64-encoded key
26
+ */
27
+ export function generateKey(): string {
28
+ const key = crypto.randomBytes(32); // 256 bits
29
+ return key.toString('base64');
30
+ }
31
+
32
+ /**
33
+ * Encrypt plaintext using AES-256-GCM
34
+ *
35
+ * @param plaintext - The string to encrypt
36
+ * @param keyBase64 - Base64-encoded 256-bit key
37
+ * @returns EncryptedPayload with base64-encoded ciphertext, iv, and tag
38
+ */
39
+ export function encrypt(plaintext: string, keyBase64: string): EncryptedPayload {
40
+ // Decode the key
41
+ const key = Buffer.from(keyBase64, 'base64');
42
+
43
+ if (key.length !== 32) {
44
+ throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
45
+ }
46
+
47
+ // Generate a random IV
48
+ const iv = generateIV();
49
+
50
+ // Create cipher
51
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv, {
52
+ authTagLength: TAG_LENGTH,
53
+ });
54
+
55
+ // Encrypt
56
+ const encrypted = Buffer.concat([
57
+ cipher.update(plaintext, 'utf8'),
58
+ cipher.final(),
59
+ ]);
60
+
61
+ // Get auth tag
62
+ const tag = cipher.getAuthTag();
63
+
64
+ // Return base64-encoded values
65
+ return {
66
+ ciphertext: encrypted.toString('base64'),
67
+ iv: Buffer.from(iv).toString('base64'),
68
+ tag: tag.toString('base64'),
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Decrypt an encrypted payload using AES-256-GCM
74
+ *
75
+ * @param payload - EncryptedPayload with base64-encoded values
76
+ * @param keyBase64 - Base64-encoded 256-bit key
77
+ * @returns Decrypted plaintext string
78
+ */
79
+ export function decrypt(payload: EncryptedPayload, keyBase64: string): string {
80
+ // Decode all components
81
+ const key = Buffer.from(keyBase64, 'base64');
82
+ const iv = Buffer.from(payload.iv, 'base64');
83
+ const ciphertext = Buffer.from(payload.ciphertext, 'base64');
84
+ const tag = Buffer.from(payload.tag, 'base64');
85
+
86
+ if (key.length !== 32) {
87
+ throw new Error(`Invalid key length: expected 32 bytes, got ${key.length}`);
88
+ }
89
+
90
+ if (iv.length !== IV_LENGTH) {
91
+ throw new Error(`Invalid IV length: expected ${IV_LENGTH} bytes, got ${iv.length}`);
92
+ }
93
+
94
+ if (tag.length !== TAG_LENGTH) {
95
+ throw new Error(`Invalid tag length: expected ${TAG_LENGTH} bytes, got ${tag.length}`);
96
+ }
97
+
98
+ // Create decipher
99
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, {
100
+ authTagLength: TAG_LENGTH,
101
+ });
102
+
103
+ // Set auth tag
104
+ decipher.setAuthTag(tag);
105
+
106
+ // Decrypt
107
+ try {
108
+ const decrypted = Buffer.concat([
109
+ decipher.update(ciphertext),
110
+ decipher.final(),
111
+ ]);
112
+
113
+ return decrypted.toString('utf8');
114
+ } catch (error) {
115
+ // GCM authentication failure
116
+ if (error instanceof Error && error.message.includes('Unsupported state')) {
117
+ throw new Error('Decryption failed: authentication tag mismatch');
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Parse an encrypted string into an EncryptedPayload
125
+ * The format is: base64(iv):base64(tag):base64(ciphertext)
126
+ * This is an alternative compact format for storage
127
+ */
128
+ export function parseEncryptedString(encrypted: string): EncryptedPayload {
129
+ const parts = encrypted.split(':');
130
+ if (parts.length !== 3) {
131
+ throw new Error('Invalid encrypted string format');
132
+ }
133
+
134
+ return {
135
+ iv: parts[0],
136
+ tag: parts[1],
137
+ ciphertext: parts[2],
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Serialize an EncryptedPayload to a compact string
143
+ * Format: base64(iv):base64(tag):base64(ciphertext)
144
+ */
145
+ export function serializeEncrypted(payload: EncryptedPayload): string {
146
+ return `${payload.iv}:${payload.tag}:${payload.ciphertext}`;
147
+ }
148
+
149
+ /**
150
+ * Check if a string looks like an encrypted payload
151
+ */
152
+ export function isEncryptedString(str: string): boolean {
153
+ // Check for compact format (iv:tag:ciphertext)
154
+ if (str.includes(':')) {
155
+ const parts = str.split(':');
156
+ if (parts.length === 3) {
157
+ // Each part should be valid base64
158
+ return parts.every(part => {
159
+ try {
160
+ Buffer.from(part, 'base64');
161
+ return true;
162
+ } catch {
163
+ return false;
164
+ }
165
+ });
166
+ }
167
+ }
168
+
169
+ // Check for JSON format
170
+ try {
171
+ const parsed = JSON.parse(str) as unknown;
172
+ return (
173
+ typeof parsed === 'object' &&
174
+ parsed !== null &&
175
+ 'ciphertext' in parsed &&
176
+ 'iv' in parsed &&
177
+ 'tag' in parsed
178
+ );
179
+ } catch {
180
+ return false;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Decrypt content that might be in either format (JSON or compact string)
186
+ */
187
+ export function decryptContent(encrypted: string, keyBase64: string): string {
188
+ let payload: EncryptedPayload;
189
+
190
+ // Try JSON format first
191
+ try {
192
+ payload = JSON.parse(encrypted) as EncryptedPayload;
193
+ } catch {
194
+ // Try compact string format
195
+ payload = parseEncryptedString(encrypted);
196
+ }
197
+
198
+ return decrypt(payload, keyBase64);
199
+ }
200
+
201
+ /**
202
+ * Encrypt content and return as JSON string
203
+ */
204
+ export function encryptContent(plaintext: string, keyBase64: string): string {
205
+ const payload = encrypt(plaintext, keyBase64);
206
+ return JSON.stringify(payload);
207
+ }
package/src/index.ts ADDED
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Recall MCP Server v3
5
+ * Local MCP server for AI coding assistants
6
+ *
7
+ * This package provides the local MCP server that connects
8
+ * to the Recall API for team memory synchronization.
9
+ */
10
+
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
+ import {
14
+ CallToolRequestSchema,
15
+ ListToolsRequestSchema,
16
+ type CallToolResult,
17
+ } from '@modelcontextprotocol/sdk/types.js';
18
+
19
+ // Tool implementations
20
+ import {
21
+ getContext,
22
+ getHistory,
23
+ getTranscripts,
24
+ saveSession,
25
+ logDecision,
26
+ type GetContextArgs,
27
+ type GetHistoryArgs,
28
+ type GetTranscriptsArgs,
29
+ type SaveSessionArgs,
30
+ type LogDecisionArgs,
31
+ } from './tools/index.js';
32
+
33
+ // Tool definitions
34
+ const TOOLS = [
35
+ {
36
+ name: 'recall_get_context',
37
+ description:
38
+ "Get team brain (context.md) for the current repository. This is the distilled current state - loads automatically at every session start. Use recall_get_history for the full encyclopedia.",
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ projectPath: {
43
+ type: 'string',
44
+ description:
45
+ 'Optional: explicit path to the project root. If not provided, uses current working directory.',
46
+ },
47
+ },
48
+ },
49
+ },
50
+ {
51
+ name: 'recall_get_history',
52
+ description:
53
+ "Get detailed session history (context.md + recent sessions). This includes more context than recall_get_context but uses more tokens.",
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ projectPath: {
58
+ type: 'string',
59
+ description:
60
+ 'Path to the project root. REQUIRED to ensure correct repo context. Use the absolute path to the project you are working in.',
61
+ },
62
+ },
63
+ required: ['projectPath'],
64
+ },
65
+ },
66
+ {
67
+ name: 'recall_get_transcripts',
68
+ description:
69
+ "Get full session transcripts (context.md + history.md). WARNING: This can be very large and use many tokens. Only use when you need complete historical details.",
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ projectPath: {
74
+ type: 'string',
75
+ description:
76
+ 'Path to the project root. REQUIRED to ensure correct repo context. Use the absolute path to the project you are working in.',
77
+ },
78
+ },
79
+ required: ['projectPath'],
80
+ },
81
+ },
82
+ {
83
+ name: 'recall_save_session',
84
+ description:
85
+ "Save a summary of what was accomplished in this coding session. This updates the team memory files.",
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: {
89
+ summary: {
90
+ type: 'string',
91
+ description:
92
+ 'What was accomplished in this session - list all items worked on, kept short',
93
+ },
94
+ decisions: {
95
+ type: 'array',
96
+ description: 'Key decisions made',
97
+ items: {
98
+ type: 'object',
99
+ properties: {
100
+ what: { type: 'string', description: 'What was decided' },
101
+ why: { type: 'string', description: 'Why this decision was made' },
102
+ },
103
+ required: ['what', 'why'],
104
+ },
105
+ },
106
+ mistakes: {
107
+ type: 'array',
108
+ description:
109
+ 'Mistakes or gotchas discovered - things the team should not repeat',
110
+ items: { type: 'string' },
111
+ },
112
+ filesChanged: {
113
+ type: 'array',
114
+ description: 'Files that were modified',
115
+ items: { type: 'string' },
116
+ },
117
+ nextSteps: {
118
+ type: 'string',
119
+ description: 'What should be done next',
120
+ },
121
+ blockers: {
122
+ type: 'string',
123
+ description: 'Any blockers encountered',
124
+ },
125
+ },
126
+ required: ['summary'],
127
+ },
128
+ },
129
+ {
130
+ name: 'recall_log_decision',
131
+ description:
132
+ "Log an important decision made during coding. Quick way to capture why something was done.",
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ decision: {
137
+ type: 'string',
138
+ description: 'What was decided',
139
+ },
140
+ reasoning: {
141
+ type: 'string',
142
+ description: 'Why this decision was made',
143
+ },
144
+ },
145
+ required: ['decision', 'reasoning'],
146
+ },
147
+ },
148
+ ];
149
+
150
+ class RecallMCPServer {
151
+ private server: Server;
152
+
153
+ constructor() {
154
+ this.server = new Server(
155
+ {
156
+ name: 'recall-mcp-v3',
157
+ version: '0.1.0',
158
+ },
159
+ {
160
+ capabilities: {
161
+ tools: {},
162
+ },
163
+ }
164
+ );
165
+
166
+ this.setupHandlers();
167
+ }
168
+
169
+ private setupHandlers(): void {
170
+ // List available tools
171
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
172
+ return { tools: TOOLS };
173
+ });
174
+
175
+ // Handle tool calls
176
+ this.server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
177
+ const { name, arguments: args = {} } = request.params;
178
+
179
+ try {
180
+ let result;
181
+ switch (name) {
182
+ case 'recall_get_context':
183
+ result = await getContext(args as unknown as GetContextArgs);
184
+ break;
185
+
186
+ case 'recall_get_history':
187
+ result = await getHistory(args as unknown as GetHistoryArgs);
188
+ break;
189
+
190
+ case 'recall_get_transcripts':
191
+ result = await getTranscripts(args as unknown as GetTranscriptsArgs);
192
+ break;
193
+
194
+ case 'recall_save_session':
195
+ result = await saveSession(args as unknown as SaveSessionArgs);
196
+ break;
197
+
198
+ case 'recall_log_decision':
199
+ result = await logDecision(args as unknown as LogDecisionArgs);
200
+ break;
201
+
202
+ default:
203
+ return {
204
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
205
+ isError: true,
206
+ };
207
+ }
208
+
209
+ return {
210
+ content: result.content.map(c => ({ type: 'text' as const, text: c.text })),
211
+ isError: result.isError,
212
+ };
213
+ } catch (error) {
214
+ const message = error instanceof Error ? error.message : 'Unknown error';
215
+ return {
216
+ content: [{ type: 'text', text: `Error: ${message}` }],
217
+ isError: true,
218
+ };
219
+ }
220
+ });
221
+ }
222
+
223
+ async run(): Promise<void> {
224
+ const transport = new StdioServerTransport();
225
+ await this.server.connect(transport);
226
+ console.error('Recall MCP v3 server running on stdio');
227
+ }
228
+ }
229
+
230
+ // Start server
231
+ const server = new RecallMCPServer();
232
+ server.run().catch(console.error);
@@ -0,0 +1,106 @@
1
+ /**
2
+ * getContext Tool Implementation
3
+ *
4
+ * Fetches the team brain (context.md) for the current repository.
5
+ * This is the distilled current state - loaded at every session start.
6
+ */
7
+
8
+ import { RecallApiClient, AuthenticationError, RecallApiError } from '../api/client.js';
9
+ import { getApiBaseUrl, getApiToken, getTeamKey, setTeamKey } from '../config/index.js';
10
+ import { decryptContent } from '../crypto/index.js';
11
+ import { successResponse, errorResponse, formattedResponse, type ToolResponse } from './types.js';
12
+ import { resolveProjectPath, getRepoInfo } from './utils.js';
13
+
14
+ export interface GetContextArgs {
15
+ projectPath?: string;
16
+ }
17
+
18
+ /**
19
+ * Execute the getContext tool
20
+ *
21
+ * @param args - Tool arguments (projectPath is optional)
22
+ * @returns MCP tool response with context.md content
23
+ */
24
+ export async function getContext(args: GetContextArgs): Promise<ToolResponse> {
25
+ try {
26
+ // Get API token
27
+ const token = getApiToken();
28
+ if (!token) {
29
+ return errorResponse(
30
+ 'Not authenticated. Run `recall auth` to connect your account, or set RECALL_API_TOKEN environment variable.'
31
+ );
32
+ }
33
+
34
+ // Resolve project path
35
+ const projectPath = resolveProjectPath(args.projectPath);
36
+
37
+ // Get repo info from git
38
+ const repoInfo = await getRepoInfo(projectPath);
39
+ if (!repoInfo) {
40
+ return errorResponse(
41
+ `Could not determine repository info for: ${projectPath}\n` +
42
+ 'Make sure this is a git repository with a remote origin.'
43
+ );
44
+ }
45
+
46
+ // Create API client
47
+ const client = new RecallApiClient({
48
+ baseUrl: getApiBaseUrl(),
49
+ token,
50
+ });
51
+
52
+ // Resolve repo to get repoId and teamId
53
+ const { repoId, teamId } = await client.resolveRepo(repoInfo.fullName, repoInfo.defaultBranch);
54
+
55
+ // Get team key (fetch from API if not cached)
56
+ let teamKey = getTeamKey(teamId);
57
+ if (!teamKey) {
58
+ const keyResponse = await client.getTeamKey(teamId);
59
+ teamKey = keyResponse.encryptionKey;
60
+ setTeamKey(teamId, teamKey);
61
+ }
62
+
63
+ // Fetch context from API
64
+ const response = await client.getContext(repoId);
65
+
66
+ // The contextMd comes encrypted from the API, decrypt it
67
+ let contextMd: string;
68
+ try {
69
+ // Check if content is encrypted
70
+ if (response.contextMd.startsWith('{') || response.contextMd.includes(':')) {
71
+ contextMd = decryptContent(response.contextMd, teamKey);
72
+ } else {
73
+ // Already plaintext (shouldn't happen, but handle gracefully)
74
+ contextMd = response.contextMd;
75
+ }
76
+ } catch (decryptError) {
77
+ // If decryption fails, it might already be plaintext
78
+ contextMd = response.contextMd;
79
+ }
80
+
81
+ // Format output with metadata
82
+ const header = `Reading from: ${projectPath}/.recall`;
83
+ return formattedResponse(header, contextMd);
84
+
85
+ } catch (error) {
86
+ if (error instanceof AuthenticationError) {
87
+ return errorResponse(
88
+ 'Authentication failed. Your token may have expired.\n' +
89
+ 'Run `recall auth` to reconnect your account.'
90
+ );
91
+ }
92
+
93
+ if (error instanceof RecallApiError) {
94
+ if (error.code === 'REPO_NOT_FOUND') {
95
+ return errorResponse(
96
+ 'This repository is not connected to Recall.\n' +
97
+ 'Run `recall init` to set up team memory for this repo.'
98
+ );
99
+ }
100
+ return errorResponse(`API Error: ${error.message}`);
101
+ }
102
+
103
+ const message = error instanceof Error ? error.message : 'Unknown error';
104
+ return errorResponse(message);
105
+ }
106
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * getHistory Tool Implementation
3
+ *
4
+ * Fetches context.md + recent session history for the repository.
5
+ * This provides more detail than getContext but uses more tokens.
6
+ */
7
+
8
+ import { RecallApiClient, AuthenticationError, RecallApiError } from '../api/client.js';
9
+ import { getApiBaseUrl, getApiToken, getTeamKey, setTeamKey } from '../config/index.js';
10
+ import { decryptContent } from '../crypto/index.js';
11
+ import { successResponse, errorResponse, formattedResponse, type ToolResponse } from './types.js';
12
+ import { resolveProjectPath, getRepoInfo } from './utils.js';
13
+
14
+ export interface GetHistoryArgs {
15
+ projectPath: string;
16
+ }
17
+
18
+ /**
19
+ * Execute the getHistory tool
20
+ *
21
+ * @param args - Tool arguments (projectPath is required)
22
+ * @returns MCP tool response with context.md + history.md content
23
+ */
24
+ export async function getHistory(args: GetHistoryArgs): Promise<ToolResponse> {
25
+ try {
26
+ // Get API token
27
+ const token = getApiToken();
28
+ if (!token) {
29
+ return errorResponse(
30
+ 'Not authenticated. Run `recall auth` to connect your account, or set RECALL_API_TOKEN environment variable.'
31
+ );
32
+ }
33
+
34
+ // Resolve project path
35
+ const projectPath = resolveProjectPath(args.projectPath);
36
+
37
+ // Get repo info from git
38
+ const repoInfo = await getRepoInfo(projectPath);
39
+ if (!repoInfo) {
40
+ return errorResponse(
41
+ `Could not determine repository info for: ${projectPath}\n` +
42
+ 'Make sure this is a git repository with a remote origin.'
43
+ );
44
+ }
45
+
46
+ // Create API client
47
+ const client = new RecallApiClient({
48
+ baseUrl: getApiBaseUrl(),
49
+ token,
50
+ });
51
+
52
+ // Resolve repo to get repoId and teamId
53
+ const { repoId, teamId } = await client.resolveRepo(repoInfo.fullName, repoInfo.defaultBranch);
54
+
55
+ // Get team key (fetch from API if not cached)
56
+ let teamKey = getTeamKey(teamId);
57
+ if (!teamKey) {
58
+ const keyResponse = await client.getTeamKey(teamId);
59
+ teamKey = keyResponse.encryptionKey;
60
+ setTeamKey(teamId, teamKey);
61
+ }
62
+
63
+ // Fetch history from API
64
+ const response = await client.getHistory(repoId);
65
+
66
+ // Decrypt content
67
+ let contextMd: string;
68
+ let historyMd: string;
69
+
70
+ try {
71
+ // Decrypt context
72
+ if (response.contextMd.startsWith('{') || response.contextMd.includes(':')) {
73
+ contextMd = decryptContent(response.contextMd, teamKey);
74
+ } else {
75
+ contextMd = response.contextMd;
76
+ }
77
+
78
+ // Decrypt history
79
+ if (response.historyMd.startsWith('{') || response.historyMd.includes(':')) {
80
+ historyMd = decryptContent(response.historyMd, teamKey);
81
+ } else {
82
+ historyMd = response.historyMd;
83
+ }
84
+ } catch (decryptError) {
85
+ // If decryption fails, use as-is
86
+ contextMd = response.contextMd;
87
+ historyMd = response.historyMd;
88
+ }
89
+
90
+ // Combine context and history
91
+ const combinedContent = `# Recall Context\n\n${contextMd}\n\n---\n\n# Session History\n\n${historyMd}`;
92
+
93
+ // Format output with metadata
94
+ const header = `Reading history from: ${projectPath}/.recall (${response.sessionCount} sessions)`;
95
+ return formattedResponse(header, combinedContent);
96
+
97
+ } catch (error) {
98
+ if (error instanceof AuthenticationError) {
99
+ return errorResponse(
100
+ 'Authentication failed. Your token may have expired.\n' +
101
+ 'Run `recall auth` to reconnect your account.'
102
+ );
103
+ }
104
+
105
+ if (error instanceof RecallApiError) {
106
+ if (error.code === 'REPO_NOT_FOUND') {
107
+ return errorResponse(
108
+ 'This repository is not connected to Recall.\n' +
109
+ 'Run `recall init` to set up team memory for this repo.'
110
+ );
111
+ }
112
+ return errorResponse(`API Error: ${error.message}`);
113
+ }
114
+
115
+ const message = error instanceof Error ? error.message : 'Unknown error';
116
+ return errorResponse(message);
117
+ }
118
+ }