@elliotding/ai-agent-mcp 0.1.8 → 0.1.10
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/ai-resource-telemetry.json +1 -1
- package/dist/git/multi-source-manager.d.ts +1 -1
- package/dist/git/multi-source-manager.d.ts.map +1 -1
- package/dist/git/multi-source-manager.js +54 -10
- package/dist/git/multi-source-manager.js.map +1 -1
- package/dist/prompts/manager.d.ts.map +1 -1
- package/dist/prompts/manager.js +59 -2
- package/dist/prompts/manager.js.map +1 -1
- package/dist/tools/sync-resources.d.ts.map +1 -1
- package/dist/tools/sync-resources.js +85 -18
- package/dist/tools/sync-resources.js.map +1 -1
- package/dist/tools/uninstall-resource.d.ts.map +1 -1
- package/dist/tools/uninstall-resource.js +20 -76
- package/dist/tools/uninstall-resource.js.map +1 -1
- package/dist/utils/cursor-paths.d.ts +25 -0
- package/dist/utils/cursor-paths.d.ts.map +1 -1
- package/dist/utils/cursor-paths.js +37 -0
- package/dist/utils/cursor-paths.js.map +1 -1
- package/package.json +1 -1
- package/src/git/multi-source-manager.ts +74 -15
- package/src/prompts/manager.ts +72 -2
- package/src/tools/sync-resources.ts +118 -21
- package/src/tools/uninstall-resource.ts +23 -44
- package/src/utils/cursor-paths.ts +39 -0
|
@@ -21,7 +21,11 @@ import * as path from 'path';
|
|
|
21
21
|
import { logger, logToolCall, logToolStep, logToolResult } from '../utils/logger';
|
|
22
22
|
import { apiClient } from '../api/client';
|
|
23
23
|
import { multiSourceGitManager } from '../git/multi-source-manager';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getCursorResourcePath,
|
|
26
|
+
getCursorTypeDirForClient,
|
|
27
|
+
getCursorRootDirForClient,
|
|
28
|
+
} from '../utils/cursor-paths';
|
|
25
29
|
import { MCPServerError } from '../types/errors';
|
|
26
30
|
import type {
|
|
27
31
|
SyncResourcesParams,
|
|
@@ -283,10 +287,24 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
283
287
|
// reading the files directly from the local git checkout.
|
|
284
288
|
let resourceFiles = downloadResult.files;
|
|
285
289
|
if (resourceFiles.length === 0) {
|
|
286
|
-
|
|
287
|
-
|
|
290
|
+
logger.info(
|
|
291
|
+
{ resourceId: sub.id, resourceName: sub.name, type: sub.type },
|
|
292
|
+
'sync_resources: API returned no files — triggering git-checkout fallback',
|
|
293
|
+
);
|
|
294
|
+
const gitType = sub.type as 'command' | 'skill' | 'rule' | 'mcp';
|
|
295
|
+
const gitFiles = await multiSourceGitManager.readResourceFiles(sub.name, gitType);
|
|
288
296
|
if (gitFiles.length > 0) {
|
|
289
297
|
resourceFiles = gitFiles;
|
|
298
|
+
logger.info(
|
|
299
|
+
{
|
|
300
|
+
resourceId: sub.id,
|
|
301
|
+
resourceName: sub.name,
|
|
302
|
+
type: sub.type,
|
|
303
|
+
fileCount: resourceFiles.length,
|
|
304
|
+
files: resourceFiles.map((f) => f.path),
|
|
305
|
+
},
|
|
306
|
+
'sync_resources: git-checkout fallback succeeded',
|
|
307
|
+
);
|
|
290
308
|
logToolStep('sync_resources', 'Loaded resource files from local git checkout', {
|
|
291
309
|
resourceId: sub.id,
|
|
292
310
|
fileCount: resourceFiles.length,
|
|
@@ -294,7 +312,7 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
294
312
|
} else {
|
|
295
313
|
logger.warn(
|
|
296
314
|
{ resourceId: sub.id, resourceName: sub.name, type: sub.type },
|
|
297
|
-
'
|
|
315
|
+
'sync_resources: git-checkout fallback found no files — marking resource failed',
|
|
298
316
|
);
|
|
299
317
|
tally.failed++;
|
|
300
318
|
details.push({ id: sub.id, name: sub.name, action: 'failed', version: resourceVersion });
|
|
@@ -305,48 +323,84 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
305
323
|
// ── MCP resource ──────────────────────────────────────────────────────
|
|
306
324
|
// Read mcp-config.json to determine Format A (local executable, has
|
|
307
325
|
// "command" field) vs Format B (remote URL map, no "command" field).
|
|
326
|
+
//
|
|
327
|
+
// IMPORTANT: all paths in LocalAction instructions must use the CLIENT-side
|
|
328
|
+
// helper (tilde-based) so they resolve correctly on the user's machine,
|
|
329
|
+
// not on this (possibly remote Linux) server.
|
|
308
330
|
if (sub.type === 'mcp') {
|
|
309
331
|
const mcpConfigFile = resourceFiles.find(
|
|
310
332
|
(f) => path.basename(f.path) === 'mcp-config.json',
|
|
311
333
|
);
|
|
312
|
-
|
|
334
|
+
// ~/.cursor/mcp.json on the user's machine
|
|
335
|
+
const mcpJsonPath = `${getCursorRootDirForClient()}/mcp.json`;
|
|
336
|
+
|
|
337
|
+
logger.info(
|
|
338
|
+
{
|
|
339
|
+
resourceId: sub.id,
|
|
340
|
+
resourceName: sub.name,
|
|
341
|
+
mcpJsonPath,
|
|
342
|
+
hasMcpConfigFile: !!mcpConfigFile,
|
|
343
|
+
availableFiles: resourceFiles.map((f) => f.path),
|
|
344
|
+
},
|
|
345
|
+
'sync_resources: processing MCP resource',
|
|
346
|
+
);
|
|
313
347
|
|
|
314
348
|
if (mcpConfigFile) {
|
|
315
349
|
let cfg: Record<string, unknown> = {};
|
|
316
350
|
try { cfg = JSON.parse(mcpConfigFile.content) as Record<string, unknown>; }
|
|
317
|
-
catch {
|
|
351
|
+
catch {
|
|
352
|
+
logger.warn(
|
|
353
|
+
{ resourceId: sub.id, resourceName: sub.name },
|
|
354
|
+
'sync_resources: failed to parse mcp-config.json — treating as empty config',
|
|
355
|
+
);
|
|
356
|
+
}
|
|
318
357
|
|
|
319
358
|
if (typeof cfg['command'] === 'string') {
|
|
320
359
|
// ── Format A: local executable ──────────────────────────────────
|
|
321
|
-
|
|
322
|
-
const
|
|
360
|
+
const installDir = `${getCursorTypeDirForClient('mcp')}/${sub.name}`;
|
|
361
|
+
const writeActions: string[] = [];
|
|
323
362
|
for (const file of resourceFiles) {
|
|
324
363
|
const normalised = path.normalize(file.path);
|
|
325
364
|
if (normalised.startsWith('..')) continue;
|
|
326
|
-
|
|
365
|
+
const fileDest = `${installDir}/${normalised}`;
|
|
366
|
+
localActions.push({ action: 'write_file', path: fileDest, content: file.content });
|
|
367
|
+
writeActions.push(fileDest);
|
|
327
368
|
}
|
|
328
369
|
const env = (cfg['env'] ?? {}) as Record<string, string>;
|
|
329
370
|
const missingEnv = Object.entries(env).filter(([, v]) => v === '').map(([k]) => k);
|
|
330
|
-
// Resolve relative args to the install directory.
|
|
331
371
|
const looksLikePath = (a: string) =>
|
|
332
|
-
a.startsWith('./') || a.startsWith('../') || a.includes(
|
|
372
|
+
a.startsWith('./') || a.startsWith('../') || a.includes('/') || /\.\w+$/.test(a);
|
|
333
373
|
const args = ((cfg['args'] ?? []) as string[]).map((a) =>
|
|
334
|
-
path.isAbsolute(a) || !looksLikePath(a) ? a :
|
|
374
|
+
path.isAbsolute(a) || !looksLikePath(a) ? a : `${installDir}/${a.replace(/^\.\//, '')}`,
|
|
335
375
|
);
|
|
376
|
+
const serverName = (cfg['name'] as string | undefined) ?? sub.name;
|
|
336
377
|
localActions.push({
|
|
337
378
|
action: 'merge_mcp_json',
|
|
338
379
|
mcp_json_path: mcpJsonPath,
|
|
339
|
-
server_name:
|
|
380
|
+
server_name: serverName,
|
|
340
381
|
entry: { ...cfg, args },
|
|
341
382
|
...(missingEnv.length > 0 ? {
|
|
342
383
|
missing_env: missingEnv,
|
|
343
384
|
setup_hint: `Fill in env vars in ${mcpJsonPath} under mcpServers["${sub.name}"]: ${missingEnv.join(', ')}.`,
|
|
344
385
|
} : {}),
|
|
345
386
|
});
|
|
387
|
+
logger.info(
|
|
388
|
+
{
|
|
389
|
+
resourceId: sub.id,
|
|
390
|
+
resourceName: sub.name,
|
|
391
|
+
format: 'A',
|
|
392
|
+
installDir,
|
|
393
|
+
mcpJsonPath,
|
|
394
|
+
serverName,
|
|
395
|
+
writeFiles: writeActions,
|
|
396
|
+
missingEnv,
|
|
397
|
+
},
|
|
398
|
+
'sync_resources: MCP Format A — write_file + merge_mcp_json actions queued',
|
|
399
|
+
);
|
|
346
400
|
logToolStep('sync_resources', 'Local-executable MCP: write_file + merge_mcp_json queued', { resourceId: sub.id });
|
|
347
401
|
} else {
|
|
348
402
|
// ── Format B: remote URL map ────────────────────────────────────
|
|
349
|
-
|
|
403
|
+
const queuedServers: string[] = [];
|
|
350
404
|
for (const [serverName, entry] of Object.entries(cfg)) {
|
|
351
405
|
const e = entry as Record<string, unknown>;
|
|
352
406
|
const env = (e['env'] ?? {}) as Record<string, string>;
|
|
@@ -361,29 +415,57 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
361
415
|
setup_hint: `Fill in env vars in ${mcpJsonPath} under mcpServers["${serverName}"]: ${missingEnv.join(', ')}.`,
|
|
362
416
|
} : {}),
|
|
363
417
|
});
|
|
418
|
+
queuedServers.push(serverName);
|
|
364
419
|
}
|
|
420
|
+
logger.info(
|
|
421
|
+
{
|
|
422
|
+
resourceId: sub.id,
|
|
423
|
+
resourceName: sub.name,
|
|
424
|
+
format: 'B',
|
|
425
|
+
mcpJsonPath,
|
|
426
|
+
serverKeys: queuedServers,
|
|
427
|
+
},
|
|
428
|
+
'sync_resources: MCP Format B — merge_mcp_json actions queued',
|
|
429
|
+
);
|
|
365
430
|
logToolStep('sync_resources', 'Remote-URL MCP: merge_mcp_json queued', {
|
|
366
431
|
resourceId: sub.id, serverKeys: Object.keys(cfg),
|
|
367
432
|
});
|
|
368
433
|
}
|
|
369
434
|
} else {
|
|
370
|
-
// No mcp-config.json: heuristic fallback
|
|
371
|
-
const installDir =
|
|
435
|
+
// No mcp-config.json: heuristic fallback
|
|
436
|
+
const installDir = `${getCursorTypeDirForClient('mcp')}/${sub.name}`;
|
|
437
|
+
const writeActions: string[] = [];
|
|
372
438
|
for (const file of resourceFiles) {
|
|
373
439
|
const normalised = path.normalize(file.path);
|
|
374
440
|
if (normalised.startsWith('..')) continue;
|
|
375
|
-
|
|
441
|
+
const fileDest = `${installDir}/${normalised}`;
|
|
442
|
+
localActions.push({ action: 'write_file', path: fileDest, content: file.content });
|
|
443
|
+
writeActions.push(fileDest);
|
|
376
444
|
}
|
|
377
445
|
const jsEntry = resourceFiles.find((f) => f.path.endsWith('.js'));
|
|
378
446
|
const pyEntry = resourceFiles.find((f) => f.path.endsWith('.py'));
|
|
379
447
|
const entryFile = jsEntry ?? pyEntry ?? resourceFiles[0];
|
|
380
448
|
const cmd = jsEntry ? 'node' : 'python3';
|
|
449
|
+
const entryPath = `${installDir}/${entryFile?.path ?? ''}`;
|
|
381
450
|
localActions.push({
|
|
382
451
|
action: 'merge_mcp_json',
|
|
383
|
-
mcp_json_path:
|
|
452
|
+
mcp_json_path: mcpJsonPath,
|
|
384
453
|
server_name: sub.name,
|
|
385
|
-
entry: { command: cmd, args: [
|
|
454
|
+
entry: { command: cmd, args: [entryPath] },
|
|
386
455
|
});
|
|
456
|
+
logger.info(
|
|
457
|
+
{
|
|
458
|
+
resourceId: sub.id,
|
|
459
|
+
resourceName: sub.name,
|
|
460
|
+
format: 'heuristic',
|
|
461
|
+
installDir,
|
|
462
|
+
mcpJsonPath,
|
|
463
|
+
cmd,
|
|
464
|
+
entryPath,
|
|
465
|
+
writeFiles: writeActions,
|
|
466
|
+
},
|
|
467
|
+
'sync_resources: MCP heuristic fallback — write_file + merge_mcp_json actions queued',
|
|
468
|
+
);
|
|
387
469
|
logToolStep('sync_resources', 'MCP heuristic fallback: write_file + merge_mcp_json queued', { resourceId: sub.id });
|
|
388
470
|
}
|
|
389
471
|
|
|
@@ -394,8 +476,10 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
394
476
|
|
|
395
477
|
// ── Rule resource ─────────────────────────────────────────────────────
|
|
396
478
|
// Return write_file actions; the AI writes the files locally.
|
|
479
|
+
// Use client-side path so the path resolves on the user's machine.
|
|
397
480
|
if (sub.type === 'rule') {
|
|
398
|
-
const typeDir =
|
|
481
|
+
const typeDir = getCursorTypeDirForClient(sub.type);
|
|
482
|
+
const writeActions: string[] = [];
|
|
399
483
|
|
|
400
484
|
for (const file of resourceFiles) {
|
|
401
485
|
const normalised = path.normalize(file.path);
|
|
@@ -403,13 +487,26 @@ export async function syncResources(params: unknown): Promise<ToolResult<SyncRes
|
|
|
403
487
|
logger.warn({ resourceId: sub.id, filePath: file.path }, 'Skipping suspicious file path');
|
|
404
488
|
continue;
|
|
405
489
|
}
|
|
490
|
+
const destPath = `${typeDir}/${normalised}`;
|
|
406
491
|
localActions.push({
|
|
407
492
|
action: 'write_file',
|
|
408
|
-
path:
|
|
493
|
+
path: destPath,
|
|
409
494
|
content: file.content,
|
|
410
495
|
});
|
|
496
|
+
writeActions.push(destPath);
|
|
411
497
|
}
|
|
412
498
|
|
|
499
|
+
logger.info(
|
|
500
|
+
{
|
|
501
|
+
resourceId: sub.id,
|
|
502
|
+
resourceName: sub.name,
|
|
503
|
+
typeDir,
|
|
504
|
+
fileCount: writeActions.length,
|
|
505
|
+
destPaths: writeActions,
|
|
506
|
+
},
|
|
507
|
+
'sync_resources: Rule — write_file actions queued for AI',
|
|
508
|
+
);
|
|
509
|
+
|
|
413
510
|
tally.synced++;
|
|
414
511
|
details.push({ id: sub.id, name: sub.name, action: 'synced', version: resourceVersion });
|
|
415
512
|
logToolStep('sync_resources', 'Rule: write_file actions queued for AI', {
|
|
@@ -6,12 +6,9 @@
|
|
|
6
6
|
* For directory-based resources (skill, mcp) the entire install directory is removed.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import * as fs from 'fs/promises';
|
|
10
|
-
import * as path from 'path';
|
|
11
9
|
import { logger, logToolCall } from '../utils/logger';
|
|
12
|
-
import { filesystemManager } from '../filesystem/manager';
|
|
13
10
|
import { apiClient } from '../api/client';
|
|
14
|
-
import {
|
|
11
|
+
import { getCursorTypeDirForClient, getCursorRootDirForClient } from '../utils/cursor-paths.js';
|
|
15
12
|
import { MCPServerError, createValidationError } from '../types/errors';
|
|
16
13
|
import type { UninstallResourceParams, UninstallResourceResult, LocalAction, ToolResult } from '../types/tools';
|
|
17
14
|
import { promptManager } from '../prompts/index.js';
|
|
@@ -93,48 +90,30 @@ export async function uninstallResource(params: unknown): Promise<ToolResult<Uni
|
|
|
93
90
|
logger.debug({ pattern }, 'Building local uninstall actions for Rule/MCP resource...');
|
|
94
91
|
|
|
95
92
|
const localActions: LocalAction[] = [];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
} catch { /* rules dir may not exist */ }
|
|
111
|
-
|
|
112
|
-
// MCP: delete install directory + remove mcp.json entry
|
|
113
|
-
try {
|
|
114
|
-
const mcpDir = getCursorTypeDir('mcp');
|
|
115
|
-
const entries = await fs.readdir(mcpDir, { withFileTypes: true });
|
|
116
|
-
for (const entry of entries) {
|
|
117
|
-
if (!entry.isDirectory()) continue;
|
|
118
|
-
if (entry.name === pattern || entry.name.includes(pattern)) {
|
|
119
|
-
const dirPath = path.join(mcpDir, entry.name);
|
|
120
|
-
localActions.push({ action: 'delete_file', path: dirPath, recursive: true });
|
|
121
|
-
localActions.push({ action: 'remove_mcp_json_entry', mcp_json_path: mcpJsonPath, server_name: entry.name });
|
|
122
|
-
removedResources.push({ id: entry.name, name: entry.name, path: dirPath });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
} catch { /* mcp-servers dir may not exist */ }
|
|
126
|
-
|
|
127
|
-
// Also check Remote-URL MCPs whose entry is only in mcp.json (no local dir).
|
|
128
|
-
// The pattern might match a server name in mcp.json directly.
|
|
129
|
-
if (localActions.filter(a => a.action === 'remove_mcp_json_entry').length === 0) {
|
|
130
|
-
// Add a conditional remove action — the AI will check if the key exists.
|
|
131
|
-
localActions.push({
|
|
132
|
-
action: 'remove_mcp_json_entry',
|
|
133
|
-
mcp_json_path: mcpJsonPath,
|
|
134
|
-
server_name: pattern,
|
|
135
|
-
});
|
|
93
|
+
// Use client-side tilde-based paths; the MCP server may be running remotely
|
|
94
|
+
// and its os.homedir() would resolve to the server's home, not the user's.
|
|
95
|
+
const mcpJsonPath = `${getCursorRootDirForClient()}/mcp.json`;
|
|
96
|
+
|
|
97
|
+
// Rule: queue delete for ~/.cursor/rules/<pattern>.mdc and .md variants.
|
|
98
|
+
// We cannot scan the server's filesystem for the user's rules, so we emit
|
|
99
|
+
// delete actions for the two common extensions and let the AI skip missing files.
|
|
100
|
+
const rulesDir = getCursorTypeDirForClient('rule');
|
|
101
|
+
for (const ext of ['.mdc', '.md']) {
|
|
102
|
+
const filePath = `${rulesDir}/${pattern}${ext}`;
|
|
103
|
+
localActions.push({ action: 'delete_file', path: filePath });
|
|
104
|
+
removedResources.push({ id: pattern, name: pattern, path: filePath });
|
|
136
105
|
}
|
|
137
106
|
|
|
107
|
+
// MCP: queue delete of install directory (Format A) + remove mcp.json entry.
|
|
108
|
+
// For Format B (remote URL only) there is no local directory, but we still
|
|
109
|
+
// need to remove the mcp.json entry — the AI will skip the delete if the
|
|
110
|
+
// directory does not exist.
|
|
111
|
+
const mcpDir = getCursorTypeDirForClient('mcp');
|
|
112
|
+
const mcpInstallDir = `${mcpDir}/${pattern}`;
|
|
113
|
+
localActions.push({ action: 'delete_file', path: mcpInstallDir, recursive: true });
|
|
114
|
+
localActions.push({ action: 'remove_mcp_json_entry', mcp_json_path: mcpJsonPath, server_name: pattern });
|
|
115
|
+
removedResources.push({ id: pattern, name: pattern, path: mcpInstallDir });
|
|
116
|
+
|
|
138
117
|
if (removedResources.length === 0 && localActions.length === 0) {
|
|
139
118
|
throw createValidationError(
|
|
140
119
|
pattern,
|
|
@@ -29,6 +29,11 @@ export const CURSOR_TYPE_DIRS: Record<string, string> = {
|
|
|
29
29
|
*
|
|
30
30
|
* macOS / Linux : ~/.cursor
|
|
31
31
|
* Windows : %APPDATA%\Cursor\User
|
|
32
|
+
*
|
|
33
|
+
* NOTE: Only use this when running code on the USER's local machine.
|
|
34
|
+
* When generating paths for LocalAction instructions (which are executed by the
|
|
35
|
+
* AI on the user's machine, not on this server), use getCursorRootDirForClient()
|
|
36
|
+
* instead to avoid returning the server's home directory.
|
|
32
37
|
*/
|
|
33
38
|
export function getCursorRootDir(): string {
|
|
34
39
|
if (process.platform === 'win32') {
|
|
@@ -40,6 +45,40 @@ export function getCursorRootDir(): string {
|
|
|
40
45
|
return path.join(os.homedir(), '.cursor');
|
|
41
46
|
}
|
|
42
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Returns a platform-neutral Cursor root path for use in LocalAction instructions.
|
|
50
|
+
*
|
|
51
|
+
* LocalAction paths are sent to the AI Agent running on the USER's local machine,
|
|
52
|
+
* not executed on this (possibly remote) server. Using os.homedir() here would
|
|
53
|
+
* produce the server's home directory (e.g. /root/.cursor on a Linux server),
|
|
54
|
+
* which is wrong when the user is on macOS or Windows.
|
|
55
|
+
*
|
|
56
|
+
* We return a tilde-prefixed path ("~/.cursor") which the AI / shell on the
|
|
57
|
+
* user's machine will expand to the correct home directory automatically.
|
|
58
|
+
* For Windows we still return the APPDATA-relative form as a hint, but note
|
|
59
|
+
* that the AI is expected to expand %APPDATA% on the client side.
|
|
60
|
+
*/
|
|
61
|
+
export function getCursorRootDirForClient(): string {
|
|
62
|
+
// Return a portable ~-based path; the AI on the user's machine expands it.
|
|
63
|
+
return '~/.cursor';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the Cursor subdirectory for a given resource type, using a
|
|
68
|
+
* client-side portable path (tilde-based). Use this when building paths
|
|
69
|
+
* that will be included in LocalAction instructions.
|
|
70
|
+
*/
|
|
71
|
+
export function getCursorTypeDirForClient(resourceType: string): string {
|
|
72
|
+
const subdir = CURSOR_TYPE_DIRS[resourceType.toLowerCase()];
|
|
73
|
+
if (!subdir) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`Unknown resource type "${resourceType}". ` +
|
|
76
|
+
`Supported types: ${Object.keys(CURSOR_TYPE_DIRS).join(', ')}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return `${getCursorRootDirForClient()}/${subdir}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
43
82
|
/**
|
|
44
83
|
* Returns the Cursor subdirectory for a given resource type.
|
|
45
84
|
*
|