@pugi/cli 0.1.0-beta.5 → 0.1.0-beta.50
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.
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/assets/pugi-mascot.ansi +15 -25
- package/assets/pugi-prozr2-mascot.ansi +9 -0
- package/bin/run.js +33 -1
- package/dist/commands/jobs-watch.js +201 -0
- package/dist/commands/jobs.js +15 -0
- package/dist/commands/smoke.js +133 -0
- package/dist/core/agent-progress/cleanup.js +134 -0
- package/dist/core/agent-progress/schema.js +144 -0
- package/dist/core/agent-progress/writer.js +101 -0
- package/dist/core/artifact-chain/dispatcher.js +148 -0
- package/dist/core/artifact-chain/exporter.js +164 -0
- package/dist/core/artifact-chain/state.js +243 -0
- package/dist/core/artifact-chain/steps.js +169 -0
- package/dist/core/auth/ensure-authenticated.js +129 -0
- package/dist/core/auth/env-provider.js +238 -0
- package/dist/core/auto-update/channels.js +122 -0
- package/dist/core/auto-update/checker.js +241 -0
- package/dist/core/auto-update/state.js +235 -0
- package/dist/core/bare-mode/index.js +107 -0
- package/dist/core/bash-classifier.js +400 -4
- package/dist/core/checkpoint/resumer.js +149 -0
- package/dist/core/checkpoint/rewinder.js +291 -0
- package/dist/core/codegraph/decision-store.js +248 -0
- package/dist/core/codegraph/detect-repo.js +459 -0
- package/dist/core/codegraph/install.js +134 -0
- package/dist/core/codegraph/offer-hook.js +220 -0
- package/dist/core/compact/auto-trigger.js +96 -0
- package/dist/core/compact/buffer-rewriter.js +115 -0
- package/dist/core/compact/summarizer.js +208 -0
- package/dist/core/compact/token-counter.js +108 -0
- package/dist/core/consensus/diff-capture.js +112 -3
- package/dist/core/context/index.js +7 -0
- package/dist/core/context/markdown-traverse.js +255 -0
- package/dist/core/cost/rate-card.js +129 -0
- package/dist/core/cost/tracker.js +221 -0
- package/dist/core/denial-tracking/index.js +8 -0
- package/dist/core/denial-tracking/state.js +264 -0
- package/dist/core/diagnostics/probe-runner.js +93 -0
- package/dist/core/diagnostics/probes/api.js +46 -0
- package/dist/core/diagnostics/probes/auth.js +86 -0
- package/dist/core/diagnostics/probes/bare-mode.js +42 -0
- package/dist/core/diagnostics/probes/cli-version.js +127 -0
- package/dist/core/diagnostics/probes/config.js +72 -0
- package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
- package/dist/core/diagnostics/probes/disk.js +81 -0
- package/dist/core/diagnostics/probes/git.js +65 -0
- package/dist/core/diagnostics/probes/hooks.js +118 -0
- package/dist/core/diagnostics/probes/mcp.js +75 -0
- package/dist/core/diagnostics/probes/node.js +59 -0
- package/dist/core/diagnostics/probes/pnpm.js +36 -0
- package/dist/core/diagnostics/probes/pugi-md.js +89 -0
- package/dist/core/diagnostics/probes/sandbox.js +40 -0
- package/dist/core/diagnostics/probes/session.js +74 -0
- package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
- package/dist/core/diagnostics/probes/workspace.js +63 -0
- package/dist/core/diagnostics/types.js +70 -0
- package/dist/core/dispatch/cache-cleanup.js +197 -0
- package/dist/core/dispatch/cache-handoff.js +295 -0
- package/dist/core/edits/dispatch.js +218 -2
- package/dist/core/edits/journal.js +199 -0
- package/dist/core/edits/layer-d-ast.js +557 -14
- package/dist/core/edits/verify-hook.js +273 -0
- package/dist/core/edits/worktree.js +322 -0
- package/dist/core/engine/anvil-client.js +115 -5
- package/dist/core/engine/budgets.js +98 -0
- package/dist/core/engine/context-prefix.js +155 -0
- package/dist/core/engine/intent.js +260 -0
- package/dist/core/engine/native-pugi.js +860 -211
- package/dist/core/engine/prompts.js +88 -2
- package/dist/core/engine/strip-internal-fields.js +124 -0
- package/dist/core/engine/tool-bridge.js +1045 -36
- package/dist/core/feedback/queue.js +177 -0
- package/dist/core/feedback/submitter.js +145 -0
- package/dist/core/file-cache.js +113 -1
- package/dist/core/hooks/events.js +44 -0
- package/dist/core/hooks/index.js +15 -0
- package/dist/core/hooks/registry.js +213 -0
- package/dist/core/hooks/runner.js +236 -0
- package/dist/core/hooks/v2/event-emitter.js +115 -0
- package/dist/core/hooks/v2/executor.js +282 -0
- package/dist/core/hooks/v2/index.js +25 -0
- package/dist/core/hooks/v2/lifecycle.js +104 -0
- package/dist/core/hooks/v2/loader.js +216 -0
- package/dist/core/hooks/v2/matcher.js +125 -0
- package/dist/core/hooks/v2/trust.js +143 -0
- package/dist/core/hooks/v2/types.js +86 -0
- package/dist/core/lsp/cache.js +105 -0
- package/dist/core/lsp/client.js +776 -0
- package/dist/core/lsp/language-detect.js +66 -0
- package/dist/core/lsp/post-edit-diagnostics.js +171 -0
- package/dist/core/mcp/client.js +75 -6
- package/dist/core/mcp/http-server.js +553 -0
- package/dist/core/mcp/orchestrator-tools.js +662 -0
- package/dist/core/mcp/permission.js +190 -0
- package/dist/core/mcp/registry.js +24 -2
- package/dist/core/mcp/server-tools.js +219 -0
- package/dist/core/mcp/server.js +397 -0
- package/dist/core/memory/dual-write.js +416 -0
- package/dist/core/memory/phase1-kinds.js +20 -0
- package/dist/core/memory-sync/queue.js +158 -0
- package/dist/core/onboarding/ensure-initialized.js +133 -0
- package/dist/core/onboarding/marker.js +111 -0
- package/dist/core/onboarding/telemetry-state.js +108 -0
- package/dist/core/output-style/presets.js +176 -0
- package/dist/core/output-style/state.js +185 -0
- package/dist/core/path-security.js +284 -2
- package/dist/core/permissions/auto-classifier.js +124 -0
- package/dist/core/permissions/circuit-breaker.js +83 -0
- package/dist/core/permissions/gate.js +278 -0
- package/dist/core/permissions/index.js +20 -0
- package/dist/core/permissions/mode.js +174 -0
- package/dist/core/permissions/state.js +241 -0
- package/dist/core/permissions/tool-class.js +93 -0
- package/dist/core/prd-check/parser.js +215 -0
- package/dist/core/prd-check/reporter.js +127 -0
- package/dist/core/prd-check/session-review.js +557 -0
- package/dist/core/prd-check/verifiers.js +223 -0
- package/dist/core/pugi-md/context-injector.js +76 -0
- package/dist/core/pugi-md/walk-up.js +207 -0
- package/dist/core/release-notes/parser.js +241 -0
- package/dist/core/release-notes/state.js +116 -0
- package/dist/core/repl/history.js +11 -1
- package/dist/core/repl/model-pricing.js +135 -0
- package/dist/core/repl/session.js +1897 -37
- package/dist/core/repl/slash-commands.js +430 -15
- package/dist/core/repl/store/session-store.js +31 -2
- package/dist/core/repl/workspace-context.js +22 -0
- package/dist/core/repo-map/build.js +125 -0
- package/dist/core/repo-map/cache.js +185 -0
- package/dist/core/repo-map/extractor.js +254 -0
- package/dist/core/repo-map/formatter.js +145 -0
- package/dist/core/repo-map/scanner.js +211 -0
- package/dist/core/retry-budget/budget.js +284 -0
- package/dist/core/retry-budget/index.js +5 -0
- package/dist/core/session.js +92 -0
- package/dist/core/settings.js +80 -0
- package/dist/core/share/formatter.js +271 -0
- package/dist/core/share/redactor.js +221 -0
- package/dist/core/share/uploader.js +267 -0
- package/dist/core/skills/defaults.js +457 -0
- package/dist/core/smoke/headless-driver.js +174 -0
- package/dist/core/smoke/orchestrator.js +194 -0
- package/dist/core/smoke/runner.js +238 -0
- package/dist/core/smoke/scenario-parser.js +316 -0
- package/dist/core/subagents/dispatcher-real.js +600 -0
- package/dist/core/subagents/dispatcher.js +113 -24
- package/dist/core/subagents/index.js +18 -5
- package/dist/core/subagents/isolation-matrix.js +213 -0
- package/dist/core/subagents/spawn.js +19 -4
- package/dist/core/telemetry/emitter.js +229 -0
- package/dist/core/telemetry/queue.js +251 -0
- package/dist/core/theme/context.js +91 -0
- package/dist/core/theme/presets.js +228 -0
- package/dist/core/theme/state.js +181 -0
- package/dist/core/todos/invariant.js +10 -0
- package/dist/core/todos/state.js +177 -0
- package/dist/core/transport/version-interceptor.js +166 -0
- package/dist/core/vim/keymap.js +288 -0
- package/dist/core/vim/state.js +92 -0
- package/dist/core/worktree-manager/cleanup.js +123 -0
- package/dist/core/worktree-manager/manager.js +303 -0
- package/dist/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +3241 -343
- package/dist/runtime/commands/cancel.js +231 -0
- package/dist/runtime/commands/chain.js +489 -0
- package/dist/runtime/commands/codegraph-status.js +227 -0
- package/dist/runtime/commands/compact.js +297 -0
- package/dist/runtime/commands/cost.js +199 -0
- package/dist/runtime/commands/delegate.js +242 -11
- package/dist/runtime/commands/dispatch.js +126 -0
- package/dist/runtime/commands/doctor.js +412 -0
- package/dist/runtime/commands/feedback.js +184 -0
- package/dist/runtime/commands/hooks.js +184 -0
- package/dist/runtime/commands/lsp.js +368 -0
- package/dist/runtime/commands/mcp.js +879 -0
- package/dist/runtime/commands/memory.js +508 -0
- package/dist/runtime/commands/model.js +237 -0
- package/dist/runtime/commands/onboarding.js +275 -0
- package/dist/runtime/commands/patch.js +128 -0
- package/dist/runtime/commands/permissions.js +112 -0
- package/dist/runtime/commands/plan.js +143 -0
- package/dist/runtime/commands/prd-check.js +285 -0
- package/dist/runtime/commands/redo-blob-store.js +92 -0
- package/dist/runtime/commands/redo.js +361 -0
- package/dist/runtime/commands/release-notes.js +229 -0
- package/dist/runtime/commands/repo-map.js +95 -0
- package/dist/runtime/commands/report.js +299 -0
- package/dist/runtime/commands/resume.js +118 -0
- package/dist/runtime/commands/review-consensus.js +17 -2
- package/dist/runtime/commands/rewind.js +333 -0
- package/dist/runtime/commands/sessions.js +163 -0
- package/dist/runtime/commands/share.js +316 -0
- package/dist/runtime/commands/status.js +186 -0
- package/dist/runtime/commands/stickers.js +82 -0
- package/dist/runtime/commands/style.js +194 -0
- package/dist/runtime/commands/theme.js +196 -0
- package/dist/runtime/commands/undo.js +32 -0
- package/dist/runtime/commands/update.js +289 -0
- package/dist/runtime/commands/vim.js +140 -0
- package/dist/runtime/commands/worktree.js +177 -0
- package/dist/runtime/commands/worktrees.js +155 -0
- package/dist/runtime/headless-repl.js +195 -0
- package/dist/runtime/headless.js +543 -0
- package/dist/runtime/load-hooks-or-exit.js +71 -0
- package/dist/runtime/plan-decompose.js +531 -0
- package/dist/runtime/version.js +65 -0
- package/dist/tools/agent-tool.js +229 -0
- package/dist/tools/apply-patch.js +556 -0
- package/dist/tools/ask-user-question.js +213 -0
- package/dist/tools/ask-user.js +115 -0
- package/dist/tools/bash.js +203 -4
- package/dist/tools/file-tools.js +85 -14
- package/dist/tools/lsp-tools.js +189 -0
- package/dist/tools/mcp-tool.js +260 -0
- package/dist/tools/multi-edit.js +361 -0
- package/dist/tools/powershell.js +268 -0
- package/dist/tools/registry.js +51 -0
- package/dist/tools/skill-tool.js +96 -0
- package/dist/tools/tasks.js +208 -0
- package/dist/tools/todo-write.js +184 -0
- package/dist/tools/web-fetch.js +147 -2
- package/dist/tools/web-search.js +458 -0
- package/dist/tui/agent-progress-card.js +111 -0
- package/dist/tui/agent-tree.js +10 -0
- package/dist/tui/ask-modal.js +2 -2
- package/dist/tui/ask-user-question-prompt.js +192 -0
- package/dist/tui/compact-banner.js +81 -0
- package/dist/tui/conversation-pane.js +82 -8
- package/dist/tui/cost-table.js +111 -0
- package/dist/tui/doctor-table.js +46 -0
- package/dist/tui/feedback-prompt.js +156 -0
- package/dist/tui/input-box.js +218 -3
- package/dist/tui/markdown-render.js +4 -4
- package/dist/tui/onboarding-wizard.js +240 -0
- package/dist/tui/permissions-picker.js +86 -0
- package/dist/tui/render.js +35 -0
- package/dist/tui/repl-render.js +313 -35
- package/dist/tui/repl-splash-art.js +1 -1
- package/dist/tui/repl-splash-mascot.js +32 -8
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +85 -5
- package/dist/tui/splash.js +1 -1
- package/dist/tui/status-bar.js +94 -16
- package/dist/tui/status-table.js +7 -0
- package/dist/tui/stickers-art.js +136 -0
- package/dist/tui/style-table.js +28 -0
- package/dist/tui/theme-table.js +29 -0
- package/dist/tui/thinking-spinner.js +123 -0
- package/dist/tui/tool-stream-pane.js +52 -3
- package/dist/tui/update-banner.js +27 -2
- package/dist/tui/vim-input.js +267 -0
- package/dist/tui/welcome-banner.js +107 -0
- package/dist/tui/welcome-data.js +293 -0
- package/docs/examples/codegraph.mcp.json +10 -0
- package/package.json +12 -6
- package/test/scenarios/codegen-create-file.scenario.txt +13 -0
- package/test/scenarios/compact-force.scenario.txt +11 -0
- package/test/scenarios/identity.scenario.txt +11 -0
- package/test/scenarios/persona-handoff.scenario.txt +11 -0
- package/test/scenarios/walkback.scenario.txt +12 -0
- package/dist/core/engine/compaction-hook.js +0 -154
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pugi hooks v2 - matcher grammar (Wave 7 Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* The matcher string is compared against the tool name for tool events
|
|
5
|
+
* (PreToolUse / PostToolUse / PostToolUseFailure) and against the empty
|
|
6
|
+
* string for non-tool events (matcher is effectively ignored unless it
|
|
7
|
+
* is the wildcard).
|
|
8
|
+
*
|
|
9
|
+
* Grammar (evaluated in order):
|
|
10
|
+
*
|
|
11
|
+
* 1. wildcard `*` - matches anything.
|
|
12
|
+
* 2. slash-regex literal - `/pattern/flags`. Flags subset of gimsuy.
|
|
13
|
+
* 3. prefix regex - starts with `^` (e.g. `^mcp__`).
|
|
14
|
+
* 4. alternation - `bash|grep|read`. Each side compiled
|
|
15
|
+
* recursively so `mcp__*|bash` works.
|
|
16
|
+
* 5. wildcard glob - any string containing `*` not handled
|
|
17
|
+
* by the regex branches (e.g. `mcp__*`).
|
|
18
|
+
* 6. exact match - case-sensitive equality.
|
|
19
|
+
*
|
|
20
|
+
* Operators in the wild write `^mcp__` expecting "starts with mcp__".
|
|
21
|
+
* The Claude Code docs document this behaviour.
|
|
22
|
+
*
|
|
23
|
+
* Brand voice: ASCII only.
|
|
24
|
+
*/
|
|
25
|
+
// Constructed regexes used internally. We avoid `/.../` literals when
|
|
26
|
+
// the pattern contains `${` because some TypeScript parser
|
|
27
|
+
// configurations mis-treat the dollar-brace sequence inside a regex
|
|
28
|
+
// character class as a template-literal opener.
|
|
29
|
+
const REGEX_META_CHARS = new RegExp('[.*+?^${}()|[\\]\\\\]');
|
|
30
|
+
const ESCAPE_REGEX_META = new RegExp('[.*+?^${}()|[\\]\\\\]', 'g');
|
|
31
|
+
const SLASH_REGEX_LITERAL = /^\/(.+)\/([A-Za-z]*)$/;
|
|
32
|
+
const ALLOWED_REGEX_FLAGS = /^[gimsuy]*$/;
|
|
33
|
+
/**
|
|
34
|
+
* Compile a matcher string into a predicate over a single candidate.
|
|
35
|
+
* The candidate is the tool name for tool events, or `''` for
|
|
36
|
+
* non-tool events.
|
|
37
|
+
*
|
|
38
|
+
* Throws on a malformed regex literal so the operator sees a clear
|
|
39
|
+
* error at config-load time rather than at hook-fire time.
|
|
40
|
+
*/
|
|
41
|
+
export function compileMatcher(matcher) {
|
|
42
|
+
const trimmed = matcher.trim();
|
|
43
|
+
if (trimmed === '' || trimmed === '*') {
|
|
44
|
+
return () => true;
|
|
45
|
+
}
|
|
46
|
+
// 1. Slash-delimited regex literal.
|
|
47
|
+
const slashMatch = SLASH_REGEX_LITERAL.exec(trimmed);
|
|
48
|
+
if (slashMatch) {
|
|
49
|
+
const body = slashMatch[1] ?? '';
|
|
50
|
+
const flags = slashMatch[2] ?? '';
|
|
51
|
+
if (!ALLOWED_REGEX_FLAGS.test(flags)) {
|
|
52
|
+
throw new Error(`pugi hooks v2: matcher '${matcher}' has unsupported regex flags '${flags}'`);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const re = new RegExp(body, flags);
|
|
56
|
+
return (candidate) => re.test(candidate);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new Error(`pugi hooks v2: matcher '${matcher}' is not a valid regex: ${error.message}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// 2. Prefix-rooted regex shortcut: starts with `^` OR ends with `$`
|
|
63
|
+
// AND no `|` (alternation handled below). Also a string
|
|
64
|
+
// containing regex meta but no `|` and not pure wildcard glob.
|
|
65
|
+
const looksLikeRegex = (trimmed.startsWith('^') || trimmed.endsWith('$')) &&
|
|
66
|
+
!trimmed.includes('|');
|
|
67
|
+
const looksLikeMetaRegex = REGEX_META_CHARS.test(trimmed) &&
|
|
68
|
+
!trimmed.includes('|') &&
|
|
69
|
+
!isPureWildcardGlob(trimmed);
|
|
70
|
+
if (looksLikeRegex || looksLikeMetaRegex) {
|
|
71
|
+
try {
|
|
72
|
+
const re = new RegExp(trimmed);
|
|
73
|
+
return (candidate) => re.test(candidate);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new Error(`pugi hooks v2: matcher '${matcher}' is not a valid regex: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// 3. Pipe alternation.
|
|
80
|
+
if (trimmed.includes('|')) {
|
|
81
|
+
const alts = trimmed
|
|
82
|
+
.split('|')
|
|
83
|
+
.map((s) => s.trim())
|
|
84
|
+
.filter((s) => s.length > 0);
|
|
85
|
+
const predicates = alts.map((alt) => compileMatcher(alt));
|
|
86
|
+
return (candidate) => predicates.some((p) => p(candidate));
|
|
87
|
+
}
|
|
88
|
+
// 4. Wildcard glob.
|
|
89
|
+
if (trimmed.includes('*')) {
|
|
90
|
+
const pattern = trimmed
|
|
91
|
+
.split('*')
|
|
92
|
+
.map((part) => escapeRegex(part))
|
|
93
|
+
.join('.*');
|
|
94
|
+
const re = new RegExp(`^${pattern}$`);
|
|
95
|
+
return (candidate) => re.test(candidate);
|
|
96
|
+
}
|
|
97
|
+
// 5. Exact match.
|
|
98
|
+
return (candidate) => candidate === trimmed;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* True iff the matcher uses ONLY the wildcard sub-grammar (literal
|
|
102
|
+
* chars + `*`) - no other regex meta. Used to disambiguate `mcp__*`
|
|
103
|
+
* from `mcp__\d+`.
|
|
104
|
+
*/
|
|
105
|
+
function isPureWildcardGlob(matcher) {
|
|
106
|
+
for (const ch of matcher) {
|
|
107
|
+
if (ch === '*')
|
|
108
|
+
continue;
|
|
109
|
+
if (REGEX_META_CHARS.test(ch))
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return matcher.includes('*');
|
|
113
|
+
}
|
|
114
|
+
function escapeRegex(value) {
|
|
115
|
+
return value.replace(ESCAPE_REGEX_META, '\\$&');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Quick predicate over a matcher + candidate. Compiles + executes in
|
|
119
|
+
* one shot. Use `compileMatcher` directly when the same matcher is
|
|
120
|
+
* applied to many candidates.
|
|
121
|
+
*/
|
|
122
|
+
export function matches(matcher, candidate) {
|
|
123
|
+
return compileMatcher(matcher)(candidate);
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=matcher.js.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pugi hooks v2 — trust ledger (Wave 7 Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `core/mcp/trust.ts`. First-run interactive prompt fires
|
|
5
|
+
* before any hook executes; the operator's decision (allow / deny /
|
|
6
|
+
* always-allow) is persisted to `~/.pugi/hooks-trust.json` so the
|
|
7
|
+
* prompt does not re-fire on every REPL boot.
|
|
8
|
+
*
|
|
9
|
+
* Why a separate ledger from MCP trust:
|
|
10
|
+
* - Hook commands run arbitrary shell as the operator. A trust
|
|
11
|
+
* decision for a hook script does not transfer to MCP servers and
|
|
12
|
+
* vice-versa; the surfaces should not collide.
|
|
13
|
+
* - The hook ledger is keyed by command STRING (with the project
|
|
14
|
+
* path scope) — two projects with the same `.pugi/hooks.json`
|
|
15
|
+
* content trust independently. This prevents a malicious upstream
|
|
16
|
+
* `pugi init` template from inheriting a trust decision from an
|
|
17
|
+
* unrelated project on the same machine.
|
|
18
|
+
*
|
|
19
|
+
* Schema:
|
|
20
|
+
* {
|
|
21
|
+
* "schema": 1,
|
|
22
|
+
* "entries": {
|
|
23
|
+
* "<workspaceRoot>::<command-hash>": {
|
|
24
|
+
* "state": "trusted" | "denied" | "pending",
|
|
25
|
+
* "decidedAt": "<iso8601>",
|
|
26
|
+
* "command": "<truncated command>",
|
|
27
|
+
* "workspaceRoot": "<absolute path>"
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* Brand voice: ASCII only.
|
|
33
|
+
*/
|
|
34
|
+
import { createHash } from 'node:crypto';
|
|
35
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
36
|
+
import { homedir } from 'node:os';
|
|
37
|
+
import { dirname, resolve } from 'node:path';
|
|
38
|
+
import { z } from 'zod';
|
|
39
|
+
const TRUST_LEDGER_FILENAME = 'hooks-trust.json';
|
|
40
|
+
const ledgerEntrySchema = z.object({
|
|
41
|
+
state: z.enum(['pending', 'trusted', 'denied']),
|
|
42
|
+
decidedAt: z.string().datetime(),
|
|
43
|
+
command: z.string().min(1),
|
|
44
|
+
workspaceRoot: z.string().min(1),
|
|
45
|
+
});
|
|
46
|
+
const ledgerSchema = z.object({
|
|
47
|
+
schema: z.literal(1).default(1),
|
|
48
|
+
entries: z.record(ledgerEntrySchema).default({}),
|
|
49
|
+
});
|
|
50
|
+
/** Override-friendly path resolution. Honors `PUGI_HOME`. */
|
|
51
|
+
export function trustLedgerPath(homeOverride) {
|
|
52
|
+
const home = homeOverride ?? process.env.PUGI_HOME ?? resolve(homedir(), '.pugi');
|
|
53
|
+
return resolve(home, TRUST_LEDGER_FILENAME);
|
|
54
|
+
}
|
|
55
|
+
/** Stable key for a (workspace, command) pair. */
|
|
56
|
+
export function trustKey(workspaceRoot, command) {
|
|
57
|
+
const hash = createHash('sha256')
|
|
58
|
+
.update(`${workspaceRoot}\0${command}`)
|
|
59
|
+
.digest('hex')
|
|
60
|
+
.slice(0, 16);
|
|
61
|
+
return `${workspaceRoot}::${hash}`;
|
|
62
|
+
}
|
|
63
|
+
function readLedger(homeOverride) {
|
|
64
|
+
const path = trustLedgerPath(homeOverride);
|
|
65
|
+
if (!existsSync(path)) {
|
|
66
|
+
return { schema: 1, entries: {} };
|
|
67
|
+
}
|
|
68
|
+
const raw = readFileSync(path, 'utf8');
|
|
69
|
+
if (raw.trim() === '') {
|
|
70
|
+
return { schema: 1, entries: {} };
|
|
71
|
+
}
|
|
72
|
+
return ledgerSchema.parse(JSON.parse(raw));
|
|
73
|
+
}
|
|
74
|
+
function writeLedger(ledger, homeOverride) {
|
|
75
|
+
const path = trustLedgerPath(homeOverride);
|
|
76
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
77
|
+
writeFileSync(path, `${JSON.stringify(ledger, null, 2)}\n`, {
|
|
78
|
+
encoding: 'utf8',
|
|
79
|
+
mode: 0o600,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Look up the trust state for (workspace, command). Returns `pending`
|
|
84
|
+
* when no decision has been recorded.
|
|
85
|
+
*/
|
|
86
|
+
export function getHookTrust(workspaceRoot, command, homeOverride) {
|
|
87
|
+
const ledger = readLedger(homeOverride);
|
|
88
|
+
const entry = ledger.entries[trustKey(workspaceRoot, command)];
|
|
89
|
+
return entry ? entry.state : 'pending';
|
|
90
|
+
}
|
|
91
|
+
/** Persist a trust decision. */
|
|
92
|
+
export function setHookTrust(workspaceRoot, command, state, homeOverride) {
|
|
93
|
+
const ledger = readLedger(homeOverride);
|
|
94
|
+
ledger.entries[trustKey(workspaceRoot, command)] = {
|
|
95
|
+
state,
|
|
96
|
+
decidedAt: new Date().toISOString(),
|
|
97
|
+
command: command.slice(0, 200),
|
|
98
|
+
workspaceRoot,
|
|
99
|
+
};
|
|
100
|
+
writeLedger(ledger, homeOverride);
|
|
101
|
+
}
|
|
102
|
+
/** Bulk listing for `pugi hooks trust list`. */
|
|
103
|
+
export function listHookTrust(homeOverride) {
|
|
104
|
+
const ledger = readLedger(homeOverride);
|
|
105
|
+
return Object.values(ledger.entries)
|
|
106
|
+
.map((entry) => ({
|
|
107
|
+
workspaceRoot: entry.workspaceRoot,
|
|
108
|
+
command: entry.command,
|
|
109
|
+
state: entry.state,
|
|
110
|
+
decidedAt: entry.decidedAt,
|
|
111
|
+
}))
|
|
112
|
+
.sort((a, b) => a.workspaceRoot.localeCompare(b.workspaceRoot));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolve trust for a hook, prompting the operator on first encounter.
|
|
116
|
+
* `once` answers do NOT persist — the hook runs but the ledger stays
|
|
117
|
+
* pending so the next session re-prompts.
|
|
118
|
+
*
|
|
119
|
+
* The headless behaviour: when `promptFn` is undefined, the result is
|
|
120
|
+
* `denied`. Non-interactive sessions must NOT execute unknown hooks.
|
|
121
|
+
*/
|
|
122
|
+
export async function ensureHookTrust(input, promptFn, homeOverride) {
|
|
123
|
+
const known = getHookTrust(input.workspaceRoot, input.command, homeOverride);
|
|
124
|
+
if (known !== 'pending') {
|
|
125
|
+
return known;
|
|
126
|
+
}
|
|
127
|
+
if (!promptFn) {
|
|
128
|
+
// Headless mode without prompt = refuse for safety.
|
|
129
|
+
return 'denied';
|
|
130
|
+
}
|
|
131
|
+
const answer = await promptFn(input);
|
|
132
|
+
if (answer === 'trust') {
|
|
133
|
+
setHookTrust(input.workspaceRoot, input.command, 'trusted', homeOverride);
|
|
134
|
+
return 'trusted';
|
|
135
|
+
}
|
|
136
|
+
if (answer === 'deny') {
|
|
137
|
+
setHookTrust(input.workspaceRoot, input.command, 'denied', homeOverride);
|
|
138
|
+
return 'denied';
|
|
139
|
+
}
|
|
140
|
+
// `once` -> run this time but do NOT persist.
|
|
141
|
+
return 'trusted';
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=trust.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pugi hooks v2 — Claude Code parity types (Wave 7 Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* The v2 surface is a fresh implementation alongside the existing
|
|
5
|
+
* `core/hooks/` MVP module. It is NOT a replacement — both surfaces
|
|
6
|
+
* co-exist so the operator's legacy `~/.pugi/hooks-mvp.json` configs
|
|
7
|
+
* keep working while v2 grows the matcher grammar, the JSON decision
|
|
8
|
+
* protocol, and the per-project trust ledger.
|
|
9
|
+
*
|
|
10
|
+
* Why v2 instead of evolving v1 in-place:
|
|
11
|
+
* - The v1 runner spawns hooks via `/bin/sh -c` with the payload in
|
|
12
|
+
* env vars + stdin. v2 ships a richer stdin contract (schema_version,
|
|
13
|
+
* transcript_path, cwd, permission_mode, agent_id, agent_type) that
|
|
14
|
+
* would silently break v1 scripts if we mutated the existing surface.
|
|
15
|
+
* - The v1 matcher is exact/star only. v2 adds regex, |-alternation,
|
|
16
|
+
* and MCP wildcards. A clean module is easier to reason about than
|
|
17
|
+
* a backward-compat branch inside the existing matcher.
|
|
18
|
+
* - The v1 trust model is implicit (operator runs `pugi hooks doctor`
|
|
19
|
+
* to surface config errors). v2 introduces a first-run interactive
|
|
20
|
+
* prompt with a persisted ledger, matching MCP's trust pattern.
|
|
21
|
+
*
|
|
22
|
+
* Phase 1 wires 9 of the 31 events Claude Code exposes. The remaining
|
|
23
|
+
* 22 land in Phase 2 along with hook chains. The type union below
|
|
24
|
+
* carries ALL 31 names so chains + future events compile against the
|
|
25
|
+
* same surface without churn.
|
|
26
|
+
*
|
|
27
|
+
* Brand voice: ASCII only, no emoji, no em-dashes.
|
|
28
|
+
*/
|
|
29
|
+
/** All 31 events the v2 surface understands. */
|
|
30
|
+
export const ALL_HOOK_EVENTS_V2 = [
|
|
31
|
+
'SessionStart',
|
|
32
|
+
'SessionEnd',
|
|
33
|
+
'UserPromptSubmit',
|
|
34
|
+
'PreToolUse',
|
|
35
|
+
'PostToolUse',
|
|
36
|
+
'PostToolUseFailure',
|
|
37
|
+
'Stop',
|
|
38
|
+
'PreCompact',
|
|
39
|
+
'PostCompact',
|
|
40
|
+
'Notification',
|
|
41
|
+
'SubagentStart',
|
|
42
|
+
'SubagentStop',
|
|
43
|
+
'SubagentToolUse',
|
|
44
|
+
'PermissionRequest',
|
|
45
|
+
'PermissionGranted',
|
|
46
|
+
'PermissionDenied',
|
|
47
|
+
'PreEdit',
|
|
48
|
+
'PostEdit',
|
|
49
|
+
'PreWrite',
|
|
50
|
+
'PostWrite',
|
|
51
|
+
'PreRead',
|
|
52
|
+
'PostRead',
|
|
53
|
+
'PreBash',
|
|
54
|
+
'PostBash',
|
|
55
|
+
'McpServerConnect',
|
|
56
|
+
'McpServerDisconnect',
|
|
57
|
+
'McpToolCall',
|
|
58
|
+
'McpToolResult',
|
|
59
|
+
'AgentSpawn',
|
|
60
|
+
'AgentComplete',
|
|
61
|
+
'TaskCompleted',
|
|
62
|
+
'PromptInjection',
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* The 9 events emitted in Phase 1. A separate constant from
|
|
66
|
+
* `ALL_HOOK_EVENTS_V2` so callers can reason about "what fires today"
|
|
67
|
+
* without coupling to the entire reservation.
|
|
68
|
+
*/
|
|
69
|
+
export const PHASE_1_HOOK_EVENTS = [
|
|
70
|
+
'SessionStart',
|
|
71
|
+
'SessionEnd',
|
|
72
|
+
'UserPromptSubmit',
|
|
73
|
+
'PreToolUse',
|
|
74
|
+
'PostToolUse',
|
|
75
|
+
'PostToolUseFailure',
|
|
76
|
+
'Stop',
|
|
77
|
+
'PreCompact',
|
|
78
|
+
'PostCompact',
|
|
79
|
+
];
|
|
80
|
+
/** Default per-hook timeout in ms. */
|
|
81
|
+
export const DEFAULT_HOOK_TIMEOUT_MS_V2 = 5_000;
|
|
82
|
+
/** Hard upper bound on per-hook timeout. */
|
|
83
|
+
export const MAX_HOOK_TIMEOUT_MS_V2 = 60_000;
|
|
84
|
+
/** Hook stdout/stderr cap. Hooks emitting more are killed. */
|
|
85
|
+
export const HOOK_OUTPUT_CAP_BYTES_V2 = 1024 * 1024;
|
|
86
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-process LSP client cache — Leak L15.
|
|
3
|
+
*
|
|
4
|
+
* The α7.7 `runtime/commands/lsp.ts` CLI surface spawns one LSP server
|
|
5
|
+
* per invocation and stops it at the end. That is correct for the
|
|
6
|
+
* one-shot `pugi lsp hover ...` shape but wrong for L15's
|
|
7
|
+
* post-edit auto-diagnostics: every successful `edit`/`write` would
|
|
8
|
+
* otherwise pay the ~2-3s cold-start of `typescript-language-server`,
|
|
9
|
+
* which is unusable inside an agent loop.
|
|
10
|
+
*
|
|
11
|
+
* This module owns a singleton map keyed by `LspLanguage` with lazy
|
|
12
|
+
* initialization (`getOrStart`). The first edit of a TS file in a
|
|
13
|
+
* session pays cold-start; every subsequent edit of any TS/TSX file
|
|
14
|
+
* in the same workspace reuses the warm client.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle:
|
|
17
|
+
* - `getOrStart(lang, cwd)` — spawn if missing, return cached otherwise.
|
|
18
|
+
* - `stopAll()` — graceful shutdown of every cached client. Called from
|
|
19
|
+
* `runCli` exit so a Ctrl-C never leaves zombie LSP processes behind.
|
|
20
|
+
* - `reset()` — test-only escape hatch, drops the cache without
|
|
21
|
+
* touching child processes (specs inject stubs that own their own
|
|
22
|
+
* lifecycle).
|
|
23
|
+
*
|
|
24
|
+
* Failure handling: a startup failure is NOT cached. The next call
|
|
25
|
+
* tries again. This keeps the cache from poisoning a session when the
|
|
26
|
+
* operator installs the missing LSP binary mid-session and re-edits.
|
|
27
|
+
*
|
|
28
|
+
* Brand voice: ASCII only, no emoji, no banned words.
|
|
29
|
+
*/
|
|
30
|
+
import { isLspLanguageDisabled, startLspClient, } from './client.js';
|
|
31
|
+
const cache = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* Return a warm client for `lang`, starting one if needed. The
|
|
34
|
+
* workspace `cwd` is captured at cache-insert time; if a subsequent
|
|
35
|
+
* call asks for the same language with a different `cwd` we tear
|
|
36
|
+
* down the old client and start a fresh one. This handles the
|
|
37
|
+
* agent-worktree case where the same process hops between workspace
|
|
38
|
+
* roots inside one Node lifetime.
|
|
39
|
+
*/
|
|
40
|
+
export async function getOrStartLspClient(lang, opts) {
|
|
41
|
+
// β7 L9: respect the per-language disable toggle BEFORE we attempt to
|
|
42
|
+
// spawn. The check is cheap and keeps the disabled path from paying
|
|
43
|
+
// the `npx --yes` warmup cost on first use.
|
|
44
|
+
if (isLspLanguageDisabled(lang, opts.lspSettings)) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
reason: 'lsp_disabled',
|
|
48
|
+
detail: `${lang} is disabled via .pugi/settings.json::lsp`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const existing = cache.get(lang);
|
|
52
|
+
if (existing && existing.cwd === opts.cwd) {
|
|
53
|
+
return { ok: true, client: existing.client };
|
|
54
|
+
}
|
|
55
|
+
if (existing && existing.cwd !== opts.cwd) {
|
|
56
|
+
// Workspace switched — stop the old client and fall through to spawn.
|
|
57
|
+
try {
|
|
58
|
+
await existing.client.stop();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// best effort; stop() is idempotent + swallow-safe
|
|
62
|
+
}
|
|
63
|
+
cache.delete(lang);
|
|
64
|
+
}
|
|
65
|
+
const result = await startLspClient(lang, opts);
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
return { ok: false, reason: result.reason, detail: result.detail };
|
|
68
|
+
}
|
|
69
|
+
cache.set(lang, { client: result.value, cwd: opts.cwd });
|
|
70
|
+
return { ok: true, client: result.value };
|
|
71
|
+
}
|
|
72
|
+
/** Look up the cached client without starting one. Returns undefined when missing. */
|
|
73
|
+
export function peekLspClient(lang) {
|
|
74
|
+
return cache.get(lang)?.client;
|
|
75
|
+
}
|
|
76
|
+
/** Snapshot of currently-cached languages — used by `pugi lsp status` debug output. */
|
|
77
|
+
export function listCachedLanguages() {
|
|
78
|
+
return Array.from(cache.keys());
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Stop every cached client and clear the cache. Called from `runCli`
|
|
82
|
+
* exit and from specs that own the lifecycle of their stub servers.
|
|
83
|
+
*/
|
|
84
|
+
export async function stopAllLspClients() {
|
|
85
|
+
const snapshot = Array.from(cache.values());
|
|
86
|
+
cache.clear();
|
|
87
|
+
await Promise.all(snapshot.map(async (entry) => {
|
|
88
|
+
try {
|
|
89
|
+
await entry.client.stop();
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// best effort — shutting down anyway
|
|
93
|
+
}
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Test-only: drop the cache map WITHOUT calling stop on the children.
|
|
98
|
+
* Specs that inject stub servers manage the stub lifecycle themselves;
|
|
99
|
+
* this lets a spec swap a stub mid-test without the cache holding a
|
|
100
|
+
* stale reference to a torn-down process.
|
|
101
|
+
*/
|
|
102
|
+
export function __resetLspCacheForTests() {
|
|
103
|
+
cache.clear();
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=cache.js.map
|