@aragon-agent/cli 0.4.3
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/CHANGELOG.md +292 -0
- package/README.md +558 -0
- package/dist/agent/coalesce.d.ts +14 -0
- package/dist/agent/coalesce.js +41 -0
- package/dist/agent/controller.d.ts +132 -0
- package/dist/agent/controller.js +332 -0
- package/dist/agent/headless.d.ts +22 -0
- package/dist/agent/headless.js +74 -0
- package/dist/agent/reducer.d.ts +181 -0
- package/dist/agent/reducer.js +420 -0
- package/dist/agent/system-prompt.d.ts +30 -0
- package/dist/agent/system-prompt.js +58 -0
- package/dist/agent/usage.d.ts +13 -0
- package/dist/agent/usage.js +41 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +482 -0
- package/dist/commands/builtins.d.ts +16 -0
- package/dist/commands/builtins.js +223 -0
- package/dist/commands/registry.d.ts +48 -0
- package/dist/commands/registry.js +45 -0
- package/dist/commands/skills.d.ts +22 -0
- package/dist/commands/skills.js +625 -0
- package/dist/config/app-paths.d.ts +11 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/env.d.ts +28 -0
- package/dist/config/env.js +92 -0
- package/dist/config/load.d.ts +48 -0
- package/dist/config/load.js +158 -0
- package/dist/config/migrate-legacy-state.d.ts +78 -0
- package/dist/config/migrate-legacy-state.js +181 -0
- package/dist/config/schema.d.ts +249 -0
- package/dist/config/schema.js +247 -0
- package/dist/config/store.d.ts +44 -0
- package/dist/config/store.js +126 -0
- package/dist/input/keymap.d.ts +20 -0
- package/dist/input/keymap.js +85 -0
- package/dist/session/persist.d.ts +24 -0
- package/dist/session/persist.js +51 -0
- package/dist/skills/archive.d.ts +41 -0
- package/dist/skills/archive.js +152 -0
- package/dist/skills/cli-commands.d.ts +52 -0
- package/dist/skills/cli-commands.js +502 -0
- package/dist/skills/fetch-source.d.ts +104 -0
- package/dist/skills/fetch-source.js +448 -0
- package/dist/skills/installer.d.ts +117 -0
- package/dist/skills/installer.js +414 -0
- package/dist/skills/lock.d.ts +75 -0
- package/dist/skills/lock.js +223 -0
- package/dist/skills/manifest.d.ts +47 -0
- package/dist/skills/manifest.js +95 -0
- package/dist/skills/node-host.d.ts +23 -0
- package/dist/skills/node-host.js +59 -0
- package/dist/skills/paths.d.ts +61 -0
- package/dist/skills/paths.js +107 -0
- package/dist/skills/service.d.ts +256 -0
- package/dist/skills/service.js +730 -0
- package/dist/skills/tools.d.ts +35 -0
- package/dist/skills/tools.js +164 -0
- package/dist/skills/updater.d.ts +101 -0
- package/dist/skills/updater.js +330 -0
- package/dist/skills/usage.d.ts +72 -0
- package/dist/skills/usage.js +216 -0
- package/dist/tools/bash-tool.d.ts +12 -0
- package/dist/tools/bash-tool.js +131 -0
- package/dist/tools/diff.d.ts +15 -0
- package/dist/tools/diff.js +50 -0
- package/dist/tools/fs-tools.d.ts +16 -0
- package/dist/tools/fs-tools.js +215 -0
- package/dist/tools/index.d.ts +93 -0
- package/dist/tools/index.js +157 -0
- package/dist/tools/search-tools.d.ts +11 -0
- package/dist/tools/search-tools.js +225 -0
- package/dist/ui/App.d.ts +24 -0
- package/dist/ui/App.js +522 -0
- package/dist/ui/AutocompletePopup.d.ts +21 -0
- package/dist/ui/AutocompletePopup.js +19 -0
- package/dist/ui/Composer.d.ts +40 -0
- package/dist/ui/Composer.js +61 -0
- package/dist/ui/Header.d.ts +30 -0
- package/dist/ui/Header.js +25 -0
- package/dist/ui/Logo.d.ts +50 -0
- package/dist/ui/Logo.js +85 -0
- package/dist/ui/Markdown.d.ts +23 -0
- package/dist/ui/Markdown.js +122 -0
- package/dist/ui/PromptInput.d.ts +81 -0
- package/dist/ui/PromptInput.js +383 -0
- package/dist/ui/SessionOpener.d.ts +30 -0
- package/dist/ui/SessionOpener.js +12 -0
- package/dist/ui/StatusBar.d.ts +39 -0
- package/dist/ui/StatusBar.js +43 -0
- package/dist/ui/ToastStack.d.ts +24 -0
- package/dist/ui/ToastStack.js +38 -0
- package/dist/ui/Transcript.d.ts +62 -0
- package/dist/ui/Transcript.js +128 -0
- package/dist/ui/capabilities.d.ts +25 -0
- package/dist/ui/capabilities.js +59 -0
- package/dist/ui/console-bridge.d.ts +19 -0
- package/dist/ui/console-bridge.js +52 -0
- package/dist/ui/density.d.ts +26 -0
- package/dist/ui/density.js +34 -0
- package/dist/ui/entries/AssistantEntry.d.ts +23 -0
- package/dist/ui/entries/AssistantEntry.js +13 -0
- package/dist/ui/entries/EntryFrame.d.ts +23 -0
- package/dist/ui/entries/EntryFrame.js +6 -0
- package/dist/ui/entries/ToolCard.d.ts +38 -0
- package/dist/ui/entries/ToolCard.js +72 -0
- package/dist/ui/entries/ToolPreview.d.ts +57 -0
- package/dist/ui/entries/ToolPreview.js +144 -0
- package/dist/ui/entries/UserEntry.d.ts +14 -0
- package/dist/ui/entries/UserEntry.js +5 -0
- package/dist/ui/exit-snapshot.d.ts +27 -0
- package/dist/ui/exit-snapshot.js +26 -0
- package/dist/ui/gauge.d.ts +24 -0
- package/dist/ui/gauge.js +36 -0
- package/dist/ui/glyphs.d.ts +87 -0
- package/dist/ui/glyphs.js +127 -0
- package/dist/ui/gradient.d.ts +11 -0
- package/dist/ui/gradient.js +50 -0
- package/dist/ui/layout/AppShell.d.ts +24 -0
- package/dist/ui/layout/AppShell.js +9 -0
- package/dist/ui/layout/Gutter.d.ts +67 -0
- package/dist/ui/layout/Gutter.js +42 -0
- package/dist/ui/layout/OverlayFrame.d.ts +62 -0
- package/dist/ui/layout/OverlayFrame.js +92 -0
- package/dist/ui/layout/ScrollViewport.d.ts +33 -0
- package/dist/ui/layout/ScrollViewport.js +71 -0
- package/dist/ui/layout/budget.d.ts +42 -0
- package/dist/ui/layout/budget.js +47 -0
- package/dist/ui/layout/frame.d.ts +58 -0
- package/dist/ui/layout/frame.js +75 -0
- package/dist/ui/layout/overlay-window.d.ts +43 -0
- package/dist/ui/layout/overlay-window.js +51 -0
- package/dist/ui/layout/scroll.d.ts +28 -0
- package/dist/ui/layout/scroll.js +76 -0
- package/dist/ui/layout/useTerminalSize.d.ts +13 -0
- package/dist/ui/layout/useTerminalSize.js +50 -0
- package/dist/ui/markdown-blocks.d.ts +62 -0
- package/dist/ui/markdown-blocks.js +120 -0
- package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
- package/dist/ui/overlays/ConfirmDialog.js +25 -0
- package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
- package/dist/ui/overlays/HelpOverlay.js +60 -0
- package/dist/ui/overlays/ModelPicker.d.ts +29 -0
- package/dist/ui/overlays/ModelPicker.js +29 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
- package/dist/ui/overlays/SettingsScreen.js +106 -0
- package/dist/ui/palettes.d.ts +79 -0
- package/dist/ui/palettes.js +115 -0
- package/dist/ui/screen.d.ts +27 -0
- package/dist/ui/screen.js +57 -0
- package/dist/ui/theme.d.ts +78 -0
- package/dist/ui/theme.js +127 -0
- package/dist/ui/transcript-text.d.ts +27 -0
- package/dist/ui/transcript-text.js +110 -0
- package/package.json +73 -0
- package/skills/skill-creator/SKILL.md +96 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aragon skills …` — the non-interactive surface (spec §7.3).
|
|
3
|
+
*
|
|
4
|
+
* Exit codes match the existing `runConfigSet` convention:
|
|
5
|
+
* 0 success · 1 run-time failure (network / fs / git) · 2 usage or validation.
|
|
6
|
+
*
|
|
7
|
+
* Nothing here renders Ink. These commands run in a one-shot process, so there
|
|
8
|
+
* is no App and therefore no human channel unless `--yes` says otherwise — the
|
|
9
|
+
* same fail-closed rule the agent tools follow, reached through the same gate.
|
|
10
|
+
*/
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
import { createNodeSkillHost } from './node-host.js';
|
|
13
|
+
import { SkillService } from './service.js';
|
|
14
|
+
import { createSkill, installSkill, removeSkill, writableRootFor } from './installer.js';
|
|
15
|
+
import { checkManifest } from './manifest.js';
|
|
16
|
+
import { getUserSkillsDir } from './paths.js';
|
|
17
|
+
import { flushUsage, getUsagePath, listUsage, resetUsage } from './usage.js';
|
|
18
|
+
import { currentPlatform } from './node-host.js';
|
|
19
|
+
import { HOST_TOOL_NAMES } from '../tools/index.js';
|
|
20
|
+
import { checkSkillUpdate, isHardFailure, updateAllSkills, updateSkill, } from './updater.js';
|
|
21
|
+
import { loadConfig } from '../config/load.js';
|
|
22
|
+
import { updatePersistedConfig } from '../config/store.js';
|
|
23
|
+
import { rankCatalogRecords } from '@aragon-agent/core';
|
|
24
|
+
import { classifyBundledFile, resolveDeclaredTool } from '@aragon-agent/core/skills';
|
|
25
|
+
const EXIT_OK = 0;
|
|
26
|
+
const EXIT_RUNTIME = 1;
|
|
27
|
+
const EXIT_USAGE = 2;
|
|
28
|
+
/**
|
|
29
|
+
* `--yes` is the ONLY way a one-shot process can approve.
|
|
30
|
+
*
|
|
31
|
+
* There is no TTY prompt implementation here on purpose: adding one would mean
|
|
32
|
+
* a half-interactive command that hangs in CI. Without `--yes`, anything
|
|
33
|
+
* requiring approval fails with exit 2 and says which flag to add.
|
|
34
|
+
*/
|
|
35
|
+
function makeCliApproval(yes) {
|
|
36
|
+
return { canPrompt: () => yes, request: async () => yes };
|
|
37
|
+
}
|
|
38
|
+
function buildService(flags, yes) {
|
|
39
|
+
const config = loadConfig(flags);
|
|
40
|
+
const service = new SkillService({
|
|
41
|
+
host: createNodeSkillHost(),
|
|
42
|
+
getCwd: () => config.cwd,
|
|
43
|
+
config: config.skills,
|
|
44
|
+
runtime: { forcedSkills: [], approveAll: yes },
|
|
45
|
+
approval: makeCliApproval(yes),
|
|
46
|
+
notify: (level, text) => process.stderr.write(`[skills] ${level}: ${text}\n`),
|
|
47
|
+
persist: (patch) => {
|
|
48
|
+
updatePersistedConfig({ skills: patch });
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
service.discover();
|
|
52
|
+
return { service, cwd: config.cwd };
|
|
53
|
+
}
|
|
54
|
+
function toJson(record) {
|
|
55
|
+
return {
|
|
56
|
+
name: record.name,
|
|
57
|
+
version: record.frontmatter.version,
|
|
58
|
+
scope: record.scope,
|
|
59
|
+
description: record.description,
|
|
60
|
+
dir: record.dir,
|
|
61
|
+
disabled: record.disabled,
|
|
62
|
+
invalid: record.invalid,
|
|
63
|
+
activation: record.frontmatter.activation,
|
|
64
|
+
writable: record.writable,
|
|
65
|
+
shadowed: record.shadowed,
|
|
66
|
+
source: record.manifest?.source ?? null,
|
|
67
|
+
integrity: record.integrity,
|
|
68
|
+
updatedAt: record.manifest?.updatedAt ?? null,
|
|
69
|
+
previousVersion: record.manifest?.previousVersion ?? null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `--sort=recent` reuses the CATALOG ranking rather than sorting by timestamp.
|
|
74
|
+
*
|
|
75
|
+
* The point of the flag is "show me the order the model sees", so a second,
|
|
76
|
+
* subtly different notion of "recent" would answer a question nobody asked and
|
|
77
|
+
* quietly disagree with the thing it is supposed to explain.
|
|
78
|
+
*/
|
|
79
|
+
function sortForListing(records, service, sort) {
|
|
80
|
+
if (sort !== 'recent')
|
|
81
|
+
return records;
|
|
82
|
+
const ranked = rankCatalogRecords(records, { usage: service.getUsage() });
|
|
83
|
+
const seen = new Set(ranked.map((r) => r.name));
|
|
84
|
+
// Ranking drops disabled / invalid / manual entries; keep them visible at the
|
|
85
|
+
// bottom, because `/skills list` is also how a user finds out WHY something
|
|
86
|
+
// is not showing up.
|
|
87
|
+
return [...ranked, ...records.filter((r) => !seen.has(r.name))];
|
|
88
|
+
}
|
|
89
|
+
/** Dispatch an `aragon skills <sub>` invocation and return the exit code. */
|
|
90
|
+
export async function runSkillsCommand(sub, argument, opts, flags, version) {
|
|
91
|
+
const yes = opts.yes === true;
|
|
92
|
+
if (sub === 'path') {
|
|
93
|
+
process.stdout.write(`${getUserSkillsDir()}\n`);
|
|
94
|
+
return EXIT_OK;
|
|
95
|
+
}
|
|
96
|
+
// Ahead of `buildService` like `path`: the counters live next to the skills
|
|
97
|
+
// directory, not inside it, so reading or deleting them needs no scan.
|
|
98
|
+
if (sub === 'usage')
|
|
99
|
+
return runUsage(opts);
|
|
100
|
+
const { service, cwd } = buildService(flags, yes);
|
|
101
|
+
switch (sub) {
|
|
102
|
+
case 'list': {
|
|
103
|
+
const records = sortForListing(service.list(), service, opts.sort);
|
|
104
|
+
if (opts.json) {
|
|
105
|
+
process.stdout.write(`${JSON.stringify(records.map(toJson), null, 2)}\n`);
|
|
106
|
+
return EXIT_OK;
|
|
107
|
+
}
|
|
108
|
+
if (records.length === 0) {
|
|
109
|
+
process.stdout.write('No skills installed.\n');
|
|
110
|
+
return EXIT_OK;
|
|
111
|
+
}
|
|
112
|
+
for (const record of records) {
|
|
113
|
+
const mark = record.invalid ? '!' : record.disabled ? 'x' : '-';
|
|
114
|
+
const modified = record.integrity === 'modified' ? ' !modified' : '';
|
|
115
|
+
process.stdout.write(`${mark} ${record.name.padEnd(24)} ${record.frontmatter.version.padEnd(8)} ` +
|
|
116
|
+
`${record.scope.padEnd(8)} ${record.description.replace(/\s+/g, ' ').slice(0, 60)}` +
|
|
117
|
+
`${modified}\n`);
|
|
118
|
+
}
|
|
119
|
+
for (const dir of service.untrustedDirs()) {
|
|
120
|
+
process.stderr.write(`[skills] untrusted project dir skipped: ${dir}\n`);
|
|
121
|
+
}
|
|
122
|
+
return EXIT_OK;
|
|
123
|
+
}
|
|
124
|
+
case 'info': {
|
|
125
|
+
if (!argument) {
|
|
126
|
+
process.stderr.write('Usage: aragon skills info <name>\n');
|
|
127
|
+
return EXIT_USAGE;
|
|
128
|
+
}
|
|
129
|
+
const record = service.get(argument);
|
|
130
|
+
if (!record) {
|
|
131
|
+
process.stderr.write(`No skill named "${argument}".\n`);
|
|
132
|
+
return EXIT_USAGE;
|
|
133
|
+
}
|
|
134
|
+
if (opts.json) {
|
|
135
|
+
process.stdout.write(`${JSON.stringify(toJson(record), null, 2)}\n`);
|
|
136
|
+
return EXIT_OK;
|
|
137
|
+
}
|
|
138
|
+
process.stdout.write([
|
|
139
|
+
`${record.name} v${record.frontmatter.version} (${record.scope})`,
|
|
140
|
+
record.description,
|
|
141
|
+
`Path: ${record.dir}`,
|
|
142
|
+
`Activation: ${record.frontmatter.activation}`,
|
|
143
|
+
record.manifest
|
|
144
|
+
? `Source: ${record.manifest.source.kind} ${record.manifest.source.url}` +
|
|
145
|
+
(record.manifest.source.ref ? ` @ ${record.manifest.source.ref}` : '')
|
|
146
|
+
: 'Source: hand-authored (no install manifest)',
|
|
147
|
+
...(record.manifest?.updatedAt
|
|
148
|
+
? [
|
|
149
|
+
`Updated: ${new Date(record.manifest.updatedAt).toISOString()}` +
|
|
150
|
+
(record.manifest.previousVersion ? ` (from v${record.manifest.previousVersion})` : ''),
|
|
151
|
+
]
|
|
152
|
+
: []),
|
|
153
|
+
...(record.integrity === 'modified' ? ['Integrity: modified since install'] : []),
|
|
154
|
+
'',
|
|
155
|
+
].join('\n'));
|
|
156
|
+
return EXIT_OK;
|
|
157
|
+
}
|
|
158
|
+
case 'install': {
|
|
159
|
+
if (!argument) {
|
|
160
|
+
process.stderr.write('Usage: aragon skills install <source> [--scope user|project] [--yes]\n');
|
|
161
|
+
return EXIT_USAGE;
|
|
162
|
+
}
|
|
163
|
+
const scope = parseScope(opts.scope);
|
|
164
|
+
if (scope === undefined && opts.scope) {
|
|
165
|
+
process.stderr.write(`Unknown scope "${opts.scope}". Use user or project.\n`);
|
|
166
|
+
return EXIT_USAGE;
|
|
167
|
+
}
|
|
168
|
+
const result = await installSkill(service, argument, {
|
|
169
|
+
...(scope ? { scope } : {}),
|
|
170
|
+
...(opts.name ? { name: opts.name } : {}),
|
|
171
|
+
initiator: 'user',
|
|
172
|
+
cwd,
|
|
173
|
+
installer: version,
|
|
174
|
+
});
|
|
175
|
+
if (!result.ok) {
|
|
176
|
+
process.stderr.write(`${result.error}\n`);
|
|
177
|
+
return isApprovalError(result.error) ? EXIT_USAGE : EXIT_RUNTIME;
|
|
178
|
+
}
|
|
179
|
+
process.stdout.write(`Installed ${result.name} v${result.version} (${result.fileCount} files) at ${result.dir}\n`);
|
|
180
|
+
return EXIT_OK;
|
|
181
|
+
}
|
|
182
|
+
case 'update':
|
|
183
|
+
return runUpdate(service, argument, opts, cwd, version);
|
|
184
|
+
case 'remove': {
|
|
185
|
+
if (!argument) {
|
|
186
|
+
process.stderr.write('Usage: aragon skills remove <name> [--yes]\n');
|
|
187
|
+
return EXIT_USAGE;
|
|
188
|
+
}
|
|
189
|
+
const result = await removeSkill(service, argument);
|
|
190
|
+
if (!result.ok) {
|
|
191
|
+
process.stderr.write(`${result.error}\n`);
|
|
192
|
+
return result.reason === 'io_error' ? EXIT_RUNTIME : EXIT_USAGE;
|
|
193
|
+
}
|
|
194
|
+
process.stdout.write(`Removed ${argument}\n`);
|
|
195
|
+
return EXIT_OK;
|
|
196
|
+
}
|
|
197
|
+
case 'create': {
|
|
198
|
+
if (!argument || !opts.description) {
|
|
199
|
+
process.stderr.write('Usage: aragon skills create <name> --description <d> [--scope user|project] [--yes]\n');
|
|
200
|
+
return EXIT_USAGE;
|
|
201
|
+
}
|
|
202
|
+
const scope = parseScope(opts.scope) ?? 'user';
|
|
203
|
+
if (!writableRootFor(scope, cwd)) {
|
|
204
|
+
process.stderr.write(`Cannot create a skill in a ${scope} scope.\n`);
|
|
205
|
+
return EXIT_USAGE;
|
|
206
|
+
}
|
|
207
|
+
const result = await createSkill(service, {
|
|
208
|
+
name: argument,
|
|
209
|
+
description: opts.description,
|
|
210
|
+
// A scaffold, not a finished skill: the body is a TODO the author is
|
|
211
|
+
// expected to open and fill in. Inventing prose here would produce
|
|
212
|
+
// plausible-looking instructions nobody wrote.
|
|
213
|
+
body: [`# ${argument}`, '', '## When to use', '', 'TODO', '', '## Steps', '', '1. TODO'].join('\n'),
|
|
214
|
+
scope,
|
|
215
|
+
initiator: 'user',
|
|
216
|
+
}, { cwd, installer: version });
|
|
217
|
+
if (!result.ok) {
|
|
218
|
+
process.stderr.write(`${result.error}\n`);
|
|
219
|
+
return isApprovalError(result.error) ? EXIT_USAGE : EXIT_RUNTIME;
|
|
220
|
+
}
|
|
221
|
+
process.stdout.write(`Created ${result.name} at ${result.dir}\nEdit SKILL.md to fill it in.\n`);
|
|
222
|
+
return EXIT_OK;
|
|
223
|
+
}
|
|
224
|
+
case 'doctor': {
|
|
225
|
+
let problems = 0;
|
|
226
|
+
for (const record of service.list()) {
|
|
227
|
+
for (const issue of record.issues) {
|
|
228
|
+
if (issue.level === 'error')
|
|
229
|
+
problems += 1;
|
|
230
|
+
process.stdout.write(`${issue.level}: ${record.name}: ${issue.code} - ${issue.message}\n`);
|
|
231
|
+
}
|
|
232
|
+
for (const line of checkDeclaredTools(record))
|
|
233
|
+
process.stdout.write(line);
|
|
234
|
+
for (const line of checkScriptPlatform(record, bundledFiles(service, record))) {
|
|
235
|
+
process.stdout.write(line);
|
|
236
|
+
}
|
|
237
|
+
if (record.integrity === 'modified') {
|
|
238
|
+
process.stdout.write(`warn: ${record.name}: SKILL.md changed since install\n`);
|
|
239
|
+
}
|
|
240
|
+
if (record.manifest) {
|
|
241
|
+
const tamper = checkManifest(record.dir, record.manifest);
|
|
242
|
+
for (const file of tamper.missing) {
|
|
243
|
+
process.stdout.write(`warn: ${record.name}: missing file ${file}\n`);
|
|
244
|
+
}
|
|
245
|
+
for (const file of tamper.modified) {
|
|
246
|
+
process.stdout.write(`warn: ${record.name}: modified since install ${file}\n`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const error of service.errors())
|
|
251
|
+
process.stdout.write(`warn: ${error}\n`);
|
|
252
|
+
process.stdout.write(problems === 0
|
|
253
|
+
? `Checked ${service.list().length} skills; no errors.\n`
|
|
254
|
+
: `Checked ${service.list().length} skills; ${problems} error(s).\n`);
|
|
255
|
+
return problems === 0 ? EXIT_OK : EXIT_USAGE;
|
|
256
|
+
}
|
|
257
|
+
case 'trust':
|
|
258
|
+
case 'untrust': {
|
|
259
|
+
if (!argument) {
|
|
260
|
+
process.stderr.write(`Usage: aragon skills ${sub} <dir>\n`);
|
|
261
|
+
return EXIT_USAGE;
|
|
262
|
+
}
|
|
263
|
+
const ok = sub === 'trust' ? service.trustDir(argument) : service.untrustDir(argument);
|
|
264
|
+
if (!ok) {
|
|
265
|
+
process.stderr.write(sub === 'trust' ? `Could not resolve "${argument}".\n` : `"${argument}" was not trusted.\n`);
|
|
266
|
+
return EXIT_USAGE;
|
|
267
|
+
}
|
|
268
|
+
process.stdout.write(`${sub === 'trust' ? 'Trusted' : 'Untrusted'} ${argument}\n`);
|
|
269
|
+
return EXIT_OK;
|
|
270
|
+
}
|
|
271
|
+
default:
|
|
272
|
+
process.stderr.write(`Unknown subcommand "${sub}". Try: list, info, install, update, remove, create, path, ` +
|
|
273
|
+
'doctor, usage, trust, untrust.\n');
|
|
274
|
+
return EXIT_USAGE;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
// doctor — the two checks added by this iteration (§5.7 / §8.4)
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
/**
|
|
281
|
+
* Can this skill's `allowed-tools` ever take effect?
|
|
282
|
+
*
|
|
283
|
+
* Resolved against `HOST_TOOL_NAMES`, not a live tool array: this process has no
|
|
284
|
+
* `AgentController` at all (P1-8), and the question doctor answers is "will this
|
|
285
|
+
* declaration work?", not "is it working at this instant". Using the superset
|
|
286
|
+
* also makes the output independent of how the CLI happened to be invoked, which
|
|
287
|
+
* is what lets it be snapshot-tested.
|
|
288
|
+
*/
|
|
289
|
+
export function checkDeclaredTools(record) {
|
|
290
|
+
const declared = record.frontmatter.allowedTools;
|
|
291
|
+
if (declared.length === 0)
|
|
292
|
+
return [];
|
|
293
|
+
const resolved = [];
|
|
294
|
+
const absent = [];
|
|
295
|
+
const unknown = [];
|
|
296
|
+
for (const name of declared) {
|
|
297
|
+
const outcome = resolveDeclaredTool(name, HOST_TOOL_NAMES);
|
|
298
|
+
if (outcome === null)
|
|
299
|
+
unknown.push(name);
|
|
300
|
+
else if (outcome === 'absent')
|
|
301
|
+
absent.push(name);
|
|
302
|
+
else
|
|
303
|
+
resolved.push(outcome);
|
|
304
|
+
}
|
|
305
|
+
if (unknown.length > 0) {
|
|
306
|
+
return [
|
|
307
|
+
`warn: ${record.name}: tools: NOT ENFORCEABLE - unknown: ${unknown.join(', ')}. ` +
|
|
308
|
+
'Fix the name or the ceiling will never apply.\n',
|
|
309
|
+
];
|
|
310
|
+
}
|
|
311
|
+
// Flagged rather than silently accepted: the author wrote a permission intent
|
|
312
|
+
// that the system then ignores wholesale (D-G9), and nothing else says so.
|
|
313
|
+
if (record.frontmatter.activation === 'always') {
|
|
314
|
+
return [
|
|
315
|
+
`warn: ${record.name}: tools: "${record.name}" is activation: always, ` +
|
|
316
|
+
'so its allowed-tools never applies.\n',
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
const ignored = absent.length > 0 ? `; ignored on this host: ${absent.join(', ')}` : '';
|
|
320
|
+
return [`ok: ${record.name}: tools: ok (${resolved.join(', ')})${ignored}\n`];
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Bundled files for a doctor check.
|
|
324
|
+
*
|
|
325
|
+
* `record.files` is `null` until a Level 2 load, and discovery deliberately does
|
|
326
|
+
* not hydrate it. `countUse: false` because inspecting a skill is not using one
|
|
327
|
+
* — counting it here would let a single `doctor` run reorder the catalog.
|
|
328
|
+
*/
|
|
329
|
+
function bundledFiles(service, record) {
|
|
330
|
+
if (record.files)
|
|
331
|
+
return record.files;
|
|
332
|
+
try {
|
|
333
|
+
return service.loadBody(record.name, { countUse: false }).files;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/** Bundled scripts that cannot run on this host's shell (§8.4). Warn, never error. */
|
|
340
|
+
export function checkScriptPlatform(record, files) {
|
|
341
|
+
const scripts = files.filter((f) => classifyBundledFile(f.path) === 'script');
|
|
342
|
+
if (scripts.length === 0)
|
|
343
|
+
return [];
|
|
344
|
+
const platform = currentPlatform();
|
|
345
|
+
const unusable = platform === 'win32' ? '.sh' : '.ps1';
|
|
346
|
+
const runnable = scripts.filter((f) => !f.path.toLowerCase().endsWith(unusable));
|
|
347
|
+
if (runnable.length > 0)
|
|
348
|
+
return [];
|
|
349
|
+
const shell = platform === 'win32' ? 'cmd.exe (win32)' : 'a POSIX shell';
|
|
350
|
+
return [
|
|
351
|
+
`warn: ${record.name}: scripts: ${record.name} bundles only ${unusable} scripts; ` +
|
|
352
|
+
`this host runs ${shell}.\n`,
|
|
353
|
+
];
|
|
354
|
+
}
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
// usage (§9 / FG7)
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
function runUsage(opts) {
|
|
359
|
+
if (opts.reset) {
|
|
360
|
+
// Same rule as every other mutating one-shot: `--yes` is the only approval
|
|
361
|
+
// channel here, because a half-interactive prompt would hang in CI.
|
|
362
|
+
if (!opts.yes) {
|
|
363
|
+
process.stderr.write('Refusing to delete usage counters without --yes. ' +
|
|
364
|
+
'Run: aragon skills usage --reset --yes\n');
|
|
365
|
+
return EXIT_USAGE;
|
|
366
|
+
}
|
|
367
|
+
const removed = resetUsage();
|
|
368
|
+
process.stdout.write(`Cleared ${removed} usage entr${removed === 1 ? 'y' : 'ies'}.\n`);
|
|
369
|
+
return EXIT_OK;
|
|
370
|
+
}
|
|
371
|
+
const rows = listUsage();
|
|
372
|
+
if (opts.json) {
|
|
373
|
+
process.stdout.write(`${JSON.stringify({ path: getUsagePath(), skills: rows }, null, 2)}\n`);
|
|
374
|
+
return EXIT_OK;
|
|
375
|
+
}
|
|
376
|
+
if (rows.length === 0) {
|
|
377
|
+
process.stdout.write(`No skill usage recorded yet.\nFile: ${getUsagePath()}\n`);
|
|
378
|
+
return EXIT_OK;
|
|
379
|
+
}
|
|
380
|
+
const width = Math.max(...rows.map((r) => r.name.length), 5);
|
|
381
|
+
process.stdout.write(`${'skill'.padEnd(width)} uses last used\n`);
|
|
382
|
+
for (const row of rows) {
|
|
383
|
+
const stamp = row.lastUsedAt > 0 ? new Date(row.lastUsedAt).toISOString() : 'never';
|
|
384
|
+
process.stdout.write(`${row.name.padEnd(width)} ${String(row.useCount).padStart(4)} ${stamp}\n`);
|
|
385
|
+
}
|
|
386
|
+
process.stdout.write(`\nFile: ${getUsagePath()}\n` +
|
|
387
|
+
'Stored locally, never transmitted. Clear it with "aragon skills usage --reset --yes", ' +
|
|
388
|
+
'or switch it off with skills.usageTracking=false.\n');
|
|
389
|
+
return EXIT_OK;
|
|
390
|
+
}
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// update (§6.5)
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
const UPDATE_USAGE = 'Usage: aragon skills update <name>|--all [--yes] [--force] [--dry-run]\n' +
|
|
395
|
+
' aragon skills update --check [<name>|--all]\n';
|
|
396
|
+
/**
|
|
397
|
+
* One row of the `--all` summary, in the tense the user will read it in.
|
|
398
|
+
*
|
|
399
|
+
* The tense comes from the FLAG, not from the shape of the result. A successful
|
|
400
|
+
* real update also carries `changedFiles`, so inferring "dry run" from its
|
|
401
|
+
* presence made every genuine `--all` run report "would update" for work it had
|
|
402
|
+
* already committed to disk.
|
|
403
|
+
*/
|
|
404
|
+
export function describeResult(result, dryRun) {
|
|
405
|
+
if (result.ok && !result.changed)
|
|
406
|
+
return 'up to date';
|
|
407
|
+
if (result.ok && result.changed) {
|
|
408
|
+
const versions = result.fromVersion && result.toVersion && result.fromVersion !== result.toVersion
|
|
409
|
+
? ` ${result.fromVersion} -> ${result.toVersion}`
|
|
410
|
+
: '';
|
|
411
|
+
return `${dryRun ? 'would update' : 'updated'}${versions}`;
|
|
412
|
+
}
|
|
413
|
+
return `skipped: ${result.error?.split('\n')[0] ?? result.reason ?? 'unknown'}`;
|
|
414
|
+
}
|
|
415
|
+
async function runUpdate(service, argument, opts, cwd, version) {
|
|
416
|
+
if (opts.check)
|
|
417
|
+
return runUpdateCheck(service, argument, opts);
|
|
418
|
+
const all = opts.all === true || argument === '--all';
|
|
419
|
+
if (!all && !argument) {
|
|
420
|
+
process.stderr.write(UPDATE_USAGE);
|
|
421
|
+
return EXIT_USAGE;
|
|
422
|
+
}
|
|
423
|
+
const updateOpts = {
|
|
424
|
+
cwd,
|
|
425
|
+
installer: version,
|
|
426
|
+
...(opts.dryRun ? { dryRun: true } : {}),
|
|
427
|
+
...(opts.force ? { force: true } : {}),
|
|
428
|
+
initiator: 'user',
|
|
429
|
+
};
|
|
430
|
+
if (!all) {
|
|
431
|
+
const result = await updateSkill(service, argument, updateOpts);
|
|
432
|
+
flushUsage();
|
|
433
|
+
if (!result.ok) {
|
|
434
|
+
process.stderr.write(`${result.error}\n`);
|
|
435
|
+
return isHardFailure(result) ? EXIT_RUNTIME : EXIT_USAGE;
|
|
436
|
+
}
|
|
437
|
+
if (!result.changed) {
|
|
438
|
+
process.stdout.write(`${result.name} is up to date (v${result.fromVersion}).\n`);
|
|
439
|
+
return EXIT_OK;
|
|
440
|
+
}
|
|
441
|
+
if (opts.dryRun) {
|
|
442
|
+
process.stdout.write(`${result.name} would update ${result.fromVersion} -> ${result.toVersion}\n` +
|
|
443
|
+
(result.changedFiles ?? []).map((f) => ` ${f}\n`).join(''));
|
|
444
|
+
return EXIT_OK;
|
|
445
|
+
}
|
|
446
|
+
process.stdout.write(`Updated ${result.name} ${result.fromVersion} -> ${result.toVersion}\n`);
|
|
447
|
+
return EXIT_OK;
|
|
448
|
+
}
|
|
449
|
+
const results = await updateAllSkills(service, updateOpts);
|
|
450
|
+
flushUsage();
|
|
451
|
+
if (results.length === 0) {
|
|
452
|
+
process.stdout.write('No skills have an upstream to update from.\n');
|
|
453
|
+
return EXIT_OK;
|
|
454
|
+
}
|
|
455
|
+
const width = Math.max(...results.map((r) => r.name.length), 5);
|
|
456
|
+
process.stdout.write(`${'skill'.padEnd(width)} result\n`);
|
|
457
|
+
process.stdout.write(`${'-'.repeat(width)} ${'-'.repeat(27)}\n`);
|
|
458
|
+
for (const result of results) {
|
|
459
|
+
process.stdout.write(`${result.name.padEnd(width)} ${describeResult(result, opts.dryRun === true)}\n`);
|
|
460
|
+
}
|
|
461
|
+
const updated = results.filter((r) => r.ok && r.changed).length;
|
|
462
|
+
const unchanged = results.filter((r) => r.ok && !r.changed).length;
|
|
463
|
+
const failed = results.filter(isHardFailure).length;
|
|
464
|
+
const skipped = results.length - updated - unchanged - failed;
|
|
465
|
+
process.stdout.write(`Updated ${updated}, unchanged ${unchanged}, skipped ${skipped}, failed ${failed}.\n`);
|
|
466
|
+
// A skill that CANNOT be updated (hand-authored, bundled, locally modified) is
|
|
467
|
+
// an expected outcome of `--all`, not an error — exiting non-zero for it would
|
|
468
|
+
// make the command useless in any script.
|
|
469
|
+
return failed > 0 ? EXIT_RUNTIME : EXIT_OK;
|
|
470
|
+
}
|
|
471
|
+
async function runUpdateCheck(service, argument, opts) {
|
|
472
|
+
const all = opts.all === true || argument === '--all' || argument === undefined;
|
|
473
|
+
const names = all
|
|
474
|
+
? service
|
|
475
|
+
.list()
|
|
476
|
+
.filter((r) => r.manifest?.source.kind === 'git')
|
|
477
|
+
.map((r) => r.name)
|
|
478
|
+
: [argument];
|
|
479
|
+
if (names.length === 0) {
|
|
480
|
+
process.stdout.write('No git-sourced skills to check.\n');
|
|
481
|
+
return EXIT_OK;
|
|
482
|
+
}
|
|
483
|
+
const width = Math.max(...names.map((n) => n.length), 5);
|
|
484
|
+
for (const name of names) {
|
|
485
|
+
const result = await checkSkillUpdate(service, name);
|
|
486
|
+
const detail = result.status === 'outdated'
|
|
487
|
+
? ` (${(result.currentRef ?? '').slice(0, 8)} -> ${(result.remoteRef ?? '').slice(0, 8)})`
|
|
488
|
+
: result.detail
|
|
489
|
+
? ` (${result.detail})`
|
|
490
|
+
: '';
|
|
491
|
+
process.stdout.write(`${name.padEnd(width)} ${result.status}${detail}\n`);
|
|
492
|
+
}
|
|
493
|
+
return EXIT_OK;
|
|
494
|
+
}
|
|
495
|
+
function parseScope(value) {
|
|
496
|
+
if (value === 'user' || value === 'project')
|
|
497
|
+
return value;
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
function isApprovalError(error) {
|
|
501
|
+
return Boolean(error && error.includes('requires approval'));
|
|
502
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install-source identification and fetching (spec §8.2 / §9.2).
|
|
3
|
+
*
|
|
4
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* `skill_install.source` IS A MODEL PARAMETER. Treat it as hostile input.
|
|
6
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* Four rules are MUST (§8.2.1 / D20). Breaking any one of them is a shell or
|
|
9
|
+
* argv injection, not a style problem:
|
|
10
|
+
*
|
|
11
|
+
* 1. External processes are started with `execFile(cmd, argv[], {shell:false})`
|
|
12
|
+
* only. No `exec()`, no `execSync()`, no `shell: true`, no string
|
|
13
|
+
* concatenation of a command line. `tools/bash-tool.ts` DOES use
|
|
14
|
+
* `shell: true` — that is the bash tool's deliberate purpose and is NOT a
|
|
15
|
+
* precedent for this module. Copy `builtins.ts::copyToClipboard` instead.
|
|
16
|
+
*
|
|
17
|
+
* 2. Validate BEFORE building argv. `resolveSource()` is pure and spawns
|
|
18
|
+
* nothing; anything that fails here never reaches a subprocess.
|
|
19
|
+
*
|
|
20
|
+
* 3. Reject a leading `-` on every model-controlled fragment. This matters
|
|
21
|
+
* even with a pure argv array: `git clone --branch --upload-pack=calc.exe`
|
|
22
|
+
* makes git execute an arbitrary program. `shell: false` does not help.
|
|
23
|
+
* The whitelists below already force an alphanumeric first character; the
|
|
24
|
+
* explicit check is a second, separately-tested statement of intent.
|
|
25
|
+
*
|
|
26
|
+
* 4. Never let git prompt. A private repo would otherwise hang a headless
|
|
27
|
+
* install forever waiting on a credential prompt nobody can see.
|
|
28
|
+
*/
|
|
29
|
+
import type { SkillManifestSource } from '@aragon-agent/core/skills';
|
|
30
|
+
export declare const HTTP_TIMEOUT_MS = 15000;
|
|
31
|
+
export declare const GIT_TIMEOUT_MS = 120000;
|
|
32
|
+
export declare const MAX_REDIRECTS = 3;
|
|
33
|
+
export declare const SKILL_MD_DOWNLOAD_MAX_BYTES: number;
|
|
34
|
+
/** `https:` only, hostname on the allowlist, never an IP literal or loopback. */
|
|
35
|
+
export declare function checkUrlAllowed(raw: string, allowedHosts: string[]): {
|
|
36
|
+
ok: true;
|
|
37
|
+
url: URL;
|
|
38
|
+
} | {
|
|
39
|
+
ok: false;
|
|
40
|
+
error: string;
|
|
41
|
+
};
|
|
42
|
+
export type SourceSpec = {
|
|
43
|
+
kind: 'local-dir';
|
|
44
|
+
path: string;
|
|
45
|
+
} | {
|
|
46
|
+
kind: 'local-file';
|
|
47
|
+
path: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: 'git';
|
|
50
|
+
url: string;
|
|
51
|
+
ref: string | null;
|
|
52
|
+
subdir: string | null;
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'https-md';
|
|
55
|
+
url: string;
|
|
56
|
+
} | {
|
|
57
|
+
kind: 'https-zip';
|
|
58
|
+
url: string;
|
|
59
|
+
};
|
|
60
|
+
export type ResolveResult = {
|
|
61
|
+
ok: true;
|
|
62
|
+
spec: SourceSpec;
|
|
63
|
+
} | {
|
|
64
|
+
ok: false;
|
|
65
|
+
error: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Classify and validate an install source.
|
|
69
|
+
*
|
|
70
|
+
* Every rejection path returns before any subprocess or socket exists — that
|
|
71
|
+
* property is what AC-15 asserts (mocked `execFile`, call count 0).
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveSource(source: string, allowedHosts: string[]): ResolveResult;
|
|
74
|
+
export interface RunProcessResult {
|
|
75
|
+
stdout: string;
|
|
76
|
+
}
|
|
77
|
+
export interface FetchDeps {
|
|
78
|
+
allowedHosts: string[];
|
|
79
|
+
/** Injectable so tests can assert the subprocess count without spawning git. */
|
|
80
|
+
runProcess?: (file: string, args: string[], options: {
|
|
81
|
+
cwd?: string;
|
|
82
|
+
}) => Promise<RunProcessResult>;
|
|
83
|
+
fetchImpl?: typeof fetch;
|
|
84
|
+
userAgent?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface FetchOutcome {
|
|
87
|
+
/** Directory holding the fetched skill content. */
|
|
88
|
+
dir: string;
|
|
89
|
+
source: SkillManifestSource;
|
|
90
|
+
}
|
|
91
|
+
/** Fetch a resolved source into `stagingDir`, returning where the content landed. */
|
|
92
|
+
export declare function fetchSource(spec: SourceSpec, stagingDir: string, deps: FetchDeps): Promise<FetchOutcome>;
|
|
93
|
+
/**
|
|
94
|
+
* Ask a remote for the sha behind `ref`, without downloading anything.
|
|
95
|
+
*
|
|
96
|
+
* Subject to the SAME four rules as every other subprocess here (D20): argv
|
|
97
|
+
* array, `shell:false`, no leading `-` on any model-influenced fragment, and no
|
|
98
|
+
* credential prompt. The caller must have re-validated the URL against the host
|
|
99
|
+
* allowlist first — this function does not do policy.
|
|
100
|
+
*
|
|
101
|
+
* Returns `null` for "cannot tell", which is the honest answer for a source
|
|
102
|
+
* git cannot reach, an unmatched ref, or a non-sha reply.
|
|
103
|
+
*/
|
|
104
|
+
export declare function gitLsRemote(url: string, ref: string | null, deps: FetchDeps): Promise<string | null>;
|