@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/src/embed.js CHANGED
@@ -1,88 +1,57 @@
1
- // Embedding locale in-process. Selezione del provider verificata con il
2
- // modello reale; il profilo resta fisso per tutta la vita del processo.
3
- import { loadConfig } from './config.js';
4
- import { createEmbeddingRuntime, nativeBackends, validateEmbeddings, MODEL_ID, DIMS, QUERY_PREFIX, docPrefix } from './backend.js';
5
- import { ensureTextModel, modelPath } from './model.js';
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 runtime = createEmbeddingRuntime({
10
- settings: () => loadConfig(),
11
- inventory: async () => nativeBackends().backends,
12
- log: (s) => process.stderr.write(`${s}\n`),
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, { normalize = true } = {}) {
16
+ export async function embed(texts, options = {}) {
59
17
  if (!Array.isArray(texts) || texts.length === 0) return [];
60
- const { pipe } = await runtime.get();
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 { profile, config } = await runtime.get();
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, repoRoot } from './util.js';
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 readEnabled(ctx) {
37
- try {
38
- for (const entry of ctx.sessionManager.getBranch()) {
39
- if (entry.type === 'custom' && entry.customType === CONFIG_ENTRY) {
40
- const data = entry.data ?? {};
41
- if (typeof data.enabled === 'boolean') return data.enabled;
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
- } catch {
45
- /* nessuno stato salvato: default attivo */
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(state.repo);
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: st.abort.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
- if (state === st && st.enabled && !st.backendWarning && runtime.attempts.some((a) => !a.ok)) {
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)}; tentativi falliti: ${runtime.attempts.filter((a) => !a.ok).map((a) => `${a.device}: ${a.error}`).join('; ')}`, 'warning');
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 (st.abort.signal.aborted) return null;
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
- /** `git status --porcelain -z` dopo un bash: cattura le modifiche indirette. */
178
- function sweepGitStatus() {
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(state.repo, { recursive: true }, (_event, filename) => {
195
- if (filename) markDirty([String(filename)]);
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 (state) state.watcher = null;
255
+ if (st.watcher === watcher) st.watcher = null;
204
256
  });
205
- state.watcher = watcher;
257
+ st.watcher = watcher;
206
258
  } catch (e) {
207
- state.watcher = null;
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('la directory corrente non è un repository git');
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: nessun repository git nella directory corrente';
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(`runtime : ${runtime.profile ? formatProfile(runtime.profile) : 'non caricato'}`);
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ò scaricare i pesi, timeout 120 s per backend)…', 'info');
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: nessun repository git nella directory corrente', 'warning');
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
- if (!state.index) await refreshMeta();
432
+ await syncProjectState(ctx);
433
+ if (state.enabled && !state.index) await refreshMeta();
374
434
  showStatus(ctx);
375
- ctx.ui.notify(statusReport(), 'info');
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
- state.enabled = sub === 'enable';
389
- if (state.enabled) {
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
- pi.appendEntry(CONFIG_ENTRY, { enabled: state.enabled });
401
- } catch {
402
- /* stato non persistito: vale per questa sessione */
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
- const repo = repoRoot(ctx.cwd);
415
- if (!repo) {
416
- state = null;
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
- enabled: readEnabled(ctx),
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
- // Non bloccare l'avvio: metadati, watcher e indice corrono in background.
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
- if (event.isError || !state?.enabled) return undefined;
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 { embed, cosine, getEmbeddingProfile, embeddingStatus, getEmbedder } from './embed.js';
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 getEmbeddingProfile(), elapsedMs: Math.round(performance.now() - t0),
14
- cosSame, cosOther, attempts: embeddingStatus().attempts };
15
- await (await getEmbedder()).dispose();
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: embeddingStatus().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; }