@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,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local skill-usage counters (spec §9) — the input that decides which skills
|
|
3
|
+
* survive Level 1 truncation.
|
|
4
|
+
*
|
|
5
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* WHAT IS STORED: skill name, use count, last-used timestamp. NOTHING ELSE.
|
|
7
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
*
|
|
9
|
+
* No arguments, no conversation content, no working directory, no machine id.
|
|
10
|
+
* It never leaves this machine and it is never attached to any request. The
|
|
11
|
+
* whole file can be deleted at any time — the only consequence is that the
|
|
12
|
+
* catalog falls back to its pre-ranking order (I-A1). `skills.usageTracking:
|
|
13
|
+
* false` switches it off entirely, and then nothing here reads or writes disk.
|
|
14
|
+
*
|
|
15
|
+
* FAILURE POLICY (D-A14): every function swallows its errors. This is ranking
|
|
16
|
+
* metadata for a display list, not business data. A read-only home directory or
|
|
17
|
+
* a half-written JSON file must degrade to "no usage data" — surfacing an error
|
|
18
|
+
* would spend the user's attention on something whose worst outcome is a
|
|
19
|
+
* slightly worse sort order.
|
|
20
|
+
*/
|
|
21
|
+
import type { SkillUsageMap } from '@aragon-agent/core/skills';
|
|
22
|
+
export declare const USAGE_FILENAME = "skill-usage.json";
|
|
23
|
+
export declare const USAGE_SCHEMA = 1;
|
|
24
|
+
/** Coalescing window for disk writes. Losing this much on exit costs nothing. */
|
|
25
|
+
export declare const USAGE_FLUSH_DEBOUNCE_MS = 2000;
|
|
26
|
+
/** Entries kept before pruning kicks in. */
|
|
27
|
+
export declare const USAGE_MAX_ENTRIES = 500;
|
|
28
|
+
/**
|
|
29
|
+
* `<data>/skill-usage.json` — deliberately a SIBLING of `<data>/skills/`, not a
|
|
30
|
+
* child. The skills directory is a scan root; every extra file inside it is one
|
|
31
|
+
* more thing the discovery walk has to recognise and skip.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getUsagePath(): string;
|
|
34
|
+
/** Read the usage map, caching it for the life of the process. */
|
|
35
|
+
export declare function loadUsage(): SkillUsageMap;
|
|
36
|
+
/**
|
|
37
|
+
* Drop entries for skills that no longer exist, then — if still over the cap —
|
|
38
|
+
* the least recently used ones. Called opportunistically, never on a hot path.
|
|
39
|
+
*/
|
|
40
|
+
export declare function pruneUsage(knownNames: string[]): void;
|
|
41
|
+
/** Record one deliberate use of a skill. Cheap, in-memory, never throws. */
|
|
42
|
+
export declare function recordUse(name: string, now?: number): void;
|
|
43
|
+
/** Write the map to disk now, if anything changed. Safe to call repeatedly. */
|
|
44
|
+
export declare function flushUsage(): void;
|
|
45
|
+
export interface UsageRow {
|
|
46
|
+
name: string;
|
|
47
|
+
useCount: number;
|
|
48
|
+
lastUsedAt: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Every counter on disk, most recently used first (ties broken by name).
|
|
52
|
+
*
|
|
53
|
+
* Reads even when `skills.usageTracking` is off: switching tracking off stops
|
|
54
|
+
* new writes, but a user who wants to know what is already stored — and then
|
|
55
|
+
* delete it — must not be told "nothing here" while the file still exists.
|
|
56
|
+
*/
|
|
57
|
+
export declare function listUsage(): UsageRow[];
|
|
58
|
+
/**
|
|
59
|
+
* Delete the file, empty the cache, cancel any pending write, and clear `dirty`.
|
|
60
|
+
* Returns how many entries were removed. Never throws (D-A14).
|
|
61
|
+
*
|
|
62
|
+
* CLEARING `dirty` IS NOT OPTIONAL. `recordUse()` sets it, and `flushUsage()`
|
|
63
|
+
* only returns early when `!dirty || memory === null`. Reset the map without
|
|
64
|
+
* resetting the flag and the very next flush — a scheduled one, or the explicit
|
|
65
|
+
* call at the end of a CLI command — rebuilds the file from an empty map. The
|
|
66
|
+
* user then sees `skill-usage.json` reappear seconds after deleting it, and only
|
|
67
|
+
* under certain call orders, which is the worst possible way to learn that
|
|
68
|
+
* "erase my data" did not.
|
|
69
|
+
*/
|
|
70
|
+
export declare function resetUsage(): number;
|
|
71
|
+
/** Test-only: forget the cached map and any pending write. */
|
|
72
|
+
export declare function resetUsageForTests(): void;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local skill-usage counters (spec §9) — the input that decides which skills
|
|
3
|
+
* survive Level 1 truncation.
|
|
4
|
+
*
|
|
5
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* WHAT IS STORED: skill name, use count, last-used timestamp. NOTHING ELSE.
|
|
7
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
*
|
|
9
|
+
* No arguments, no conversation content, no working directory, no machine id.
|
|
10
|
+
* It never leaves this machine and it is never attached to any request. The
|
|
11
|
+
* whole file can be deleted at any time — the only consequence is that the
|
|
12
|
+
* catalog falls back to its pre-ranking order (I-A1). `skills.usageTracking:
|
|
13
|
+
* false` switches it off entirely, and then nothing here reads or writes disk.
|
|
14
|
+
*
|
|
15
|
+
* FAILURE POLICY (D-A14): every function swallows its errors. This is ranking
|
|
16
|
+
* metadata for a display list, not business data. A read-only home directory or
|
|
17
|
+
* a half-written JSON file must degrade to "no usage data" — surfacing an error
|
|
18
|
+
* would spend the user's attention on something whose worst outcome is a
|
|
19
|
+
* slightly worse sort order.
|
|
20
|
+
*/
|
|
21
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { getUserDataDir } from './paths.js';
|
|
24
|
+
export const USAGE_FILENAME = 'skill-usage.json';
|
|
25
|
+
export const USAGE_SCHEMA = 1;
|
|
26
|
+
/** Coalescing window for disk writes. Losing this much on exit costs nothing. */
|
|
27
|
+
export const USAGE_FLUSH_DEBOUNCE_MS = 2000;
|
|
28
|
+
/** Entries kept before pruning kicks in. */
|
|
29
|
+
export const USAGE_MAX_ENTRIES = 500;
|
|
30
|
+
/**
|
|
31
|
+
* `<data>/skill-usage.json` — deliberately a SIBLING of `<data>/skills/`, not a
|
|
32
|
+
* child. The skills directory is a scan root; every extra file inside it is one
|
|
33
|
+
* more thing the discovery walk has to recognise and skip.
|
|
34
|
+
*/
|
|
35
|
+
export function getUsagePath() {
|
|
36
|
+
return join(getUserDataDir(), USAGE_FILENAME);
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// In-memory state
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
let memory = null;
|
|
42
|
+
let dirty = false;
|
|
43
|
+
let timer = null;
|
|
44
|
+
function isStat(value) {
|
|
45
|
+
if (!value || typeof value !== 'object')
|
|
46
|
+
return false;
|
|
47
|
+
const v = value;
|
|
48
|
+
return typeof v.useCount === 'number' && typeof v.lastUsedAt === 'number';
|
|
49
|
+
}
|
|
50
|
+
function readFromDisk() {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = JSON.parse(readFileSync(getUsagePath(), 'utf-8'));
|
|
53
|
+
if (!parsed || typeof parsed !== 'object')
|
|
54
|
+
return {};
|
|
55
|
+
if (parsed.schema !== USAGE_SCHEMA)
|
|
56
|
+
return {};
|
|
57
|
+
if (!parsed.skills || typeof parsed.skills !== 'object')
|
|
58
|
+
return {};
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const [name, stat] of Object.entries(parsed.skills)) {
|
|
61
|
+
// A hand-edited file is a normal thing to encounter; keep the rows that
|
|
62
|
+
// still make sense and drop the rest rather than discarding everything.
|
|
63
|
+
if (!isStat(stat))
|
|
64
|
+
continue;
|
|
65
|
+
out[name] = {
|
|
66
|
+
useCount: Math.max(0, Math.floor(stat.useCount)),
|
|
67
|
+
lastUsedAt: Math.max(0, Math.floor(stat.lastUsedAt)),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Missing / unreadable / corrupt — all the same answer, and the file is
|
|
74
|
+
// left alone: the user may have been editing it, and deleting their work to
|
|
75
|
+
// tidy up our own state would be a strictly worse trade.
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Read the usage map, caching it for the life of the process. */
|
|
80
|
+
export function loadUsage() {
|
|
81
|
+
if (memory === null)
|
|
82
|
+
memory = readFromDisk();
|
|
83
|
+
return memory;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Drop entries for skills that no longer exist, then — if still over the cap —
|
|
87
|
+
* the least recently used ones. Called opportunistically, never on a hot path.
|
|
88
|
+
*/
|
|
89
|
+
export function pruneUsage(knownNames) {
|
|
90
|
+
const map = loadUsage();
|
|
91
|
+
const known = new Set(knownNames);
|
|
92
|
+
let changed = false;
|
|
93
|
+
for (const name of Object.keys(map)) {
|
|
94
|
+
if (!known.has(name)) {
|
|
95
|
+
delete map[name];
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const names = Object.keys(map);
|
|
100
|
+
if (names.length > USAGE_MAX_ENTRIES) {
|
|
101
|
+
const oldestFirst = names.sort((a, b) => (map[a]?.lastUsedAt ?? 0) - (map[b]?.lastUsedAt ?? 0));
|
|
102
|
+
for (const name of oldestFirst.slice(0, names.length - USAGE_MAX_ENTRIES)) {
|
|
103
|
+
delete map[name];
|
|
104
|
+
changed = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (changed) {
|
|
108
|
+
dirty = true;
|
|
109
|
+
scheduleFlush();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** Record one deliberate use of a skill. Cheap, in-memory, never throws. */
|
|
113
|
+
export function recordUse(name, now = Date.now()) {
|
|
114
|
+
if (name.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
const map = loadUsage();
|
|
117
|
+
const current = map[name];
|
|
118
|
+
map[name] = {
|
|
119
|
+
useCount: (current?.useCount ?? 0) + 1,
|
|
120
|
+
lastUsedAt: now,
|
|
121
|
+
};
|
|
122
|
+
dirty = true;
|
|
123
|
+
scheduleFlush();
|
|
124
|
+
}
|
|
125
|
+
function scheduleFlush() {
|
|
126
|
+
if (timer !== null)
|
|
127
|
+
return;
|
|
128
|
+
timer = setTimeout(() => {
|
|
129
|
+
timer = null;
|
|
130
|
+
flushUsage();
|
|
131
|
+
}, USAGE_FLUSH_DEBOUNCE_MS);
|
|
132
|
+
// A pending counter write must never be the reason a one-shot `aragon skills
|
|
133
|
+
// list` sits there for two seconds before exiting.
|
|
134
|
+
timer.unref?.();
|
|
135
|
+
}
|
|
136
|
+
/** Write the map to disk now, if anything changed. Safe to call repeatedly. */
|
|
137
|
+
export function flushUsage() {
|
|
138
|
+
if (timer !== null) {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
timer = null;
|
|
141
|
+
}
|
|
142
|
+
if (!dirty || memory === null)
|
|
143
|
+
return;
|
|
144
|
+
const path = getUsagePath();
|
|
145
|
+
// Same tmp+rename discipline the config store uses: a process killed
|
|
146
|
+
// mid-write must not leave a truncated file that reads back as "no usage".
|
|
147
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
148
|
+
try {
|
|
149
|
+
mkdirSync(getUserDataDir(), { recursive: true });
|
|
150
|
+
const payload = { schema: USAGE_SCHEMA, skills: memory };
|
|
151
|
+
writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
|
|
152
|
+
renameSync(tmp, path);
|
|
153
|
+
dirty = false;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
try {
|
|
157
|
+
rmSync(tmp, { force: true });
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// Nothing further to try; the counters simply stay in memory.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Every counter on disk, most recently used first (ties broken by name).
|
|
166
|
+
*
|
|
167
|
+
* Reads even when `skills.usageTracking` is off: switching tracking off stops
|
|
168
|
+
* new writes, but a user who wants to know what is already stored — and then
|
|
169
|
+
* delete it — must not be told "nothing here" while the file still exists.
|
|
170
|
+
*/
|
|
171
|
+
export function listUsage() {
|
|
172
|
+
const map = loadUsage();
|
|
173
|
+
return Object.entries(map)
|
|
174
|
+
.map(([name, stat]) => ({ name, useCount: stat.useCount, lastUsedAt: stat.lastUsedAt }))
|
|
175
|
+
.sort((a, b) => b.lastUsedAt !== a.lastUsedAt
|
|
176
|
+
? b.lastUsedAt - a.lastUsedAt
|
|
177
|
+
: a.name.localeCompare(b.name, 'en'));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Delete the file, empty the cache, cancel any pending write, and clear `dirty`.
|
|
181
|
+
* Returns how many entries were removed. Never throws (D-A14).
|
|
182
|
+
*
|
|
183
|
+
* CLEARING `dirty` IS NOT OPTIONAL. `recordUse()` sets it, and `flushUsage()`
|
|
184
|
+
* only returns early when `!dirty || memory === null`. Reset the map without
|
|
185
|
+
* resetting the flag and the very next flush — a scheduled one, or the explicit
|
|
186
|
+
* call at the end of a CLI command — rebuilds the file from an empty map. The
|
|
187
|
+
* user then sees `skill-usage.json` reappear seconds after deleting it, and only
|
|
188
|
+
* under certain call orders, which is the worst possible way to learn that
|
|
189
|
+
* "erase my data" did not.
|
|
190
|
+
*/
|
|
191
|
+
export function resetUsage() {
|
|
192
|
+
const removed = Object.keys(loadUsage()).length;
|
|
193
|
+
if (timer !== null) {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
timer = null;
|
|
196
|
+
}
|
|
197
|
+
memory = {};
|
|
198
|
+
dirty = false;
|
|
199
|
+
try {
|
|
200
|
+
rmSync(getUsagePath(), { force: true });
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Same policy as everywhere else in this file: a counter file we could not
|
|
204
|
+
// delete is not worth an error the user can do nothing about.
|
|
205
|
+
}
|
|
206
|
+
return removed;
|
|
207
|
+
}
|
|
208
|
+
/** Test-only: forget the cached map and any pending write. */
|
|
209
|
+
export function resetUsageForTests() {
|
|
210
|
+
if (timer !== null) {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
timer = null;
|
|
213
|
+
}
|
|
214
|
+
memory = null;
|
|
215
|
+
dirty = false;
|
|
216
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bash tool — run a shell command at full permission (spec §3.6).
|
|
3
|
+
*
|
|
4
|
+
* Shell selection: Windows uses `process.env.ComSpec` (cmd.exe by default);
|
|
5
|
+
* POSIX uses `/bin/sh -c`. The command's stdout+stderr and exit code are
|
|
6
|
+
* captured. The `timeout?` param can only SHRINK within the executor ceiling
|
|
7
|
+
* (the core ToolExecutor owns the real timeout + 100 KB truncation — R4), so
|
|
8
|
+
* this tool does not re-implement an output cap.
|
|
9
|
+
*/
|
|
10
|
+
import { type AgentTool } from '@aragon-agent/core';
|
|
11
|
+
import type { ToolDeps } from './fs-tools.js';
|
|
12
|
+
export declare function makeBash(deps: ToolDeps): AgentTool;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bash tool — run a shell command at full permission (spec §3.6).
|
|
3
|
+
*
|
|
4
|
+
* Shell selection: Windows uses `process.env.ComSpec` (cmd.exe by default);
|
|
5
|
+
* POSIX uses `/bin/sh -c`. The command's stdout+stderr and exit code are
|
|
6
|
+
* captured. The `timeout?` param can only SHRINK within the executor ceiling
|
|
7
|
+
* (the core ToolExecutor owns the real timeout + 100 KB truncation — R4), so
|
|
8
|
+
* this tool does not re-implement an output cap.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
import { defineTool, errorResult, textResult, } from '@aragon-agent/core';
|
|
13
|
+
/**
|
|
14
|
+
* Kill the whole process tree. With `shell:true` the direct child is the shell
|
|
15
|
+
* wrapper, so on Windows we must `taskkill /t` to reach the grandchild; a plain
|
|
16
|
+
* `child.kill()` would orphan it (leaving pipes open and cwd locked).
|
|
17
|
+
*/
|
|
18
|
+
function killTree(child) {
|
|
19
|
+
if (child.pid === undefined)
|
|
20
|
+
return;
|
|
21
|
+
if (process.platform === 'win32') {
|
|
22
|
+
try {
|
|
23
|
+
spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { windowsHide: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
try {
|
|
27
|
+
child.kill('SIGKILL');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* ignore */
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
try {
|
|
36
|
+
child.kill('SIGKILL');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* ignore */
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function makeBash(deps) {
|
|
44
|
+
return defineTool({
|
|
45
|
+
name: 'bash',
|
|
46
|
+
label: 'Shell',
|
|
47
|
+
description: 'Run a shell command at full permission and return its combined ' +
|
|
48
|
+
'stdout+stderr and exit code. `timeout` (ms) may only shrink within the ' +
|
|
49
|
+
'agent tool-timeout ceiling; `cwd` defaults to the working directory. ' +
|
|
50
|
+
'Emit shell syntax compatible with the OS/shell stated in the system prompt.',
|
|
51
|
+
parameters: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
command: { type: 'string', description: 'The shell command to run.' },
|
|
55
|
+
timeout: { type: 'number', description: 'Optional timeout in milliseconds (shrinks within the ceiling).' },
|
|
56
|
+
cwd: { type: 'string', description: 'Working directory for this command.' },
|
|
57
|
+
},
|
|
58
|
+
required: ['command'],
|
|
59
|
+
},
|
|
60
|
+
async execute(_id, rawParams, ctx) {
|
|
61
|
+
const params = rawParams;
|
|
62
|
+
const command = params.command;
|
|
63
|
+
const cwd = params.cwd ? params.cwd : deps.getCwd();
|
|
64
|
+
return new Promise((resolvePromise) => {
|
|
65
|
+
let child;
|
|
66
|
+
try {
|
|
67
|
+
// shell:true delegates to ComSpec (Windows) / /bin/sh (POSIX) and
|
|
68
|
+
// handles command-line quoting far more reliably than a hand-built
|
|
69
|
+
// cmd.exe arg array, which double-escapes nested quotes on Windows.
|
|
70
|
+
child = spawn(command, {
|
|
71
|
+
shell: true,
|
|
72
|
+
cwd,
|
|
73
|
+
env: process.env,
|
|
74
|
+
windowsHide: true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
resolvePromise(errorResult(err instanceof Error ? err.message : String(err)));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
let output = '';
|
|
82
|
+
let killedByTimeout = false;
|
|
83
|
+
let settled = false;
|
|
84
|
+
const finish = (fn) => {
|
|
85
|
+
if (settled)
|
|
86
|
+
return;
|
|
87
|
+
settled = true;
|
|
88
|
+
cleanup();
|
|
89
|
+
resolvePromise(fn());
|
|
90
|
+
};
|
|
91
|
+
// Self-timer for the `timeout` param (shrinks within the executor ceiling).
|
|
92
|
+
let timer;
|
|
93
|
+
if (params.timeout && params.timeout > 0) {
|
|
94
|
+
timer = setTimeout(() => {
|
|
95
|
+
killedByTimeout = true;
|
|
96
|
+
killTree(child);
|
|
97
|
+
}, params.timeout);
|
|
98
|
+
}
|
|
99
|
+
// Cooperative cancellation via the executor/agent AbortSignal.
|
|
100
|
+
const onAbort = () => {
|
|
101
|
+
killTree(child);
|
|
102
|
+
};
|
|
103
|
+
ctx.signal?.addEventListener('abort', onAbort, { once: true });
|
|
104
|
+
function cleanup() {
|
|
105
|
+
if (timer)
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
ctx.signal?.removeEventListener('abort', onAbort);
|
|
108
|
+
}
|
|
109
|
+
child.stdout?.on('data', (d) => {
|
|
110
|
+
output += d.toString();
|
|
111
|
+
});
|
|
112
|
+
child.stderr?.on('data', (d) => {
|
|
113
|
+
output += d.toString();
|
|
114
|
+
});
|
|
115
|
+
child.on('error', (err) => {
|
|
116
|
+
finish(() => errorResult(`Failed to run command: ${err.message}`));
|
|
117
|
+
});
|
|
118
|
+
child.on('close', (code, signalName) => {
|
|
119
|
+
const trimmed = output.length > 0 ? output : '(no output)';
|
|
120
|
+
if (killedByTimeout) {
|
|
121
|
+
finish(() => errorResult(`Command timed out after ${params.timeout}ms.\n${trimmed}`));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const status = code === null ? `signal ${signalName}` : `exit code ${code}`;
|
|
125
|
+
const body = `$ ${command}\n${trimmed}\n[${status}]`;
|
|
126
|
+
finish(() => (code === 0 ? textResult(body) : errorResult(body)));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal unified-diff renderer for `edit_file` previews (spec §4).
|
|
3
|
+
*
|
|
4
|
+
* Not a full Myers diff: it trims the common leading/trailing lines and renders
|
|
5
|
+
* the differing middle as removed (`-`) / added (`+`) blocks with a small
|
|
6
|
+
* amount of surrounding context. That is enough for a legible edit preview
|
|
7
|
+
* without pulling in a diff dependency.
|
|
8
|
+
*/
|
|
9
|
+
export interface DiffOptions {
|
|
10
|
+
/** File path shown in the `---`/`+++` header. */
|
|
11
|
+
path?: string;
|
|
12
|
+
/** Context lines around the change (default 2). */
|
|
13
|
+
context?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function renderUnifiedDiff(oldStr: string, newStr: string, options?: DiffOptions): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal unified-diff renderer for `edit_file` previews (spec §4).
|
|
3
|
+
*
|
|
4
|
+
* Not a full Myers diff: it trims the common leading/trailing lines and renders
|
|
5
|
+
* the differing middle as removed (`-`) / added (`+`) blocks with a small
|
|
6
|
+
* amount of surrounding context. That is enough for a legible edit preview
|
|
7
|
+
* without pulling in a diff dependency.
|
|
8
|
+
*/
|
|
9
|
+
const CONTEXT_LINES = 2;
|
|
10
|
+
export function renderUnifiedDiff(oldStr, newStr, options = {}) {
|
|
11
|
+
const context = options.context ?? CONTEXT_LINES;
|
|
12
|
+
const oldLines = oldStr.split('\n');
|
|
13
|
+
const newLines = newStr.split('\n');
|
|
14
|
+
// Common leading lines.
|
|
15
|
+
let start = 0;
|
|
16
|
+
const maxStart = Math.min(oldLines.length, newLines.length);
|
|
17
|
+
while (start < maxStart && oldLines[start] === newLines[start])
|
|
18
|
+
start += 1;
|
|
19
|
+
// Common trailing lines (not overlapping the common prefix).
|
|
20
|
+
let endOld = oldLines.length - 1;
|
|
21
|
+
let endNew = newLines.length - 1;
|
|
22
|
+
while (endOld >= start &&
|
|
23
|
+
endNew >= start &&
|
|
24
|
+
oldLines[endOld] === newLines[endNew]) {
|
|
25
|
+
endOld -= 1;
|
|
26
|
+
endNew -= 1;
|
|
27
|
+
}
|
|
28
|
+
const header = options.path
|
|
29
|
+
? [`--- ${options.path}`, `+++ ${options.path}`]
|
|
30
|
+
: [];
|
|
31
|
+
const lines = [];
|
|
32
|
+
const ctxStart = Math.max(0, start - context);
|
|
33
|
+
for (let i = ctxStart; i < start; i += 1) {
|
|
34
|
+
lines.push(` ${oldLines[i]}`);
|
|
35
|
+
}
|
|
36
|
+
for (let i = start; i <= endOld; i += 1) {
|
|
37
|
+
lines.push(`- ${oldLines[i]}`);
|
|
38
|
+
}
|
|
39
|
+
for (let i = start; i <= endNew; i += 1) {
|
|
40
|
+
lines.push(`+ ${newLines[i]}`);
|
|
41
|
+
}
|
|
42
|
+
const ctxEnd = Math.min(oldLines.length - 1, endOld + context);
|
|
43
|
+
for (let i = endOld + 1; i <= ctxEnd; i += 1) {
|
|
44
|
+
lines.push(` ${oldLines[i]}`);
|
|
45
|
+
}
|
|
46
|
+
if (lines.length === 0) {
|
|
47
|
+
return `${header.join('\n')}${header.length ? '\n' : ''}(no textual change)`;
|
|
48
|
+
}
|
|
49
|
+
return [...header, ...lines].join('\n');
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem tools: read_file / write_file / edit_file / list_dir (spec §3.6).
|
|
3
|
+
*
|
|
4
|
+
* All relative paths resolve against the session cwd (via the injected
|
|
5
|
+
* `getCwd`). Every tool returns `errorResult` on bad input — it never throws.
|
|
6
|
+
* Timeout + 100 KB truncation are owned by the core ToolExecutor (R4); these
|
|
7
|
+
* tools do not re-implement them.
|
|
8
|
+
*/
|
|
9
|
+
import { type AgentTool } from '@aragon-agent/core';
|
|
10
|
+
export interface ToolDeps {
|
|
11
|
+
getCwd: () => string;
|
|
12
|
+
}
|
|
13
|
+
export declare function makeReadFile(deps: ToolDeps): AgentTool;
|
|
14
|
+
export declare function makeWriteFile(deps: ToolDeps): AgentTool;
|
|
15
|
+
export declare function makeEditFile(deps: ToolDeps): AgentTool;
|
|
16
|
+
export declare function makeListDir(deps: ToolDeps): AgentTool;
|