jinzd-ai-cli 0.4.233 → 0.4.235

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.
Files changed (37) hide show
  1. package/dist/{auth-FSTAKSYF.js → auth-PE3Z5OHS.js} +1 -1
  2. package/dist/{batch-WWNPYQ6G.js → batch-Y7MHXVR7.js} +2 -2
  3. package/dist/{chat-index-O56HDGFI.js → chat-index-7HXBWQFH.js} +2 -2
  4. package/dist/{chat-index-WSI7ULRQ.js → chat-index-WYXYD7YP.js} +1 -1
  5. package/dist/{chunk-4KM53GLG.js → chunk-2QQKS56X.js} +1 -1
  6. package/dist/{chunk-NVUCDUXE.js → chunk-5MYPIQ3Y.js} +11 -5
  7. package/dist/{chunk-NXYXFRPA.js → chunk-7D5EJBHO.js} +3 -3
  8. package/dist/{chunk-5PKNKJYP.js → chunk-BFTTW5LS.js} +1 -1
  9. package/dist/{chunk-FQSAK75P.js → chunk-CDOQNIVV.js} +3 -3
  10. package/dist/{chunk-7KS3RGZ7.js → chunk-DHNYGJYB.js} +2 -2
  11. package/dist/{chunk-HX67JAK6.js → chunk-FYJGVFXL.js} +87 -54
  12. package/dist/{chunk-ACTO5C6K.js → chunk-GDXKFWFL.js} +1 -1
  13. package/dist/{chunk-EZGZOK6E.js → chunk-GFDRC2JZ.js} +1 -1
  14. package/dist/{chunk-4OAV7QMB.js → chunk-HHYMSEWS.js} +1 -1
  15. package/dist/{chunk-ZWU7FWHO.js → chunk-JMP3LJC2.js} +38 -91
  16. package/dist/chunk-KCEO2XJ4.js +420 -0
  17. package/dist/{chunk-M4FW2NPU.js → chunk-Q5GVANMH.js} +1 -1
  18. package/dist/{chunk-JVKAL5Q3.js → chunk-RYQBBEMM.js} +33 -3
  19. package/dist/{chunk-LF3OUJHK.js → chunk-UO3XWZFE.js} +302 -4
  20. package/dist/{chunk-TJK7AU65.js → chunk-WXFQ6LYN.js} +1 -1
  21. package/dist/{ci-V5V3JSJC.js → ci-OAFNH3MA.js} +4 -4
  22. package/dist/{ci-format-XS7LKBO5.js → ci-format-4LSXJNND.js} +2 -2
  23. package/dist/{constants-UROQJXLY.js → constants-ERIAS7GV.js} +1 -1
  24. package/dist/{doctor-cli-BY3FIV2B.js → doctor-cli-VFSVWZU6.js} +4 -4
  25. package/dist/electron-server.js +235 -224
  26. package/dist/{hub-L2QCRI5M.js → hub-X4EUYRIN.js} +2 -2
  27. package/dist/index.js +23 -23
  28. package/dist/{persist-I3JI27OK.js → persist-AMH5SQ4W.js} +3 -3
  29. package/dist/{pr-IJRUPR5C.js → pr-2CZN5JLE.js} +4 -4
  30. package/dist/{run-tests-FLCVJ6TK.js → run-tests-DXRJDPFF.js} +2 -2
  31. package/dist/{run-tests-NNXBS4EL.js → run-tests-YSRET52H.js} +1 -1
  32. package/dist/{server-W2X3C6GJ.js → server-5ZAXMKRI.js} +21 -20
  33. package/dist/{server-E6LZB72O.js → server-FGJGXHZD.js} +6 -6
  34. package/dist/{task-orchestrator-W55VKYTC.js → task-orchestrator-QMERCD77.js} +6 -6
  35. package/dist/{usage-MZCDTJWB.js → usage-633DUDNT.js} +2 -2
  36. package/package.json +2 -4
  37. package/dist/chunk-B5TYJO7V.js +0 -123
@@ -1,13 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- redactJson
4
- } from "./chunk-B5TYJO7V.js";
3
+ listFromIndex,
4
+ readSessionMeta,
5
+ redactJson,
6
+ removeSessionFromIndex,
7
+ safeDate,
8
+ searchCandidates,
9
+ updateSessionInIndex
10
+ } from "./chunk-KCEO2XJ4.js";
5
11
  import {
6
12
  atomicWriteFileSync
7
13
  } from "./chunk-IW3Q7AE5.js";
8
14
 
9
15
  // src/session/session-manager.ts
10
- import { readFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, openSync, readSync, closeSync, statSync } from "fs";
16
+ import { readFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, statSync } from "fs";
11
17
  import { join } from "path";
12
18
  import { v4 as uuidv4 } from "uuid";
13
19
 
@@ -543,19 +549,6 @@ var Session = class _Session {
543
549
  };
544
550
 
545
551
  // src/session/session-manager.ts
546
- function safeDate(value) {
547
- const d = new Date(value);
548
- return isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
549
- }
550
- function extractJsonField(header, field) {
551
- const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, "i");
552
- const m = header.match(re);
553
- return m ? m[1] : void 0;
554
- }
555
- function extractJsonNumberField(header, field) {
556
- const m = header.match(new RegExp(`"${field}"\\s*:\\s*(\\d+)`));
557
- return m ? Number(m[1]) : void 0;
558
- }
559
552
  var SessionManager = class {
560
553
  _current = null;
561
554
  historyDir;
@@ -613,6 +606,18 @@ var SessionManager = class {
613
606
  this.lastRedactionHits = hits.length;
614
607
  atomicWriteFileSync(filePath, JSON.stringify(payload, null, 2));
615
608
  this._current.dirty = false;
609
+ try {
610
+ updateSessionInIndex(this.historyDir, {
611
+ id: this._current.id,
612
+ provider: this._current.provider,
613
+ model: this._current.model,
614
+ title: this._current.title,
615
+ messageCount: this._current.messages.length,
616
+ created: this._current.created,
617
+ updated: this._current.updated
618
+ }, filePath);
619
+ } catch {
620
+ }
616
621
  }
617
622
  loadSession(id) {
618
623
  const filePath = join(this.historyDir, `${id}.json`);
@@ -631,12 +636,16 @@ var SessionManager = class {
631
636
  }
632
637
  listSessions(opts) {
633
638
  if (!existsSync(this.historyDir)) return [];
639
+ try {
640
+ return listFromIndex(this.historyDir, opts);
641
+ } catch {
642
+ }
634
643
  const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json"));
635
644
  const metas = [];
636
645
  for (const file of files) {
637
646
  try {
638
647
  const filePath = join(this.historyDir, file);
639
- const meta = this.readSessionMeta(filePath);
648
+ const meta = readSessionMeta(filePath);
640
649
  if (!meta) continue;
641
650
  if (opts?.includeFileSize) {
642
651
  try {
@@ -654,78 +663,6 @@ var SessionManager = class {
654
663
  }
655
664
  return metas.sort((a, b) => b.updated.getTime() - a.updated.getTime());
656
665
  }
657
- /**
658
- * P1-B: Read only the first ~1KB of a session file to extract metadata fields.
659
- * Session JSON format puts id/provider/model/created/updated/title before the
660
- * large "messages" array, so a small header read suffices for metadata extraction.
661
- * Falls back to full file read if header parsing fails.
662
- */
663
- readSessionMeta(filePath) {
664
- const HEADER_SIZE = 1024;
665
- let header;
666
- try {
667
- const fd = openSync(filePath, "r");
668
- const buf = Buffer.alloc(HEADER_SIZE);
669
- const bytesRead = readSync(fd, buf, 0, HEADER_SIZE, 0);
670
- closeSync(fd);
671
- header = buf.toString("utf-8", 0, bytesRead);
672
- } catch {
673
- return null;
674
- }
675
- const id = extractJsonField(header, "id");
676
- const provider = extractJsonField(header, "provider");
677
- const model = extractJsonField(header, "model");
678
- const created = extractJsonField(header, "created");
679
- const updated = extractJsonField(header, "updated");
680
- const title = extractJsonField(header, "title");
681
- if (id && provider && model) {
682
- let messageCount = extractJsonNumberField(header, "messageCount");
683
- if (messageCount === void 0) {
684
- messageCount = 0;
685
- try {
686
- const full = readFileSync(filePath, "utf-8");
687
- const matches = full.match(/"role"\s*:/g);
688
- messageCount = matches ? matches.length : 0;
689
- } catch {
690
- }
691
- }
692
- let branchCount;
693
- let activeBranch;
694
- try {
695
- const brMatch = header.match(/"branches"\s*:\s*\[/);
696
- if (brMatch) {
697
- const brIds = header.match(/"id"\s*:\s*"[^"]*"/g);
698
- const parentMatches = header.match(/"parentBranchId"/g);
699
- branchCount = parentMatches ? parentMatches.length + 1 : 1;
700
- }
701
- activeBranch = extractJsonField(header, "activeBranchId");
702
- } catch {
703
- }
704
- return {
705
- id,
706
- provider,
707
- model,
708
- messageCount,
709
- created: safeDate(created),
710
- updated: safeDate(updated),
711
- title: title || void 0,
712
- branchCount,
713
- activeBranch
714
- };
715
- }
716
- const data = JSON.parse(readFileSync(filePath, "utf-8"));
717
- return {
718
- id: data.id,
719
- provider: data.provider,
720
- model: data.model,
721
- messageCount: data.messages?.length ?? 0,
722
- created: safeDate(data.created),
723
- updated: safeDate(data.updated),
724
- title: data.title,
725
- branchCount: data.branches?.length ?? 1,
726
- activeBranch: data.activeBranchId
727
- };
728
- }
729
666
  deleteSession(id) {
730
667
  const filePath = join(this.historyDir, `${id}.json`);
731
668
  if (!existsSync(filePath)) return false;
@@ -734,6 +671,10 @@ var SessionManager = class {
734
671
  if (this._current && this._current.id === id) {
735
672
  this._current = null;
736
673
  }
674
+ try {
675
+ removeSessionFromIndex(this.historyDir, id);
676
+ } catch {
677
+ }
737
678
  return true;
738
679
  } catch {
739
680
  return false;
@@ -767,9 +708,15 @@ var SessionManager = class {
767
708
  searchMessages(query, maxResults = 20) {
768
709
  if (!existsSync(this.historyDir)) return [];
769
710
  const q = query.toLowerCase();
770
- const files = readdirSync(this.historyDir).filter((f) => f.endsWith(".json")).map((f) => join(this.historyDir, f));
711
+ const candidateIds = searchCandidates(this.historyDir, q);
712
+ let filesToSearch;
713
+ if (candidateIds !== null) {
714
+ filesToSearch = candidateIds.map((id) => join(this.historyDir, `${id}.json`)).filter((fp) => existsSync(fp));
715
+ } else {
716
+ filesToSearch = readdirSync(this.historyDir).filter((f) => f.endsWith(".json")).map((f) => join(this.historyDir, f));
717
+ }
771
718
  const results = [];
772
- for (const filePath of files) {
719
+ for (const filePath of filesToSearch) {
773
720
  if (results.length >= maxResults) break;
774
721
  try {
775
722
  const data = JSON.parse(readFileSync(filePath, "utf-8"));
@@ -0,0 +1,420 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ atomicWriteFileSync
4
+ } from "./chunk-IW3Q7AE5.js";
5
+
6
+ // src/security/redactor.ts
7
+ var DEFAULT_PATTERNS = [
8
+ // password: xxx / password = xxx / password="xxx"
9
+ // Covers YAML / JSON / shell-ish / env-file forms.
10
+ { kind: "password", regex: /\b(password|passwd|pwd)\s*[:=]\s*["']?([^\s"',;{}]{4,200})["']?/gi },
11
+ // PGPASSWORD=xxx (explicit bash env-var form, separate rule because no quotes usually)
12
+ { kind: "pgpassword-env", regex: /\b(PGPASSWORD)=([^\s"']{4,200})/g },
13
+ // JDBC/PG/MySQL/Mongo connection strings with inline credentials
14
+ // postgresql://user:pass@host/db → redact pass
15
+ { kind: "db-uri-password", regex: /(\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^:\s]+:)([^@\s]+)(@)/gi },
16
+ // Anthropic API keys
17
+ { kind: "anthropic-key", regex: /(sk-ant-[a-zA-Z0-9_-]{90,})/g },
18
+ // L6 (v0.4.108): Zhipu / GLM API keys — `<24+ hex/base64-ish>.<32+>`
19
+ // Two segments separated by a dot, each safely identifiable by length
20
+ // and char class. Conservative on the lower bound so we don't eat
21
+ // version strings like `1.0.0` or filenames.
22
+ { kind: "zhipu-key", regex: /\b([a-zA-Z0-9]{24,}\.[a-zA-Z0-9]{32,})\b/g },
23
+ // OpenAI / generic sk- keys — requires length ≥32 to avoid eating short identifiers
24
+ { kind: "openai-key", regex: /(sk-(?:proj-)?[a-zA-Z0-9_-]{32,})/g },
25
+ // GitHub personal access tokens
26
+ { kind: "github-pat", regex: /\b(ghp_[a-zA-Z0-9]{36})\b/g },
27
+ { kind: "github-oauth", regex: /\b(gho_[a-zA-Z0-9]{36})\b/g },
28
+ { kind: "github-install", regex: /\b(ghs_[a-zA-Z0-9]{36})\b/g },
29
+ // Slack tokens
30
+ { kind: "slack-bot", regex: /\b(xoxb-\d+-\d+-[a-zA-Z0-9]+)\b/g },
31
+ { kind: "slack-user", regex: /\b(xoxp-\d+-\d+-\d+-[a-zA-Z0-9]+)\b/g },
32
+ // AWS access key IDs (AKIA...) and secret access keys are context-dependent;
33
+ // we only catch the ID because secret key alone is indistinguishable from random base64.
34
+ { kind: "aws-access-key-id", regex: /\b(AKIA[0-9A-Z]{16})\b/g },
35
+ // Google API keys
36
+ { kind: "google-api-key", regex: /\b(AIza[0-9A-Za-z_-]{35})\b/g },
37
+ // Generic "api_key": "..." / "apiKey": "..." / api-key=xxx
38
+ { kind: "api-key", regex: /\b(api[_-]?key)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{16,200})["']?/gi },
39
+ // Generic token: xxx (only when value looks token-shaped; avoids eating human prose)
40
+ { kind: "token", regex: /\b(token|access[_-]?token|bearer[_-]?token)\s*[:=]\s*["']?([a-zA-Z0-9_\-.]{20,300})["']?/gi },
41
+ // Bearer <token> in Authorization headers
42
+ { kind: "bearer", regex: /\b(Authorization:\s*Bearer\s+)([a-zA-Z0-9_\-.=]{20,500})/g },
43
+ // Private key PEM blocks — catch the header+footer together
44
+ // P2-PERF-02: PEM bodies use strict Base64 + =\n — narrowed char class avoids backtracking
45
+ { kind: "private-key", regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[ \t\n\r]*[A-Za-z0-9+/=\n\r-]*[ \t\n\r]*-----END [A-Z ]*PRIVATE KEY-----/g }
46
+ ];
47
+ var MAX_CUSTOM = 32;
48
+ var MAX_PATTERN_LEN = 500;
49
+ var SUSPICIOUS_REDOS = /\([^)]*[+*][^)]*\)\s*[+*{]/;
50
+ var MAX_REDACT_INPUT = 512e3;
51
+ function render(placeholder, kind) {
52
+ return placeholder.replace("{kind}", kind);
53
+ }
54
+ function redactString(input, options) {
55
+ if (!options.enabled || !input) return { redacted: input, hits: [] };
56
+ if (input.length > MAX_REDACT_INPUT) return { redacted: input, hits: [] };
57
+ const placeholder = options.placeholder ?? "[REDACTED:{kind}]";
58
+ const customSrcs = (options.customRegexes ?? []).slice(0, MAX_CUSTOM);
59
+ const patterns = [
60
+ ...options.patterns ?? DEFAULT_PATTERNS,
61
+ ...customSrcs.flatMap((src, i) => {
62
+ if (typeof src !== "string" || src.length === 0 || src.length > MAX_PATTERN_LEN) return [];
63
+ try {
64
+ const flags = src.match(/^\/.*\/([gimsuy]*)$/)?.[1] ?? "";
65
+ const body = src.replace(/^\/(.*)\/[gimsuy]*$/, "$1");
66
+ if (SUSPICIOUS_REDOS.test(body)) return [];
67
+ const regex = new RegExp(body, flags.includes("g") ? flags : flags + "g");
68
+ return [{ kind: `custom-${i}`, regex }];
69
+ } catch {
70
+ return [];
71
+ }
72
+ })
73
+ ];
74
+ let redacted = input;
75
+ const hits = [];
76
+ for (const { kind, regex } of patterns) {
77
+ const rx = new RegExp(regex.source, regex.flags);
78
+ const captureCount = new RegExp(rx.source + "|").exec("").length - 1;
79
+ redacted = redacted.replace(rx, (...args) => {
80
+ const match = args[0];
81
+ const g1 = captureCount >= 1 ? args[1] : void 0;
82
+ const g2 = captureCount >= 2 ? args[2] : void 0;
83
+ const offset = args[1 + captureCount];
84
+ if (captureCount >= 2 && typeof g2 === "string") {
85
+ hits.push({ kind, start: offset + (g1?.length ?? 0), length: g2.length, secret: g2 });
86
+ return `${g1}${render(placeholder, kind)}`;
87
+ }
88
+ hits.push({ kind, start: offset, length: match.length, secret: g1 ?? match });
89
+ return render(placeholder, kind);
90
+ });
91
+ }
92
+ return { redacted, hits };
93
+ }
94
+ function redactJson(value, options) {
95
+ if (!options.enabled) return { value, hits: [] };
96
+ const allHits = [];
97
+ function walk(v) {
98
+ if (typeof v === "string") {
99
+ const r = redactString(v, options);
100
+ allHits.push(...r.hits);
101
+ return r.redacted;
102
+ }
103
+ if (Array.isArray(v)) return v.map(walk);
104
+ if (v && typeof v === "object") {
105
+ const out = {};
106
+ for (const [k, vv] of Object.entries(v)) {
107
+ out[k] = walk(vv);
108
+ }
109
+ return out;
110
+ }
111
+ return v;
112
+ }
113
+ const redacted = walk(value);
114
+ return { value: redacted, hits: allHits };
115
+ }
116
+ function scanString(input, options) {
117
+ const { hits } = redactString(input, { ...options, enabled: true });
118
+ return hits;
119
+ }
120
+
121
+ // src/session/session-index.ts
122
+ import { existsSync, readFileSync as readFileSync2, readdirSync, statSync } from "fs";
123
+ import { join } from "path";
124
+
125
+ // src/session/session-meta.ts
126
+ import { readFileSync, openSync, readSync, closeSync } from "fs";
127
+ function safeDate(value) {
128
+ const d = new Date(value);
129
+ return isNaN(d.getTime()) ? /* @__PURE__ */ new Date(0) : d;
130
+ }
131
+ function extractJsonField(header, field) {
132
+ const re = new RegExp(`"${field}"\\s*:\\s*"([^"]*)"`, "i");
133
+ const m = header.match(re);
134
+ return m ? m[1] : void 0;
135
+ }
136
+ function extractJsonNumberField(header, field) {
137
+ const m = header.match(new RegExp(`"${field}"\\s*:\\s*(\\d+)`));
138
+ return m ? Number(m[1]) : void 0;
139
+ }
140
+ function readSessionMeta(filePath) {
141
+ const HEADER_SIZE = 1024;
142
+ let header;
143
+ try {
144
+ const fd = openSync(filePath, "r");
145
+ const buf = Buffer.alloc(HEADER_SIZE);
146
+ const bytesRead = readSync(fd, buf, 0, HEADER_SIZE, 0);
147
+ closeSync(fd);
148
+ header = buf.toString("utf-8", 0, bytesRead);
149
+ } catch {
150
+ return null;
151
+ }
152
+ const id = extractJsonField(header, "id");
153
+ const provider = extractJsonField(header, "provider");
154
+ const model = extractJsonField(header, "model");
155
+ const created = extractJsonField(header, "created");
156
+ const updated = extractJsonField(header, "updated");
157
+ const title = extractJsonField(header, "title");
158
+ if (id && provider && model) {
159
+ let messageCount = extractJsonNumberField(header, "messageCount");
160
+ if (messageCount === void 0) {
161
+ messageCount = 0;
162
+ try {
163
+ const full = readFileSync(filePath, "utf-8");
164
+ const matches = full.match(/"role"\s*:/g);
165
+ messageCount = matches ? matches.length : 0;
166
+ } catch {
167
+ }
168
+ }
169
+ let branchCount;
170
+ let activeBranch;
171
+ try {
172
+ const brMatch = header.match(/"branches"\s*:\s*\[/);
173
+ if (brMatch) {
174
+ const parentMatches = header.match(/"parentBranchId"/g);
175
+ branchCount = parentMatches ? parentMatches.length + 1 : 1;
176
+ }
177
+ activeBranch = extractJsonField(header, "activeBranchId");
178
+ } catch {
179
+ }
180
+ return {
181
+ id,
182
+ provider,
183
+ model,
184
+ messageCount,
185
+ created: safeDate(created),
186
+ updated: safeDate(updated),
187
+ title: title || void 0,
188
+ branchCount,
189
+ activeBranch
190
+ };
191
+ }
192
+ const data = JSON.parse(readFileSync(filePath, "utf-8"));
193
+ return {
194
+ id: data.id,
195
+ provider: data.provider,
196
+ model: data.model,
197
+ messageCount: data.messages?.length ?? 0,
198
+ created: safeDate(data.created),
199
+ updated: safeDate(data.updated),
200
+ title: data.title,
201
+ branchCount: data.branches?.length ?? 1,
202
+ activeBranch: data.activeBranchId
203
+ };
204
+ }
205
+
206
+ // src/session/session-index.ts
207
+ var INDEX_FILE = "sessions.index.json";
208
+ function indexFilePath(historyDir) {
209
+ return join(historyDir, INDEX_FILE);
210
+ }
211
+ function loadIndex(historyDir) {
212
+ const fp = indexFilePath(historyDir);
213
+ if (!existsSync(fp)) return null;
214
+ try {
215
+ const data = JSON.parse(readFileSync2(fp, "utf-8"));
216
+ if (data?.version === 1 && data.sessions && data.invertedIndex) {
217
+ return data;
218
+ }
219
+ return null;
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+ function saveIndex(historyDir, idx) {
225
+ idx.built = (/* @__PURE__ */ new Date()).toISOString();
226
+ const fp = indexFilePath(historyDir);
227
+ atomicWriteFileSync(fp, JSON.stringify(idx));
228
+ }
229
+ function rebuildIndex(historyDir) {
230
+ if (!existsSync(historyDir)) {
231
+ const empty = { version: 1, built: (/* @__PURE__ */ new Date()).toISOString(), sessions: {}, invertedIndex: {} };
232
+ return empty;
233
+ }
234
+ const files = readdirSync(historyDir).filter((f) => f.endsWith(".json"));
235
+ const sessions = {};
236
+ const invertedIndex = /* @__PURE__ */ new Map();
237
+ for (const file of files) {
238
+ const filePath = join(historyDir, file);
239
+ const meta = readSessionMeta(filePath);
240
+ if (!meta) continue;
241
+ let fileSize = 0;
242
+ try {
243
+ fileSize = statSync(filePath).size;
244
+ } catch {
245
+ }
246
+ let mtimeMs = 0;
247
+ try {
248
+ mtimeMs = statSync(filePath).mtimeMs;
249
+ } catch {
250
+ }
251
+ const entry = {
252
+ id: meta.id,
253
+ provider: meta.provider,
254
+ model: meta.model,
255
+ title: meta.title,
256
+ messageCount: meta.messageCount,
257
+ fileSize,
258
+ created: meta.created.toISOString(),
259
+ updated: meta.updated.toISOString(),
260
+ branchCount: meta.branchCount,
261
+ activeBranch: meta.activeBranch,
262
+ _mtimeMs: mtimeMs
263
+ };
264
+ sessions[meta.id] = entry;
265
+ addTokensToIndex(invertedIndex, meta.title ?? "", meta.id);
266
+ }
267
+ const serializedIndex = {};
268
+ for (const [token, ids] of invertedIndex) {
269
+ serializedIndex[token] = [...ids];
270
+ }
271
+ const idx = {
272
+ version: 1,
273
+ built: (/* @__PURE__ */ new Date()).toISOString(),
274
+ sessions,
275
+ invertedIndex: serializedIndex
276
+ };
277
+ saveIndex(historyDir, idx);
278
+ return idx;
279
+ }
280
+ function updateSessionInIndex(historyDir, meta, filePath) {
281
+ let idx = loadIndex(historyDir);
282
+ if (!idx) {
283
+ rebuildIndex(historyDir);
284
+ return;
285
+ }
286
+ let fileSize = 0;
287
+ try {
288
+ fileSize = statSync(filePath).size;
289
+ } catch {
290
+ }
291
+ let mtimeMs = 0;
292
+ try {
293
+ mtimeMs = statSync(filePath).mtimeMs;
294
+ } catch {
295
+ }
296
+ const id = meta.id;
297
+ for (const [token, ids] of Object.entries(idx.invertedIndex)) {
298
+ const filtered = ids.filter((sid) => sid !== id);
299
+ if (filtered.length === 0) {
300
+ delete idx.invertedIndex[token];
301
+ } else {
302
+ idx.invertedIndex[token] = filtered;
303
+ }
304
+ }
305
+ idx.sessions[id] = {
306
+ id,
307
+ provider: meta.provider,
308
+ model: meta.model,
309
+ title: meta.title,
310
+ messageCount: meta.messageCount,
311
+ fileSize,
312
+ created: meta.created.toISOString(),
313
+ updated: meta.updated.toISOString(),
314
+ _mtimeMs: mtimeMs
315
+ };
316
+ const tokens = tokenize(meta.title ?? "");
317
+ for (const token of tokens) {
318
+ const existing = idx.invertedIndex[token] ?? [];
319
+ if (!existing.includes(id)) {
320
+ existing.push(id);
321
+ idx.invertedIndex[token] = existing;
322
+ }
323
+ }
324
+ saveIndex(historyDir, idx);
325
+ }
326
+ function removeSessionFromIndex(historyDir, id) {
327
+ const idx = loadIndex(historyDir);
328
+ if (!idx) return;
329
+ delete idx.sessions[id];
330
+ for (const [token, ids] of Object.entries(idx.invertedIndex)) {
331
+ const filtered = ids.filter((sid) => sid !== id);
332
+ if (filtered.length === 0) {
333
+ delete idx.invertedIndex[token];
334
+ } else {
335
+ idx.invertedIndex[token] = filtered;
336
+ }
337
+ }
338
+ saveIndex(historyDir, idx);
339
+ }
340
+ function listFromIndex(historyDir, opts) {
341
+ let idx = loadIndex(historyDir);
342
+ if (!idx) {
343
+ idx = rebuildIndex(historyDir);
344
+ }
345
+ return Object.values(idx.sessions).map((entry) => ({
346
+ id: entry.id,
347
+ provider: entry.provider,
348
+ model: entry.model,
349
+ title: entry.title,
350
+ messageCount: entry.messageCount,
351
+ created: new Date(entry.created),
352
+ updated: new Date(entry.updated),
353
+ fileSize: opts?.includeFileSize ? entry.fileSize : void 0,
354
+ branchCount: entry.branchCount,
355
+ activeBranch: entry.activeBranch
356
+ })).sort((a, b) => b.updated.getTime() - a.updated.getTime());
357
+ }
358
+ function searchCandidates(historyDir, query) {
359
+ const idx = loadIndex(historyDir);
360
+ if (!idx) return null;
361
+ const queryTokens = tokenize(query.toLowerCase());
362
+ if (queryTokens.length === 0) return null;
363
+ const scoreMap = /* @__PURE__ */ new Map();
364
+ for (const token of queryTokens) {
365
+ const matchingIds = idx.invertedIndex[token];
366
+ if (!matchingIds) continue;
367
+ for (const sid of matchingIds) {
368
+ scoreMap.set(sid, (scoreMap.get(sid) ?? 0) + 1);
369
+ }
370
+ }
371
+ const candidates = [...scoreMap.entries()].filter(([, score]) => score > 0).sort((a, b) => {
372
+ const scoreDiff = b[1] - a[1];
373
+ if (scoreDiff !== 0) return scoreDiff;
374
+ const aMeta = idx.sessions[a[0]];
375
+ const bMeta = idx.sessions[b[0]];
376
+ return (bMeta?.updated ?? "").localeCompare(aMeta?.updated ?? "");
377
+ }).map(([sid]) => sid);
378
+ return candidates.length > 0 ? candidates : null;
379
+ }
380
+ function getIndexMtimeMap(historyDir) {
381
+ const idx = loadIndex(historyDir);
382
+ if (!idx) return null;
383
+ const map = {};
384
+ for (const entry of Object.values(idx.sessions)) {
385
+ map[entry.id] = entry._mtimeMs;
386
+ }
387
+ return map;
388
+ }
389
+ function addTokensToIndex(invertedIndex, text, sessionId) {
390
+ for (const token of tokenize(text)) {
391
+ const s = invertedIndex.get(token) ?? /* @__PURE__ */ new Set();
392
+ s.add(sessionId);
393
+ invertedIndex.set(token, s);
394
+ }
395
+ }
396
+ function tokenize(text) {
397
+ const tokens = /* @__PURE__ */ new Set();
398
+ const cjkOnly = text.replace(/[^\u4e00-\u9fff\u3400-\u4dbf]/g, "");
399
+ for (let i = 0; i < cjkOnly.length - 1; i++) {
400
+ tokens.add(cjkOnly.slice(i, i + 2));
401
+ }
402
+ const english = text.replace(/[\u4e00-\u9fff\u3400-\u4dbf]/g, " ").toLowerCase();
403
+ const words = english.split(/[^a-z0-9]+/).filter((w) => w.length >= 2);
404
+ for (const w of words) tokens.add(w);
405
+ return [...tokens];
406
+ }
407
+
408
+ export {
409
+ DEFAULT_PATTERNS,
410
+ redactString,
411
+ redactJson,
412
+ scanString,
413
+ safeDate,
414
+ readSessionMeta,
415
+ updateSessionInIndex,
416
+ removeSessionFromIndex,
417
+ listFromIndex,
418
+ searchCandidates,
419
+ getIndexMtimeMap
420
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  TEST_TIMEOUT
4
- } from "./chunk-ACTO5C6K.js";
4
+ } from "./chunk-GDXKFWFL.js";
5
5
 
6
6
  // src/tools/builtin/run-tests.ts
7
7
  import { execSync, spawnSync } from "child_process";
@@ -5,7 +5,7 @@ import {
5
5
 
6
6
  // src/web/auth.ts
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync } from "fs";
8
- import { join } from "path";
8
+ import { isAbsolute, join, relative } from "path";
9
9
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
10
10
  var USERS_FILE = "users.json";
11
11
  var TOKEN_EXPIRY_HOURS = 24;
@@ -241,8 +241,17 @@ var AuthManager = class {
241
241
  loadOrCreate() {
242
242
  if (existsSync(this.usersFile)) {
243
243
  try {
244
- return JSON.parse(readFileSync(this.usersFile, "utf-8"));
245
- } catch {
244
+ const parsed = JSON.parse(readFileSync(this.usersFile, "utf-8"));
245
+ if (!this.isValidUsersDB(parsed)) {
246
+ throw new Error("database structure is invalid");
247
+ }
248
+ return parsed;
249
+ } catch (err) {
250
+ const detail = err instanceof Error ? err.message : String(err);
251
+ throw new Error(
252
+ `Authentication database is unreadable or corrupt: ${this.usersFile} (${detail}). Refusing to start without authentication; restore or remove the file explicitly.`,
253
+ { cause: err }
254
+ );
246
255
  }
247
256
  }
248
257
  const db = {
@@ -253,6 +262,27 @@ var AuthManager = class {
253
262
  this.saveDB(db);
254
263
  return db;
255
264
  }
265
+ isValidUsersDB(value) {
266
+ if (!value || typeof value !== "object") return false;
267
+ const db = value;
268
+ if (db.version !== 1 || typeof db.secret !== "string" || db.secret.length < 32 || !Array.isArray(db.users)) {
269
+ return false;
270
+ }
271
+ const usernames = /* @__PURE__ */ new Set();
272
+ for (const user of db.users) {
273
+ if (!user || typeof user !== "object") return false;
274
+ const record = user;
275
+ if (typeof record.username !== "string" || !/^[a-z0-9_-]{2,32}$/.test(record.username) || usernames.has(record.username) || typeof record.passwordHash !== "string" || record.passwordHash.length === 0 || typeof record.salt !== "string" || record.salt.length === 0 || typeof record.createdAt !== "string" || typeof record.dataDir !== "string" || record.dataDir.length === 0 || isAbsolute(record.dataDir)) {
276
+ return false;
277
+ }
278
+ const dataDirFromBase = relative(this.baseDir, join(this.baseDir, record.dataDir));
279
+ if (dataDirFromBase.startsWith("..") || isAbsolute(dataDirFromBase)) return false;
280
+ if (record.hashVersion !== void 0 && record.hashVersion !== 1 && record.hashVersion !== 2) return false;
281
+ if (record.tokensRevokedBefore !== void 0 && !Number.isFinite(record.tokensRevokedBefore)) return false;
282
+ usernames.add(record.username);
283
+ }
284
+ return true;
285
+ }
256
286
  save() {
257
287
  this.saveDB(this.db);
258
288
  }