@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,231 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { loadMcpRegistry } from '../../core/mcp/registry.js';
6
+ import { listMcpTrust, setMcpTrust, } from '../../core/mcp/trust.js';
7
+ import { trustWorkspace } from '../../core/trust.js';
8
+ /**
9
+ * `pugi config` — operator-level configuration surface.
10
+ *
11
+ * Subcommands:
12
+ * - `pugi config get <key>` read a value from `~/.pugi/config.json`
13
+ * - `pugi config set <key> <value>` write a value
14
+ * - `pugi config list` dump all values
15
+ * - `pugi config trust .` trust the current workspace (delegates to core/trust.ts)
16
+ * - `pugi config mcp trust <name>` flip MCP server to trusted
17
+ * - `pugi config mcp deny <name>` flip MCP server to denied
18
+ * - `pugi config mcp list` show declared servers + their trust state
19
+ *
20
+ * Schema (pugi-config-v1):
21
+ * {
22
+ * "permissionMode": "ask" | "acceptEdits" | "auto" | "plan" | "dontAsk" | "bypassPermissions",
23
+ * "privacy": "local-only" | "metadata" | "full",
24
+ * "model": "<id>" | null,
25
+ * "preferredEndpoint": "https://api.pugi.io"
26
+ * }
27
+ *
28
+ * The config file lives at `~/.pugi/config.json` (PUGI_HOME-aware) and uses
29
+ * mode 0o600. Unknown keys are rejected by `set` so a typo never silently
30
+ * persists.
31
+ */
32
+ const configSchema = z
33
+ .object({
34
+ permissionMode: z
35
+ .enum(['plan', 'ask', 'acceptEdits', 'auto', 'dontAsk', 'bypassPermissions'])
36
+ .optional(),
37
+ privacy: z.enum(['local-only', 'metadata', 'full']).optional(),
38
+ model: z.string().nullable().optional(),
39
+ preferredEndpoint: z.string().url().optional(),
40
+ })
41
+ .strict();
42
+ const CONFIG_KEYS = ['permissionMode', 'privacy', 'model', 'preferredEndpoint'];
43
+ export async function runConfigCommand(args, ctx) {
44
+ const sub = args[0];
45
+ if (!sub || sub === '--help' || sub === '-h') {
46
+ ctx.writeOutput({
47
+ command: 'config',
48
+ usage: [
49
+ 'pugi config get <key>',
50
+ 'pugi config set <key> <value>',
51
+ 'pugi config list',
52
+ 'pugi config trust .',
53
+ 'pugi config mcp trust <name>',
54
+ 'pugi config mcp deny <name>',
55
+ 'pugi config mcp list',
56
+ ],
57
+ }, [
58
+ 'Usage:',
59
+ ' pugi config get <key> Read a config value.',
60
+ ' pugi config set <key> <value> Write a config value.',
61
+ ' pugi config list Show all config values.',
62
+ ' pugi config trust . Trust the current workspace for hooks + MCP.',
63
+ ' pugi config mcp trust <name> Mark an MCP server as trusted.',
64
+ ' pugi config mcp deny <name> Block an MCP server.',
65
+ ' pugi config mcp list Show declared MCP servers + trust state.',
66
+ ].join('\n'));
67
+ return;
68
+ }
69
+ switch (sub) {
70
+ case 'get':
71
+ return runConfigGet(args.slice(1), ctx);
72
+ case 'set':
73
+ return runConfigSet(args.slice(1), ctx);
74
+ case 'list':
75
+ return runConfigList(ctx);
76
+ case 'trust':
77
+ return runConfigTrust(args.slice(1), ctx);
78
+ case 'mcp':
79
+ return runConfigMcp(args.slice(1), ctx);
80
+ default:
81
+ throw new Error(`Unknown sub-command "pugi config ${sub}". Expected get, set, list, trust, or mcp.`);
82
+ }
83
+ }
84
+ function configPath() {
85
+ const home = process.env.PUGI_HOME ?? resolve(homedir(), '.pugi');
86
+ return resolve(home, 'config.json');
87
+ }
88
+ export function readConfig() {
89
+ const path = configPath();
90
+ if (!existsSync(path))
91
+ return {};
92
+ const raw = readFileSync(path, 'utf8');
93
+ if (raw.trim() === '')
94
+ return {};
95
+ const parsed = JSON.parse(raw);
96
+ return configSchema.parse(parsed);
97
+ }
98
+ function writeConfig(config) {
99
+ const path = configPath();
100
+ mkdirSync(dirname(path), { recursive: true });
101
+ // 0o600 — `preferredEndpoint` could be a private self-hosted URL; the
102
+ // config does not contain secrets, but file mode parity with the trust
103
+ // ledger keeps the audit surface uniform.
104
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, {
105
+ encoding: 'utf8',
106
+ mode: 0o600,
107
+ });
108
+ }
109
+ function isConfigKey(value) {
110
+ return CONFIG_KEYS.includes(value);
111
+ }
112
+ function runConfigGet(args, ctx) {
113
+ const key = args[0];
114
+ if (!key)
115
+ throw new Error('pugi config get requires a key.');
116
+ if (!isConfigKey(key)) {
117
+ throw new Error(`Unknown config key "${key}". Allowed: ${CONFIG_KEYS.join(', ')}.`);
118
+ }
119
+ const config = readConfig();
120
+ const value = config[key] ?? null;
121
+ ctx.writeOutput({ command: 'config.get', key, value }, value === null || value === undefined ? `${key} = (unset)` : `${key} = ${String(value)}`);
122
+ }
123
+ function runConfigSet(args, ctx) {
124
+ const key = args[0];
125
+ const value = args.slice(1).join(' ');
126
+ if (!key)
127
+ throw new Error('pugi config set requires a key.');
128
+ if (value.length === 0)
129
+ throw new Error('pugi config set requires a value.');
130
+ if (!isConfigKey(key)) {
131
+ throw new Error(`Unknown config key "${key}". Allowed: ${CONFIG_KEYS.join(', ')}.`);
132
+ }
133
+ const current = readConfig();
134
+ // Build the candidate and validate via the schema so an invalid value
135
+ // (e.g. `permissionMode = nonsense`) is rejected before persistence.
136
+ const candidate = { ...current };
137
+ candidate[key] = coerceValue(key, value);
138
+ const validated = configSchema.parse(candidate);
139
+ writeConfig(validated);
140
+ ctx.writeOutput({
141
+ command: 'config.set',
142
+ key,
143
+ value: validated[key] ?? null,
144
+ }, `${key} = ${String(validated[key] ?? '')}`);
145
+ }
146
+ function coerceValue(key, raw) {
147
+ if (key === 'model') {
148
+ return raw === 'null' || raw === '' ? null : raw;
149
+ }
150
+ return raw;
151
+ }
152
+ function runConfigList(ctx) {
153
+ const config = readConfig();
154
+ const entries = CONFIG_KEYS.map((key) => ({
155
+ key,
156
+ value: config[key] ?? null,
157
+ }));
158
+ ctx.writeOutput({ command: 'config.list', config, entries }, entries
159
+ .map((entry) => entry.value === null || entry.value === undefined
160
+ ? `${entry.key} = (unset)`
161
+ : `${entry.key} = ${String(entry.value)}`)
162
+ .join('\n'));
163
+ }
164
+ async function runConfigTrust(args, ctx) {
165
+ const target = args[0];
166
+ if (!target)
167
+ throw new Error('pugi config trust requires a path (use "." for cwd).');
168
+ const root = target === '.' ? ctx.workspaceRoot : resolve(ctx.workspaceRoot, target);
169
+ // Identity for the audit entry: prefer the explicit env override
170
+ // (`PUGI_TRUSTED_BY`), then `USER` from the shell, then literal
171
+ // 'cli'. The trust ledger requires a non-empty string and we want
172
+ // it to mean something for a future audit replay.
173
+ const by = process.env.PUGI_TRUSTED_BY?.trim() ||
174
+ process.env.USER?.trim() ||
175
+ process.env.USERNAME?.trim() ||
176
+ 'cli';
177
+ await trustWorkspace(root, by);
178
+ ctx.writeOutput({ command: 'config.trust', workspaceRoot: root, trustedBy: by }, `Trusted workspace: ${root}`);
179
+ }
180
+ async function runConfigMcp(args, ctx) {
181
+ const sub = args[0];
182
+ if (!sub) {
183
+ throw new Error('pugi config mcp requires a sub-command: trust, deny, or list.');
184
+ }
185
+ switch (sub) {
186
+ case 'list':
187
+ return runConfigMcpList(ctx);
188
+ case 'trust':
189
+ return runConfigMcpFlip(args.slice(1), ctx, 'trusted');
190
+ case 'deny':
191
+ return runConfigMcpFlip(args.slice(1), ctx, 'denied');
192
+ default:
193
+ throw new Error(`Unknown sub-command "pugi config mcp ${sub}". Expected trust, deny, or list.`);
194
+ }
195
+ }
196
+ async function runConfigMcpList(ctx) {
197
+ const registry = await loadMcpRegistry(ctx.workspaceRoot, { connect: false });
198
+ const declared = Array.from(registry.servers.values()).map((state) => ({
199
+ name: state.name,
200
+ command: state.config.command,
201
+ args: state.config.args,
202
+ trust: state.trust,
203
+ surfacedTools: state.surfacedTools.length,
204
+ lastError: state.lastError ?? null,
205
+ }));
206
+ const ledger = await listMcpTrust();
207
+ await registry.shutdown();
208
+ if (declared.length === 0) {
209
+ ctx.writeOutput({ command: 'config.mcp.list', servers: [], ledger }, 'No MCP servers declared. Add one to .pugi/mcp.json or ~/.pugi/mcp.json.');
210
+ return;
211
+ }
212
+ ctx.writeOutput({ command: 'config.mcp.list', servers: declared, ledger }, [
213
+ 'MCP servers:',
214
+ ...declared.map((server) => ` ${server.name.padEnd(20)} ${server.trust.padEnd(8)} ${server.command} ${server.args.join(' ')}`),
215
+ ].join('\n'));
216
+ }
217
+ async function runConfigMcpFlip(args, ctx, state) {
218
+ const name = args[0];
219
+ if (!name) {
220
+ throw new Error(`pugi config mcp ${state === 'trusted' ? 'trust' : 'deny'} requires a server name.`);
221
+ }
222
+ const by = process.env.PUGI_TRUSTED_BY?.trim() ||
223
+ process.env.USER?.trim() ||
224
+ process.env.USERNAME?.trim() ||
225
+ 'cli';
226
+ await setMcpTrust(name, state, by);
227
+ ctx.writeOutput({ command: `config.mcp.${state === 'trusted' ? 'trust' : 'deny'}`, name, state, decidedBy: by }, state === 'trusted'
228
+ ? `MCP server "${name}" is now trusted.`
229
+ : `MCP server "${name}" is now denied.`);
230
+ }
231
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1,107 @@
1
+ import { z } from 'zod';
2
+ import { readConfig } from './config.js';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, resolve } from 'node:path';
6
+ /**
7
+ * `pugi privacy` — read or update the operator's privacy mode.
8
+ *
9
+ * Subcommands:
10
+ * - `pugi privacy show` — print current mode + source
11
+ * - `pugi privacy set <mode>` — write `local-only | metadata | full`
12
+ *
13
+ * Persistence:
14
+ * - Stored in `~/.pugi/privacy.json` (PUGI_HOME-aware).
15
+ * - The `privacy` key in `~/.pugi/config.json` is also consulted at
16
+ * read time (config takes precedence when both exist) so a user who
17
+ * already set `pugi config set privacy ...` sees the same value
18
+ * here.
19
+ *
20
+ * Modes:
21
+ * - `local-only` — nothing leaves the workstation. Engine commands
22
+ * refuse to ship transcripts; sync is a no-op.
23
+ * - `metadata` — only artifact metadata + session timeline ships
24
+ * (no raw file contents).
25
+ * - `full` — full sync allowed (operator-acknowledged).
26
+ */
27
+ export const privacyModeSchema = z.enum(['local-only', 'metadata', 'full']);
28
+ const privacyFileSchema = z.object({
29
+ schema: z.number().int().positive().default(1),
30
+ mode: privacyModeSchema,
31
+ });
32
+ function privacyPath() {
33
+ const home = process.env.PUGI_HOME ?? resolve(homedir(), '.pugi');
34
+ return resolve(home, 'privacy.json');
35
+ }
36
+ export function readPrivacyFile() {
37
+ const path = privacyPath();
38
+ if (!existsSync(path))
39
+ return null;
40
+ const raw = readFileSync(path, 'utf8');
41
+ if (raw.trim() === '')
42
+ return null;
43
+ const parsed = JSON.parse(raw);
44
+ const result = privacyFileSchema.safeParse(parsed);
45
+ if (!result.success)
46
+ return null;
47
+ return result.data.mode;
48
+ }
49
+ function writePrivacyFile(mode) {
50
+ const path = privacyPath();
51
+ mkdirSync(dirname(path), { recursive: true });
52
+ writeFileSync(path, `${JSON.stringify({ schema: 1, mode }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
53
+ }
54
+ /**
55
+ * Effective privacy mode resolution order:
56
+ * 1. `~/.pugi/config.json` privacy key (when set via `pugi config set`)
57
+ * 2. `~/.pugi/privacy.json` (when set via `pugi privacy set`)
58
+ * 3. `metadata` (default — matches the M1 default-ship posture)
59
+ */
60
+ export function resolvePrivacyMode() {
61
+ const config = readConfig();
62
+ if (config.privacy)
63
+ return { mode: config.privacy, source: 'config' };
64
+ const fromFile = readPrivacyFile();
65
+ if (fromFile)
66
+ return { mode: fromFile, source: 'privacy' };
67
+ return { mode: 'metadata', source: 'default' };
68
+ }
69
+ export async function runPrivacyCommand(args, ctx) {
70
+ const sub = args[0];
71
+ if (!sub || sub === '--help' || sub === '-h') {
72
+ ctx.writeOutput({
73
+ command: 'privacy',
74
+ usage: ['pugi privacy show', 'pugi privacy set <mode>'],
75
+ modes: ['local-only', 'metadata', 'full'],
76
+ }, [
77
+ 'Usage:',
78
+ ' pugi privacy show Show current privacy mode.',
79
+ ' pugi privacy set <mode> Set mode to local-only, metadata, or full.',
80
+ ].join('\n'));
81
+ return;
82
+ }
83
+ switch (sub) {
84
+ case 'show':
85
+ return runPrivacyShow(ctx);
86
+ case 'set':
87
+ return runPrivacySet(args.slice(1), ctx);
88
+ default:
89
+ throw new Error(`Unknown sub-command "pugi privacy ${sub}". Expected show or set.`);
90
+ }
91
+ }
92
+ function runPrivacyShow(ctx) {
93
+ const resolved = resolvePrivacyMode();
94
+ ctx.writeOutput({ command: 'privacy.show', ...resolved }, `Privacy mode: ${resolved.mode} (source: ${resolved.source})`);
95
+ }
96
+ function runPrivacySet(args, ctx) {
97
+ const raw = args[0];
98
+ if (!raw)
99
+ throw new Error('pugi privacy set requires a mode: local-only, metadata, or full.');
100
+ const result = privacyModeSchema.safeParse(raw);
101
+ if (!result.success) {
102
+ throw new Error(`Invalid privacy mode "${raw}". Allowed: local-only, metadata, full.`);
103
+ }
104
+ writePrivacyFile(result.data);
105
+ ctx.writeOutput({ command: 'privacy.set', mode: result.data }, `Privacy mode set to ${result.data}.`);
106
+ }
107
+ //# sourceMappingURL=privacy.js.map