@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,625 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/skills …` management commands and the dynamic `/<skill-name>` registrations
|
|
3
|
+
* (spec §7.1 / §7.2).
|
|
4
|
+
*/
|
|
5
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { computeToolPolicy, rankCatalogRecords, renderSkillInvocation, } from '@aragon-agent/core';
|
|
8
|
+
import { resolveDeclaredTool } from '@aragon-agent/core/skills';
|
|
9
|
+
import { installSkill, removeSkill, renderSkillMarkdown } from '../skills/installer.js';
|
|
10
|
+
import { writableRootFor } from '../skills/installer.js';
|
|
11
|
+
import { checkManifest } from '../skills/manifest.js';
|
|
12
|
+
import { updateAllSkills, updateSkill, isHardFailure } from '../skills/updater.js';
|
|
13
|
+
import { currentPlatform } from '../skills/node-host.js';
|
|
14
|
+
import { listUsage, resetUsage, getUsagePath } from '../skills/usage.js';
|
|
15
|
+
import { HOST_TOOL_NAMES, SKILL_TOOL_FLOOR } from '../tools/index.js';
|
|
16
|
+
import { SKILLS_TOOL_POLICY_MODES, isSkillsToolPolicyMode } from '../config/schema.js';
|
|
17
|
+
import { pickGlyphs } from '../ui/glyphs.js';
|
|
18
|
+
function marksFor(ctx) {
|
|
19
|
+
const cfg = ctx.controller.getConfig();
|
|
20
|
+
const glyphs = pickGlyphs({ colorLevel: cfg.colorLevel ?? 0, unicode: cfg.unicode ?? false });
|
|
21
|
+
return {
|
|
22
|
+
glyphs,
|
|
23
|
+
marks: {
|
|
24
|
+
ok: glyphs.check,
|
|
25
|
+
disabled: glyphs.times,
|
|
26
|
+
invalid: glyphs.warn,
|
|
27
|
+
shadowed: '~',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function markFor(record, marks) {
|
|
32
|
+
if (record.invalid)
|
|
33
|
+
return marks.invalid;
|
|
34
|
+
if (record.disabled)
|
|
35
|
+
return marks.disabled;
|
|
36
|
+
return marks.ok;
|
|
37
|
+
}
|
|
38
|
+
function shortDescription(record, max = 60, ellipsis = '...') {
|
|
39
|
+
const one = record.description.replace(/\s+/g, ' ').trim();
|
|
40
|
+
return one.length > max ? `${one.slice(0, max - ellipsis.length)}${ellipsis}` : one;
|
|
41
|
+
}
|
|
42
|
+
function formatRow(record, marks, glyphs) {
|
|
43
|
+
const shadow = record.shadowed.length > 0 ? ` ${marks.shadowed}shadowed by ${record.shadowed[0].scope}` : '';
|
|
44
|
+
// Spelled out rather than folded into the mark column: `!` there already means
|
|
45
|
+
// "failed validation", and conflating "broken" with "you edited this" would
|
|
46
|
+
// send the user looking for a problem that does not exist.
|
|
47
|
+
const modified = record.integrity === 'modified' ? ' !modified' : '';
|
|
48
|
+
return (`${markFor(record, marks)} ${record.name.padEnd(24)} ${record.frontmatter.version.padEnd(8)} ` +
|
|
49
|
+
`${record.scope.padEnd(8)} ${shortDescription(record, 60, glyphs.ellipsis)}${modified}${shadow}`);
|
|
50
|
+
}
|
|
51
|
+
function parseScopeFlag(args) {
|
|
52
|
+
const match = /--scope[= ](user|project)\b/.exec(args);
|
|
53
|
+
if (!match)
|
|
54
|
+
return { rest: args.trim() };
|
|
55
|
+
return { rest: args.replace(match[0], '').trim(), scope: match[1] };
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// /skills
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
async function runSkills(ctx, service, version) {
|
|
61
|
+
const [sub, ...rest] = ctx.args.trim().split(/\s+/).filter((s) => s.length > 0);
|
|
62
|
+
const argument = rest.join(' ');
|
|
63
|
+
switch (sub) {
|
|
64
|
+
case undefined:
|
|
65
|
+
return listSkills(ctx, service);
|
|
66
|
+
case 'list':
|
|
67
|
+
return listSkills(ctx, service, argument);
|
|
68
|
+
case 'info':
|
|
69
|
+
return infoSkill(ctx, service, argument);
|
|
70
|
+
case 'install':
|
|
71
|
+
return installFromCommand(ctx, service, argument, version);
|
|
72
|
+
case 'update':
|
|
73
|
+
return updateFromCommand(ctx, service, argument, version);
|
|
74
|
+
case 'remove':
|
|
75
|
+
return removeFromCommand(ctx, service, argument);
|
|
76
|
+
case 'enable':
|
|
77
|
+
case 'disable':
|
|
78
|
+
return toggleSkill(ctx, service, argument, sub === 'disable');
|
|
79
|
+
case 'reload': {
|
|
80
|
+
const result = service.reload();
|
|
81
|
+
ctx.toast('success', `Reloaded ${result.records.length} skills.`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
case 'create':
|
|
85
|
+
return scaffoldSkill(ctx, service, argument);
|
|
86
|
+
case 'trust':
|
|
87
|
+
case 'untrust':
|
|
88
|
+
return trustFromCommand(ctx, service, argument, sub === 'untrust');
|
|
89
|
+
case 'policy':
|
|
90
|
+
return policyFromCommand(ctx, service, argument);
|
|
91
|
+
case 'unload':
|
|
92
|
+
return unloadFrames(ctx, service);
|
|
93
|
+
case 'usage':
|
|
94
|
+
return usageFromCommand(ctx, service, argument);
|
|
95
|
+
default:
|
|
96
|
+
ctx.notify('warn', `Unknown subcommand "/skills ${sub}". Try: list, info, install, update, remove, enable, ` +
|
|
97
|
+
'disable, reload, create, trust, untrust, policy, unload, usage.');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// /skills policy · unload · usage (§5.5 / §9)
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
/**
|
|
104
|
+
* What the frame's declarations actually amount to, for display only.
|
|
105
|
+
*
|
|
106
|
+
* DERIVED FROM `computeToolPolicy`, NOT FROM THE RAW FRONTMATTER. The two answers
|
|
107
|
+
* differ in exactly the cases that matter: `skills.toolPolicy=off` enforces
|
|
108
|
+
* nothing at all, and a single unresolvable tool name waives a whole declaration
|
|
109
|
+
* (D-G4). Reading `allowedTools` directly would print "Tool ceiling this turn: …"
|
|
110
|
+
* for a ceiling nobody is applying — the same overclaim `/skills info` was
|
|
111
|
+
* rewritten to stop making (P0-3 / RG3), and it would contradict the
|
|
112
|
+
* `NOT ENFORCEABLE` line that `info` prints for the very same skill.
|
|
113
|
+
*
|
|
114
|
+
* Resolved against `HOST_TOOL_NAMES` like `doctor` and `info` (P1-8): this is a
|
|
115
|
+
* statement about what a declaration means, so it must not change depending on
|
|
116
|
+
* whether this process happens to have an agent attached.
|
|
117
|
+
*/
|
|
118
|
+
function ceilingDecision(service) {
|
|
119
|
+
const frame = service
|
|
120
|
+
.frameNames()
|
|
121
|
+
.map((name) => service.get(name))
|
|
122
|
+
.filter((record) => record !== undefined);
|
|
123
|
+
// `computeToolPolicy` drops disabled / invalid / `activation: always` records
|
|
124
|
+
// itself, so there is no second copy of that rule here.
|
|
125
|
+
return computeToolPolicy({
|
|
126
|
+
frame,
|
|
127
|
+
registered: HOST_TOOL_NAMES,
|
|
128
|
+
floor: SKILL_TOOL_FLOOR,
|
|
129
|
+
mode: service.effectiveToolPolicy().mode,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** `read_file, write_file (+ read-only) - from pdf-forms`, or `''` when unconstrained. */
|
|
133
|
+
function ceilingSummary(service) {
|
|
134
|
+
return summarizeCeiling(ceilingDecision(service));
|
|
135
|
+
}
|
|
136
|
+
function summarizeCeiling(decision) {
|
|
137
|
+
if (decision.allowed === null)
|
|
138
|
+
return '';
|
|
139
|
+
const granted = [...new Set(decision.sources.flatMap((s) => s.granted))].sort((a, b) => a.localeCompare(b, 'en'));
|
|
140
|
+
// A declaration naming only tools this host lacks still imposes a ceiling — the
|
|
141
|
+
// floor and nothing else. Saying so beats printing a bare "(+ read-only)".
|
|
142
|
+
const list = granted.length > 0 ? `${granted.join(', ')} (+ read-only)` : 'read-only tools only';
|
|
143
|
+
return `${list} - from ${decision.sourceNames.join(', ')}`;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* `/skills policy [off|warn|enforce]`.
|
|
147
|
+
*
|
|
148
|
+
* Writing BOTH the session slot and the config file is the whole point (P1-1).
|
|
149
|
+
* Persisting only would leave a `--skill-tool-policy` run unchanged, and the
|
|
150
|
+
* refusal message that sent the user here names this exact command — so the
|
|
151
|
+
* override is also announced when it actually overrode something.
|
|
152
|
+
*/
|
|
153
|
+
function policyFromCommand(ctx, service, argument) {
|
|
154
|
+
const requested = argument.trim();
|
|
155
|
+
const current = service.effectiveToolPolicy();
|
|
156
|
+
if (requested.length === 0) {
|
|
157
|
+
const decision = ceilingDecision(service);
|
|
158
|
+
const ceiling = summarizeCeiling(decision);
|
|
159
|
+
ctx.notify('info', [
|
|
160
|
+
`Tool policy: ${current.mode} (from ${current.from})`,
|
|
161
|
+
ceiling ? `Tool ceiling this turn: ${ceiling}` : 'No tool ceiling in effect this turn.',
|
|
162
|
+
// Naming the waived skills matters here: "no ceiling because nothing
|
|
163
|
+
// declared anything" and "no ceiling because a name is misspelled" look
|
|
164
|
+
// identical otherwise, and only the second one is a bug to go and fix.
|
|
165
|
+
...decision.ignored.map((entry) => ` Not enforceable: "${entry.name}" declares unknown tools ` +
|
|
166
|
+
`(${entry.unresolved.join(', ')}).`),
|
|
167
|
+
`Usage: /skills policy ${SKILLS_TOOL_POLICY_MODES.join('|')}`,
|
|
168
|
+
].join('\n'));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!isSkillsToolPolicyMode(requested)) {
|
|
172
|
+
ctx.notify('warn', `Unknown policy "${requested}". Use ${SKILLS_TOOL_POLICY_MODES.join(', ')}.`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
service.setSessionToolPolicy(requested);
|
|
176
|
+
service.persistToolPolicy(requested);
|
|
177
|
+
const overrode = current.from === 'flag' ? ' (overriding --skill-tool-policy for this session)' : '';
|
|
178
|
+
ctx.notify('info', `Tool policy: ${current.mode} -> ${requested} (this session and saved).${overrode}`);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* `/skills unload` — clears the ceiling frame.
|
|
182
|
+
*
|
|
183
|
+
* The name reads like "take the skill back out of my context", which is not
|
|
184
|
+
* something anything can do: the body is already in the message history. The
|
|
185
|
+
* text says so outright rather than renaming the command, because a rename would
|
|
186
|
+
* strand every reader of the current README (P2-8).
|
|
187
|
+
*/
|
|
188
|
+
function unloadFrames(ctx, service) {
|
|
189
|
+
const cleared = service.frameNames();
|
|
190
|
+
service.clearFrames();
|
|
191
|
+
ctx.notify('info', cleared.length === 0
|
|
192
|
+
? 'No skill was constraining this turn. (This command clears the tool ceiling only - ' +
|
|
193
|
+
'skill text already in the conversation cannot be reclaimed.)'
|
|
194
|
+
: `Tool ceiling cleared (was: ${cleared.join(', ')}). This does NOT remove the skill text ` +
|
|
195
|
+
'already in the conversation - nothing can.');
|
|
196
|
+
}
|
|
197
|
+
function formatRelative(then, now) {
|
|
198
|
+
if (then <= 0)
|
|
199
|
+
return 'never';
|
|
200
|
+
const seconds = Math.max(0, Math.round((now - then) / 1000));
|
|
201
|
+
if (seconds < 60)
|
|
202
|
+
return `${seconds}s ago`;
|
|
203
|
+
const minutes = Math.round(seconds / 60);
|
|
204
|
+
if (minutes < 60)
|
|
205
|
+
return `${minutes}m ago`;
|
|
206
|
+
const hours = Math.round(minutes / 60);
|
|
207
|
+
if (hours < 48)
|
|
208
|
+
return `${hours}h ago`;
|
|
209
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
210
|
+
}
|
|
211
|
+
/** `YYYY-MM-DD HH:MM` in local time — paired with the relative form, never alone. */
|
|
212
|
+
function formatAbsolute(then) {
|
|
213
|
+
if (then <= 0)
|
|
214
|
+
return '-';
|
|
215
|
+
const d = new Date(then);
|
|
216
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
217
|
+
return (`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
218
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}`);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* `/skills usage [--reset]` (§9 / FG7).
|
|
222
|
+
*
|
|
223
|
+
* The counters were being written from day one and were readable by nobody and
|
|
224
|
+
* erasable by no one. "Visible and erasable" is the floor for locally stored
|
|
225
|
+
* behavioural data, however innocuous the contents.
|
|
226
|
+
*/
|
|
227
|
+
async function usageFromCommand(ctx, service, argument) {
|
|
228
|
+
if (/--reset\b/.test(argument)) {
|
|
229
|
+
const approval = service.getApproval();
|
|
230
|
+
// Same fail-closed probe the installer uses (D17): "is there a human?" is
|
|
231
|
+
// asked before "do you agree?", never inferred from a callback's default.
|
|
232
|
+
if (!approval.canPrompt()) {
|
|
233
|
+
ctx.notify('warn', 'Cannot confirm a reset without an interactive session. ' +
|
|
234
|
+
'Run "aragon skills usage --reset --yes" instead.');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const ok = await approval.request({
|
|
238
|
+
tool: 'skills usage --reset',
|
|
239
|
+
summary: `Delete all local skill usage counters (${getUsagePath()})`,
|
|
240
|
+
});
|
|
241
|
+
if (!ok) {
|
|
242
|
+
ctx.notify('info', 'Usage counters left untouched.');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const removed = resetUsage();
|
|
246
|
+
ctx.toast('success', `Cleared ${removed} usage ${removed === 1 ? 'entry' : 'entries'}.`);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const rows = listUsage();
|
|
250
|
+
if (rows.length === 0) {
|
|
251
|
+
ctx.notify('info', `No skill usage recorded yet.\nFile: ${getUsagePath()}`);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const now = Date.now();
|
|
255
|
+
const width = Math.max(...rows.map((r) => r.name.length), 5);
|
|
256
|
+
ctx.notify('info', [
|
|
257
|
+
'Skill usage:',
|
|
258
|
+
...rows.map((r) => ` ${r.name.padEnd(width)} ${String(r.useCount).padStart(4)} ` +
|
|
259
|
+
`${formatRelative(r.lastUsedAt, now)} (${formatAbsolute(r.lastUsedAt)})`),
|
|
260
|
+
'',
|
|
261
|
+
`File: ${getUsagePath()}`,
|
|
262
|
+
'Stored locally, never transmitted. Clear it with "/skills usage --reset", ' +
|
|
263
|
+
'or switch it off with skills.usageTracking=false.',
|
|
264
|
+
].join('\n'));
|
|
265
|
+
}
|
|
266
|
+
function listSkills(ctx, service, args = '') {
|
|
267
|
+
const recent = /--sort[= ]recent\b/.test(args);
|
|
268
|
+
const all = service.list();
|
|
269
|
+
// Same ranking the catalog uses, so "why is that one not listed for the
|
|
270
|
+
// model?" and "what does /skills show me" cannot drift apart.
|
|
271
|
+
const ranked = recent ? rankCatalogRecords(all, { usage: service.getUsage() }) : all;
|
|
272
|
+
const seen = new Set(ranked.map((r) => r.name));
|
|
273
|
+
const records = recent ? [...ranked, ...all.filter((r) => !seen.has(r.name))] : all;
|
|
274
|
+
const pending = service.untrustedDirs();
|
|
275
|
+
if (records.length === 0) {
|
|
276
|
+
const hint = pending.length > 0
|
|
277
|
+
? `\nUntrusted project skill directories: ${pending.join(', ')}\nRun "/skills trust <dir>" to load them.`
|
|
278
|
+
: '';
|
|
279
|
+
ctx.notify('info', `No skills installed.${hint}`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const { marks, glyphs } = marksFor(ctx);
|
|
283
|
+
const lines = records.map((r) => formatRow(r, marks, glyphs));
|
|
284
|
+
const active = service.getRegistry().activeNames;
|
|
285
|
+
const ceiling = ceilingSummary(service);
|
|
286
|
+
const footer = [
|
|
287
|
+
'',
|
|
288
|
+
`${records.length} skills - ${marks.ok} active ${marks.disabled} disabled ` +
|
|
289
|
+
`${marks.invalid} invalid ${marks.shadowed} shadowed`,
|
|
290
|
+
...(active.length > 0 ? [`Loaded this session: ${active.join(', ')}`] : []),
|
|
291
|
+
...(ceiling ? [`Tool ceiling this turn: ${ceiling}`] : []),
|
|
292
|
+
...(pending.length > 0 ? [`Untrusted project dirs: ${pending.join(', ')}`] : []),
|
|
293
|
+
...service.errors().map((e) => `warning: ${e}`),
|
|
294
|
+
];
|
|
295
|
+
ctx.notify('info', ['Skills:', ...lines, ...footer].join('\n'));
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The `Declared tools:` block for `/skills info` (§5.6b).
|
|
299
|
+
*
|
|
300
|
+
* NEVER the bare word `enforced`, and never `advisory` (P0-3 / C3). The scope is
|
|
301
|
+
* a TURN, and the ceiling is a UNION with `skill` permanently in the floor — so
|
|
302
|
+
* a constrained skill can widen its own ceiling by telling the model to load
|
|
303
|
+
* another one (D-G17). Calling that "enforced" full stop would be a fresh
|
|
304
|
+
* version of the same overclaim this iteration exists to retire, and `advisory`
|
|
305
|
+
* is the word that caused the original problem.
|
|
306
|
+
*/
|
|
307
|
+
function declaredToolsLines(record, service) {
|
|
308
|
+
const declared = record.frontmatter.allowedTools;
|
|
309
|
+
const unresolved = [];
|
|
310
|
+
const absent = [];
|
|
311
|
+
for (const name of declared) {
|
|
312
|
+
const outcome = resolveDeclaredTool(name, HOST_TOOL_NAMES);
|
|
313
|
+
if (outcome === null)
|
|
314
|
+
unresolved.push(name);
|
|
315
|
+
else if (outcome === 'absent')
|
|
316
|
+
absent.push(name);
|
|
317
|
+
}
|
|
318
|
+
if (unresolved.length > 0) {
|
|
319
|
+
return [
|
|
320
|
+
`Declared tools: ${declared.join(', ')}`,
|
|
321
|
+
` NOT ENFORCEABLE: unknown tool ${unresolved.map((n) => `"${n}"`).join(', ')}; ` +
|
|
322
|
+
'this skill imposes no ceiling. Fix the name or the ceiling will never apply.',
|
|
323
|
+
];
|
|
324
|
+
}
|
|
325
|
+
const mode = service.effectiveToolPolicy().mode;
|
|
326
|
+
const header = mode === 'off'
|
|
327
|
+
? `Declared tools (not enforced - skills.toolPolicy=off): ${declared.join(', ')}`
|
|
328
|
+
: record.frontmatter.activation === 'always'
|
|
329
|
+
? `Declared tools (never enforced - activation: always is exempt): ${declared.join(', ')}`
|
|
330
|
+
: `Declared tools (enforced this turn, union with other loaded skills): ${declared.join(', ')}`;
|
|
331
|
+
if (absent.length === 0)
|
|
332
|
+
return [header];
|
|
333
|
+
const verb = absent.length === 1 ? 'is' : 'are';
|
|
334
|
+
return [header, ` ${absent.join(', ')} ${verb} not a tool on this host and ${verb} ignored`];
|
|
335
|
+
}
|
|
336
|
+
function infoSkill(ctx, service, name) {
|
|
337
|
+
if (!name) {
|
|
338
|
+
ctx.notify('warn', 'Usage: /skills info <name>');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const record = service.get(name);
|
|
342
|
+
if (!record) {
|
|
343
|
+
ctx.notify('warn', `No skill named "${name}".`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const lines = [
|
|
347
|
+
`${record.name} v${record.frontmatter.version} (${record.scope}${record.writable ? '' : ', read-only'})`,
|
|
348
|
+
record.description,
|
|
349
|
+
`Path: ${record.dir}`,
|
|
350
|
+
];
|
|
351
|
+
if (record.frontmatter.author)
|
|
352
|
+
lines.push(`Author: ${record.frontmatter.author}`);
|
|
353
|
+
if (record.frontmatter.homepage)
|
|
354
|
+
lines.push(`Homepage: ${record.frontmatter.homepage}`);
|
|
355
|
+
if (record.frontmatter.license)
|
|
356
|
+
lines.push(`License: ${record.frontmatter.license}`);
|
|
357
|
+
if (record.frontmatter.keywords.length > 0) {
|
|
358
|
+
lines.push(`Keywords: ${record.frontmatter.keywords.join(', ')}`);
|
|
359
|
+
}
|
|
360
|
+
if (record.frontmatter.allowedTools.length > 0) {
|
|
361
|
+
lines.push(...declaredToolsLines(record, service));
|
|
362
|
+
}
|
|
363
|
+
lines.push(`Activation: ${record.frontmatter.activation}`);
|
|
364
|
+
const manifest = record.manifest;
|
|
365
|
+
if (manifest) {
|
|
366
|
+
lines.push(`Source: ${manifest.source.kind} ${manifest.source.url}` +
|
|
367
|
+
(manifest.source.ref ? ` @ ${manifest.source.ref}` : '') +
|
|
368
|
+
(manifest.source.resolvedRef ? ` (${manifest.source.resolvedRef.slice(0, 8)})` : ''), `Installed: ${new Date(manifest.installedAt).toISOString()} by ${manifest.installer}`, `Files: ${manifest.files.length} (${Math.round(manifest.totalBytes / 1024)} KB)`);
|
|
369
|
+
if (manifest.updatedAt) {
|
|
370
|
+
lines.push(`Updated: ${new Date(manifest.updatedAt).toISOString()}` +
|
|
371
|
+
(manifest.previousVersion ? ` (from v${manifest.previousVersion})` : ''));
|
|
372
|
+
}
|
|
373
|
+
if (record.integrity === 'modified') {
|
|
374
|
+
lines.push('Integrity: SKILL.md has been modified since install');
|
|
375
|
+
}
|
|
376
|
+
const tamper = checkManifest(record.dir, manifest);
|
|
377
|
+
if (tamper.missing.length > 0)
|
|
378
|
+
lines.push(`Missing files: ${tamper.missing.join(', ')}`);
|
|
379
|
+
if (tamper.modified.length > 0)
|
|
380
|
+
lines.push(`Modified files: ${tamper.modified.join(', ')}`);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
lines.push('Source: hand-authored (no install manifest)');
|
|
384
|
+
}
|
|
385
|
+
if (record.shadowed.length > 0) {
|
|
386
|
+
lines.push(`Shadows: ${record.shadowed.map((s) => `${s.scope}:${s.dir}`).join(', ')}`);
|
|
387
|
+
}
|
|
388
|
+
for (const issue of record.issues)
|
|
389
|
+
lines.push(`${issue.level}: ${issue.code} - ${issue.message}`);
|
|
390
|
+
if (service.getRegistry().isActive(record.name))
|
|
391
|
+
lines.push('Loaded in this conversation.');
|
|
392
|
+
ctx.notify('info', lines.join('\n'));
|
|
393
|
+
}
|
|
394
|
+
async function installFromCommand(ctx, service, args, version) {
|
|
395
|
+
const { rest: source, scope } = parseScopeFlag(args);
|
|
396
|
+
if (!source) {
|
|
397
|
+
ctx.notify('warn', 'Usage: /skills install <source> [--scope user|project]');
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
ctx.toast('info', `Installing ${source}${marksFor(ctx).glyphs.ellipsis}`);
|
|
401
|
+
const result = await installSkill(service, source, {
|
|
402
|
+
...(scope ? { scope } : {}),
|
|
403
|
+
initiator: 'user',
|
|
404
|
+
cwd: ctx.controller.getCwd(),
|
|
405
|
+
installer: version,
|
|
406
|
+
});
|
|
407
|
+
if (!result.ok) {
|
|
408
|
+
ctx.notify('error', `Install failed: ${result.error}`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
ctx.refreshSkills();
|
|
412
|
+
ctx.notify('info', `Installed "${result.name}" v${result.version} into ${result.scope} scope (${result.fileCount} files) at ${result.dir}.`);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* `/skills update <name> [--force]` / `/skills update --all`.
|
|
416
|
+
*
|
|
417
|
+
* `--dry-run` is intentionally absent from the slash surface (§6.5): a list of
|
|
418
|
+
* paths that *would* change is worth reading in a terminal you can scroll, and
|
|
419
|
+
* is not worth another flag to explain in a TUI notice.
|
|
420
|
+
*/
|
|
421
|
+
async function updateFromCommand(ctx, service, args, version) {
|
|
422
|
+
const force = /--force\b/.test(args);
|
|
423
|
+
const rest = args.replace(/--force\b/g, '').trim();
|
|
424
|
+
const all = /^--all\b/.test(rest);
|
|
425
|
+
const name = all ? '' : rest;
|
|
426
|
+
if (!all && !name) {
|
|
427
|
+
ctx.notify('warn', 'Usage: /skills update <name> [--force] | /skills update --all');
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const opts = {
|
|
431
|
+
cwd: ctx.controller.getCwd(),
|
|
432
|
+
installer: version,
|
|
433
|
+
...(force ? { force: true } : {}),
|
|
434
|
+
initiator: 'user',
|
|
435
|
+
};
|
|
436
|
+
// Arrows come from the resolved glyph table, never inline (§4.1 tier B): a
|
|
437
|
+
// legacy cmd.exe renders a hardcoded U+2192 as mojibake, and `glyphs.test.ts`
|
|
438
|
+
// fails the build for it.
|
|
439
|
+
const { glyphs } = marksFor(ctx);
|
|
440
|
+
if (!all) {
|
|
441
|
+
ctx.toast('info', `Updating ${name}${glyphs.ellipsis}`);
|
|
442
|
+
const result = await updateSkill(service, name, opts);
|
|
443
|
+
if (!result.ok) {
|
|
444
|
+
ctx.notify(isHardFailure(result) ? 'error' : 'warn', result.error ?? `Could not update "${name}".`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!result.changed) {
|
|
448
|
+
ctx.notify('info', `${result.name} is already up to date (v${result.fromVersion}).`);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
ctx.refreshSkills();
|
|
452
|
+
ctx.notify('info', `Updated ${result.name} ${result.fromVersion} ${glyphs.arrowRight} ${result.toVersion}.`);
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
ctx.toast('info', `Updating all skills${glyphs.ellipsis}`);
|
|
456
|
+
const results = await updateAllSkills(service, opts);
|
|
457
|
+
if (results.length === 0) {
|
|
458
|
+
ctx.notify('info', 'No skills have an upstream to update from.');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const updated = results.filter((r) => r.ok && r.changed);
|
|
462
|
+
const unchanged = results.filter((r) => r.ok && !r.changed).length;
|
|
463
|
+
const failed = results.filter(isHardFailure).length;
|
|
464
|
+
if (updated.length > 0)
|
|
465
|
+
ctx.refreshSkills();
|
|
466
|
+
ctx.notify('info', [
|
|
467
|
+
'Skill updates:',
|
|
468
|
+
...results.map((r) => {
|
|
469
|
+
if (r.ok && r.changed) {
|
|
470
|
+
return ` ${r.name}: updated ${r.fromVersion} ${glyphs.arrowRight} ${r.toVersion}`;
|
|
471
|
+
}
|
|
472
|
+
if (r.ok)
|
|
473
|
+
return ` ${r.name}: up to date`;
|
|
474
|
+
return ` ${r.name}: skipped - ${r.error?.split('\n')[0] ?? r.reason}`;
|
|
475
|
+
}),
|
|
476
|
+
'',
|
|
477
|
+
`Updated ${updated.length}, unchanged ${unchanged}, ` +
|
|
478
|
+
`skipped ${results.length - updated.length - unchanged - failed}, failed ${failed}.`,
|
|
479
|
+
].join('\n'));
|
|
480
|
+
}
|
|
481
|
+
async function removeFromCommand(ctx, service, name) {
|
|
482
|
+
if (!name) {
|
|
483
|
+
ctx.notify('warn', 'Usage: /skills remove <name>');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const result = await removeSkill(service, name);
|
|
487
|
+
if (!result.ok) {
|
|
488
|
+
ctx.notify('error', result.error ?? `Could not remove "${name}".`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
ctx.refreshSkills();
|
|
492
|
+
ctx.toast('success', `Removed skill "${name}".`);
|
|
493
|
+
}
|
|
494
|
+
function toggleSkill(ctx, service, name, disable) {
|
|
495
|
+
if (!name) {
|
|
496
|
+
ctx.notify('warn', `Usage: /skills ${disable ? 'disable' : 'enable'} <name>`);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (!service.get(name)) {
|
|
500
|
+
ctx.notify('warn', `No skill named "${name}".`);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
service.setDisabled(name, disable);
|
|
504
|
+
ctx.refreshSkills();
|
|
505
|
+
ctx.toast('success', `Skill "${name}" ${disable ? 'disabled' : 'enabled'}.`);
|
|
506
|
+
}
|
|
507
|
+
function scaffoldSkill(ctx, service, name) {
|
|
508
|
+
if (!name) {
|
|
509
|
+
ctx.notify('warn', 'Usage: /skills create <name>');
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const root = writableRootFor('user', ctx.controller.getCwd());
|
|
513
|
+
if (!root) {
|
|
514
|
+
ctx.notify('error', 'No writable skills directory available.');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
// Deliberately a scaffold, not an interactive wizard (§7.2): the useful next
|
|
519
|
+
// step is opening the file in the user's own editor, not answering prompts
|
|
520
|
+
// in a TUI that has no text-area affordance.
|
|
521
|
+
const dir = writeScaffold(root, name);
|
|
522
|
+
service.reload();
|
|
523
|
+
ctx.refreshSkills();
|
|
524
|
+
ctx.notify('info', `Created a skill skeleton at ${dir}. Edit SKILL.md, then run /skills reload.`);
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
ctx.notify('error', `Could not create the skill: ${err.message}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function writeScaffold(root, name) {
|
|
531
|
+
const dir = join(root, name);
|
|
532
|
+
mkdirSync(dir, { recursive: true });
|
|
533
|
+
writeFileSync(join(dir, 'SKILL.md'), renderSkillMarkdown({
|
|
534
|
+
name,
|
|
535
|
+
description: `TODO: what this skill does AND when to use it - the model sees only this line.`,
|
|
536
|
+
body: ['# ' + name, '', '## When to use', '', 'TODO', '', '## Steps', '', '1. TODO'].join('\n'),
|
|
537
|
+
}), 'utf-8');
|
|
538
|
+
return dir;
|
|
539
|
+
}
|
|
540
|
+
function trustFromCommand(ctx, service, dir, untrust) {
|
|
541
|
+
if (!dir) {
|
|
542
|
+
ctx.notify('warn', `Usage: /skills ${untrust ? 'untrust' : 'trust'} <dir>`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const ok = untrust ? service.untrustDir(dir) : service.trustDir(dir);
|
|
546
|
+
if (!ok) {
|
|
547
|
+
ctx.notify('warn', untrust ? `"${dir}" was not trusted.` : `Could not resolve "${dir}".`);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
service.reload();
|
|
551
|
+
ctx.refreshSkills();
|
|
552
|
+
ctx.toast('success', `${untrust ? 'Untrusted' : 'Trusted'} ${dir}.`);
|
|
553
|
+
}
|
|
554
|
+
// ---------------------------------------------------------------------------
|
|
555
|
+
// Registration
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
export function makeSkillsCommand(service, version) {
|
|
558
|
+
return {
|
|
559
|
+
name: 'skills',
|
|
560
|
+
description: 'List and manage skills',
|
|
561
|
+
run: (ctx) => runSkills(ctx, service, version),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Register one dynamic command per usable skill.
|
|
566
|
+
*
|
|
567
|
+
* CONFLICT RULE (D6 / P1-2): the namespaced `/skill:<name>` form is ALWAYS
|
|
568
|
+
* registered — documentation and scripts need a stable handle — while the bare
|
|
569
|
+
* `/<name>` is only taken when `registry.get(name)` is undefined.
|
|
570
|
+
*
|
|
571
|
+
* The probe MUST be `get()`, never `all().map(c => c.name)`. `all()` de-dupes
|
|
572
|
+
* by command OBJECT, so it returns primary names only and silently omits every
|
|
573
|
+
* alias. `quit` is an alias of `exit`, and `register()` overwrites
|
|
574
|
+
* unconditionally — so a third-party skill named `quit` would take over `/quit`.
|
|
575
|
+
* D6 calls hijacking the user's muscle memory a security boundary, not a
|
|
576
|
+
* papercut, and `get()` is the only probe that sees the alias table.
|
|
577
|
+
*/
|
|
578
|
+
export function registerSkillCommands(registry, service) {
|
|
579
|
+
for (const record of service.list()) {
|
|
580
|
+
if (record.disabled || record.invalid)
|
|
581
|
+
continue;
|
|
582
|
+
if (record.frontmatter.activation === 'manual')
|
|
583
|
+
continue;
|
|
584
|
+
const command = makeSkillCommand(record, service);
|
|
585
|
+
registry.register({ ...command, name: `skill:${record.name}` });
|
|
586
|
+
if (registry.get(record.name) === undefined) {
|
|
587
|
+
registry.register({ ...command, name: record.name });
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function makeSkillCommand(record, service) {
|
|
592
|
+
return {
|
|
593
|
+
name: record.name,
|
|
594
|
+
description: shortDescription(record),
|
|
595
|
+
run: (ctx) => {
|
|
596
|
+
let loaded;
|
|
597
|
+
try {
|
|
598
|
+
loaded = service.loadBody(record.name);
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
ctx.notify('error', `Could not load skill "${record.name}": ${err.message}`);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const hydrated = { ...record, body: loaded.body, files: loaded.files };
|
|
605
|
+
service.getRegistry().activate(record.name);
|
|
606
|
+
// QUEUE, do not enter (D-G2). `ctx.submit()` below turns into the user
|
|
607
|
+
// message that OPENS the next turn, and `controller.prompt()` clears the
|
|
608
|
+
// frame on its way in — so entering here would leave the one turn the user
|
|
609
|
+
// explicitly asked this skill to run as the only unconstrained one.
|
|
610
|
+
service.queueFrame(record.name);
|
|
611
|
+
const policy = service.pendingToolPolicyView(() => ctx.controller.listTools().map((t) => t.name));
|
|
612
|
+
const message = renderSkillInvocation(hydrated, ctx.args, {
|
|
613
|
+
// The same knob as the tool path, at last (GG3 / FG3).
|
|
614
|
+
bodyMaxBytes: service.bodyMaxBytes(),
|
|
615
|
+
platform: currentPlatform(),
|
|
616
|
+
...(policy ? { policy } : {}),
|
|
617
|
+
});
|
|
618
|
+
// The SUBMITTED size, not the file's. Reporting 480 KB while sending 30 KB
|
|
619
|
+
// is how a user concludes the truncation marker is a rendering glitch.
|
|
620
|
+
const kb = Math.max(1, Math.round(Buffer.byteLength(message, 'utf-8') / 1024));
|
|
621
|
+
ctx.notify('info', `Skill "${record.name}" loaded (${kb} KB submitted, ${loaded.files.length} bundled files).`);
|
|
622
|
+
ctx.submit(message);
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `env-paths` application name, in one place.
|
|
3
|
+
*
|
|
4
|
+
* `config/store.ts` and `skills/paths.ts` each used to carry their own copy of
|
|
5
|
+
* this string literal. Two copies of a name is exactly the shape that lets a
|
|
6
|
+
* rename land in one of them and not the other, and the failure is silent: the
|
|
7
|
+
* config file and the skills directory simply stop agreeing about where the
|
|
8
|
+
* user's data lives.
|
|
9
|
+
*/
|
|
10
|
+
export declare const APP_NAME = "aragon-agent";
|
|
11
|
+
export declare const appPaths: import("env-paths").Paths;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `env-paths` application name, in one place.
|
|
3
|
+
*
|
|
4
|
+
* `config/store.ts` and `skills/paths.ts` each used to carry their own copy of
|
|
5
|
+
* this string literal. Two copies of a name is exactly the shape that lets a
|
|
6
|
+
* rename land in one of them and not the other, and the failure is silent: the
|
|
7
|
+
* config file and the skills directory simply stop agreeing about where the
|
|
8
|
+
* user's data lives.
|
|
9
|
+
*/
|
|
10
|
+
import envPaths from 'env-paths';
|
|
11
|
+
export const APP_NAME = 'aragon-agent';
|
|
12
|
+
export const appPaths = envPaths(APP_NAME);
|
|
13
|
+
// The pre-rename name lives in `migrate-legacy-state.ts`, not here. It has
|
|
14
|
+
// exactly one consumer, it is transitional, and keeping it out of this module
|
|
15
|
+
// means retiring the migration some day is a single-file delete rather than an
|
|
16
|
+
// edit that has to remember to come back through here.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment + `.env` loading and mapping to config fields.
|
|
3
|
+
*
|
|
4
|
+
* Recognized keys (spec §3.5):
|
|
5
|
+
* - Provider secrets: ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_API_KEY /
|
|
6
|
+
* GEMINI_API_KEY.
|
|
7
|
+
* - Overrides: ARAGON_PROVIDER, ARAGON_MODEL, ARAGON_BASE_URL, ARAGON_THINKING,
|
|
8
|
+
* ARAGON_MAX_TOKENS, ARAGON_THEME.
|
|
9
|
+
*/
|
|
10
|
+
import { type PersistedConfig } from './schema.js';
|
|
11
|
+
/**
|
|
12
|
+
* Load a project `.env` from the given directory into `process.env` (existing
|
|
13
|
+
* process env wins — dotenv never overrides already-set variables).
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadDotenv(cwd: string): void;
|
|
16
|
+
export interface EnvConfig {
|
|
17
|
+
/** Non-secret overrides sourced from ARAGON_* env vars. */
|
|
18
|
+
partial: Partial<PersistedConfig>;
|
|
19
|
+
/** Resolved provider -> key from provider env vars. */
|
|
20
|
+
apiKeys: Record<string, string | undefined>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read ARAGON_* overrides and provider secrets from the current environment.
|
|
24
|
+
* Must be called after `loadDotenv()` so `.env` values are visible.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readEnvConfig(): EnvConfig;
|
|
27
|
+
/** Skill names disabled for this run via `ARAGON_SKILLS_DISABLED` (comma-separated). */
|
|
28
|
+
export declare function readEnvDisabledSkills(): string[];
|