@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
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/** Default TTL for cached entries. 5 minutes matches the spec target. */
|
|
2
|
+
export const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
3
|
+
/** Default size cap. Empirically large enough for a heavy navigate session. */
|
|
4
|
+
export const DEFAULT_MAX_ENTRIES = 1000;
|
|
5
|
+
/**
|
|
6
|
+
* Generic TTL cache. The unknown value type is intentional — the cache
|
|
7
|
+
* stores serialized result objects produced by the 13 symbol tools; we
|
|
8
|
+
* surface them back to the caller as the SAME unknown so the cache
|
|
9
|
+
* does not encode the result schema.
|
|
10
|
+
*
|
|
11
|
+
* Thread-safety: Node single-threaded, no concurrent writes possible
|
|
12
|
+
* inside one `pugi` invocation. The cache is NOT shared across
|
|
13
|
+
* spawned subagents — each subagent runs its own LspClient + cache.
|
|
14
|
+
*/
|
|
15
|
+
export class SymbolCache {
|
|
16
|
+
store = new Map();
|
|
17
|
+
ttlMs;
|
|
18
|
+
maxEntries;
|
|
19
|
+
now;
|
|
20
|
+
metrics = { hits: 0, misses: 0, evictions: 0, size: 0 };
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.ttlMs = options?.ttlMs ?? DEFAULT_TTL_MS;
|
|
23
|
+
this.maxEntries = options?.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
24
|
+
this.now = options?.now ?? (() => Date.now());
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a canonical key from the dispatch coordinates. Workspace +
|
|
28
|
+
* language scopes the cache so two different repos opened in the same
|
|
29
|
+
* REPL session do not pollute each other.
|
|
30
|
+
*/
|
|
31
|
+
static makeKey(lang, workspace, verb, args) {
|
|
32
|
+
// Sort keys deterministically so `{file: 'a', line: 1}` and
|
|
33
|
+
// `{line: 1, file: 'a'}` map to the same cache entry.
|
|
34
|
+
const sorted = {};
|
|
35
|
+
for (const key of Object.keys(args).sort()) {
|
|
36
|
+
sorted[key] = args[key];
|
|
37
|
+
}
|
|
38
|
+
return `${lang}::${workspace}::${verb}::${JSON.stringify(sorted)}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Look up a cached value. Returns undefined when:
|
|
42
|
+
*
|
|
43
|
+
* - the key is not in the cache
|
|
44
|
+
* - the entry has aged past `ttlMs` (the entry is deleted in this
|
|
45
|
+
* case so the eviction counter is accurate)
|
|
46
|
+
*
|
|
47
|
+
* Hit updates the entry's `lastAccessed` so the LRU policy keeps
|
|
48
|
+
* frequently-touched entries warm.
|
|
49
|
+
*/
|
|
50
|
+
get(key) {
|
|
51
|
+
const entry = this.store.get(key);
|
|
52
|
+
if (!entry) {
|
|
53
|
+
this.metrics.misses++;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const now = this.now();
|
|
57
|
+
if (now - entry.storedAt > this.ttlMs) {
|
|
58
|
+
this.store.delete(key);
|
|
59
|
+
this.metrics.evictions++;
|
|
60
|
+
this.metrics.misses++;
|
|
61
|
+
this.metrics.size = this.store.size;
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
entry.lastAccessed = now;
|
|
65
|
+
this.metrics.hits++;
|
|
66
|
+
return entry.result;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Store a value. Evicts the LRU entry when the cache hits `maxEntries`.
|
|
70
|
+
* The eviction counter increments for each LRU drop AND each TTL drop
|
|
71
|
+
* so the operator surface can distinguish "cache too small" from "TTL
|
|
72
|
+
* too short".
|
|
73
|
+
*/
|
|
74
|
+
set(key, value) {
|
|
75
|
+
const now = this.now();
|
|
76
|
+
if (this.store.size >= this.maxEntries && !this.store.has(key)) {
|
|
77
|
+
// Find the LRU entry.
|
|
78
|
+
let oldestKey;
|
|
79
|
+
let oldestAccess = Number.POSITIVE_INFINITY;
|
|
80
|
+
for (const [k, entry] of this.store) {
|
|
81
|
+
if (entry.lastAccessed < oldestAccess) {
|
|
82
|
+
oldestAccess = entry.lastAccessed;
|
|
83
|
+
oldestKey = k;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (oldestKey !== undefined) {
|
|
87
|
+
this.store.delete(oldestKey);
|
|
88
|
+
this.metrics.evictions++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
this.store.set(key, { result: value, storedAt: now, lastAccessed: now });
|
|
92
|
+
this.metrics.size = this.store.size;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Invalidate every entry for a workspace. Called by the post-edit
|
|
96
|
+
* hook so a successful `edit`/`write` clears stale symbol locations.
|
|
97
|
+
* Returns the number of entries dropped.
|
|
98
|
+
*/
|
|
99
|
+
invalidateWorkspace(workspace) {
|
|
100
|
+
const prefix = `${workspace}::`;
|
|
101
|
+
// Two prefix forms: `<lang>::<workspace>::...` — we scan the full
|
|
102
|
+
// store and drop entries whose key contains the workspace literal.
|
|
103
|
+
let dropped = 0;
|
|
104
|
+
for (const key of Array.from(this.store.keys())) {
|
|
105
|
+
// The key shape is `lang::workspace::verb::args` — split on `::`
|
|
106
|
+
// and check the second segment. Avoid substring match because a
|
|
107
|
+
// workspace path could legally contain `::` (e.g. a Windows
|
|
108
|
+
// network path).
|
|
109
|
+
const parts = key.split('::');
|
|
110
|
+
if (parts.length >= 2 && parts[1] === workspace) {
|
|
111
|
+
this.store.delete(key);
|
|
112
|
+
dropped++;
|
|
113
|
+
}
|
|
114
|
+
else if (parts.length >= 2 && parts[1]?.startsWith(prefix)) {
|
|
115
|
+
this.store.delete(key);
|
|
116
|
+
dropped++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (dropped > 0) {
|
|
120
|
+
this.metrics.evictions += dropped;
|
|
121
|
+
this.metrics.size = this.store.size;
|
|
122
|
+
}
|
|
123
|
+
return dropped;
|
|
124
|
+
}
|
|
125
|
+
/** Drop every cached entry. */
|
|
126
|
+
clear() {
|
|
127
|
+
this.metrics.evictions += this.store.size;
|
|
128
|
+
this.store.clear();
|
|
129
|
+
this.metrics.size = 0;
|
|
130
|
+
}
|
|
131
|
+
/** Snapshot of the current metrics. The returned record is a copy. */
|
|
132
|
+
snapshot() {
|
|
133
|
+
return { ...this.metrics, size: this.store.size };
|
|
134
|
+
}
|
|
135
|
+
/** Test-only: reset metrics counters without dropping entries. */
|
|
136
|
+
resetMetrics() {
|
|
137
|
+
this.metrics = { hits: 0, misses: 0, evictions: 0, size: this.store.size };
|
|
138
|
+
}
|
|
139
|
+
/** Test-only: number of entries currently stored. */
|
|
140
|
+
size() {
|
|
141
|
+
return this.store.size;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Process-global symbol cache, lazily constructed on first access. The
|
|
146
|
+
* CLI surface picks this up via `getGlobalSymbolCache()` so every
|
|
147
|
+
* subagent / tool invocation in the same process shares the same cache.
|
|
148
|
+
* Subagents spawned in a fresh child process get a fresh cache (by
|
|
149
|
+
* design — the cache is process-scoped, not user-scoped).
|
|
150
|
+
*/
|
|
151
|
+
let globalCache;
|
|
152
|
+
export function getGlobalSymbolCache(options) {
|
|
153
|
+
if (!globalCache) {
|
|
154
|
+
globalCache = new SymbolCache(options);
|
|
155
|
+
}
|
|
156
|
+
return globalCache;
|
|
157
|
+
}
|
|
158
|
+
/** Test-only: drop the singleton so a spec starts with a fresh cache. */
|
|
159
|
+
export function __resetGlobalSymbolCache() {
|
|
160
|
+
globalCache = undefined;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=symbol-cache.js.map
|
|
@@ -67,15 +67,28 @@ export function symbolKindLabel(kind) {
|
|
|
67
67
|
return SYMBOL_KIND_LABELS[kind] ?? 'unknown';
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
|
-
* Build a `SymbolToolsTransport` from a real `LspClient`.
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* surface degrades to
|
|
70
|
+
* Build a `SymbolToolsTransport` from a real `LspClient`. PUGI-78 Phase
|
|
71
|
+
* 1: wires every method the client exposes (13 total). Methods the
|
|
72
|
+
* `LspClient` does NOT support yet stay omitted on the returned
|
|
73
|
+
* transport so the symbol-tools surface degrades to null / [] without
|
|
74
|
+
* forcing the caller to wrap.
|
|
74
75
|
*/
|
|
75
76
|
export function transportFromLspClient(client) {
|
|
76
77
|
return {
|
|
77
78
|
definition: (file, pos) => client.definition(file, pos),
|
|
78
79
|
references: (file, pos) => client.references(file, pos),
|
|
80
|
+
documentSymbols: (file) => client.documentSymbols(file),
|
|
81
|
+
workspaceSymbols: (query) => client.workspaceSymbols(query),
|
|
82
|
+
hover: (file, pos) => client.hover(file, pos),
|
|
83
|
+
signatureHelp: (file, pos) => client.signatureHelp(file, pos),
|
|
84
|
+
implementations: (file, pos) => client.implementations(file, pos),
|
|
85
|
+
typeDefinition: (file, pos) => client.typeDefinition(file, pos),
|
|
86
|
+
rename: (file, pos, newName) => client.rename(file, pos, newName),
|
|
87
|
+
prepareRename: (file, pos) => client.prepareRename(file, pos),
|
|
88
|
+
codeActions: (file, range) => client.codeActions(file, range),
|
|
89
|
+
formatting: (file, options) => client.formatting(file, undefined, options),
|
|
90
|
+
diagnostics: (file) => client.diagnostics(file),
|
|
91
|
+
callHierarchy: (file, pos) => client.callHierarchy(file, pos),
|
|
79
92
|
};
|
|
80
93
|
}
|
|
81
94
|
/**
|
|
@@ -222,6 +235,284 @@ export async function findWorkspaceSymbol(transport, query) {
|
|
|
222
235
|
}
|
|
223
236
|
return out;
|
|
224
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* PUGI-78 Phase 1: hover (type info + docstring). Returns null when:
|
|
240
|
+
*
|
|
241
|
+
* - `filePath` is empty / whitespace
|
|
242
|
+
* - `line` / `character` non-finite or negative
|
|
243
|
+
* - the transport does not implement `hover`
|
|
244
|
+
* - `ok: false` from the transport
|
|
245
|
+
* - the server reports no hover (cursor not on a symbol)
|
|
246
|
+
* - the transport throws
|
|
247
|
+
*
|
|
248
|
+
* Caps the body at 4 KB so a verbose generic type signature does not
|
|
249
|
+
* blow the agent's context window. The cap mirrors the `lspHover`
|
|
250
|
+
* tool's 8 KB cap halved — the symbol surface is closer to "give me
|
|
251
|
+
* one sentence" than the full hover blob.
|
|
252
|
+
*/
|
|
253
|
+
export async function hoverSymbol(transport, filePath, line, character) {
|
|
254
|
+
if (!isNonEmptyString(filePath))
|
|
255
|
+
return null;
|
|
256
|
+
if (!isFiniteNonNegativeInt(line) || !isFiniteNonNegativeInt(character))
|
|
257
|
+
return null;
|
|
258
|
+
if (!transport.hover)
|
|
259
|
+
return null;
|
|
260
|
+
let result;
|
|
261
|
+
try {
|
|
262
|
+
result = await transport.hover(filePath, { line, character });
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
if (!result.ok)
|
|
268
|
+
return null;
|
|
269
|
+
if (!result.value)
|
|
270
|
+
return null;
|
|
271
|
+
const HOVER_CAP_BYTES = 4 * 1024;
|
|
272
|
+
const raw = result.value.content;
|
|
273
|
+
const bytes = Buffer.byteLength(raw, 'utf8');
|
|
274
|
+
const truncated = bytes > HOVER_CAP_BYTES;
|
|
275
|
+
const content = truncated
|
|
276
|
+
? Buffer.from(raw, 'utf8').subarray(0, HOVER_CAP_BYTES).toString('utf8') + '\n... [truncated]'
|
|
277
|
+
: raw;
|
|
278
|
+
return {
|
|
279
|
+
content,
|
|
280
|
+
...(result.value.range ? { range: result.value.range } : {}),
|
|
281
|
+
...(truncated ? { truncated: true } : {}),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* PUGI-78 Phase 1: function signature at a call site. Returns null when
|
|
286
|
+
* the transport does not implement `signatureHelp` OR the server
|
|
287
|
+
* reports no signature OR the transport fails / throws.
|
|
288
|
+
*/
|
|
289
|
+
export async function signatureAt(transport, filePath, line, character) {
|
|
290
|
+
if (!isNonEmptyString(filePath))
|
|
291
|
+
return null;
|
|
292
|
+
if (!isFiniteNonNegativeInt(line) || !isFiniteNonNegativeInt(character))
|
|
293
|
+
return null;
|
|
294
|
+
if (!transport.signatureHelp)
|
|
295
|
+
return null;
|
|
296
|
+
let result;
|
|
297
|
+
try {
|
|
298
|
+
result = await transport.signatureHelp(filePath, { line, character });
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
if (!result.ok)
|
|
304
|
+
return null;
|
|
305
|
+
if (!result.value)
|
|
306
|
+
return null;
|
|
307
|
+
const out = { label: result.value.label, parameters: result.value.parameters };
|
|
308
|
+
if (result.value.documentation)
|
|
309
|
+
out.documentation = result.value.documentation;
|
|
310
|
+
if (typeof result.value.activeParameter === 'number')
|
|
311
|
+
out.activeParameter = result.value.activeParameter;
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* PUGI-78 Phase 1: implementations of an interface / abstract method.
|
|
316
|
+
* Returns `[]` on any failure mode (parity with `findReferences`).
|
|
317
|
+
*/
|
|
318
|
+
export async function findImplementations(transport, filePath, line, character) {
|
|
319
|
+
if (!isNonEmptyString(filePath))
|
|
320
|
+
return [];
|
|
321
|
+
if (!isFiniteNonNegativeInt(line) || !isFiniteNonNegativeInt(character))
|
|
322
|
+
return [];
|
|
323
|
+
if (!transport.implementations)
|
|
324
|
+
return [];
|
|
325
|
+
let result;
|
|
326
|
+
try {
|
|
327
|
+
result = await transport.implementations(filePath, { line, character });
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
if (!result.ok)
|
|
333
|
+
return [];
|
|
334
|
+
return collapseLocations(result.value);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* PUGI-78 Phase 1: type-definition (vs value-definition). Returns null
|
|
338
|
+
* matching `findDefinition`'s contract.
|
|
339
|
+
*/
|
|
340
|
+
export async function findTypeDefinition(transport, filePath, line, character) {
|
|
341
|
+
if (!isNonEmptyString(filePath))
|
|
342
|
+
return null;
|
|
343
|
+
if (!isFiniteNonNegativeInt(line) || !isFiniteNonNegativeInt(character))
|
|
344
|
+
return null;
|
|
345
|
+
if (!transport.typeDefinition)
|
|
346
|
+
return null;
|
|
347
|
+
let result;
|
|
348
|
+
try {
|
|
349
|
+
result = await transport.typeDefinition(filePath, { line, character });
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
if (!result.ok)
|
|
355
|
+
return null;
|
|
356
|
+
const first = result.value[0];
|
|
357
|
+
if (!first)
|
|
358
|
+
return null;
|
|
359
|
+
const file = pickFilePath(first);
|
|
360
|
+
if (!isNonEmptyString(file))
|
|
361
|
+
return null;
|
|
362
|
+
return { file, line: first.range.start.line, character: first.range.start.character };
|
|
363
|
+
}
|
|
364
|
+
export async function renameSymbol(transport, filePath, line, character, newName) {
|
|
365
|
+
if (!isNonEmptyString(filePath))
|
|
366
|
+
return null;
|
|
367
|
+
if (!isFiniteNonNegativeInt(line) || !isFiniteNonNegativeInt(character))
|
|
368
|
+
return null;
|
|
369
|
+
if (!isNonEmptyString(newName))
|
|
370
|
+
return null;
|
|
371
|
+
if (!transport.rename)
|
|
372
|
+
return null;
|
|
373
|
+
// Best-effort prepare-rename precheck. If the server says the cursor
|
|
374
|
+
// is not on a renameable token, we return null without dispatching
|
|
375
|
+
// the full rename — saves a wasted round-trip + avoids returning a
|
|
376
|
+
// misleading "0 edits" preview.
|
|
377
|
+
if (transport.prepareRename) {
|
|
378
|
+
try {
|
|
379
|
+
const prep = await transport.prepareRename(filePath, { line, character });
|
|
380
|
+
if (prep.ok && prep.value === null)
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// Continue — some servers fail prepare-rename but still execute
|
|
385
|
+
// the full rename. Phase 1 honors the looser fallback.
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
let result;
|
|
389
|
+
try {
|
|
390
|
+
result = await transport.rename(filePath, { line, character }, newName);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
if (!result.ok)
|
|
396
|
+
return null;
|
|
397
|
+
if (!result.value)
|
|
398
|
+
return null;
|
|
399
|
+
if (result.value.edits.length === 0)
|
|
400
|
+
return null;
|
|
401
|
+
return { files: result.value.files, edits: result.value.edits };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* PUGI-78 Phase 1: code-action / quick-fix list at a range. Returns
|
|
405
|
+
* `[]` on any failure mode.
|
|
406
|
+
*/
|
|
407
|
+
export async function codeActionsAt(transport, filePath, startLine, startChar, endLine, endChar) {
|
|
408
|
+
if (!isNonEmptyString(filePath))
|
|
409
|
+
return [];
|
|
410
|
+
if (!isFiniteNonNegativeInt(startLine) ||
|
|
411
|
+
!isFiniteNonNegativeInt(startChar) ||
|
|
412
|
+
!isFiniteNonNegativeInt(endLine) ||
|
|
413
|
+
!isFiniteNonNegativeInt(endChar)) {
|
|
414
|
+
return [];
|
|
415
|
+
}
|
|
416
|
+
if (!transport.codeActions)
|
|
417
|
+
return [];
|
|
418
|
+
const range = {
|
|
419
|
+
start: { line: startLine, character: startChar },
|
|
420
|
+
end: { line: endLine, character: endChar },
|
|
421
|
+
};
|
|
422
|
+
let result;
|
|
423
|
+
try {
|
|
424
|
+
result = await transport.codeActions(filePath, range);
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
if (!result.ok)
|
|
430
|
+
return [];
|
|
431
|
+
return [...result.value];
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* PUGI-78 Phase 1: formatter — returns the text edits the server would
|
|
435
|
+
* apply. Returns `[]` on any failure mode.
|
|
436
|
+
*/
|
|
437
|
+
export async function formatFile(transport, filePath, options) {
|
|
438
|
+
if (!isNonEmptyString(filePath))
|
|
439
|
+
return [];
|
|
440
|
+
if (!transport.formatting)
|
|
441
|
+
return [];
|
|
442
|
+
let result;
|
|
443
|
+
try {
|
|
444
|
+
result = await transport.formatting(filePath, options);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
if (!result.ok)
|
|
450
|
+
return [];
|
|
451
|
+
return [...result.value];
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* PUGI-78 Phase 1: diagnostics for `filePath`. Returns `[]` on any
|
|
455
|
+
* failure mode. Mirrors the agent surface of `lspDiagnostics` but
|
|
456
|
+
* folded into the symbols.* namespace for ergonomic discoverability.
|
|
457
|
+
*/
|
|
458
|
+
export async function diagnosticsFor(transport, filePath) {
|
|
459
|
+
if (!isNonEmptyString(filePath))
|
|
460
|
+
return [];
|
|
461
|
+
if (!transport.diagnostics)
|
|
462
|
+
return [];
|
|
463
|
+
let result;
|
|
464
|
+
try {
|
|
465
|
+
result = await transport.diagnostics(filePath);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
return [];
|
|
469
|
+
}
|
|
470
|
+
if (!result.ok)
|
|
471
|
+
return [];
|
|
472
|
+
return [...result.value];
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* PUGI-78 Phase 1: call hierarchy at a symbol position. Returns the
|
|
476
|
+
* incoming and outgoing edges. Empty arrays on any failure mode.
|
|
477
|
+
*/
|
|
478
|
+
export async function callHierarchyAt(transport, filePath, line, character) {
|
|
479
|
+
const empty = { incoming: [], outgoing: [] };
|
|
480
|
+
if (!isNonEmptyString(filePath))
|
|
481
|
+
return empty;
|
|
482
|
+
if (!isFiniteNonNegativeInt(line) || !isFiniteNonNegativeInt(character))
|
|
483
|
+
return empty;
|
|
484
|
+
if (!transport.callHierarchy)
|
|
485
|
+
return empty;
|
|
486
|
+
let result;
|
|
487
|
+
try {
|
|
488
|
+
result = await transport.callHierarchy(filePath, { line, character });
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
return empty;
|
|
492
|
+
}
|
|
493
|
+
if (!result.ok)
|
|
494
|
+
return empty;
|
|
495
|
+
return { incoming: [...result.value.incoming], outgoing: [...result.value.outgoing] };
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Internal: collapse `LspLocation[]` to flat references the agent
|
|
499
|
+
* surface ships. Identical to the `findReferences` loop body — lifted
|
|
500
|
+
* to avoid a second copy.
|
|
501
|
+
*/
|
|
502
|
+
function collapseLocations(locations) {
|
|
503
|
+
const out = [];
|
|
504
|
+
for (const loc of locations) {
|
|
505
|
+
const file = pickFilePath(loc);
|
|
506
|
+
if (!isNonEmptyString(file))
|
|
507
|
+
continue;
|
|
508
|
+
out.push({
|
|
509
|
+
file,
|
|
510
|
+
line: loc.range.start.line,
|
|
511
|
+
character: loc.range.start.character,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return out;
|
|
515
|
+
}
|
|
225
516
|
// -- internals ---------------------------------------------------------------
|
|
226
517
|
function isNonEmptyString(value) {
|
|
227
518
|
return typeof value === 'string' && value.trim().length > 0;
|
|
@@ -368,5 +659,6 @@ export const __test__ = {
|
|
|
368
659
|
pickRangeStart,
|
|
369
660
|
symbolKindLabel,
|
|
370
661
|
walkDocumentSymbol,
|
|
662
|
+
collapseLocations,
|
|
371
663
|
};
|
|
372
664
|
//# sourceMappingURL=symbol-tools.js.map
|
|
@@ -6,7 +6,7 @@ import { bashToolSync } from '../../tools/bash.js';
|
|
|
6
6
|
* The shapes intentionally mirror the engine-loop tool schemas in
|
|
7
7
|
* `core/engine/tool-bridge.ts` so an MCP client and the Pugi engine see
|
|
8
8
|
* the same parameter contracts. This is the "Pugi as MCP server"
|
|
9
|
-
* surface — other agents (the upstream tool, Codex,
|
|
9
|
+
* surface — other agents (the upstream tool, Codex, peer tooling) call these to
|
|
10
10
|
* read / mutate the workspace through us, with all our security gates
|
|
11
11
|
* (path containment, plan-mode refusal, bash classifier, settings) in
|
|
12
12
|
* the loop.
|
package/dist/core/mcp/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
/**
|
|
3
3
|
* Pugi MCP server (β4 M2) — exposes Pugi's native tool surface to other
|
|
4
|
-
* agents (the upstream tool,
|
|
4
|
+
* agents (the upstream tool, peer tooling, peer CLI, any client that speaks
|
|
5
5
|
* MCP).
|
|
6
6
|
*
|
|
7
7
|
* Transport-agnostic core. The stdio entry-point lives at the bottom of
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Persona-memory secret scanner (backlog,
|
|
2
|
+
* Persona-memory secret scanner (backlog, hardening).
|
|
3
3
|
*
|
|
4
4
|
* Defense against API keys / credentials accidentally landing in shared
|
|
5
5
|
* persona memory. A naive operator typing `pugi memory write fact "Use
|
|
6
6
|
* API key sk-ant-..."` would silently persist that secret to the
|
|
7
|
-
* `
|
|
7
|
+
* `the platform database.persona_memory` table — visible to every persona, every
|
|
8
8
|
* recall query, every dual-write sink. This module is the chokepoint
|
|
9
9
|
* that refuses such writes.
|
|
10
10
|
*
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* 3. GitHub PAT / installation tokens (ghp_/ghs_/gho_/ghu_)
|
|
26
26
|
* high
|
|
27
27
|
* 4. AWS access key id AKIA… high
|
|
28
|
-
* 5. Plane API token plane_api_… high
|
|
28
|
+
* 5. Plane API token plane_api_… high [pugi-leak-ok]
|
|
29
29
|
* 6. npm token npm_… high
|
|
30
30
|
* 7. Slack token xox[bpoars]-… high
|
|
31
31
|
* 8. Stripe secret key sk_(live|test)_… high
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
* instead of reject. The scanner exposes `redactSecrets` for that
|
|
60
60
|
* caller — see `runtime/commands/memory.ts`.
|
|
61
61
|
*
|
|
62
|
-
* #
|
|
62
|
+
* # independent implementation provenance
|
|
63
63
|
*
|
|
64
64
|
* Inspired by the the upstream tool teamMemorySync.secretScanner pattern
|
|
65
|
-
* (intel from leak-research memos).
|
|
65
|
+
* (intel from leak-research memos). independent implementation TypeScript
|
|
66
66
|
* implementation — no upstream code reused. Pattern vocabulary was
|
|
67
67
|
* cross-referenced against the existing
|
|
68
68
|
* `apps/pugi-cli/scripts/secret-scanner.mjs` tarball gate so a single
|
|
@@ -124,7 +124,7 @@ const SECRET_RULES = [
|
|
|
124
124
|
pattern: 'plane-api-token',
|
|
125
125
|
// Plane (project management) personal API tokens. Bounded to 20+
|
|
126
126
|
// url-safe chars after the prefix.
|
|
127
|
-
regex: /\bplane_api_[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g,
|
|
127
|
+
regex: /\bplane_api_[A-Za-z0-9]{20,}(?![A-Za-z0-9])/g, // [pugi-leak-ok]
|
|
128
128
|
confidence: 'high',
|
|
129
129
|
},
|
|
130
130
|
{
|
|
@@ -119,7 +119,7 @@ export async function ensureInitialized(opts) {
|
|
|
119
119
|
write(`No Pugi workspace found at ${root}.\n`);
|
|
120
120
|
const answer = (await opts.prompt('Initialize a new Pugi workspace here? (Y/n) ')).trim().toLowerCase();
|
|
121
121
|
// Default = yes (empty input OR leading 'y'). Anything else = no.
|
|
122
|
-
// Mirrors the gh CLI /
|
|
122
|
+
// Mirrors the gh CLI / the upstream prompt convention where the upper-
|
|
123
123
|
// case option in `(Y/n)` is the default-on-Enter answer.
|
|
124
124
|
const acceptedShort = answer === '' || answer === 'y' || answer === 'yes';
|
|
125
125
|
if (!acceptedShort) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Plan-as-FILE artifact store (Pugi backlog).
|
|
3
3
|
*
|
|
4
4
|
* Pattern absorbed from the the upstream tool `ExitPlanMode` leak intel
|
|
5
|
-
* (
|
|
5
|
+
* (independent implementation TypeScript reimplementation — no source was copied; only
|
|
6
6
|
* the file-as-artifact concept). When Pugi enters plan-mode the engine
|
|
7
7
|
* routes the plan body to `.pugi/plans/<plan-id>.md` instead of the
|
|
8
8
|
* message stream so it survives `/compact`, becomes diffable across
|
|
@@ -205,7 +205,7 @@ function yamlScalar(value) {
|
|
|
205
205
|
// Empty string is also disambiguated by quoting.
|
|
206
206
|
if (value.length === 0)
|
|
207
207
|
return '""';
|
|
208
|
-
if (/[
|
|
208
|
+
if (/[-"]/.test(value)) {
|
|
209
209
|
return JSON.stringify(value);
|
|
210
210
|
}
|
|
211
211
|
if (/[:#\n\t]/.test(value)) {
|
package/dist/core/repl/ask.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Pugi's persona prompt teaches Pugi to emit two structured XML envelopes
|
|
5
5
|
* when she would otherwise have to guess. Operator chat then pauses on a
|
|
6
6
|
* modal until the operator answers, eliminating the "fabricate a default
|
|
7
|
-
* silently" failure mode that
|
|
7
|
+
* silently" failure mode that peer CLI, the upstream tool, and Gemini CLI all
|
|
8
8
|
* trip on with low-confidence intents.
|
|
9
9
|
*
|
|
10
10
|
* <pugi-ask>
|
|
@@ -2019,7 +2019,7 @@ export class ReplSession {
|
|
|
2019
2019
|
// Surface the operator's choice as a transcript row so the
|
|
2020
2020
|
// conversation reads linearly. The label of the chosen option
|
|
2021
2021
|
// (or the literal custom input) is more readable than the bare
|
|
2022
|
-
// value -
|
|
2022
|
+
// value - peer CLI's "you chose: Vercel" pattern.
|
|
2023
2023
|
const humanLabel = humanLabelForVerdict(tag, sanitisedVerdict);
|
|
2024
2024
|
this.appendOperatorLine(humanLabel);
|
|
2025
2025
|
// Local-origin modals (operator typed `/ask`) never need an
|
|
@@ -4105,7 +4105,7 @@ export function colorizeQuotaRow(row, pct) {
|
|
|
4105
4105
|
* string and emit a synthesised `ToolCallEntry`. Returns null when no
|
|
4106
4106
|
* known tool pattern matches.
|
|
4107
4107
|
*
|
|
4108
|
-
* The grammar mirrors the way the upstream tool,
|
|
4108
|
+
* The grammar mirrors the way the upstream tool, peer CLI, and Gemini CLI
|
|
4109
4109
|
* display tool calls in their tool stream panes:
|
|
4110
4110
|
*
|
|
4111
4111
|
* Read(path)
|
|
@@ -4293,7 +4293,7 @@ function encodePlanReviewVerdictLocal(result) {
|
|
|
4293
4293
|
}
|
|
4294
4294
|
/**
|
|
4295
4295
|
* Compose the human-readable transcript line that records the
|
|
4296
|
-
* operator's ask verdict. Mirrors
|
|
4296
|
+
* operator's ask verdict. Mirrors peer CLI's "you chose: <label>"
|
|
4297
4297
|
* pattern so the conversation reads linearly.
|
|
4298
4298
|
*/
|
|
4299
4299
|
function humanLabelForVerdict(tag, verdict) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The REPL input box surfaces a palette of slash commands the operator
|
|
5
5
|
* can run from inside a persistent session. The wave-2 expansion (CEO
|
|
6
6
|
*) grows the surface from 6 to 20 commands so the `/help`
|
|
7
|
-
* overlay matches the breadth the upstream tool /
|
|
7
|
+
* overlay matches the breadth the upstream tool / peer CLI operators expect.
|
|
8
8
|
*
|
|
9
9
|
* The registry is pure: each `parseSlashCommand` call returns a
|
|
10
10
|
* `SlashCommandResult` describing what the REPL session should do next.
|