jinzd-ai-cli 0.4.87 → 0.4.89

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.
@@ -0,0 +1,460 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ EMBEDDING_DIM,
4
+ embed,
5
+ embedOne
6
+ } from "./chunk-KHYD3WXE.js";
7
+
8
+ // src/memory/chat-index.ts
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import os from "os";
12
+ import crypto from "crypto";
13
+
14
+ // src/security/redactor.ts
15
+ var DEFAULT_PATTERNS = [
16
+ // password: xxx / password = xxx / password="xxx"
17
+ // Covers YAML / JSON / shell-ish / env-file forms.
18
+ { kind: "password", regex: /\b(password|passwd|pwd)\s*[:=]\s*["']?([^\s"',;{}]{4,200})["']?/gi },
19
+ // PGPASSWORD=xxx (explicit bash env-var form, separate rule because no quotes usually)
20
+ { kind: "pgpassword-env", regex: /\b(PGPASSWORD)=([^\s"']{4,200})/g },
21
+ // JDBC/PG/MySQL/Mongo connection strings with inline credentials
22
+ // postgresql://user:pass@host/db → redact pass
23
+ { kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
24
+ // Anthropic API keys
25
+ { kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
26
+ // OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
27
+ { kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
28
+ // GitHub personal access tokens
29
+ { kind: "github-pat", regex: /\b(ghp_[a-zA-Z0-9]{36})\b/g },
30
+ { kind: "github-oauth", regex: /\b(gho_[a-zA-Z0-9]{36})\b/g },
31
+ { kind: "github-install", regex: /\b(ghs_[a-zA-Z0-9]{36})\b/g },
32
+ // Slack tokens
33
+ { kind: "slack-bot", regex: /\b(xoxb-\d+-\d+-[a-zA-Z0-9]+)\b/g },
34
+ { kind: "slack-user", regex: /\b(xoxp-\d+-\d+-\d+-[a-zA-Z0-9]+)\b/g },
35
+ // AWS access key IDs (AKIA...) and secret access keys are context-dependent;
36
+ // we only catch the ID because secret key alone is indistinguishable from random base64.
37
+ { kind: "aws-access-key-id", regex: /\b(AKIA[0-9A-Z]{16})\b/g },
38
+ // Google API keys
39
+ { kind: "google-api-key", regex: /\b(AIza[0-9A-Za-z_-]{35})\b/g },
40
+ // Generic "api_key": "..." / "apiKey": "..." / api-key=xxx
41
+ { kind: "api-key", regex: /\b(api[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{16,200})["']?/gi },
42
+ // Generic token: xxx (only when value looks token-shaped; avoids eating human prose)
43
+ { kind: "token", regex: /\b(token|access[_-]?token|bearer[_-]?token)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{20,300})["']?/gi },
44
+ // Bearer <token> in Authorization headers
45
+ { kind: "bearer", regex: /\b(Authorization:\s*Bearer\s+)([a-zA-Z0-9_\-.=]{20,500})/g },
46
+ // Private key PEM blocks — catch the header+footer together
47
+ { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g }
48
+ ];
49
+ function render(placeholder, kind) {
50
+ return placeholder.replace("{kind}", kind);
51
+ }
52
+ function redactString(input, options) {
53
+ if (!options.enabled || !input) return { redacted: input, hits: [] };
54
+ const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
55
+ const patterns = [
56
+ ...options.patterns ?? DEFAULT_PATTERNS,
57
+ ...(options.customRegexes ?? []).flatMap((src, i) => {
58
+ try {
59
+ const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
60
+ const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
61
+ const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
62
+ return [{ kind: `custom-${i}`, regex }];
63
+ } catch {
64
+ return [];
65
+ }
66
+ })
67
+ ];
68
+ let redacted = input;
69
+ const hits = [];
70
+ for (const { kind, regex } of patterns) {
71
+ const rx = new RegExp(regex.source, regex.flags);
72
+ redacted = redacted.replace(rx, (...args) => {
73
+ const match = args[0];
74
+ const probe = new RegExp(rx.source).exec(match);
75
+ const captureCount = probe ? probe.length - 1 : 0;
76
+ const g1 = captureCount >= 1 ? args[1] : void 0;
77
+ const g2 = captureCount >= 2 ? args[2] : void 0;
78
+ const offset = args[1 + captureCount];
79
+ if (captureCount >= 2 && typeof g2 === "string") {
80
+ hits.push({ kind, start: offset + (g1?.length ?? 0), length: g2.length, secret: g2 });
81
+ return `${g1}${render(placeholder, kind)}`;
82
+ }
83
+ hits.push({ kind, start: offset, length: match.length, secret: g1 ?? match });
84
+ return render(placeholder, kind);
85
+ });
86
+ }
87
+ return { redacted, hits };
88
+ }
89
+ function redactJson(value, options) {
90
+ if (!options.enabled) return { value, hits: [] };
91
+ const allHits = [];
92
+ function walk(v) {
93
+ if (typeof v === "string") {
94
+ const r = redactString(v, options);
95
+ allHits.push(...r.hits);
96
+ return r.redacted;
97
+ }
98
+ if (Array.isArray(v)) return v.map(walk);
99
+ if (v && typeof v === "object") {
100
+ const out = {};
101
+ for (const [k, vv] of Object.entries(v)) {
102
+ out[k] = walk(vv);
103
+ }
104
+ return out;
105
+ }
106
+ return v;
107
+ }
108
+ const redacted = walk(value);
109
+ return { value: redacted, hits: allHits };
110
+ }
111
+ function scanString(input, options) {
112
+ const { hits } = redactString(input, { ...options, enabled: true });
113
+ return hits;
114
+ }
115
+
116
+ // src/memory/chat-index.ts
117
+ var MEMORY_DIR_NAME = "memory-index";
118
+ var CHUNKS_FILE = "chunks.json";
119
+ var VECTORS_FILE = "vectors.vec";
120
+ var VEC_MAGIC = 1094929750;
121
+ var VEC_VERSION = 1;
122
+ var VEC_HEADER_BYTES = 16;
123
+ function memoryIndexDir() {
124
+ return path.join(os.homedir(), ".aicli", MEMORY_DIR_NAME);
125
+ }
126
+ function chunksPath() {
127
+ return path.join(memoryIndexDir(), CHUNKS_FILE);
128
+ }
129
+ function vectorsPath() {
130
+ return path.join(memoryIndexDir(), VECTORS_FILE);
131
+ }
132
+ function historyDir() {
133
+ return path.join(os.homedir(), ".aicli", "history");
134
+ }
135
+ var MAX_CHUNK_CHARS = 1200;
136
+ var MIN_CHUNK_CHARS = 40;
137
+ function extractMessageText(msg) {
138
+ if (typeof msg.content === "string") return msg.content;
139
+ if (Array.isArray(msg.content)) {
140
+ return msg.content.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("\n");
141
+ }
142
+ return "";
143
+ }
144
+ function chunkSession(session) {
145
+ const chunks = [];
146
+ let pending = null;
147
+ const flush = () => {
148
+ if (!pending) return;
149
+ const rawText = pending.parts.join("\n").trim();
150
+ if (rawText.length < MIN_CHUNK_CHARS) {
151
+ pending = null;
152
+ return;
153
+ }
154
+ const { redacted } = redactString(rawText, { enabled: true });
155
+ const id = crypto.createHash("sha1").update(`${session.id}|${pending.start}|${pending.end}|${redacted.length}`).digest("hex").slice(0, 16);
156
+ chunks.push({
157
+ id,
158
+ sessionId: session.id,
159
+ sessionTitle: session.title,
160
+ provider: session.provider,
161
+ model: session.model,
162
+ startMessageIdx: pending.start,
163
+ endMessageIdx: pending.end,
164
+ text: redacted,
165
+ timestamp: pending.latestTs,
166
+ roles: pending.roles
167
+ });
168
+ pending = null;
169
+ };
170
+ for (let i = 0; i < session.messages.length; i++) {
171
+ const m = session.messages[i];
172
+ if (m.role !== "user" && m.role !== "assistant" && m.role !== "system") continue;
173
+ const text = extractMessageText(m).trim();
174
+ if (!text) continue;
175
+ const ts = m.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
176
+ const prefix = m.role === "user" ? "[USER] " : m.role === "assistant" ? "[AI] " : "[SYS] ";
177
+ const part = `${prefix}${text}`;
178
+ if (!pending) {
179
+ pending = { start: i, end: i, parts: [part], roles: [m.role], latestTs: ts };
180
+ continue;
181
+ }
182
+ const projected = pending.parts.reduce((n, p) => n + p.length + 1, 0) + part.length;
183
+ if (projected > MAX_CHUNK_CHARS) {
184
+ flush();
185
+ pending = { start: i, end: i, parts: [part], roles: [m.role], latestTs: ts };
186
+ } else {
187
+ pending.parts.push(part);
188
+ pending.end = i;
189
+ pending.roles.push(m.role);
190
+ pending.latestTs = ts;
191
+ }
192
+ }
193
+ flush();
194
+ return chunks;
195
+ }
196
+ function writeVectorsFile(chunks, vectors) {
197
+ if (chunks.length * EMBEDDING_DIM !== vectors.length) {
198
+ throw new Error(
199
+ `writeVectorsFile: length mismatch \u2014 ${chunks.length} chunks vs ${vectors.length / EMBEDDING_DIM} vectors`
200
+ );
201
+ }
202
+ const dir = memoryIndexDir();
203
+ fs.mkdirSync(dir, { recursive: true });
204
+ const totalBytes = VEC_HEADER_BYTES + vectors.byteLength;
205
+ const buf = Buffer.alloc(totalBytes);
206
+ buf.writeUInt32LE(VEC_MAGIC, 0);
207
+ buf.writeUInt32LE(VEC_VERSION, 4);
208
+ buf.writeUInt32LE(chunks.length, 8);
209
+ buf.writeUInt32LE(EMBEDDING_DIM, 12);
210
+ Buffer.from(vectors.buffer, vectors.byteOffset, vectors.byteLength).copy(buf, VEC_HEADER_BYTES);
211
+ const target = vectorsPath();
212
+ const tmp = `${target}.tmp`;
213
+ fs.writeFileSync(tmp, buf);
214
+ fs.renameSync(tmp, target);
215
+ }
216
+ function readVectorsFile(expectedCount) {
217
+ const p = vectorsPath();
218
+ if (!fs.existsSync(p)) return null;
219
+ let buf;
220
+ try {
221
+ buf = fs.readFileSync(p);
222
+ } catch {
223
+ return null;
224
+ }
225
+ if (buf.length < VEC_HEADER_BYTES) return null;
226
+ const magic = buf.readUInt32LE(0);
227
+ const version = buf.readUInt32LE(4);
228
+ const count = buf.readUInt32LE(8);
229
+ const dim = buf.readUInt32LE(12);
230
+ if (magic !== VEC_MAGIC || version !== VEC_VERSION || dim !== EMBEDDING_DIM) return null;
231
+ if (count !== expectedCount) return null;
232
+ const expected = VEC_HEADER_BYTES + count * dim * 4;
233
+ if (buf.length !== expected) return null;
234
+ return new Float32Array(
235
+ buf.buffer.slice(buf.byteOffset + VEC_HEADER_BYTES, buf.byteOffset + expected)
236
+ );
237
+ }
238
+ function writeIndexFile(idx) {
239
+ const dir = memoryIndexDir();
240
+ fs.mkdirSync(dir, { recursive: true });
241
+ const target = chunksPath();
242
+ const tmp = `${target}.tmp`;
243
+ fs.writeFileSync(tmp, JSON.stringify(idx, null, 2), "utf-8");
244
+ fs.renameSync(tmp, target);
245
+ }
246
+ function readIndexFile() {
247
+ const p = chunksPath();
248
+ if (!fs.existsSync(p)) return null;
249
+ try {
250
+ const raw = fs.readFileSync(p, "utf-8");
251
+ const data = JSON.parse(raw);
252
+ if (data.version !== 1) return null;
253
+ return data;
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+ function loadChatIndex() {
259
+ const idx = readIndexFile();
260
+ if (!idx) return null;
261
+ const vectors = readVectorsFile(idx.chunks.length);
262
+ if (!vectors) return null;
263
+ return { idx, vectors };
264
+ }
265
+ function clearChatIndex() {
266
+ try {
267
+ if (fs.existsSync(chunksPath())) fs.unlinkSync(chunksPath());
268
+ } catch {
269
+ }
270
+ try {
271
+ if (fs.existsSync(vectorsPath())) fs.unlinkSync(vectorsPath());
272
+ } catch {
273
+ }
274
+ }
275
+ function listSessionFiles() {
276
+ const dir = historyDir();
277
+ if (!fs.existsSync(dir)) return [];
278
+ const out = [];
279
+ for (const name of fs.readdirSync(dir)) {
280
+ if (!name.endsWith(".json")) continue;
281
+ const id = name.replace(/\.json$/, "");
282
+ const p = path.join(dir, name);
283
+ try {
284
+ const st = fs.statSync(p);
285
+ out.push({ id, path: p, mtime: st.mtimeMs });
286
+ } catch {
287
+ }
288
+ }
289
+ return out;
290
+ }
291
+ function readSession(p) {
292
+ try {
293
+ const data = JSON.parse(fs.readFileSync(p, "utf-8"));
294
+ if (!data.id || !Array.isArray(data.messages)) return null;
295
+ return data;
296
+ } catch {
297
+ return null;
298
+ }
299
+ }
300
+ async function buildChatIndex(options = {}) {
301
+ const t0 = Date.now();
302
+ const onProgress = options.onProgress ?? (() => {
303
+ });
304
+ onProgress({ stage: "scanning" });
305
+ const files = listSessionFiles();
306
+ const existing = options.full ? null : loadChatIndex();
307
+ const prevMtimes = existing?.idx.sessionMtimes ?? {};
308
+ const prevChunksBySession = /* @__PURE__ */ new Map();
309
+ const prevVectorsByChunkId = /* @__PURE__ */ new Map();
310
+ if (existing) {
311
+ for (let i = 0; i < existing.idx.chunks.length; i++) {
312
+ const c = existing.idx.chunks[i];
313
+ const arr = prevChunksBySession.get(c.sessionId) ?? [];
314
+ arr.push(c);
315
+ prevChunksBySession.set(c.sessionId, arr);
316
+ prevVectorsByChunkId.set(
317
+ c.id,
318
+ existing.vectors.slice(i * EMBEDDING_DIM, (i + 1) * EMBEDDING_DIM)
319
+ );
320
+ }
321
+ }
322
+ onProgress({ stage: "chunking" });
323
+ const stats = {
324
+ sessionsScanned: files.length,
325
+ sessionsIndexed: 0,
326
+ sessionsSkipped: 0,
327
+ chunksTotal: 0,
328
+ chunksAdded: 0,
329
+ chunksRemoved: 0,
330
+ durationMs: 0
331
+ };
332
+ const newMtimes = {};
333
+ const finalChunks = [];
334
+ const finalVectors = [];
335
+ const toEmbed = [];
336
+ for (const f of files) {
337
+ newMtimes[f.id] = f.mtime;
338
+ const prevMtime = prevMtimes[f.id];
339
+ if (prevMtime === f.mtime && prevChunksBySession.has(f.id)) {
340
+ stats.sessionsSkipped++;
341
+ const cached = prevChunksBySession.get(f.id);
342
+ for (const c of cached) {
343
+ const v = prevVectorsByChunkId.get(c.id);
344
+ if (!v) continue;
345
+ finalChunks.push(c);
346
+ finalVectors.push(v);
347
+ }
348
+ continue;
349
+ }
350
+ const sess = readSession(f.path);
351
+ if (!sess) continue;
352
+ stats.sessionsIndexed++;
353
+ const chunks = chunkSession(sess);
354
+ for (const c of chunks) {
355
+ finalChunks.push(c);
356
+ toEmbed.push(c);
357
+ stats.chunksAdded++;
358
+ }
359
+ }
360
+ if (existing) {
361
+ for (const prevId of Object.keys(prevMtimes)) {
362
+ if (!(prevId in newMtimes)) {
363
+ const removed = prevChunksBySession.get(prevId) ?? [];
364
+ stats.chunksRemoved += removed.length;
365
+ }
366
+ }
367
+ }
368
+ stats.chunksTotal = finalChunks.length;
369
+ const BATCH = 16;
370
+ onProgress({ stage: "embedding", processed: 0, total: toEmbed.length });
371
+ const newVectorsByChunkId = /* @__PURE__ */ new Map();
372
+ for (let i = 0; i < toEmbed.length; i += BATCH) {
373
+ const batch = toEmbed.slice(i, i + BATCH);
374
+ const vecs = await embed(batch.map((c) => c.text));
375
+ for (let j = 0; j < batch.length; j++) {
376
+ newVectorsByChunkId.set(batch[j].id, vecs[j]);
377
+ }
378
+ onProgress({ stage: "embedding", processed: Math.min(i + BATCH, toEmbed.length), total: toEmbed.length });
379
+ }
380
+ const flat = new Float32Array(finalChunks.length * EMBEDDING_DIM);
381
+ for (let i = 0; i < finalChunks.length; i++) {
382
+ const c = finalChunks[i];
383
+ const v = newVectorsByChunkId.get(c.id) ?? prevVectorsByChunkId.get(c.id);
384
+ if (!v || v.length !== EMBEDDING_DIM) {
385
+ continue;
386
+ }
387
+ flat.set(v, i * EMBEDDING_DIM);
388
+ }
389
+ onProgress({ stage: "saving" });
390
+ const idx = {
391
+ version: 1,
392
+ built: (/* @__PURE__ */ new Date()).toISOString(),
393
+ model: "Xenova/paraphrase-multilingual-MiniLM-L12-v2",
394
+ sessionMtimes: newMtimes,
395
+ chunks: finalChunks
396
+ };
397
+ writeIndexFile(idx);
398
+ writeVectorsFile(finalChunks, flat);
399
+ stats.durationMs = Date.now() - t0;
400
+ onProgress({ stage: "done" });
401
+ return stats;
402
+ }
403
+ async function searchChatMemory(query, options = {}) {
404
+ const topK = options.topK ?? 5;
405
+ const minScore = options.minScore ?? 0.25;
406
+ const loaded = loadChatIndex();
407
+ if (!loaded || loaded.idx.chunks.length === 0) return [];
408
+ const { idx, vectors } = loaded;
409
+ const { redacted } = redactString(query, { enabled: true });
410
+ const qvec = await embedOne(redacted);
411
+ const candidates = [];
412
+ for (let i = 0; i < idx.chunks.length; i++) {
413
+ const c = idx.chunks[i];
414
+ if (options.sessionId && c.sessionId !== options.sessionId) continue;
415
+ if (options.excludeSessionId && c.sessionId === options.excludeSessionId) continue;
416
+ let score = 0;
417
+ const base = i * EMBEDDING_DIM;
418
+ for (let d = 0; d < EMBEDDING_DIM; d++) {
419
+ score += vectors[base + d] * qvec[d];
420
+ }
421
+ if (score < minScore) continue;
422
+ candidates.push({ chunk: c, score });
423
+ }
424
+ candidates.sort((a, b) => b.score - a.score);
425
+ return candidates.slice(0, topK);
426
+ }
427
+ function getChatIndexStatus() {
428
+ const status = {
429
+ exists: false,
430
+ chunks: 0,
431
+ sessions: 0,
432
+ vecFileSizeBytes: 0,
433
+ chunksFileSizeBytes: 0
434
+ };
435
+ try {
436
+ if (fs.existsSync(vectorsPath())) status.vecFileSizeBytes = fs.statSync(vectorsPath()).size;
437
+ if (fs.existsSync(chunksPath())) status.chunksFileSizeBytes = fs.statSync(chunksPath()).size;
438
+ } catch {
439
+ }
440
+ const idx = readIndexFile();
441
+ if (!idx) return status;
442
+ status.exists = true;
443
+ status.chunks = idx.chunks.length;
444
+ status.sessions = Object.keys(idx.sessionMtimes).length;
445
+ status.built = idx.built;
446
+ status.model = idx.model;
447
+ return status;
448
+ }
449
+
450
+ export {
451
+ DEFAULT_PATTERNS,
452
+ redactJson,
453
+ scanString,
454
+ chunkSession,
455
+ loadChatIndex,
456
+ clearChatIndex,
457
+ buildChatIndex,
458
+ searchChatMemory,
459
+ getChatIndexStatus
460
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/core/constants.ts
4
- var VERSION = "0.4.87";
4
+ var VERSION = "0.4.89";
5
5
  var APP_NAME = "ai-cli";
6
6
  var CONFIG_DIR_NAME = ".aicli";
7
7
  var CONFIG_FILE_NAME = "config.json";
@@ -2,13 +2,16 @@
2
2
  import {
3
3
  schemaToJsonSchema,
4
4
  truncateForPersist
5
- } from "./chunk-SP6RFAKW.js";
5
+ } from "./chunk-ABPT6XCI.js";
6
6
  import {
7
7
  AuthError,
8
8
  ProviderError,
9
9
  ProviderNotFoundError,
10
10
  RateLimitError
11
11
  } from "./chunk-2ZD3YTVM.js";
12
+ import {
13
+ redactJson
14
+ } from "./chunk-ANYYM4CF.js";
12
15
  import {
13
16
  APP_NAME,
14
17
  CONFIG_DIR_NAME,
@@ -18,7 +21,7 @@ import {
18
21
  MCP_PROTOCOL_VERSION,
19
22
  MCP_TOOL_PREFIX,
20
23
  VERSION
21
- } from "./chunk-BAOIXQHD.js";
24
+ } from "./chunk-E7YC4GWV.js";
22
25
 
23
26
  // src/providers/claude.ts
24
27
  import Anthropic from "@anthropic-ai/sdk";
@@ -2668,9 +2671,27 @@ function extractJsonField(header, field) {
2668
2671
  var SessionManager = class {
2669
2672
  _current = null;
2670
2673
  historyDir;
2674
+ config;
2675
+ /** Last save's redaction hit count — exposed for /security status reporting */
2676
+ lastRedactionHits = 0;
2671
2677
  constructor(config) {
2678
+ this.config = config;
2672
2679
  this.historyDir = config.getHistoryDir();
2673
2680
  }
2681
+ /**
2682
+ * Build redaction options from config. Returns `{ enabled: false }` when
2683
+ * `security.redactOnSave` is off or `security.mode` is 'off'.
2684
+ */
2685
+ redactOptionsForSave() {
2686
+ const security = this.config.get("security");
2687
+ if (!security || !security.redactOnSave || security.mode === "off") {
2688
+ return { enabled: false };
2689
+ }
2690
+ return {
2691
+ enabled: true,
2692
+ customRegexes: security.customPatterns ?? []
2693
+ };
2694
+ }
2674
2695
  get current() {
2675
2696
  return this._current;
2676
2697
  }
@@ -2696,8 +2717,12 @@ var SessionManager = class {
2696
2717
  if (!this._current) return;
2697
2718
  mkdirSync(this.historyDir, { recursive: true });
2698
2719
  const filePath = join(this.historyDir, `${this._current.id}.json`);
2720
+ const raw = this._current.toJSON();
2721
+ const opts = this.redactOptionsForSave();
2722
+ const { value: payload, hits } = redactJson(raw, opts);
2723
+ this.lastRedactionHits = hits.length;
2699
2724
  const tmpPath = filePath + ".tmp";
2700
- writeFileSync(tmpPath, JSON.stringify(this._current.toJSON(), null, 2), "utf-8");
2725
+ writeFileSync(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
2701
2726
  renameSync(tmpPath, filePath);
2702
2727
  }
2703
2728
  loadSession(id) {
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/symbols/embedder.ts
4
+ import path from "path";
5
+ import os from "os";
6
+ import fs from "fs";
7
+ var EMBEDDING_MODEL_ID = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
8
+ var EMBEDDING_DIM = 384;
9
+ var pipelinePromise = null;
10
+ function cacheDir() {
11
+ return path.join(os.homedir(), ".aicli", "models");
12
+ }
13
+ async function getEmbedder() {
14
+ if (pipelinePromise) return pipelinePromise;
15
+ pipelinePromise = (async () => {
16
+ const mod = await import("@huggingface/transformers");
17
+ const dir = cacheDir();
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ mod.env.cacheDir = dir;
20
+ mod.env.allowRemoteModels = true;
21
+ mod.env.allowLocalModels = true;
22
+ const pipe = await mod.pipeline("feature-extraction", EMBEDDING_MODEL_ID, {
23
+ // Keep the ONNX session in float32; int8 quantization exists but the
24
+ // quality drop on short code identifiers is noticeable.
25
+ dtype: "fp32"
26
+ });
27
+ return pipe;
28
+ })();
29
+ return pipelinePromise;
30
+ }
31
+ async function embed(texts) {
32
+ if (texts.length === 0) return [];
33
+ const pipe = await getEmbedder();
34
+ const out = await pipe(texts, { pooling: "mean", normalize: true });
35
+ const batch = texts.length;
36
+ const dim = EMBEDDING_DIM;
37
+ const rows = new Array(batch);
38
+ for (let i = 0; i < batch; i++) {
39
+ rows[i] = new Float32Array(out.data.buffer, out.data.byteOffset + i * dim * 4, dim).slice();
40
+ }
41
+ return rows;
42
+ }
43
+ async function embedOne(text) {
44
+ const [vec] = await embed([text]);
45
+ return vec;
46
+ }
47
+
48
+ export {
49
+ EMBEDDING_DIM,
50
+ embed,
51
+ embedOne
52
+ };
@@ -3,13 +3,15 @@ import {
3
3
  loadIndex
4
4
  } from "./chunk-6VRJGH25.js";
5
5
  import {
6
- EMBEDDING_DIM,
7
- embed,
8
- embedOne,
9
6
  loadVectorStore,
10
7
  saveVectorStore,
11
8
  searchVectorStore
12
- } from "./chunk-PFYAAX2S.js";
9
+ } from "./chunk-2DXY7UGF.js";
10
+ import {
11
+ EMBEDDING_DIM,
12
+ embed,
13
+ embedOne
14
+ } from "./chunk-KHYD3WXE.js";
13
15
 
14
16
  // src/symbols/semantic.ts
15
17
  function pathTokens(absFile, root) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TEST_TIMEOUT
4
- } from "./chunk-BAOIXQHD.js";
4
+ } from "./chunk-E7YC4GWV.js";
5
5
 
6
6
  // src/tools/builtin/run-tests.ts
7
7
  import { execSync } from "child_process";