@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 ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ ## 1.1.1
4
+
5
+ - Porta loopback predefinita `43127`, sempre configurabile; l'override esplicito
6
+ `0` conserva la scelta deterministica precedente. Nessuna garanzia che una
7
+ porta fissa sia libera o non esclusa su ogni macchina.
8
+ - Attivazione dell'integrazione pi persistente **per progetto**, spenta in
9
+ assenza di consenso. `/codeindex enable|disable` salva la scelta fuori dai
10
+ sorgenti; progetti e sessioni non si abilitano globalmente a vicenda.
11
+ - Il servizio embedding resta condiviso. Disabilitare un progetto annulla
12
+ watcher/indicizzazione e libera le cache del client, senza arrestare il
13
+ modello eventualmente usato da altri progetti.
14
+ - Gli errori `listen EACCES` suggeriscono una porta alternativa configurabile,
15
+ senza fallback in-process. Nessuna correzione dichiarata per il timeout
16
+ intermittente segnalato su Windows: la causa resta da diagnosticare.
17
+ - Nessun caricamento automatico dell'indice o reindex a progetto spento.
18
+ I vecchi flag di sessione non vengono migrati in consenso per un progetto.
19
+ - Prima di cambiare porta, arrestare il servizio con la configurazione
20
+ precedente e riavviare i client. Anche il nuovo default richiede questa
21
+ precauzione se è ancora presente un servizio della versione precedente.
22
+
23
+ ## 1.1.0
24
+
25
+ ### Novità
26
+
27
+ - Modello di embedding in un servizio locale condiviso tra processi pi e CLI:
28
+ una sola pipeline per endpoint, avvio automatico, inferenze serializzate e
29
+ coda limitata. Nessun fallback in-process in caso di errore del servizio.
30
+ - Rilascio del modello dopo inattività (default 5 minuti), configurabile con
31
+ `serviceIdleMs` / `CODEINDEX_SERVICE_IDLE_MS`.
32
+ - `/codeindex service status` e `/codeindex service stop`, disponibili anche
33
+ dalla CLI. Autenticazione locale, verifica del profilo dopo riconnessione e
34
+ rifiuto dei conflitti di configurazione senza creare un secondo modello.
35
+ - Indicizzazione e sincronizzazione anche nelle cartelle senza repository Git.
36
+ Home e radice del disco richiedono consenso esplicito in ogni sessione.
37
+ - Diagnostica, configurazione e documentazione aggiornate; test di concorrenza,
38
+ crash, shutdown e prove opt-in con modello reale.
39
+
40
+ ### Aggiornamento
41
+
42
+ Chiudere e riaprire i vecchi processi pi dopo l'aggiornamento: le copie del
43
+ modello caricate dal vecchio runtime in-process non vengono liberate dal
44
+ nuovo servizio. Per cambiare il profilo di un servizio già attivo usare
45
+ `/codeindex service stop` quando inattivo, oppure attendere il timeout idle.
46
+ I probe espliciti di doctor restano isolati e possono caricare un modello
47
+ aggiuntivo. Endpoint diversi permettono deliberatamente istanze separate.
48
+
49
+ Il servizio è stato verificato su Linux x64 con CPU, CUDA, WebGPU automatico
50
+ e WebGPU/Vulkan; DirectML/Windows e CoreML/macOS restano da verificare sulle
51
+ rispettive piattaforme. Le misure storiche Fase 5–7 non sono nuovi benchmark
52
+ del servizio condiviso.
package/README.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # pi-codeindex
2
2
 
3
3
  Indice semantico locale del codice per l'agente pi: embedding con
4
- **EmbeddingGemma 2** (ONNX, in-process) e ricerca ibrida (coseno + lessico)
5
- che restituisce `path:righe` + simbolo + anteprima. Nessun server, nessun
6
- database: due file in `~/.cache/pi-codeindex/<sha1(repo)>/`.
4
+ **EmbeddingGemma 2** (ONNX, servizio locale condiviso) e ricerca ibrida
5
+ (coseno + lessico) che restituisce `path:righe` + simbolo + anteprima.
6
+ Una sola copia del modello fra processi pi/CLI sullo stesso endpoint;
7
+ nessun servizio remoto o database. Indice: due file in
8
+ `~/.cache/pi-codeindex/<sha1(repo)>/`.
9
+
10
+ **Default dalla versione 1.1.1:** porta locale `43127`; integrazione pi
11
+ spenta nei progetti mai abilitati. `/codeindex enable` salva il consenso
12
+ **per progetto**, mentre il modello resta condiviso fra i progetti attivi.
7
13
 
8
14
  ## Requisiti
9
15
 
@@ -18,7 +24,7 @@ database: due file in `~/.cache/pi-codeindex/<sha1(repo)>/`.
18
24
  ### Da npm, solo su richiesta
19
25
 
20
26
  ```bash
21
- pi -e npm:@alucard_24/pi-codeindex@1.0.0
27
+ pi -e npm:@alucard_24/pi-codeindex@1.1.1
22
28
  ```
23
29
 
24
30
  Carica il pacchetto per questa invocazione, senza aggiungerlo alle impostazioni
@@ -50,11 +56,12 @@ modificata dal pacchetto.
50
56
 
51
57
  ## Embedding e backend
52
58
 
53
- Il modello **EmbeddingGemma 2** gira localmente, in-process. I soli pesi testo
54
- vengono scaricati al primo utilizzo (anche dall'indicizzazione automatica) e
55
- riutilizzati fra repository. Nessun server o API di embedding; dopo il primo
56
- download è possibile lavorare offline. Il codice da embeddare non viene
57
- inviato al server dei pesi. Un processo pi mantiene una propria pipeline.
59
+ Il modello **EmbeddingGemma 2** gira in un servizio locale condiviso avviato
60
+ automaticamente al primo utilizzo, anche dall'indicizzazione automatica.
61
+ I client pi/CLI non caricano una pipeline propria. I soli pesi testo vengono
62
+ scaricati e riutilizzati fra repository; dopo il primo download è possibile
63
+ lavorare offline. Il codice da embeddare viaggia solo sul loopback locale,
64
+ non viene inviato al server dei pesi o a un'API di embedding remota.
58
65
 
59
66
  | `device` | Default `dtype: auto` | Stato |
60
67
  |---|---|---|
@@ -130,6 +137,72 @@ macchina Linux x64 / RTX 5070 Ti: EmbeddingGemma fp16, indice incrementale,
130
137
  ricerca su fixture e coesistenza con i provider standard. Queste prove non
131
138
  sostituiscono un benchmark di qualità o prestazioni su altri hardware.
132
139
 
140
+ ## Servizio condiviso e memoria
141
+
142
+ Il primo client sceglie il profilo secondo la configurazione; gli altri
143
+ riutilizzano la stessa pipeline se quel profilo è consentito. Un profilo
144
+ incompatibile, una cache pesi diversa o valori diversi di `cpuThreads` o
145
+ `serviceIdleMs` danno errore: **nessun secondo modello e nessun fallback
146
+ in-process**. Un device forzato può riutilizzare un servizio CPU solo con
147
+ `fallback: cpu`, con avviso esplicito. Le richieste sono serializzate, con
148
+ massimo 16 richieste attive/in coda; ogni batch contiene al massimo 128 testi
149
+ e 2 MiB di testo. La saturazione della coda produce un errore, non un altro
150
+ worker. Un crash interrompe la richiesta corrente; un uso successivo può
151
+ riavviare il servizio, verificando il profilo già fissato dal client.
152
+
153
+ Dopo **5 minuti senza operazioni sul modello** il processo termina e l'OS
154
+ libera RAM/VRAM native. `status` non rinnova il timer. Chiudere/disabilitare
155
+ una singola sessione non arresta un modello che può servire altri processi.
156
+ Per liberarlo subito, quando non ci sono inferenze in corso:
157
+
158
+ ```text
159
+ /codeindex service status
160
+ /codeindex service status --json
161
+ /codeindex service stop
162
+ ```
163
+
164
+ Gli stessi comandi funzionano come `node src/cli.js service status|stop`.
165
+ Stop viene rifiutato se ci sono richieste attive/in coda; non interrompe altri
166
+ client. Il servizio si riavvierà al prossimo utilizzo del modello. Nessun
167
+ avvio al login, servizio di sistema o modifica alle impostazioni globali di pi.
168
+
169
+ Il listener è solo `127.0.0.1`, sulla porta predefinita **`43127`**, con
170
+ autenticazione tramite token in `~/.cache/pi-codeindex/service/`
171
+ (directory 0700, record 0600 su POSIX). Il binding esclusivo della porta
172
+ impedisce due avvii simultanei sullo stesso endpoint. Nessuna porta è garantita
173
+ su tutte le macchine: se occupata o esclusa dal sistema, scegli una porta
174
+ libera con `/codeindex config set servicePort NUMERO` e riavvia pi.
175
+ Su Windows `netsh interface ipv4 show excludedportrange protocol=tcp` mostra
176
+ gli intervalli esclusi; non cancellare le prenotazioni del sistema.
177
+
178
+ Usa gli stessi `serviceDir`/`servicePort` in tutti i client per condividere il
179
+ modello. **Arresta il vecchio servizio inattivo prima di cambiare porta**
180
+ (`/codeindex service stop` con la configurazione precedente), poi chiudi e
181
+ riapri i client. Questo vale anche per il passaggio dalla porta automatica
182
+ della 1.1.0 al nuovo default: un vecchio record può riferirsi alla porta
183
+ precedente. Gli override già salvati non vengono sovrascritti; `servicePort: 0`
184
+ mantiene esplicitamente la scelta deterministica precedente.
185
+ Directory diverse e porte diverse permettono deliberatamente più modelli;
186
+ una stessa directory non va usata contemporaneamente con porte diverse.
187
+ Non condividere il token o la directory.
188
+
189
+ Restano per-processo le cache degli indici, dei vettori e dei grafi: il servizio
190
+ elimina le copie del **modello**, non tutta la memoria di pi. Dopo l'aggiornamento
191
+ **chiudi e riapri i vecchi processi pi**: i modelli già caricati dal vecchio
192
+ codice in-process non possono essere liberati dal nuovo servizio.
193
+
194
+ Verifica reale opt-in, con pesi già in cache (offline), due processi client,
195
+ indice/ricerca e uscita idle:
196
+
197
+ ```bash
198
+ CODEINDEX_TEST_SERVICE=1 CODEINDEX_DEVICE=cuda node --test test/service-integration.test.js
199
+ # Oppure cpu/q8, oppure WebGPU con CODEINDEX_WEBGPU_BACKEND=vulkan dopo setup-vulkan.
200
+ ```
201
+
202
+ Questi test non sono un nuovo benchmark Fase 5–7. La piattaforma iniziale di
203
+ verifica del servizio è Linux x64; Windows/DirectML e macOS/CoreML richiedono
204
+ verifica sulle rispettive macchine.
205
+
133
206
  ## Configurazione
134
207
 
135
208
  Dentro pi:
@@ -142,6 +215,8 @@ Dentro pi:
142
215
  /codeindex config set webgpuBackend vulkan solo WebGPU, setup-vulkan richiesto
143
216
  /codeindex config set fallback cpu
144
217
  /codeindex config unset device ripristina la scelta automatica
218
+ /codeindex service status processo condiviso, profilo e memoria
219
+ /codeindex service stop rilascia il modello quando inattivo
145
220
  /codeindex doctor inventario, senza inferenza/download
146
221
  /codeindex doctor --probe --device webgpu prova reale isolata (può scaricare i pesi)
147
222
  ```
@@ -175,9 +250,15 @@ non default silenziosi. Schema per editor: [config.schema.json](config.schema.js
175
250
  | `offline` | booleano; niente download, errore se mancano pesi | `CODEINDEX_OFFLINE=true\|false\|1\|0` |
176
251
  | `cacheDir` | cache pesi, default `~/.cache/pi-codeindex/text-model` | `CODEINDEX_MODEL_CACHE` |
177
252
  | `cpuThreads` | 0 = default ORT, oppure 1–256; non limita la GPU | `CODEINDEX_CPU_THREADS` |
253
+ | `serviceIdleMs` | default 300000 (5 min), da 1000 a 86400000 ms | `CODEINDEX_SERVICE_IDLE_MS` |
254
+ | `serviceDir` | directory privata condivisa, default `~/.cache/pi-codeindex/service` | `CODEINDEX_SERVICE_DIR` |
255
+ | `servicePort` | default **43127**; 1024–65535, oppure 0 per la scelta deterministica precedente; solo loopback | `CODEINDEX_SERVICE_PORT` |
178
256
 
179
257
  **Riavvia pi con un nuovo processo** dopo una modifica: non si cambia pipeline
180
- nel mezzo di un run. Il menu non riavvia né reindicizza da solo. Device, dtype
258
+ nel mezzo di un run. Se il servizio ha già un profilo/configurazione diversi,
259
+ usa anche `service stop` quando inattivo, oppure attendi l'uscita idle.
260
+ Prima di aggiornare a un protocollo incompatibile, arresta il vecchio servizio.
261
+ Il menu non riavvia né reindicizza da solo. Device, dtype
181
262
  e backend WebGPU effettivi sono salvati nell'indice; se diversi, la ricerca rifiuta di mescolare
182
263
  i vettori e chiede un re-index. Gli indici precedenti senza `device` vengono
183
264
  ricostruiti alla prossima indicizzazione.
@@ -198,9 +279,11 @@ CODEINDEX_DEVICE=cuda CODEINDEX_DTYPE=fp16 pi -e ./src/extension.js
198
279
  in subprocessi isolati con timeout di 120 s per backend; `--all` può scaricare
199
280
  più varianti. Ogni provider forzato viene provato senza fallback, per non
200
281
  attribuire alla GPU un successo ottenuto in CPU. Usa `offline: true` per provarle solo dalla cache. Un crash nativo
201
- di una prova non abbatte pi; questo isolamento riguarda `doctor`, non la normale
202
- pipeline in-process. Smoke del menu e dei comandi via RPC, senza chiamate al
203
- modello: `CODEINDEX_TEST_RPC=1 node --test test/config-rpc.test.js` (richiede
282
+ di una prova non abbatte pi. I probe restano esplicitamente isolati e **possono
283
+ caricare una copia aggiuntiva del modello** mentre il servizio è attivo: arresta
284
+ prima il servizio se vuoi evitare questo picco. Anche la pipeline ordinaria è
285
+ ora isolata, nel worker condiviso. Smoke del menu e dei comandi via RPC, senza
286
+ chiamate LLM e con cache modello temporanea/offline: `CODEINDEX_TEST_RPC=1 node --test test/config-rpc.test.js` (richiede
204
287
  `pi` sul PATH; usa un file config temporaneo, non modifica quello utente).
205
288
 
206
289
  ## Comandi
@@ -220,11 +303,43 @@ node src/cli.js embed-test
220
303
  mai dal repo.
221
304
  - `eval` misura recall@3/@8 e latenza sulle query della fixture.
222
305
 
306
+ ## Attivazione per progetto
307
+
308
+ Il caricamento dell'estensione non abilita automaticamente un progetto.
309
+ Prima del consenso non vengono avviati modello, download, indicizzazione,
310
+ watcher o caricamento automatico dell'indice. Configurazione, stato e
311
+ `doctor` restano disponibili; `doctor --probe` richiede esplicitamente il modello.
312
+
313
+ ```text
314
+ /codeindex enable # abilita solo il progetto corrente, anche per le sessioni future
315
+ /codeindex disable # disabilita solo quel progetto, senza fermare il modello condiviso
316
+ /codeindex status # mostra radice, stato e percorso del file di attivazione
317
+ ```
318
+
319
+ La scelta è salvata fuori dai sorgenti, in
320
+ `projects/<sha256(radice canonica)>.json` accanto al file restituito da
321
+ `/codeindex config path` (normalmente `~/.config/pi-codeindex/projects/`,
322
+ oppure `%APPDATA%\\pi-codeindex\\projects\\` su Windows).
323
+ Non viene aggiunto un flag globale di attivazione alla configurazione di pi
324
+ o al profilo embedding. Symlink e differenze di maiuscole su Windows non
325
+ creano consensi separati per lo stesso percorso canonico.
326
+
327
+ Nei repository Git la scelta vale per l'intera radice Git, anche dalle
328
+ sottocartelle. Senza Git vale per la cartella di avvio. Gli altri client dello
329
+ stesso progetto rileggono la scelta al prompt, alle richieste di stato/reindex
330
+ e prima di indicizzare; i progetti diversi mantengono il proprio stato.
331
+
332
+ I vecchi flag nel branch della sessione non abilitano implicitamente un
333
+ progetto: dopo l'aggiornamento usa `/codeindex enable` una volta per progetto.
334
+ Stato corrotto o illeggibile significa spento con avviso, non consenso implicito.
335
+ `/codeindex reindex` a progetto spento invita prima ad abilitarlo; i comandi CLI
336
+ espliciti di indice/ricerca non cambiano il consenso dell'integrazione pi.
337
+
223
338
  ## Uso dentro pi
224
339
 
225
340
  ```bash
226
341
  # pacchetto npm: solo questa invocazione, nessuna configurazione globale
227
- pi -e npm:@alucard_24/pi-codeindex@1.0.0
342
+ pi -e npm:@alucard_24/pi-codeindex@1.1.1
228
343
 
229
344
  # in sviluppo, dalla directory del progetto
230
345
  pi -e ./src/extension.js
@@ -247,18 +362,46 @@ Cosa aggiunge:
247
362
  durante l'indicizzazione).
248
363
  - **`/codeindex status|reindex [--force]|enable|disable|config|configure|doctor`**. `disable` spegne
249
364
  tutto: tool, outline, riga di stato, riga nel system prompt, watcher e
250
- indicizzazione in corso. Lo stato è persistito nel branch della sessione;
251
- una sessione nuova riparte attiva.
252
- - **indicizzazione automatica** all'avvio della sessione, in background,
365
+ indicizzazione in corso. Lo stato è persistito **per progetto**, non nel
366
+ branch della sessione e non globalmente. Disabilitare libera anche le cache
367
+ dell'indice e del grafo di quel client, senza fermare il servizio condiviso.
368
+ - **indicizzazione automatica solo nei progetti già abilitati**, in background,
253
369
  single-flight, interrompibile, con cap 5.000 file / 200 MB.
254
370
  - **sincronizzazione**: watcher ricorsivo con debounce 800 ms, invalidazione
255
- diretta da `write`/`edit` e sweep `git status` dopo `bash`. Una riconciliazione
256
- riusa i vettori dei file intatti (misurato: 1 chunk re-embedded su 3 dopo una
257
- modifica, 920 ms fino all'indice aggiornato).
258
-
259
- Fuori da un repo git l'estensione si disattiva da sola; se il modello non è
260
- scaricabile o il reindex fallisce, `search_code` risponde con lo stato reale e
261
- la sessione continua.
371
+ diretta da `write`/`edit` e controllo dopo `bash`: sweep `git status` nei
372
+ repository, riconciliazione incrementale nelle cartelle senza Git. Una
373
+ riconciliazione riusa i vettori dei file intatti (misurato nei repository:
374
+ 1 chunk re-embedded su 3 dopo una modifica, 920 ms fino all'indice aggiornato).
375
+
376
+ Se il modello non è scaricabile o il reindex fallisce, `search_code` risponde
377
+ con lo stato reale e la sessione continua.
378
+
379
+ ### Cartelle senza Git
380
+
381
+ Git è opzionale. Nei repository la radice resta quella restituita da Git,
382
+ anche avviando pi da una sottocartella. Altrimenti viene usata **esattamente
383
+ la directory da cui avvii pi**: non viene creato `.git`, non servono commit
384
+ né un remote. `/codeindex status` mostra radice e modalità.
385
+
386
+ In modalità cartella la scansione esclude le directory nascoste e le consuete
387
+ directory di dipendenze/output (`node_modules`, `dist`, `build`, `out`,
388
+ `vendor`, `target`, `coverage`, `venv`, `__pycache__`); non segue i symlink.
389
+ Restano i limiti di estensione, dimensione, 5.000 file e 200 MB. Le esclusioni
390
+ sono quelle fisse della scansione locale: **senza Git i file `.gitignore`
391
+ non vengono interpretati**. Nei repository continua a valere `git ls-files`
392
+ con le esclusioni Git.
393
+
394
+ Il watcher gestisce anche rename/cancellazioni di directory; dopo `bash`
395
+ viene accodata una riconciliazione senza ricalcolare i vettori invariati.
396
+ Se il watcher non è disponibile, la riconciliazione viene accodata anche
397
+ all'inizio di un nuovo prompt. `/codeindex reindex` resta disponibile per
398
+ aggiornare manualmente l'indice; `--force` ricalcola tutti i vettori.
399
+
400
+ Per evitare scansioni involontarie dell'intera macchina, nella **home** e
401
+ nella **radice del disco**, se non sono repository Git, l'indice parte spento.
402
+ Avvia pi dalla cartella del progetto oppure abilitalo esplicitamente con
403
+ `/codeindex enable`; nelle due cartelle protette il consenso va ripetuto a
404
+ ogni avvio della sessione. Nessuna nuova configurazione globale è necessaria.
262
405
 
263
406
  ## Cosa indicizza
264
407
 
@@ -267,7 +410,10 @@ Markdown. Chunk declaration-aware, con fusione dei chunk piccoli (Markdown fino
267
410
  a 80 righe; codice solo micro-chunk ≤6 righe). Esclusi: binari, file >1 MB,
268
411
  minified, lockfile, `.pbl`/`.pbt`/`.pbw`.
269
412
 
270
- ## Numeri misurati (RTX 5070 Ti, fp16)
413
+ ## Numeri misurati (runtime originale in-process, RTX 5070 Ti, fp16)
414
+
415
+ Queste misure precedono il servizio condiviso e non ne misurano memoria,
416
+ latenza IPC o prestazioni con client concorrenti.
271
417
 
272
418
  | Repo | File | Chunk | Primo indice | Re-index |
273
419
  |---|---|---|---|---|
@@ -11,6 +11,9 @@
11
11
  "fallback": { "enum": ["error", "cpu"], "default": "error", "description": "Fallback consentito per un device forzato; auto include sempre CPU." },
12
12
  "offline": { "type": "boolean", "default": false },
13
13
  "cacheDir": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Cache dei pesi. Supporta ~/; i percorsi relativi sono risolti dal cwd." },
14
- "cpuThreads": { "type": "integer", "minimum": 0, "maximum": 256, "default": 0, "description": "0 usa il default di ONNX Runtime; non limita la GPU." }
14
+ "cpuThreads": { "type": "integer", "minimum": 0, "maximum": 256, "default": 0, "description": "0 usa il default di ONNX Runtime; non limita la GPU." },
15
+ "serviceDir": { "type": "string", "minLength": 1, "pattern": "\\S", "description": "Directory privata del servizio condiviso. Default ~/.cache/pi-codeindex/service; non cambiarla fra sessioni se vuoi condividere il modello." },
16
+ "servicePort": { "type": "integer", "anyOf": [{ "const": 0 }, { "minimum": 1024, "maximum": 65535 }], "default": 43127, "description": "Porta TCP loopback predefinita 43127, da cambiare se occupata o esclusa dal sistema. 0 mantiene la scelta deterministica precedente. Arrestare il vecchio servizio prima di cambiare porta nella stessa directory." },
17
+ "serviceIdleMs": { "type": "integer", "minimum": 1000, "maximum": 86400000, "default": 300000, "description": "Inattività prima di terminare il processo e liberare il modello (default 5 minuti). Status non rinnova il timer." }
15
18
  }
16
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alucard_24/pi-codeindex",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Indice semantico locale del codice per pi (EmbeddingGemma 2, ONNX)",
5
5
  "license": "MIT",
6
6
  "author": "Alucard24",
@@ -9,7 +9,7 @@
9
9
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
10
10
  "main": "./src/extension.js",
11
11
  "bin": { "pi-codeindex": "src/cli.js" },
12
- "files": ["src", "scripts/setup-gpu.sh", "scripts/setup-vulkan.mjs", "scripts/vulkan-sources.json", "eval/queries.jsonl", "config.schema.json", "README.md", "LICENSE"],
12
+ "files": ["src", "scripts/setup-gpu.sh", "scripts/setup-vulkan.mjs", "scripts/vulkan-sources.json", "eval/queries.jsonl", "config.schema.json", "README.md", "CHANGELOG.md", "LICENSE"],
13
13
  "engines": { "node": ">=22" },
14
14
  "scripts": {
15
15
  "test": "node --test 'test/*.test.js'",
package/src/cli.js CHANGED
@@ -5,7 +5,7 @@
5
5
  import fs from 'node:fs';
6
6
  import { spawnSync } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { embed, embedQuery, embedDocs, cosine, MODEL_ID, getEmbeddingProfile, modelDir } from './embed.js';
8
+ import { embed, embedQuery, embedDocs, cosine, MODEL_ID, getEmbeddingProfile, modelDir, getServiceStatus, serviceCommand, embeddingStatus } from './embed.js';
9
9
  import { configCommand } from './config.js';
10
10
  import { formatProfile } from './backend.js';
11
11
  import { doctor, formatDoctor, parseDoctorArgs } from './doctor.js';
@@ -44,7 +44,9 @@ async function embedTest() {
44
44
  console.log('cos(same) :', cosine(sameA, sameB).toFixed(6), '(atteso ~1.0)');
45
45
  console.log('cos(diverse) :', cosine(sameA, other).toFixed(6), '(atteso < 0.95)');
46
46
  console.log('latency cold :', tCold, 'ms per 3 testi');
47
- console.log('rss :', rssMb(), 'MB');
47
+ console.log('rss client :', rssMb(), 'MiB');
48
+ const service = await getServiceStatus();
49
+ console.log('servizio :', service.pid, '· RSS', Math.round(service.rssBytes / 1024 / 1024), 'MiB');
48
50
 
49
51
  const batch = Array.from({ length: 16 }, (_, i) => `${SAME} // variante ${i}`);
50
52
  const t1 = performance.now();
@@ -59,7 +61,7 @@ async function embedTest() {
59
61
  ]);
60
62
  console.log('query->args :', cosine(q[0], docs[0]).toFixed(4));
61
63
  console.log('query->idle :', cosine(q[0], docs[1]).toFixed(4));
62
- console.log('rss finale :', rssMb(), 'MB');
64
+ console.log('rss client finale:', rssMb(), 'MiB');
63
65
 
64
66
  const dump = arg('dump');
65
67
  if (dump && dump !== true) {
@@ -78,6 +80,7 @@ async function cmdIndex() {
78
80
  const r = await indexRepo({ root, maxFiles, maxBytes, batchSize, force });
79
81
  const out = {
80
82
  repo: r.repo,
83
+ profile: { model: r.index.model, dims: r.index.dims, device: r.index.device, dtype: r.index.dtype, webgpuBackend: r.index.webgpuBackend },
81
84
  files: r.index.stats.files,
82
85
  chunks: r.total,
83
86
  embedded: r.embedded,
@@ -91,6 +94,7 @@ async function cmdIndex() {
91
94
  console.log(JSON.stringify(out));
92
95
  } else {
93
96
  console.log(`repo : ${r.repo}`);
97
+ console.log(`runtime : ${formatProfile(r.index)} · servizio condiviso`);
94
98
  console.log(`file : ${out.files} (${fmtBytes(out.bytes)}) · invariati ${out.unchangedFiles}`);
95
99
  console.log(`chunk : ${out.chunks} · riusati ${out.reused} · embeddati ${out.embedded}`);
96
100
  console.log(`esclusi : estensione ${out.skip.ext}, dimensione ${out.skip.big}, binari ${out.skip.binary}, minified ${out.skip.minified}, cap ${out.skip.cap}`);
@@ -164,6 +168,7 @@ async function cmdEval() {
164
168
  const cmd = process.argv[2];
165
169
  try {
166
170
  if (cmd === 'config') console.log(await configCommand(process.argv.slice(3)));
171
+ else if (cmd === 'service') console.log(await serviceCommand(process.argv.slice(3)));
167
172
  else if (cmd === 'setup-vulkan') {
168
173
  const { setupVulkan } = await import('../scripts/setup-vulkan.mjs');
169
174
  await setupVulkan(process.argv.slice(3));
@@ -186,9 +191,12 @@ try {
186
191
  else if (cmd === 'search') await cmdSearch();
187
192
  else if (cmd === 'eval') await cmdEval();
188
193
  else {
189
- console.error('comandi: embed-test | index | status | search | eval | setup-gpu | setup-vulkan | config [show|path|set CHIAVE VALORE|unset CHIAVE] | doctor [--probe] [--all] [--device DEVICE] [--dtype DTYPE] [--webgpu-backend auto|vulkan] [--json]');
194
+ console.error('comandi: embed-test | index | status | search | eval | setup-gpu | setup-vulkan | service [status [--json]|stop] | config [show|path|set CHIAVE VALORE|unset CHIAVE] | doctor [--probe] [--all] [--device DEVICE] [--dtype DTYPE] [--webgpu-backend auto|vulkan] [--json]');
190
195
  process.exit(1);
191
196
  }
197
+ const runtime = embeddingStatus();
198
+ const warnings = [...runtime.warnings, ...runtime.attempts.filter((a) => !a.ok).map((a) => `${a.device}: ${a.error}`)];
199
+ if (runtime.profile && warnings.length) console.error(`codeindex: selezionato ${formatProfile(runtime.profile)}; ${warnings.join('; ')}`);
192
200
  } catch (e) {
193
201
  console.error(`errore: ${e.message}`);
194
202
  if (process.env.CODEINDEX_DEBUG) console.error(e.stack);
package/src/config-ui.js CHANGED
@@ -14,10 +14,10 @@ export async function configure(ctx, options = {}) {
14
14
  if (!selected || selected === 'Annulla') return null;
15
15
  if (selected === 'Salva') {
16
16
  if (!Object.keys(patch).length) return null;
17
- if (!(await ctx.ui.confirm('Salvare la configurazione?', `${loaded.file}\n${JSON.stringify(patch, null, 2)}\nRiavvia pi dopo il salvataggio. Cambiare device/dtype/backend WebGPU richiede un re-index.`))) return null;
17
+ if (!(await ctx.ui.confirm('Salvare la configurazione?', `${loaded.file}\n${JSON.stringify(patch, null, 2)}\nRiavvia pi dopo il salvataggio. Per cambiare un servizio già attivo usa /codeindex service stop quando inattivo. Cambiare device/dtype/backend WebGPU richiede un re-index.`))) return null;
18
18
  const file = await updateConfig(patch, options);
19
19
  const overrides = Object.keys(patch).filter((key) => loaded.sources[key] === ENV_KEYS[key]);
20
- return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare.` +
20
+ return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare; per cambiare un servizio già attivo usa /codeindex service stop quando inattivo.` +
21
21
  (overrides.length ? `\nAttenzione: override d'ambiente attivi per ${overrides.join(', ')}.` : '') +
22
22
  `\n${formatConfig(await loadConfig(options))}`;
23
23
  }
@@ -25,9 +25,11 @@ export async function configure(ctx, options = {}) {
25
25
  if (!key) continue;
26
26
  const choices = key === 'device' ? DEVICES : key === 'dtype' ? DTYPES
27
27
  : key === 'webgpuBackend' ? WEBGPU_BACKENDS : key === 'fallback' ? ['error', 'cpu'] : key === 'offline' ? ['false', 'true'] : null;
28
- const hint = key === 'webgpuBackend' ? ' — solo WebGPU; vulkan richiede setup-vulkan' : '';
28
+ const hint = key === 'webgpuBackend' ? ' — solo WebGPU; vulkan richiede setup-vulkan'
29
+ : key === 'serviceIdleMs' ? ' — millisecondi; 300000 = 5 minuti'
30
+ : key === 'serviceDir' || key === 'servicePort' ? ' — lascia uguale fra sessioni per condividere un solo modello' : '';
29
31
  const text = choices ? await ctx.ui.select(`${key}${hint} (attuale: ${draft[key]})`, choices)
30
- : await ctx.ui.input(`${key} (attuale: ${draft[key]})`, String(draft[key]));
32
+ : await ctx.ui.input(`${key}${hint} (attuale: ${draft[key]})`, String(draft[key]));
31
33
  if (text === undefined) continue;
32
34
  try { draft[key] = patch[key] = parseSetting(key, text); }
33
35
  catch (e) { ctx.ui.notify(e.message, 'error'); }
package/src/config.js CHANGED
@@ -12,6 +12,7 @@ export const ENV_KEYS = {
12
12
  device: 'CODEINDEX_DEVICE', dtype: 'CODEINDEX_DTYPE', fallback: 'CODEINDEX_FALLBACK',
13
13
  offline: 'CODEINDEX_OFFLINE', cacheDir: 'CODEINDEX_MODEL_CACHE', cpuThreads: 'CODEINDEX_CPU_THREADS',
14
14
  webgpuBackend: 'CODEINDEX_WEBGPU_BACKEND',
15
+ serviceDir: 'CODEINDEX_SERVICE_DIR', servicePort: 'CODEINDEX_SERVICE_PORT', serviceIdleMs: 'CODEINDEX_SERVICE_IDLE_MS',
15
16
  };
16
17
 
17
18
  export function configPath(env = process.env, home = os.homedir()) {
@@ -26,7 +27,8 @@ function expandPath(value, home = os.homedir()) {
26
27
 
27
28
  export function defaults(home = os.homedir()) {
28
29
  return { device: 'auto', dtype: 'auto', webgpuBackend: 'auto', fallback: 'error', offline: false,
29
- cacheDir: path.join(home, '.cache', 'pi-codeindex', 'text-model'), cpuThreads: 0 };
30
+ cacheDir: path.join(home, '.cache', 'pi-codeindex', 'text-model'), cpuThreads: 0,
31
+ serviceDir: path.join(home, '.cache', 'pi-codeindex', 'service'), servicePort: 43127, serviceIdleMs: 300_000 };
30
32
  }
31
33
 
32
34
  export function validateConfig(value, source = 'config') {
@@ -44,8 +46,10 @@ export function validateConfig(value, source = 'config') {
44
46
  : key === 'webgpuBackend' ? WEBGPU_BACKENDS : key === 'fallback' ? ['error', 'cpu'] : null;
45
47
  if (choices && !choices.includes(v)) throw new Error(`${source}: ${key} deve essere uno di: ${choices.join(', ')}`);
46
48
  if (key === 'offline' && typeof v !== 'boolean') throw new Error(`${source}: offline deve essere booleano`);
47
- if (key === 'cacheDir' && (typeof v !== 'string' || !v.trim())) throw new Error(`${source}: cacheDir deve essere un percorso non vuoto`);
49
+ if (['cacheDir', 'serviceDir'].includes(key) && (typeof v !== 'string' || !v.trim())) throw new Error(`${source}: ${key} deve essere un percorso non vuoto`);
48
50
  if (key === 'cpuThreads' && (!Number.isInteger(v) || v < 0 || v > 256)) throw new Error(`${source}: cpuThreads deve essere un intero tra 0 e 256`);
51
+ if (key === 'servicePort' && (!Number.isInteger(v) || (v !== 0 && (v < 1024 || v > 65535)))) throw new Error(`${source}: servicePort deve essere 0 (automatico) oppure un intero tra 1024 e 65535`);
52
+ if (key === 'serviceIdleMs' && (!Number.isInteger(v) || v < 1000 || v > 86_400_000)) throw new Error(`${source}: serviceIdleMs deve essere un intero tra 1000 e 86400000`);
49
53
  }
50
54
  return value;
51
55
  }
@@ -56,8 +60,8 @@ export function parseSetting(key, text) {
56
60
  if (key === 'offline') {
57
61
  if (!['true', 'false', '1', '0'].includes(text)) throw new Error('offline: usa true oppure false');
58
62
  value = text === 'true' || text === '1';
59
- } else if (key === 'cpuThreads') {
60
- if (!/^\d+$/.test(text)) throw new Error('cpuThreads: atteso un intero tra 0 e 256');
63
+ } else if (['cpuThreads', 'servicePort', 'serviceIdleMs'].includes(key)) {
64
+ if (!/^\d+$/.test(text)) throw new Error(`${key}: atteso un intero`);
61
65
  value = Number(text);
62
66
  }
63
67
  validateConfig({ [key]: value });
@@ -86,6 +90,7 @@ export async function loadConfig({ env = process.env, home = os.homedir() } = {}
86
90
  }
87
91
  validateConfig(config, file);
88
92
  config.cacheDir = expandPath(config.cacheDir, home);
93
+ config.serviceDir = expandPath(config.serviceDir, home);
89
94
  return { config: Object.freeze(config), sources, file };
90
95
  }
91
96
 
@@ -122,5 +127,5 @@ export async function configCommand(args, options = {}) {
122
127
  if (action === 'set' && key && rest.length) file = await updateConfig({ [key]: parseSetting(key, rest.join(' ')) }, options);
123
128
  else if (action === 'unset' && key && !rest.length) file = await updateConfig({}, { ...options, unset: [key] });
124
129
  else throw new Error('uso: config [show|path|set CHIAVE VALORE|unset CHIAVE]');
125
- return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare. Gli override CODEINDEX_* hanno precedenza.\n${formatConfig(await loadConfig(options))}`;
130
+ return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare. Per cambiare un profilo già attivo usa anche /codeindex service stop quando il servizio è inattivo. Gli override CODEINDEX_* hanno precedenza.\n${formatConfig(await loadConfig(options))}`;
126
131
  }
package/src/doctor.js CHANGED
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { loadConfig, parseSetting } from './config.js';
6
6
  import { nativeBackends, formatProfile } from './backend.js';
7
7
  import { vulkanStatus } from './vulkan.js';
8
+ import { createEmbeddingClient } from './service-client.js';
8
9
 
9
10
  export function parseDoctorArgs(args) {
10
11
  const options = { probe: false, all: false, json: false };
@@ -63,7 +64,9 @@ export async function doctor({ probe = false, all = false, device, dtype, webgpu
63
64
  }
64
65
  const settings = await loadConfig({ env });
65
66
  const native = nativeBackends();
66
- const report = { ok: true, platform: process.platform, arch: process.arch, node: process.version,
67
+ const service = await createEmbeddingClient({ settings: () => settings, env }).status()
68
+ .catch((e) => ({ running: false, error: e.message }));
69
+ const report = { ok: !service.error, service, platform: process.platform, arch: process.arch, node: process.version,
67
70
  runtime: native.version, settings,
68
71
  requested: { device: device ?? settings.config.device, dtype: dtype ?? settings.config.dtype,
69
72
  webgpuBackend: webgpuBackend ?? settings.config.webgpuBackend },
@@ -73,7 +76,7 @@ export async function doctor({ probe = false, all = false, device, dtype, webgpu
73
76
  for (const selected of devices) {
74
77
  report.probes.push(await probeBackend({ device: selected, dtype, webgpuBackend, timeoutMs, env }));
75
78
  }
76
- report.ok = report.probes.length > 0 && report.probes.every((p) => p.ok);
79
+ report.ok = !service.error && report.probes.length > 0 && report.probes.every((p) => p.ok);
77
80
  }
78
81
  return report;
79
82
  }
@@ -85,12 +88,14 @@ export function formatDoctor(report) {
85
88
  ...report.backends.map((b) => `${b.device.padEnd(8)}: ${b.available ? 'presente' : 'assente'} — ${b.reason}`),
86
89
  `backend WebGPU richiesto: ${(report.requested ?? report.settings.config).webgpuBackend} (solo quando il provider selezionato è WebGPU)`,
87
90
  `Vulkan forzato: ${report.vulkan?.available ? 'presente' : 'assente'} — ${report.vulkan?.reason ?? 'non verificato'}`];
91
+ lines.push(`servizio condiviso: ${report.service?.error ?? (report.service?.running ? `PID ${report.service.pid} · ${report.service.pending} richieste · ${Math.round(report.service.rssBytes / 1024 / 1024)} MiB RSS` : report.service?.stopping ? 'in arresto' : 'fermo (nessun avvio da doctor)')}`);
88
92
  if (!report.probes.length) lines.push('Nessuna inferenza eseguita. Usa doctor --probe (può scaricare i pesi), oppure --probe --all.');
89
93
  for (const p of report.probes) {
90
94
  lines.push(p.ok ? `PASS ${p.requested}: ${formatProfile(p.profile)} · ${p.profile.dims}d · ${p.elapsedMs} ms · cos(stesso) ${p.cosSame.toFixed(6)}`
91
95
  : `FAIL ${p.requested}: ${p.error}`);
92
96
  for (const a of p.attempts ?? []) if (!a.ok) lines.push(` ${formatProfile(a)}: ${a.error}`);
93
97
  }
98
+ if (report.probes.length) lines.push('Probe isolati: possono caricare una copia aggiuntiva del modello mentre il servizio condiviso è attivo.');
94
99
  lines.push('Un provider presente non prova compatibilità/velocità del modello; ORT può eseguire alcuni operatori su CPU.');
95
100
  return lines.join('\n');
96
101
  }