@pugi/cli 0.1.0-alpha.10

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 (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/run.js +2 -0
  4. package/dist/commands/jobs.js +245 -0
  5. package/dist/core/agents/loader.js +104 -0
  6. package/dist/core/agents/registry.js +69 -0
  7. package/dist/core/auto-open-browser.js +128 -0
  8. package/dist/core/bash-classifier.js +1001 -0
  9. package/dist/core/clipboard.js +70 -0
  10. package/dist/core/context/builder.js +114 -0
  11. package/dist/core/context/compaction-events.js +99 -0
  12. package/dist/core/context/compaction.js +602 -0
  13. package/dist/core/context/invariants.js +250 -0
  14. package/dist/core/context/markdown-loader.js +270 -0
  15. package/dist/core/credentials.js +355 -0
  16. package/dist/core/engine/adapter-runner.js +8 -0
  17. package/dist/core/engine/anvil-client.js +156 -0
  18. package/dist/core/engine/compaction-hook.js +154 -0
  19. package/dist/core/engine/index.js +12 -0
  20. package/dist/core/engine/native-pugi.js +369 -0
  21. package/dist/core/engine/noop.js +27 -0
  22. package/dist/core/engine/prompts.js +118 -0
  23. package/dist/core/engine/tool-bridge.js +313 -0
  24. package/dist/core/file-cache.js +29 -0
  25. package/dist/core/hooks.js +415 -0
  26. package/dist/core/index-store.js +260 -0
  27. package/dist/core/jobs/registry.js +462 -0
  28. package/dist/core/mcp/client.js +316 -0
  29. package/dist/core/mcp/registry.js +171 -0
  30. package/dist/core/mcp/trust.js +91 -0
  31. package/dist/core/path-security.js +63 -0
  32. package/dist/core/permission.js +309 -0
  33. package/dist/core/repl/cap-warning.js +91 -0
  34. package/dist/core/repl/clipboard-read.js +174 -0
  35. package/dist/core/repl/history-search.js +175 -0
  36. package/dist/core/repl/history.js +172 -0
  37. package/dist/core/repl/kill-ring.js +138 -0
  38. package/dist/core/repl/session.js +618 -0
  39. package/dist/core/repl/slash-commands.js +227 -0
  40. package/dist/core/repl/workspace-context.js +113 -0
  41. package/dist/core/session.js +258 -0
  42. package/dist/core/settings.js +59 -0
  43. package/dist/core/skills/loader.js +454 -0
  44. package/dist/core/skills/sources.js +480 -0
  45. package/dist/core/skills/trust.js +172 -0
  46. package/dist/core/subagents/dispatcher.js +258 -0
  47. package/dist/core/subagents/index.js +26 -0
  48. package/dist/core/subagents/spawn.js +86 -0
  49. package/dist/core/trust.js +109 -0
  50. package/dist/index.js +8 -0
  51. package/dist/runtime/cli.js +3405 -0
  52. package/dist/runtime/commands/agents.js +385 -0
  53. package/dist/runtime/commands/budget.js +192 -0
  54. package/dist/runtime/commands/config.js +231 -0
  55. package/dist/runtime/commands/privacy.js +107 -0
  56. package/dist/runtime/commands/skills.js +401 -0
  57. package/dist/runtime/commands/undo.js +329 -0
  58. package/dist/runtime/update-check.js +294 -0
  59. package/dist/tools/bash.js +660 -0
  60. package/dist/tools/file-tools.js +346 -0
  61. package/dist/tools/registry.js +25 -0
  62. package/dist/tools/web-fetch.js +535 -0
  63. package/dist/tui/agent-tree.js +66 -0
  64. package/dist/tui/conversation-pane.js +45 -0
  65. package/dist/tui/device-flow.js +142 -0
  66. package/dist/tui/input-box.js +474 -0
  67. package/dist/tui/login-picker.js +69 -0
  68. package/dist/tui/render.js +125 -0
  69. package/dist/tui/repl-render.js +240 -0
  70. package/dist/tui/repl-splash-art.js +64 -0
  71. package/dist/tui/repl-splash.js +111 -0
  72. package/dist/tui/repl.js +214 -0
  73. package/dist/tui/slash-palette.js +106 -0
  74. package/dist/tui/splash-data.js +61 -0
  75. package/dist/tui/splash.js +31 -0
  76. package/dist/tui/status-bar.js +71 -0
  77. package/dist/tui/update-banner.js +8 -0
  78. package/dist/tui/workspace-context.js +105 -0
  79. package/package.json +71 -0
@@ -0,0 +1,401 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { stdin as input, stdout as output } from 'node:process';
3
+ import { assertValidSlug, globalSkillDir, installSkill, listSkills, parseSkillMarkdown, removeSkill, workspaceSkillDir, } from '../../core/skills/loader.js';
4
+ import { cleanupTmp, fetchSource } from '../../core/skills/sources.js';
5
+ import { hashSkillDir, recordTrust, revokeTrust, verifyTrust, } from '../../core/skills/trust.js';
6
+ import { readdirSync, readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ const USAGE = [
9
+ 'Usage:',
10
+ ' pugi skills list [--global|--workspace] Show installed skills.',
11
+ ' pugi skills install <source> [--global|--workspace] [--yes] [--as <name>]',
12
+ ' Fetch + trust-prompt + activate a skill.',
13
+ ' pugi skills info <name> Show metadata + body preview.',
14
+ ' pugi skills remove <name> [--global|--workspace] Delete an installed skill.',
15
+ ' pugi skills trust <name> [--global|--workspace] Re-trust the on-disk payload.',
16
+ '',
17
+ 'Source forms accepted by `install`:',
18
+ ' gh:owner/repo[/subdir][@ref] GitHub tree fetch (default ref: main).',
19
+ ' https://github.com/... GitHub tree/blob URL.',
20
+ ' anthropic:<slug> Shortcut for gh:anthropics/skills/<slug>@main.',
21
+ ' npm:<package>[@version] npm registry tarball.',
22
+ ' ./path | /abs/path Local filesystem.',
23
+ ' <slug> Catalog lookup (catalog.pugi.dev).',
24
+ ].join('\n');
25
+ export async function runSkillsCommand(args, ctx) {
26
+ const sub = args[0];
27
+ if (!sub || sub === '--help' || sub === '-h') {
28
+ ctx.writeOutput({ command: 'skills', usage: USAGE.split('\n') }, USAGE);
29
+ return;
30
+ }
31
+ switch (sub) {
32
+ case 'list':
33
+ return runSkillsList(args.slice(1), ctx);
34
+ case 'install':
35
+ return runSkillsInstall(args.slice(1), ctx);
36
+ case 'info':
37
+ return runSkillsInfo(args.slice(1), ctx);
38
+ case 'remove':
39
+ return runSkillsRemove(args.slice(1), ctx);
40
+ case 'trust':
41
+ return runSkillsTrust(args.slice(1), ctx);
42
+ default:
43
+ throw new Error(`Unknown sub-command "pugi skills ${sub}". Expected list, install, info, remove, or trust.`);
44
+ }
45
+ }
46
+ function parseFlags(args) {
47
+ let scope = 'both';
48
+ let yes = false;
49
+ let asName;
50
+ const positional = [];
51
+ for (let i = 0; i < args.length; i++) {
52
+ const arg = args[i];
53
+ if (arg === '--global')
54
+ scope = 'global';
55
+ else if (arg === '--workspace')
56
+ scope = 'workspace';
57
+ else if (arg === '--yes' || arg === '-y')
58
+ yes = true;
59
+ else if (arg === '--as') {
60
+ asName = args[++i];
61
+ }
62
+ else if (arg && !arg.startsWith('--')) {
63
+ positional.push(arg);
64
+ }
65
+ else if (arg) {
66
+ throw new Error(`Unknown flag: ${arg}`);
67
+ }
68
+ }
69
+ return { scope, yes, asName, positional };
70
+ }
71
+ async function runSkillsList(args, ctx) {
72
+ const flags = parseFlags(args);
73
+ const includeGlobal = flags.scope === 'global' || flags.scope === 'both';
74
+ const includeWorkspace = flags.scope === 'workspace' || flags.scope === 'both';
75
+ const global = includeGlobal ? listSkills('global', ctx.workspaceRoot) : [];
76
+ const workspace = includeWorkspace ? listSkills('workspace', ctx.workspaceRoot) : [];
77
+ const all = [...global, ...workspace];
78
+ const trustStatuses = await Promise.all(all.map(async (skill) => {
79
+ const actual = hashSkillDir(skill.dir);
80
+ const verdict = await verifyTrust('skill', skill.scope, skill.name, actual);
81
+ return { skill, trust: verdict };
82
+ }));
83
+ if (ctx.json) {
84
+ ctx.writeOutput({
85
+ command: 'skills.list',
86
+ skills: trustStatuses.map(({ skill, trust }) => ({
87
+ name: skill.name,
88
+ scope: skill.scope,
89
+ dir: skill.dir,
90
+ description: skill.frontmatter.description,
91
+ version: skill.frontmatter.metadata.version ?? null,
92
+ tools: skill.frontmatter.metadata.tools ?? [],
93
+ trust: trust.status,
94
+ })),
95
+ }, '');
96
+ return;
97
+ }
98
+ if (trustStatuses.length === 0) {
99
+ ctx.writeOutput({ command: 'skills.list', skills: [] }, 'No skills installed. Try `pugi skills install anthropic:python-coding-standards`.');
100
+ return;
101
+ }
102
+ const lines = ['Installed skills:'];
103
+ for (const { skill, trust } of trustStatuses) {
104
+ const version = skill.frontmatter.metadata.version ? ` v${String(skill.frontmatter.metadata.version)}` : '';
105
+ const trustMark = trust.status === 'trusted' ? '[trusted]' : trust.status === 'unsigned' ? '[unsigned]' : '[mismatch]';
106
+ lines.push(` ${skill.name.padEnd(28)} ${skill.scope.padEnd(10)} ${trustMark.padEnd(11)}${version}`);
107
+ lines.push(` ${truncate(skill.frontmatter.description, 80)}`);
108
+ }
109
+ ctx.writeOutput({ command: 'skills.list', skills: trustStatuses }, lines.join('\n'));
110
+ }
111
+ async function runSkillsInstall(args, ctx) {
112
+ const flags = parseFlags(args);
113
+ const source = flags.positional[0];
114
+ if (!source) {
115
+ throw new Error('pugi skills install requires a <source> argument. See `pugi skills --help`.');
116
+ }
117
+ const scope = flags.scope === 'workspace' ? 'workspace' : 'global';
118
+ // 1. Fetch payload to tmp dir.
119
+ let fetched;
120
+ try {
121
+ fetched = await fetchSource(source);
122
+ }
123
+ catch (error) {
124
+ const message = error instanceof Error ? error.message : String(error);
125
+ ctx.writeOutput({ command: 'skills.install', status: 'fetch_failed', source, error: message }, `Skill fetch failed: ${message}`);
126
+ process.exitCode = 1;
127
+ return;
128
+ }
129
+ if (fetched.inferredKind !== 'skill') {
130
+ cleanupTmp(fetched.tmpDir);
131
+ const message = `Source ${source} looks like an agent (single .md), not a skill. Use \`pugi agents install\` instead.`;
132
+ ctx.writeOutput({ command: 'skills.install', status: 'wrong_kind', source }, message);
133
+ process.exitCode = 1;
134
+ return;
135
+ }
136
+ // 2. Parse SKILL.md to surface metadata in the trust prompt.
137
+ const skillMdPath = join(fetched.tmpDir, 'SKILL.md');
138
+ let parsed;
139
+ try {
140
+ parsed = parseSkillMarkdown(readFileSync(skillMdPath, 'utf8'));
141
+ }
142
+ catch (error) {
143
+ cleanupTmp(fetched.tmpDir);
144
+ const message = error instanceof Error ? error.message : String(error);
145
+ ctx.writeOutput({ command: 'skills.install', status: 'parse_failed', source, error: message }, `Skill parse failed: ${message}`);
146
+ process.exitCode = 1;
147
+ return;
148
+ }
149
+ const skillName = flags.asName ?? parsed.frontmatter.name;
150
+ // Fail-fast before any FS or trust-prompt work: a hostile `--as` flag
151
+ // or a hostile frontmatter `name` must never reach path.join.
152
+ try {
153
+ assertValidSlug(skillName, 'skill');
154
+ }
155
+ catch (error) {
156
+ cleanupTmp(fetched.tmpDir);
157
+ const message = error instanceof Error ? error.message : String(error);
158
+ ctx.writeOutput({ command: 'skills.install', status: 'invalid_name', source, name: skillName, error: message }, message);
159
+ process.exitCode = 1;
160
+ return;
161
+ }
162
+ const sha256 = hashSkillDir(fetched.tmpDir);
163
+ const targetDir = scope === 'global' ? globalSkillDir(skillName) : workspaceSkillDir(ctx.workspaceRoot, skillName);
164
+ const summary = {
165
+ name: skillName,
166
+ description: parsed.frontmatter.description,
167
+ version: parsed.frontmatter.metadata.version ?? null,
168
+ tools: parsed.frontmatter.metadata.tools ?? [],
169
+ files: countFiles(fetched.tmpDir),
170
+ source: fetched.sourceUrl,
171
+ targetDir,
172
+ sha256,
173
+ };
174
+ if (!flags.yes) {
175
+ const decision = await promptTrust('skill', summary, parsed.body);
176
+ if (decision === 'no') {
177
+ cleanupTmp(fetched.tmpDir);
178
+ ctx.writeOutput({ command: 'skills.install', status: 'declined', source, name: skillName }, 'Install declined. Nothing changed.');
179
+ return;
180
+ }
181
+ }
182
+ // 3. Install to disk.
183
+ const installedDir = installSkill({
184
+ payloadDir: fetched.tmpDir,
185
+ name: skillName,
186
+ scope,
187
+ workspaceRoot: ctx.workspaceRoot,
188
+ });
189
+ // 4. Record trust.
190
+ await recordTrust({
191
+ kind: 'skill',
192
+ scope,
193
+ name: skillName,
194
+ sha256,
195
+ source: fetched.sourceUrl,
196
+ signedBy: signerIdentity(),
197
+ });
198
+ cleanupTmp(fetched.tmpDir);
199
+ ctx.writeOutput({
200
+ command: 'skills.install',
201
+ status: 'installed',
202
+ name: skillName,
203
+ scope,
204
+ dir: installedDir,
205
+ sha256,
206
+ source: fetched.sourceUrl,
207
+ }, [
208
+ `Installed skill "${skillName}" (${scope}).`,
209
+ ` Path: ${installedDir}`,
210
+ ` Source: ${fetched.sourceUrl}`,
211
+ ` sha256: ${sha256}`,
212
+ ].join('\n'));
213
+ }
214
+ async function runSkillsInfo(args, ctx) {
215
+ const flags = parseFlags(args);
216
+ const name = flags.positional[0];
217
+ if (!name) {
218
+ throw new Error('pugi skills info requires a <name> argument.');
219
+ }
220
+ const matches = findSkill(name, flags.scope, ctx.workspaceRoot);
221
+ if (matches.length === 0) {
222
+ ctx.writeOutput({ command: 'skills.info', status: 'not_found', name }, `No skill "${name}" installed. Try \`pugi skills list\`.`);
223
+ process.exitCode = 1;
224
+ return;
225
+ }
226
+ const skill = matches[0];
227
+ if (!skill) {
228
+ ctx.writeOutput({ command: 'skills.info', status: 'not_found', name }, `No skill "${name}" installed.`);
229
+ process.exitCode = 1;
230
+ return;
231
+ }
232
+ const actualSha = hashSkillDir(skill.dir);
233
+ const trust = await verifyTrust('skill', skill.scope, skill.name, actualSha);
234
+ const preview = skill.body.slice(0, 800);
235
+ const lines = [
236
+ `Skill: ${skill.name}`,
237
+ ` Scope: ${skill.scope}`,
238
+ ` Path: ${skill.dir}`,
239
+ ` Description: ${skill.frontmatter.description}`,
240
+ ` Version: ${String(skill.frontmatter.metadata.version ?? '(unset)')}`,
241
+ ` Tools: ${(skill.frontmatter.metadata.tools ?? []).join(', ') || '(none)'}`,
242
+ ` Trust: ${trust.status}`,
243
+ ` sha256: ${actualSha}`,
244
+ '',
245
+ 'Body preview:',
246
+ preview,
247
+ skill.body.length > 800 ? `... (${skill.body.length - 800} more chars)` : '',
248
+ ]
249
+ .filter((line) => line !== '')
250
+ .join('\n');
251
+ ctx.writeOutput({
252
+ command: 'skills.info',
253
+ name: skill.name,
254
+ scope: skill.scope,
255
+ dir: skill.dir,
256
+ frontmatter: skill.frontmatter,
257
+ sha256: actualSha,
258
+ trust,
259
+ bodyPreview: preview,
260
+ bodyLength: skill.body.length,
261
+ }, lines);
262
+ }
263
+ async function runSkillsRemove(args, ctx) {
264
+ const flags = parseFlags(args);
265
+ const name = flags.positional[0];
266
+ if (!name) {
267
+ throw new Error('pugi skills remove requires a <name> argument.');
268
+ }
269
+ const matches = findSkill(name, flags.scope, ctx.workspaceRoot);
270
+ if (matches.length === 0) {
271
+ ctx.writeOutput({ command: 'skills.remove', status: 'not_found', name }, `No skill "${name}" installed.`);
272
+ process.exitCode = 1;
273
+ return;
274
+ }
275
+ for (const skill of matches) {
276
+ removeSkill(skill.name, skill.scope, ctx.workspaceRoot);
277
+ await revokeTrust('skill', skill.scope, skill.name);
278
+ }
279
+ ctx.writeOutput({
280
+ command: 'skills.remove',
281
+ status: 'removed',
282
+ name,
283
+ removed: matches.map((m) => ({ name: m.name, scope: m.scope, dir: m.dir })),
284
+ }, `Removed ${matches.length} skill install(s) named "${name}".`);
285
+ }
286
+ async function runSkillsTrust(args, ctx) {
287
+ const flags = parseFlags(args);
288
+ const name = flags.positional[0];
289
+ if (!name) {
290
+ throw new Error('pugi skills trust requires a <name> argument.');
291
+ }
292
+ const matches = findSkill(name, flags.scope, ctx.workspaceRoot);
293
+ if (matches.length === 0) {
294
+ ctx.writeOutput({ command: 'skills.trust', status: 'not_found', name }, `No skill "${name}" installed.`);
295
+ process.exitCode = 1;
296
+ return;
297
+ }
298
+ const skill = matches[0];
299
+ if (!skill) {
300
+ process.exitCode = 1;
301
+ return;
302
+ }
303
+ const sha256 = hashSkillDir(skill.dir);
304
+ await recordTrust({
305
+ kind: 'skill',
306
+ scope: skill.scope,
307
+ name: skill.name,
308
+ sha256,
309
+ source: skill.dir,
310
+ signedBy: signerIdentity(),
311
+ });
312
+ ctx.writeOutput({ command: 'skills.trust', status: 'trusted', name: skill.name, scope: skill.scope, sha256 }, `Re-trusted skill "${skill.name}" (${skill.scope}). sha256 = ${sha256}.`);
313
+ }
314
+ /* ----------------------------- shared helpers ----------------------------- */
315
+ function findSkill(name, scope, workspaceRoot) {
316
+ const scopes = scope === 'both' ? ['global', 'workspace'] : [scope];
317
+ const out = [];
318
+ for (const s of scopes) {
319
+ const all = listSkills(s, workspaceRoot);
320
+ for (const skill of all) {
321
+ if (skill.name === name)
322
+ out.push(skill);
323
+ }
324
+ }
325
+ return out;
326
+ }
327
+ function countFiles(dir) {
328
+ // Conservative — recurse and count regular files only. Used for the
329
+ // trust prompt summary so the operator sees payload bulk.
330
+ let total = 0;
331
+ const walk = (d) => {
332
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
333
+ const full = join(d, entry.name);
334
+ if (entry.isDirectory())
335
+ walk(full);
336
+ else if (entry.isFile())
337
+ total++;
338
+ }
339
+ };
340
+ walk(dir);
341
+ return total;
342
+ }
343
+ function truncate(text, max) {
344
+ if (text.length <= max)
345
+ return text;
346
+ return `${text.slice(0, max - 1)}…`;
347
+ }
348
+ function signerIdentity() {
349
+ return (process.env.PUGI_TRUSTED_BY?.trim() ||
350
+ process.env.USER?.trim() ||
351
+ process.env.USERNAME?.trim() ||
352
+ 'cli');
353
+ }
354
+ /**
355
+ * Interactive trust prompt. Reads from stdin; `info` shows the full
356
+ * body; `y` accepts; everything else declines.
357
+ *
358
+ * Bypassable only via the operator-passed `--yes` flag in the caller
359
+ * (never auto-injected from another code path).
360
+ */
361
+ async function promptTrust(kind, summary, body) {
362
+ const banner = [
363
+ '',
364
+ `About to install ${kind} "${summary.name}"`,
365
+ ` Description: ${summary.description}`,
366
+ ` Version: ${summary.version ?? '(unset)'}`,
367
+ ` Tools: ${summary.tools.join(', ') || '(none)'}`,
368
+ ` Files: ${summary.files}`,
369
+ ` Source: ${summary.source}`,
370
+ ` Target: ${summary.targetDir}`,
371
+ ` sha256: ${summary.sha256}`,
372
+ '',
373
+ 'This payload becomes part of the system prompt and may influence agent decisions.',
374
+ 'Trust this install? [y/N/info] ',
375
+ ].join('\n');
376
+ process.stdout.write(banner);
377
+ if (!input.isTTY) {
378
+ // Non-interactive caller without --yes: refuse rather than block.
379
+ process.stdout.write('\n(non-interactive stdin; declining install)\n');
380
+ return 'no';
381
+ }
382
+ const rl = createInterface({ input, output });
383
+ try {
384
+ while (true) {
385
+ const answer = (await rl.question('')).trim().toLowerCase();
386
+ if (answer === 'y' || answer === 'yes')
387
+ return 'yes';
388
+ if (answer === 'info') {
389
+ process.stdout.write('\n');
390
+ process.stdout.write(body);
391
+ process.stdout.write('\n\nTrust this install? [y/N] ');
392
+ continue;
393
+ }
394
+ return 'no';
395
+ }
396
+ }
397
+ finally {
398
+ rl.close();
399
+ }
400
+ }
401
+ //# sourceMappingURL=skills.js.map