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.
- package/dist/{auth-FSTAKSYF.js → auth-PE3Z5OHS.js} +1 -1
- package/dist/{batch-WWNPYQ6G.js → batch-Y7MHXVR7.js} +2 -2
- package/dist/{chat-index-O56HDGFI.js → chat-index-7HXBWQFH.js} +2 -2
- package/dist/{chat-index-WSI7ULRQ.js → chat-index-WYXYD7YP.js} +1 -1
- package/dist/{chunk-4KM53GLG.js → chunk-2QQKS56X.js} +1 -1
- package/dist/{chunk-NVUCDUXE.js → chunk-5MYPIQ3Y.js} +11 -5
- package/dist/{chunk-NXYXFRPA.js → chunk-7D5EJBHO.js} +3 -3
- package/dist/{chunk-5PKNKJYP.js → chunk-BFTTW5LS.js} +1 -1
- package/dist/{chunk-FQSAK75P.js → chunk-CDOQNIVV.js} +3 -3
- package/dist/{chunk-7KS3RGZ7.js → chunk-DHNYGJYB.js} +2 -2
- package/dist/{chunk-HX67JAK6.js → chunk-FYJGVFXL.js} +87 -54
- package/dist/{chunk-ACTO5C6K.js → chunk-GDXKFWFL.js} +1 -1
- package/dist/{chunk-EZGZOK6E.js → chunk-GFDRC2JZ.js} +1 -1
- package/dist/{chunk-4OAV7QMB.js → chunk-HHYMSEWS.js} +1 -1
- package/dist/{chunk-ZWU7FWHO.js → chunk-JMP3LJC2.js} +38 -91
- package/dist/chunk-KCEO2XJ4.js +420 -0
- package/dist/{chunk-M4FW2NPU.js → chunk-Q5GVANMH.js} +1 -1
- package/dist/{chunk-JVKAL5Q3.js → chunk-RYQBBEMM.js} +33 -3
- package/dist/{chunk-LF3OUJHK.js → chunk-UO3XWZFE.js} +302 -4
- package/dist/{chunk-TJK7AU65.js → chunk-WXFQ6LYN.js} +1 -1
- package/dist/{ci-V5V3JSJC.js → ci-OAFNH3MA.js} +4 -4
- package/dist/{ci-format-XS7LKBO5.js → ci-format-4LSXJNND.js} +2 -2
- package/dist/{constants-UROQJXLY.js → constants-ERIAS7GV.js} +1 -1
- package/dist/{doctor-cli-BY3FIV2B.js → doctor-cli-VFSVWZU6.js} +4 -4
- package/dist/electron-server.js +235 -224
- package/dist/{hub-L2QCRI5M.js → hub-X4EUYRIN.js} +2 -2
- package/dist/index.js +23 -23
- package/dist/{persist-I3JI27OK.js → persist-AMH5SQ4W.js} +3 -3
- package/dist/{pr-IJRUPR5C.js → pr-2CZN5JLE.js} +4 -4
- package/dist/{run-tests-FLCVJ6TK.js → run-tests-DXRJDPFF.js} +2 -2
- package/dist/{run-tests-NNXBS4EL.js → run-tests-YSRET52H.js} +1 -1
- package/dist/{server-W2X3C6GJ.js → server-5ZAXMKRI.js} +21 -20
- package/dist/{server-E6LZB72O.js → server-FGJGXHZD.js} +6 -6
- package/dist/{task-orchestrator-W55VKYTC.js → task-orchestrator-QMERCD77.js} +6 -6
- package/dist/{usage-MZCDTJWB.js → usage-633DUDNT.js} +2 -2
- package/package.json +2 -4
- package/dist/chunk-B5TYJO7V.js +0 -123
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
|
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
|
+
};
|
|
@@ -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
|
-
|
|
245
|
-
|
|
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
|
}
|