@alucard_24/pi-codeindex 1.0.0 → 1.1.1
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 +52 -0
- package/README.md +171 -25
- package/config.schema.json +4 -1
- package/package.json +2 -2
- package/src/cli.js +12 -4
- package/src/config-ui.js +6 -4
- package/src/config.js +10 -5
- package/src/doctor.js +7 -2
- package/src/embed.js +39 -70
- package/src/extension.js +159 -76
- package/src/local-embed.js +51 -0
- package/src/probe.js +10 -7
- package/src/project-state.js +53 -0
- package/src/service-client.js +130 -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/sync.js +9 -5
- package/src/workspace.js +26 -0
package/src/embed.js
CHANGED
|
@@ -1,88 +1,57 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
// Facciata dei client pi/CLI. L'unica pipeline vive nel servizio condiviso;
|
|
2
|
+
// nessun errore di IPC attiva un fallback in-process.
|
|
3
|
+
import { createEmbeddingClient } from './service-client.js';
|
|
4
|
+
import { modelPath } from './model.js';
|
|
5
|
+
import { MODEL_ID, DIMS, QUERY_PREFIX, docPrefix } from './backend.js';
|
|
6
6
|
|
|
7
7
|
export { MODEL_ID, DIMS, QUERY_PREFIX, docPrefix };
|
|
8
|
+
const client = createEmbeddingClient();
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
initialize: async (profile, config) => {
|
|
14
|
-
const sessionOptions = {};
|
|
15
|
-
if (profile.webgpuBackend === 'vulkan') {
|
|
16
|
-
// Il setup mancante fallisce PRIMA di scaricare il modello. Backend unico,
|
|
17
|
-
// esplicito e non registrato come fallback dei provider standard.
|
|
18
|
-
const { registerVulkanBackend } = await import('./vulkan.js');
|
|
19
|
-
sessionOptions.executionProviders = [await registerVulkanBackend()];
|
|
20
|
-
}
|
|
21
|
-
const dir = await ensureTextModel(profile, config);
|
|
22
|
-
// Import lazy: config/status/doctor senza --probe non caricano il modello.
|
|
23
|
-
const { pipeline } = await import('@huggingface/transformers');
|
|
24
|
-
if (config.cpuThreads) sessionOptions.intraOpNumThreads = config.cpuThreads;
|
|
25
|
-
if (profile.device === 'dml') {
|
|
26
|
-
sessionOptions.executionMode = 'sequential';
|
|
27
|
-
sessionOptions.enableMemPattern = false;
|
|
28
|
-
}
|
|
29
|
-
let pipe;
|
|
30
|
-
try {
|
|
31
|
-
pipe = await pipeline('feature-extraction', dir, {
|
|
32
|
-
dtype: profile.dtype, device: profile.device, local_files_only: true,
|
|
33
|
-
session_options: sessionOptions,
|
|
34
|
-
});
|
|
35
|
-
const out = await pipe([QUERY_PREFIX + 'code retrieval smoke test'], { pooling: 'mean', normalize: true });
|
|
36
|
-
validateEmbeddings(out.tolist(), 1);
|
|
37
|
-
return pipe;
|
|
38
|
-
} catch (e) {
|
|
39
|
-
try { await pipe?.dispose(); } catch { /* conserva l'errore originale */ }
|
|
40
|
-
throw e;
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
export async function getEmbedder() {
|
|
46
|
-
return (await runtime.get()).pipe;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function getEmbeddingProfile() {
|
|
50
|
-
return (await runtime.get()).profile;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function embeddingStatus() {
|
|
54
|
-
return runtime.info();
|
|
55
|
-
}
|
|
10
|
+
export const getEmbeddingProfile = () => client.getProfile();
|
|
11
|
+
export const embeddingStatus = () => client.info();
|
|
12
|
+
export const getServiceStatus = () => client.status();
|
|
13
|
+
export const stopEmbeddingService = () => client.stop();
|
|
56
14
|
|
|
57
15
|
/** Testi -> Float32Array[] finiti, normalizzati L2 per default. */
|
|
58
|
-
export async function embed(texts,
|
|
16
|
+
export async function embed(texts, options = {}) {
|
|
59
17
|
if (!Array.isArray(texts) || texts.length === 0) return [];
|
|
60
|
-
|
|
61
|
-
// Non fare fallback qui: l'indice è già associato al profilo selezionato.
|
|
62
|
-
const out = await pipe(texts, { pooling: 'mean', normalize });
|
|
63
|
-
const rows = validateEmbeddings(out.tolist(), texts.length);
|
|
64
|
-
return rows.map((row) => Float32Array.from(row));
|
|
18
|
+
return client.embed(texts, options);
|
|
65
19
|
}
|
|
66
|
-
|
|
67
|
-
export function embedQuery(q) {
|
|
68
|
-
return embed([QUERY_PREFIX + q]);
|
|
69
|
-
}
|
|
70
|
-
|
|
20
|
+
export function embedQuery(q) { return embed([QUERY_PREFIX + q]); }
|
|
71
21
|
export function embedDocs(docs) {
|
|
72
22
|
return embed(docs.map((d) => typeof d === 'string' ? d : docPrefix(d.path, d.title) + d.text));
|
|
73
23
|
}
|
|
74
|
-
|
|
75
24
|
export function cosine(a, b) {
|
|
76
25
|
let dot = 0, na = 0, nb = 0;
|
|
77
26
|
for (let i = 0; i < a.length; i++) {
|
|
78
|
-
dot += a[i] * b[i];
|
|
79
|
-
na += a[i] * a[i];
|
|
80
|
-
nb += b[i] * b[i];
|
|
27
|
+
dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i];
|
|
81
28
|
}
|
|
82
29
|
return dot / (Math.sqrt(na) * Math.sqrt(nb) || 1);
|
|
83
30
|
}
|
|
84
|
-
|
|
85
31
|
export async function modelDir() {
|
|
86
|
-
const
|
|
87
|
-
return modelPath(config, profile.dtype);
|
|
32
|
+
const profile = await client.getProfile();
|
|
33
|
+
return modelPath(client.info().config, profile.dtype);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatServiceStatus(status) {
|
|
37
|
+
if (status.stopping) return `servizio condiviso: in arresto · PID ${status.pid} (rilascio delle risorse in corso)`;
|
|
38
|
+
if (!status.running) return `servizio condiviso: fermo (avvio automatico al prossimo embedding) · porta ${status.port}`;
|
|
39
|
+
const profile = status.profile;
|
|
40
|
+
return [`servizio condiviso: attivo · PID ${status.pid} · 127.0.0.1:${status.port}`,
|
|
41
|
+
`modello: ${profile ? `${profile.device}${profile.device === 'webgpu' ? `→${profile.webgpuBackend}` : ''}/${profile.dtype}` : status.initializing ? 'caricamento' : 'non caricato'}`,
|
|
42
|
+
`richieste attive/in coda: ${status.pending} · RSS servizio: ${Math.round(status.rssBytes / 1024 / 1024)} MiB`,
|
|
43
|
+
`uscita dopo inattività: ${status.idleMs} ms`].join('\n');
|
|
44
|
+
}
|
|
45
|
+
export async function serviceCommand(args, { service = client } = {}) {
|
|
46
|
+
const [action = 'status', ...rest] = args;
|
|
47
|
+
if (rest.some((arg) => arg !== '--json') || rest.length > 1 || (action !== 'status' && rest.length)) throw new Error('uso: service [status [--json]|stop]');
|
|
48
|
+
if (action === 'status') {
|
|
49
|
+
const status = await service.status();
|
|
50
|
+
return rest.includes('--json') ? JSON.stringify(status) : formatServiceStatus(status);
|
|
51
|
+
}
|
|
52
|
+
if (action === 'stop') {
|
|
53
|
+
const result = await service.stop();
|
|
54
|
+
return result.stopping ? `arresto del servizio condiviso richiesto (PID ${result.pid}); si riavvierà al prossimo embedding` : 'servizio condiviso già fermo';
|
|
55
|
+
}
|
|
56
|
+
throw new Error('uso: service [status [--json]|stop]');
|
|
88
57
|
}
|
package/src/extension.js
CHANGED
|
@@ -10,41 +10,70 @@ import { Type } from '@earendil-works/pi-ai';
|
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import { indexRepo } from './indexer.js';
|
|
13
|
-
import { embeddingStatus } from './embed.js';
|
|
13
|
+
import { embeddingStatus, serviceCommand, getServiceStatus, formatServiceStatus } from './embed.js';
|
|
14
14
|
import { formatProfile } from './backend.js';
|
|
15
15
|
import { configCommand } from './config.js';
|
|
16
|
+
import { readProjectState, writeProjectState } from './project-state.js';
|
|
16
17
|
import { configure } from './config-ui.js';
|
|
17
18
|
import { doctor, formatDoctor, parseDoctorArgs } from './doctor.js';
|
|
18
19
|
import { loadIndex } from './store.js';
|
|
19
20
|
import { invalidateSearcher, loadSearcher, search } from './search.js';
|
|
20
21
|
import { buildBundle, buildGraph } from './graph.js';
|
|
21
22
|
import { createDebouncedQueue, filterDirty, parsePorcelainStatus } from './sync.js';
|
|
22
|
-
import { fmtBytes, git, headCommit
|
|
23
|
+
import { fmtBytes, git, headCommit } from './util.js';
|
|
24
|
+
import { resolveWorkspace } from './workspace.js';
|
|
23
25
|
import { formatBundle, formatHits, outlineText, promptLine, shouldAnnotateRead, statusLine, toRepoRel } from './present.js';
|
|
24
26
|
|
|
25
27
|
const STATUS_KEY = 'codeindex';
|
|
26
28
|
const DEFAULT_K = 8;
|
|
27
29
|
const MAX_K = 30;
|
|
28
30
|
const MAX_CAPS = { maxFiles: 5000, maxBytes: 200 * 1024 * 1024 };
|
|
29
|
-
const CONFIG_ENTRY = 'codeindex-config';
|
|
30
31
|
const DEBOUNCE_MS = 800;
|
|
31
32
|
const MAX_SWEEP_PATHS = 200;
|
|
32
33
|
|
|
33
|
-
let state = null; // { repo, enabled, indexing, progress, index, error, abort, queue, watcher, rerun }
|
|
34
|
+
let state = null; // { repo, mode, enabled, indexing, progress, index, error, abort, queue, watcher, rerun }
|
|
34
35
|
const running = new Map(); // repo -> promise (single-flight per repo)
|
|
35
36
|
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
function applyEnabled(ctx, enabled, { start = true } = {}) {
|
|
38
|
+
const st = state;
|
|
39
|
+
st.enabled = enabled;
|
|
40
|
+
if (enabled) {
|
|
41
|
+
if (st.abort.signal.aborted) st.abort = new AbortController();
|
|
42
|
+
st.error = null;
|
|
43
|
+
if (start) {
|
|
44
|
+
if (!st.watcher) startWatcher(ctx);
|
|
45
|
+
void runIndex(ctx);
|
|
43
46
|
}
|
|
44
|
-
}
|
|
45
|
-
|
|
47
|
+
} else {
|
|
48
|
+
stopBackground();
|
|
49
|
+
st.index = null;
|
|
50
|
+
st.graph = null;
|
|
51
|
+
invalidateSearcher(st.repo);
|
|
52
|
+
}
|
|
53
|
+
showStatus(ctx);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function syncProjectState(ctx, options = {}) {
|
|
57
|
+
const st = state;
|
|
58
|
+
if (!st) return false;
|
|
59
|
+
try {
|
|
60
|
+
const saved = await readProjectState(st.repo);
|
|
61
|
+
if (state !== st) return false;
|
|
62
|
+
if (st.error === st.activationError) st.error = null;
|
|
63
|
+
st.activationError = null;
|
|
64
|
+
st.activationFile = saved.file;
|
|
65
|
+
const enabled = saved.enabled && (st.autoIndex || st.manualConsent);
|
|
66
|
+
if (enabled !== st.enabled) applyEnabled(ctx, enabled, options);
|
|
67
|
+
return enabled;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
if (state !== st) return false;
|
|
70
|
+
const changed = st.activationError !== e.message;
|
|
71
|
+
applyEnabled(ctx, false, options);
|
|
72
|
+
st.activationError = e.message;
|
|
73
|
+
st.error = e.message;
|
|
74
|
+
if (changed) ctx.ui.notify(`codeindex: ${e.message}`, 'warning');
|
|
75
|
+
return false;
|
|
46
76
|
}
|
|
47
|
-
return true;
|
|
48
77
|
}
|
|
49
78
|
|
|
50
79
|
function showStatus(ctx) {
|
|
@@ -65,11 +94,13 @@ function showStatus(ctx) {
|
|
|
65
94
|
|
|
66
95
|
/** Legge solo i metadati (per l'outline e la riga di stato): niente vettori. */
|
|
67
96
|
async function refreshMeta() {
|
|
97
|
+
const st = state;
|
|
98
|
+
if (!st) return;
|
|
68
99
|
try {
|
|
69
|
-
const { index } = await loadIndex(
|
|
70
|
-
state.index = index;
|
|
100
|
+
const { index } = await loadIndex(st.repo);
|
|
101
|
+
if (state === st) st.index = index;
|
|
71
102
|
} catch {
|
|
72
|
-
state.index = null;
|
|
103
|
+
if (state === st) st.index = null;
|
|
73
104
|
}
|
|
74
105
|
}
|
|
75
106
|
|
|
@@ -90,18 +121,20 @@ function runIndex(ctx, { force = false } = {}) {
|
|
|
90
121
|
return inflight;
|
|
91
122
|
}
|
|
92
123
|
|
|
124
|
+
const signal = st.abort.signal;
|
|
93
125
|
const p = (async () => {
|
|
94
|
-
st.indexing = true;
|
|
95
|
-
st.rerun = false;
|
|
96
|
-
st.progress = null;
|
|
97
|
-
st.error = null;
|
|
98
|
-
showStatus(ctx);
|
|
99
126
|
try {
|
|
127
|
+
if (!await syncProjectState(ctx, { start: false }) || state !== st || signal.aborted) return null;
|
|
128
|
+
st.indexing = true;
|
|
129
|
+
st.rerun = false;
|
|
130
|
+
st.progress = null;
|
|
131
|
+
st.error = null;
|
|
132
|
+
showStatus(ctx);
|
|
100
133
|
const r = await indexRepo({
|
|
101
134
|
root: repo,
|
|
102
135
|
...MAX_CAPS,
|
|
103
136
|
force,
|
|
104
|
-
signal
|
|
137
|
+
signal,
|
|
105
138
|
log: () => {}, // niente rumore su stderr dentro la TUI
|
|
106
139
|
onProgress: (p) => {
|
|
107
140
|
if (state !== st) return; // sessione sostituita: non toccarne lo stato
|
|
@@ -109,17 +142,19 @@ function runIndex(ctx, { force = false } = {}) {
|
|
|
109
142
|
showStatus(ctx);
|
|
110
143
|
},
|
|
111
144
|
});
|
|
145
|
+
if (state !== st || !st.enabled || signal.aborted) return null;
|
|
112
146
|
st.index = r.index;
|
|
113
147
|
const runtime = embeddingStatus();
|
|
114
|
-
|
|
148
|
+
const warnings = [...runtime.warnings, ...runtime.attempts.filter((a) => !a.ok).map((a) => `${a.device}: ${a.error}`)];
|
|
149
|
+
if (state === st && st.enabled && !st.backendWarning && warnings.length) {
|
|
115
150
|
st.backendWarning = true;
|
|
116
|
-
ctx.ui.notify(`codeindex: selezionato ${formatProfile(runtime.profile)};
|
|
151
|
+
ctx.ui.notify(`codeindex: selezionato ${formatProfile(runtime.profile)}; ${warnings.join('; ')}`, 'warning');
|
|
117
152
|
}
|
|
118
153
|
st.graph = null; // il grafo è derivato dall'indice: va ricostruito
|
|
119
154
|
invalidateSearcher(repo);
|
|
120
155
|
return r;
|
|
121
156
|
} catch (e) {
|
|
122
|
-
if (
|
|
157
|
+
if (signal.aborted) return null;
|
|
123
158
|
st.error = e.message ?? String(e);
|
|
124
159
|
if (state === st && st.enabled && !/abort/i.test(st.error)) {
|
|
125
160
|
// Degradazione silenziosa: il tool resta disponibile e risponde con lo stato.
|
|
@@ -167,16 +202,22 @@ function stopBackground() {
|
|
|
167
202
|
}
|
|
168
203
|
|
|
169
204
|
/** Path toccati da watcher/hook: passa dal debounce e poi riconcilia. */
|
|
170
|
-
function markDirty(paths) {
|
|
205
|
+
function markDirty(paths, options) {
|
|
171
206
|
if (!state?.queue) return;
|
|
172
|
-
const rel = filterDirty(paths, state.repo);
|
|
207
|
+
const rel = filterDirty(paths, state.repo, options);
|
|
173
208
|
if (rel.size === 0) return;
|
|
174
209
|
state.queue.add(rel);
|
|
175
210
|
}
|
|
176
211
|
|
|
177
|
-
/**
|
|
178
|
-
function
|
|
212
|
+
/** Dopo bash: status Git, oppure riconciliazione incrementale senza Git. */
|
|
213
|
+
function sweepChanges() {
|
|
179
214
|
if (!state?.enabled || !state.repo) return;
|
|
215
|
+
if (state.mode === 'folder') {
|
|
216
|
+
// Non possiamo dedurre i path dal comando. La coda coalesca le richieste
|
|
217
|
+
// e indexRepo riusa i vettori invariati, anche senza watcher disponibile.
|
|
218
|
+
state.queue?.add(['.']);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
180
221
|
// Costa ~50–100 ms su un repo grande: il watcher è il segnale primario, questo
|
|
181
222
|
// è la rete di sicurezza. Un giro ogni 1,5 s basta.
|
|
182
223
|
const now = Date.now();
|
|
@@ -190,9 +231,20 @@ function sweepGitStatus() {
|
|
|
190
231
|
|
|
191
232
|
/** Watcher ricorsivo; se il sistema non lo consente si degrada senza watcher. */
|
|
192
233
|
function startWatcher(ctx) {
|
|
234
|
+
const st = state;
|
|
235
|
+
if (!st?.enabled) return;
|
|
193
236
|
try {
|
|
194
|
-
const watcher = fs.watch(
|
|
195
|
-
if (
|
|
237
|
+
const watcher = fs.watch(st.repo, { recursive: true }, (event, filename) => {
|
|
238
|
+
if (state !== st || !st.enabled) return;
|
|
239
|
+
if (!filename) {
|
|
240
|
+
if (st.mode === 'folder') st.queue?.add(['.']);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// Un rename può essere una directory intera: senza Git non esiste uno
|
|
244
|
+
// status successivo che recuperi i file aggiunti, spostati o cancellati.
|
|
245
|
+
markDirty([String(filename)], {
|
|
246
|
+
includeDirectories: st.mode === 'folder' && event === 'rename',
|
|
247
|
+
});
|
|
196
248
|
});
|
|
197
249
|
watcher.on('error', () => {
|
|
198
250
|
try {
|
|
@@ -200,12 +252,12 @@ function startWatcher(ctx) {
|
|
|
200
252
|
} catch {
|
|
201
253
|
/* già chiuso */
|
|
202
254
|
}
|
|
203
|
-
if (
|
|
255
|
+
if (st.watcher === watcher) st.watcher = null;
|
|
204
256
|
});
|
|
205
|
-
|
|
257
|
+
st.watcher = watcher;
|
|
206
258
|
} catch (e) {
|
|
207
|
-
|
|
208
|
-
ctx.ui.notify(`codeindex: watcher non disponibile (${e.message})`, 'warning');
|
|
259
|
+
st.watcher = null;
|
|
260
|
+
if (state === st) ctx.ui.notify(`codeindex: watcher non disponibile (${e.message})`, 'warning');
|
|
209
261
|
}
|
|
210
262
|
}
|
|
211
263
|
|
|
@@ -248,7 +300,7 @@ const searchCode = defineTool({
|
|
|
248
300
|
})),
|
|
249
301
|
}),
|
|
250
302
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
251
|
-
if (!state?.repo) return unavailable('
|
|
303
|
+
if (!state?.repo) return unavailable('nessuna cartella di progetto attiva');
|
|
252
304
|
if (!state.enabled) return unavailable('indice disattivato (/codeindex enable per riattivarlo)');
|
|
253
305
|
|
|
254
306
|
const k = Math.min(MAX_K, Math.max(1, Math.round(params.k ?? DEFAULT_K)));
|
|
@@ -312,23 +364,26 @@ async function annotateRead(event, ctx) {
|
|
|
312
364
|
}
|
|
313
365
|
|
|
314
366
|
function statusReport() {
|
|
315
|
-
if (!state) return 'codeindex:
|
|
367
|
+
if (!state) return 'codeindex: nessuna cartella di progetto attiva';
|
|
316
368
|
const i = state.index;
|
|
317
369
|
const lines = [
|
|
318
370
|
`repo : ${state.repo}`,
|
|
371
|
+
`modalità : ${state.mode === 'git' ? 'repository Git' : 'cartella (senza Git)'}`,
|
|
319
372
|
`stato : ${state.enabled ? 'attivo' : 'disattivato'}${state.indexing ? ' · indicizzazione in corso' : ''}`,
|
|
320
373
|
];
|
|
321
374
|
if (state.progress) lines.push(`progresso : ${state.progress.done}/${state.progress.total} chunk`);
|
|
322
375
|
if (i) {
|
|
323
376
|
lines.push(`file : ${i.stats?.files ?? '?'} (${fmtBytes(i.stats?.bytes ?? 0)})`);
|
|
324
377
|
lines.push(`chunk : ${i.chunks.length} · ${i.dims}d · ${i.model} (${i.dtype})`);
|
|
325
|
-
lines.push(`head : ${i.head ?? '?'}`);
|
|
378
|
+
if (state.mode === 'git') lines.push(`head : ${i.head ?? '?'}`);
|
|
326
379
|
lines.push(`aggiornato : ${i.updatedAt}`);
|
|
327
380
|
} else {
|
|
328
|
-
lines.push('indice : assente (usa /codeindex reindex)'
|
|
381
|
+
lines.push(state.enabled ? 'indice : assente (usa /codeindex reindex)'
|
|
382
|
+
: 'indice : non caricato (usa /codeindex enable)');
|
|
329
383
|
}
|
|
384
|
+
if (state.activationFile) lines.push(`attivazione progetto: ${state.activationFile}`);
|
|
330
385
|
const runtime = embeddingStatus();
|
|
331
|
-
lines.push(`
|
|
386
|
+
lines.push(`profilo client: ${runtime.profile ? formatProfile(runtime.profile) : 'non ancora associato al servizio'}`);
|
|
332
387
|
if (i?.device) lines.push(`profilo indice: ${formatProfile(i)}`);
|
|
333
388
|
if (state.error) lines.push(`ultimo errore: ${state.error}`);
|
|
334
389
|
return lines.join('\n');
|
|
@@ -338,7 +393,7 @@ export default function codeindexExtension(pi) {
|
|
|
338
393
|
pi.registerTool(searchCode);
|
|
339
394
|
|
|
340
395
|
pi.registerCommand('codeindex', {
|
|
341
|
-
description: 'Indice e embedding locali (status|reindex|enable|disable|config|configure|doctor)',
|
|
396
|
+
description: 'Indice e embedding locali (status|reindex|enable|disable|config|configure|doctor|service)',
|
|
342
397
|
handler: async (args, ctx) => {
|
|
343
398
|
const [sub = 'status', ...rest] = (args ?? '').trim().split(/\s+/).filter(Boolean);
|
|
344
399
|
// Configurazione/diagnostica funzionano anche fuori da git o con indice spento.
|
|
@@ -352,9 +407,13 @@ export default function codeindexExtension(pi) {
|
|
|
352
407
|
if (result) ctx.ui.notify(result, 'info');
|
|
353
408
|
return;
|
|
354
409
|
}
|
|
410
|
+
if (sub === 'service') {
|
|
411
|
+
ctx.ui.notify(await serviceCommand(rest), 'info');
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
355
414
|
if (sub === 'doctor') {
|
|
356
415
|
const options = parseDoctorArgs(rest);
|
|
357
|
-
if (options.probe) ctx.ui.notify('codeindex: prova isolata del modello (può
|
|
416
|
+
if (options.probe) ctx.ui.notify('codeindex: prova isolata del modello (può caricare una copia aggiuntiva, scaricare pesi; timeout 120 s per backend)…', 'info');
|
|
358
417
|
const report = await doctor(options);
|
|
359
418
|
ctx.ui.notify(options.json ? JSON.stringify(report) : formatDoctor(report), report.ok ? 'info' : 'warning');
|
|
360
419
|
return;
|
|
@@ -364,18 +423,27 @@ export default function codeindexExtension(pi) {
|
|
|
364
423
|
return;
|
|
365
424
|
}
|
|
366
425
|
if (!state) {
|
|
367
|
-
ctx.ui.notify('codeindex:
|
|
426
|
+
ctx.ui.notify('codeindex: nessuna cartella di progetto attiva', 'warning');
|
|
368
427
|
return;
|
|
369
428
|
}
|
|
370
429
|
if (sub === 'status') {
|
|
371
430
|
// La sessione può chiedere lo stato prima che il refresh in background
|
|
372
431
|
// abbia finito: meglio aspettare che mentire ("indice assente").
|
|
373
|
-
|
|
432
|
+
await syncProjectState(ctx);
|
|
433
|
+
if (state.enabled && !state.index) await refreshMeta();
|
|
374
434
|
showStatus(ctx);
|
|
375
|
-
|
|
435
|
+
let service;
|
|
436
|
+
try { service = formatServiceStatus(await getServiceStatus()); }
|
|
437
|
+
catch (e) { service = `servizio condiviso: ${e.message}`; }
|
|
438
|
+
ctx.ui.notify(`${statusReport()}\n${service}`, 'info');
|
|
376
439
|
return;
|
|
377
440
|
}
|
|
378
441
|
if (sub === 'reindex') {
|
|
442
|
+
await syncProjectState(ctx);
|
|
443
|
+
if (!state.enabled) {
|
|
444
|
+
ctx.ui.notify('codeindex: progetto disattivato; usa prima /codeindex enable', 'warning');
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
379
447
|
if (state.indexing) {
|
|
380
448
|
ctx.ui.notify('codeindex: indicizzazione già in corso', 'warning');
|
|
381
449
|
return;
|
|
@@ -385,41 +453,41 @@ export default function codeindexExtension(pi) {
|
|
|
385
453
|
return;
|
|
386
454
|
}
|
|
387
455
|
if (sub === 'enable' || sub === 'disable') {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// Un abort precedente resta sul vecchio controller: senza rinnovarlo
|
|
391
|
-
// ogni run futuro uscirebbe subito.
|
|
392
|
-
state.abort = new AbortController();
|
|
393
|
-
state.error = null;
|
|
394
|
-
if (!state.watcher) startWatcher(ctx);
|
|
395
|
-
} else {
|
|
396
|
-
stopBackground(); // watcher, coda, run in corso e rerun pendente
|
|
397
|
-
}
|
|
398
|
-
showStatus(ctx);
|
|
456
|
+
const st = state;
|
|
457
|
+
const enabled = sub === 'enable';
|
|
399
458
|
try {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
459
|
+
const saved = await writeProjectState(st.repo, enabled);
|
|
460
|
+
if (state !== st) return;
|
|
461
|
+
st.activationFile = saved.file;
|
|
462
|
+
st.activationError = null;
|
|
463
|
+
st.error = null;
|
|
464
|
+
st.manualConsent = enabled;
|
|
465
|
+
applyEnabled(ctx, enabled);
|
|
466
|
+
ctx.ui.notify(`codeindex: ${enabled ? 'attivo' : 'disattivato'} per questo progetto (${st.repo})`, 'info');
|
|
467
|
+
} catch (e) {
|
|
468
|
+
ctx.ui.notify(`codeindex: attivazione progetto non salvata (${e.message})`, 'error');
|
|
403
469
|
}
|
|
404
|
-
if (state.enabled) void runIndex(ctx);
|
|
405
|
-
ctx.ui.notify(`codeindex: ${state.enabled ? 'attivo' : 'disattivato'}`, 'info');
|
|
406
470
|
return;
|
|
407
471
|
}
|
|
408
|
-
ctx.ui.notify('uso: /codeindex status|reindex [--force]|enable|disable|config [show|set CHIAVE VALORE|unset CHIAVE]|configure|doctor [--probe] [--all] [--device DEVICE] [--webgpu-backend auto|vulkan]', 'warning');
|
|
472
|
+
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');
|
|
409
473
|
},
|
|
410
474
|
});
|
|
411
475
|
|
|
412
476
|
// Risorse di sessione: watcher e timer non vanno creati nel factory.
|
|
413
477
|
pi.on('session_start', async (_event, ctx) => {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
478
|
+
stopBackground();
|
|
479
|
+
invalidateSearcher();
|
|
480
|
+
const workspace = resolveWorkspace(ctx.cwd);
|
|
420
481
|
const st = {
|
|
421
|
-
repo,
|
|
422
|
-
|
|
482
|
+
repo: workspace.root,
|
|
483
|
+
mode: workspace.mode,
|
|
484
|
+
// Le cartelle protette richiedono consenso a ogni nuova sessione,
|
|
485
|
+
// anche se un enable era stato salvato nel branch in un altro progetto.
|
|
486
|
+
enabled: false,
|
|
487
|
+
autoIndex: workspace.autoIndex,
|
|
488
|
+
manualConsent: false,
|
|
489
|
+
activationFile: null,
|
|
490
|
+
activationError: null,
|
|
423
491
|
indexing: false,
|
|
424
492
|
progress: null,
|
|
425
493
|
index: null,
|
|
@@ -434,8 +502,14 @@ export default function codeindexExtension(pi) {
|
|
|
434
502
|
if (state === st && st.enabled) void runIndex(ctx);
|
|
435
503
|
}, { delayMs: DEBOUNCE_MS });
|
|
436
504
|
state = st;
|
|
505
|
+
await syncProjectState(ctx, { start: false });
|
|
506
|
+
if (state !== st) return;
|
|
437
507
|
showStatus(ctx);
|
|
438
|
-
|
|
508
|
+
if (!workspace.autoIndex && !st.enabled) {
|
|
509
|
+
ctx.ui.notify('codeindex: indicizzazione automatica disattivata nella home o nella radice del disco; usa una cartella di progetto o /codeindex enable', 'info');
|
|
510
|
+
}
|
|
511
|
+
if (!st.enabled) return;
|
|
512
|
+
// Solo i progetti abilitati caricano metadati, watcher e indice.
|
|
439
513
|
void (async () => {
|
|
440
514
|
await refreshMeta();
|
|
441
515
|
if (state !== st) return;
|
|
@@ -455,12 +529,16 @@ export default function codeindexExtension(pi) {
|
|
|
455
529
|
// Una riga stabile nel system prompt rende il tool scopribile; i conteggi
|
|
456
530
|
// esatti restano nel footer per non invalidare la prompt cache a ogni run.
|
|
457
531
|
pi.on('before_agent_start', async (event, ctx) => {
|
|
532
|
+
await syncProjectState(ctx);
|
|
458
533
|
showStatus(ctx);
|
|
459
534
|
if (!state?.enabled) return undefined; // spento: nessuna riga nel prompt
|
|
460
535
|
// Un checkout può cambiare HEAD senza eventi utili del watcher.
|
|
461
|
-
if (!state.indexing && state.index?.head && headCommit(state.repo) !== state.index.head) {
|
|
536
|
+
if (state.mode === 'git' && !state.indexing && state.index?.head && headCommit(state.repo) !== state.index.head) {
|
|
462
537
|
void runIndex(ctx);
|
|
463
538
|
}
|
|
539
|
+
// Senza watcher e senza Git, un nuovo prompt recupera anche le modifiche
|
|
540
|
+
// esterne a pi. Nessun polling o processo aggiuntivo.
|
|
541
|
+
if (state.mode === 'folder' && !state.watcher) state.queue?.add(['.']);
|
|
464
542
|
const line = promptLine(state?.index?.chunks?.length ?? 0);
|
|
465
543
|
if (!line) return undefined;
|
|
466
544
|
return { systemPrompt: `${event.systemPrompt}\n\n${line}` };
|
|
@@ -468,15 +546,20 @@ export default function codeindexExtension(pi) {
|
|
|
468
546
|
|
|
469
547
|
// Invalidazione diretta: `write`/`edit` sanno esattamente che path hanno toccato.
|
|
470
548
|
pi.on('tool_result', async (event, ctx) => {
|
|
471
|
-
|
|
549
|
+
await syncProjectState(ctx);
|
|
550
|
+
if (!state?.enabled) return undefined;
|
|
472
551
|
try {
|
|
552
|
+
// Anche un comando terminato con errore può aver modificato dei file.
|
|
553
|
+
if (event.toolName === 'bash') {
|
|
554
|
+
sweepChanges();
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
if (event.isError) return undefined;
|
|
473
558
|
if (event.toolName === 'read') return await annotateRead(event, ctx);
|
|
474
559
|
if (event.toolName === 'write' || event.toolName === 'edit') {
|
|
475
560
|
markDirty([String(event.input?.path ?? '')]);
|
|
476
561
|
return undefined;
|
|
477
562
|
}
|
|
478
|
-
// Dopo un bash il filesystem può essere cambiato in modo invisibile agli hook.
|
|
479
|
-
if (event.toolName === 'bash') sweepGitStatus();
|
|
480
563
|
} catch {
|
|
481
564
|
return undefined; // un hook non deve mai rompere un tool
|
|
482
565
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Solo nel servizio e nei probe esplicitamente isolati. I client pi non
|
|
2
|
+
// importano Transformers/ORT e non possiedono una pipeline nativa.
|
|
3
|
+
import { createEmbeddingRuntime, nativeBackends, validateEmbeddings, QUERY_PREFIX } from './backend.js';
|
|
4
|
+
import { ensureTextModel } from './model.js';
|
|
5
|
+
|
|
6
|
+
export function createLocalEmbedder(config) {
|
|
7
|
+
const runtime = createEmbeddingRuntime({
|
|
8
|
+
settings: () => ({ config }),
|
|
9
|
+
inventory: async () => nativeBackends().backends,
|
|
10
|
+
log: (s) => process.stderr.write(`${s}\n`),
|
|
11
|
+
initialize: async (profile, settings) => {
|
|
12
|
+
const sessionOptions = {};
|
|
13
|
+
if (profile.webgpuBackend === 'vulkan') {
|
|
14
|
+
const { registerVulkanBackend } = await import('./vulkan.js');
|
|
15
|
+
sessionOptions.executionProviders = [await registerVulkanBackend()];
|
|
16
|
+
}
|
|
17
|
+
const dir = await ensureTextModel(profile, settings);
|
|
18
|
+
const { pipeline } = await import('@huggingface/transformers');
|
|
19
|
+
if (settings.cpuThreads) sessionOptions.intraOpNumThreads = settings.cpuThreads;
|
|
20
|
+
if (profile.device === 'dml') {
|
|
21
|
+
sessionOptions.executionMode = 'sequential';
|
|
22
|
+
sessionOptions.enableMemPattern = false;
|
|
23
|
+
}
|
|
24
|
+
let pipe;
|
|
25
|
+
try {
|
|
26
|
+
pipe = await pipeline('feature-extraction', dir, {
|
|
27
|
+
dtype: profile.dtype, device: profile.device, local_files_only: true, session_options: sessionOptions,
|
|
28
|
+
});
|
|
29
|
+
const out = await pipe([QUERY_PREFIX + 'code retrieval smoke test'], { pooling: 'mean', normalize: true });
|
|
30
|
+
validateEmbeddings(out.tolist(), 1);
|
|
31
|
+
return pipe;
|
|
32
|
+
} catch (e) {
|
|
33
|
+
try { await pipe?.dispose(); } catch { /* conserva l'errore originale */ }
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
getProfile: async () => (await runtime.get()).profile,
|
|
40
|
+
info: () => runtime.info(),
|
|
41
|
+
embed: async (texts, { normalize = true } = {}) => {
|
|
42
|
+
const { pipe } = await runtime.get();
|
|
43
|
+
// Non cambiare provider dopo la prima inferenza riuscita.
|
|
44
|
+
const out = await pipe(texts, { pooling: 'mean', normalize });
|
|
45
|
+
return validateEmbeddings(out.tolist(), texts.length);
|
|
46
|
+
},
|
|
47
|
+
dispose: async () => {
|
|
48
|
+
if (runtime.info().profile) await (await runtime.get()).pipe.dispose();
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/probe.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
// Entry point interno, eseguito solo da doctor --probe in un processo isolato.
|
|
2
|
-
import {
|
|
2
|
+
import { cosine } from './embed.js';
|
|
3
|
+
import { loadConfig } from './config.js';
|
|
4
|
+
import { createLocalEmbedder } from './local-embed.js';
|
|
3
5
|
|
|
4
|
-
let result;
|
|
6
|
+
let result, runtime;
|
|
5
7
|
try {
|
|
6
8
|
const t0 = performance.now();
|
|
9
|
+
runtime = createLocalEmbedder((await loadConfig()).config);
|
|
7
10
|
const same = 'title: args.ts | text: export function parseArgs(argv) { return argv.slice(2); }';
|
|
8
11
|
const other = 'title: math.py | text: def factorial(n): return 1 if n < 2 else n * factorial(n - 1)';
|
|
9
|
-
const [a, b, c] = await embed([same, same, other]);
|
|
12
|
+
const [a, b, c] = await runtime.embed([same, same, other]);
|
|
10
13
|
const cosSame = cosine(a, b);
|
|
11
14
|
const cosOther = cosine(a, c);
|
|
12
15
|
if (cosSame < 0.999 || cosOther > 0.9999) throw new Error(`smoke test discriminazione fallito: stesso=${cosSame}, diverso=${cosOther}`);
|
|
13
|
-
result = { ok: true, profile: await
|
|
14
|
-
cosSame, cosOther, attempts:
|
|
15
|
-
await
|
|
16
|
+
result = { ok: true, profile: await runtime.getProfile(), elapsedMs: Math.round(performance.now() - t0),
|
|
17
|
+
cosSame, cosOther, attempts: runtime.info().attempts };
|
|
18
|
+
await runtime.dispose();
|
|
16
19
|
} catch (e) {
|
|
17
|
-
result = { ok: false, error: String(e.message ?? e), attempts:
|
|
20
|
+
result = { ok: false, error: String(e.message ?? e), attempts: runtime?.info().attempts ?? [] };
|
|
18
21
|
}
|
|
19
22
|
if (process.send) process.send(result, () => process.exit(result.ok ? 0 : 1));
|
|
20
23
|
else { console.log(JSON.stringify(result)); process.exitCode = result.ok ? 0 : 1; }
|