@pugi/cli 0.1.0-alpha.9 → 0.1.0-beta.2

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 (68) hide show
  1. package/README.md +33 -0
  2. package/assets/pugi-mascot.ansi +41 -0
  3. package/dist/commands/deploy.js +439 -0
  4. package/dist/core/agents/loader.js +104 -0
  5. package/dist/core/agents/registry.js +1 -1
  6. package/dist/core/consensus/anvil-fanout.js +276 -0
  7. package/dist/core/consensus/diff-capture.js +382 -0
  8. package/dist/core/consensus/rubric.js +233 -0
  9. package/dist/core/context/index.js +21 -0
  10. package/dist/core/context/pugiignore.js +316 -0
  11. package/dist/core/context/repo-skeleton.js +533 -0
  12. package/dist/core/context/watcher.js +342 -0
  13. package/dist/core/context/working-set.js +165 -0
  14. package/dist/core/edits/dispatch.js +185 -0
  15. package/dist/core/edits/index.js +15 -0
  16. package/dist/core/edits/layer-a-apply.js +217 -0
  17. package/dist/core/edits/layer-b-apply.js +211 -0
  18. package/dist/core/edits/layer-c-apply.js +160 -0
  19. package/dist/core/edits/layer-d-ast.js +29 -0
  20. package/dist/core/edits/marker-parser.js +401 -0
  21. package/dist/core/edits/security-gate.js +223 -0
  22. package/dist/core/edits/worktree.js +229 -0
  23. package/dist/core/engine/native-pugi.js +6 -1
  24. package/dist/core/engine/prompts.js +4 -1
  25. package/dist/core/engine/tool-bridge.js +33 -1
  26. package/dist/core/lsp/client.js +631 -0
  27. package/dist/core/repl/ask.js +512 -0
  28. package/dist/core/repl/cancellation.js +98 -0
  29. package/dist/core/repl/dispatch-fsm.js +220 -0
  30. package/dist/core/repl/privacy-banner.js +71 -0
  31. package/dist/core/repl/session.js +1896 -13
  32. package/dist/core/repl/slash-commands.js +59 -32
  33. package/dist/core/repl/store/index.js +12 -0
  34. package/dist/core/repl/store/jsonl-log.js +321 -0
  35. package/dist/core/repl/store/lockfile.js +155 -0
  36. package/dist/core/repl/store/session-store.js +792 -0
  37. package/dist/core/repl/store/types.js +44 -0
  38. package/dist/core/repl/store/uuid-v7.js +68 -0
  39. package/dist/core/repl/workspace-context.js +72 -1
  40. package/dist/core/skills/loader.js +454 -0
  41. package/dist/core/skills/sources.js +480 -0
  42. package/dist/core/skills/trust.js +172 -0
  43. package/dist/runtime/cli.js +767 -10
  44. package/dist/runtime/commands/agents.js +385 -0
  45. package/dist/runtime/commands/config.js +338 -8
  46. package/dist/runtime/commands/lsp.js +184 -0
  47. package/dist/runtime/commands/patch.js +111 -0
  48. package/dist/runtime/commands/review-consensus.js +399 -0
  49. package/dist/runtime/commands/skills.js +401 -0
  50. package/dist/runtime/commands/worktree.js +133 -0
  51. package/dist/tools/apply-patch.js +314 -0
  52. package/dist/tools/file-tools.js +90 -0
  53. package/dist/tools/lsp-tools.js +189 -0
  54. package/dist/tools/registry.js +18 -0
  55. package/dist/tools/web-fetch.js +1 -1
  56. package/dist/tui/agent-tree-pane.js +9 -0
  57. package/dist/tui/ask-cli.js +52 -0
  58. package/dist/tui/ask-modal.js +211 -0
  59. package/dist/tui/conversation-pane.js +48 -3
  60. package/dist/tui/input-box.js +48 -5
  61. package/dist/tui/markdown-render.js +266 -0
  62. package/dist/tui/repl-render.js +185 -0
  63. package/dist/tui/repl-splash-mascot.js +130 -0
  64. package/dist/tui/repl-splash.js +7 -1
  65. package/dist/tui/repl.js +82 -11
  66. package/dist/tui/status-bar.js +63 -3
  67. package/dist/tui/tool-stream-pane.js +91 -0
  68. package/package.json +11 -5
@@ -0,0 +1,385 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { stdin as input, stdout as output } from 'node:process';
5
+ import { globalAgentPath, installAgent, listAgents, removeAgent, workspaceAgentPath, } from '../../core/agents/loader.js';
6
+ import { assertValidSlug, parseSkillMarkdown } from '../../core/skills/loader.js';
7
+ import { cleanupTmp, fetchSource } from '../../core/skills/sources.js';
8
+ import { hashAgentFile, recordTrust, revokeTrust, verifyTrust, } from '../../core/skills/trust.js';
9
+ const USAGE = [
10
+ 'Usage:',
11
+ ' pugi agents list [--global|--workspace] Show installed agents.',
12
+ ' pugi agents install <source> [--global|--workspace] [--yes] [--as <slug>]',
13
+ ' Fetch + trust-prompt + activate an agent.',
14
+ ' pugi agents info <slug> Show metadata + body preview.',
15
+ ' pugi agents remove <slug> [--global|--workspace] Delete an installed agent.',
16
+ '',
17
+ 'Source forms accepted by `install` (same as `pugi skills install`):',
18
+ ' gh:owner/repo/path/to/agent.md[@ref]',
19
+ ' https://github.com/...',
20
+ ' anthropic:<slug>',
21
+ ' npm:<package>',
22
+ ' ./path/to/agent.md or /abs/path/to/agent.md',
23
+ ' <slug> Catalog lookup (catalog.pugi.dev).',
24
+ ].join('\n');
25
+ export async function runAgentsCommand(args, ctx) {
26
+ const sub = args[0];
27
+ if (!sub || sub === '--help' || sub === '-h') {
28
+ ctx.writeOutput({ command: 'agents', usage: USAGE.split('\n') }, USAGE);
29
+ return;
30
+ }
31
+ switch (sub) {
32
+ case 'list':
33
+ return runAgentsList(args.slice(1), ctx);
34
+ case 'install':
35
+ return runAgentsInstall(args.slice(1), ctx);
36
+ case 'info':
37
+ return runAgentsInfo(args.slice(1), ctx);
38
+ case 'remove':
39
+ return runAgentsRemove(args.slice(1), ctx);
40
+ default:
41
+ throw new Error(`Unknown sub-command "pugi agents ${sub}". Expected list, install, info, or remove.`);
42
+ }
43
+ }
44
+ function parseFlags(args) {
45
+ let scope = 'both';
46
+ let yes = false;
47
+ let asName;
48
+ const positional = [];
49
+ for (let i = 0; i < args.length; i++) {
50
+ const arg = args[i];
51
+ if (arg === '--global')
52
+ scope = 'global';
53
+ else if (arg === '--workspace')
54
+ scope = 'workspace';
55
+ else if (arg === '--yes' || arg === '-y')
56
+ yes = true;
57
+ else if (arg === '--as') {
58
+ asName = args[++i];
59
+ }
60
+ else if (arg && !arg.startsWith('--')) {
61
+ positional.push(arg);
62
+ }
63
+ else if (arg) {
64
+ throw new Error(`Unknown flag: ${arg}`);
65
+ }
66
+ }
67
+ return { scope, yes, asName, positional };
68
+ }
69
+ async function runAgentsList(args, ctx) {
70
+ const flags = parseFlags(args);
71
+ const includeGlobal = flags.scope === 'global' || flags.scope === 'both';
72
+ const includeWorkspace = flags.scope === 'workspace' || flags.scope === 'both';
73
+ const global = includeGlobal ? listAgents('global', ctx.workspaceRoot) : [];
74
+ const workspace = includeWorkspace ? listAgents('workspace', ctx.workspaceRoot) : [];
75
+ const all = [...global, ...workspace];
76
+ const verdicts = await Promise.all(all.map(async (agent) => {
77
+ const actual = hashAgentFile(agent.filePath);
78
+ const verdict = await verifyTrust('agent', agent.scope, agent.slug, actual);
79
+ return { agent, trust: verdict };
80
+ }));
81
+ if (ctx.json) {
82
+ ctx.writeOutput({
83
+ command: 'agents.list',
84
+ agents: verdicts.map(({ agent, trust }) => ({
85
+ slug: agent.slug,
86
+ scope: agent.scope,
87
+ filePath: agent.filePath,
88
+ description: agent.frontmatter.description,
89
+ model: agent.frontmatter.metadata.model ?? null,
90
+ tools: agent.frontmatter.metadata.tools ?? [],
91
+ trust: trust.status,
92
+ })),
93
+ }, '');
94
+ return;
95
+ }
96
+ if (verdicts.length === 0) {
97
+ ctx.writeOutput({ command: 'agents.list', agents: [] }, 'No agents installed. Try `pugi agents install gh:anthropics/claude-code-agents/code-reviewer.md@main`.');
98
+ return;
99
+ }
100
+ const lines = ['Installed agents:'];
101
+ for (const { agent, trust } of verdicts) {
102
+ const model = agent.frontmatter.metadata.model ? ` model=${String(agent.frontmatter.metadata.model)}` : '';
103
+ const trustMark = trust.status === 'trusted' ? '[trusted]' : trust.status === 'unsigned' ? '[unsigned]' : '[mismatch]';
104
+ lines.push(` ${agent.slug.padEnd(28)} ${agent.scope.padEnd(10)} ${trustMark.padEnd(11)}${model}`);
105
+ lines.push(` ${truncate(agent.frontmatter.description, 80)}`);
106
+ }
107
+ ctx.writeOutput({ command: 'agents.list', agents: verdicts }, lines.join('\n'));
108
+ }
109
+ async function runAgentsInstall(args, ctx) {
110
+ const flags = parseFlags(args);
111
+ const source = flags.positional[0];
112
+ if (!source) {
113
+ throw new Error('pugi agents install requires a <source> argument. See `pugi agents --help`.');
114
+ }
115
+ const scope = flags.scope === 'workspace' ? 'workspace' : 'global';
116
+ let fetched;
117
+ try {
118
+ fetched = await fetchSource(source);
119
+ }
120
+ catch (error) {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ ctx.writeOutput({ command: 'agents.install', status: 'fetch_failed', source, error: message }, `Agent fetch failed: ${message}`);
123
+ process.exitCode = 1;
124
+ return;
125
+ }
126
+ // Pick the .md file inside the payload. fetchSource for local single
127
+ // files already wrote into a tmp dir; for gh: we need to look for the
128
+ // .md inside the payload root (one-file convention).
129
+ const stat = statSync(fetched.tmpDir);
130
+ let agentFile;
131
+ if (stat.isFile()) {
132
+ agentFile = fetched.tmpDir;
133
+ }
134
+ else {
135
+ const candidates = readdirSync(fetched.tmpDir).filter((name) => name.toLowerCase().endsWith('.md'));
136
+ if (candidates.length === 0) {
137
+ cleanupTmp(fetched.tmpDir);
138
+ ctx.writeOutput({ command: 'agents.install', status: 'no_markdown', source }, `Agent fetch from ${source} contains no .md file at root.`);
139
+ process.exitCode = 1;
140
+ return;
141
+ }
142
+ if (candidates.length > 1) {
143
+ cleanupTmp(fetched.tmpDir);
144
+ ctx.writeOutput({ command: 'agents.install', status: 'ambiguous', source, candidates }, `Agent fetch from ${source} contains ${candidates.length} .md files. Specify one with a subdir path.`);
145
+ process.exitCode = 1;
146
+ return;
147
+ }
148
+ const first = candidates[0];
149
+ if (!first) {
150
+ cleanupTmp(fetched.tmpDir);
151
+ process.exitCode = 1;
152
+ return;
153
+ }
154
+ agentFile = join(fetched.tmpDir, first);
155
+ }
156
+ let parsed;
157
+ try {
158
+ parsed = parseSkillMarkdown(readFileSync(agentFile, 'utf8'));
159
+ }
160
+ catch (error) {
161
+ cleanupTmp(fetched.tmpDir);
162
+ const message = error instanceof Error ? error.message : String(error);
163
+ ctx.writeOutput({ command: 'agents.install', status: 'parse_failed', source, error: message }, `Agent parse failed: ${message}`);
164
+ process.exitCode = 1;
165
+ return;
166
+ }
167
+ // Agents are single-file. If the frontmatter declares "skill" (Pugi
168
+ // native dialect) the operator picked the wrong command; refuse and
169
+ // hint. If the frontmatter omits type entirely (Anthropic flat
170
+ // dialect), the loader defaulted to "skill" — we override to "agent"
171
+ // since the operator explicitly chose `pugi agents install`. The
172
+ // single-file storage convention is the disambiguator.
173
+ if (parsed.frontmatter.metadata.type === 'skill' &&
174
+ Object.prototype.hasOwnProperty.call(parsed.frontmatter, 'metadata')) {
175
+ // Did the source explicitly declare type=skill? We detect via the
176
+ // raw frontmatter — when the type was inferred (no metadata block),
177
+ // we permit the install.
178
+ const sourceHasExplicitMetadata = /^metadata:\s*$/m.test(parsed.source);
179
+ if (sourceHasExplicitMetadata) {
180
+ cleanupTmp(fetched.tmpDir);
181
+ ctx.writeOutput({ command: 'agents.install', status: 'wrong_kind', source, declaredType: 'skill' }, `Source ${source} declares metadata.type="skill", not "agent". Use \`pugi skills install\` for skills.`);
182
+ process.exitCode = 1;
183
+ return;
184
+ }
185
+ }
186
+ const slug = flags.asName ?? parsed.frontmatter.name;
187
+ // Fail-fast before trust prompt + FS write: a hostile `--as` flag or
188
+ // hostile frontmatter `name` must never reach path.join.
189
+ try {
190
+ assertValidSlug(slug, 'agent');
191
+ }
192
+ catch (error) {
193
+ cleanupTmp(fetched.tmpDir);
194
+ const message = error instanceof Error ? error.message : String(error);
195
+ ctx.writeOutput({ command: 'agents.install', status: 'invalid_slug', source, slug, error: message }, message);
196
+ process.exitCode = 1;
197
+ return;
198
+ }
199
+ const sha256 = hashAgentFile(agentFile);
200
+ const targetPath = scope === 'global' ? globalAgentPath(slug) : workspaceAgentPath(ctx.workspaceRoot, slug);
201
+ const summary = {
202
+ name: slug,
203
+ description: parsed.frontmatter.description,
204
+ version: parsed.frontmatter.metadata.version
205
+ ? String(parsed.frontmatter.metadata.version)
206
+ : null,
207
+ tools: (parsed.frontmatter.metadata.tools ?? []),
208
+ files: 1,
209
+ source: fetched.sourceUrl,
210
+ targetDir: targetPath,
211
+ sha256,
212
+ };
213
+ if (!flags.yes) {
214
+ const decision = await promptTrust('agent', summary, parsed.body);
215
+ if (decision === 'no') {
216
+ cleanupTmp(fetched.tmpDir);
217
+ ctx.writeOutput({ command: 'agents.install', status: 'declined', source, slug }, 'Install declined. Nothing changed.');
218
+ return;
219
+ }
220
+ }
221
+ installAgent({
222
+ payloadDir: agentFile,
223
+ slug,
224
+ scope,
225
+ workspaceRoot: ctx.workspaceRoot,
226
+ });
227
+ await recordTrust({
228
+ kind: 'agent',
229
+ scope,
230
+ name: slug,
231
+ sha256,
232
+ source: fetched.sourceUrl,
233
+ signedBy: signerIdentity(),
234
+ });
235
+ cleanupTmp(fetched.tmpDir);
236
+ ctx.writeOutput({
237
+ command: 'agents.install',
238
+ status: 'installed',
239
+ slug,
240
+ scope,
241
+ path: targetPath,
242
+ sha256,
243
+ source: fetched.sourceUrl,
244
+ }, [
245
+ `Installed agent "${slug}" (${scope}).`,
246
+ ` Path: ${targetPath}`,
247
+ ` Source: ${fetched.sourceUrl}`,
248
+ ` sha256: ${sha256}`,
249
+ ].join('\n'));
250
+ }
251
+ async function runAgentsInfo(args, ctx) {
252
+ const flags = parseFlags(args);
253
+ const slug = flags.positional[0];
254
+ if (!slug) {
255
+ throw new Error('pugi agents info requires a <slug> argument.');
256
+ }
257
+ const matches = findAgent(slug, flags.scope, ctx.workspaceRoot);
258
+ if (matches.length === 0) {
259
+ ctx.writeOutput({ command: 'agents.info', status: 'not_found', slug }, `No agent "${slug}" installed. Try \`pugi agents list\`.`);
260
+ process.exitCode = 1;
261
+ return;
262
+ }
263
+ const agent = matches[0];
264
+ if (!agent) {
265
+ process.exitCode = 1;
266
+ return;
267
+ }
268
+ const actualSha = hashAgentFile(agent.filePath);
269
+ const trust = await verifyTrust('agent', agent.scope, agent.slug, actualSha);
270
+ const preview = agent.body.slice(0, 800);
271
+ const lines = [
272
+ `Agent: ${agent.slug}`,
273
+ ` Scope: ${agent.scope}`,
274
+ ` Path: ${agent.filePath}`,
275
+ ` Description: ${agent.frontmatter.description}`,
276
+ ` Model: ${String(agent.frontmatter.metadata.model ?? '(default)')}`,
277
+ ` Tools: ${(agent.frontmatter.metadata.tools ?? []).join(', ') || '(none)'}`,
278
+ ` Trust: ${trust.status}`,
279
+ ` sha256: ${actualSha}`,
280
+ '',
281
+ 'Body preview:',
282
+ preview,
283
+ agent.body.length > 800 ? `... (${agent.body.length - 800} more chars)` : '',
284
+ ]
285
+ .filter((line) => line !== '')
286
+ .join('\n');
287
+ ctx.writeOutput({
288
+ command: 'agents.info',
289
+ slug: agent.slug,
290
+ scope: agent.scope,
291
+ filePath: agent.filePath,
292
+ frontmatter: agent.frontmatter,
293
+ sha256: actualSha,
294
+ trust,
295
+ bodyPreview: preview,
296
+ bodyLength: agent.body.length,
297
+ }, lines);
298
+ }
299
+ async function runAgentsRemove(args, ctx) {
300
+ const flags = parseFlags(args);
301
+ const slug = flags.positional[0];
302
+ if (!slug) {
303
+ throw new Error('pugi agents remove requires a <slug> argument.');
304
+ }
305
+ const matches = findAgent(slug, flags.scope, ctx.workspaceRoot);
306
+ if (matches.length === 0) {
307
+ ctx.writeOutput({ command: 'agents.remove', status: 'not_found', slug }, `No agent "${slug}" installed.`);
308
+ process.exitCode = 1;
309
+ return;
310
+ }
311
+ for (const agent of matches) {
312
+ removeAgent(agent.slug, agent.scope, ctx.workspaceRoot);
313
+ await revokeTrust('agent', agent.scope, agent.slug);
314
+ }
315
+ ctx.writeOutput({
316
+ command: 'agents.remove',
317
+ status: 'removed',
318
+ slug,
319
+ removed: matches.map((m) => ({ slug: m.slug, scope: m.scope, filePath: m.filePath })),
320
+ }, `Removed ${matches.length} agent install(s) named "${slug}".`);
321
+ }
322
+ /* ----------------------------- shared helpers ----------------------------- */
323
+ function findAgent(slug, scope, workspaceRoot) {
324
+ const scopes = scope === 'both' ? ['global', 'workspace'] : [scope];
325
+ const out = [];
326
+ for (const s of scopes) {
327
+ const all = listAgents(s, workspaceRoot);
328
+ for (const agent of all) {
329
+ if (agent.slug === slug)
330
+ out.push(agent);
331
+ }
332
+ }
333
+ return out;
334
+ }
335
+ function truncate(text, max) {
336
+ if (text.length <= max)
337
+ return text;
338
+ return `${text.slice(0, max - 1)}…`;
339
+ }
340
+ function signerIdentity() {
341
+ return (process.env.PUGI_TRUSTED_BY?.trim() ||
342
+ process.env.USER?.trim() ||
343
+ process.env.USERNAME?.trim() ||
344
+ 'cli');
345
+ }
346
+ async function promptTrust(kind, summary, body) {
347
+ const banner = [
348
+ '',
349
+ `About to install ${kind} "${summary.name}"`,
350
+ ` Description: ${summary.description}`,
351
+ ` Version: ${summary.version ?? '(unset)'}`,
352
+ ` Tools: ${summary.tools.join(', ') || '(none)'}`,
353
+ ` Files: ${summary.files}`,
354
+ ` Source: ${summary.source}`,
355
+ ` Target: ${summary.targetDir}`,
356
+ ` sha256: ${summary.sha256}`,
357
+ '',
358
+ 'This payload becomes part of the system prompt and may influence agent decisions.',
359
+ 'Trust this install? [y/N/info] ',
360
+ ].join('\n');
361
+ process.stdout.write(banner);
362
+ if (!input.isTTY) {
363
+ process.stdout.write('\n(non-interactive stdin; declining install)\n');
364
+ return 'no';
365
+ }
366
+ const rl = createInterface({ input, output });
367
+ try {
368
+ while (true) {
369
+ const answer = (await rl.question('')).trim().toLowerCase();
370
+ if (answer === 'y' || answer === 'yes')
371
+ return 'yes';
372
+ if (answer === 'info') {
373
+ process.stdout.write('\n');
374
+ process.stdout.write(body);
375
+ process.stdout.write('\n\nTrust this install? [y/N] ');
376
+ continue;
377
+ }
378
+ return 'no';
379
+ }
380
+ }
381
+ finally {
382
+ rl.close();
383
+ }
384
+ }
385
+ //# sourceMappingURL=agents.js.map