@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/src/service.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Un listener esclusivo per directory/utente, un modello, una coda seriale.
|
|
2
|
+
import net from 'node:net';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
5
|
+
import { PROTOCOL, HOST, prepareServiceDirectory, readServiceRecord, writeServiceRecord, readFrame, writeFrame, serviceAddress, serviceError } from './service-protocol.js';
|
|
6
|
+
import { validateServiceConfig, assertServiceCompatible, validateTexts } from './service-policy.js';
|
|
7
|
+
|
|
8
|
+
export async function startEmbeddingService({ directory, port, maxQueue = 16, createRuntime = async (config) => {
|
|
9
|
+
const { createLocalEmbedder } = await import('./local-embed.js');
|
|
10
|
+
return createLocalEmbedder(config);
|
|
11
|
+
} }) {
|
|
12
|
+
const address = serviceAddress({ serviceDir: directory, servicePort: port });
|
|
13
|
+
await prepareServiceDirectory(address);
|
|
14
|
+
const token = randomBytes(32).toString('hex');
|
|
15
|
+
const record = { protocol: PROTOCOL, pid: process.pid, port: address.port, token };
|
|
16
|
+
const sockets = new Set();
|
|
17
|
+
const startedAt = Date.now();
|
|
18
|
+
let lastUsedAt = startedAt;
|
|
19
|
+
let idleMs = 300_000;
|
|
20
|
+
let timer, runtime = null, activeConfig = null;
|
|
21
|
+
let pending = 0, initializing = false, stopping = false, shutdownStarted = false, tail = Promise.resolve();
|
|
22
|
+
let resolveStopped;
|
|
23
|
+
const stopped = new Promise((resolve) => { resolveStopped = resolve; });
|
|
24
|
+
|
|
25
|
+
function info() {
|
|
26
|
+
return { running: !stopping, stopping, shared: true, pid: process.pid, protocol: PROTOCOL, port: address.port,
|
|
27
|
+
pending, initializing, startedAt, lastUsedAt, idleMs, rssBytes: process.memoryUsage().rss,
|
|
28
|
+
...(runtime?.info() ?? { profile: null, attempts: [], error: null }) };
|
|
29
|
+
}
|
|
30
|
+
function reply(socket, value) {
|
|
31
|
+
if (socket.destroyed) return;
|
|
32
|
+
try { writeFrame(socket, { protocol: PROTOCOL, ...value }); socket.end(); }
|
|
33
|
+
catch { socket.destroy(); }
|
|
34
|
+
}
|
|
35
|
+
function armIdle() {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
if (!stopping && pending === 0) timer = setTimeout(() => { void stop(); }, Math.max(1, idleMs - (Date.now() - lastUsedAt)));
|
|
38
|
+
}
|
|
39
|
+
async function stop() {
|
|
40
|
+
if (shutdownStarted) return stopped;
|
|
41
|
+
shutdownStarted = true;
|
|
42
|
+
stopping = true;
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
// L'inferenza attiva termina; quelle in coda ricevono un errore esplicito.
|
|
45
|
+
await tail;
|
|
46
|
+
for (const socket of sockets) socket.destroy();
|
|
47
|
+
if (runtime) {
|
|
48
|
+
let timeout;
|
|
49
|
+
try { await Promise.race([runtime.dispose(), new Promise((resolve) => { timeout = setTimeout(resolve, 3000); })]); }
|
|
50
|
+
catch { /* l'uscita del processo libera comunque le risorse native */ }
|
|
51
|
+
finally { clearTimeout(timeout); }
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const current = await readServiceRecord(address);
|
|
55
|
+
if (current?.token === token) await fs.rm(address.file, { force: true });
|
|
56
|
+
} catch { /* non cancellare record sostituiti/non sicuri */ }
|
|
57
|
+
// Chiudi anche connessioni arrivate durante dispose, prima di rilasciare
|
|
58
|
+
// la porta: nessun client incompleto deve trattenere il worker nativo.
|
|
59
|
+
for (const socket of sockets) socket.destroy();
|
|
60
|
+
await new Promise((resolve) => server.close(resolve));
|
|
61
|
+
resolveStopped();
|
|
62
|
+
}
|
|
63
|
+
async function modelFor(config, expectedProfile) {
|
|
64
|
+
if (!runtime) {
|
|
65
|
+
activeConfig = config;
|
|
66
|
+
idleMs = config.serviceIdleMs;
|
|
67
|
+
initializing = true;
|
|
68
|
+
try {
|
|
69
|
+
runtime = await createRuntime(config);
|
|
70
|
+
await runtime.getProfile();
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// Dopo un'inizializzazione fallita si esce: nessuna risorsa nativa
|
|
73
|
+
// residua viene riusata per avviare un altro profilo.
|
|
74
|
+
e.initializationFailed = true;
|
|
75
|
+
throw e;
|
|
76
|
+
} finally { initializing = false; }
|
|
77
|
+
}
|
|
78
|
+
const profile = await runtime.getProfile();
|
|
79
|
+
assertServiceCompatible(config, activeConfig, profile, expectedProfile);
|
|
80
|
+
return { runtime, profile };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const server = net.createServer((socket) => {
|
|
84
|
+
socket.on('error', () => {});
|
|
85
|
+
sockets.add(socket);
|
|
86
|
+
socket.once('close', () => sockets.delete(socket));
|
|
87
|
+
if (sockets.size > maxQueue + 16) { socket.destroy(); return; }
|
|
88
|
+
void (async () => {
|
|
89
|
+
let authenticated = false;
|
|
90
|
+
try {
|
|
91
|
+
const request = await readFrame(socket, { timeoutMs: 3000 });
|
|
92
|
+
if (typeof request?.token !== 'string' || !/^[a-f0-9]{64}$/.test(request.token) ||
|
|
93
|
+
!timingSafeEqual(Buffer.from(request.token), Buffer.from(token))) throw serviceError('UNAUTHORIZED', 'autenticazione del servizio locale fallita');
|
|
94
|
+
authenticated = true;
|
|
95
|
+
if (request.protocol !== PROTOCOL) throw serviceError('PROTOCOL_MISMATCH', 'protocollo del servizio incompatibile; arresta il servizio e riavvia pi');
|
|
96
|
+
if (request.op === 'status') { reply(socket, { ok: true, result: info() }); return; }
|
|
97
|
+
if (request.op === 'stop') {
|
|
98
|
+
if (stopping) { reply(socket, { ok: true, result: { stopping: true, pid: process.pid } }); return; }
|
|
99
|
+
if (pending) throw serviceError('SERVICE_BUSY', `servizio occupato (${pending} richieste): stop rifiutato per non interrompere altre sessioni`);
|
|
100
|
+
reply(socket, { ok: true, result: { stopping: true, pid: process.pid } });
|
|
101
|
+
// Blocca subito nuove operazioni, poi lascia uscire la risposta.
|
|
102
|
+
stopping = true;
|
|
103
|
+
setImmediate(() => { void stop(); });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (stopping) throw serviceError('SERVICE_STOPPING', 'servizio locale in arresto; riprova tra poco');
|
|
107
|
+
if (!['profile', 'embed'].includes(request.op)) throw serviceError('BAD_OPERATION', 'operazione del servizio sconosciuta');
|
|
108
|
+
const config = validateServiceConfig(request.config);
|
|
109
|
+
const requestedAddress = serviceAddress(config);
|
|
110
|
+
if (requestedAddress.directory !== address.directory || requestedAddress.port !== address.port) throw serviceError('BAD_CONFIG', 'endpoint client diverso da quello del servizio');
|
|
111
|
+
if (request.op === 'embed') validateTexts(request.texts, request.normalize);
|
|
112
|
+
if (pending >= maxQueue) throw serviceError('QUEUE_FULL', `coda del servizio piena (massimo ${maxQueue}); riprova senza avviare altri modelli`);
|
|
113
|
+
pending++;
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
const task = tail.then(async () => {
|
|
116
|
+
try {
|
|
117
|
+
if (socket.destroyed) return;
|
|
118
|
+
if (stopping) throw serviceError('SERVICE_STOPPING', 'servizio locale in arresto');
|
|
119
|
+
const { runtime: model, profile } = await modelFor(config, request.expectedProfile);
|
|
120
|
+
const result = request.op === 'embed' ? await model.embed(request.texts, { normalize: request.normalize }) : profile;
|
|
121
|
+
reply(socket, { ok: true, result, info: info() });
|
|
122
|
+
} catch (e) {
|
|
123
|
+
reply(socket, { ok: false, code: e.code ?? 'INFERENCE_FAILED', error: String(e.message ?? e).slice(0, 4000), info: info() });
|
|
124
|
+
if (e.initializationFailed) {
|
|
125
|
+
stopping = true;
|
|
126
|
+
setImmediate(() => { void stop(); });
|
|
127
|
+
}
|
|
128
|
+
} finally {
|
|
129
|
+
pending--;
|
|
130
|
+
lastUsedAt = Date.now();
|
|
131
|
+
armIdle();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// La coda non viene avvelenata da richieste fallite/disconnesse.
|
|
135
|
+
tail = task.catch(() => {});
|
|
136
|
+
} catch (e) {
|
|
137
|
+
reply(socket, { ok: false, code: e.code ?? 'BAD_REQUEST', error: String(e.message ?? e).slice(0, 4000), ...(authenticated ? { info: info() } : {}) });
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
});
|
|
141
|
+
server.on('error', () => {});
|
|
142
|
+
await new Promise((resolve, reject) => {
|
|
143
|
+
server.once('error', reject);
|
|
144
|
+
server.listen({ host: HOST, port: address.port, exclusive: true }, () => { server.off('error', reject); resolve(); });
|
|
145
|
+
});
|
|
146
|
+
try { await writeServiceRecord(address, record); }
|
|
147
|
+
catch (e) { server.close(); throw e; }
|
|
148
|
+
armIdle();
|
|
149
|
+
return { info, stop, stopped };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Entry point condiviso dal worker reale e da fixture senza modello.
|
|
153
|
+
export async function runServiceWorker(options = {}) {
|
|
154
|
+
let service;
|
|
155
|
+
try {
|
|
156
|
+
const directory = process.argv[2];
|
|
157
|
+
const port = Number(process.argv[3]);
|
|
158
|
+
if (!directory || !Number.isInteger(port) || port < 1024 || port > 65535) throw new Error('argomenti interni del servizio non validi');
|
|
159
|
+
service = await startEmbeddingService({ ...options, directory, port });
|
|
160
|
+
process.once('SIGTERM', () => { void service.stop(); });
|
|
161
|
+
process.once('SIGINT', () => { void service.stop(); });
|
|
162
|
+
if (process.connected) process.send({ ready: true, pid: process.pid }, () => { if (process.connected) process.disconnect(); });
|
|
163
|
+
await service.stopped;
|
|
164
|
+
// Necessario anche se ORT/Dawn trattiene handle dopo dispose: l'OS libera
|
|
165
|
+
// l'intero processo. Non si scarica/ricarica un modello nello stesso worker.
|
|
166
|
+
process.exit(0);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
const exit = () => process.exit(e.code === 'EADDRINUSE' ? 0 : 1);
|
|
169
|
+
if (process.connected) process.send({ ready: false, code: e.code, error: e.message }, exit);
|
|
170
|
+
else exit();
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/sync.js
CHANGED
|
@@ -19,8 +19,12 @@ const extOf = (p) => {
|
|
|
19
19
|
return m ? m[0].toLowerCase() : '';
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Path relativo -> normalizzato con '/', o null se fuori scope.
|
|
24
|
+
* Su rename, includeDirectories accetta anche directory/nomi senza estensione
|
|
25
|
+
* supportata: un evento sulla directory può rappresentare molti file rimossi.
|
|
26
|
+
*/
|
|
27
|
+
export function watchablePath(rel, root, { includeDirectories = false } = {}) {
|
|
24
28
|
if (!rel) return null;
|
|
25
29
|
let p = String(rel).replace(/\\/g, '/');
|
|
26
30
|
if (path.isAbsolute(p)) {
|
|
@@ -33,15 +37,15 @@ export function watchablePath(rel, root) {
|
|
|
33
37
|
if (segments.length === 0 || segments[0] === '..') return null;
|
|
34
38
|
p = segments.join('/');
|
|
35
39
|
if (segments.some((s) => IGNORED_DIRS.has(s))) return null;
|
|
36
|
-
if (!(extOf(p) in LANG_BY_EXT)) return null;
|
|
40
|
+
if (!includeDirectories && !(extOf(p) in LANG_BY_EXT)) return null;
|
|
37
41
|
return segments.join('/');
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/** Filtra e deduplica una lista di path (watcher, hook dei tool, git status). */
|
|
41
|
-
export function filterDirty(paths, root) {
|
|
45
|
+
export function filterDirty(paths, root, options) {
|
|
42
46
|
const out = new Set();
|
|
43
47
|
for (const p of paths ?? []) {
|
|
44
|
-
const w = watchablePath(p, root);
|
|
48
|
+
const w = watchablePath(p, root, options);
|
|
45
49
|
if (w) out.add(w);
|
|
46
50
|
}
|
|
47
51
|
return out;
|
package/src/workspace.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Radice del progetto: Git quando disponibile, altrimenti la cartella corrente.
|
|
2
|
+
// Nessuna configurazione di progetto viene letta e nessun repository viene creato.
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { repoRoot } from './util.js';
|
|
6
|
+
|
|
7
|
+
export function resolveWorkspace(cwd = process.cwd(), {
|
|
8
|
+
findRepoRoot = repoRoot,
|
|
9
|
+
home = os.homedir(),
|
|
10
|
+
} = {}) {
|
|
11
|
+
const gitRoot = findRepoRoot(cwd);
|
|
12
|
+
const root = path.resolve(gitRoot ?? cwd);
|
|
13
|
+
const mode = gitRoot ? 'git' : 'folder';
|
|
14
|
+
// Un avvio dalla home o dalla radice del disco non deve scandire l'intera
|
|
15
|
+
// macchina per errore. /codeindex enable resta un consenso esplicito.
|
|
16
|
+
const broadFolder = root === path.parse(root).root || root === path.resolve(home);
|
|
17
|
+
return { root, mode, autoIndex: mode === 'git' || !broadFolder };
|
|
18
|
+
}
|