@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,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall API Client
|
|
3
|
+
*
|
|
4
|
+
* HTTP client for communicating with the Recall backend API.
|
|
5
|
+
* Handles authentication, retries, and error handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
SummarizeRequest,
|
|
10
|
+
SummarizeResponse,
|
|
11
|
+
SaveSessionRequest,
|
|
12
|
+
SaveSessionResponse,
|
|
13
|
+
GetContextResponse,
|
|
14
|
+
GetHistoryResponse,
|
|
15
|
+
GetTranscriptsResponse,
|
|
16
|
+
LogDecisionRequest,
|
|
17
|
+
LogDecisionResponse,
|
|
18
|
+
TeamKeyResponse,
|
|
19
|
+
RecallStatusResponse,
|
|
20
|
+
ListTeamsResponse,
|
|
21
|
+
ApiResponse,
|
|
22
|
+
} from '@recall_v3/shared';
|
|
23
|
+
|
|
24
|
+
// Error types for the API client
|
|
25
|
+
export class RecallApiError extends Error {
|
|
26
|
+
constructor(
|
|
27
|
+
message: string,
|
|
28
|
+
public readonly code: string,
|
|
29
|
+
public readonly statusCode?: number,
|
|
30
|
+
public readonly retryable: boolean = false
|
|
31
|
+
) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = 'RecallApiError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class NetworkError extends RecallApiError {
|
|
38
|
+
constructor(message: string, public readonly cause?: Error) {
|
|
39
|
+
super(message, 'NETWORK_ERROR', undefined, true);
|
|
40
|
+
this.name = 'NetworkError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class AuthenticationError extends RecallApiError {
|
|
45
|
+
constructor(message: string = 'Authentication failed') {
|
|
46
|
+
super(message, 'AUTH_ERROR', 401, false);
|
|
47
|
+
this.name = 'AuthenticationError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class TimeoutError extends RecallApiError {
|
|
52
|
+
constructor(message: string = 'Request timed out') {
|
|
53
|
+
super(message, 'TIMEOUT', undefined, true);
|
|
54
|
+
this.name = 'TimeoutError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Client configuration
|
|
59
|
+
interface RecallApiClientConfig {
|
|
60
|
+
baseUrl: string;
|
|
61
|
+
token: string;
|
|
62
|
+
timeout?: number;
|
|
63
|
+
maxRetries?: number;
|
|
64
|
+
retryBaseDelay?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* RecallApiClient - Thin HTTP client for Recall API
|
|
69
|
+
*
|
|
70
|
+
* Features:
|
|
71
|
+
* - Bearer token authentication
|
|
72
|
+
* - Exponential backoff retry on 5xx errors
|
|
73
|
+
* - Configurable timeout (default 30s)
|
|
74
|
+
* - Typed responses using @recall_v3/shared types
|
|
75
|
+
*/
|
|
76
|
+
export class RecallApiClient {
|
|
77
|
+
private readonly baseUrl: string;
|
|
78
|
+
private readonly token: string;
|
|
79
|
+
private readonly timeout: number;
|
|
80
|
+
private readonly maxRetries: number;
|
|
81
|
+
private readonly retryBaseDelay: number;
|
|
82
|
+
|
|
83
|
+
constructor(config: RecallApiClientConfig) {
|
|
84
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash
|
|
85
|
+
this.token = config.token;
|
|
86
|
+
this.timeout = config.timeout ?? 30000; // 30s default
|
|
87
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
88
|
+
this.retryBaseDelay = config.retryBaseDelay ?? 1000; // 1s base delay
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Internal fetch wrapper with timeout and retry logic
|
|
93
|
+
*/
|
|
94
|
+
private async request<T>(
|
|
95
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
96
|
+
path: string,
|
|
97
|
+
body?: unknown
|
|
98
|
+
): Promise<T> {
|
|
99
|
+
const url = `${this.baseUrl}${path}`;
|
|
100
|
+
|
|
101
|
+
let lastError: Error | undefined;
|
|
102
|
+
|
|
103
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
104
|
+
try {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch(url, {
|
|
110
|
+
method,
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
'Authorization': `Bearer ${this.token}`,
|
|
114
|
+
},
|
|
115
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
116
|
+
signal: controller.signal,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
clearTimeout(timeoutId);
|
|
120
|
+
|
|
121
|
+
// Handle non-OK responses
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const errorBody = await response.json().catch(() => ({})) as { error?: { code?: string; message?: string } };
|
|
124
|
+
const errorCode = errorBody?.error?.code ?? 'UNKNOWN_ERROR';
|
|
125
|
+
const errorMessage = errorBody?.error?.message ?? `HTTP ${response.status}`;
|
|
126
|
+
|
|
127
|
+
// 401 = auth error, don't retry
|
|
128
|
+
if (response.status === 401) {
|
|
129
|
+
throw new AuthenticationError(errorMessage);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 5xx = server error, retry with backoff
|
|
133
|
+
if (response.status >= 500) {
|
|
134
|
+
throw new RecallApiError(errorMessage, errorCode, response.status, true);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 4xx = client error, don't retry
|
|
138
|
+
throw new RecallApiError(errorMessage, errorCode, response.status, false);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 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);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return data.data as T;
|
|
149
|
+
} finally {
|
|
150
|
+
clearTimeout(timeoutId);
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// Handle abort (timeout)
|
|
154
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
155
|
+
lastError = new TimeoutError();
|
|
156
|
+
// Timeout is retryable
|
|
157
|
+
if (attempt < this.maxRetries) {
|
|
158
|
+
await this.sleep(this.calculateBackoff(attempt));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
throw lastError;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Handle network errors
|
|
165
|
+
if (error instanceof TypeError && error.message.includes('fetch')) {
|
|
166
|
+
lastError = new NetworkError('Network request failed', error);
|
|
167
|
+
// Network errors are retryable
|
|
168
|
+
if (attempt < this.maxRetries) {
|
|
169
|
+
await this.sleep(this.calculateBackoff(attempt));
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
throw lastError;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Handle retryable API errors
|
|
176
|
+
if (error instanceof RecallApiError && error.retryable) {
|
|
177
|
+
lastError = error;
|
|
178
|
+
if (attempt < this.maxRetries) {
|
|
179
|
+
await this.sleep(this.calculateBackoff(attempt));
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Non-retryable errors throw immediately
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Should not reach here, but just in case
|
|
191
|
+
throw lastError ?? new RecallApiError('Unknown error', 'UNKNOWN_ERROR');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Calculate exponential backoff delay
|
|
196
|
+
*/
|
|
197
|
+
private calculateBackoff(attempt: number): number {
|
|
198
|
+
// Exponential backoff: 1s, 2s, 4s, 8s...
|
|
199
|
+
const delay = this.retryBaseDelay * Math.pow(2, attempt);
|
|
200
|
+
// Add jitter (0-25% of delay)
|
|
201
|
+
const jitter = delay * Math.random() * 0.25;
|
|
202
|
+
return delay + jitter;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Sleep for a given number of milliseconds
|
|
207
|
+
*/
|
|
208
|
+
private sleep(ms: number): Promise<void> {
|
|
209
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// =========================================================================
|
|
213
|
+
// API Methods
|
|
214
|
+
// =========================================================================
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Summarize a session transcript
|
|
218
|
+
* Called by saveSession tool after reading JSONL from disk
|
|
219
|
+
*/
|
|
220
|
+
async summarize(request: SummarizeRequest): Promise<SummarizeResponse> {
|
|
221
|
+
return this.request<SummarizeResponse>('POST', '/api/v1/summarize', request);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Save a session with manual input (not from transcript)
|
|
226
|
+
* Used when user provides summary directly via MCP tool
|
|
227
|
+
*/
|
|
228
|
+
async saveSession(repoId: string, data: SaveSessionRequest): Promise<SaveSessionResponse> {
|
|
229
|
+
return this.request<SaveSessionResponse>('POST', `/api/v1/repos/${repoId}/sessions`, data);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get context for a repository (context.md)
|
|
234
|
+
* Returns the distilled team brain for this repo
|
|
235
|
+
*/
|
|
236
|
+
async getContext(repoId: string): Promise<GetContextResponse> {
|
|
237
|
+
return this.request<GetContextResponse>('GET', `/api/v1/repos/${repoId}/context`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get history for a repository (context.md + history.md)
|
|
242
|
+
* Returns more detail than getContext
|
|
243
|
+
*/
|
|
244
|
+
async getHistory(repoId: string): Promise<GetHistoryResponse> {
|
|
245
|
+
return this.request<GetHistoryResponse>('GET', `/api/v1/repos/${repoId}/history`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get full transcripts for a repository
|
|
250
|
+
* WARNING: Can be very large, uses many tokens
|
|
251
|
+
*/
|
|
252
|
+
async getTranscripts(repoId: string): Promise<GetTranscriptsResponse> {
|
|
253
|
+
return this.request<GetTranscriptsResponse>('GET', `/api/v1/repos/${repoId}/transcripts`);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Log a decision for a repository
|
|
258
|
+
*/
|
|
259
|
+
async logDecision(repoId: string, data: LogDecisionRequest): Promise<LogDecisionResponse> {
|
|
260
|
+
return this.request<LogDecisionResponse>('POST', `/api/v1/repos/${repoId}/decisions`, data);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the encryption key for a team
|
|
265
|
+
* Used to decrypt content locally
|
|
266
|
+
*/
|
|
267
|
+
async getTeamKey(teamId: string): Promise<TeamKeyResponse> {
|
|
268
|
+
return this.request<TeamKeyResponse>('GET', `/api/v1/teams/${teamId}/key`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* List teams the user belongs to
|
|
273
|
+
*/
|
|
274
|
+
async listTeams(): Promise<ListTeamsResponse> {
|
|
275
|
+
return this.request<ListTeamsResponse>('GET', '/api/v1/teams');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get authentication status
|
|
280
|
+
*/
|
|
281
|
+
async getStatus(): Promise<RecallStatusResponse> {
|
|
282
|
+
return this.request<RecallStatusResponse>('GET', '/api/v1/status');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Lookup or create repo by GitHub repo info
|
|
287
|
+
* Returns the repo ID for subsequent API calls
|
|
288
|
+
*/
|
|
289
|
+
async resolveRepo(fullName: string, defaultBranch?: string): Promise<{ repoId: string; teamId: string }> {
|
|
290
|
+
return this.request<{ repoId: string; teamId: string }>('POST', '/api/v1/repos/resolve', {
|
|
291
|
+
fullName,
|
|
292
|
+
defaultBranch,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall Configuration Management
|
|
3
|
+
*
|
|
4
|
+
* Manages the local config file at ~/.recall/config.json.
|
|
5
|
+
* Handles secure storage of API tokens and team keys.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import type { RecallConfig } from '@recall_v3/shared';
|
|
12
|
+
|
|
13
|
+
// Current config file version
|
|
14
|
+
const CONFIG_VERSION = 1;
|
|
15
|
+
|
|
16
|
+
// Default API base URL (v3)
|
|
17
|
+
const DEFAULT_API_BASE_URL = 'https://api-v3.recall.team';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the path to the Recall config directory
|
|
21
|
+
*/
|
|
22
|
+
export function getConfigDir(): string {
|
|
23
|
+
return path.join(os.homedir(), '.recall');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get the path to the config file
|
|
28
|
+
*/
|
|
29
|
+
export function getConfigPath(): string {
|
|
30
|
+
return path.join(getConfigDir(), 'config.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ensure the config directory exists with secure permissions
|
|
35
|
+
*/
|
|
36
|
+
function ensureConfigDir(): void {
|
|
37
|
+
const configDir = getConfigDir();
|
|
38
|
+
if (!fs.existsSync(configDir)) {
|
|
39
|
+
fs.mkdirSync(configDir, { mode: 0o700, recursive: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a default empty config
|
|
45
|
+
*/
|
|
46
|
+
function createDefaultConfig(): RecallConfig {
|
|
47
|
+
return {
|
|
48
|
+
token: '',
|
|
49
|
+
defaultTeamId: null,
|
|
50
|
+
activeTeamId: null,
|
|
51
|
+
teamKeys: {},
|
|
52
|
+
user: null,
|
|
53
|
+
lastSyncAt: null,
|
|
54
|
+
version: CONFIG_VERSION,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Migrate old config format to new if necessary
|
|
60
|
+
*/
|
|
61
|
+
function migrateConfig(config: Partial<RecallConfig>): RecallConfig {
|
|
62
|
+
const migrated = { ...createDefaultConfig(), ...config };
|
|
63
|
+
|
|
64
|
+
// Future migration logic would go here
|
|
65
|
+
// For now, just ensure version is set
|
|
66
|
+
migrated.version = CONFIG_VERSION;
|
|
67
|
+
|
|
68
|
+
return migrated;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load configuration from disk
|
|
73
|
+
* Returns a default config if the file doesn't exist
|
|
74
|
+
*/
|
|
75
|
+
export function loadConfig(): RecallConfig {
|
|
76
|
+
const configPath = getConfigPath();
|
|
77
|
+
|
|
78
|
+
if (!fs.existsSync(configPath)) {
|
|
79
|
+
return createDefaultConfig();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
84
|
+
const parsed = JSON.parse(raw) as Partial<RecallConfig>;
|
|
85
|
+
|
|
86
|
+
// Migrate if necessary
|
|
87
|
+
if (!parsed.version || parsed.version < CONFIG_VERSION) {
|
|
88
|
+
const migrated = migrateConfig(parsed);
|
|
89
|
+
saveConfig(migrated);
|
|
90
|
+
return migrated;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return migrateConfig(parsed);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// If config is corrupted, return default
|
|
96
|
+
console.error('Warning: Could not load Recall config, using defaults');
|
|
97
|
+
return createDefaultConfig();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Save configuration to disk with secure permissions (0600)
|
|
103
|
+
*/
|
|
104
|
+
export function saveConfig(config: Partial<RecallConfig>): void {
|
|
105
|
+
ensureConfigDir();
|
|
106
|
+
|
|
107
|
+
const current = loadConfig();
|
|
108
|
+
const merged: RecallConfig = {
|
|
109
|
+
...current,
|
|
110
|
+
...config,
|
|
111
|
+
version: CONFIG_VERSION,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const configPath = getConfigPath();
|
|
115
|
+
const json = JSON.stringify(merged, null, 2);
|
|
116
|
+
|
|
117
|
+
// Write with secure permissions (0600 = owner read/write only)
|
|
118
|
+
fs.writeFileSync(configPath, json, { mode: 0o600 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the API base URL from config or environment
|
|
123
|
+
*/
|
|
124
|
+
export function getApiBaseUrl(): string {
|
|
125
|
+
// Environment variable takes precedence (for development)
|
|
126
|
+
const envUrl = process.env.RECALL_API_URL;
|
|
127
|
+
if (envUrl) {
|
|
128
|
+
return envUrl;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return DEFAULT_API_BASE_URL;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get the API token from config or environment
|
|
136
|
+
*/
|
|
137
|
+
export function getApiToken(): string | null {
|
|
138
|
+
// Environment variable takes precedence
|
|
139
|
+
const envToken = process.env.RECALL_API_TOKEN;
|
|
140
|
+
if (envToken) {
|
|
141
|
+
return envToken;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const config = loadConfig();
|
|
145
|
+
return config.token || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Store the API token securely
|
|
150
|
+
*/
|
|
151
|
+
export function setApiToken(token: string): void {
|
|
152
|
+
saveConfig({ token });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the active team ID
|
|
157
|
+
*/
|
|
158
|
+
export function getActiveTeamId(): string | null {
|
|
159
|
+
const config = loadConfig();
|
|
160
|
+
return config.activeTeamId ?? config.defaultTeamId ?? null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Set the active team ID for this session
|
|
165
|
+
*/
|
|
166
|
+
export function setActiveTeamId(teamId: string): void {
|
|
167
|
+
saveConfig({ activeTeamId: teamId });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Set the default team ID
|
|
172
|
+
*/
|
|
173
|
+
export function setDefaultTeamId(teamId: string): void {
|
|
174
|
+
saveConfig({ defaultTeamId: teamId, activeTeamId: teamId });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get a cached team encryption key
|
|
179
|
+
*/
|
|
180
|
+
export function getTeamKey(teamId: string): string | null {
|
|
181
|
+
const config = loadConfig();
|
|
182
|
+
return config.teamKeys[teamId] ?? null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Cache a team encryption key
|
|
187
|
+
*/
|
|
188
|
+
export function setTeamKey(teamId: string, key: string): void {
|
|
189
|
+
const config = loadConfig();
|
|
190
|
+
const teamKeys = { ...config.teamKeys, [teamId]: key };
|
|
191
|
+
saveConfig({ teamKeys });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Update cached user info
|
|
196
|
+
*/
|
|
197
|
+
export function setUserInfo(user: { id: string; email: string | null; githubUsername: string }): void {
|
|
198
|
+
saveConfig({ user });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Update last sync timestamp
|
|
203
|
+
*/
|
|
204
|
+
export function setLastSyncAt(timestamp: string): void {
|
|
205
|
+
saveConfig({ lastSyncAt: timestamp });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check if the user is authenticated (has a token)
|
|
210
|
+
*/
|
|
211
|
+
export function isAuthenticated(): boolean {
|
|
212
|
+
return !!getApiToken();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Clear all authentication data (logout)
|
|
217
|
+
*/
|
|
218
|
+
export function clearAuth(): void {
|
|
219
|
+
saveConfig({
|
|
220
|
+
token: '',
|
|
221
|
+
activeTeamId: null,
|
|
222
|
+
teamKeys: {},
|
|
223
|
+
user: null,
|
|
224
|
+
lastSyncAt: null,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Extended config interface for the MCP client
|
|
230
|
+
* Adds computed properties and convenience methods
|
|
231
|
+
*/
|
|
232
|
+
export interface ExtendedConfig extends RecallConfig {
|
|
233
|
+
apiBaseUrl: string;
|
|
234
|
+
isAuthenticated: boolean;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get the full extended config
|
|
239
|
+
*/
|
|
240
|
+
export function getExtendedConfig(): ExtendedConfig {
|
|
241
|
+
const config = loadConfig();
|
|
242
|
+
return {
|
|
243
|
+
...config,
|
|
244
|
+
apiBaseUrl: getApiBaseUrl(),
|
|
245
|
+
isAuthenticated: isAuthenticated(),
|
|
246
|
+
};
|
|
247
|
+
}
|