@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.
Files changed (156) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +558 -0
  3. package/dist/agent/coalesce.d.ts +14 -0
  4. package/dist/agent/coalesce.js +41 -0
  5. package/dist/agent/controller.d.ts +132 -0
  6. package/dist/agent/controller.js +332 -0
  7. package/dist/agent/headless.d.ts +22 -0
  8. package/dist/agent/headless.js +74 -0
  9. package/dist/agent/reducer.d.ts +181 -0
  10. package/dist/agent/reducer.js +420 -0
  11. package/dist/agent/system-prompt.d.ts +30 -0
  12. package/dist/agent/system-prompt.js +58 -0
  13. package/dist/agent/usage.d.ts +13 -0
  14. package/dist/agent/usage.js +41 -0
  15. package/dist/cli.d.ts +11 -0
  16. package/dist/cli.js +482 -0
  17. package/dist/commands/builtins.d.ts +16 -0
  18. package/dist/commands/builtins.js +223 -0
  19. package/dist/commands/registry.d.ts +48 -0
  20. package/dist/commands/registry.js +45 -0
  21. package/dist/commands/skills.d.ts +22 -0
  22. package/dist/commands/skills.js +625 -0
  23. package/dist/config/app-paths.d.ts +11 -0
  24. package/dist/config/app-paths.js +16 -0
  25. package/dist/config/env.d.ts +28 -0
  26. package/dist/config/env.js +92 -0
  27. package/dist/config/load.d.ts +48 -0
  28. package/dist/config/load.js +158 -0
  29. package/dist/config/migrate-legacy-state.d.ts +78 -0
  30. package/dist/config/migrate-legacy-state.js +181 -0
  31. package/dist/config/schema.d.ts +249 -0
  32. package/dist/config/schema.js +247 -0
  33. package/dist/config/store.d.ts +44 -0
  34. package/dist/config/store.js +126 -0
  35. package/dist/input/keymap.d.ts +20 -0
  36. package/dist/input/keymap.js +85 -0
  37. package/dist/session/persist.d.ts +24 -0
  38. package/dist/session/persist.js +51 -0
  39. package/dist/skills/archive.d.ts +41 -0
  40. package/dist/skills/archive.js +152 -0
  41. package/dist/skills/cli-commands.d.ts +52 -0
  42. package/dist/skills/cli-commands.js +502 -0
  43. package/dist/skills/fetch-source.d.ts +104 -0
  44. package/dist/skills/fetch-source.js +448 -0
  45. package/dist/skills/installer.d.ts +117 -0
  46. package/dist/skills/installer.js +414 -0
  47. package/dist/skills/lock.d.ts +75 -0
  48. package/dist/skills/lock.js +223 -0
  49. package/dist/skills/manifest.d.ts +47 -0
  50. package/dist/skills/manifest.js +95 -0
  51. package/dist/skills/node-host.d.ts +23 -0
  52. package/dist/skills/node-host.js +59 -0
  53. package/dist/skills/paths.d.ts +61 -0
  54. package/dist/skills/paths.js +107 -0
  55. package/dist/skills/service.d.ts +256 -0
  56. package/dist/skills/service.js +730 -0
  57. package/dist/skills/tools.d.ts +35 -0
  58. package/dist/skills/tools.js +164 -0
  59. package/dist/skills/updater.d.ts +101 -0
  60. package/dist/skills/updater.js +330 -0
  61. package/dist/skills/usage.d.ts +72 -0
  62. package/dist/skills/usage.js +216 -0
  63. package/dist/tools/bash-tool.d.ts +12 -0
  64. package/dist/tools/bash-tool.js +131 -0
  65. package/dist/tools/diff.d.ts +15 -0
  66. package/dist/tools/diff.js +50 -0
  67. package/dist/tools/fs-tools.d.ts +16 -0
  68. package/dist/tools/fs-tools.js +215 -0
  69. package/dist/tools/index.d.ts +93 -0
  70. package/dist/tools/index.js +157 -0
  71. package/dist/tools/search-tools.d.ts +11 -0
  72. package/dist/tools/search-tools.js +225 -0
  73. package/dist/ui/App.d.ts +24 -0
  74. package/dist/ui/App.js +522 -0
  75. package/dist/ui/AutocompletePopup.d.ts +21 -0
  76. package/dist/ui/AutocompletePopup.js +19 -0
  77. package/dist/ui/Composer.d.ts +40 -0
  78. package/dist/ui/Composer.js +61 -0
  79. package/dist/ui/Header.d.ts +30 -0
  80. package/dist/ui/Header.js +25 -0
  81. package/dist/ui/Logo.d.ts +50 -0
  82. package/dist/ui/Logo.js +85 -0
  83. package/dist/ui/Markdown.d.ts +23 -0
  84. package/dist/ui/Markdown.js +122 -0
  85. package/dist/ui/PromptInput.d.ts +81 -0
  86. package/dist/ui/PromptInput.js +383 -0
  87. package/dist/ui/SessionOpener.d.ts +30 -0
  88. package/dist/ui/SessionOpener.js +12 -0
  89. package/dist/ui/StatusBar.d.ts +39 -0
  90. package/dist/ui/StatusBar.js +43 -0
  91. package/dist/ui/ToastStack.d.ts +24 -0
  92. package/dist/ui/ToastStack.js +38 -0
  93. package/dist/ui/Transcript.d.ts +62 -0
  94. package/dist/ui/Transcript.js +128 -0
  95. package/dist/ui/capabilities.d.ts +25 -0
  96. package/dist/ui/capabilities.js +59 -0
  97. package/dist/ui/console-bridge.d.ts +19 -0
  98. package/dist/ui/console-bridge.js +52 -0
  99. package/dist/ui/density.d.ts +26 -0
  100. package/dist/ui/density.js +34 -0
  101. package/dist/ui/entries/AssistantEntry.d.ts +23 -0
  102. package/dist/ui/entries/AssistantEntry.js +13 -0
  103. package/dist/ui/entries/EntryFrame.d.ts +23 -0
  104. package/dist/ui/entries/EntryFrame.js +6 -0
  105. package/dist/ui/entries/ToolCard.d.ts +38 -0
  106. package/dist/ui/entries/ToolCard.js +72 -0
  107. package/dist/ui/entries/ToolPreview.d.ts +57 -0
  108. package/dist/ui/entries/ToolPreview.js +144 -0
  109. package/dist/ui/entries/UserEntry.d.ts +14 -0
  110. package/dist/ui/entries/UserEntry.js +5 -0
  111. package/dist/ui/exit-snapshot.d.ts +27 -0
  112. package/dist/ui/exit-snapshot.js +26 -0
  113. package/dist/ui/gauge.d.ts +24 -0
  114. package/dist/ui/gauge.js +36 -0
  115. package/dist/ui/glyphs.d.ts +87 -0
  116. package/dist/ui/glyphs.js +127 -0
  117. package/dist/ui/gradient.d.ts +11 -0
  118. package/dist/ui/gradient.js +50 -0
  119. package/dist/ui/layout/AppShell.d.ts +24 -0
  120. package/dist/ui/layout/AppShell.js +9 -0
  121. package/dist/ui/layout/Gutter.d.ts +67 -0
  122. package/dist/ui/layout/Gutter.js +42 -0
  123. package/dist/ui/layout/OverlayFrame.d.ts +62 -0
  124. package/dist/ui/layout/OverlayFrame.js +92 -0
  125. package/dist/ui/layout/ScrollViewport.d.ts +33 -0
  126. package/dist/ui/layout/ScrollViewport.js +71 -0
  127. package/dist/ui/layout/budget.d.ts +42 -0
  128. package/dist/ui/layout/budget.js +47 -0
  129. package/dist/ui/layout/frame.d.ts +58 -0
  130. package/dist/ui/layout/frame.js +75 -0
  131. package/dist/ui/layout/overlay-window.d.ts +43 -0
  132. package/dist/ui/layout/overlay-window.js +51 -0
  133. package/dist/ui/layout/scroll.d.ts +28 -0
  134. package/dist/ui/layout/scroll.js +76 -0
  135. package/dist/ui/layout/useTerminalSize.d.ts +13 -0
  136. package/dist/ui/layout/useTerminalSize.js +50 -0
  137. package/dist/ui/markdown-blocks.d.ts +62 -0
  138. package/dist/ui/markdown-blocks.js +120 -0
  139. package/dist/ui/overlays/ConfirmDialog.d.ts +30 -0
  140. package/dist/ui/overlays/ConfirmDialog.js +25 -0
  141. package/dist/ui/overlays/HelpOverlay.d.ts +17 -0
  142. package/dist/ui/overlays/HelpOverlay.js +60 -0
  143. package/dist/ui/overlays/ModelPicker.d.ts +29 -0
  144. package/dist/ui/overlays/ModelPicker.js +29 -0
  145. package/dist/ui/overlays/SettingsScreen.d.ts +37 -0
  146. package/dist/ui/overlays/SettingsScreen.js +106 -0
  147. package/dist/ui/palettes.d.ts +79 -0
  148. package/dist/ui/palettes.js +115 -0
  149. package/dist/ui/screen.d.ts +27 -0
  150. package/dist/ui/screen.js +57 -0
  151. package/dist/ui/theme.d.ts +78 -0
  152. package/dist/ui/theme.js +127 -0
  153. package/dist/ui/transcript-text.d.ts +27 -0
  154. package/dist/ui/transcript-text.js +110 -0
  155. package/package.json +73 -0
  156. package/skills/skill-creator/SKILL.md +96 -0
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Editing keymap (spec §3.10). Pure: maps a raw `useInput` `(input, key)` pair
3
+ * to a line-editing intent. Ink does not surface Home/End/word keys on its
4
+ * `key` object — those arrive as raw escape sequences in `input` — so this
5
+ * recognizer inspects both the parsed `key` flags (for Ctrl/Alt combos) and the
6
+ * raw sequence (for Home/End/word jumps), and returns `null` for anything it
7
+ * does not own so the caller can drop unknown control bytes instead of
8
+ * inserting them into the buffer.
9
+ */
10
+ export type EditIntent = 'home' | 'end' | 'wordLeft' | 'wordRight' | 'deleteWordBack' | 'killToStart' | 'killToEnd';
11
+ /** Minimal shape of Ink's `Key` we consult (structurally compatible). */
12
+ export interface KeyState {
13
+ ctrl?: boolean;
14
+ meta?: boolean;
15
+ shift?: boolean;
16
+ leftArrow?: boolean;
17
+ rightArrow?: boolean;
18
+ }
19
+ /** Recognize a line-editing intent, or `null` if this key is not an edit op. */
20
+ export declare function recognize(input: string, key: KeyState): EditIntent | null;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Editing keymap (spec §3.10). Pure: maps a raw `useInput` `(input, key)` pair
3
+ * to a line-editing intent. Ink does not surface Home/End/word keys on its
4
+ * `key` object — those arrive as raw escape sequences in `input` — so this
5
+ * recognizer inspects both the parsed `key` flags (for Ctrl/Alt combos) and the
6
+ * raw sequence (for Home/End/word jumps), and returns `null` for anything it
7
+ * does not own so the caller can drop unknown control bytes instead of
8
+ * inserting them into the buffer.
9
+ */
10
+ /** Map a Ctrl-modified key to an intent, if any. */
11
+ function fromCtrl(input, key) {
12
+ if (key.leftArrow)
13
+ return 'wordLeft';
14
+ if (key.rightArrow)
15
+ return 'wordRight';
16
+ switch (input) {
17
+ case 'a':
18
+ return 'home';
19
+ case 'e':
20
+ return 'end';
21
+ case 'w':
22
+ return 'deleteWordBack';
23
+ case 'u':
24
+ return 'killToStart';
25
+ case 'k':
26
+ return 'killToEnd';
27
+ default:
28
+ return null;
29
+ }
30
+ }
31
+ /** Map an Alt/Meta-modified key to an intent, if any. */
32
+ function fromMeta(input) {
33
+ if (input === 'b')
34
+ return 'wordLeft';
35
+ if (input === 'f')
36
+ return 'wordRight';
37
+ if (input === '\x7f' || input === '\b')
38
+ return 'deleteWordBack';
39
+ return null;
40
+ }
41
+ /** Map a raw terminal escape / control sequence to an intent, if any. */
42
+ function fromRaw(input) {
43
+ switch (input) {
44
+ case '\x1b[H':
45
+ case '\x1b[1~':
46
+ case '\x1bOH':
47
+ case '\x01': // Ctrl+A (raw)
48
+ return 'home';
49
+ case '\x1b[F':
50
+ case '\x1b[4~':
51
+ case '\x1bOF':
52
+ case '\x05': // Ctrl+E (raw)
53
+ return 'end';
54
+ case '\x1b[1;5D':
55
+ case '\x1bb':
56
+ return 'wordLeft';
57
+ case '\x1b[1;5C':
58
+ case '\x1bf':
59
+ return 'wordRight';
60
+ case '\x17': // Ctrl+W (raw)
61
+ case '\x1b\x7f':
62
+ case '\x1b\b':
63
+ return 'deleteWordBack';
64
+ case '\x15': // Ctrl+U (raw)
65
+ return 'killToStart';
66
+ case '\x0b': // Ctrl+K (raw)
67
+ return 'killToEnd';
68
+ default:
69
+ return null;
70
+ }
71
+ }
72
+ /** Recognize a line-editing intent, or `null` if this key is not an edit op. */
73
+ export function recognize(input, key) {
74
+ if (key.ctrl) {
75
+ const hit = fromCtrl(input, key);
76
+ if (hit)
77
+ return hit;
78
+ }
79
+ if (key.meta) {
80
+ const hit = fromMeta(input);
81
+ if (hit)
82
+ return hit;
83
+ }
84
+ return fromRaw(input);
85
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Session save/resume (spec §6.3). Persists BOTH the engine `messages` and the
3
+ * visual `entries`: on abort the loop breaks before pushing the in-flight
4
+ * assistant message, so `messages` and `entries` legitimately diverge (R9).
5
+ * `messages` is what the engine resends on resume; `entries` is the faithful
6
+ * visual replay.
7
+ */
8
+ import type { Message, ModelRef } from '@aragon-agent/core';
9
+ import type { Entry } from '../agent/reducer.js';
10
+ export interface SavedSession {
11
+ version: number;
12
+ savedAt: number;
13
+ model: ModelRef;
14
+ messages: Message[];
15
+ entries: Entry[];
16
+ }
17
+ /** Resolve a `/save` target: absolute/relative path, or a name in the data dir. */
18
+ export declare function resolveSessionPath(nameOrPath: string | undefined, cwd: string): string;
19
+ export declare function saveSession(filePath: string, data: {
20
+ model: ModelRef;
21
+ messages: Message[];
22
+ entries: Entry[];
23
+ }): void;
24
+ export declare function loadSession(filePath: string): SavedSession;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Session save/resume (spec §6.3). Persists BOTH the engine `messages` and the
3
+ * visual `entries`: on abort the loop breaks before pushing the in-flight
4
+ * assistant message, so `messages` and `entries` legitimately diverge (R9).
5
+ * `messages` is what the engine resends on resume; `entries` is the faithful
6
+ * visual replay.
7
+ */
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
10
+ import { getSessionsDir } from '../config/store.js';
11
+ const SESSION_VERSION = 1;
12
+ /** Resolve a `/save` target: absolute/relative path, or a name in the data dir. */
13
+ export function resolveSessionPath(nameOrPath, cwd) {
14
+ if (!nameOrPath || nameOrPath.trim().length === 0) {
15
+ return join(getSessionsDir(), `session-${defaultStamp()}.json`);
16
+ }
17
+ const value = nameOrPath.trim();
18
+ if (isAbsolute(value))
19
+ return ensureJsonExt(value);
20
+ if (value.includes('/') || value.includes('\\'))
21
+ return ensureJsonExt(resolve(cwd, value));
22
+ return join(getSessionsDir(), ensureJsonExt(value));
23
+ }
24
+ function ensureJsonExt(p) {
25
+ return p.endsWith('.json') ? p : `${p}.json`;
26
+ }
27
+ function defaultStamp() {
28
+ // Filesystem-safe timestamp without punctuation that breaks on Windows.
29
+ return new Date().toISOString().replace(/[:.]/g, '-');
30
+ }
31
+ export function saveSession(filePath, data) {
32
+ const dir = dirname(filePath);
33
+ if (!existsSync(dir))
34
+ mkdirSync(dir, { recursive: true });
35
+ const payload = {
36
+ version: SESSION_VERSION,
37
+ savedAt: Date.now(),
38
+ model: data.model,
39
+ messages: data.messages,
40
+ entries: data.entries,
41
+ };
42
+ writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf-8');
43
+ }
44
+ export function loadSession(filePath) {
45
+ const raw = readFileSync(filePath, 'utf-8');
46
+ const parsed = JSON.parse(raw);
47
+ if (!parsed || !Array.isArray(parsed.messages) || !Array.isArray(parsed.entries)) {
48
+ throw new Error('Invalid session file (missing messages/entries).');
49
+ }
50
+ return parsed;
51
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Zip extraction with runtime gates (spec §9.2.1 / P1-4).
3
+ *
4
+ * WHY STREAMING AND NOT `fflate.unzipSync()`:
5
+ *
6
+ * `unzipSync` inflates the whole archive into memory in one call. There is
7
+ * literally no "before extraction" moment at which a size check could run,
8
+ * and no way to abort partway. A 5 MB upload at a 1000:1 ratio is 5 GB in the
9
+ * process before anything gets a vote.
10
+ *
11
+ * The earlier design proposed summing the entries' DECLARED uncompressed
12
+ * sizes first. That is not a check: the zip central directory is written by
13
+ * whoever built the archive, so an attacker declares 1 KB and ships 50 MB.
14
+ * The only judge that cannot be lied to is the count of bytes we have
15
+ * actually written, which is why all three gates below sit on the write path
16
+ * and every one of them can abort mid-entry.
17
+ */
18
+ /** Cap on the COMPRESSED download. The 20 MB below is the inflated total. */
19
+ export declare const ARCHIVE_MAX_DOWNLOAD_BYTES: number;
20
+ /** Cap on bytes actually written to disk across the whole archive. */
21
+ export declare const ARCHIVE_MAX_TOTAL_BYTES: number;
22
+ /** Cap on bytes actually written for one entry. */
23
+ export declare const ARCHIVE_MAX_ENTRY_BYTES: number;
24
+ /** Inflated:compressed ratio ceiling, applied only to non-trivial entries. */
25
+ export declare const ARCHIVE_MAX_RATIO = 200;
26
+ /** Entries below this compressed size skip the ratio gate (a 20-byte file
27
+ * legitimately expands enormously and would false-positive). */
28
+ export declare const ARCHIVE_RATIO_MIN_COMPRESSED = 1024;
29
+ export declare const ARCHIVE_MAX_ENTRIES = 200;
30
+ export declare class ArchiveError extends Error {
31
+ }
32
+ /**
33
+ * Extract a zip into `targetDir`, enforcing the four gates.
34
+ *
35
+ * On ANY violation the target directory is removed entirely — a partially
36
+ * extracted bomb left on disk would be indistinguishable from a successful
37
+ * install to the next `discover()` pass.
38
+ */
39
+ export declare function extractZip(buffer: Buffer, targetDir: string): Promise<string[]>;
40
+ /** Convenience for tests and callers that want the relative view. */
41
+ export declare function relativePaths(root: string, files: string[]): string[];
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Zip extraction with runtime gates (spec §9.2.1 / P1-4).
3
+ *
4
+ * WHY STREAMING AND NOT `fflate.unzipSync()`:
5
+ *
6
+ * `unzipSync` inflates the whole archive into memory in one call. There is
7
+ * literally no "before extraction" moment at which a size check could run,
8
+ * and no way to abort partway. A 5 MB upload at a 1000:1 ratio is 5 GB in the
9
+ * process before anything gets a vote.
10
+ *
11
+ * The earlier design proposed summing the entries' DECLARED uncompressed
12
+ * sizes first. That is not a check: the zip central directory is written by
13
+ * whoever built the archive, so an attacker declares 1 KB and ships 50 MB.
14
+ * The only judge that cannot be lied to is the count of bytes we have
15
+ * actually written, which is why all three gates below sit on the write path
16
+ * and every one of them can abort mid-entry.
17
+ */
18
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
19
+ import { dirname, relative, resolve } from 'node:path';
20
+ import { Unzip, UnzipInflate } from 'fflate';
21
+ import { checkStagedPath } from '@aragon-agent/core/skills';
22
+ /** Cap on the COMPRESSED download. The 20 MB below is the inflated total. */
23
+ export const ARCHIVE_MAX_DOWNLOAD_BYTES = 5 * 1024 * 1024;
24
+ /** Cap on bytes actually written to disk across the whole archive. */
25
+ export const ARCHIVE_MAX_TOTAL_BYTES = 20 * 1024 * 1024;
26
+ /** Cap on bytes actually written for one entry. */
27
+ export const ARCHIVE_MAX_ENTRY_BYTES = 5 * 1024 * 1024;
28
+ /** Inflated:compressed ratio ceiling, applied only to non-trivial entries. */
29
+ export const ARCHIVE_MAX_RATIO = 200;
30
+ /** Entries below this compressed size skip the ratio gate (a 20-byte file
31
+ * legitimately expands enormously and would false-positive). */
32
+ export const ARCHIVE_RATIO_MIN_COMPRESSED = 1024;
33
+ export const ARCHIVE_MAX_ENTRIES = 200;
34
+ export class ArchiveError extends Error {
35
+ }
36
+ /**
37
+ * Extract a zip into `targetDir`, enforcing the four gates.
38
+ *
39
+ * On ANY violation the target directory is removed entirely — a partially
40
+ * extracted bomb left on disk would be indistinguishable from a successful
41
+ * install to the next `discover()` pass.
42
+ */
43
+ export async function extractZip(buffer, targetDir) {
44
+ mkdirSync(targetDir, { recursive: true });
45
+ const written = [];
46
+ let totalInflated = 0;
47
+ let entryCount = 0;
48
+ let failure = null;
49
+ const fail = (message) => {
50
+ if (!failure)
51
+ failure = new ArchiveError(message);
52
+ };
53
+ await new Promise((resolvePromise, rejectPromise) => {
54
+ const unzip = new Unzip((file) => {
55
+ if (failure)
56
+ return;
57
+ entryCount += 1;
58
+ if (entryCount > ARCHIVE_MAX_ENTRIES) {
59
+ fail(`archive has more than ${ARCHIVE_MAX_ENTRIES} entries`);
60
+ return;
61
+ }
62
+ const name = file.name.replace(/\\/g, '/');
63
+ // Directory entries carry no data; the parent mkdir below covers them.
64
+ if (name.endsWith('/'))
65
+ return;
66
+ // FIRST of the two path defences (§8.3 validate is the second). Applied
67
+ // before a single byte is written, so a zip-slip entry never exists.
68
+ const pathIssue = checkStagedPath(name);
69
+ if (pathIssue) {
70
+ fail(`${pathIssue.code}: ${pathIssue.message}`);
71
+ return;
72
+ }
73
+ const absolute = resolve(targetDir, name);
74
+ const rel = relative(targetDir, absolute);
75
+ if (rel.startsWith('..') || rel.length === 0) {
76
+ fail(`entry escapes the extraction root: ${name}`);
77
+ return;
78
+ }
79
+ const pending = { path: absolute, chunks: [], written: 0 };
80
+ // fflate names the COMPRESSED size `size` and the uncompressed one
81
+ // `originalSize`; both are absent for streaming-built archives, in which
82
+ // case the ratio gate simply does not apply and the byte gates carry it.
83
+ const compressed = file.size ?? 0;
84
+ file.ondata = (err, chunk, final) => {
85
+ if (failure)
86
+ return;
87
+ if (err) {
88
+ fail(`failed to inflate ${name}: ${err.message}`);
89
+ return;
90
+ }
91
+ if (chunk && chunk.length > 0) {
92
+ pending.written += chunk.length;
93
+ totalInflated += chunk.length;
94
+ // Gate 1 + 2: real bytes, checked as they arrive so we can stop.
95
+ if (pending.written > ARCHIVE_MAX_ENTRY_BYTES) {
96
+ fail(`${name} inflates to more than ${ARCHIVE_MAX_ENTRY_BYTES} bytes`);
97
+ return;
98
+ }
99
+ if (totalInflated > ARCHIVE_MAX_TOTAL_BYTES) {
100
+ fail(`archive inflates to more than ${ARCHIVE_MAX_TOTAL_BYTES} bytes`);
101
+ return;
102
+ }
103
+ pending.chunks.push(Buffer.from(chunk));
104
+ }
105
+ if (!final)
106
+ return;
107
+ // Gate 3: compression ratio, computed from what we actually produced.
108
+ if (compressed >= ARCHIVE_RATIO_MIN_COMPRESSED &&
109
+ pending.written / compressed > ARCHIVE_MAX_RATIO) {
110
+ fail(`${name} has a compression ratio above ${ARCHIVE_MAX_RATIO}:1 ` +
111
+ `(${pending.written} from ${compressed} bytes)`);
112
+ return;
113
+ }
114
+ try {
115
+ mkdirSync(dirname(pending.path), { recursive: true });
116
+ writeFileSync(pending.path, Buffer.concat(pending.chunks));
117
+ written.push(pending.path);
118
+ }
119
+ catch (writeErr) {
120
+ fail(`could not write ${name}: ${writeErr.message}`);
121
+ }
122
+ };
123
+ file.start();
124
+ });
125
+ unzip.register(UnzipInflate);
126
+ try {
127
+ unzip.push(new Uint8Array(buffer), true);
128
+ resolvePromise();
129
+ }
130
+ catch (err) {
131
+ rejectPromise(err instanceof Error ? err : new Error(String(err)));
132
+ }
133
+ }).catch((err) => {
134
+ fail(`malformed zip archive: ${err.message}`);
135
+ });
136
+ // fflate's streaming reader does not throw on garbage — it simply never
137
+ // yields an entry. Left unchecked, "this .zip is actually an HTML error page"
138
+ // would surface much later as a puzzling SKILL_MD_MISSING instead of the
139
+ // truth, so zero entries is treated as a malformed archive here.
140
+ if (!failure && entryCount === 0) {
141
+ fail('no entries found - the file is not a valid zip archive');
142
+ }
143
+ if (failure) {
144
+ rmSync(targetDir, { recursive: true, force: true });
145
+ throw failure;
146
+ }
147
+ return written;
148
+ }
149
+ /** Convenience for tests and callers that want the relative view. */
150
+ export function relativePaths(root, files) {
151
+ return files.map((f) => relative(root, f).replace(/\\/g, '/')).sort();
152
+ }
@@ -0,0 +1,52 @@
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 { type UpdateResult } from './updater.js';
12
+ import { type CliFlags } from '../config/load.js';
13
+ import { type SkillRecord } from '@aragon-agent/core';
14
+ export interface SkillsCliOptions {
15
+ scope?: string;
16
+ name?: string;
17
+ description?: string;
18
+ yes?: boolean;
19
+ json?: boolean;
20
+ all?: boolean;
21
+ force?: boolean;
22
+ dryRun?: boolean;
23
+ check?: boolean;
24
+ sort?: string;
25
+ /** `skills usage --reset` — delete the local counters. */
26
+ reset?: boolean;
27
+ }
28
+ /** Dispatch an `aragon skills <sub>` invocation and return the exit code. */
29
+ export declare function runSkillsCommand(sub: string, argument: string | undefined, opts: SkillsCliOptions, flags: CliFlags, version: string): Promise<number>;
30
+ /**
31
+ * Can this skill's `allowed-tools` ever take effect?
32
+ *
33
+ * Resolved against `HOST_TOOL_NAMES`, not a live tool array: this process has no
34
+ * `AgentController` at all (P1-8), and the question doctor answers is "will this
35
+ * declaration work?", not "is it working at this instant". Using the superset
36
+ * also makes the output independent of how the CLI happened to be invoked, which
37
+ * is what lets it be snapshot-tested.
38
+ */
39
+ export declare function checkDeclaredTools(record: SkillRecord): string[];
40
+ /** Bundled scripts that cannot run on this host's shell (§8.4). Warn, never error. */
41
+ export declare function checkScriptPlatform(record: SkillRecord, files: Array<{
42
+ path: string;
43
+ }>): string[];
44
+ /**
45
+ * One row of the `--all` summary, in the tense the user will read it in.
46
+ *
47
+ * The tense comes from the FLAG, not from the shape of the result. A successful
48
+ * real update also carries `changedFiles`, so inferring "dry run" from its
49
+ * presence made every genuine `--all` run report "would update" for work it had
50
+ * already committed to disk.
51
+ */
52
+ export declare function describeResult(result: UpdateResult, dryRun: boolean): string;