@geotechcli/core 0.4.11 → 0.4.13
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/agents/brain.d.ts +1 -0
- package/dist/agents/brain.d.ts.map +1 -1
- package/dist/agents/brain.js +34 -3
- package/dist/agents/brain.js.map +1 -1
- package/dist/agents/skill-tools.d.ts +2 -0
- package/dist/agents/skill-tools.d.ts.map +1 -0
- package/dist/agents/skill-tools.js +132 -0
- package/dist/agents/skill-tools.js.map +1 -0
- package/dist/agents/swarm.d.ts +3 -2
- package/dist/agents/swarm.d.ts.map +1 -1
- package/dist/agents/swarm.js +46 -16
- package/dist/agents/swarm.js.map +1 -1
- package/dist/config/index.d.ts +31 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -1
- package/dist/config/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/types.d.ts +1 -0
- package/dist/llm/types.d.ts.map +1 -1
- package/dist/llm/types.js.map +1 -1
- package/dist/meta/metadata.json +1 -1
- package/dist/skills/approval.d.ts +10 -0
- package/dist/skills/approval.d.ts.map +1 -0
- package/dist/skills/approval.js +70 -0
- package/dist/skills/approval.js.map +1 -0
- package/dist/skills/index.d.ts +89 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +869 -0
- package/dist/skills/index.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,869 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
|
+
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
6
|
+
import { loadConfig } from '../config/index.js';
|
|
7
|
+
import { ensureWorkspace } from '../agents/sandbox.js';
|
|
8
|
+
import { validateReadPath, validateWritePath, getWorkspaceDir } from '../agents/sandbox.js';
|
|
9
|
+
import { addArtifact, addNote, saveNamedDataset } from '../storage/index.js';
|
|
10
|
+
export { getStrongBetaSkillApproval, isStrongBetaSkillApproved, STRONG_BETA_SKILL_APPROVALS, } from './approval.js';
|
|
11
|
+
export const AGENT_SKILL_TOOL_NAMES = ['list_skills', 'describe_skill', 'run_skill'];
|
|
12
|
+
const MANIFEST_FILENAME = '.geotechcli-skill.json';
|
|
13
|
+
const SUPPORTED_SKILL_FILE_EXTENSIONS = new Set([
|
|
14
|
+
'.py',
|
|
15
|
+
'.md',
|
|
16
|
+
'.json',
|
|
17
|
+
'.csv',
|
|
18
|
+
'.yaml',
|
|
19
|
+
'.yml',
|
|
20
|
+
'.txt',
|
|
21
|
+
]);
|
|
22
|
+
const SYSTEM_ENV_KEYS = [
|
|
23
|
+
'PATH',
|
|
24
|
+
'PATHEXT',
|
|
25
|
+
'SYSTEMROOT',
|
|
26
|
+
'COMSPEC',
|
|
27
|
+
'WINDIR',
|
|
28
|
+
'TMP',
|
|
29
|
+
'TEMP',
|
|
30
|
+
'HOME',
|
|
31
|
+
'USERPROFILE',
|
|
32
|
+
'LANG',
|
|
33
|
+
'LC_ALL',
|
|
34
|
+
];
|
|
35
|
+
const DEFAULT_SKILL_EXECUTION_PROFILE = {
|
|
36
|
+
contract: 'input-dir-output-dir',
|
|
37
|
+
};
|
|
38
|
+
const SKILL_EXECUTION_PROFILES = {
|
|
39
|
+
'epb-conditioning-clogging': {
|
|
40
|
+
contract: 'legacy-file-inputs',
|
|
41
|
+
inputBindings: [
|
|
42
|
+
{ flag: '--ground-model', fileName: 'ground_model.json' },
|
|
43
|
+
{ flag: '--machine-config', fileName: 'machine_config.json' },
|
|
44
|
+
{ flag: '--alignment-zones', fileName: 'alignment_zones.csv' },
|
|
45
|
+
],
|
|
46
|
+
candidateArtifactTypes: ['assumptions', 'results', 'issues-and-corrections'],
|
|
47
|
+
},
|
|
48
|
+
'epb-face-support-window': {
|
|
49
|
+
contract: 'legacy-file-inputs',
|
|
50
|
+
inputBindings: [
|
|
51
|
+
{ flag: '--ground-model', fileName: 'ground_model.json' },
|
|
52
|
+
{ flag: '--machine-config', fileName: 'machine_config.json' },
|
|
53
|
+
{ flag: '--alignment-zones', fileName: 'alignment_zones.csv' },
|
|
54
|
+
],
|
|
55
|
+
candidateArtifactTypes: ['results', 'review-checklist', 'acceptance-status'],
|
|
56
|
+
},
|
|
57
|
+
'epb-production-and-ring-cycle': {
|
|
58
|
+
contract: 'legacy-file-inputs',
|
|
59
|
+
inputBindings: [
|
|
60
|
+
{ flag: '--monitoring', fileName: 'monitoring.csv' },
|
|
61
|
+
{ flag: '--machine-config', fileName: 'machine_config.json' },
|
|
62
|
+
],
|
|
63
|
+
candidateArtifactTypes: ['results', 'final-report', 'review-checklist'],
|
|
64
|
+
},
|
|
65
|
+
'epb-soft-ground-screening': {
|
|
66
|
+
contract: 'legacy-file-inputs',
|
|
67
|
+
inputBindings: [
|
|
68
|
+
{ flag: '--ground-model', fileName: 'ground_model.json' },
|
|
69
|
+
{ flag: '--machine-config', fileName: 'machine_config.json' },
|
|
70
|
+
{ flag: '--alignment-zones', fileName: 'alignment_zones.csv' },
|
|
71
|
+
],
|
|
72
|
+
candidateArtifactTypes: ['ground-model', 'analysis-plan', 'results'],
|
|
73
|
+
},
|
|
74
|
+
'mixed-face-transition-planning': {
|
|
75
|
+
contract: 'legacy-file-inputs',
|
|
76
|
+
inputBindings: [
|
|
77
|
+
{ flag: '--ground-model', fileName: 'ground_model.json' },
|
|
78
|
+
{ flag: '--machine-config', fileName: 'machine_config.json' },
|
|
79
|
+
{ flag: '--alignment-zones', fileName: 'alignment_zones.csv' },
|
|
80
|
+
],
|
|
81
|
+
candidateArtifactTypes: ['analysis-plan', 'results', 'review-checklist', 'issues-and-corrections'],
|
|
82
|
+
},
|
|
83
|
+
'soft-ground-settlement-observational-control': {
|
|
84
|
+
contract: 'legacy-file-inputs',
|
|
85
|
+
inputBindings: [
|
|
86
|
+
{ flag: '--ground-model', fileName: 'ground_model.json' },
|
|
87
|
+
{ flag: '--machine-config', fileName: 'machine_config.json' },
|
|
88
|
+
{ flag: '--alignment-zones', fileName: 'alignment_zones.csv' },
|
|
89
|
+
{ flag: '--monitoring', fileName: 'monitoring.csv' },
|
|
90
|
+
],
|
|
91
|
+
candidateArtifactTypes: ['results', 'review-checklist', 'acceptance-status', 'issues-and-corrections'],
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
function getSkillsDirFallback() {
|
|
95
|
+
return join(process.env.GEOTECHCLI_CONFIG_DIR ?? join(homedir(), '.geotechcli'), 'skills');
|
|
96
|
+
}
|
|
97
|
+
function nowIso() {
|
|
98
|
+
return new Date().toISOString();
|
|
99
|
+
}
|
|
100
|
+
function isDirectory(path) {
|
|
101
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
102
|
+
}
|
|
103
|
+
function isFile(path) {
|
|
104
|
+
return existsSync(path) && statSync(path).isFile();
|
|
105
|
+
}
|
|
106
|
+
function titleCaseFromName(value) {
|
|
107
|
+
return value
|
|
108
|
+
.split(/[-_]+/)
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
111
|
+
.join(' ');
|
|
112
|
+
}
|
|
113
|
+
function readTextIfExists(path) {
|
|
114
|
+
return isFile(path) ? readFileSync(path, 'utf-8') : null;
|
|
115
|
+
}
|
|
116
|
+
function parseFrontmatter(skillMarkdown) {
|
|
117
|
+
const match = skillMarkdown.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
118
|
+
const block = match?.[1] ?? '';
|
|
119
|
+
const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim();
|
|
120
|
+
const description = block.match(/^description:\s*(.+)$/m)?.[1]?.trim();
|
|
121
|
+
return {
|
|
122
|
+
name,
|
|
123
|
+
description,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function parseOpenAIYamlDisplayName(openAIYaml) {
|
|
127
|
+
if (!openAIYaml)
|
|
128
|
+
return undefined;
|
|
129
|
+
return openAIYaml.match(/^\s*display_name:\s*"?(.+?)"?\s*$/m)?.[1]?.trim();
|
|
130
|
+
}
|
|
131
|
+
function discoverSkillRoots(rootDir) {
|
|
132
|
+
const discovered = new Set();
|
|
133
|
+
function walk(currentDir) {
|
|
134
|
+
if (isFile(join(currentDir, 'SKILL.md'))) {
|
|
135
|
+
discovered.add(currentDir);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
139
|
+
if (!entry.isDirectory())
|
|
140
|
+
continue;
|
|
141
|
+
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
142
|
+
continue;
|
|
143
|
+
walk(join(currentDir, entry.name));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
walk(rootDir);
|
|
147
|
+
return [...discovered].sort((left, right) => left.localeCompare(right));
|
|
148
|
+
}
|
|
149
|
+
function walkFiles(rootDir) {
|
|
150
|
+
const files = [];
|
|
151
|
+
function walk(currentDir) {
|
|
152
|
+
for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
|
|
153
|
+
const fullPath = join(currentDir, entry.name);
|
|
154
|
+
if (entry.isDirectory()) {
|
|
155
|
+
walk(fullPath);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
files.push(fullPath);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
walk(rootDir);
|
|
162
|
+
return files.sort((left, right) => left.localeCompare(right));
|
|
163
|
+
}
|
|
164
|
+
function detectEntryScript(skillRoot, skillName, skillMarkdown) {
|
|
165
|
+
const scriptsDir = join(skillRoot, 'scripts');
|
|
166
|
+
if (!isDirectory(scriptsDir)) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const explicitMatches = [...skillMarkdown.matchAll(/scripts\/([A-Za-z0-9._-]+\.py)/g)]
|
|
170
|
+
.map((match) => match[1])
|
|
171
|
+
.filter((entry) => isFile(join(scriptsDir, entry)));
|
|
172
|
+
if (explicitMatches.length > 0) {
|
|
173
|
+
return join('scripts', explicitMatches[0]);
|
|
174
|
+
}
|
|
175
|
+
const scripts = readdirSync(scriptsDir, { withFileTypes: true })
|
|
176
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.py'))
|
|
177
|
+
.map((entry) => entry.name)
|
|
178
|
+
.sort((left, right) => left.localeCompare(right));
|
|
179
|
+
if (scripts.length === 1) {
|
|
180
|
+
return join('scripts', scripts[0]);
|
|
181
|
+
}
|
|
182
|
+
const normalizedName = skillName.replace(/-/g, '_');
|
|
183
|
+
const exactMatch = scripts.find((entry) => basename(entry, '.py') === normalizedName);
|
|
184
|
+
if (exactMatch) {
|
|
185
|
+
return join('scripts', exactMatch);
|
|
186
|
+
}
|
|
187
|
+
const nonHelperScripts = scripts.filter((entry) => !/^common[_-]/.test(entry) && !/^__/.test(entry));
|
|
188
|
+
if (nonHelperScripts.length === 1) {
|
|
189
|
+
return join('scripts', nonHelperScripts[0]);
|
|
190
|
+
}
|
|
191
|
+
return nonHelperScripts[0] ? join('scripts', nonHelperScripts[0]) : undefined;
|
|
192
|
+
}
|
|
193
|
+
function computeInstallHash(rootDir) {
|
|
194
|
+
const hash = createHash('sha256');
|
|
195
|
+
for (const filePath of walkFiles(rootDir)) {
|
|
196
|
+
hash.update(relative(rootDir, filePath).replace(/\\/g, '/'));
|
|
197
|
+
hash.update('\0');
|
|
198
|
+
hash.update(readFileSync(filePath));
|
|
199
|
+
hash.update('\0');
|
|
200
|
+
}
|
|
201
|
+
return hash.digest('hex');
|
|
202
|
+
}
|
|
203
|
+
function toCandidate(skillRoot) {
|
|
204
|
+
const issues = [];
|
|
205
|
+
const skillFile = join(skillRoot, 'SKILL.md');
|
|
206
|
+
const skillMarkdown = readTextIfExists(skillFile);
|
|
207
|
+
if (!skillMarkdown) {
|
|
208
|
+
return {
|
|
209
|
+
rootDir: skillRoot,
|
|
210
|
+
issues: [{ severity: 'error', message: 'Missing SKILL.md.' }],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const frontmatter = parseFrontmatter(skillMarkdown);
|
|
214
|
+
if (!frontmatter.name) {
|
|
215
|
+
issues.push({ severity: 'error', message: 'SKILL.md frontmatter is missing "name".' });
|
|
216
|
+
}
|
|
217
|
+
if (!frontmatter.description) {
|
|
218
|
+
issues.push({ severity: 'error', message: 'SKILL.md frontmatter is missing "description".' });
|
|
219
|
+
}
|
|
220
|
+
const files = walkFiles(skillRoot);
|
|
221
|
+
for (const filePath of files) {
|
|
222
|
+
const relativePath = relative(skillRoot, filePath).replace(/\\/g, '/');
|
|
223
|
+
if (relativePath === MANIFEST_FILENAME)
|
|
224
|
+
continue;
|
|
225
|
+
const extension = extname(filePath).toLowerCase();
|
|
226
|
+
if (!SUPPORTED_SKILL_FILE_EXTENSIONS.has(extension)) {
|
|
227
|
+
issues.push({
|
|
228
|
+
severity: 'error',
|
|
229
|
+
message: `Unsupported file type in skill bundle: ${relativePath}`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const openAIYaml = readTextIfExists(join(skillRoot, 'agents', 'openai.yaml'));
|
|
234
|
+
const displayName = parseOpenAIYamlDisplayName(openAIYaml) ?? (frontmatter.name ? titleCaseFromName(frontmatter.name) : undefined);
|
|
235
|
+
const runtime = isDirectory(join(skillRoot, 'scripts')) ? 'python-script' : 'prompt-only';
|
|
236
|
+
const entryScript = frontmatter.name ? detectEntryScript(skillRoot, frontmatter.name, skillMarkdown) : undefined;
|
|
237
|
+
if (runtime === 'python-script' && !entryScript) {
|
|
238
|
+
issues.push({
|
|
239
|
+
severity: 'error',
|
|
240
|
+
message: 'Python-script skill does not have a detectable entry script in scripts/.',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (!openAIYaml) {
|
|
244
|
+
issues.push({
|
|
245
|
+
severity: 'warning',
|
|
246
|
+
message: 'agents/openai.yaml is missing; display metadata will fall back to the skill name.',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
name: frontmatter.name,
|
|
251
|
+
displayName,
|
|
252
|
+
description: frontmatter.description,
|
|
253
|
+
runtime,
|
|
254
|
+
rootDir: skillRoot,
|
|
255
|
+
entryScript,
|
|
256
|
+
contentHash: computeInstallHash(skillRoot),
|
|
257
|
+
issues,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function isPathWithin(targetPath, parentPath) {
|
|
261
|
+
const resolvedTarget = resolve(targetPath);
|
|
262
|
+
const resolvedParent = resolve(parentPath);
|
|
263
|
+
return resolvedTarget === resolvedParent || resolvedTarget.startsWith(`${resolvedParent}\\`) || resolvedTarget.startsWith(`${resolvedParent}/`);
|
|
264
|
+
}
|
|
265
|
+
function isTrustedSkillSourcePath(sourcePath) {
|
|
266
|
+
const resolvedSource = resolve(sourcePath);
|
|
267
|
+
const trustedRoots = [
|
|
268
|
+
process.cwd(),
|
|
269
|
+
getWorkspaceDir(),
|
|
270
|
+
process.env.GEOTECHCLI_CONFIG_DIR ?? join(homedir(), '.geotechcli'),
|
|
271
|
+
].map((rootPath) => resolve(rootPath));
|
|
272
|
+
return trustedRoots.some((rootPath) => isPathWithin(resolvedSource, rootPath));
|
|
273
|
+
}
|
|
274
|
+
function buildInstalledSkill(candidate, sourceType, sourceLabel, trusted) {
|
|
275
|
+
const rootDir = resolve(candidate.rootDir);
|
|
276
|
+
const files = walkFiles(rootDir);
|
|
277
|
+
const discoveredCandidate = candidate;
|
|
278
|
+
return {
|
|
279
|
+
name: candidate.name,
|
|
280
|
+
displayName: candidate.displayName ?? titleCaseFromName(candidate.name),
|
|
281
|
+
description: candidate.description ?? '',
|
|
282
|
+
runtime: candidate.runtime ?? 'prompt-only',
|
|
283
|
+
installPath: rootDir,
|
|
284
|
+
skillFile: join(rootDir, 'SKILL.md'),
|
|
285
|
+
installedAt: nowIso(),
|
|
286
|
+
entryScript: candidate.entryScript ? join(rootDir, candidate.entryScript) : undefined,
|
|
287
|
+
sourceType,
|
|
288
|
+
sourceLabel,
|
|
289
|
+
installHash: discoveredCandidate.contentHash ?? computeInstallHash(rootDir),
|
|
290
|
+
scriptCount: files.filter((filePath) => relative(rootDir, filePath).replace(/\\/g, '/').startsWith('scripts/')).length,
|
|
291
|
+
referenceCount: files.filter((filePath) => relative(rootDir, filePath).replace(/\\/g, '/').startsWith('references/')).length,
|
|
292
|
+
assetCount: files.filter((filePath) => relative(rootDir, filePath).replace(/\\/g, '/').startsWith('assets/')).length,
|
|
293
|
+
hasOpenAIYaml: isFile(join(rootDir, 'agents', 'openai.yaml')),
|
|
294
|
+
trusted,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function manifestPathFor(dirPath) {
|
|
298
|
+
return join(dirPath, MANIFEST_FILENAME);
|
|
299
|
+
}
|
|
300
|
+
function writeManifest(skillDir, skill) {
|
|
301
|
+
const manifest = {
|
|
302
|
+
...skill,
|
|
303
|
+
installPath: skillDir,
|
|
304
|
+
skillFile: join(skillDir, 'SKILL.md'),
|
|
305
|
+
entryScript: skill.entryScript
|
|
306
|
+
? join(skillDir, relative(skill.installPath, skill.entryScript))
|
|
307
|
+
: undefined,
|
|
308
|
+
};
|
|
309
|
+
writeFileSync(manifestPathFor(skillDir), JSON.stringify(manifest, null, 2), 'utf-8');
|
|
310
|
+
}
|
|
311
|
+
function loadManifest(dirPath) {
|
|
312
|
+
const manifestPath = manifestPathFor(dirPath);
|
|
313
|
+
if (!isFile(manifestPath))
|
|
314
|
+
return null;
|
|
315
|
+
const parsed = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
316
|
+
if (!parsed.name || !parsed.description || !parsed.installPath || !parsed.skillFile || !parsed.runtime) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
name: parsed.name,
|
|
321
|
+
displayName: parsed.displayName ?? titleCaseFromName(parsed.name),
|
|
322
|
+
description: parsed.description,
|
|
323
|
+
runtime: parsed.runtime,
|
|
324
|
+
installPath: parsed.installPath,
|
|
325
|
+
skillFile: parsed.skillFile,
|
|
326
|
+
installedAt: parsed.installedAt ?? nowIso(),
|
|
327
|
+
entryScript: parsed.entryScript,
|
|
328
|
+
sourceType: parsed.sourceType ?? 'directory',
|
|
329
|
+
sourceLabel: parsed.sourceLabel ?? basename(dirPath),
|
|
330
|
+
installHash: parsed.installHash ?? computeInstallHash(dirPath),
|
|
331
|
+
scriptCount: parsed.scriptCount ?? 0,
|
|
332
|
+
referenceCount: parsed.referenceCount ?? 0,
|
|
333
|
+
assetCount: parsed.assetCount ?? 0,
|
|
334
|
+
hasOpenAIYaml: parsed.hasOpenAIYaml ?? false,
|
|
335
|
+
trusted: parsed.trusted ?? false,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function ensureDirectory(dirPath) {
|
|
339
|
+
if (!existsSync(dirPath)) {
|
|
340
|
+
mkdirSync(dirPath, { recursive: true });
|
|
341
|
+
}
|
|
342
|
+
return dirPath;
|
|
343
|
+
}
|
|
344
|
+
function createTempDir(prefix) {
|
|
345
|
+
const dirPath = join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
346
|
+
mkdirSync(dirPath, { recursive: true });
|
|
347
|
+
return dirPath;
|
|
348
|
+
}
|
|
349
|
+
function buildPythonEnv() {
|
|
350
|
+
const env = {
|
|
351
|
+
PYTHONIOENCODING: 'utf-8',
|
|
352
|
+
PYTHONUTF8: '1',
|
|
353
|
+
};
|
|
354
|
+
for (const key of SYSTEM_ENV_KEYS) {
|
|
355
|
+
if (process.env[key]) {
|
|
356
|
+
env[key] = process.env[key];
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return env;
|
|
360
|
+
}
|
|
361
|
+
function runPythonScript(scriptContent, args, cwd) {
|
|
362
|
+
const pythonPath = getSkillsRuntimeConfig().pythonPath;
|
|
363
|
+
const helperPath = join(createTempDir('geotechcli-skill-helper'), 'helper.py');
|
|
364
|
+
writeFileSync(helperPath, scriptContent, 'utf-8');
|
|
365
|
+
const result = spawnSync(pythonPath, [helperPath, ...args], {
|
|
366
|
+
cwd,
|
|
367
|
+
encoding: 'utf-8',
|
|
368
|
+
timeout: 30_000,
|
|
369
|
+
env: buildPythonEnv(),
|
|
370
|
+
});
|
|
371
|
+
rmSync(dirname(helperPath), { recursive: true, force: true });
|
|
372
|
+
if (result.error) {
|
|
373
|
+
throw result.error;
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
status: result.status,
|
|
377
|
+
stdout: result.stdout ?? '',
|
|
378
|
+
stderr: result.stderr ?? '',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function extractZipToTemp(sourcePath) {
|
|
382
|
+
const extractRoot = createTempDir('geotechcli-skill-import');
|
|
383
|
+
const helperScript = [
|
|
384
|
+
'import pathlib',
|
|
385
|
+
'import sys',
|
|
386
|
+
'import zipfile',
|
|
387
|
+
'',
|
|
388
|
+
'src = pathlib.Path(sys.argv[1])',
|
|
389
|
+
'dest = pathlib.Path(sys.argv[2])',
|
|
390
|
+
'with zipfile.ZipFile(src) as archive:',
|
|
391
|
+
' for info in archive.infolist():',
|
|
392
|
+
" parts = pathlib.PurePosixPath(info.filename).parts",
|
|
393
|
+
" if not parts:",
|
|
394
|
+
' continue',
|
|
395
|
+
" if any(part in ('..', '') for part in parts):",
|
|
396
|
+
" raise SystemExit(f'unsafe zip entry: {info.filename}')",
|
|
397
|
+
" if pathlib.PurePosixPath(info.filename).is_absolute():",
|
|
398
|
+
" raise SystemExit(f'unsafe zip entry: {info.filename}')",
|
|
399
|
+
' target = dest.joinpath(*parts)',
|
|
400
|
+
" if info.is_dir():",
|
|
401
|
+
' target.mkdir(parents=True, exist_ok=True)',
|
|
402
|
+
' continue',
|
|
403
|
+
' target.parent.mkdir(parents=True, exist_ok=True)',
|
|
404
|
+
' with archive.open(info) as source_handle, open(target, "wb") as target_handle:',
|
|
405
|
+
' target_handle.write(source_handle.read())',
|
|
406
|
+
].join('\n');
|
|
407
|
+
const extractResult = runPythonScript(helperScript, [sourcePath, extractRoot]);
|
|
408
|
+
if (extractResult.status !== 0) {
|
|
409
|
+
rmSync(extractRoot, { recursive: true, force: true });
|
|
410
|
+
throw new Error(extractResult.stderr.trim() || extractResult.stdout.trim() || 'Failed to extract skill archive.');
|
|
411
|
+
}
|
|
412
|
+
return extractRoot;
|
|
413
|
+
}
|
|
414
|
+
function withPreparedSource(sourcePath, callback) {
|
|
415
|
+
const resolvedPath = resolve(sourcePath);
|
|
416
|
+
if (isDirectory(resolvedPath)) {
|
|
417
|
+
return callback(resolvedPath, 'directory');
|
|
418
|
+
}
|
|
419
|
+
if (isFile(resolvedPath) && extname(resolvedPath).toLowerCase() === '.zip') {
|
|
420
|
+
const extractedRoot = extractZipToTemp(resolvedPath);
|
|
421
|
+
try {
|
|
422
|
+
return callback(extractedRoot, 'zip');
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
rmSync(extractedRoot, { recursive: true, force: true });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
throw new Error(`Unsupported skill source: ${sourcePath}. Expected a directory or .zip file.`);
|
|
429
|
+
}
|
|
430
|
+
function discoverPreparedSkillCandidates(preparedRoot) {
|
|
431
|
+
const cleanupPaths = [];
|
|
432
|
+
const seenRoots = new Set();
|
|
433
|
+
const candidates = [];
|
|
434
|
+
const addSkillRootsFrom = (rootDir) => {
|
|
435
|
+
for (const skillRoot of discoverSkillRoots(rootDir)) {
|
|
436
|
+
const resolvedRoot = resolve(skillRoot);
|
|
437
|
+
if (seenRoots.has(resolvedRoot))
|
|
438
|
+
continue;
|
|
439
|
+
seenRoots.add(resolvedRoot);
|
|
440
|
+
candidates.push(toCandidate(skillRoot));
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
addSkillRootsFrom(preparedRoot);
|
|
444
|
+
const nestedZipPaths = walkFiles(preparedRoot)
|
|
445
|
+
.filter((filePath) => extname(filePath).toLowerCase() === '.zip')
|
|
446
|
+
.sort((left, right) => left.localeCompare(right));
|
|
447
|
+
for (const zipPath of nestedZipPaths) {
|
|
448
|
+
const nestedPreparedRoot = extractZipToTemp(zipPath);
|
|
449
|
+
cleanupPaths.push(nestedPreparedRoot);
|
|
450
|
+
addSkillRootsFrom(nestedPreparedRoot);
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
candidates,
|
|
454
|
+
cleanupPaths,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function dedupeEquivalentCandidates(candidates) {
|
|
458
|
+
const deduped = new Map();
|
|
459
|
+
for (const candidate of candidates) {
|
|
460
|
+
const key = candidate.name && candidate.contentHash
|
|
461
|
+
? `${candidate.name}::${candidate.contentHash}`
|
|
462
|
+
: resolve(candidate.rootDir);
|
|
463
|
+
if (!deduped.has(key)) {
|
|
464
|
+
deduped.set(key, {
|
|
465
|
+
...candidate,
|
|
466
|
+
issues: [...candidate.issues],
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return [...deduped.values()]
|
|
471
|
+
.sort((left, right) => {
|
|
472
|
+
const leftKey = left.name ?? left.rootDir;
|
|
473
|
+
const rightKey = right.name ?? right.rootDir;
|
|
474
|
+
return leftKey.localeCompare(rightKey);
|
|
475
|
+
})
|
|
476
|
+
.map(({ contentHash, ...candidate }) => candidate);
|
|
477
|
+
}
|
|
478
|
+
function buildValidationResult(sourcePath, candidates) {
|
|
479
|
+
const issues = [];
|
|
480
|
+
if (candidates.length === 0) {
|
|
481
|
+
issues.push({ severity: 'error', message: 'No SKILL.md files were found in the provided source.' });
|
|
482
|
+
}
|
|
483
|
+
const duplicateNames = new Map();
|
|
484
|
+
for (const candidate of candidates) {
|
|
485
|
+
if (!candidate.name)
|
|
486
|
+
continue;
|
|
487
|
+
duplicateNames.set(candidate.name, (duplicateNames.get(candidate.name) ?? 0) + 1);
|
|
488
|
+
}
|
|
489
|
+
for (const candidate of candidates) {
|
|
490
|
+
if (candidate.name && (duplicateNames.get(candidate.name) ?? 0) > 1) {
|
|
491
|
+
candidate.issues.push({
|
|
492
|
+
severity: 'error',
|
|
493
|
+
message: `Duplicate skill name "${candidate.name}" detected in the same source.`,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
issues.push(...candidate.issues);
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
sourcePath: resolve(sourcePath),
|
|
500
|
+
valid: issues.every((issue) => issue.severity !== 'error'),
|
|
501
|
+
candidates,
|
|
502
|
+
issues,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function withPreparedSkillCandidates(sourcePath, callback) {
|
|
506
|
+
return withPreparedSource(sourcePath, (preparedRoot, sourceType) => {
|
|
507
|
+
const { candidates, cleanupPaths } = discoverPreparedSkillCandidates(preparedRoot);
|
|
508
|
+
const dedupedCandidates = dedupeEquivalentCandidates(candidates);
|
|
509
|
+
try {
|
|
510
|
+
return callback({
|
|
511
|
+
resolvedSourcePath: resolve(sourcePath),
|
|
512
|
+
sourceType,
|
|
513
|
+
candidates: dedupedCandidates,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
finally {
|
|
517
|
+
for (const cleanupPath of cleanupPaths) {
|
|
518
|
+
rmSync(cleanupPath, { recursive: true, force: true });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
export function getSkillsRuntimeConfig() {
|
|
524
|
+
const config = loadConfig();
|
|
525
|
+
const configuredDirectory = config.skills.directory?.trim();
|
|
526
|
+
return {
|
|
527
|
+
enabled: process.env.GEOTECHCLI_ENABLE_SKILLS === '1' || config.skills.enabled,
|
|
528
|
+
directory: process.env.GEOTECHCLI_SKILLS_DIR?.trim() || configuredDirectory || getSkillsDirFallback(),
|
|
529
|
+
pythonPath: process.env.GEOTECHCLI_SKILLS_PYTHON?.trim() || config.skills.python_path || 'python',
|
|
530
|
+
trustedOnly: process.env.GEOTECHCLI_SKILLS_TRUSTED_ONLY === '0'
|
|
531
|
+
? false
|
|
532
|
+
: config.skills.trusted_only,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
export function areAgentSkillToolsEnabled() {
|
|
536
|
+
return getSkillsRuntimeConfig().enabled;
|
|
537
|
+
}
|
|
538
|
+
export function isAgentSkillToolName(toolName) {
|
|
539
|
+
return AGENT_SKILL_TOOL_NAMES.includes(toolName);
|
|
540
|
+
}
|
|
541
|
+
export function getSkillsDirectory() {
|
|
542
|
+
return ensureDirectory(resolve(getSkillsRuntimeConfig().directory));
|
|
543
|
+
}
|
|
544
|
+
export function listInstalledSkills() {
|
|
545
|
+
const skillsDir = getSkillsDirectory();
|
|
546
|
+
return readdirSync(skillsDir, { withFileTypes: true })
|
|
547
|
+
.filter((entry) => entry.isDirectory())
|
|
548
|
+
.map((entry) => loadManifest(join(skillsDir, entry.name)))
|
|
549
|
+
.filter((skill) => skill !== null)
|
|
550
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
551
|
+
}
|
|
552
|
+
export function getInstalledSkill(name) {
|
|
553
|
+
const skill = listInstalledSkills().find((entry) => entry.name === name);
|
|
554
|
+
if (!skill) {
|
|
555
|
+
throw new Error(`Skill "${name}" is not installed.`);
|
|
556
|
+
}
|
|
557
|
+
return skill;
|
|
558
|
+
}
|
|
559
|
+
export function readInstalledSkillGuide(name) {
|
|
560
|
+
const skill = getInstalledSkill(name);
|
|
561
|
+
return readFileSync(skill.skillFile, 'utf-8');
|
|
562
|
+
}
|
|
563
|
+
export function validateSkillSource(sourcePath) {
|
|
564
|
+
return withPreparedSkillCandidates(sourcePath, ({ resolvedSourcePath, candidates }) => buildValidationResult(resolvedSourcePath, candidates));
|
|
565
|
+
}
|
|
566
|
+
export function validateInstalledSkill(name) {
|
|
567
|
+
const skill = getInstalledSkill(name);
|
|
568
|
+
const candidate = toCandidate(skill.installPath);
|
|
569
|
+
return {
|
|
570
|
+
sourcePath: skill.installPath,
|
|
571
|
+
valid: candidate.issues.every((issue) => issue.severity !== 'error'),
|
|
572
|
+
candidates: [candidate],
|
|
573
|
+
issues: [...candidate.issues],
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
export function importSkillsFromSource(sourcePath, options) {
|
|
577
|
+
return withPreparedSkillCandidates(sourcePath, ({ resolvedSourcePath, sourceType, candidates, }) => {
|
|
578
|
+
const validation = buildValidationResult(resolvedSourcePath, candidates);
|
|
579
|
+
if (!validation.valid) {
|
|
580
|
+
const errors = validation.issues.filter((issue) => issue.severity === 'error');
|
|
581
|
+
throw new Error(errors.map((issue) => issue.message).join(' '));
|
|
582
|
+
}
|
|
583
|
+
const trusted = isTrustedSkillSourcePath(sourcePath);
|
|
584
|
+
if (getSkillsRuntimeConfig().trustedOnly && !trusted) {
|
|
585
|
+
throw new Error(`Skill import blocked: "${resolvedSourcePath}" is outside trusted strong-beta skill locations.`);
|
|
586
|
+
}
|
|
587
|
+
const sourceLabel = basename(resolvedSourcePath);
|
|
588
|
+
const imported = [];
|
|
589
|
+
const replaced = [];
|
|
590
|
+
for (const candidate of candidates) {
|
|
591
|
+
if (!candidate.name)
|
|
592
|
+
continue;
|
|
593
|
+
const installDir = join(getSkillsDirectory(), candidate.name);
|
|
594
|
+
if (existsSync(installDir)) {
|
|
595
|
+
if (!options?.force) {
|
|
596
|
+
throw new Error(`Skill "${candidate.name}" is already installed. Re-run with --force to replace it.`);
|
|
597
|
+
}
|
|
598
|
+
rmSync(installDir, { recursive: true, force: true });
|
|
599
|
+
replaced.push(candidate.name);
|
|
600
|
+
}
|
|
601
|
+
cpSync(candidate.rootDir, installDir, { recursive: true });
|
|
602
|
+
const installedSkill = buildInstalledSkill({
|
|
603
|
+
...candidate,
|
|
604
|
+
rootDir: installDir,
|
|
605
|
+
}, sourceType, sourceLabel, trusted);
|
|
606
|
+
writeManifest(installDir, installedSkill);
|
|
607
|
+
imported.push(installedSkill);
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
sourcePath: resolvedSourcePath,
|
|
611
|
+
imported,
|
|
612
|
+
replaced,
|
|
613
|
+
};
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
function parseJsonFile(path) {
|
|
617
|
+
if (!isFile(path))
|
|
618
|
+
return undefined;
|
|
619
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
620
|
+
}
|
|
621
|
+
function getSkillExecutionProfile(name) {
|
|
622
|
+
return SKILL_EXECUTION_PROFILES[name] ?? DEFAULT_SKILL_EXECUTION_PROFILE;
|
|
623
|
+
}
|
|
624
|
+
function asNonEmptyString(value) {
|
|
625
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
626
|
+
}
|
|
627
|
+
function asStringArray(value) {
|
|
628
|
+
if (!Array.isArray(value))
|
|
629
|
+
return [];
|
|
630
|
+
return value
|
|
631
|
+
.filter((item) => typeof item === 'string' && item.trim().length > 0)
|
|
632
|
+
.map((item) => item.trim());
|
|
633
|
+
}
|
|
634
|
+
function asFiniteNumber(value) {
|
|
635
|
+
if (typeof value === 'number') {
|
|
636
|
+
return Number.isFinite(value) ? value : undefined;
|
|
637
|
+
}
|
|
638
|
+
if (typeof value === 'string' && value.trim()) {
|
|
639
|
+
const parsed = Number(value);
|
|
640
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
641
|
+
}
|
|
642
|
+
return undefined;
|
|
643
|
+
}
|
|
644
|
+
function normalizeCriticalRisks(value) {
|
|
645
|
+
return asStringArray(value).filter((item) => {
|
|
646
|
+
const token = item.trim().toLowerCase();
|
|
647
|
+
return token !== 'none' && token !== 'no critical risks' && token !== 'no critical risk';
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
function getLegacyAssessmentScope(payload) {
|
|
651
|
+
if (Array.isArray(payload.zone_results)) {
|
|
652
|
+
return { count: payload.zone_results.length, noun: 'zone' };
|
|
653
|
+
}
|
|
654
|
+
const totalRings = asFiniteNumber(payload.total_rings);
|
|
655
|
+
if (totalRings != null) {
|
|
656
|
+
return { count: totalRings, noun: 'ring' };
|
|
657
|
+
}
|
|
658
|
+
return { count: 0, noun: 'item' };
|
|
659
|
+
}
|
|
660
|
+
function pluralize(count, noun) {
|
|
661
|
+
return `${count} ${noun}${count === 1 ? '' : 's'}`;
|
|
662
|
+
}
|
|
663
|
+
function buildLegacyProjectSummary(skill, payload, overallRecommendation, assessmentCount, assessmentNoun, criticalRiskCount, missingInputCount) {
|
|
664
|
+
const targetRings = asFiniteNumber(payload.target_rings);
|
|
665
|
+
const avgCycle = asFiniteNumber(payload.avg_cycle_min);
|
|
666
|
+
const metRingTarget = payload.met_ring_target === true
|
|
667
|
+
? 'met'
|
|
668
|
+
: payload.met_ring_target === false
|
|
669
|
+
? 'missed'
|
|
670
|
+
: undefined;
|
|
671
|
+
const lead = overallRecommendation
|
|
672
|
+
? `${skill.displayName} overall recommendation: ${overallRecommendation}.`
|
|
673
|
+
: assessmentNoun === 'ring'
|
|
674
|
+
? `${skill.displayName} reviewed ${pluralize(assessmentCount, 'ring')}${targetRings != null ? ` against a target of ${targetRings}` : ''}${avgCycle != null ? ` with an average cycle of ${avgCycle.toFixed(1)} minutes` : ''}${metRingTarget ? ` and ${metRingTarget} the ring target` : ''}.`
|
|
675
|
+
: `${skill.displayName} evaluated ${pluralize(assessmentCount, assessmentNoun)}.`;
|
|
676
|
+
const risk = criticalRiskCount > 0
|
|
677
|
+
? ` Flagged ${pluralize(criticalRiskCount, 'critical risk')}.`
|
|
678
|
+
: ' No critical risks were flagged.';
|
|
679
|
+
const missing = missingInputCount > 0
|
|
680
|
+
? ` ${pluralize(missingInputCount, 'missing input')} still need confirmation.`
|
|
681
|
+
: '';
|
|
682
|
+
return `${lead}${risk}${missing}`.trim();
|
|
683
|
+
}
|
|
684
|
+
function buildLegacyRunSummary(overallRecommendation, criticalRiskCount, missingInputCount, assessmentCount, assessmentNoun) {
|
|
685
|
+
const parts = [];
|
|
686
|
+
if (overallRecommendation) {
|
|
687
|
+
parts.push(`overall recommendation ${overallRecommendation}`);
|
|
688
|
+
}
|
|
689
|
+
parts.push(`${pluralize(assessmentCount, assessmentNoun)} assessed`);
|
|
690
|
+
if (criticalRiskCount > 0) {
|
|
691
|
+
parts.push(`${pluralize(criticalRiskCount, 'critical risk')} flagged`);
|
|
692
|
+
}
|
|
693
|
+
if (missingInputCount > 0) {
|
|
694
|
+
parts.push(`${pluralize(missingInputCount, 'missing input')} remain`);
|
|
695
|
+
}
|
|
696
|
+
return parts.join('; ') + '.';
|
|
697
|
+
}
|
|
698
|
+
function normalizeLegacySkillHandoff(skill, payload) {
|
|
699
|
+
const criticalRisks = normalizeCriticalRisks(payload.critical_risks);
|
|
700
|
+
const missingInputs = asStringArray(payload.missing_inputs);
|
|
701
|
+
const assessmentScope = getLegacyAssessmentScope(payload);
|
|
702
|
+
const overallRecommendation = asNonEmptyString(payload.overall_recommendation) ?? asNonEmptyString(payload.recommendation);
|
|
703
|
+
const projectSummary = asNonEmptyString(payload.project_summary) ??
|
|
704
|
+
buildLegacyProjectSummary(skill, payload, overallRecommendation, assessmentScope.count, assessmentScope.noun, criticalRisks.length, missingInputs.length);
|
|
705
|
+
const summary = asNonEmptyString(payload.summary) ??
|
|
706
|
+
buildLegacyRunSummary(overallRecommendation, criticalRisks.length, missingInputs.length, assessmentScope.count, assessmentScope.noun);
|
|
707
|
+
return {
|
|
708
|
+
...payload,
|
|
709
|
+
skill: payload.skill ?? skill.name,
|
|
710
|
+
project_summary: projectSummary,
|
|
711
|
+
summary,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function buildLegacyArtifactMap(skill, profile, swarmHandoff) {
|
|
715
|
+
const missingInputs = asStringArray(swarmHandoff.missing_inputs);
|
|
716
|
+
const recommendedNextTools = asStringArray(swarmHandoff.recommended_next_tools);
|
|
717
|
+
const criticalRisks = normalizeCriticalRisks(swarmHandoff.critical_risks);
|
|
718
|
+
return {
|
|
719
|
+
skill: skill.name,
|
|
720
|
+
execution_contract: profile.contract,
|
|
721
|
+
named_dataset_keys: [
|
|
722
|
+
`skill:${skill.name}:swarm_handoff`,
|
|
723
|
+
`skill:${skill.name}:artifact_map`,
|
|
724
|
+
],
|
|
725
|
+
derived_parameters: [],
|
|
726
|
+
assumption_records: missingInputs.map((item) => ({
|
|
727
|
+
type: 'missing_input',
|
|
728
|
+
detail: item,
|
|
729
|
+
})),
|
|
730
|
+
candidate_case_file_artifact_types: profile.candidateArtifactTypes,
|
|
731
|
+
recommended_next_tools: recommendedNextTools,
|
|
732
|
+
critical_risks: criticalRisks,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
function runLegacyFileInputSkill(skill, inputDir, outputDir, profile, swarmHandoffPath, engineeringReportPath, caseFileArtifactMapPath) {
|
|
736
|
+
const args = [skill.entryScript];
|
|
737
|
+
for (const binding of profile.inputBindings) {
|
|
738
|
+
const inputPath = join(inputDir, binding.fileName);
|
|
739
|
+
if (!isFile(inputPath)) {
|
|
740
|
+
throw new Error(`Skill "${skill.name}" requires ${binding.fileName} in the input directory for ${binding.flag}.`);
|
|
741
|
+
}
|
|
742
|
+
args.push(binding.flag, inputPath);
|
|
743
|
+
}
|
|
744
|
+
args.push('--output-json', swarmHandoffPath, '--output-markdown', engineeringReportPath);
|
|
745
|
+
const result = spawnSync(getSkillsRuntimeConfig().pythonPath, args, {
|
|
746
|
+
cwd: skill.installPath,
|
|
747
|
+
encoding: 'utf-8',
|
|
748
|
+
timeout: 90_000,
|
|
749
|
+
env: buildPythonEnv(),
|
|
750
|
+
});
|
|
751
|
+
if (result.error) {
|
|
752
|
+
throw result.error;
|
|
753
|
+
}
|
|
754
|
+
if (result.status === 0) {
|
|
755
|
+
const rawHandoff = parseJsonFile(swarmHandoffPath);
|
|
756
|
+
if (rawHandoff) {
|
|
757
|
+
const normalized = normalizeLegacySkillHandoff(skill, rawHandoff);
|
|
758
|
+
writeFileSync(swarmHandoffPath, JSON.stringify(normalized, null, 2) + '\n');
|
|
759
|
+
writeFileSync(caseFileArtifactMapPath, JSON.stringify(buildLegacyArtifactMap(skill, profile, normalized), null, 2) + '\n');
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return result;
|
|
763
|
+
}
|
|
764
|
+
function buildSkillRunSummary(skill, success, swarmHandoff) {
|
|
765
|
+
if (!success) {
|
|
766
|
+
return `${skill.name} failed to complete.`;
|
|
767
|
+
}
|
|
768
|
+
const summary = typeof swarmHandoff?.summary === 'string' ? swarmHandoff.summary : undefined;
|
|
769
|
+
return summary ? `${skill.displayName}: ${summary}` : `${skill.displayName} completed successfully.`;
|
|
770
|
+
}
|
|
771
|
+
function persistSkillRun(projectId, run) {
|
|
772
|
+
if (run.swarmHandoff) {
|
|
773
|
+
saveNamedDataset(projectId, {
|
|
774
|
+
name: `skill:${run.skill.name}:swarm_handoff`,
|
|
775
|
+
kind: 'skill-swarm-handoff',
|
|
776
|
+
data: run.swarmHandoff,
|
|
777
|
+
source: run.skill.name,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
if (run.caseFileArtifactMap) {
|
|
781
|
+
saveNamedDataset(projectId, {
|
|
782
|
+
name: `skill:${run.skill.name}:artifact_map`,
|
|
783
|
+
kind: 'skill-artifact-map',
|
|
784
|
+
data: run.caseFileArtifactMap,
|
|
785
|
+
source: run.skill.name,
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
if (run.engineeringReportPath && run.engineeringReport) {
|
|
789
|
+
addArtifact(projectId, {
|
|
790
|
+
kind: 'skill-report',
|
|
791
|
+
title: `${run.skill.displayName} report`,
|
|
792
|
+
path: run.engineeringReportPath,
|
|
793
|
+
content: run.engineeringReport,
|
|
794
|
+
mimeType: 'text/markdown',
|
|
795
|
+
metadata: {
|
|
796
|
+
skill: run.skill.name,
|
|
797
|
+
runDir: run.runDir,
|
|
798
|
+
outputDir: run.outputDir,
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
addNote(projectId, `Ran skill "${run.skill.name}" (${run.success ? 'success' : 'failed'}).`);
|
|
803
|
+
}
|
|
804
|
+
export function runInstalledSkill(name, options) {
|
|
805
|
+
const skill = getInstalledSkill(name);
|
|
806
|
+
if (skill.runtime !== 'python-script' || !skill.entryScript) {
|
|
807
|
+
throw new Error(`Skill "${name}" is not an executable python-script skill.`);
|
|
808
|
+
}
|
|
809
|
+
if (getSkillsRuntimeConfig().trustedOnly && !skill.trusted) {
|
|
810
|
+
throw new Error(`Skill "${name}" is blocked because it is not trusted for strong-beta execution.`);
|
|
811
|
+
}
|
|
812
|
+
const inputDirCheck = validateReadPath(options.inputDir, [skill.installPath, getSkillsDirectory()]);
|
|
813
|
+
if (!inputDirCheck.safe) {
|
|
814
|
+
throw new Error(inputDirCheck.error ?? `Input directory not allowed: ${options.inputDir}`);
|
|
815
|
+
}
|
|
816
|
+
const inputDir = inputDirCheck.resolved;
|
|
817
|
+
if (!isDirectory(inputDir)) {
|
|
818
|
+
throw new Error(`Input directory not found: ${inputDir}`);
|
|
819
|
+
}
|
|
820
|
+
const runDir = ensureDirectory(join(ensureWorkspace(), 'skills', 'runs', `${Date.now()}-${randomUUID().slice(0, 8)}-${skill.name}`));
|
|
821
|
+
let outputDir = join(runDir, 'output');
|
|
822
|
+
if (options.outputDir) {
|
|
823
|
+
const outputDirCheck = validateWritePath(options.outputDir, [getWorkspaceDir()]);
|
|
824
|
+
if (!outputDirCheck.safe) {
|
|
825
|
+
throw new Error(outputDirCheck.error ?? `Output directory not allowed: ${options.outputDir}`);
|
|
826
|
+
}
|
|
827
|
+
outputDir = outputDirCheck.resolved;
|
|
828
|
+
}
|
|
829
|
+
outputDir = ensureDirectory(outputDir);
|
|
830
|
+
const executionProfile = getSkillExecutionProfile(skill.name);
|
|
831
|
+
const swarmHandoffPath = join(outputDir, 'swarm_handoff.json');
|
|
832
|
+
const engineeringReportPath = join(outputDir, 'engineering_report.md');
|
|
833
|
+
const caseFileArtifactMapPath = join(outputDir, 'case_file_artifact_map.json');
|
|
834
|
+
const result = executionProfile.contract === 'legacy-file-inputs'
|
|
835
|
+
? runLegacyFileInputSkill(skill, inputDir, outputDir, executionProfile, swarmHandoffPath, engineeringReportPath, caseFileArtifactMapPath)
|
|
836
|
+
: spawnSync(getSkillsRuntimeConfig().pythonPath, [skill.entryScript, '--input-dir', inputDir, '--output-dir', outputDir], {
|
|
837
|
+
cwd: skill.installPath,
|
|
838
|
+
encoding: 'utf-8',
|
|
839
|
+
timeout: 90_000,
|
|
840
|
+
env: buildPythonEnv(),
|
|
841
|
+
});
|
|
842
|
+
if (result.error) {
|
|
843
|
+
throw result.error;
|
|
844
|
+
}
|
|
845
|
+
const run = {
|
|
846
|
+
skill,
|
|
847
|
+
success: result.status === 0,
|
|
848
|
+
exitCode: result.status,
|
|
849
|
+
runDir,
|
|
850
|
+
outputDir,
|
|
851
|
+
stdout: result.stdout ?? '',
|
|
852
|
+
stderr: result.stderr ?? '',
|
|
853
|
+
swarmHandoffPath: isFile(swarmHandoffPath) ? swarmHandoffPath : undefined,
|
|
854
|
+
engineeringReportPath: isFile(engineeringReportPath) ? engineeringReportPath : undefined,
|
|
855
|
+
caseFileArtifactMapPath: isFile(caseFileArtifactMapPath) ? caseFileArtifactMapPath : undefined,
|
|
856
|
+
swarmHandoff: parseJsonFile(swarmHandoffPath),
|
|
857
|
+
engineeringReport: readTextIfExists(engineeringReportPath) ?? undefined,
|
|
858
|
+
caseFileArtifactMap: parseJsonFile(caseFileArtifactMapPath),
|
|
859
|
+
persistedToProject: false,
|
|
860
|
+
summary: '',
|
|
861
|
+
};
|
|
862
|
+
run.summary = buildSkillRunSummary(skill, run.success, run.swarmHandoff);
|
|
863
|
+
if (options.projectId) {
|
|
864
|
+
persistSkillRun(options.projectId, run);
|
|
865
|
+
run.persistedToProject = true;
|
|
866
|
+
}
|
|
867
|
+
return run;
|
|
868
|
+
}
|
|
869
|
+
//# sourceMappingURL=index.js.map
|