@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.
- package/dist/api/client.d.ts +111 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +244 -0
- package/dist/config/index.d.ts +89 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +256 -0
- package/dist/crypto/index.d.ts +56 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +224 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189 -0
- package/dist/tools/getContext.d.ts +18 -0
- package/dist/tools/getContext.d.ts.map +1 -0
- package/dist/tools/getContext.js +87 -0
- package/dist/tools/getHistory.d.ts +18 -0
- package/dist/tools/getHistory.d.ts.map +1 -0
- package/dist/tools/getHistory.js +97 -0
- package/dist/tools/getTranscripts.d.ts +19 -0
- package/dist/tools/getTranscripts.d.ts.map +1 -0
- package/dist/tools/getTranscripts.js +129 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/logDecision.d.ts +19 -0
- package/dist/tools/logDecision.d.ts.map +1 -0
- package/dist/tools/logDecision.js +92 -0
- package/dist/tools/saveSession.d.ts +26 -0
- package/dist/tools/saveSession.d.ts.map +1 -0
- package/dist/tools/saveSession.js +115 -0
- package/dist/tools/types.d.ts +32 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +33 -0
- package/dist/tools/utils.d.ts +52 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +238 -0
- package/package.json +46 -0
- package/src/api/client.ts +295 -0
- package/src/config/index.ts +247 -0
- package/src/crypto/index.ts +207 -0
- package/src/index.ts +232 -0
- package/src/tools/getContext.ts +106 -0
- package/src/tools/getHistory.ts +118 -0
- package/src/tools/getTranscripts.ts +150 -0
- package/src/tools/index.ts +13 -0
- package/src/tools/logDecision.ts +118 -0
- package/src/tools/saveSession.ts +159 -0
- package/src/tools/types.ts +47 -0
- package/src/tools/utils.ts +226 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* getTranscripts Tool Implementation
|
|
3
|
+
*
|
|
4
|
+
* Fetches full session transcripts (context.md + history.md + transcripts.md).
|
|
5
|
+
* WARNING: This can be very large and use many tokens.
|
|
6
|
+
* Only use when you need complete historical details.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { RecallApiClient, AuthenticationError, RecallApiError } from '../api/client.js';
|
|
10
|
+
import { getApiBaseUrl, getApiToken, getTeamKey, setTeamKey } from '../config/index.js';
|
|
11
|
+
import { decryptContent } from '../crypto/index.js';
|
|
12
|
+
import { successResponse, errorResponse, formattedResponse, type ToolResponse } from './types.js';
|
|
13
|
+
import { resolveProjectPath, getRepoInfo } from './utils.js';
|
|
14
|
+
|
|
15
|
+
export interface GetTranscriptsArgs {
|
|
16
|
+
projectPath: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Execute the getTranscripts tool
|
|
21
|
+
*
|
|
22
|
+
* @param args - Tool arguments (projectPath is required)
|
|
23
|
+
* @returns MCP tool response with full transcripts content
|
|
24
|
+
*/
|
|
25
|
+
export async function getTranscripts(args: GetTranscriptsArgs): Promise<ToolResponse> {
|
|
26
|
+
try {
|
|
27
|
+
// Get API token
|
|
28
|
+
const token = getApiToken();
|
|
29
|
+
if (!token) {
|
|
30
|
+
return errorResponse(
|
|
31
|
+
'Not authenticated. Run `recall auth` to connect your account, or set RECALL_API_TOKEN environment variable.'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Resolve project path
|
|
36
|
+
const projectPath = resolveProjectPath(args.projectPath);
|
|
37
|
+
|
|
38
|
+
// Get repo info from git
|
|
39
|
+
const repoInfo = await getRepoInfo(projectPath);
|
|
40
|
+
if (!repoInfo) {
|
|
41
|
+
return errorResponse(
|
|
42
|
+
`Could not determine repository info for: ${projectPath}\n` +
|
|
43
|
+
'Make sure this is a git repository with a remote origin.'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Create API client
|
|
48
|
+
const client = new RecallApiClient({
|
|
49
|
+
baseUrl: getApiBaseUrl(),
|
|
50
|
+
token,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Resolve repo to get repoId and teamId
|
|
54
|
+
const { repoId, teamId } = await client.resolveRepo(repoInfo.fullName, repoInfo.defaultBranch);
|
|
55
|
+
|
|
56
|
+
// Get team key (fetch from API if not cached)
|
|
57
|
+
let teamKey = getTeamKey(teamId);
|
|
58
|
+
if (!teamKey) {
|
|
59
|
+
const keyResponse = await client.getTeamKey(teamId);
|
|
60
|
+
teamKey = keyResponse.encryptionKey;
|
|
61
|
+
setTeamKey(teamId, teamKey);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fetch transcripts from API
|
|
65
|
+
const response = await client.getTranscripts(repoId);
|
|
66
|
+
|
|
67
|
+
// Decrypt all content
|
|
68
|
+
let contextMd: string;
|
|
69
|
+
let historyMd: string;
|
|
70
|
+
let transcriptsMd: string;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Decrypt context
|
|
74
|
+
if (response.contextMd.startsWith('{') || response.contextMd.includes(':')) {
|
|
75
|
+
contextMd = decryptContent(response.contextMd, teamKey);
|
|
76
|
+
} else {
|
|
77
|
+
contextMd = response.contextMd;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Decrypt history
|
|
81
|
+
if (response.historyMd.startsWith('{') || response.historyMd.includes(':')) {
|
|
82
|
+
historyMd = decryptContent(response.historyMd, teamKey);
|
|
83
|
+
} else {
|
|
84
|
+
historyMd = response.historyMd;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Decrypt transcripts
|
|
88
|
+
if (response.transcriptsMd.startsWith('{') || response.transcriptsMd.includes(':')) {
|
|
89
|
+
transcriptsMd = decryptContent(response.transcriptsMd, teamKey);
|
|
90
|
+
} else {
|
|
91
|
+
transcriptsMd = response.transcriptsMd;
|
|
92
|
+
}
|
|
93
|
+
} catch (decryptError) {
|
|
94
|
+
// If decryption fails, use as-is
|
|
95
|
+
contextMd = response.contextMd;
|
|
96
|
+
historyMd = response.historyMd;
|
|
97
|
+
transcriptsMd = response.transcriptsMd;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Combine all content with warning
|
|
101
|
+
const combinedContent = [
|
|
102
|
+
`# TOKEN WARNING`,
|
|
103
|
+
``,
|
|
104
|
+
response.tokenWarning,
|
|
105
|
+
``,
|
|
106
|
+
`---`,
|
|
107
|
+
``,
|
|
108
|
+
`# Recall Context`,
|
|
109
|
+
``,
|
|
110
|
+
contextMd,
|
|
111
|
+
``,
|
|
112
|
+
`---`,
|
|
113
|
+
``,
|
|
114
|
+
`# Session History`,
|
|
115
|
+
``,
|
|
116
|
+
historyMd,
|
|
117
|
+
``,
|
|
118
|
+
`---`,
|
|
119
|
+
``,
|
|
120
|
+
`# Full Transcripts`,
|
|
121
|
+
``,
|
|
122
|
+
transcriptsMd,
|
|
123
|
+
].join('\n');
|
|
124
|
+
|
|
125
|
+
// Format output
|
|
126
|
+
const header = `Reading full transcripts from: ${projectPath}/.recall`;
|
|
127
|
+
return formattedResponse(header, combinedContent);
|
|
128
|
+
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (error instanceof AuthenticationError) {
|
|
131
|
+
return errorResponse(
|
|
132
|
+
'Authentication failed. Your token may have expired.\n' +
|
|
133
|
+
'Run `recall auth` to reconnect your account.'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (error instanceof RecallApiError) {
|
|
138
|
+
if (error.code === 'REPO_NOT_FOUND') {
|
|
139
|
+
return errorResponse(
|
|
140
|
+
'This repository is not connected to Recall.\n' +
|
|
141
|
+
'Run `recall init` to set up team memory for this repo.'
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return errorResponse(`API Error: ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
148
|
+
return errorResponse(message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Implementations
|
|
3
|
+
*
|
|
4
|
+
* Exports all MCP tool implementations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { getContext, type GetContextArgs } from './getContext.js';
|
|
8
|
+
export { getHistory, type GetHistoryArgs } from './getHistory.js';
|
|
9
|
+
export { getTranscripts, type GetTranscriptsArgs } from './getTranscripts.js';
|
|
10
|
+
export { saveSession, type SaveSessionArgs } from './saveSession.js';
|
|
11
|
+
export { logDecision, type LogDecisionArgs } from './logDecision.js';
|
|
12
|
+
export { type ToolResponse, successResponse, errorResponse, formattedResponse } from './types.js';
|
|
13
|
+
export * from './utils.js';
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logDecision Tool Implementation
|
|
3
|
+
*
|
|
4
|
+
* Logs an important decision made during coding.
|
|
5
|
+
* Quick way to capture why something was done.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RecallApiClient, AuthenticationError, RecallApiError } from '../api/client.js';
|
|
9
|
+
import { getApiBaseUrl, getApiToken, getTeamKey, setTeamKey } from '../config/index.js';
|
|
10
|
+
import { successResponse, errorResponse, type ToolResponse } from './types.js';
|
|
11
|
+
import { resolveProjectPath, getRepoInfo } from './utils.js';
|
|
12
|
+
import type { LogDecisionRequest } from '@recall_v3/shared';
|
|
13
|
+
|
|
14
|
+
export interface LogDecisionArgs {
|
|
15
|
+
decision: string;
|
|
16
|
+
reasoning: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Execute the logDecision tool
|
|
21
|
+
*
|
|
22
|
+
* @param args - Tool arguments with decision and reasoning
|
|
23
|
+
* @returns MCP tool response with confirmation
|
|
24
|
+
*/
|
|
25
|
+
export async function logDecision(args: LogDecisionArgs): Promise<ToolResponse> {
|
|
26
|
+
try {
|
|
27
|
+
// Validate required fields
|
|
28
|
+
if (!args.decision || args.decision.trim().length === 0) {
|
|
29
|
+
return errorResponse('Decision is required. Please describe what was decided.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!args.reasoning || args.reasoning.trim().length === 0) {
|
|
33
|
+
return errorResponse('Reasoning is required. Please explain why this decision was made.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get API token
|
|
37
|
+
const token = getApiToken();
|
|
38
|
+
if (!token) {
|
|
39
|
+
return errorResponse(
|
|
40
|
+
'Not authenticated. Run `recall auth` to connect your account, or set RECALL_API_TOKEN environment variable.'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Resolve project path (use cwd)
|
|
45
|
+
const projectPath = resolveProjectPath(undefined);
|
|
46
|
+
|
|
47
|
+
// Get repo info from git
|
|
48
|
+
const repoInfo = await getRepoInfo(projectPath);
|
|
49
|
+
if (!repoInfo) {
|
|
50
|
+
return errorResponse(
|
|
51
|
+
`Could not determine repository info for: ${projectPath}\n` +
|
|
52
|
+
'Make sure this is a git repository with a remote origin.'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create API client
|
|
57
|
+
const client = new RecallApiClient({
|
|
58
|
+
baseUrl: getApiBaseUrl(),
|
|
59
|
+
token,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Resolve repo to get repoId and teamId
|
|
63
|
+
const { repoId, teamId } = await client.resolveRepo(repoInfo.fullName, repoInfo.defaultBranch);
|
|
64
|
+
|
|
65
|
+
// Get team key (fetch from API if not cached)
|
|
66
|
+
let teamKey = getTeamKey(teamId);
|
|
67
|
+
if (!teamKey) {
|
|
68
|
+
const keyResponse = await client.getTeamKey(teamId);
|
|
69
|
+
teamKey = keyResponse.encryptionKey;
|
|
70
|
+
setTeamKey(teamId, teamKey);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build the decision request
|
|
74
|
+
const decisionRequest: LogDecisionRequest = {
|
|
75
|
+
decision: args.decision.trim(),
|
|
76
|
+
reasoning: args.reasoning.trim(),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Log decision via API
|
|
80
|
+
const response = await client.logDecision(repoId, decisionRequest);
|
|
81
|
+
|
|
82
|
+
// Build success message
|
|
83
|
+
const successMessage = [
|
|
84
|
+
`Decision logged successfully.`,
|
|
85
|
+
``,
|
|
86
|
+
`Decision ID: ${response.decisionId}`,
|
|
87
|
+
`Repository: ${repoInfo.fullName}`,
|
|
88
|
+
``,
|
|
89
|
+
`What: ${args.decision.trim()}`,
|
|
90
|
+
`Why: ${args.reasoning.trim()}`,
|
|
91
|
+
``,
|
|
92
|
+
`This decision has been added to the team memory.`,
|
|
93
|
+
].join('\n');
|
|
94
|
+
|
|
95
|
+
return successResponse(successMessage);
|
|
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
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* saveSession Tool Implementation
|
|
3
|
+
*
|
|
4
|
+
* Saves a summary of what was accomplished in this coding session.
|
|
5
|
+
* Updates the team memory files via the Recall API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RecallApiClient, AuthenticationError, RecallApiError } from '../api/client.js';
|
|
9
|
+
import { getApiBaseUrl, getApiToken, getTeamKey, setTeamKey } from '../config/index.js';
|
|
10
|
+
import { encryptContent } from '../crypto/index.js';
|
|
11
|
+
import { successResponse, errorResponse, type ToolResponse } from './types.js';
|
|
12
|
+
import { resolveProjectPath, getRepoInfo } from './utils.js';
|
|
13
|
+
import type { SaveSessionRequest } from '@recall_v3/shared';
|
|
14
|
+
|
|
15
|
+
export interface SaveSessionArgs {
|
|
16
|
+
summary: string;
|
|
17
|
+
decisions?: Array<{
|
|
18
|
+
what: string;
|
|
19
|
+
why: string;
|
|
20
|
+
}>;
|
|
21
|
+
mistakes?: string[];
|
|
22
|
+
filesChanged?: string[];
|
|
23
|
+
nextSteps?: string;
|
|
24
|
+
blockers?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Execute the saveSession tool
|
|
29
|
+
*
|
|
30
|
+
* @param args - Tool arguments with session summary and metadata
|
|
31
|
+
* @returns MCP tool response with save confirmation
|
|
32
|
+
*/
|
|
33
|
+
export async function saveSession(args: SaveSessionArgs): Promise<ToolResponse> {
|
|
34
|
+
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.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Get API token
|
|
41
|
+
const token = getApiToken();
|
|
42
|
+
if (!token) {
|
|
43
|
+
return errorResponse(
|
|
44
|
+
'Not authenticated. Run `recall auth` to connect your account, or set RECALL_API_TOKEN environment variable.'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Resolve project path (use cwd since projectPath is not in args)
|
|
49
|
+
const projectPath = resolveProjectPath(undefined);
|
|
50
|
+
|
|
51
|
+
// Get repo info from git
|
|
52
|
+
const repoInfo = await getRepoInfo(projectPath);
|
|
53
|
+
if (!repoInfo) {
|
|
54
|
+
return errorResponse(
|
|
55
|
+
`Could not determine repository info for: ${projectPath}\n` +
|
|
56
|
+
'Make sure this is a git repository with a remote origin.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create API client
|
|
61
|
+
const client = new RecallApiClient({
|
|
62
|
+
baseUrl: getApiBaseUrl(),
|
|
63
|
+
token,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Resolve repo to get repoId and teamId
|
|
67
|
+
const { repoId, teamId } = await client.resolveRepo(repoInfo.fullName, repoInfo.defaultBranch);
|
|
68
|
+
|
|
69
|
+
// Get team key (fetch from API if not cached)
|
|
70
|
+
let teamKey = getTeamKey(teamId);
|
|
71
|
+
if (!teamKey) {
|
|
72
|
+
const keyResponse = await client.getTeamKey(teamId);
|
|
73
|
+
teamKey = keyResponse.encryptionKey;
|
|
74
|
+
setTeamKey(teamId, teamKey);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Build the session request
|
|
78
|
+
const sessionRequest: SaveSessionRequest = {
|
|
79
|
+
summary: args.summary.trim(),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (args.decisions && args.decisions.length > 0) {
|
|
83
|
+
sessionRequest.decisions = args.decisions;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (args.mistakes && args.mistakes.length > 0) {
|
|
87
|
+
sessionRequest.mistakes = args.mistakes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (args.filesChanged && args.filesChanged.length > 0) {
|
|
91
|
+
sessionRequest.filesChanged = args.filesChanged;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (args.nextSteps && args.nextSteps.trim().length > 0) {
|
|
95
|
+
sessionRequest.nextSteps = args.nextSteps.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (args.blockers && args.blockers.trim().length > 0) {
|
|
99
|
+
sessionRequest.blockers = args.blockers.trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Save session via API
|
|
103
|
+
const response = await client.saveSession(repoId, sessionRequest);
|
|
104
|
+
|
|
105
|
+
// Build success message
|
|
106
|
+
const parts: string[] = [
|
|
107
|
+
`Session saved successfully.`,
|
|
108
|
+
``,
|
|
109
|
+
`Session ID: ${response.sessionId}`,
|
|
110
|
+
`Repository: ${repoInfo.fullName}`,
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
if (args.decisions && args.decisions.length > 0) {
|
|
114
|
+
parts.push(`Decisions logged: ${args.decisions.length}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (args.mistakes && args.mistakes.length > 0) {
|
|
118
|
+
parts.push(`Mistakes documented: ${args.mistakes.length}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (args.filesChanged && args.filesChanged.length > 0) {
|
|
122
|
+
parts.push(`Files tracked: ${args.filesChanged.length}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (args.nextSteps) {
|
|
126
|
+
parts.push(`Next steps: ${args.nextSteps}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (args.blockers) {
|
|
130
|
+
parts.push(`Blockers: ${args.blockers}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
parts.push(``);
|
|
134
|
+
parts.push(`Team memory has been updated.`);
|
|
135
|
+
|
|
136
|
+
return successResponse(parts.join('\n'));
|
|
137
|
+
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error instanceof AuthenticationError) {
|
|
140
|
+
return errorResponse(
|
|
141
|
+
'Authentication failed. Your token may have expired.\n' +
|
|
142
|
+
'Run `recall auth` to reconnect your account.'
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (error instanceof RecallApiError) {
|
|
147
|
+
if (error.code === 'REPO_NOT_FOUND') {
|
|
148
|
+
return errorResponse(
|
|
149
|
+
'This repository is not connected to Recall.\n' +
|
|
150
|
+
'Run `recall init` to set up team memory for this repo.'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return errorResponse(`API Error: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
157
|
+
return errorResponse(message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Response Types
|
|
3
|
+
*
|
|
4
|
+
* Shared types for tool implementations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Content item in MCP response
|
|
9
|
+
*/
|
|
10
|
+
export interface TextContent {
|
|
11
|
+
type: 'text';
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Standard MCP tool response format
|
|
17
|
+
*/
|
|
18
|
+
export interface ToolResponse {
|
|
19
|
+
content: TextContent[];
|
|
20
|
+
isError?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a success response
|
|
25
|
+
*/
|
|
26
|
+
export function successResponse(text: string): ToolResponse {
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: 'text', text }],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create an error response
|
|
34
|
+
*/
|
|
35
|
+
export function errorResponse(message: string): ToolResponse {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
38
|
+
isError: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a formatted response with header and content
|
|
44
|
+
*/
|
|
45
|
+
export function formattedResponse(header: string, content: string): ToolResponse {
|
|
46
|
+
return successResponse(`[${header}]\n\n${content}`);
|
|
47
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared utilities for tool implementations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Repository information extracted from git
|
|
13
|
+
*/
|
|
14
|
+
export interface RepoInfo {
|
|
15
|
+
/** Full name like "owner/repo" */
|
|
16
|
+
fullName: string;
|
|
17
|
+
/** Remote URL */
|
|
18
|
+
remoteUrl: string;
|
|
19
|
+
/** Default branch name */
|
|
20
|
+
defaultBranch: string;
|
|
21
|
+
/** Local repo root path */
|
|
22
|
+
localPath: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve project path from argument or cwd
|
|
27
|
+
*/
|
|
28
|
+
export function resolveProjectPath(projectPath?: string): string {
|
|
29
|
+
if (projectPath) {
|
|
30
|
+
// Resolve to absolute path
|
|
31
|
+
return path.resolve(projectPath);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Use current working directory
|
|
35
|
+
return process.cwd();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find the git root directory for a given path
|
|
40
|
+
*/
|
|
41
|
+
export function findGitRoot(startPath: string): string | null {
|
|
42
|
+
let currentPath = path.resolve(startPath);
|
|
43
|
+
|
|
44
|
+
while (currentPath !== path.dirname(currentPath)) {
|
|
45
|
+
if (fs.existsSync(path.join(currentPath, '.git'))) {
|
|
46
|
+
return currentPath;
|
|
47
|
+
}
|
|
48
|
+
currentPath = path.dirname(currentPath);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Execute a git command in the given directory
|
|
56
|
+
*/
|
|
57
|
+
function execGit(args: string[], cwd: string): string | null {
|
|
58
|
+
try {
|
|
59
|
+
const result = execSync(`git ${args.join(' ')}`, {
|
|
60
|
+
cwd,
|
|
61
|
+
encoding: 'utf-8',
|
|
62
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
63
|
+
});
|
|
64
|
+
return result.trim();
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse GitHub/GitLab remote URL to get owner/repo
|
|
72
|
+
*/
|
|
73
|
+
function parseRemoteUrl(url: string): string | null {
|
|
74
|
+
// SSH format: git@github.com:owner/repo.git
|
|
75
|
+
const sshMatch = url.match(/git@[\w.-]+:([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
|
|
76
|
+
if (sshMatch) {
|
|
77
|
+
return sshMatch[1];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// HTTPS format: https://github.com/owner/repo.git
|
|
81
|
+
const httpsMatch = url.match(/https?:\/\/[\w.-]+\/([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
|
|
82
|
+
if (httpsMatch) {
|
|
83
|
+
return httpsMatch[1];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get repository information from git
|
|
91
|
+
*/
|
|
92
|
+
export async function getRepoInfo(projectPath: string): Promise<RepoInfo | null> {
|
|
93
|
+
// Find git root
|
|
94
|
+
const gitRoot = findGitRoot(projectPath);
|
|
95
|
+
if (!gitRoot) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Get remote URL
|
|
100
|
+
const remoteUrl = execGit(['config', '--get', 'remote.origin.url'], gitRoot);
|
|
101
|
+
if (!remoteUrl) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Parse full name from URL
|
|
106
|
+
const fullName = parseRemoteUrl(remoteUrl);
|
|
107
|
+
if (!fullName) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get default branch
|
|
112
|
+
// Try to get from remote HEAD first
|
|
113
|
+
let defaultBranch = execGit(['symbolic-ref', 'refs/remotes/origin/HEAD', '--short'], gitRoot);
|
|
114
|
+
if (defaultBranch) {
|
|
115
|
+
// Remove "origin/" prefix
|
|
116
|
+
defaultBranch = defaultBranch.replace(/^origin\//, '');
|
|
117
|
+
} else {
|
|
118
|
+
// Fallback to common defaults
|
|
119
|
+
defaultBranch = 'main';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
fullName,
|
|
124
|
+
remoteUrl,
|
|
125
|
+
defaultBranch,
|
|
126
|
+
localPath: gitRoot,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Read a file if it exists
|
|
132
|
+
*/
|
|
133
|
+
export function readFileIfExists(filePath: string): string | null {
|
|
134
|
+
try {
|
|
135
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get the Claude session directory for a project
|
|
143
|
+
* Claude stores sessions in ~/.claude/projects/<hash>/
|
|
144
|
+
*/
|
|
145
|
+
export function getClaudeSessionDir(projectPath: string): string | null {
|
|
146
|
+
const claudeDir = path.join(process.env.HOME || '', '.claude', 'projects');
|
|
147
|
+
|
|
148
|
+
if (!fs.existsSync(claudeDir)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Claude uses a hash of the project path
|
|
153
|
+
// We need to find the matching directory
|
|
154
|
+
try {
|
|
155
|
+
const dirs = fs.readdirSync(claudeDir);
|
|
156
|
+
|
|
157
|
+
for (const dir of dirs) {
|
|
158
|
+
const sessionPath = path.join(claudeDir, dir);
|
|
159
|
+
const stat = fs.statSync(sessionPath);
|
|
160
|
+
|
|
161
|
+
if (!stat.isDirectory()) continue;
|
|
162
|
+
|
|
163
|
+
// Check if this directory's config points to our project
|
|
164
|
+
const configPath = path.join(sessionPath, 'config.json');
|
|
165
|
+
if (fs.existsSync(configPath)) {
|
|
166
|
+
try {
|
|
167
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as { projectPath?: string };
|
|
168
|
+
if (config.projectPath === projectPath) {
|
|
169
|
+
return sessionPath;
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// Skip invalid config files
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Find the most recent Claude session JSONL file
|
|
185
|
+
*/
|
|
186
|
+
export function findLatestSessionFile(projectPath: string): string | null {
|
|
187
|
+
const sessionDir = getClaudeSessionDir(projectPath);
|
|
188
|
+
if (!sessionDir) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const files = fs.readdirSync(sessionDir)
|
|
194
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
195
|
+
.map(f => ({
|
|
196
|
+
name: f,
|
|
197
|
+
path: path.join(sessionDir, f),
|
|
198
|
+
mtime: fs.statSync(path.join(sessionDir, f)).mtime,
|
|
199
|
+
}))
|
|
200
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
201
|
+
|
|
202
|
+
return files.length > 0 ? files[0].path : null;
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Format bytes to human-readable size
|
|
210
|
+
*/
|
|
211
|
+
export function formatBytes(bytes: number): string {
|
|
212
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
213
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
214
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Format duration in seconds to human-readable
|
|
219
|
+
*/
|
|
220
|
+
export function formatDuration(seconds: number): string {
|
|
221
|
+
if (seconds < 60) return `${seconds}s`;
|
|
222
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
223
|
+
const hours = Math.floor(seconds / 3600);
|
|
224
|
+
const mins = Math.floor((seconds % 3600) / 60);
|
|
225
|
+
return `${hours}h ${mins}m`;
|
|
226
|
+
}
|