deepseek-pp-shell-host 0.7.4 → 0.7.5
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/native/shell-mcp-host.mjs +452 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync, spawn } from 'node:child_process';
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import {
|
|
6
6
|
arch,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type as osType,
|
|
13
13
|
version as osVersion,
|
|
14
14
|
} from 'node:os';
|
|
15
|
-
import { existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
|
15
|
+
import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
|
|
16
16
|
|
|
17
17
|
// Resolve package root from this script's location (native/ -> package root).
|
|
18
18
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -63,6 +63,14 @@ const MAX_PYTHON_CODE_BYTES = 60_000;
|
|
|
63
63
|
const MAX_PYTHON_OUTPUT_BYTES = 64_000;
|
|
64
64
|
const PYTHON_PACKAGE_CHECKS = ['numpy', 'pandas', 'sympy'];
|
|
65
65
|
const PYTHON_NOT_FOUND_MESSAGE = 'No local Python interpreter found. Tried environment variables, PATH entries, common paths, and python/python3/py --version.';
|
|
66
|
+
const MAX_LOCAL_SKILLS = 80;
|
|
67
|
+
const MAX_LOCAL_SKILL_BYTES = 120_000;
|
|
68
|
+
const MAX_LOCAL_RESOURCE_FILES_PER_SKILL = 16;
|
|
69
|
+
const MAX_LOCAL_RESOURCE_BYTES_PER_SKILL = 100_000;
|
|
70
|
+
const MAX_LOCAL_RESOURCE_FILE_BYTES = 40_000;
|
|
71
|
+
const MAX_LOCAL_TOTAL_CONTENT_BYTES = 420_000;
|
|
72
|
+
const LOCAL_TEXT_RESOURCE_EXTENSIONS = new Set(['.md', '.txt', '.yaml', '.yml', '.json', '.tex']);
|
|
73
|
+
const LOCAL_SCRIPT_EXTENSIONS = new Set(['.py', '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.sh', '.bash', '.zsh', '.ps1', '.rb', '.pl', '.php', '.lua', '.r']);
|
|
66
74
|
const DEFAULT_SHELL = platform() === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/sh';
|
|
67
75
|
const WINDOWS_POWERSHELL_UTF8_PREAMBLE = [
|
|
68
76
|
'[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)',
|
|
@@ -118,6 +126,39 @@ const TOOL_DEFINITIONS = [
|
|
|
118
126
|
},
|
|
119
127
|
annotations: { operation: 'execute', risk: 'high' },
|
|
120
128
|
},
|
|
129
|
+
{
|
|
130
|
+
name: 'local_skill_preview',
|
|
131
|
+
title: 'Preview Local Skill Folder',
|
|
132
|
+
description: 'Read SKILL.md files, nearby text resources, and script file manifests from a local Skill folder. Does not execute local code.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
rootPath: { type: 'string', description: 'Absolute local folder path that contains one or more SKILL.md files.' },
|
|
137
|
+
selectedPaths: {
|
|
138
|
+
type: 'array',
|
|
139
|
+
items: { type: 'string' },
|
|
140
|
+
description: 'Optional SKILL.md paths relative to rootPath. When omitted, previews all detected Skills up to the limit.',
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
required: ['rootPath'],
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
},
|
|
146
|
+
annotations: { operation: 'read', risk: 'medium' },
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'local_folder_pick',
|
|
150
|
+
title: 'Pick Local Folder',
|
|
151
|
+
description: 'Open the operating system folder picker and return the absolute path selected by the user.',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
title: { type: 'string', description: 'Optional prompt shown in the native folder picker.' },
|
|
156
|
+
defaultPath: { type: 'string', description: 'Optional local folder path to use as the initial picker location.' },
|
|
157
|
+
},
|
|
158
|
+
additionalProperties: false,
|
|
159
|
+
},
|
|
160
|
+
annotations: { operation: 'read', risk: 'low' },
|
|
161
|
+
},
|
|
121
162
|
];
|
|
122
163
|
|
|
123
164
|
// --- Native messaging framing (4-byte LE length prefix) ---
|
|
@@ -288,9 +329,418 @@ async function handleCallTool(id, params) {
|
|
|
288
329
|
return jsonRpcResult(id, await executePythonTool(args));
|
|
289
330
|
}
|
|
290
331
|
|
|
332
|
+
if (name === 'local_skill_preview') {
|
|
333
|
+
return jsonRpcResult(id, createLocalSkillPreviewResult(args));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (name === 'local_folder_pick') {
|
|
337
|
+
return jsonRpcResult(id, createLocalFolderPickResult(args));
|
|
338
|
+
}
|
|
339
|
+
|
|
291
340
|
return jsonRpcError(id, -32602, `Unknown tool: ${name}`);
|
|
292
341
|
}
|
|
293
342
|
|
|
343
|
+
// --- Local folder picker ---
|
|
344
|
+
|
|
345
|
+
function createLocalFolderPickResult(args) {
|
|
346
|
+
const title = typeof args?.title === 'string' && args.title.trim()
|
|
347
|
+
? args.title.trim()
|
|
348
|
+
: 'Choose a local Skill folder';
|
|
349
|
+
const defaultPath = typeof args?.defaultPath === 'string' && args.defaultPath.trim()
|
|
350
|
+
? resolveFolderPickerDefault(args.defaultPath)
|
|
351
|
+
: '';
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const selectedPath = pickLocalFolder({ title, defaultPath });
|
|
355
|
+
const normalizedPath = resolveLocalPath(selectedPath);
|
|
356
|
+
const selectedStat = safeStat(normalizedPath);
|
|
357
|
+
if (!selectedStat || !selectedStat.isDirectory()) {
|
|
358
|
+
throw new Error(`Selected path is not a readable directory: ${normalizedPath}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: 'text', text: `Selected local folder: ${normalizedPath}` }],
|
|
363
|
+
structuredContent: {
|
|
364
|
+
ok: true,
|
|
365
|
+
data: { path: normalizedPath },
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
} catch (err) {
|
|
369
|
+
return {
|
|
370
|
+
isError: true,
|
|
371
|
+
content: [{ type: 'text', text: normalizeFolderPickerError(err) }],
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function pickLocalFolder({ title, defaultPath }) {
|
|
377
|
+
const hostPlatform = platform();
|
|
378
|
+
if (hostPlatform === 'darwin') return pickLocalFolderOnMac(title, defaultPath);
|
|
379
|
+
if (hostPlatform === 'win32') return pickLocalFolderOnWindows(title, defaultPath);
|
|
380
|
+
return pickLocalFolderOnLinux(title, defaultPath);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function pickLocalFolderOnMac(title, defaultPath) {
|
|
384
|
+
const script = [
|
|
385
|
+
'on run argv',
|
|
386
|
+
' set promptText to item 1 of argv',
|
|
387
|
+
' set defaultPath to item 2 of argv',
|
|
388
|
+
' if defaultPath is not "" then',
|
|
389
|
+
' set chosenFolder to choose folder with prompt promptText default location (POSIX file defaultPath)',
|
|
390
|
+
' else',
|
|
391
|
+
' set chosenFolder to choose folder with prompt promptText',
|
|
392
|
+
' end if',
|
|
393
|
+
' return POSIX path of chosenFolder',
|
|
394
|
+
'end run',
|
|
395
|
+
].join('\n');
|
|
396
|
+
return execFileSync('osascript', ['-e', script, title, defaultPath || ''], {
|
|
397
|
+
encoding: 'utf8',
|
|
398
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
399
|
+
windowsHide: true,
|
|
400
|
+
}).trim();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function pickLocalFolderOnWindows(title, defaultPath) {
|
|
404
|
+
const script = [
|
|
405
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
406
|
+
'$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
407
|
+
'$dialog.Description = $args[0]',
|
|
408
|
+
'$dialog.ShowNewFolderButton = $false',
|
|
409
|
+
'if ($args.Count -gt 1 -and $args[1]) { $dialog.SelectedPath = $args[1] }',
|
|
410
|
+
'if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
411
|
+
' [Console]::Out.Write($dialog.SelectedPath)',
|
|
412
|
+
'} else {',
|
|
413
|
+
' [Environment]::Exit(2)',
|
|
414
|
+
'}',
|
|
415
|
+
].join('; ');
|
|
416
|
+
return execFileSync('powershell.exe', ['-NoProfile', '-STA', '-Command', script, title, defaultPath || ''], {
|
|
417
|
+
encoding: 'utf8',
|
|
418
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
419
|
+
windowsHide: false,
|
|
420
|
+
}).trim();
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function pickLocalFolderOnLinux(title, defaultPath) {
|
|
424
|
+
const linuxPickers = [
|
|
425
|
+
{
|
|
426
|
+
command: 'zenity',
|
|
427
|
+
args: ['--file-selection', '--directory', '--title', title, ...(defaultPath ? ['--filename', ensureTrailingPathSeparator(defaultPath)] : [])],
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
command: 'kdialog',
|
|
431
|
+
args: ['--getexistingdirectory', defaultPath || homedir(), '--title', title],
|
|
432
|
+
},
|
|
433
|
+
];
|
|
434
|
+
const missing = [];
|
|
435
|
+
for (const picker of linuxPickers) {
|
|
436
|
+
try {
|
|
437
|
+
return execFileSync(picker.command, picker.args, {
|
|
438
|
+
encoding: 'utf8',
|
|
439
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
440
|
+
windowsHide: true,
|
|
441
|
+
}).trim();
|
|
442
|
+
} catch (err) {
|
|
443
|
+
if (err?.code === 'ENOENT') {
|
|
444
|
+
missing.push(picker.command);
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
throw err;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
throw new Error(`No graphical folder picker is available. Install one of: ${missing.join(', ')}.`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function resolveFolderPickerDefault(input) {
|
|
454
|
+
const resolved = resolveLocalPath(input);
|
|
455
|
+
const stat = safeStat(resolved);
|
|
456
|
+
if (stat?.isDirectory()) return resolved;
|
|
457
|
+
return homedir();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function ensureTrailingPathSeparator(value) {
|
|
461
|
+
return value.endsWith('/') ? value : `${value}/`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function normalizeFolderPickerError(err) {
|
|
465
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
466
|
+
if (/User canceled|cancelled|canceled|exit code 2|The operation couldn.?t be completed/i.test(message)) {
|
|
467
|
+
return 'Folder selection was cancelled.';
|
|
468
|
+
}
|
|
469
|
+
return message;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// --- Local Skill preview ---
|
|
473
|
+
|
|
474
|
+
function createLocalSkillPreviewResult(args) {
|
|
475
|
+
const rootInput = args?.rootPath;
|
|
476
|
+
if (typeof rootInput !== 'string' || rootInput.trim().length === 0) {
|
|
477
|
+
return {
|
|
478
|
+
isError: true,
|
|
479
|
+
content: [{ type: 'text', text: 'rootPath is required and must be a non-empty string.' }],
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const selectedPaths = Array.isArray(args?.selectedPaths)
|
|
485
|
+
? new Set(args.selectedPaths.filter(item => typeof item === 'string' && item.trim()).map(normalizeRelativePath))
|
|
486
|
+
: null;
|
|
487
|
+
const data = scanLocalSkillFolder(rootInput, selectedPaths);
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: 'text', text: `Found ${data.skills.length} local Skill(s) in ${data.rootPath}` }],
|
|
490
|
+
structuredContent: {
|
|
491
|
+
ok: true,
|
|
492
|
+
data,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
} catch (err) {
|
|
496
|
+
return {
|
|
497
|
+
isError: true,
|
|
498
|
+
content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function scanLocalSkillFolder(rootInput, selectedPaths) {
|
|
504
|
+
const rootPath = resolveLocalPath(rootInput);
|
|
505
|
+
const rootStat = safeStat(rootPath);
|
|
506
|
+
if (!rootStat || !rootStat.isDirectory()) {
|
|
507
|
+
throw new Error(`Local Skill root is not a readable directory: ${rootPath}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const warnings = [];
|
|
511
|
+
const allSkillPaths = findLocalSkillPaths(rootPath);
|
|
512
|
+
if (allSkillPaths.length === 0) {
|
|
513
|
+
throw new Error(`No SKILL.md found under ${rootPath}`);
|
|
514
|
+
}
|
|
515
|
+
if (allSkillPaths.length > MAX_LOCAL_SKILLS) {
|
|
516
|
+
warnings.push(`Found ${allSkillPaths.length} Skills; preview is limited to ${MAX_LOCAL_SKILLS}.`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const limitedPaths = allSkillPaths.slice(0, MAX_LOCAL_SKILLS);
|
|
520
|
+
const selected = selectedPaths
|
|
521
|
+
? limitedPaths.filter(path => selectedPaths.has(path))
|
|
522
|
+
: limitedPaths;
|
|
523
|
+
if (selectedPaths && selected.length === 0) {
|
|
524
|
+
throw new Error('Selected local Skill paths were not found under the root path.');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
let totalContentBytes = 0;
|
|
528
|
+
const skills = [];
|
|
529
|
+
for (const skillPath of selected) {
|
|
530
|
+
const item = readLocalSkill(rootPath, skillPath, totalContentBytes);
|
|
531
|
+
totalContentBytes += item.contentBytes;
|
|
532
|
+
skills.push(item.skill);
|
|
533
|
+
warnings.push(...item.warnings);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
rootPath,
|
|
538
|
+
displayName: basename(rootPath) || rootPath,
|
|
539
|
+
directoryName: basename(rootPath) || rootPath,
|
|
540
|
+
skills,
|
|
541
|
+
warnings: dedupeStrings(warnings),
|
|
542
|
+
truncated: allSkillPaths.length > MAX_LOCAL_SKILLS || warnings.some(warning => warning.includes('content budget')),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function findLocalSkillPaths(rootPath) {
|
|
547
|
+
const result = [];
|
|
548
|
+
walkLocalDirectory(rootPath, '', (relativePath, absolutePath, entry) => {
|
|
549
|
+
if (!entry.isFile()) return;
|
|
550
|
+
if (entry.name === 'SKILL.md') result.push(normalizeRelativePath(relativePath));
|
|
551
|
+
});
|
|
552
|
+
return result.sort((a, b) => a.localeCompare(b));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function readLocalSkill(rootPath, skillPath, usedContentBytes) {
|
|
556
|
+
const absoluteSkillPath = resolveUnderRoot(rootPath, skillPath);
|
|
557
|
+
const skillStat = safeStat(absoluteSkillPath);
|
|
558
|
+
if (!skillStat || !skillStat.isFile()) {
|
|
559
|
+
throw new Error(`Local Skill file is not readable: ${skillPath}`);
|
|
560
|
+
}
|
|
561
|
+
if (skillStat.size > MAX_LOCAL_SKILL_BYTES) {
|
|
562
|
+
throw new Error(`${skillPath} exceeds the SKILL.md size limit (${skillStat.size} bytes).`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const content = readTextFile(absoluteSkillPath);
|
|
566
|
+
const directory = normalizeRelativePath(dirname(skillPath));
|
|
567
|
+
const directoryPath = dirname(absoluteSkillPath);
|
|
568
|
+
const bundle = collectLocalSkillResources(rootPath, directory, content, usedContentBytes + Buffer.byteLength(content, 'utf8'));
|
|
569
|
+
const skill = {
|
|
570
|
+
path: skillPath,
|
|
571
|
+
directory,
|
|
572
|
+
directoryPath,
|
|
573
|
+
content,
|
|
574
|
+
bodyBytes: Buffer.byteLength(content, 'utf8'),
|
|
575
|
+
includedFiles: bundle.includedFiles,
|
|
576
|
+
omittedFiles: bundle.omittedFiles,
|
|
577
|
+
scriptFiles: bundle.scriptFiles,
|
|
578
|
+
warnings: bundle.warnings,
|
|
579
|
+
};
|
|
580
|
+
const contentBytes = skill.bodyBytes + bundle.includedFiles.reduce((sum, file) => sum + file.bytes, 0);
|
|
581
|
+
return {
|
|
582
|
+
skill,
|
|
583
|
+
contentBytes,
|
|
584
|
+
warnings: bundle.warnings,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function collectLocalSkillResources(rootPath, directory, skillBody, startingContentBytes) {
|
|
589
|
+
const prefix = directory ? directory + '/' : '';
|
|
590
|
+
const candidates = [];
|
|
591
|
+
walkLocalDirectory(resolveUnderRoot(rootPath, directory || '.'), prefix, (relativePath, absolutePath, entry) => {
|
|
592
|
+
if (!entry.isFile()) return;
|
|
593
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
594
|
+
if (normalized === `${prefix}SKILL.md` || normalized.endsWith('/SKILL.md')) return;
|
|
595
|
+
const stat = safeStat(absolutePath);
|
|
596
|
+
if (!stat) return;
|
|
597
|
+
candidates.push({
|
|
598
|
+
path: normalized,
|
|
599
|
+
absolutePath,
|
|
600
|
+
bytes: stat.size,
|
|
601
|
+
});
|
|
602
|
+
}, { stopAtNestedSkillRoots: true });
|
|
603
|
+
|
|
604
|
+
const scriptFiles = candidates
|
|
605
|
+
.filter(candidate => isLocalScriptFile(candidate.path))
|
|
606
|
+
.map(({ path, bytes }) => ({ path, bytes }));
|
|
607
|
+
const textCandidates = candidates
|
|
608
|
+
.filter(candidate => isLocalTextResource(candidate.path))
|
|
609
|
+
.sort((a, b) => rankLocalResource(a.path, skillBody) - rankLocalResource(b.path, skillBody) || a.path.localeCompare(b.path));
|
|
610
|
+
|
|
611
|
+
const includedFiles = [];
|
|
612
|
+
const omittedFiles = [];
|
|
613
|
+
const warnings = [];
|
|
614
|
+
let resourceBytes = 0;
|
|
615
|
+
let totalBytes = startingContentBytes;
|
|
616
|
+
|
|
617
|
+
for (const candidate of textCandidates) {
|
|
618
|
+
if (includedFiles.length >= MAX_LOCAL_RESOURCE_FILES_PER_SKILL) {
|
|
619
|
+
omittedFiles.push({ path: candidate.path, bytes: candidate.bytes });
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (candidate.bytes > MAX_LOCAL_RESOURCE_FILE_BYTES) {
|
|
623
|
+
omittedFiles.push({ path: candidate.path, bytes: candidate.bytes });
|
|
624
|
+
warnings.push(`${candidate.path} exceeds the per-file resource limit and was not bundled.`);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (resourceBytes + candidate.bytes > MAX_LOCAL_RESOURCE_BYTES_PER_SKILL) {
|
|
628
|
+
omittedFiles.push({ path: candidate.path, bytes: candidate.bytes });
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
if (totalBytes + candidate.bytes > MAX_LOCAL_TOTAL_CONTENT_BYTES) {
|
|
632
|
+
omittedFiles.push({ path: candidate.path, bytes: candidate.bytes });
|
|
633
|
+
warnings.push(`${candidate.path} was omitted because the local Skill preview reached the content budget.`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const content = readTextFile(candidate.absolutePath);
|
|
638
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
639
|
+
resourceBytes += bytes;
|
|
640
|
+
totalBytes += bytes;
|
|
641
|
+
includedFiles.push({ path: candidate.path, bytes, content });
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (omittedFiles.length > 0) {
|
|
645
|
+
warnings.push(`${omittedFiles.length} local supporting file(s) were omitted.`);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return { includedFiles, omittedFiles, scriptFiles, warnings: dedupeStrings(warnings) };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function walkLocalDirectory(rootPath, prefix, visit, options = {}) {
|
|
652
|
+
const stack = [{ absolutePath: rootPath, relativePrefix: prefix }];
|
|
653
|
+
while (stack.length > 0) {
|
|
654
|
+
const current = stack.pop();
|
|
655
|
+
const entries = safeReadDirectory(current.absolutePath);
|
|
656
|
+
for (const entry of entries) {
|
|
657
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.svn' || entry.name === '.hg') continue;
|
|
658
|
+
const absolutePath = join(current.absolutePath, entry.name);
|
|
659
|
+
const relativePath = normalizeRelativePath(join(current.relativePrefix, entry.name));
|
|
660
|
+
visit(relativePath, absolutePath, entry);
|
|
661
|
+
if (entry.isDirectory()) {
|
|
662
|
+
if (options.stopAtNestedSkillRoots && hasLocalSkillFile(absolutePath)) continue;
|
|
663
|
+
stack.push({ absolutePath, relativePrefix: relativePath });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function resolveLocalPath(input) {
|
|
670
|
+
const trimmed = input.trim();
|
|
671
|
+
if (trimmed === '~') return homedir();
|
|
672
|
+
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
673
|
+
return resolve(homedir(), trimmed.slice(2));
|
|
674
|
+
}
|
|
675
|
+
return resolve(trimmed);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function resolveUnderRoot(rootPath, relativePath) {
|
|
679
|
+
const resolved = resolve(rootPath, relativePath);
|
|
680
|
+
const rel = relative(rootPath, resolved);
|
|
681
|
+
if (rel.startsWith('..') || rel === '..' || isAbsolute(rel)) {
|
|
682
|
+
throw new Error(`Path escapes local Skill root: ${relativePath}`);
|
|
683
|
+
}
|
|
684
|
+
return resolved;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function readTextFile(filePath) {
|
|
688
|
+
return readFileSync(filePath, 'utf8');
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function safeReadDirectory(dir) {
|
|
692
|
+
try {
|
|
693
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
694
|
+
} catch {
|
|
695
|
+
return [];
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function safeStat(path) {
|
|
700
|
+
try {
|
|
701
|
+
return statSync(path);
|
|
702
|
+
} catch {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function hasLocalSkillFile(directoryPath) {
|
|
708
|
+
return safeStat(join(directoryPath, 'SKILL.md'))?.isFile() === true;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function isLocalTextResource(path) {
|
|
712
|
+
return LOCAL_TEXT_RESOURCE_EXTENSIONS.has(pathExtension(path));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function isLocalScriptFile(path) {
|
|
716
|
+
return LOCAL_SCRIPT_EXTENSIONS.has(pathExtension(path));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function rankLocalResource(path, skillBody) {
|
|
720
|
+
const relativeName = path.split('/').slice(-2).join('/');
|
|
721
|
+
if (skillBody.includes(path) || skillBody.includes(relativeName)) return 0;
|
|
722
|
+
if (path.includes('/agents/')) return 1;
|
|
723
|
+
if (path.includes('/references/')) return 2;
|
|
724
|
+
if (path.includes('/templates/')) return 3;
|
|
725
|
+
if (path.includes('/examples/')) return 4;
|
|
726
|
+
return 5;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function pathExtension(path) {
|
|
730
|
+
const name = path.split('/').pop() ?? '';
|
|
731
|
+
const index = name.lastIndexOf('.');
|
|
732
|
+
return index >= 0 ? name.slice(index).toLowerCase() : '';
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function normalizeRelativePath(path) {
|
|
736
|
+
const normalized = String(path || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
737
|
+
return normalized === '.' ? '' : normalized;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function dedupeStrings(values) {
|
|
741
|
+
return [...new Set(values.filter(Boolean))];
|
|
742
|
+
}
|
|
743
|
+
|
|
294
744
|
// --- Shell execution ---
|
|
295
745
|
|
|
296
746
|
function execCommand(command, { cwd, env, timeoutMs }) {
|