@pugi/cli 0.1.0-beta.87 → 0.1.0-beta.89
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/CHANGELOG.md +36 -0
- package/LICENSE +1 -1
- package/dist/core/agents/registry.js +1 -1
- package/dist/core/auth/env-provider.js +1 -1
- package/dist/core/checkpoints/shadow-git.js +1 -1
- package/dist/core/context/compaction.js +1 -1
- package/dist/core/context/markdown-traverse.js +1 -1
- package/dist/core/credentials.js +1 -1
- package/dist/core/denial-tracking/state.js +1 -1
- package/dist/core/edits/fuzzy-ladder.js +1 -1
- package/dist/core/edits/layer-a-fuzzy-apply.js +1 -1
- package/dist/core/engine/anvil-client.js +76 -2
- package/dist/core/engine/native-pugi.js +1 -1
- package/dist/core/engine/tool-bridge.js +436 -0
- package/dist/core/hooks/events.js +3 -1
- package/dist/core/hooks/registry.js +3 -0
- package/dist/core/hooks/worktree-events.js +158 -0
- package/dist/core/lsp/client.js +453 -0
- package/dist/core/lsp/server-detect.js +173 -0
- package/dist/core/lsp/symbol-cache.js +162 -0
- package/dist/core/lsp/symbol-tools.js +296 -4
- package/dist/core/mcp/server-tools.js +1 -1
- package/dist/core/mcp/server.js +1 -1
- package/dist/core/memory/secret-scanner.js +6 -6
- package/dist/core/onboarding/ensure-initialized.js +1 -1
- package/dist/core/plans/plan-artifact.js +2 -2
- package/dist/core/repl/ask.js +1 -1
- package/dist/core/repl/cap-warning.js +1 -1
- package/dist/core/repl/session.js +3 -3
- package/dist/core/repl/slash-commands.js +1 -1
- package/dist/core/routing/pre-flight-estimator.js +1 -1
- package/dist/core/settings.js +38 -0
- package/dist/core/worktree/include-parser.js +249 -0
- package/dist/index.js +8 -0
- package/dist/runtime/cli.js +176 -28
- package/dist/runtime/commands/agents.js +1 -1
- package/dist/runtime/commands/config.js +41 -7
- package/dist/runtime/commands/hooks.js +3 -0
- package/dist/runtime/commands/review-consensus.js +1 -1
- package/dist/runtime/sigint-guard.js +272 -0
- package/dist/runtime/version.js +1 -1
- package/dist/runtime/worktree-bootstrap.js +579 -0
- package/dist/skills/bundled/batch.js +2 -2
- package/dist/skills/bundled/index.js +3 -3
- package/dist/skills/bundled/loop.js +2 -2
- package/dist/skills/bundled/remember.js +1 -1
- package/dist/skills/bundled/simplify.js +1 -1
- package/dist/skills/bundled/skillify.js +2 -2
- package/dist/skills/bundled/stuck.js +1 -1
- package/dist/skills/bundled/verify.js +2 -2
- package/dist/testing/vcr.js +2 -2
- package/dist/tools/ask-user-question.js +66 -0
- package/dist/tools/bash.js +2 -2
- package/dist/tools/lsp-tools.js +377 -1
- package/dist/tools/powershell.js +1 -1
- package/dist/tools/registry.js +23 -0
- package/dist/tui/ask-user-question-chips.js +257 -0
- package/dist/tui/input-box.js +1 -1
- package/dist/tui/render.js +1 -1
- package/dist/tui/repl.js +1 -1
- package/dist/tui/status-bar.js +1 -1
- package/dist/tui/update-banner.js +1 -1
- package/dist/tui/welcome-data.js +4 -4
- package/package.json +4 -3
- package/test/scenarios/compact-force.scenario.txt +3 -2
- package/test/scenarios/identity.scenario.txt +6 -5
- package/test/scenarios/persona-handoff.scenario.txt +2 -1
- package/test/scenarios/walkback.scenario.txt +6 -6
|
@@ -48,6 +48,22 @@ export const ASK_USER_QUESTION_OPTION_DESC_MAX = 200;
|
|
|
48
48
|
/** Option count: 2-4 strict. UI adds "Other" automatically. */
|
|
49
49
|
export const ASK_USER_QUESTION_OPTIONS_MIN = 2;
|
|
50
50
|
export const ASK_USER_QUESTION_OPTIONS_MAX = 4;
|
|
51
|
+
/** PUGI-480 short-format chip rules: ≤ 5 words / option label, ≤ 3 questions / call. */
|
|
52
|
+
export const ASK_USER_QUESTION_CHIP_LABEL_WORD_MAX = 5;
|
|
53
|
+
export const ASK_USER_QUESTION_CHIPS_MAX = 3;
|
|
54
|
+
/**
|
|
55
|
+
* Reusable validator for option labels rendered в chip mode.
|
|
56
|
+
* Counts whitespace-delimited words. Throws с a clear message that
|
|
57
|
+
* names the offending question + option so the model can self-correct.
|
|
58
|
+
*/
|
|
59
|
+
export function assertChipLabelWordCap(questionHeader, optionLabel) {
|
|
60
|
+
const words = optionLabel.trim().split(/\s+/u).filter((w) => w.length > 0);
|
|
61
|
+
if (words.length > ASK_USER_QUESTION_CHIP_LABEL_WORD_MAX) {
|
|
62
|
+
throw new Error(`ask_user_question chip "${questionHeader}" option "${optionLabel}" ` +
|
|
63
|
+
`exceeds the ${ASK_USER_QUESTION_CHIP_LABEL_WORD_MAX}-word label cap ` +
|
|
64
|
+
`(saw ${words.length} words). Shorten the label before dispatch.`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
51
67
|
/**
|
|
52
68
|
* Structured option. `label` is the display text; `description` is the
|
|
53
69
|
* implication line shown dim below it. Both are required — the model
|
|
@@ -100,6 +116,56 @@ export const askUserQuestionSchema = z.strictObject({
|
|
|
100
116
|
.default(false)
|
|
101
117
|
.describe('Allow multiple selections. Default false.'),
|
|
102
118
|
});
|
|
119
|
+
/**
|
|
120
|
+
* PUGI-480 multi-question chip payload — a bundle of up to 3 short-format
|
|
121
|
+
* chip questions rendered side-by-side. Schema is intentionally narrow:
|
|
122
|
+
* the chip renderer relies on the ≤ 5-word label invariant и will
|
|
123
|
+
* truncate с "…" if the model ever bypasses Zod. The 3-question cap
|
|
124
|
+
* forecloses paragraph-wall prompts at the schema level — the model
|
|
125
|
+
* cannot legally ask "10 quick questions" in one shot.
|
|
126
|
+
*/
|
|
127
|
+
export const askUserQuestionChipsQuestionSchema = z.strictObject({
|
|
128
|
+
header: z
|
|
129
|
+
.string()
|
|
130
|
+
.min(ASK_USER_QUESTION_HEADER_MIN)
|
|
131
|
+
.max(ASK_USER_QUESTION_HEADER_MAX)
|
|
132
|
+
.describe('Short chip label (max 12 chars). E.g. "Stack".'),
|
|
133
|
+
question: z
|
|
134
|
+
.string()
|
|
135
|
+
.min(ASK_USER_QUESTION_MIN)
|
|
136
|
+
.max(ASK_USER_QUESTION_MAX)
|
|
137
|
+
.optional()
|
|
138
|
+
.describe('Optional full-prose question (shown in non-TTY fallback only).'),
|
|
139
|
+
options: z
|
|
140
|
+
.array(askUserQuestionOptionSchema)
|
|
141
|
+
.min(ASK_USER_QUESTION_OPTIONS_MIN)
|
|
142
|
+
.max(ASK_USER_QUESTION_OPTIONS_MAX + 1)
|
|
143
|
+
.superRefine((opts, ctx) => {
|
|
144
|
+
// Enforce ≤ 5-word label cap on every option. Schema-level so
|
|
145
|
+
// the model gets immediate feedback on overflow rather than a
|
|
146
|
+
// silent truncation at render time.
|
|
147
|
+
for (const opt of opts) {
|
|
148
|
+
const words = opt.label
|
|
149
|
+
.trim()
|
|
150
|
+
.split(/\s+/u)
|
|
151
|
+
.filter((w) => w.length > 0);
|
|
152
|
+
if (words.length > ASK_USER_QUESTION_CHIP_LABEL_WORD_MAX) {
|
|
153
|
+
ctx.addIssue({
|
|
154
|
+
code: z.ZodIssueCode.custom,
|
|
155
|
+
message: `option "${opt.label}" has ${words.length} words; chip labels must be ≤ ${ASK_USER_QUESTION_CHIP_LABEL_WORD_MAX} words`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
.describe('2-5 mutually-exclusive options. Each label ≤ 5 words. UI inserts "Skip — use defaults" as a final option when defaults are present.'),
|
|
161
|
+
});
|
|
162
|
+
export const askUserQuestionChipsSchema = z.strictObject({
|
|
163
|
+
questions: z
|
|
164
|
+
.array(askUserQuestionChipsQuestionSchema)
|
|
165
|
+
.min(1)
|
|
166
|
+
.max(ASK_USER_QUESTION_CHIPS_MAX)
|
|
167
|
+
.describe('Bundle of 1-3 short clarifier questions rendered side-by-side as chips.'),
|
|
168
|
+
});
|
|
103
169
|
/**
|
|
104
170
|
* Dispatch the structured tool: validate args via Zod, then route
|
|
105
171
|
* through the shared `askUser` primitive so abort/timeout/non-TTY
|
package/dist/tools/bash.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* `jobId`. `listJobs()` and `killJob(jobId)` are exported.
|
|
24
24
|
* 5. 60s default timeout. SIGTERM at deadline, SIGKILL 5s later.
|
|
25
25
|
* Emit `bash.timeout`.
|
|
26
|
-
* 6. POSIX-only (`/bin/sh`). The non-goal in
|
|
26
|
+
* 6. POSIX-only (`/bin/sh`). The non-goal in explicitly
|
|
27
27
|
* drops Windows shell support for M1.
|
|
28
28
|
*/
|
|
29
29
|
import { randomUUID } from 'node:crypto';
|
|
@@ -191,7 +191,7 @@ export async function bashTool(input, ctx) {
|
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
-
// POSIX-only `/bin/sh -c <cmd>`. The
|
|
194
|
+
// POSIX-only `/bin/sh -c <cmd>`. The non-goals explicitly
|
|
195
195
|
// exclude Windows for M1.
|
|
196
196
|
//
|
|
197
197
|
// stdio layout:
|
package/dist/tools/lsp-tools.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { callHierarchyAt, codeActionsAt, diagnosticsFor, findDefinition as symbolsFindDefinition, findImplementations, findReferences as symbolsFindReferences, findTypeDefinition, findWorkspaceSymbol, formatFile, hoverSymbol, listDocumentSymbols, renameSymbol, signatureAt, transportFromLspClient, } from '../core/lsp/symbol-tools.js';
|
|
1
2
|
import { gateOnCancellation, OperatorAbortedError } from './file-tools.js';
|
|
2
3
|
import { recordToolCall, recordToolResult } from '../core/session.js';
|
|
4
|
+
import { getGlobalSymbolCache, SymbolCache } from '../core/lsp/symbol-cache.js';
|
|
3
5
|
/** Cap for any single LSP tool's payload size. Keeps model context lean. */
|
|
4
6
|
const LSP_PAYLOAD_CAP_BYTES = 8 * 1024;
|
|
5
7
|
export async function lspHover(ctx, lang, file, line, col) {
|
|
@@ -184,6 +186,380 @@ function capDiagnostics(items) {
|
|
|
184
186
|
}
|
|
185
187
|
return { value: items.slice(0, 1), truncated: true };
|
|
186
188
|
}
|
|
189
|
+
/* ------------------------------------------------------------------------- */
|
|
190
|
+
/* PUGI-78 Phase 1: symbols.* namespace tools (13 categories). */
|
|
191
|
+
/* ------------------------------------------------------------------------- */
|
|
192
|
+
/**
|
|
193
|
+
* PUGI-78 Phase 1: symbols.find_definition.
|
|
194
|
+
*
|
|
195
|
+
* Pugi reads a whole file (~5-50 KB tokens) every time the model wants
|
|
196
|
+
* to know "where is foo defined?". The LSP wire here returns ~200 bytes
|
|
197
|
+
* (file + line + character). Two orders of magnitude reduction in token
|
|
198
|
+
* cost per refactor turn, hence the BIG-leverage classification in the
|
|
199
|
+
* spec (10-100x token savings per refactor).
|
|
200
|
+
*
|
|
201
|
+
* Failure folds to `lsp_unavailable` when no client is registered for
|
|
202
|
+
* the inferred language. The agent surface is expected to fall back to
|
|
203
|
+
* grep when this fires (the tool-preference prompt rule steers it).
|
|
204
|
+
*/
|
|
205
|
+
export async function symbolsFindDefinitionTool(ctx, lang, file, line, col) {
|
|
206
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_find_definition', `${lang}:${file}:${line}:${col}`);
|
|
207
|
+
return guard(ctx, 'symbols_find_definition', toolCallId, async () => {
|
|
208
|
+
const client = ctx.lspClients?.get(lang);
|
|
209
|
+
if (!client)
|
|
210
|
+
return unavailable(lang);
|
|
211
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
212
|
+
const cwd = workspaceForCache(ctx);
|
|
213
|
+
const key = SymbolCache.makeKey(lang, cwd, 'find_definition', { file, line, col });
|
|
214
|
+
const cached = cache?.get(key);
|
|
215
|
+
if (cached !== undefined) {
|
|
216
|
+
if (cached === null)
|
|
217
|
+
return notFound('definition');
|
|
218
|
+
return { ok: true, value: cached };
|
|
219
|
+
}
|
|
220
|
+
const result = await symbolsFindDefinition(transportFromLspClient(client), file, line, col);
|
|
221
|
+
cache?.set(key, result);
|
|
222
|
+
if (!result)
|
|
223
|
+
return notFound('definition');
|
|
224
|
+
return { ok: true, value: result };
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* PUGI-78 Phase 1: symbols.find_references.
|
|
229
|
+
*
|
|
230
|
+
* Returns the flat list of call sites for the symbol at (line, col).
|
|
231
|
+
* The 200-row cap from the legacy `lspReferences` carries over via
|
|
232
|
+
* `capSymbolReferences`.
|
|
233
|
+
*/
|
|
234
|
+
export async function symbolsFindReferencesTool(ctx, lang, file, line, col) {
|
|
235
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_find_references', `${lang}:${file}:${line}:${col}`);
|
|
236
|
+
return guard(ctx, 'symbols_find_references', toolCallId, async () => {
|
|
237
|
+
const client = ctx.lspClients?.get(lang);
|
|
238
|
+
if (!client)
|
|
239
|
+
return unavailable(lang);
|
|
240
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
241
|
+
const cwd = workspaceForCache(ctx);
|
|
242
|
+
const key = SymbolCache.makeKey(lang, cwd, 'find_references', { file, line, col });
|
|
243
|
+
const cached = cache?.get(key);
|
|
244
|
+
if (cached !== undefined) {
|
|
245
|
+
const capped = capSymbolReferences(cached);
|
|
246
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
247
|
+
}
|
|
248
|
+
const result = await symbolsFindReferences(transportFromLspClient(client), file, line, col);
|
|
249
|
+
cache?.set(key, result);
|
|
250
|
+
const capped = capSymbolReferences(result);
|
|
251
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* PUGI-78 Phase 1: symbols.list_in_file.
|
|
256
|
+
*
|
|
257
|
+
* Outline view — the document-symbol surface. Returns the flat list of
|
|
258
|
+
* top-level + nested symbols with their kind. Cache HIT on the same
|
|
259
|
+
* file across consecutive turns is the common case (the model usually
|
|
260
|
+
* lists, then dives into a single symbol).
|
|
261
|
+
*/
|
|
262
|
+
export async function symbolsListInFileTool(ctx, lang, file) {
|
|
263
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_list_in_file', `${lang}:${file}`);
|
|
264
|
+
return guard(ctx, 'symbols_list_in_file', toolCallId, async () => {
|
|
265
|
+
const client = ctx.lspClients?.get(lang);
|
|
266
|
+
if (!client)
|
|
267
|
+
return unavailable(lang);
|
|
268
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
269
|
+
const cwd = workspaceForCache(ctx);
|
|
270
|
+
const key = SymbolCache.makeKey(lang, cwd, 'list_in_file', { file });
|
|
271
|
+
const cached = cache?.get(key);
|
|
272
|
+
if (cached !== undefined) {
|
|
273
|
+
return { ok: true, value: [...cached] };
|
|
274
|
+
}
|
|
275
|
+
const result = await listDocumentSymbols(transportFromLspClient(client), file);
|
|
276
|
+
cache?.set(key, result);
|
|
277
|
+
return { ok: true, value: result };
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* PUGI-78 Phase 1: symbols.rename.
|
|
282
|
+
*
|
|
283
|
+
* Returns the workspace-edit preview the server proposes. The
|
|
284
|
+
* dispatcher applies via `apply_patch` in a future ticket; Phase 1 is
|
|
285
|
+
* read-only — the agent surface gets the affected file list + per-edit
|
|
286
|
+
* line/character so the model can summarize the change.
|
|
287
|
+
*/
|
|
288
|
+
export async function symbolsRenameTool(ctx, lang, file, line, col, newName) {
|
|
289
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_rename', `${lang}:${file}:${line}:${col}:${newName}`);
|
|
290
|
+
return guard(ctx, 'symbols_rename', toolCallId, async () => {
|
|
291
|
+
const client = ctx.lspClients?.get(lang);
|
|
292
|
+
if (!client)
|
|
293
|
+
return unavailable(lang);
|
|
294
|
+
// No cache for rename — the edit set depends on the live source
|
|
295
|
+
// file state. A subsequent rename of the same symbol after the
|
|
296
|
+
// first applied would otherwise return stale edits.
|
|
297
|
+
const result = await renameSymbol(transportFromLspClient(client), file, line, col, newName);
|
|
298
|
+
if (!result)
|
|
299
|
+
return notFound('rename');
|
|
300
|
+
return { ok: true, value: result };
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* PUGI-78 Phase 1: symbols.hover.
|
|
305
|
+
*
|
|
306
|
+
* Returns the hover content (type info + docstring) at the position.
|
|
307
|
+
* Body capped at 4 KB so a verbose generic does not blow context.
|
|
308
|
+
*/
|
|
309
|
+
export async function symbolsHoverTool(ctx, lang, file, line, col) {
|
|
310
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_hover', `${lang}:${file}:${line}:${col}`);
|
|
311
|
+
return guard(ctx, 'symbols_hover', toolCallId, async () => {
|
|
312
|
+
const client = ctx.lspClients?.get(lang);
|
|
313
|
+
if (!client)
|
|
314
|
+
return unavailable(lang);
|
|
315
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
316
|
+
const cwd = workspaceForCache(ctx);
|
|
317
|
+
const key = SymbolCache.makeKey(lang, cwd, 'hover', { file, line, col });
|
|
318
|
+
const cached = cache?.get(key);
|
|
319
|
+
if (cached !== undefined) {
|
|
320
|
+
if (cached === null)
|
|
321
|
+
return notFound('hover');
|
|
322
|
+
const next = {
|
|
323
|
+
ok: true,
|
|
324
|
+
value: { content: cached.content, ...(cached.range ? { range: cached.range } : {}) },
|
|
325
|
+
...(cached.truncated ? { truncated: true } : {}),
|
|
326
|
+
};
|
|
327
|
+
return next;
|
|
328
|
+
}
|
|
329
|
+
const result = await hoverSymbol(transportFromLspClient(client), file, line, col);
|
|
330
|
+
cache?.set(key, result);
|
|
331
|
+
if (!result)
|
|
332
|
+
return notFound('hover');
|
|
333
|
+
return {
|
|
334
|
+
ok: true,
|
|
335
|
+
value: { content: result.content, ...(result.range ? { range: result.range } : {}) },
|
|
336
|
+
...(result.truncated ? { truncated: true } : {}),
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* PUGI-78 Phase 1: symbols.signature.
|
|
342
|
+
*
|
|
343
|
+
* Function signature at a call site. Returns the active overload's
|
|
344
|
+
* label, parameters, and active-parameter index.
|
|
345
|
+
*/
|
|
346
|
+
export async function symbolsSignatureTool(ctx, lang, file, line, col) {
|
|
347
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_signature', `${lang}:${file}:${line}:${col}`);
|
|
348
|
+
return guard(ctx, 'symbols_signature', toolCallId, async () => {
|
|
349
|
+
const client = ctx.lspClients?.get(lang);
|
|
350
|
+
if (!client)
|
|
351
|
+
return unavailable(lang);
|
|
352
|
+
const result = await signatureAt(transportFromLspClient(client), file, line, col);
|
|
353
|
+
if (!result)
|
|
354
|
+
return notFound('signature');
|
|
355
|
+
return { ok: true, value: result };
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* PUGI-78 Phase 1: symbols.workspace_symbols.
|
|
360
|
+
*
|
|
361
|
+
* Workspace-wide fuzzy search. The server picks the matching algorithm
|
|
362
|
+
* (substring / fuzzy / prefix); we forward verbatim.
|
|
363
|
+
*/
|
|
364
|
+
export async function symbolsWorkspaceSymbolsTool(ctx, lang, query) {
|
|
365
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_workspace_symbols', `${lang}:${query}`);
|
|
366
|
+
return guard(ctx, 'symbols_workspace_symbols', toolCallId, async () => {
|
|
367
|
+
const client = ctx.lspClients?.get(lang);
|
|
368
|
+
if (!client)
|
|
369
|
+
return unavailable(lang);
|
|
370
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
371
|
+
const cwd = workspaceForCache(ctx);
|
|
372
|
+
const key = SymbolCache.makeKey(lang, cwd, 'workspace_symbols', { query });
|
|
373
|
+
const cached = cache?.get(key);
|
|
374
|
+
if (cached !== undefined) {
|
|
375
|
+
return { ok: true, value: [...cached] };
|
|
376
|
+
}
|
|
377
|
+
const result = await findWorkspaceSymbol(transportFromLspClient(client), query);
|
|
378
|
+
cache?.set(key, result);
|
|
379
|
+
return { ok: true, value: result };
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* PUGI-78 Phase 1: symbols.call_hierarchy.
|
|
384
|
+
*
|
|
385
|
+
* Returns the incoming + outgoing call edges at the symbol position.
|
|
386
|
+
* The two arrays are independent — a function may have many incoming
|
|
387
|
+
* callers and zero outgoing calls (a pure leaf), or vice versa.
|
|
388
|
+
*/
|
|
389
|
+
export async function symbolsCallHierarchyTool(ctx, lang, file, line, col) {
|
|
390
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_call_hierarchy', `${lang}:${file}:${line}:${col}`);
|
|
391
|
+
return guard(ctx, 'symbols_call_hierarchy', toolCallId, async () => {
|
|
392
|
+
const client = ctx.lspClients?.get(lang);
|
|
393
|
+
if (!client)
|
|
394
|
+
return unavailable(lang);
|
|
395
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
396
|
+
const cwd = workspaceForCache(ctx);
|
|
397
|
+
const key = SymbolCache.makeKey(lang, cwd, 'call_hierarchy', { file, line, col });
|
|
398
|
+
const cached = cache?.get(key);
|
|
399
|
+
if (cached !== undefined) {
|
|
400
|
+
return { ok: true, value: { incoming: [...cached.incoming], outgoing: [...cached.outgoing] } };
|
|
401
|
+
}
|
|
402
|
+
const result = await callHierarchyAt(transportFromLspClient(client), file, line, col);
|
|
403
|
+
cache?.set(key, result);
|
|
404
|
+
return { ok: true, value: result };
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* PUGI-78 Phase 1: symbols.implementations.
|
|
409
|
+
*
|
|
410
|
+
* Implementations of an interface / abstract method. Returns the flat
|
|
411
|
+
* list of concrete sites.
|
|
412
|
+
*/
|
|
413
|
+
export async function symbolsImplementationsTool(ctx, lang, file, line, col) {
|
|
414
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_implementations', `${lang}:${file}:${line}:${col}`);
|
|
415
|
+
return guard(ctx, 'symbols_implementations', toolCallId, async () => {
|
|
416
|
+
const client = ctx.lspClients?.get(lang);
|
|
417
|
+
if (!client)
|
|
418
|
+
return unavailable(lang);
|
|
419
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
420
|
+
const cwd = workspaceForCache(ctx);
|
|
421
|
+
const key = SymbolCache.makeKey(lang, cwd, 'implementations', { file, line, col });
|
|
422
|
+
const cached = cache?.get(key);
|
|
423
|
+
if (cached !== undefined) {
|
|
424
|
+
const capped = capSymbolReferences(cached);
|
|
425
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
426
|
+
}
|
|
427
|
+
const result = await findImplementations(transportFromLspClient(client), file, line, col);
|
|
428
|
+
cache?.set(key, result);
|
|
429
|
+
const capped = capSymbolReferences(result);
|
|
430
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* PUGI-78 Phase 1: symbols.type_definition.
|
|
435
|
+
*
|
|
436
|
+
* Type definition (vs value definition). Useful for "what type is foo?"
|
|
437
|
+
* questions when the model has a variable and wants its type declaration.
|
|
438
|
+
*/
|
|
439
|
+
export async function symbolsTypeDefinitionTool(ctx, lang, file, line, col) {
|
|
440
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_type_definition', `${lang}:${file}:${line}:${col}`);
|
|
441
|
+
return guard(ctx, 'symbols_type_definition', toolCallId, async () => {
|
|
442
|
+
const client = ctx.lspClients?.get(lang);
|
|
443
|
+
if (!client)
|
|
444
|
+
return unavailable(lang);
|
|
445
|
+
const cache = getSymbolCacheFromCtx(ctx);
|
|
446
|
+
const cwd = workspaceForCache(ctx);
|
|
447
|
+
const key = SymbolCache.makeKey(lang, cwd, 'type_definition', { file, line, col });
|
|
448
|
+
const cached = cache?.get(key);
|
|
449
|
+
if (cached !== undefined) {
|
|
450
|
+
if (cached === null)
|
|
451
|
+
return notFound('type_definition');
|
|
452
|
+
return { ok: true, value: cached };
|
|
453
|
+
}
|
|
454
|
+
const result = await findTypeDefinition(transportFromLspClient(client), file, line, col);
|
|
455
|
+
cache?.set(key, result);
|
|
456
|
+
if (!result)
|
|
457
|
+
return notFound('type_definition');
|
|
458
|
+
return { ok: true, value: result };
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* PUGI-78 Phase 1: symbols.code_actions.
|
|
463
|
+
*
|
|
464
|
+
* Quick-fix list at the given range. Server-provided actions include
|
|
465
|
+
* auto-import, unused-import removal, refactor extractions, etc.
|
|
466
|
+
*/
|
|
467
|
+
export async function symbolsCodeActionsTool(ctx, lang, file, startLine, startChar, endLine, endChar) {
|
|
468
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_code_actions', `${lang}:${file}:${startLine}:${startChar}-${endLine}:${endChar}`);
|
|
469
|
+
return guard(ctx, 'symbols_code_actions', toolCallId, async () => {
|
|
470
|
+
const client = ctx.lspClients?.get(lang);
|
|
471
|
+
if (!client)
|
|
472
|
+
return unavailable(lang);
|
|
473
|
+
const result = await codeActionsAt(transportFromLspClient(client), file, startLine, startChar, endLine, endChar);
|
|
474
|
+
return { ok: true, value: result };
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* PUGI-78 Phase 1: symbols.format.
|
|
479
|
+
*
|
|
480
|
+
* Formatter — returns the text edits the LSP server would apply for
|
|
481
|
+
* `textDocument/formatting`. Phase 1 is read-only (returns the edits);
|
|
482
|
+
* the dispatcher composes + applies in a future ticket.
|
|
483
|
+
*/
|
|
484
|
+
export async function symbolsFormatTool(ctx, lang, file, options) {
|
|
485
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_format', `${lang}:${file}`);
|
|
486
|
+
return guard(ctx, 'symbols_format', toolCallId, async () => {
|
|
487
|
+
const client = ctx.lspClients?.get(lang);
|
|
488
|
+
if (!client)
|
|
489
|
+
return unavailable(lang);
|
|
490
|
+
const result = await formatFile(transportFromLspClient(client), file, options);
|
|
491
|
+
return { ok: true, value: result };
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* PUGI-78 Phase 1: symbols.diagnostics.
|
|
496
|
+
*
|
|
497
|
+
* Pulls the cached `publishDiagnostics` payload for the file. Mirrors
|
|
498
|
+
* the legacy `lspDiagnostics` tool but exposed via the namespaced
|
|
499
|
+
* `symbols.*` surface for discoverability.
|
|
500
|
+
*/
|
|
501
|
+
export async function symbolsDiagnosticsTool(ctx, lang, file) {
|
|
502
|
+
const toolCallId = recordToolCall(ctx.session, 'symbols_diagnostics', `${lang}:${file}`);
|
|
503
|
+
return guard(ctx, 'symbols_diagnostics', toolCallId, async () => {
|
|
504
|
+
const client = ctx.lspClients?.get(lang);
|
|
505
|
+
if (!client)
|
|
506
|
+
return unavailable(lang);
|
|
507
|
+
const result = await diagnosticsFor(transportFromLspClient(client), file);
|
|
508
|
+
const capped = capDiagnostics(result);
|
|
509
|
+
return { ok: true, value: capped.value, ...(capped.truncated ? { truncated: true } : {}) };
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function workspaceForCache(ctx) {
|
|
513
|
+
// ToolContext exposes `root` (the workspace root). Fall back to a
|
|
514
|
+
// tagged anonymous workspace when somehow absent so the cache still
|
|
515
|
+
// keys deterministically.
|
|
516
|
+
const root = ctx.root;
|
|
517
|
+
if (typeof root === 'string' && root.length > 0)
|
|
518
|
+
return root;
|
|
519
|
+
return '<unknown>';
|
|
520
|
+
}
|
|
521
|
+
function getSymbolCacheFromCtx(ctx) {
|
|
522
|
+
// The agent dispatcher injects an optional cache via the ctx — when
|
|
523
|
+
// present we use the dispatcher-scoped cache so test harnesses can
|
|
524
|
+
// inject a deterministic clock. When absent, we fall back to the
|
|
525
|
+
// process-global cache.
|
|
526
|
+
if (ctx.symbolCache)
|
|
527
|
+
return ctx.symbolCache;
|
|
528
|
+
return getGlobalSymbolCache();
|
|
529
|
+
}
|
|
530
|
+
function notFound(verb) {
|
|
531
|
+
return {
|
|
532
|
+
ok: false,
|
|
533
|
+
reason: 'lsp_not_found',
|
|
534
|
+
detail: `LSP returned no ${verb} for the requested position.`,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function capSymbolReferences(items) {
|
|
538
|
+
const COUNT_CAP = 200;
|
|
539
|
+
if (items.length === 0)
|
|
540
|
+
return { value: items, truncated: false };
|
|
541
|
+
const trimmed = items.slice(0, COUNT_CAP);
|
|
542
|
+
const serialized = JSON.stringify(trimmed);
|
|
543
|
+
if (Buffer.byteLength(serialized, 'utf8') <= LSP_PAYLOAD_CAP_BYTES && trimmed.length === items.length) {
|
|
544
|
+
return { value: trimmed, truncated: false };
|
|
545
|
+
}
|
|
546
|
+
let upper = trimmed.length;
|
|
547
|
+
while (upper > 1) {
|
|
548
|
+
const half = Math.floor(upper / 2);
|
|
549
|
+
const sub = trimmed.slice(0, half);
|
|
550
|
+
if (Buffer.byteLength(JSON.stringify(sub), 'utf8') <= LSP_PAYLOAD_CAP_BYTES) {
|
|
551
|
+
return { value: sub, truncated: true };
|
|
552
|
+
}
|
|
553
|
+
upper = half;
|
|
554
|
+
}
|
|
555
|
+
return { value: trimmed.slice(0, 1), truncated: true };
|
|
556
|
+
}
|
|
187
557
|
/** Test-only surface so specs can poke truncation directly. */
|
|
188
|
-
export const __test__ = {
|
|
558
|
+
export const __test__ = {
|
|
559
|
+
truncate,
|
|
560
|
+
capLocations,
|
|
561
|
+
capDiagnostics,
|
|
562
|
+
capSymbolReferences,
|
|
563
|
+
LSP_PAYLOAD_CAP_BYTES,
|
|
564
|
+
};
|
|
189
565
|
//# sourceMappingURL=lsp-tools.js.map
|
package/dist/tools/powershell.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* на cross-platform PowerShell 7+ binary so Windows-first workflows are
|
|
7
7
|
* first-class на Pugi.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* independent implementation re-implementation. Surface mirrors bashTool's permission
|
|
10
10
|
* gate, env sanitiser, output cap, timeout, and exit-code propagation;
|
|
11
11
|
* the only difference is the shell binary selection. Per-platform
|
|
12
12
|
* resolution:
|
package/dist/tools/registry.js
CHANGED
|
@@ -43,6 +43,29 @@ const registry = [
|
|
|
43
43
|
{ name: 'lsp_diagnostics', permission: 'read', risk: 'low', concurrencySafe: true, m1: true },
|
|
44
44
|
{ name: 'lsp_hover', permission: 'read', risk: 'low', concurrencySafe: true, m1: true },
|
|
45
45
|
{ name: 'lsp_references', permission: 'read', risk: 'low', concurrencySafe: true, m1: true },
|
|
46
|
+
// PUGI-78 Phase 1 — symbols.* namespace. 13 first-class tools that
|
|
47
|
+
// expose the full LSP symbol-aware surface (definition, references,
|
|
48
|
+
// hover, signature, document/workspace symbols, rename preview, call
|
|
49
|
+
// hierarchy, implementations, type definition, code actions,
|
|
50
|
+
// formatter, diagnostics). All read-only in Phase 1 — `rename` /
|
|
51
|
+
// `format` / `code_actions` return PREVIEW edits the dispatcher
|
|
52
|
+
// applies via apply_patch in a future ticket. Permission stays
|
|
53
|
+
// `read` because no workspace mutation happens on dispatch; risk
|
|
54
|
+
// stays `low` because the LSP server is local and the payload is
|
|
55
|
+
// capped at 8 KB per tool.
|
|
56
|
+
{ name: 'symbols_call_hierarchy', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
57
|
+
{ name: 'symbols_code_actions', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
58
|
+
{ name: 'symbols_diagnostics', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
59
|
+
{ name: 'symbols_find_definition', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
60
|
+
{ name: 'symbols_find_references', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
61
|
+
{ name: 'symbols_format', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
62
|
+
{ name: 'symbols_hover', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
63
|
+
{ name: 'symbols_implementations', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
64
|
+
{ name: 'symbols_list_in_file', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
65
|
+
{ name: 'symbols_rename', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
66
|
+
{ name: 'symbols_signature', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
67
|
+
{ name: 'symbols_type_definition', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
68
|
+
{ name: 'symbols_workspace_symbols', permission: 'read', risk: 'low', concurrencySafe: true, m1: false },
|
|
46
69
|
// β7 L5+T11: multi_edit dispatches an ordered batch of Layer A edits
|
|
47
70
|
// as a single transaction. Risk = medium (same chokepoints as `edit`).
|
|
48
71
|
// concurrencySafe = false because the journal serialises one dispatch
|