@alucard_24/pi-codeindex 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +117 -20
- 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 +76 -39
- package/src/local-embed.js +51 -0
- package/src/probe.js +10 -7
- package/src/service-client.js +126 -0
- package/src/service-policy.js +33 -0
- package/src/service-protocol.js +122 -0
- package/src/service-worker.js +3 -0
- package/src/service.js +172 -0
- package/src/sync.js +9 -5
- package/src/workspace.js +18 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Novità
|
|
6
|
+
|
|
7
|
+
- Modello di embedding in un servizio locale condiviso tra processi pi e CLI:
|
|
8
|
+
una sola pipeline per endpoint, avvio automatico, inferenze serializzate e
|
|
9
|
+
coda limitata. Nessun fallback in-process in caso di errore del servizio.
|
|
10
|
+
- Rilascio del modello dopo inattività (default 5 minuti), configurabile con
|
|
11
|
+
`serviceIdleMs` / `CODEINDEX_SERVICE_IDLE_MS`.
|
|
12
|
+
- `/codeindex service status` e `/codeindex service stop`, disponibili anche
|
|
13
|
+
dalla CLI. Autenticazione locale, verifica del profilo dopo riconnessione e
|
|
14
|
+
rifiuto dei conflitti di configurazione senza creare un secondo modello.
|
|
15
|
+
- Indicizzazione e sincronizzazione anche nelle cartelle senza repository Git.
|
|
16
|
+
Home e radice del disco richiedono consenso esplicito in ogni sessione.
|
|
17
|
+
- Diagnostica, configurazione e documentazione aggiornate; test di concorrenza,
|
|
18
|
+
crash, shutdown e prove opt-in con modello reale.
|
|
19
|
+
|
|
20
|
+
### Aggiornamento
|
|
21
|
+
|
|
22
|
+
Chiudere e riaprire i vecchi processi pi dopo l'aggiornamento: le copie del
|
|
23
|
+
modello caricate dal vecchio runtime in-process non vengono liberate dal
|
|
24
|
+
nuovo servizio. Per cambiare il profilo di un servizio già attivo usare
|
|
25
|
+
`/codeindex service stop` quando inattivo, oppure attendere il timeout idle.
|
|
26
|
+
I probe espliciti di doctor restano isolati e possono caricare un modello
|
|
27
|
+
aggiuntivo. Endpoint diversi permettono deliberatamente istanze separate.
|
|
28
|
+
|
|
29
|
+
Il servizio è stato verificato su Linux x64 con CPU, CUDA, WebGPU automatico
|
|
30
|
+
e WebGPU/Vulkan; DirectML/Windows e CoreML/macOS restano da verificare sulle
|
|
31
|
+
rispettive piattaforme. Le misure storiche Fase 5–7 non sono nuovi benchmark
|
|
32
|
+
del servizio condiviso.
|
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# pi-codeindex
|
|
2
2
|
|
|
3
3
|
Indice semantico locale del codice per l'agente pi: embedding con
|
|
4
|
-
**EmbeddingGemma 2** (ONNX,
|
|
5
|
-
che restituisce `path:righe` + simbolo + anteprima.
|
|
6
|
-
|
|
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)>/`.
|
|
7
9
|
|
|
8
10
|
## Requisiti
|
|
9
11
|
|
|
@@ -50,11 +52,12 @@ modificata dal pacchetto.
|
|
|
50
52
|
|
|
51
53
|
## Embedding e backend
|
|
52
54
|
|
|
53
|
-
Il modello **EmbeddingGemma 2** gira
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
Il modello **EmbeddingGemma 2** gira in un servizio locale condiviso avviato
|
|
56
|
+
automaticamente al primo utilizzo, anche dall'indicizzazione automatica.
|
|
57
|
+
I client pi/CLI non caricano una pipeline propria. I soli pesi testo vengono
|
|
58
|
+
scaricati e riutilizzati fra repository; dopo il primo download è possibile
|
|
59
|
+
lavorare offline. Il codice da embeddare viaggia solo sul loopback locale,
|
|
60
|
+
non viene inviato al server dei pesi o a un'API di embedding remota.
|
|
58
61
|
|
|
59
62
|
| `device` | Default `dtype: auto` | Stato |
|
|
60
63
|
|---|---|---|
|
|
@@ -130,6 +133,60 @@ macchina Linux x64 / RTX 5070 Ti: EmbeddingGemma fp16, indice incrementale,
|
|
|
130
133
|
ricerca su fixture e coesistenza con i provider standard. Queste prove non
|
|
131
134
|
sostituiscono un benchmark di qualità o prestazioni su altri hardware.
|
|
132
135
|
|
|
136
|
+
## Servizio condiviso e memoria
|
|
137
|
+
|
|
138
|
+
Il primo client sceglie il profilo secondo la configurazione; gli altri
|
|
139
|
+
riutilizzano la stessa pipeline se quel profilo è consentito. Un profilo
|
|
140
|
+
incompatibile, una cache pesi diversa o valori diversi di `cpuThreads` o
|
|
141
|
+
`serviceIdleMs` danno errore: **nessun secondo modello e nessun fallback
|
|
142
|
+
in-process**. Un device forzato può riutilizzare un servizio CPU solo con
|
|
143
|
+
`fallback: cpu`, con avviso esplicito. Le richieste sono serializzate, con
|
|
144
|
+
massimo 16 richieste attive/in coda; ogni batch contiene al massimo 128 testi
|
|
145
|
+
e 2 MiB di testo. La saturazione della coda produce un errore, non un altro
|
|
146
|
+
worker. Un crash interrompe la richiesta corrente; un uso successivo può
|
|
147
|
+
riavviare il servizio, verificando il profilo già fissato dal client.
|
|
148
|
+
|
|
149
|
+
Dopo **5 minuti senza operazioni sul modello** il processo termina e l'OS
|
|
150
|
+
libera RAM/VRAM native. `status` non rinnova il timer. Chiudere/disabilitare
|
|
151
|
+
una singola sessione non arresta un modello che può servire altri processi.
|
|
152
|
+
Per liberarlo subito, quando non ci sono inferenze in corso:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
/codeindex service status
|
|
156
|
+
/codeindex service status --json
|
|
157
|
+
/codeindex service stop
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Gli stessi comandi funzionano come `node src/cli.js service status|stop`.
|
|
161
|
+
Stop viene rifiutato se ci sono richieste attive/in coda; non interrompe altri
|
|
162
|
+
client. Il servizio si riavvierà al prossimo utilizzo del modello. Nessun
|
|
163
|
+
avvio al login, servizio di sistema o modifica alle impostazioni globali di pi.
|
|
164
|
+
|
|
165
|
+
Il listener è solo `127.0.0.1`, con porta deterministica per la directory
|
|
166
|
+
utente e autenticazione tramite token in `~/.cache/pi-codeindex/service/`
|
|
167
|
+
(directory 0700, record 0600 su POSIX). Il binding esclusivo della porta
|
|
168
|
+
impedisce due avvii simultanei. Una porta occupata da un altro programma dà
|
|
169
|
+
un errore: `servicePort` consente una scelta esplicita. Usa gli stessi
|
|
170
|
+
`serviceDir`/`servicePort` in tutti i client: cambiarli crea endpoint distinti
|
|
171
|
+
e permette deliberatamente più modelli. Non condividere il token o la directory.
|
|
172
|
+
|
|
173
|
+
Restano per-processo le cache degli indici, dei vettori e dei grafi: il servizio
|
|
174
|
+
elimina le copie del **modello**, non tutta la memoria di pi. Dopo l'aggiornamento
|
|
175
|
+
**chiudi e riapri i vecchi processi pi**: i modelli già caricati dal vecchio
|
|
176
|
+
codice in-process non possono essere liberati dal nuovo servizio.
|
|
177
|
+
|
|
178
|
+
Verifica reale opt-in, con pesi già in cache (offline), due processi client,
|
|
179
|
+
indice/ricerca e uscita idle:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
CODEINDEX_TEST_SERVICE=1 CODEINDEX_DEVICE=cuda node --test test/service-integration.test.js
|
|
183
|
+
# Oppure cpu/q8, oppure WebGPU con CODEINDEX_WEBGPU_BACKEND=vulkan dopo setup-vulkan.
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Questi test non sono un nuovo benchmark Fase 5–7. La piattaforma iniziale di
|
|
187
|
+
verifica del servizio è Linux x64; Windows/DirectML e macOS/CoreML richiedono
|
|
188
|
+
verifica sulle rispettive macchine.
|
|
189
|
+
|
|
133
190
|
## Configurazione
|
|
134
191
|
|
|
135
192
|
Dentro pi:
|
|
@@ -142,6 +199,8 @@ Dentro pi:
|
|
|
142
199
|
/codeindex config set webgpuBackend vulkan solo WebGPU, setup-vulkan richiesto
|
|
143
200
|
/codeindex config set fallback cpu
|
|
144
201
|
/codeindex config unset device ripristina la scelta automatica
|
|
202
|
+
/codeindex service status processo condiviso, profilo e memoria
|
|
203
|
+
/codeindex service stop rilascia il modello quando inattivo
|
|
145
204
|
/codeindex doctor inventario, senza inferenza/download
|
|
146
205
|
/codeindex doctor --probe --device webgpu prova reale isolata (può scaricare i pesi)
|
|
147
206
|
```
|
|
@@ -175,9 +234,15 @@ non default silenziosi. Schema per editor: [config.schema.json](config.schema.js
|
|
|
175
234
|
| `offline` | booleano; niente download, errore se mancano pesi | `CODEINDEX_OFFLINE=true\|false\|1\|0` |
|
|
176
235
|
| `cacheDir` | cache pesi, default `~/.cache/pi-codeindex/text-model` | `CODEINDEX_MODEL_CACHE` |
|
|
177
236
|
| `cpuThreads` | 0 = default ORT, oppure 1–256; non limita la GPU | `CODEINDEX_CPU_THREADS` |
|
|
237
|
+
| `serviceIdleMs` | default 300000 (5 min), da 1000 a 86400000 ms | `CODEINDEX_SERVICE_IDLE_MS` |
|
|
238
|
+
| `serviceDir` | directory privata condivisa, default `~/.cache/pi-codeindex/service` | `CODEINDEX_SERVICE_DIR` |
|
|
239
|
+
| `servicePort` | 0 = deterministica, oppure 1024–65535; solo loopback | `CODEINDEX_SERVICE_PORT` |
|
|
178
240
|
|
|
179
241
|
**Riavvia pi con un nuovo processo** dopo una modifica: non si cambia pipeline
|
|
180
|
-
nel mezzo di un run.
|
|
242
|
+
nel mezzo di un run. Se il servizio ha già un profilo/configurazione diversi,
|
|
243
|
+
usa anche `service stop` quando inattivo, oppure attendi l'uscita idle.
|
|
244
|
+
Prima di aggiornare a un protocollo incompatibile, arresta il vecchio servizio.
|
|
245
|
+
Il menu non riavvia né reindicizza da solo. Device, dtype
|
|
181
246
|
e backend WebGPU effettivi sono salvati nell'indice; se diversi, la ricerca rifiuta di mescolare
|
|
182
247
|
i vettori e chiede un re-index. Gli indici precedenti senza `device` vengono
|
|
183
248
|
ricostruiti alla prossima indicizzazione.
|
|
@@ -198,9 +263,11 @@ CODEINDEX_DEVICE=cuda CODEINDEX_DTYPE=fp16 pi -e ./src/extension.js
|
|
|
198
263
|
in subprocessi isolati con timeout di 120 s per backend; `--all` può scaricare
|
|
199
264
|
più varianti. Ogni provider forzato viene provato senza fallback, per non
|
|
200
265
|
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
|
|
202
|
-
|
|
203
|
-
|
|
266
|
+
di una prova non abbatte pi. I probe restano esplicitamente isolati e **possono
|
|
267
|
+
caricare una copia aggiuntiva del modello** mentre il servizio è attivo: arresta
|
|
268
|
+
prima il servizio se vuoi evitare questo picco. Anche la pipeline ordinaria è
|
|
269
|
+
ora isolata, nel worker condiviso. Smoke del menu e dei comandi via RPC, senza
|
|
270
|
+
chiamate LLM e con cache modello temporanea/offline: `CODEINDEX_TEST_RPC=1 node --test test/config-rpc.test.js` (richiede
|
|
204
271
|
`pi` sul PATH; usa un file config temporaneo, non modifica quello utente).
|
|
205
272
|
|
|
206
273
|
## Comandi
|
|
@@ -252,13 +319,40 @@ Cosa aggiunge:
|
|
|
252
319
|
- **indicizzazione automatica** all'avvio della sessione, in background,
|
|
253
320
|
single-flight, interrompibile, con cap 5.000 file / 200 MB.
|
|
254
321
|
- **sincronizzazione**: watcher ricorsivo con debounce 800 ms, invalidazione
|
|
255
|
-
diretta da `write`/`edit` e sweep `git status`
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
scaricabile o il reindex fallisce, `search_code` risponde
|
|
261
|
-
la sessione continua.
|
|
322
|
+
diretta da `write`/`edit` e controllo dopo `bash`: sweep `git status` nei
|
|
323
|
+
repository, riconciliazione incrementale nelle cartelle senza Git. Una
|
|
324
|
+
riconciliazione riusa i vettori dei file intatti (misurato nei repository:
|
|
325
|
+
1 chunk re-embedded su 3 dopo una modifica, 920 ms fino all'indice aggiornato).
|
|
326
|
+
|
|
327
|
+
Se il modello non è scaricabile o il reindex fallisce, `search_code` risponde
|
|
328
|
+
con lo stato reale e la sessione continua.
|
|
329
|
+
|
|
330
|
+
### Cartelle senza Git
|
|
331
|
+
|
|
332
|
+
Git è opzionale. Nei repository la radice resta quella restituita da Git,
|
|
333
|
+
anche avviando pi da una sottocartella. Altrimenti viene usata **esattamente
|
|
334
|
+
la directory da cui avvii pi**: non viene creato `.git`, non servono commit
|
|
335
|
+
né un remote. `/codeindex status` mostra radice e modalità.
|
|
336
|
+
|
|
337
|
+
In modalità cartella la scansione esclude le directory nascoste e le consuete
|
|
338
|
+
directory di dipendenze/output (`node_modules`, `dist`, `build`, `out`,
|
|
339
|
+
`vendor`, `target`, `coverage`, `venv`, `__pycache__`); non segue i symlink.
|
|
340
|
+
Restano i limiti di estensione, dimensione, 5.000 file e 200 MB. Le esclusioni
|
|
341
|
+
sono quelle fisse della scansione locale: **senza Git i file `.gitignore`
|
|
342
|
+
non vengono interpretati**. Nei repository continua a valere `git ls-files`
|
|
343
|
+
con le esclusioni Git.
|
|
344
|
+
|
|
345
|
+
Il watcher gestisce anche rename/cancellazioni di directory; dopo `bash`
|
|
346
|
+
viene accodata una riconciliazione senza ricalcolare i vettori invariati.
|
|
347
|
+
Se il watcher non è disponibile, la riconciliazione viene accodata anche
|
|
348
|
+
all'inizio di un nuovo prompt. `/codeindex reindex` resta disponibile per
|
|
349
|
+
aggiornare manualmente l'indice; `--force` ricalcola tutti i vettori.
|
|
350
|
+
|
|
351
|
+
Per evitare scansioni involontarie dell'intera macchina, nella **home** e
|
|
352
|
+
nella **radice del disco**, se non sono repository Git, l'indice parte spento.
|
|
353
|
+
Avvia pi dalla cartella del progetto oppure abilitalo esplicitamente con
|
|
354
|
+
`/codeindex enable`; nelle due cartelle protette il consenso va ripetuto a
|
|
355
|
+
ogni avvio della sessione. Nessuna nuova configurazione globale è necessaria.
|
|
262
356
|
|
|
263
357
|
## Cosa indicizza
|
|
264
358
|
|
|
@@ -267,7 +361,10 @@ Markdown. Chunk declaration-aware, con fusione dei chunk piccoli (Markdown fino
|
|
|
267
361
|
a 80 righe; codice solo micro-chunk ≤6 righe). Esclusi: binari, file >1 MB,
|
|
268
362
|
minified, lockfile, `.pbl`/`.pbt`/`.pbw`.
|
|
269
363
|
|
|
270
|
-
## Numeri misurati (RTX 5070 Ti, fp16)
|
|
364
|
+
## Numeri misurati (runtime originale in-process, RTX 5070 Ti, fp16)
|
|
365
|
+
|
|
366
|
+
Queste misure precedono il servizio condiviso e non ne misurano memoria,
|
|
367
|
+
latenza IPC o prestazioni con client concorrenti.
|
|
271
368
|
|
|
272
369
|
| Repo | File | Chunk | Primo indice | Re-index |
|
|
273
370
|
|---|---|---|---|---|
|
package/config.schema.json
CHANGED
|
@@ -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": 0, "description": "0 sceglie una porta loopback deterministica dalla directory del servizio. Cambiarla crea un endpoint distinto." },
|
|
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.
|
|
3
|
+
"version": "1.1.0",
|
|
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
|
|
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
|
|
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: 0, 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 (
|
|
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 (
|
|
60
|
-
if (!/^\d+$/.test(text)) throw new Error(
|
|
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
|
|
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
|
}
|
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
|
}
|