@pugi/cli 0.1.0-beta.4 → 0.1.0-beta.40
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/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 +108 -1
- 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 +73 -0
- 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/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/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 +992 -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/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 +1899 -38
- package/dist/core/repl/slash-commands.js +406 -21
- 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/index.js +28 -0
- package/dist/runtime/bootstrap.js +190 -0
- package/dist/runtime/cli.js +3073 -321
- 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 +390 -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/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/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/registry.js +46 -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 +69 -2
- 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 +303 -13
- package/dist/tui/repl-splash.js +2 -2
- package/dist/tui/repl.js +72 -14
- 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/tool-stream-pane.js +52 -3
- package/dist/tui/update-banner.js +20 -2
- package/dist/tui/vim-input.js +267 -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,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
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `pugi lsp <op> <file> [args...]` — α7.7 Phase 1.
|
|
3
|
+
*
|
|
4
|
+
* Direct LSP queries from the CLI surface. Operators use this for
|
|
5
|
+
* debugging and scripting; the agent loop reaches the same operations
|
|
6
|
+
* via the `lsp_hover` / `lsp_definition` / `lsp_references` /
|
|
7
|
+
* `lsp_diagnostics` tool wrappers in `src/tools/lsp-tools.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Supported subcommands:
|
|
10
|
+
*
|
|
11
|
+
* pugi lsp hover <file> <line> <col> [--lang ts|js|py|go|rust]
|
|
12
|
+
* pugi lsp definition <file> <line> <col> [--lang ...]
|
|
13
|
+
* pugi lsp references <file> <line> <col> [--lang ...]
|
|
14
|
+
* pugi lsp diagnostics <file> [--lang ...]
|
|
15
|
+
*
|
|
16
|
+
* When `--lang` is omitted we infer from the file extension. An unknown
|
|
17
|
+
* extension surfaces `language_unsupported` so the operator can specify
|
|
18
|
+
* the language explicitly.
|
|
19
|
+
*
|
|
20
|
+
* Lifecycle: we spawn an LSP server per invocation and stop it before
|
|
21
|
+
* returning. This is slow on cold start (TS server takes ~2-3s the
|
|
22
|
+
* first time) but the single-shot scripting path doesn't need a
|
|
23
|
+
* persistent daemon. Future work (α7.7b) wires a per-REPL daemon.
|
|
24
|
+
*
|
|
25
|
+
* Brand voice: ASCII only, no emoji, no banned words.
|
|
26
|
+
*/
|
|
27
|
+
import { inspectLspServers, startLspClient } from '../../core/lsp/client.js';
|
|
28
|
+
import { languageForFile as inferLanguage } from '../../core/lsp/language-detect.js';
|
|
29
|
+
import { loadSettings } from '../../core/settings.js';
|
|
30
|
+
export async function runLspCommand(args, opts) {
|
|
31
|
+
const [op, file, ...rest] = args;
|
|
32
|
+
if (!op) {
|
|
33
|
+
return usage();
|
|
34
|
+
}
|
|
35
|
+
// β7 L9: introspection subcommand. `pugi lsp servers` reports the
|
|
36
|
+
// language matrix — binary-on-PATH + enabled-via-settings — so the
|
|
37
|
+
// operator can debug `lsp_unavailable` vs `lsp_disabled` without
|
|
38
|
+
// re-running an actual hover.
|
|
39
|
+
if (op === 'servers' || op === 'status') {
|
|
40
|
+
const settings = loadSettings(opts.cwd);
|
|
41
|
+
const lspSettings = settings.lsp;
|
|
42
|
+
const rows = inspectLspServers(lspSettings);
|
|
43
|
+
if (opts.json) {
|
|
44
|
+
return { ok: true, text: JSON.stringify(rows, null, 2), exitCode: 0 };
|
|
45
|
+
}
|
|
46
|
+
const lines = ['language\tcommand\tavailable\tenabled'];
|
|
47
|
+
for (const r of rows) {
|
|
48
|
+
lines.push(`${r.language}\t${r.command}\t${r.available ? 'yes' : 'no'}\t${r.enabled ? 'yes' : 'no'}`);
|
|
49
|
+
}
|
|
50
|
+
return { ok: true, text: lines.join('\n'), exitCode: 0 };
|
|
51
|
+
}
|
|
52
|
+
if (!file) {
|
|
53
|
+
return usage();
|
|
54
|
+
}
|
|
55
|
+
// β7 L6: `find_definition` convenience subcommand. Identical wire to
|
|
56
|
+
// `definition` but takes `<symbol>` instead of <line> <col>. We grep
|
|
57
|
+
// the workspace for the first hit on `<symbol>` (whole-word match) to
|
|
58
|
+
// recover a position, then route through LSP. Falls back cleanly when
|
|
59
|
+
// the symbol is not present in the file.
|
|
60
|
+
if (op === 'find_definition' || op === 'find-definition') {
|
|
61
|
+
const { lang: explicitLangFD, positional: positionalFD } = pullLangFlag(rest);
|
|
62
|
+
const symbol = positionalFD[0];
|
|
63
|
+
if (!symbol) {
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
text: 'Usage: pugi lsp find_definition <file> <symbol> [--lang ts|js|py|go|rust]',
|
|
67
|
+
exitCode: 2,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const lang = explicitLangFD ?? inferLanguage(file);
|
|
71
|
+
if (!lang) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
text: `cannot infer language from ${file}; pass --lang ts|js|py|go|rust. ` +
|
|
75
|
+
`Supported extensions: .ts/.tsx, .js/.jsx/.mjs, .py, .go, .rs`,
|
|
76
|
+
exitCode: 2,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return await findDefinition(file, symbol, lang, opts);
|
|
80
|
+
}
|
|
81
|
+
if (!['hover', 'definition', 'references', 'diagnostics'].includes(op)) {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
text: `unknown lsp operation: ${op}. Supported: hover, definition, references, diagnostics, find_definition, servers`,
|
|
85
|
+
exitCode: 2,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const { lang: explicitLang, positional } = pullLangFlag(rest);
|
|
89
|
+
const lang = explicitLang ?? inferLanguage(file);
|
|
90
|
+
if (!lang) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
text: `cannot infer language from ${file}; pass --lang ts|js|py|go|rust. ` +
|
|
94
|
+
`Supported extensions: .ts/.tsx, .js/.jsx/.mjs, .py, .go, .rs`,
|
|
95
|
+
exitCode: 2,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
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
|
+
}
|
|
118
|
+
const clientResult = await startLspClient(lang, {
|
|
119
|
+
cwd: opts.cwd,
|
|
120
|
+
...(settings.lsp ? { lspSettings: settings.lsp } : {}),
|
|
121
|
+
});
|
|
122
|
+
if (!clientResult.ok) {
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
text: `lsp_unavailable: ${clientResult.detail}`,
|
|
126
|
+
exitCode: 1,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const client = clientResult.value;
|
|
130
|
+
// R1 fix (2026-05-26, PR #413 r1, P2 #12): propagate SIGINT/SIGTERM
|
|
131
|
+
// to the LSP child process. Without this, ^C in the middle of a
|
|
132
|
+
// hung definition request would kill the CLI but leave the spawned
|
|
133
|
+
// language server orphaned (especially expensive for rust-analyzer
|
|
134
|
+
// / pyright which hold workspace indices in memory). We register
|
|
135
|
+
// listeners narrowly scoped to this single command invocation and
|
|
136
|
+
// tear them down in the `finally` block.
|
|
137
|
+
let interrupted = false;
|
|
138
|
+
const signalHandler = () => {
|
|
139
|
+
interrupted = true;
|
|
140
|
+
void client.stop();
|
|
141
|
+
};
|
|
142
|
+
process.once('SIGINT', signalHandler);
|
|
143
|
+
process.once('SIGTERM', signalHandler);
|
|
144
|
+
try {
|
|
145
|
+
if (op === 'diagnostics') {
|
|
146
|
+
const result = await client.diagnostics(file);
|
|
147
|
+
if (!result.ok) {
|
|
148
|
+
return { ok: false, text: `${result.reason}: ${result.detail}`, exitCode: 1 };
|
|
149
|
+
}
|
|
150
|
+
if (opts.json) {
|
|
151
|
+
return { ok: true, text: JSON.stringify(result.value, null, 2), exitCode: 0 };
|
|
152
|
+
}
|
|
153
|
+
if (result.value.length === 0) {
|
|
154
|
+
return { ok: true, text: `${file}: no diagnostics`, exitCode: 0 };
|
|
155
|
+
}
|
|
156
|
+
const lines = result.value.map((d) => `${d.severityLabel}\t${file}:${d.range.start.line + 1}:${d.range.start.character + 1}\t${d.message}`);
|
|
157
|
+
return { ok: true, text: lines.join('\n'), exitCode: 0 };
|
|
158
|
+
}
|
|
159
|
+
const line = Number.parseInt(positional[0] ?? '', 10);
|
|
160
|
+
const col = Number.parseInt(positional[1] ?? '', 10);
|
|
161
|
+
if (!Number.isFinite(line) || !Number.isFinite(col)) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
text: `lsp ${op} requires <line> <col> arguments (1-based)`,
|
|
165
|
+
exitCode: 2,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// LSP positions are 0-based; we accept 1-based input from the CLI
|
|
169
|
+
// (matches every other editor convention) and convert here.
|
|
170
|
+
const pos = { line: Math.max(0, line - 1), character: Math.max(0, col - 1) };
|
|
171
|
+
if (op === 'hover') {
|
|
172
|
+
const result = await client.hover(file, pos);
|
|
173
|
+
if (!result.ok) {
|
|
174
|
+
return { ok: false, text: `${result.reason}: ${result.detail}`, exitCode: 1 };
|
|
175
|
+
}
|
|
176
|
+
if (opts.json) {
|
|
177
|
+
return { ok: true, text: JSON.stringify(result.value ?? null, null, 2), exitCode: 0 };
|
|
178
|
+
}
|
|
179
|
+
if (!result.value) {
|
|
180
|
+
return { ok: true, text: `${file}:${line}:${col}: no hover available`, exitCode: 0 };
|
|
181
|
+
}
|
|
182
|
+
return { ok: true, text: result.value.content, exitCode: 0 };
|
|
183
|
+
}
|
|
184
|
+
if (op === 'definition') {
|
|
185
|
+
const result = await client.definition(file, pos);
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
return { ok: false, text: `${result.reason}: ${result.detail}`, exitCode: 1 };
|
|
188
|
+
}
|
|
189
|
+
if (opts.json) {
|
|
190
|
+
return { ok: true, text: JSON.stringify(result.value, null, 2), exitCode: 0 };
|
|
191
|
+
}
|
|
192
|
+
const lines = result.value.map((loc) => `${loc.path || loc.uri}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`);
|
|
193
|
+
return { ok: true, text: lines.join('\n') || 'no definition', exitCode: 0 };
|
|
194
|
+
}
|
|
195
|
+
// references
|
|
196
|
+
const result = await client.references(file, pos);
|
|
197
|
+
if (!result.ok) {
|
|
198
|
+
return { ok: false, text: `${result.reason}: ${result.detail}`, exitCode: 1 };
|
|
199
|
+
}
|
|
200
|
+
if (opts.json) {
|
|
201
|
+
return { ok: true, text: JSON.stringify(result.value, null, 2), exitCode: 0 };
|
|
202
|
+
}
|
|
203
|
+
const lines = result.value.map((loc) => `${loc.path || loc.uri}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`);
|
|
204
|
+
return { ok: true, text: lines.join('\n') || 'no references', exitCode: 0 };
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
process.removeListener('SIGINT', signalHandler);
|
|
208
|
+
process.removeListener('SIGTERM', signalHandler);
|
|
209
|
+
await client.stop();
|
|
210
|
+
if (interrupted) {
|
|
211
|
+
// Propagate the interruption so the shell sees a sensible exit
|
|
212
|
+
// code on ^C rather than the last successful result code.
|
|
213
|
+
// 130 is the canonical "terminated by SIGINT" exit value.
|
|
214
|
+
return { ok: false, text: 'lsp aborted by signal', exitCode: 130 };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function usage() {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
text: 'Usage: pugi lsp <op> <file> [line] [col] [--lang ts|js|py|go|rust]\n' +
|
|
222
|
+
' pugi lsp hover <file> <line> <col>\n' +
|
|
223
|
+
' pugi lsp definition <file> <line> <col>\n' +
|
|
224
|
+
' pugi lsp references <file> <line> <col>\n' +
|
|
225
|
+
' pugi lsp diagnostics <file>\n' +
|
|
226
|
+
' pugi lsp check <file> (Leak L15: probe post-edit tail)\n' +
|
|
227
|
+
' pugi lsp find_definition <file> <symbol>\n' +
|
|
228
|
+
' pugi lsp servers',
|
|
229
|
+
exitCode: 2,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* β7 L6: find_definition convenience.
|
|
234
|
+
*
|
|
235
|
+
* Reads the file, walks line-by-line looking for the first whole-word
|
|
236
|
+
* match of `<symbol>`, then runs `lsp definition` at that position.
|
|
237
|
+
* The result is a normal location array — empty when the LSP server
|
|
238
|
+
* can't resolve the symbol (typically because the workspace TS server
|
|
239
|
+
* hasn't indexed the import yet).
|
|
240
|
+
*
|
|
241
|
+
* "Whole-word match" uses the same identifier-boundary rules as Layer
|
|
242
|
+
* D's tokenizer: matches inside string literals / comments / partial
|
|
243
|
+
* identifiers are skipped. This keeps the result aligned with what the
|
|
244
|
+
* operator means by "where is foo defined" — not "any character sequence
|
|
245
|
+
* spelling foo".
|
|
246
|
+
*/
|
|
247
|
+
async function findDefinition(file, symbol, lang, opts) {
|
|
248
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
249
|
+
const { resolve } = await import('node:path');
|
|
250
|
+
const abs = resolve(opts.cwd, file);
|
|
251
|
+
if (!existsSync(abs)) {
|
|
252
|
+
return { ok: false, text: `file not found: ${file}`, exitCode: 1 };
|
|
253
|
+
}
|
|
254
|
+
let body;
|
|
255
|
+
try {
|
|
256
|
+
body = readFileSync(abs, 'utf8');
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
260
|
+
return { ok: false, text: `cannot read ${file}: ${detail}`, exitCode: 1 };
|
|
261
|
+
}
|
|
262
|
+
const position = locateIdentifier(body, symbol);
|
|
263
|
+
if (!position) {
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
text: `symbol '${symbol}' not found in ${file}`,
|
|
267
|
+
exitCode: 1,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const settings = loadSettings(opts.cwd);
|
|
271
|
+
const clientResult = await startLspClient(lang, {
|
|
272
|
+
cwd: opts.cwd,
|
|
273
|
+
...(settings.lsp ? { lspSettings: settings.lsp } : {}),
|
|
274
|
+
});
|
|
275
|
+
if (!clientResult.ok) {
|
|
276
|
+
return { ok: false, text: `${clientResult.reason}: ${clientResult.detail}`, exitCode: 1 };
|
|
277
|
+
}
|
|
278
|
+
const client = clientResult.value;
|
|
279
|
+
try {
|
|
280
|
+
const result = await client.definition(file, position);
|
|
281
|
+
if (!result.ok) {
|
|
282
|
+
return { ok: false, text: `${result.reason}: ${result.detail}`, exitCode: 1 };
|
|
283
|
+
}
|
|
284
|
+
if (opts.json) {
|
|
285
|
+
return {
|
|
286
|
+
ok: true,
|
|
287
|
+
text: JSON.stringify({
|
|
288
|
+
symbol,
|
|
289
|
+
file,
|
|
290
|
+
sourcePosition: { line: position.line + 1, character: position.character + 1 },
|
|
291
|
+
definitions: result.value,
|
|
292
|
+
}, null, 2),
|
|
293
|
+
exitCode: 0,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (result.value.length === 0) {
|
|
297
|
+
return { ok: true, text: `no definition for ${symbol}`, exitCode: 0 };
|
|
298
|
+
}
|
|
299
|
+
const lines = result.value.map((loc) => `${loc.path || loc.uri}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`);
|
|
300
|
+
return { ok: true, text: lines.join('\n'), exitCode: 0 };
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
await client.stop();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Find the first whole-word occurrence of `symbol` in `body`. Returns
|
|
308
|
+
* the 0-based LSP position (line + character) or null when absent.
|
|
309
|
+
* Uses the same identifier boundary rule as Layer D's tokenizer to
|
|
310
|
+
* avoid matching inside larger identifiers.
|
|
311
|
+
*/
|
|
312
|
+
export function locateIdentifier(body, symbol) {
|
|
313
|
+
const lines = body.split('\n');
|
|
314
|
+
// Build the boundary regex once. We can't use `\b` directly because
|
|
315
|
+
// it treats `$` as a word boundary and TS/JS allow `$` in identifiers;
|
|
316
|
+
// implement the boundary check with a manual lookaround.
|
|
317
|
+
const isIdent = (ch) => {
|
|
318
|
+
if (!ch)
|
|
319
|
+
return false;
|
|
320
|
+
return /[A-Za-z0-9_$]/.test(ch);
|
|
321
|
+
};
|
|
322
|
+
for (let line = 0; line < lines.length; line += 1) {
|
|
323
|
+
const text = lines[line];
|
|
324
|
+
let from = 0;
|
|
325
|
+
while (from < text.length) {
|
|
326
|
+
const idx = text.indexOf(symbol, from);
|
|
327
|
+
if (idx === -1)
|
|
328
|
+
break;
|
|
329
|
+
const before = idx > 0 ? text[idx - 1] : undefined;
|
|
330
|
+
const after = idx + symbol.length < text.length ? text[idx + symbol.length] : undefined;
|
|
331
|
+
if (!isIdent(before) && !isIdent(after)) {
|
|
332
|
+
return { line, character: idx };
|
|
333
|
+
}
|
|
334
|
+
from = idx + symbol.length;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
function pullLangFlag(args) {
|
|
340
|
+
let lang;
|
|
341
|
+
const positional = [];
|
|
342
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
343
|
+
const arg = args[i] ?? '';
|
|
344
|
+
if (arg === '--lang') {
|
|
345
|
+
const value = args[i + 1];
|
|
346
|
+
if (isLspLanguage(value))
|
|
347
|
+
lang = value;
|
|
348
|
+
i += 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (arg.startsWith('--lang=')) {
|
|
352
|
+
const value = arg.slice('--lang='.length);
|
|
353
|
+
if (isLspLanguage(value))
|
|
354
|
+
lang = value;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
positional.push(arg);
|
|
358
|
+
}
|
|
359
|
+
return { lang, positional };
|
|
360
|
+
}
|
|
361
|
+
function isLspLanguage(value) {
|
|
362
|
+
return value === 'ts' || value === 'js' || value === 'py' || value === 'go' || value === 'rust';
|
|
363
|
+
}
|
|
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 };
|
|
368
|
+
//# sourceMappingURL=lsp.js.map
|