@pugi/cli 0.1.0-beta.24 → 0.1.0-beta.26

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 (41) hide show
  1. package/dist/core/checkpoint/resumer.js +149 -0
  2. package/dist/core/checkpoint/rewinder.js +291 -0
  3. package/dist/core/compact/summarizer.js +12 -0
  4. package/dist/core/dispatch/cache-cleanup.js +197 -0
  5. package/dist/core/dispatch/cache-handoff.js +295 -0
  6. package/dist/core/engine/native-pugi.js +67 -3
  7. package/dist/core/engine/tool-bridge.js +123 -3
  8. package/dist/core/hooks/events.js +44 -0
  9. package/dist/core/hooks/index.js +15 -0
  10. package/dist/core/hooks/registry.js +213 -0
  11. package/dist/core/hooks/runner.js +236 -0
  12. package/dist/core/lsp/cache.js +105 -0
  13. package/dist/core/lsp/language-detect.js +66 -0
  14. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  15. package/dist/core/memory-sync/queue.js +158 -0
  16. package/dist/core/memory-sync/queue.spec.js +105 -0
  17. package/dist/core/repl/session.js +73 -1
  18. package/dist/core/repl/slash-commands.js +20 -0
  19. package/dist/core/repl/store/session-store.js +31 -2
  20. package/dist/core/repo-map/build.js +125 -0
  21. package/dist/core/repo-map/cache.js +185 -0
  22. package/dist/core/repo-map/extractor.js +254 -0
  23. package/dist/core/repo-map/formatter.js +145 -0
  24. package/dist/core/repo-map/scanner.js +211 -0
  25. package/dist/core/session.js +44 -0
  26. package/dist/core/settings.js +9 -0
  27. package/dist/core/telemetry/emitter.js +229 -0
  28. package/dist/core/telemetry/queue.js +251 -0
  29. package/dist/runtime/cli.js +216 -0
  30. package/dist/runtime/commands/dispatch.js +126 -0
  31. package/dist/runtime/commands/hooks.js +184 -0
  32. package/dist/runtime/commands/lsp.js +25 -23
  33. package/dist/runtime/commands/memory.js +508 -0
  34. package/dist/runtime/commands/memory.spec.js +174 -0
  35. package/dist/runtime/commands/repo-map.js +95 -0
  36. package/dist/runtime/commands/resume.js +118 -0
  37. package/dist/runtime/commands/rewind.js +333 -0
  38. package/dist/runtime/commands/sessions.js +163 -0
  39. package/dist/runtime/version.js +1 -1
  40. package/dist/tools/agent-tool.js +23 -0
  41. package/package.json +2 -2
@@ -0,0 +1,126 @@
1
+ /**
2
+ * `pugi dispatch <sub>` — fork-subagent cache-handoff surface (Leak L10 — 2026-05-27).
3
+ *
4
+ * Sub-commands:
5
+ *
6
+ * - `pugi dispatch list-cache-refs` — show active cache-inherit refs
7
+ * persisted under `.pugi/cache-refs/`. Renders an aligned table or
8
+ * JSON depending on `--json`.
9
+ *
10
+ * - `pugi dispatch clear-cache-refs [--older-than 1h] [-v]` — GC stale
11
+ * refs. Without `--older-than`, defaults to 24h (the same cutoff
12
+ * the REPL boot-time auto-sweep uses). The flag accepts duration
13
+ * strings the `parseDuration` helper recognises (`1h`, `30m`,
14
+ * `7d`, `500ms`, ...).
15
+ *
16
+ * The command lives in its own module (not inline in cli.ts) so the
17
+ * dispatch table stays narrow and tests can drive it without spinning
18
+ * up the full CLI.
19
+ */
20
+ import { cleanupStaleCacheRefs, parseDuration, } from '../../core/dispatch/cache-cleanup.js';
21
+ import { listCacheRefs } from '../../core/dispatch/cache-handoff.js';
22
+ const USAGE = [
23
+ 'Usage:',
24
+ ' pugi dispatch list-cache-refs',
25
+ ' Show every active subagent cache-inherit',
26
+ ' reference persisted under .pugi/cache-refs/.',
27
+ '',
28
+ ' pugi dispatch clear-cache-refs [--older-than <duration>] [-v]',
29
+ ' Evict stale refs. Accepts 500ms / 30s / 5m /',
30
+ ' 1h / 7d. Default --older-than 24h.',
31
+ ].join('\n');
32
+ export async function runDispatchCommand(args, ctx) {
33
+ const sub = args[0];
34
+ if (!sub || sub === '--help' || sub === '-h') {
35
+ ctx.writeOutput({ command: 'dispatch', usage: USAGE.split('\n') }, USAGE);
36
+ return;
37
+ }
38
+ switch (sub) {
39
+ case 'list-cache-refs':
40
+ return runListCacheRefs(ctx);
41
+ case 'clear-cache-refs':
42
+ return runClearCacheRefs(args.slice(1), ctx);
43
+ default:
44
+ ctx.writeOutput({
45
+ ok: false,
46
+ error: `unknown subcommand: ${sub}`,
47
+ usage: USAGE.split('\n'),
48
+ }, `pugi dispatch: unknown subcommand '${sub}'\n\n${USAGE}`);
49
+ process.exitCode = 2;
50
+ return;
51
+ }
52
+ }
53
+ function runListCacheRefs(ctx) {
54
+ const refs = listCacheRefs(ctx.workspaceRoot);
55
+ if (ctx.json) {
56
+ ctx.writeOutput({ ok: true, count: refs.length, refs }, '');
57
+ return;
58
+ }
59
+ if (refs.length === 0) {
60
+ ctx.writeOutput({ ok: true, count: 0, refs: [] }, 'No active cache-inherit refs under .pugi/cache-refs/.');
61
+ return;
62
+ }
63
+ // Aligned column table. Widths derived from the data — no hardcoded
64
+ // sizes so a long childAgentId does not overflow the layout.
65
+ const header = ['CHILD AGENT', 'CACHE ID', 'PARENT SESSION', 'CREATED'];
66
+ const rows = refs.map((r) => [
67
+ r.childAgentId,
68
+ r.cacheId,
69
+ r.parentSessionId,
70
+ r.createdAt,
71
+ ]);
72
+ const widths = header.map((h, i) => Math.max(h.length, ...rows.map((row) => (row[i] ?? '').length)));
73
+ const lines = [];
74
+ lines.push(header.map((h, i) => h.padEnd(widths[i] ?? 0)).join(' '));
75
+ lines.push(widths.map((w) => '-'.repeat(w)).join(' '));
76
+ for (const row of rows) {
77
+ lines.push(row.map((cell, i) => (cell ?? '').padEnd(widths[i] ?? 0)).join(' '));
78
+ }
79
+ ctx.writeOutput({ ok: true, count: refs.length, refs }, lines.join('\n'));
80
+ }
81
+ function runClearCacheRefs(args, ctx) {
82
+ let olderThanArg;
83
+ let verbose = false;
84
+ for (let i = 0; i < args.length; i++) {
85
+ const arg = args[i];
86
+ if (arg === '--older-than') {
87
+ olderThanArg = args[i + 1];
88
+ i++;
89
+ continue;
90
+ }
91
+ if (arg && arg.startsWith('--older-than=')) {
92
+ olderThanArg = arg.slice('--older-than='.length);
93
+ continue;
94
+ }
95
+ if (arg === '-v' || arg === '--verbose') {
96
+ verbose = true;
97
+ continue;
98
+ }
99
+ }
100
+ // Default 24h matches the REPL boot-time auto-sweep cutoff.
101
+ const olderThanMs = olderThanArg ? parseDuration(olderThanArg) : 24 * 60 * 60 * 1000;
102
+ if (olderThanMs === null) {
103
+ ctx.writeOutput({
104
+ ok: false,
105
+ error: `invalid --older-than value: ${olderThanArg ?? '(missing)'}`,
106
+ usage: USAGE.split('\n'),
107
+ }, `pugi dispatch clear-cache-refs: invalid --older-than '${olderThanArg ?? '(missing)'}'\n\n${USAGE}`);
108
+ process.exitCode = 2;
109
+ return;
110
+ }
111
+ const result = cleanupStaleCacheRefs(ctx.workspaceRoot, {
112
+ olderThanMs,
113
+ verbose,
114
+ });
115
+ const summary = `Removed ${result.removedCount} ref(s) (${result.corruptCount} corrupt), kept ${result.keptCount}.`;
116
+ ctx.writeOutput({
117
+ ok: true,
118
+ olderThanMs,
119
+ removedCount: result.removedCount,
120
+ keptCount: result.keptCount,
121
+ corruptCount: result.corruptCount,
122
+ removed: result.removed,
123
+ corrupt: result.corrupt,
124
+ }, summary);
125
+ }
126
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1,184 @@
1
+ /**
2
+ * `pugi hooks` — operator surface for user-config hooks (Leak L12 MVP).
3
+ *
4
+ * Two subcommands ship in the MVP:
5
+ *
6
+ * pugi hooks list List configured hooks per event.
7
+ * pugi hooks doctor Validate the config and surface any
8
+ * parse / schema errors.
9
+ *
10
+ * Both accept `--json` for scripted callers. Argument grammar is
11
+ * intentionally narrow — no `add` / `remove` / `test` subcommands in
12
+ * the MVP. Operators hand-edit `~/.pugi/hooks-mvp.json` for now.
13
+ *
14
+ * Exit codes:
15
+ * 0 -> happy path (no hooks OR config valid).
16
+ * 1 -> config present but invalid (only `doctor` returns this).
17
+ * 2 -> unknown subcommand / argument error.
18
+ *
19
+ * Brand voice: ASCII only.
20
+ */
21
+ import { ALL_HOOK_EVENTS_V2, defaultHooksMvpPath, loadHooksConfig, } from '../../core/hooks/index.js';
22
+ function parseFlags(args) {
23
+ const rest = [];
24
+ const flags = { json: false };
25
+ for (const arg of args) {
26
+ if (arg === '--json') {
27
+ flags.json = true;
28
+ continue;
29
+ }
30
+ rest.push(arg);
31
+ }
32
+ return { rest, flags };
33
+ }
34
+ /**
35
+ * Top-level dispatcher for `pugi hooks <subcommand>`. Returns the
36
+ * intended process exit code. `cli.ts` is expected to set
37
+ * `process.exitCode = <return value>` so error states propagate to
38
+ * scripted callers without throwing.
39
+ */
40
+ export async function runHooksCommand(args, ctx) {
41
+ const { rest, flags } = parseFlags(args);
42
+ const sub = rest[0];
43
+ if (!sub || sub === 'help' || sub === '--help') {
44
+ emitUsage(ctx, flags);
45
+ return sub ? 0 : 2;
46
+ }
47
+ if (sub === 'list') {
48
+ return runList(ctx, flags);
49
+ }
50
+ if (sub === 'doctor') {
51
+ return runDoctor(ctx, flags);
52
+ }
53
+ ctx.writeOutput({ ok: false, error: `unknown subcommand: ${sub}` }, `pugi hooks: unknown subcommand '${sub}'. Try 'pugi hooks --help'.`);
54
+ return 2;
55
+ }
56
+ function emitUsage(ctx, flags) {
57
+ const text = [
58
+ 'pugi hooks — user-config lifecycle hooks (MVP).',
59
+ '',
60
+ 'Subcommands:',
61
+ ' pugi hooks list Show hooks configured per event.',
62
+ ' pugi hooks doctor Validate ~/.pugi/hooks-mvp.json.',
63
+ '',
64
+ 'Flags:',
65
+ ' --json Emit a JSON envelope instead of human text.',
66
+ '',
67
+ 'Config file:',
68
+ ' ~/.pugi/hooks-mvp.json',
69
+ '',
70
+ 'Status:',
71
+ ' MVP — 2 events out of 8. Remaining events (PostToolUse,',
72
+ " UserPromptSubmit, Stop, SubagentStop, PreCompact, Notification)",
73
+ ' deferred to fast-follow PR.',
74
+ ].join('\n');
75
+ ctx.writeOutput({
76
+ ok: true,
77
+ command: 'hooks',
78
+ usage: text,
79
+ }, text);
80
+ if (flags.json) {
81
+ // The structured payload is already emitted by writeOutput when
82
+ // --json is on; nothing extra to do.
83
+ }
84
+ }
85
+ function runList(ctx, flags) {
86
+ let config;
87
+ try {
88
+ config = loadHooksConfig(ctx.configPath);
89
+ }
90
+ catch (error) {
91
+ const msg = error.message;
92
+ ctx.writeOutput({ ok: false, error: msg }, `pugi hooks list: ${msg}\nFix the config or remove the file. Run 'pugi hooks doctor' for details.`);
93
+ return 1;
94
+ }
95
+ const perEvent = {
96
+ SessionStart: [],
97
+ PreToolUse: [],
98
+ PostToolUse: [],
99
+ UserPromptSubmit: [],
100
+ Stop: [],
101
+ SubagentStop: [],
102
+ PreCompact: [],
103
+ Notification: [],
104
+ };
105
+ for (const event of ALL_HOOK_EVENTS_V2) {
106
+ perEvent[event] = config.list(event).map((entry) => ({
107
+ matcher: entry.matcher,
108
+ command: entry.command,
109
+ timeoutMs: entry.timeoutMs,
110
+ blocking: entry.blocking,
111
+ }));
112
+ }
113
+ const total = Object.values(perEvent).reduce((acc, list) => acc + list.length, 0);
114
+ const payload = {
115
+ ok: true,
116
+ configPath: config.configPath(),
117
+ total,
118
+ perEvent,
119
+ };
120
+ if (flags.json) {
121
+ ctx.writeOutput(payload, JSON.stringify(payload, null, 2));
122
+ return 0;
123
+ }
124
+ const lines = [];
125
+ lines.push(`pugi hooks (${total} configured)`);
126
+ lines.push(` config: ${config.configPath()}`);
127
+ if (total === 0) {
128
+ lines.push(' no hooks configured — create the file above to add one.');
129
+ }
130
+ else {
131
+ for (const event of ALL_HOOK_EVENTS_V2) {
132
+ const list = perEvent[event];
133
+ if (list.length === 0)
134
+ continue;
135
+ lines.push(` ${event}:`);
136
+ for (const entry of list) {
137
+ const tags = [];
138
+ if (entry.matcher)
139
+ tags.push(`matcher=${entry.matcher}`);
140
+ if (entry.timeoutMs)
141
+ tags.push(`timeoutMs=${entry.timeoutMs}`);
142
+ if (entry.blocking)
143
+ tags.push('blocking');
144
+ const suffix = tags.length ? ` [${tags.join(', ')}]` : '';
145
+ lines.push(` - ${entry.command}${suffix}`);
146
+ }
147
+ }
148
+ }
149
+ const text = lines.join('\n');
150
+ ctx.writeOutput(payload, text);
151
+ return 0;
152
+ }
153
+ function runDoctor(ctx, flags) {
154
+ const path = ctx.configPath ?? defaultHooksMvpPath();
155
+ try {
156
+ const config = loadHooksConfig(ctx.configPath);
157
+ const total = config.flatten().length;
158
+ const payload = {
159
+ ok: true,
160
+ configPath: config.configPath(),
161
+ total,
162
+ issues: [],
163
+ };
164
+ const text = total
165
+ ? `pugi hooks doctor: ${path} OK (${total} hooks).`
166
+ : `pugi hooks doctor: ${path} not present (no hooks configured).`;
167
+ ctx.writeOutput(payload, text);
168
+ return 0;
169
+ }
170
+ catch (error) {
171
+ const msg = error.message;
172
+ const payload = {
173
+ ok: false,
174
+ configPath: path,
175
+ error: msg,
176
+ };
177
+ const text = `pugi hooks doctor: ${msg}`;
178
+ ctx.writeOutput(payload, text);
179
+ return 1;
180
+ }
181
+ // flags.json is consumed by writeOutput in the host shell.
182
+ void flags;
183
+ }
184
+ //# sourceMappingURL=hooks.js.map
@@ -24,8 +24,8 @@
24
24
  *
25
25
  * Brand voice: ASCII only, no emoji, no banned words.
26
26
  */
27
- import { extname } from 'node:path';
28
27
  import { inspectLspServers, startLspClient } from '../../core/lsp/client.js';
28
+ import { languageForFile as inferLanguage } from '../../core/lsp/language-detect.js';
29
29
  import { loadSettings } from '../../core/settings.js';
30
30
  export async function runLspCommand(args, opts) {
31
31
  const [op, file, ...rest] = args;
@@ -96,6 +96,25 @@ export async function runLspCommand(args, opts) {
96
96
  };
97
97
  }
98
98
  const settings = loadSettings(opts.cwd);
99
+ // Leak L15 (2026-05-27): `pugi lsp check <file>` — manual probe of
100
+ // the post-edit diagnostics pipeline. Identical output to what the
101
+ // model sees appended to its tool envelope after a successful
102
+ // edit/write. Lets operators dry-run the auto-diagnostic surface
103
+ // without dispatching an actual edit.
104
+ if (op === 'check') {
105
+ const { runPostEditDiagnostics } = await import('../../core/lsp/post-edit-diagnostics.js');
106
+ const result = await runPostEditDiagnostics(file, {
107
+ cwd: opts.cwd,
108
+ ...(settings.lsp ? { lspSettings: settings.lsp } : {}),
109
+ });
110
+ if (opts.json) {
111
+ return { ok: true, text: JSON.stringify(result, null, 2), exitCode: 0 };
112
+ }
113
+ if (result.skip) {
114
+ return { ok: true, text: `${file}: skipped (${result.reason})`, exitCode: 0 };
115
+ }
116
+ return { ok: true, text: result.tail, exitCode: 0 };
117
+ }
99
118
  const clientResult = await startLspClient(lang, {
100
119
  cwd: opts.cwd,
101
120
  ...(settings.lsp ? { lspSettings: settings.lsp } : {}),
@@ -204,6 +223,7 @@ function usage() {
204
223
  ' pugi lsp definition <file> <line> <col>\n' +
205
224
  ' pugi lsp references <file> <line> <col>\n' +
206
225
  ' pugi lsp diagnostics <file>\n' +
226
+ ' pugi lsp check <file> (Leak L15: probe post-edit tail)\n' +
207
227
  ' pugi lsp find_definition <file> <symbol>\n' +
208
228
  ' pugi lsp servers',
209
229
  exitCode: 2,
@@ -341,26 +361,8 @@ function pullLangFlag(args) {
341
361
  function isLspLanguage(value) {
342
362
  return value === 'ts' || value === 'js' || value === 'py' || value === 'go' || value === 'rust';
343
363
  }
344
- export function inferLanguage(file) {
345
- const ext = extname(file).toLowerCase();
346
- switch (ext) {
347
- case '.ts':
348
- case '.tsx':
349
- return 'ts';
350
- case '.js':
351
- case '.jsx':
352
- case '.mjs':
353
- case '.cjs':
354
- return 'js';
355
- case '.py':
356
- case '.pyi':
357
- return 'py';
358
- case '.go':
359
- return 'go';
360
- case '.rs':
361
- return 'rust';
362
- default:
363
- return undefined;
364
- }
365
- }
364
+ // Leak L15 (2026-05-27): single source of truth for ext → language
365
+ // lives in `core/lsp/language-detect.ts`. The CLI surface re-exports
366
+ // the lookup so existing call sites keep their import path.
367
+ export { inferLanguage };
366
368
  //# sourceMappingURL=lsp.js.map