@alucard_24/pi-codeindex 0.0.0-stage → 1.1.0
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 +32 -0
- package/LICENSE +21 -0
- package/README.md +442 -2
- package/config.schema.json +19 -0
- package/eval/queries.jsonl +8 -0
- package/package.json +33 -3
- package/scripts/setup-gpu.sh +65 -0
- package/scripts/setup-vulkan.mjs +140 -0
- package/scripts/vulkan-sources.json +29 -0
- package/src/backend.js +120 -0
- package/src/chunker.js +276 -0
- package/src/cli.js +204 -0
- package/src/config-ui.js +37 -0
- package/src/config.js +131 -0
- package/src/doctor.js +101 -0
- package/src/embed.js +57 -0
- package/src/eval.js +72 -0
- package/src/extension.js +522 -0
- package/src/graph.js +306 -0
- package/src/indexer.js +302 -0
- package/src/local-embed.js +51 -0
- package/src/model.js +71 -0
- package/src/present.js +177 -0
- package/src/probe.js +23 -0
- package/src/search.js +189 -0
- package/src/service-client.js +126 -0
- package/src/service-policy.js +33 -0
- package/src/service-protocol.js +122 -0
- package/src/service-worker.js +3 -0
- package/src/service.js +172 -0
- package/src/store.js +142 -0
- package/src/sync.js +110 -0
- package/src/util.js +108 -0
- package/src/vulkan.js +151 -0
- package/src/workspace.js +18 -0
package/src/extension.js
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
// Adapter pi (Fase 3): search_code, outline su `read`, riga di stato, /codeindex.
|
|
2
|
+
//
|
|
3
|
+
// Qui c'è solo colla: tutta la logica sta nei moduli puri (chunker, indexer,
|
|
4
|
+
// search, present). Nessuna eccezione esce da questo file: un indice assente,
|
|
5
|
+
// un modello non scaricabile o un reindex fallito degradano il tool, non la
|
|
6
|
+
// sessione.
|
|
7
|
+
|
|
8
|
+
import { defineTool } from '@earendil-works/pi-coding-agent';
|
|
9
|
+
import { Type } from '@earendil-works/pi-ai';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { indexRepo } from './indexer.js';
|
|
13
|
+
import { embeddingStatus, serviceCommand, getServiceStatus, formatServiceStatus } from './embed.js';
|
|
14
|
+
import { formatProfile } from './backend.js';
|
|
15
|
+
import { configCommand } from './config.js';
|
|
16
|
+
import { configure } from './config-ui.js';
|
|
17
|
+
import { doctor, formatDoctor, parseDoctorArgs } from './doctor.js';
|
|
18
|
+
import { loadIndex } from './store.js';
|
|
19
|
+
import { invalidateSearcher, loadSearcher, search } from './search.js';
|
|
20
|
+
import { buildBundle, buildGraph } from './graph.js';
|
|
21
|
+
import { createDebouncedQueue, filterDirty, parsePorcelainStatus } from './sync.js';
|
|
22
|
+
import { fmtBytes, git, headCommit } from './util.js';
|
|
23
|
+
import { resolveWorkspace } from './workspace.js';
|
|
24
|
+
import { formatBundle, formatHits, outlineText, promptLine, shouldAnnotateRead, statusLine, toRepoRel } from './present.js';
|
|
25
|
+
|
|
26
|
+
const STATUS_KEY = 'codeindex';
|
|
27
|
+
const DEFAULT_K = 8;
|
|
28
|
+
const MAX_K = 30;
|
|
29
|
+
const MAX_CAPS = { maxFiles: 5000, maxBytes: 200 * 1024 * 1024 };
|
|
30
|
+
const CONFIG_ENTRY = 'codeindex-config';
|
|
31
|
+
const DEBOUNCE_MS = 800;
|
|
32
|
+
const MAX_SWEEP_PATHS = 200;
|
|
33
|
+
|
|
34
|
+
let state = null; // { repo, mode, enabled, indexing, progress, index, error, abort, queue, watcher, rerun }
|
|
35
|
+
const running = new Map(); // repo -> promise (single-flight per repo)
|
|
36
|
+
|
|
37
|
+
function readEnabled(ctx) {
|
|
38
|
+
try {
|
|
39
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
40
|
+
if (entry.type === 'custom' && entry.customType === CONFIG_ENTRY) {
|
|
41
|
+
const data = entry.data ?? {};
|
|
42
|
+
if (typeof data.enabled === 'boolean') return data.enabled;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
/* nessuno stato salvato: default attivo */
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function showStatus(ctx) {
|
|
52
|
+
// Spento significa spento: niente riga nel footer, come se l'estensione non ci fosse.
|
|
53
|
+
if (!state || state.enabled === false) {
|
|
54
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const profile = embeddingStatus().profile;
|
|
58
|
+
ctx.ui.setStatus(STATUS_KEY, statusLine({
|
|
59
|
+
repo: state.repo,
|
|
60
|
+
enabled: state.enabled,
|
|
61
|
+
indexing: state.indexing,
|
|
62
|
+
progress: state.progress,
|
|
63
|
+
chunks: state.index?.chunks?.length ?? 0,
|
|
64
|
+
}) + (profile ? ` · ${formatProfile(profile)}` : ''));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Legge solo i metadati (per l'outline e la riga di stato): niente vettori. */
|
|
68
|
+
async function refreshMeta() {
|
|
69
|
+
const st = state;
|
|
70
|
+
if (!st) return;
|
|
71
|
+
try {
|
|
72
|
+
const { index } = await loadIndex(st.repo);
|
|
73
|
+
if (state === st) st.index = index;
|
|
74
|
+
} catch {
|
|
75
|
+
if (state === st) st.index = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Indicizzazione in background. Riconcilia sempre (un file non committato non
|
|
81
|
+
* cambia HEAD) ma riusa i vettori: su un repo già indicizzato costa secondi.
|
|
82
|
+
* Single-flight: due session_start, un evento del watcher e un /codeindex
|
|
83
|
+
* reindex non si sovrappongono. Una richiesta arrivata durante un run viene
|
|
84
|
+
* riaccodata (`rerun`) invece di essere persa.
|
|
85
|
+
*/
|
|
86
|
+
function runIndex(ctx, { force = false } = {}) {
|
|
87
|
+
const st = state;
|
|
88
|
+
const repo = st?.repo;
|
|
89
|
+
if (!repo || !st.enabled) return Promise.resolve(null);
|
|
90
|
+
const inflight = running.get(repo);
|
|
91
|
+
if (inflight && !force) {
|
|
92
|
+
st.rerun = true;
|
|
93
|
+
return inflight;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const p = (async () => {
|
|
97
|
+
st.indexing = true;
|
|
98
|
+
st.rerun = false;
|
|
99
|
+
st.progress = null;
|
|
100
|
+
st.error = null;
|
|
101
|
+
showStatus(ctx);
|
|
102
|
+
try {
|
|
103
|
+
const r = await indexRepo({
|
|
104
|
+
root: repo,
|
|
105
|
+
...MAX_CAPS,
|
|
106
|
+
force,
|
|
107
|
+
signal: st.abort.signal,
|
|
108
|
+
log: () => {}, // niente rumore su stderr dentro la TUI
|
|
109
|
+
onProgress: (p) => {
|
|
110
|
+
if (state !== st) return; // sessione sostituita: non toccarne lo stato
|
|
111
|
+
st.progress = { done: p.done, total: p.total };
|
|
112
|
+
showStatus(ctx);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
st.index = r.index;
|
|
116
|
+
const runtime = embeddingStatus();
|
|
117
|
+
const warnings = [...runtime.warnings, ...runtime.attempts.filter((a) => !a.ok).map((a) => `${a.device}: ${a.error}`)];
|
|
118
|
+
if (state === st && st.enabled && !st.backendWarning && warnings.length) {
|
|
119
|
+
st.backendWarning = true;
|
|
120
|
+
ctx.ui.notify(`codeindex: selezionato ${formatProfile(runtime.profile)}; ${warnings.join('; ')}`, 'warning');
|
|
121
|
+
}
|
|
122
|
+
st.graph = null; // il grafo è derivato dall'indice: va ricostruito
|
|
123
|
+
invalidateSearcher(repo);
|
|
124
|
+
return r;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
if (st.abort.signal.aborted) return null;
|
|
127
|
+
st.error = e.message ?? String(e);
|
|
128
|
+
if (state === st && st.enabled && !/abort/i.test(st.error)) {
|
|
129
|
+
// Degradazione silenziosa: il tool resta disponibile e risponde con lo stato.
|
|
130
|
+
ctx.ui.notify(`codeindex: indicizzazione non riuscita (${st.error})`, 'warning');
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
} finally {
|
|
134
|
+
st.indexing = false;
|
|
135
|
+
st.progress = null;
|
|
136
|
+
running.delete(repo);
|
|
137
|
+
if (state === st) {
|
|
138
|
+
showStatus(ctx);
|
|
139
|
+
if (st.rerun && st.enabled && !st.abort.signal.aborted) {
|
|
140
|
+
st.rerun = false;
|
|
141
|
+
void runIndex(ctx);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
})();
|
|
146
|
+
running.set(repo, p);
|
|
147
|
+
return p;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Grafo L2 in cache finché l'indice non cambia (costruirlo su Strata: ~120 ms). */
|
|
151
|
+
async function ensureGraph(index) {
|
|
152
|
+
const stamp = `${index.updatedAt}:${index.chunks.length}`;
|
|
153
|
+
if (state.graph?.stamp === stamp) return state.graph.value;
|
|
154
|
+
const value = await buildGraph(state.repo, index);
|
|
155
|
+
state.graph = { stamp, value };
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Spegne tutto: watcher, coda, indicizzazione in corso. Riaccendibile con enable. */
|
|
160
|
+
function stopBackground() {
|
|
161
|
+
if (!state) return;
|
|
162
|
+
state.abort.abort();
|
|
163
|
+
state.queue?.cancel();
|
|
164
|
+
state.rerun = false;
|
|
165
|
+
try {
|
|
166
|
+
state.watcher?.close();
|
|
167
|
+
} catch {
|
|
168
|
+
/* già chiuso */
|
|
169
|
+
}
|
|
170
|
+
state.watcher = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Path toccati da watcher/hook: passa dal debounce e poi riconcilia. */
|
|
174
|
+
function markDirty(paths, options) {
|
|
175
|
+
if (!state?.queue) return;
|
|
176
|
+
const rel = filterDirty(paths, state.repo, options);
|
|
177
|
+
if (rel.size === 0) return;
|
|
178
|
+
state.queue.add(rel);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Dopo bash: status Git, oppure riconciliazione incrementale senza Git. */
|
|
182
|
+
function sweepChanges() {
|
|
183
|
+
if (!state?.enabled || !state.repo) return;
|
|
184
|
+
if (state.mode === 'folder') {
|
|
185
|
+
// Non possiamo dedurre i path dal comando. La coda coalesca le richieste
|
|
186
|
+
// e indexRepo riusa i vettori invariati, anche senza watcher disponibile.
|
|
187
|
+
state.queue?.add(['.']);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
// Costa ~50–100 ms su un repo grande: il watcher è il segnale primario, questo
|
|
191
|
+
// è la rete di sicurezza. Un giro ogni 1,5 s basta.
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
if (now - (state.lastSweep ?? 0) < 1500) return;
|
|
194
|
+
state.lastSweep = now;
|
|
195
|
+
const out = git(state.repo, ['status', '--porcelain', '-z']);
|
|
196
|
+
if (!out) return;
|
|
197
|
+
const paths = parsePorcelainStatus(out).slice(0, MAX_SWEEP_PATHS);
|
|
198
|
+
if (paths.length) markDirty(paths);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Watcher ricorsivo; se il sistema non lo consente si degrada senza watcher. */
|
|
202
|
+
function startWatcher(ctx) {
|
|
203
|
+
const st = state;
|
|
204
|
+
if (!st?.enabled) return;
|
|
205
|
+
try {
|
|
206
|
+
const watcher = fs.watch(st.repo, { recursive: true }, (event, filename) => {
|
|
207
|
+
if (state !== st || !st.enabled) return;
|
|
208
|
+
if (!filename) {
|
|
209
|
+
if (st.mode === 'folder') st.queue?.add(['.']);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Un rename può essere una directory intera: senza Git non esiste uno
|
|
213
|
+
// status successivo che recuperi i file aggiunti, spostati o cancellati.
|
|
214
|
+
markDirty([String(filename)], {
|
|
215
|
+
includeDirectories: st.mode === 'folder' && event === 'rename',
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
watcher.on('error', () => {
|
|
219
|
+
try {
|
|
220
|
+
watcher.close();
|
|
221
|
+
} catch {
|
|
222
|
+
/* già chiuso */
|
|
223
|
+
}
|
|
224
|
+
if (st.watcher === watcher) st.watcher = null;
|
|
225
|
+
});
|
|
226
|
+
st.watcher = watcher;
|
|
227
|
+
} catch (e) {
|
|
228
|
+
st.watcher = null;
|
|
229
|
+
if (state === st) ctx.ui.notify(`codeindex: watcher non disponibile (${e.message})`, 'warning');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function unavailable(reason) {
|
|
234
|
+
return {
|
|
235
|
+
content: [{ type: 'text', text: `search_code non disponibile: ${reason}` }],
|
|
236
|
+
details: { status: 'unavailable', reason },
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const searchCode = defineTool({
|
|
241
|
+
name: 'search_code',
|
|
242
|
+
label: 'Search code (semantic)',
|
|
243
|
+
description:
|
|
244
|
+
'Semantic search over a local embedding index of this repository. Returns the most relevant ' +
|
|
245
|
+
'code chunks as path:start-end with the enclosing symbol, score and a short preview. ' +
|
|
246
|
+
'Use it to locate code when you do not know the file or the identifiers; grep remains better ' +
|
|
247
|
+
'for exact strings and search_code does not replace it.',
|
|
248
|
+
promptSnippet: 'Semantic search over the local code index (path:lines + symbol)',
|
|
249
|
+
promptGuidelines: [
|
|
250
|
+
'Use search_code before grep when you need to find where a behavior lives and you do not know the names.',
|
|
251
|
+
'Use grep for exact identifiers, error strings and regex; search_code finds concepts, not literals.',
|
|
252
|
+
'Do not repeat the same search_code query: if the hits are wrong, rephrase or switch to grep.',
|
|
253
|
+
],
|
|
254
|
+
parameters: Type.Object({
|
|
255
|
+
query: Type.String({ description: 'What you are looking for, in natural language (Italian or English).' }),
|
|
256
|
+
k: Type.Optional(Type.Number({
|
|
257
|
+
description: `How many chunks to return (default ${DEFAULT_K}, max ${MAX_K}).`,
|
|
258
|
+
minimum: 1,
|
|
259
|
+
maximum: MAX_K,
|
|
260
|
+
})),
|
|
261
|
+
path_prefix: Type.Optional(Type.String({
|
|
262
|
+
description: 'Restrict results to paths starting with this prefix, e.g. "src/extension/".',
|
|
263
|
+
})),
|
|
264
|
+
context: Type.Optional(Type.String({
|
|
265
|
+
description:
|
|
266
|
+
'snippets (default) = matches with previews. bundle = also imports, importers and ' +
|
|
267
|
+
'heuristic callers of the first 3 matches, in one call (slower, saves round-trips).',
|
|
268
|
+
enum: ['snippets', 'bundle'],
|
|
269
|
+
})),
|
|
270
|
+
}),
|
|
271
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
272
|
+
if (!state?.repo) return unavailable('nessuna cartella di progetto attiva');
|
|
273
|
+
if (!state.enabled) return unavailable('indice disattivato (/codeindex enable per riattivarlo)');
|
|
274
|
+
|
|
275
|
+
const k = Math.min(MAX_K, Math.max(1, Math.round(params.k ?? DEFAULT_K)));
|
|
276
|
+
const tCall = performance.now();
|
|
277
|
+
try {
|
|
278
|
+
const res = await search(params.query, {
|
|
279
|
+
root: state.repo,
|
|
280
|
+
k,
|
|
281
|
+
pathPrefix: params.path_prefix ?? null,
|
|
282
|
+
checkStale: true, // L1: se il file è cambiato dopo l'indicizzazione, dillo
|
|
283
|
+
});
|
|
284
|
+
if (signal?.aborted) return unavailable('richiesta annullata');
|
|
285
|
+
|
|
286
|
+
const bundle = (params.context ?? 'snippets') === 'bundle';
|
|
287
|
+
if (bundle) {
|
|
288
|
+
const { index } = await loadSearcher(state.repo);
|
|
289
|
+
const graph = await ensureGraph(index);
|
|
290
|
+
await buildBundle(state.repo, index, res.results, graph, { signal });
|
|
291
|
+
}
|
|
292
|
+
const tookMsTotal = Math.round((performance.now() - tCall) * 10) / 10;
|
|
293
|
+
return {
|
|
294
|
+
content: [{ type: 'text', text: bundle ? formatBundle(params.query, res) : formatHits(params.query, res) }],
|
|
295
|
+
details: {
|
|
296
|
+
status: 'ok',
|
|
297
|
+
context: bundle ? 'bundle' : 'snippets',
|
|
298
|
+
repo: res.repo,
|
|
299
|
+
chunks: res.chunks,
|
|
300
|
+
tookMs: res.tookMs,
|
|
301
|
+
tookMsTotal,
|
|
302
|
+
graph: bundle ? state.graph?.value?.stats : undefined,
|
|
303
|
+
results: res.results,
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
} catch (e) {
|
|
307
|
+
const building = state.indexing
|
|
308
|
+
? ` indicizzazione in corso (${state.progress?.done ?? 0}/${state.progress?.total ?? '?'} chunk)`
|
|
309
|
+
: '';
|
|
310
|
+
return unavailable(`${e.message}${building}`);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
/** Aggiunge l'outline a una `read` completa di un file lungo. */
|
|
316
|
+
async function annotateRead(event, ctx) {
|
|
317
|
+
if (!state?.enabled || !state.repo) return undefined;
|
|
318
|
+
const first = event.content?.find((c) => c.type === 'text');
|
|
319
|
+
if (!first) return undefined;
|
|
320
|
+
|
|
321
|
+
// `details.truncation.totalLines` è il conteggio vero; il testo può essere troncato a 50 KB.
|
|
322
|
+
const lineCount = event.details?.truncation?.totalLines ?? first.text.split('\n').length;
|
|
323
|
+
if (!shouldAnnotateRead(event.input, lineCount)) return undefined;
|
|
324
|
+
|
|
325
|
+
const rel = toRepoRel(state.repo, path.resolve(ctx.cwd, String(event.input.path ?? '')));
|
|
326
|
+
if (!rel) return undefined;
|
|
327
|
+
|
|
328
|
+
if (!state.index) await refreshMeta();
|
|
329
|
+
const outline = outlineText(rel, lineCount, state.index?.chunks);
|
|
330
|
+
if (!outline) return undefined;
|
|
331
|
+
|
|
332
|
+
return { content: [{ type: 'text', text: outline }, ...event.content] };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function statusReport() {
|
|
336
|
+
if (!state) return 'codeindex: nessuna cartella di progetto attiva';
|
|
337
|
+
const i = state.index;
|
|
338
|
+
const lines = [
|
|
339
|
+
`repo : ${state.repo}`,
|
|
340
|
+
`modalità : ${state.mode === 'git' ? 'repository Git' : 'cartella (senza Git)'}`,
|
|
341
|
+
`stato : ${state.enabled ? 'attivo' : 'disattivato'}${state.indexing ? ' · indicizzazione in corso' : ''}`,
|
|
342
|
+
];
|
|
343
|
+
if (state.progress) lines.push(`progresso : ${state.progress.done}/${state.progress.total} chunk`);
|
|
344
|
+
if (i) {
|
|
345
|
+
lines.push(`file : ${i.stats?.files ?? '?'} (${fmtBytes(i.stats?.bytes ?? 0)})`);
|
|
346
|
+
lines.push(`chunk : ${i.chunks.length} · ${i.dims}d · ${i.model} (${i.dtype})`);
|
|
347
|
+
if (state.mode === 'git') lines.push(`head : ${i.head ?? '?'}`);
|
|
348
|
+
lines.push(`aggiornato : ${i.updatedAt}`);
|
|
349
|
+
} else {
|
|
350
|
+
lines.push('indice : assente (usa /codeindex reindex)');
|
|
351
|
+
}
|
|
352
|
+
const runtime = embeddingStatus();
|
|
353
|
+
lines.push(`profilo client: ${runtime.profile ? formatProfile(runtime.profile) : 'non ancora associato al servizio'}`);
|
|
354
|
+
if (i?.device) lines.push(`profilo indice: ${formatProfile(i)}`);
|
|
355
|
+
if (state.error) lines.push(`ultimo errore: ${state.error}`);
|
|
356
|
+
return lines.join('\n');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export default function codeindexExtension(pi) {
|
|
360
|
+
pi.registerTool(searchCode);
|
|
361
|
+
|
|
362
|
+
pi.registerCommand('codeindex', {
|
|
363
|
+
description: 'Indice e embedding locali (status|reindex|enable|disable|config|configure|doctor|service)',
|
|
364
|
+
handler: async (args, ctx) => {
|
|
365
|
+
const [sub = 'status', ...rest] = (args ?? '').trim().split(/\s+/).filter(Boolean);
|
|
366
|
+
// Configurazione/diagnostica funzionano anche fuori da git o con indice spento.
|
|
367
|
+
try {
|
|
368
|
+
if (sub === 'config') {
|
|
369
|
+
ctx.ui.notify(await configCommand(rest), 'info');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (sub === 'configure') {
|
|
373
|
+
const result = await configure(ctx);
|
|
374
|
+
if (result) ctx.ui.notify(result, 'info');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (sub === 'service') {
|
|
378
|
+
ctx.ui.notify(await serviceCommand(rest), 'info');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (sub === 'doctor') {
|
|
382
|
+
const options = parseDoctorArgs(rest);
|
|
383
|
+
if (options.probe) ctx.ui.notify('codeindex: prova isolata del modello (può caricare una copia aggiuntiva, scaricare pesi; timeout 120 s per backend)…', 'info');
|
|
384
|
+
const report = await doctor(options);
|
|
385
|
+
ctx.ui.notify(options.json ? JSON.stringify(report) : formatDoctor(report), report.ok ? 'info' : 'warning');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
} catch (e) {
|
|
389
|
+
ctx.ui.notify(`codeindex: ${e.message}`, 'error');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (!state) {
|
|
393
|
+
ctx.ui.notify('codeindex: nessuna cartella di progetto attiva', 'warning');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (sub === 'status') {
|
|
397
|
+
// La sessione può chiedere lo stato prima che il refresh in background
|
|
398
|
+
// abbia finito: meglio aspettare che mentire ("indice assente").
|
|
399
|
+
if (!state.index) await refreshMeta();
|
|
400
|
+
showStatus(ctx);
|
|
401
|
+
let service;
|
|
402
|
+
try { service = formatServiceStatus(await getServiceStatus()); }
|
|
403
|
+
catch (e) { service = `servizio condiviso: ${e.message}`; }
|
|
404
|
+
ctx.ui.notify(`${statusReport()}\n${service}`, 'info');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (sub === 'reindex') {
|
|
408
|
+
if (state.indexing) {
|
|
409
|
+
ctx.ui.notify('codeindex: indicizzazione già in corso', 'warning');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
ctx.ui.notify('codeindex: reindex avviato in background', 'info');
|
|
413
|
+
void runIndex(ctx, { force: (args ?? '').includes('--force') });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (sub === 'enable' || sub === 'disable') {
|
|
417
|
+
state.enabled = sub === 'enable';
|
|
418
|
+
if (state.enabled) {
|
|
419
|
+
// Un abort precedente resta sul vecchio controller: senza rinnovarlo
|
|
420
|
+
// ogni run futuro uscirebbe subito.
|
|
421
|
+
state.abort = new AbortController();
|
|
422
|
+
state.error = null;
|
|
423
|
+
if (!state.watcher) startWatcher(ctx);
|
|
424
|
+
} else {
|
|
425
|
+
stopBackground(); // watcher, coda, run in corso e rerun pendente
|
|
426
|
+
}
|
|
427
|
+
showStatus(ctx);
|
|
428
|
+
try {
|
|
429
|
+
pi.appendEntry(CONFIG_ENTRY, { enabled: state.enabled });
|
|
430
|
+
} catch {
|
|
431
|
+
/* stato non persistito: vale per questa sessione */
|
|
432
|
+
}
|
|
433
|
+
if (state.enabled) void runIndex(ctx);
|
|
434
|
+
ctx.ui.notify(`codeindex: ${state.enabled ? 'attivo' : 'disattivato'}`, 'info');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
ctx.ui.notify('uso: /codeindex status|reindex [--force]|enable|disable|config [show|set CHIAVE VALORE|unset CHIAVE]|configure|service [status [--json]|stop]|doctor [--probe] [--all] [--device DEVICE] [--webgpu-backend auto|vulkan]', 'warning');
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Risorse di sessione: watcher e timer non vanno creati nel factory.
|
|
442
|
+
pi.on('session_start', async (_event, ctx) => {
|
|
443
|
+
const workspace = resolveWorkspace(ctx.cwd);
|
|
444
|
+
const st = {
|
|
445
|
+
repo: workspace.root,
|
|
446
|
+
mode: workspace.mode,
|
|
447
|
+
// Le cartelle protette richiedono consenso a ogni nuova sessione,
|
|
448
|
+
// anche se un enable era stato salvato nel branch in un altro progetto.
|
|
449
|
+
enabled: workspace.autoIndex && readEnabled(ctx),
|
|
450
|
+
indexing: false,
|
|
451
|
+
progress: null,
|
|
452
|
+
index: null,
|
|
453
|
+
error: null,
|
|
454
|
+
rerun: false,
|
|
455
|
+
abort: new AbortController(),
|
|
456
|
+
queue: null,
|
|
457
|
+
watcher: null,
|
|
458
|
+
graph: null,
|
|
459
|
+
};
|
|
460
|
+
st.queue = createDebouncedQueue(() => {
|
|
461
|
+
if (state === st && st.enabled) void runIndex(ctx);
|
|
462
|
+
}, { delayMs: DEBOUNCE_MS });
|
|
463
|
+
state = st;
|
|
464
|
+
showStatus(ctx);
|
|
465
|
+
if (!workspace.autoIndex && !st.enabled) {
|
|
466
|
+
ctx.ui.notify('codeindex: indicizzazione automatica disattivata nella home o nella radice del disco; usa una cartella di progetto o /codeindex enable', 'info');
|
|
467
|
+
}
|
|
468
|
+
// Non bloccare l'avvio: metadati, watcher e indice corrono in background.
|
|
469
|
+
void (async () => {
|
|
470
|
+
await refreshMeta();
|
|
471
|
+
if (state !== st) return;
|
|
472
|
+
showStatus(ctx);
|
|
473
|
+
if (!st.enabled) return;
|
|
474
|
+
startWatcher(ctx);
|
|
475
|
+
void runIndex(ctx);
|
|
476
|
+
})();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
pi.on('session_shutdown', async () => {
|
|
480
|
+
stopBackground();
|
|
481
|
+
invalidateSearcher();
|
|
482
|
+
state = null;
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Una riga stabile nel system prompt rende il tool scopribile; i conteggi
|
|
486
|
+
// esatti restano nel footer per non invalidare la prompt cache a ogni run.
|
|
487
|
+
pi.on('before_agent_start', async (event, ctx) => {
|
|
488
|
+
showStatus(ctx);
|
|
489
|
+
if (!state?.enabled) return undefined; // spento: nessuna riga nel prompt
|
|
490
|
+
// Un checkout può cambiare HEAD senza eventi utili del watcher.
|
|
491
|
+
if (state.mode === 'git' && !state.indexing && state.index?.head && headCommit(state.repo) !== state.index.head) {
|
|
492
|
+
void runIndex(ctx);
|
|
493
|
+
}
|
|
494
|
+
// Senza watcher e senza Git, un nuovo prompt recupera anche le modifiche
|
|
495
|
+
// esterne a pi. Nessun polling o processo aggiuntivo.
|
|
496
|
+
if (state.mode === 'folder' && !state.watcher) state.queue?.add(['.']);
|
|
497
|
+
const line = promptLine(state?.index?.chunks?.length ?? 0);
|
|
498
|
+
if (!line) return undefined;
|
|
499
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${line}` };
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Invalidazione diretta: `write`/`edit` sanno esattamente che path hanno toccato.
|
|
503
|
+
pi.on('tool_result', async (event, ctx) => {
|
|
504
|
+
if (!state?.enabled) return undefined;
|
|
505
|
+
try {
|
|
506
|
+
// Anche un comando terminato con errore può aver modificato dei file.
|
|
507
|
+
if (event.toolName === 'bash') {
|
|
508
|
+
sweepChanges();
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
if (event.isError) return undefined;
|
|
512
|
+
if (event.toolName === 'read') return await annotateRead(event, ctx);
|
|
513
|
+
if (event.toolName === 'write' || event.toolName === 'edit') {
|
|
514
|
+
markDirty([String(event.input?.path ?? '')]);
|
|
515
|
+
return undefined;
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
return undefined; // un hook non deve mai rompere un tool
|
|
519
|
+
}
|
|
520
|
+
return undefined;
|
|
521
|
+
});
|
|
522
|
+
}
|