@pugi/cli 0.1.0-alpha.6 → 0.1.0-alpha.8

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.
@@ -1,31 +1,99 @@
1
1
  /**
2
- * REPL slash command registry - Sprint α5.7 (ADR-0056 PR-PUGI-CLI-REPL-DEFAULT).
2
+ * REPL slash command registry Sprint α5.7, expanded α6.14 wave 2.
3
3
  *
4
- * The REPL input box surfaces a small palette of slash commands that the
5
- * operator can run from inside a persistent session: dispatch a brief,
6
- * inspect the agent roster, stop a running persona, open the help
7
- * overlay, or quit. The registry is intentionally narrow at M1 -
8
- * complementary surfaces (`/sync`, `/handoff`, `/budget`) ship as proper
9
- * subcommands in α5.8+ and stay reachable from a non-REPL `pugi
10
- * <command>` invocation.
4
+ * The REPL input box surfaces a palette of slash commands the operator
5
+ * can run from inside a persistent session. The wave-2 expansion (CEO
6
+ * 2026-05-25) grows the surface from 6 to 20 commands so the `/help`
7
+ * overlay matches the breadth Claude Code / Codex CLI operators expect.
11
8
  *
12
- * The registry is pure: each entry returns a `SlashCommandResult`
13
- * describing what the REPL session should do next. The session module
14
- * owns the side effects (network calls, dispatcher invocation, exit).
15
- * Keeping the surface pure lets the unit test exercise every shape
16
- * without standing up an Ink runtime or an Anvil endpoint.
9
+ * The registry is pure: each `parseSlashCommand` call returns a
10
+ * `SlashCommandResult` describing what the REPL session should do next.
11
+ * The session module owns the side effects (network calls, dispatcher
12
+ * invocation, exit, transcript clear). Keeping the surface pure lets
13
+ * the unit test exercise every shape without standing up an Ink runtime
14
+ * or an Anvil endpoint.
15
+ *
16
+ * Tiering (per CEO wave-2 spec):
17
+ *
18
+ * Tier 1 — wired against real state (3 + existing 6 = 9 wired):
19
+ * brief, agents, stop, help, quit, web, clear, version, jobs.
20
+ *
21
+ * Tier 2 — best-effort wiring against existing surfaces (3):
22
+ * diff, cost, status.
23
+ *
24
+ * Tier 3 — deterministic stubs ("coming in αX.Y") (8):
25
+ * compact, resume, memory, config, privacy, budget, mcp, undo.
17
26
  *
18
27
  * Brand voice (brandbook §08): power words `brief / dispatch / stop /
19
- * agents / quit`. Tagline `Brief it. It ships.` reserved for `/quit`
20
- * confirmation and `/help` footer - never inline.
28
+ * agents / quit / shipped`. Tagline `Brief it. It ships.` reserved for
29
+ * `/quit` confirmation and `/help` footer never inline.
21
30
  */
22
31
  import { listRoles } from '../agents/registry.js';
32
+ /**
33
+ * Deterministic stub copy returned by the Tier 3 commands. Spec'd
34
+ * inline so the unit test can pin the exact text without poking at
35
+ * the help overlay. The version tag at the end maps to the sprint we
36
+ * intend to land the real wiring in.
37
+ */
38
+ export const SLASH_STUB_MESSAGES = Object.freeze({
39
+ brief: '',
40
+ agents: '',
41
+ stop: '',
42
+ help: '',
43
+ quit: '',
44
+ web: '',
45
+ clear: '',
46
+ version: '',
47
+ jobs: '',
48
+ diff: '',
49
+ cost: '',
50
+ status: '',
51
+ compact: 'Manual context compaction lands in α6.5.',
52
+ resume: 'Resume last session lands in α6.4 once SQLite session.db lands.',
53
+ memory: 'Session memory editor lands in α6.5.',
54
+ config: 'Run `pugi config list` from a fresh shell for the full surface; in-REPL editor lands in α6.5.',
55
+ privacy: 'Run `pugi privacy show` from a fresh shell; in-REPL toggle lands in α6.5.',
56
+ budget: 'Run `pugi budget` from a fresh shell; in-REPL summary lands in α6.5.',
57
+ mcp: 'Run `pugi config mcp list` from a fresh shell; in-REPL palette lands in α6.5.',
58
+ undo: 'Run `pugi undo` from a fresh shell; in-REPL undo lands in α6.5.',
59
+ });
23
60
  export const SLASH_COMMAND_HELP = Object.freeze([
24
- { name: 'brief', args: '<text>', gloss: 'Dispatch a brief to the workforce' },
25
- { name: 'agents', args: '', gloss: 'List the on-watch agent roster' },
26
- { name: 'stop', args: '<persona>', gloss: 'Stop one agent by persona slug' },
27
- { name: 'help', args: '', gloss: 'Show this help overlay' },
28
- { name: 'quit', args: '', gloss: 'Exit the REPL (session resumes via pugi resume)' },
61
+ // Workforce dispatch
62
+ { name: 'brief', args: '<text>', gloss: 'Dispatch a brief to the workforce', group: 'Workforce dispatch' },
63
+ { name: 'agents', args: '', gloss: 'List the on-watch agent roster', group: 'Workforce dispatch' },
64
+ { name: 'stop', args: '<persona>', gloss: 'Stop one agent by persona slug', group: 'Workforce dispatch' },
65
+ { name: 'jobs', args: '', gloss: 'List background jobs', group: 'Workforce dispatch' },
66
+ // Session
67
+ { name: 'clear', args: '', gloss: 'Clear conversation pane', group: 'Session' },
68
+ { name: 'resume', args: '', gloss: 'Resume last session (α6.4)', group: 'Session', stub: true },
69
+ { name: 'compact', args: '', gloss: 'Manual context compaction (α6.5)', group: 'Session', stub: true },
70
+ { name: 'memory', args: '', gloss: 'Session memory editor (α6.5)', group: 'Session', stub: true },
71
+ // Pugi tools
72
+ { name: 'web', args: '<url>', gloss: 'Fetch a URL into context', group: 'Pugi tools' },
73
+ { name: 'diff', args: '', gloss: 'Show pending diff', group: 'Pugi tools' },
74
+ { name: 'cost', args: '', gloss: 'Token usage + budget', group: 'Pugi tools' },
75
+ { name: 'status', args: '', gloss: 'Backend + tenant status', group: 'Pugi tools' },
76
+ // Settings
77
+ { name: 'config', args: '', gloss: 'Show config', group: 'Settings', stub: true },
78
+ { name: 'privacy', args: '', gloss: 'Show privacy mode', group: 'Settings', stub: true },
79
+ { name: 'budget', args: '', gloss: 'Show usage budget', group: 'Settings', stub: true },
80
+ { name: 'mcp', args: '', gloss: 'List MCP servers', group: 'Settings', stub: true },
81
+ { name: 'undo', args: '', gloss: 'Undo last write', group: 'Settings', stub: true },
82
+ // Meta
83
+ { name: 'help', args: '', gloss: 'Show this help overlay', group: 'Meta' },
84
+ { name: 'version', args: '', gloss: 'Show CLI version', group: 'Meta' },
85
+ { name: 'quit', args: '', gloss: 'Exit the REPL', group: 'Meta' },
86
+ ]);
87
+ /**
88
+ * Ordered list of groups. Drives the `/help` overlay sectioning so the
89
+ * operator reads commands by intent (dispatch first, meta last).
90
+ */
91
+ export const SLASH_COMMAND_GROUPS = Object.freeze([
92
+ 'Workforce dispatch',
93
+ 'Session',
94
+ 'Pugi tools',
95
+ 'Settings',
96
+ 'Meta',
29
97
  ]);
30
98
  /**
31
99
  * Parse one line of input from the REPL. The contract:
@@ -33,13 +101,15 @@ export const SLASH_COMMAND_HELP = Object.freeze([
33
101
  * - Empty / whitespace-only input returns `noop` with the original
34
102
  * text so the REPL can ignore it without printing anything.
35
103
  * - Input that does not start with `/` is treated as an implicit
36
- * `/brief <text>` - the most-common operator action.
104
+ * `/brief <text>` the most-common operator action.
37
105
  * - `/<name> [args]` resolves the name against the registry; unknown
38
106
  * names return `error` so the REPL can render a one-line tip
39
107
  * instead of silently dropping the input.
108
+ * - Tier 3 stubs return `{ kind: 'stub', name, message }` so the REPL
109
+ * can render the deterministic "coming in αX.Y" copy uniformly.
40
110
  *
41
111
  * The function never throws. Bad input maps to a structured result the
42
- * REPL can render - the alternative (throwing from a keystroke handler)
112
+ * REPL can render the alternative (throwing from a keystroke handler)
43
113
  * would unmount Ink mid-frame.
44
114
  */
45
115
  export function parseSlashCommand(input) {
@@ -90,6 +160,47 @@ export function parseSlashCommand(input) {
90
160
  case 'q': {
91
161
  return { kind: 'quit' };
92
162
  }
163
+ case 'web':
164
+ case 'fetch': {
165
+ if (tail.length === 0) {
166
+ return { kind: 'error', message: 'Usage: /web <url>' };
167
+ }
168
+ return { kind: 'web', url: tail };
169
+ }
170
+ case 'clear':
171
+ case 'cls': {
172
+ return { kind: 'clear' };
173
+ }
174
+ case 'version':
175
+ case 'v': {
176
+ return { kind: 'version' };
177
+ }
178
+ case 'jobs': {
179
+ return { kind: 'jobs' };
180
+ }
181
+ case 'diff': {
182
+ return { kind: 'diff' };
183
+ }
184
+ case 'cost': {
185
+ return { kind: 'cost' };
186
+ }
187
+ case 'status': {
188
+ return { kind: 'status' };
189
+ }
190
+ case 'compact':
191
+ case 'resume':
192
+ case 'memory':
193
+ case 'config':
194
+ case 'privacy':
195
+ case 'budget':
196
+ case 'mcp':
197
+ case 'undo': {
198
+ return {
199
+ kind: 'stub',
200
+ name: name,
201
+ message: SLASH_STUB_MESSAGES[name],
202
+ };
203
+ }
93
204
  default: {
94
205
  return {
95
206
  kind: 'error',
@@ -34,6 +34,19 @@ const pugiSettingsSchema = z.object({
34
34
  promoteExplicitly: z.boolean().default(true),
35
35
  })
36
36
  .default({}),
37
+ // `web.fetch.enabled` gates the `pugi web` / `/web` SSRF-guarded
38
+ // fetcher. Default-off matches the spec posture; the schema must
39
+ // declare it explicitly because Zod's strict-pass strips unknown
40
+ // keys and would silently swallow the operator's intent.
41
+ web: z
42
+ .object({
43
+ fetch: z
44
+ .object({
45
+ enabled: z.boolean().optional(),
46
+ })
47
+ .optional(),
48
+ })
49
+ .optional(),
37
50
  });
38
51
  export function loadSettings(root) {
39
52
  const settingsPath = resolve(root, '.pugi/settings.json');