reasonix 0.41.0 → 0.44.0-rc.5
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/README.md +73 -12
- package/README.zh-CN.md +35 -7
- package/dashboard/app.css +225 -4
- package/dashboard/dist/app.js +6538 -6082
- package/dashboard/dist/app.js.map +1 -1
- package/data/deepseek-tokenizer.json.gz +0 -0
- package/dist/cli/{acp-64VQZLDJ.js → acp-TYZ2CTDL.js} +40 -37
- package/dist/cli/acp-TYZ2CTDL.js.map +1 -0
- package/dist/cli/chat-TH7VNNCJ.js +51 -0
- package/dist/cli/{chunk-E46ECXJD.js → chunk-2425HK6U.js} +2 -1
- package/dist/cli/{chunk-E46ECXJD.js.map → chunk-2425HK6U.js.map} +1 -1
- package/dist/cli/chunk-25T6CVUP.js +172 -0
- package/dist/cli/chunk-25T6CVUP.js.map +1 -0
- package/dist/cli/{chunk-3Q3C4W66.js → chunk-2UQP6H6T.js} +2 -1
- package/dist/cli/{chunk-3Q3C4W66.js.map → chunk-2UQP6H6T.js.map} +1 -1
- package/dist/cli/{chunk-SXLJBFIV.js → chunk-2V6EAEUW.js} +99 -31
- package/dist/cli/chunk-2V6EAEUW.js.map +1 -0
- package/dist/cli/{chunk-4H3ZRJ2U.js → chunk-4CTDEJUF.js} +4 -3
- package/dist/cli/{chunk-4H3ZRJ2U.js.map → chunk-4CTDEJUF.js.map} +1 -1
- package/dist/cli/{chunk-7VFNPMKG.js → chunk-4QUNBQQ2.js} +3 -2
- package/dist/cli/{chunk-7VFNPMKG.js.map → chunk-4QUNBQQ2.js.map} +1 -1
- package/dist/cli/chunk-5QCB62C4.js +25319 -0
- package/dist/cli/chunk-5QCB62C4.js.map +1 -0
- package/dist/cli/chunk-6OWJV3YW.js +390 -0
- package/dist/cli/chunk-6OWJV3YW.js.map +1 -0
- package/dist/cli/{chunk-YJFKFTAL.js → chunk-6PBZN4VI.js} +15 -3
- package/dist/cli/chunk-6PBZN4VI.js.map +1 -0
- package/dist/cli/{chunk-WE3YZULK.js → chunk-7CIGMZT3.js} +3 -2
- package/dist/cli/{chunk-WE3YZULK.js.map → chunk-7CIGMZT3.js.map} +1 -1
- package/dist/cli/{chunk-LTXADNCO.js → chunk-7UCMM425.js} +8 -3
- package/dist/cli/chunk-7UCMM425.js.map +1 -0
- package/dist/cli/{chunk-UV7XJUJH.js → chunk-AB2RED3C.js} +16 -9
- package/dist/cli/{chunk-UV7XJUJH.js.map → chunk-AB2RED3C.js.map} +1 -1
- package/dist/cli/chunk-AVFXO2EZ.js +2996 -0
- package/dist/cli/chunk-AVFXO2EZ.js.map +1 -0
- package/dist/cli/{chunk-7SPOFTMT.js → chunk-C53JQES5.js} +4 -3
- package/dist/cli/{chunk-7SPOFTMT.js.map → chunk-C53JQES5.js.map} +1 -1
- package/dist/cli/{chunk-MHGPBJ2T.js → chunk-CGDR2ELH.js} +131 -34
- package/dist/cli/chunk-CGDR2ELH.js.map +1 -0
- package/dist/cli/{chunk-4DCHFFEY.js → chunk-CWZKQ5FE.js} +8 -4
- package/dist/cli/chunk-CWZKQ5FE.js.map +1 -0
- package/dist/cli/chunk-FEZK652I.js +3644 -0
- package/dist/cli/chunk-FEZK652I.js.map +1 -0
- package/dist/cli/{chunk-A7VHMMDE.js → chunk-HNXDZGC6.js} +114 -5
- package/dist/cli/chunk-HNXDZGC6.js.map +1 -0
- package/dist/cli/{chunk-FWGEHRB7.js → chunk-J5XJHLWM.js} +2 -1
- package/dist/cli/{chunk-FWGEHRB7.js.map → chunk-J5XJHLWM.js.map} +1 -1
- package/dist/cli/{chunk-Y5XNV3NX.js → chunk-JMBMLOBP.js} +2 -1
- package/dist/cli/{chunk-Y5XNV3NX.js.map → chunk-JMBMLOBP.js.map} +1 -1
- package/dist/cli/{chunk-ARF3N2SY.js → chunk-JNAQYELD.js} +17 -8
- package/dist/cli/{chunk-ARF3N2SY.js.map → chunk-JNAQYELD.js.map} +1 -1
- package/dist/cli/{chunk-AT6GGIBV.js → chunk-KGBG6M2X.js} +25 -18
- package/dist/cli/chunk-KGBG6M2X.js.map +1 -0
- package/dist/cli/{chunk-A3LL4XDV.js → chunk-KLQTAZIY.js} +69 -5
- package/dist/cli/chunk-KLQTAZIY.js.map +1 -0
- package/dist/cli/{chunk-KZYLMMU5.js → chunk-KM465GST.js} +13 -12
- package/dist/cli/{chunk-KZYLMMU5.js.map → chunk-KM465GST.js.map} +1 -1
- package/dist/cli/{chunk-2CXPDAWX.js → chunk-LIR2HBQH.js} +3 -2
- package/dist/cli/{chunk-2CXPDAWX.js.map → chunk-LIR2HBQH.js.map} +1 -1
- package/dist/cli/{chunk-RAUPWSYA.js → chunk-MJ6W5UN3.js} +3 -2
- package/dist/cli/{chunk-RAUPWSYA.js.map → chunk-MJ6W5UN3.js.map} +1 -1
- package/dist/cli/{chunk-4W2CICFQ.js → chunk-MRHHQJAQ.js} +72 -60
- package/dist/cli/chunk-MRHHQJAQ.js.map +1 -0
- package/dist/cli/{chunk-BOFL3T45.js → chunk-NVURFF27.js} +23 -11
- package/dist/cli/chunk-NVURFF27.js.map +1 -0
- package/dist/cli/chunk-OPFUUYHL.js +11645 -0
- package/dist/cli/chunk-OPFUUYHL.js.map +1 -0
- package/dist/cli/{chunk-FM57FNPJ.js → chunk-PLHAZOLZ.js} +2 -1
- package/dist/cli/{chunk-FM57FNPJ.js.map → chunk-PLHAZOLZ.js.map} +1 -1
- package/dist/cli/{chunk-ZTLZO42A.js → chunk-R3CTO2HM.js} +3 -2
- package/dist/cli/{chunk-ZTLZO42A.js.map → chunk-R3CTO2HM.js.map} +1 -1
- package/dist/cli/{chunk-BYZGO3BX.js → chunk-RDRC3XDT.js} +145 -39
- package/dist/cli/chunk-RDRC3XDT.js.map +1 -0
- package/dist/cli/chunk-S4XVGLRW.js +499 -0
- package/dist/cli/chunk-S4XVGLRW.js.map +1 -0
- package/dist/cli/{chunk-WJ3YX4PZ.js → chunk-SZ5XES2N.js} +3 -2
- package/dist/cli/{chunk-WJ3YX4PZ.js.map → chunk-SZ5XES2N.js.map} +1 -1
- package/dist/cli/{chunk-CD4SCQL4.js → chunk-TEUDEGX2.js} +64 -10
- package/dist/cli/chunk-TEUDEGX2.js.map +1 -0
- package/dist/cli/chunk-TKVXTQ3T.js +96 -0
- package/dist/cli/chunk-TKVXTQ3T.js.map +1 -0
- package/dist/cli/chunk-TUK7OWJA.js +51 -0
- package/dist/cli/chunk-TXJMRPIL.js +54 -0
- package/dist/cli/{chunk-L7W3HJZQ.js.map → chunk-TXJMRPIL.js.map} +1 -1
- package/dist/cli/{chunk-H4OLWRSX.js → chunk-V26WPN3J.js} +11 -7
- package/dist/cli/chunk-V26WPN3J.js.map +1 -0
- package/dist/cli/{chunk-VFG4GIT3.js → chunk-WK3UFQY3.js} +3 -2
- package/dist/cli/{chunk-VFG4GIT3.js.map → chunk-WK3UFQY3.js.map} +1 -1
- package/dist/cli/chunk-X53B3JIX.js +34320 -0
- package/dist/cli/chunk-X53B3JIX.js.map +1 -0
- package/dist/cli/{chunk-SOZE7V7V.js → chunk-XJXDHAES.js} +3 -2
- package/dist/cli/{chunk-SOZE7V7V.js.map → chunk-XJXDHAES.js.map} +1 -1
- package/dist/cli/{chunk-CFY2XLY6.js → chunk-XSU4QVFW.js} +7 -85
- package/dist/cli/chunk-XSU4QVFW.js.map +1 -0
- package/dist/cli/{chunk-CRPQUBP6.js → chunk-XXC2BYTV.js} +2 -1
- package/dist/cli/{chunk-CRPQUBP6.js.map → chunk-XXC2BYTV.js.map} +1 -1
- package/dist/cli/chunk-Z4S7EYXG.js +63813 -0
- package/dist/cli/chunk-Z4S7EYXG.js.map +1 -0
- package/dist/cli/chunk-ZZM6QJ4W.js +109 -0
- package/dist/cli/chunk-ZZM6QJ4W.js.map +1 -0
- package/dist/cli/{chunk-ORM6PK57.js → chunk-ZZYBBX5N.js} +14 -5
- package/dist/cli/chunk-ZZYBBX5N.js.map +1 -0
- package/dist/cli/code-PSVJ3KEN.js +166 -0
- package/dist/cli/code-PSVJ3KEN.js.map +1 -0
- package/dist/cli/{commands-QY7MSQG7.js → commands-OCU42XG4.js} +7 -5
- package/dist/cli/{commands-QY7MSQG7.js.map → commands-OCU42XG4.js.map} +1 -1
- package/dist/cli/{commit-BRCQ3OQO.js → commit-XCQIQCYG.js} +7 -4
- package/dist/cli/{commit-BRCQ3OQO.js.map → commit-XCQIQCYG.js.map} +1 -1
- package/dist/cli/{desktop-ZTMHQR2Y.js → desktop-KWGR4BNE.js} +327 -98
- package/dist/cli/desktop-KWGR4BNE.js.map +1 -0
- package/dist/cli/devtools-HW3WDT3Q.js +91 -0
- package/dist/cli/devtools-HW3WDT3Q.js.map +1 -0
- package/dist/cli/diff-NHANTNC3.js +165 -0
- package/dist/cli/{diff-YASCB7PU.js.map → diff-NHANTNC3.js.map} +1 -1
- package/dist/cli/doctor-CC5CLOGG.js +27 -0
- package/dist/cli/{events-2AJTXR7I.js → events-XEFAD5VX.js} +6 -4
- package/dist/cli/{events-2AJTXR7I.js.map → events-XEFAD5VX.js.map} +1 -1
- package/dist/cli/index.js +3266 -152
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{mcp-YMWBLRR7.js → mcp-MPVGBBJF.js} +6 -4
- package/dist/cli/{mcp-YMWBLRR7.js.map → mcp-MPVGBBJF.js.map} +1 -1
- package/dist/cli/{mcp-browse-XLDUE6SB.js → mcp-browse-4XOTC3FJ.js} +32 -21
- package/dist/cli/{mcp-browse-XLDUE6SB.js.map → mcp-browse-4XOTC3FJ.js.map} +1 -1
- package/dist/cli/{mcp-inspect-H4D2HSJP.js → mcp-inspect-CEMGKKAH.js} +18 -10
- package/dist/cli/mcp-inspect-CEMGKKAH.js.map +1 -0
- package/dist/cli/package.json +3 -0
- package/dist/cli/prompt-2D7ID24X.js +16 -0
- package/dist/cli/{prune-sessions-4N3BVST2.js → prune-sessions-3RWUBYRS.js} +4 -2
- package/dist/cli/{prune-sessions-4N3BVST2.js.map → prune-sessions-3RWUBYRS.js.map} +1 -1
- package/dist/cli/{replay-3GTWM75X.js → replay-SR44E6RS.js} +42 -30
- package/dist/cli/{replay-3GTWM75X.js.map → replay-SR44E6RS.js.map} +1 -1
- package/dist/cli/{run-BLZPTRDX.js → run-MDGL27WL.js} +42 -40
- package/dist/cli/run-MDGL27WL.js.map +1 -0
- package/dist/cli/{server-DRFPXXSH.js → server-27ARQXIZ.js} +95 -55
- package/dist/cli/server-27ARQXIZ.js.map +1 -0
- package/dist/cli/{sessions-BOWFPTXT.js → sessions-CKQXCYGP.js} +25 -22
- package/dist/cli/sessions-CKQXCYGP.js.map +1 -0
- package/dist/cli/setup-TPAGSVXO.js +618 -0
- package/dist/cli/setup-TPAGSVXO.js.map +1 -0
- package/dist/cli/stats-DPUBZNVX.js +16 -0
- package/dist/cli/update-6ITLPRDV.js +15 -0
- package/dist/cli/version-2X3BHVVK.js +33 -0
- package/dist/cli/{version-XQXYSJ5L.js.map → version-2X3BHVVK.js.map} +1 -1
- package/dist/index.d.ts +182 -45
- package/dist/index.js +1420 -546
- package/dist/index.js.map +1 -1
- package/package.json +21 -8
- package/dist/cli/.-3G6VX5S7.js +0 -327
- package/dist/cli/.-6YRPB2C7.js +0 -329
- package/dist/cli/.-EYSVINK3.js +0 -317
- package/dist/cli/acp-64VQZLDJ.js.map +0 -1
- package/dist/cli/chat-ZAGX52RV.js +0 -46
- package/dist/cli/chunk-4DCHFFEY.js.map +0 -1
- package/dist/cli/chunk-4W2CICFQ.js.map +0 -1
- package/dist/cli/chunk-5X7LZJDE.js +0 -36
- package/dist/cli/chunk-5X7LZJDE.js.map +0 -1
- package/dist/cli/chunk-65Q5HQ26.js +0 -892
- package/dist/cli/chunk-65Q5HQ26.js.map +0 -1
- package/dist/cli/chunk-A3LL4XDV.js.map +0 -1
- package/dist/cli/chunk-A7VHMMDE.js.map +0 -1
- package/dist/cli/chunk-AFFZF3MW.js +0 -36
- package/dist/cli/chunk-AFFZF3MW.js.map +0 -1
- package/dist/cli/chunk-AT6GGIBV.js.map +0 -1
- package/dist/cli/chunk-BOFL3T45.js.map +0 -1
- package/dist/cli/chunk-BYZGO3BX.js.map +0 -1
- package/dist/cli/chunk-CD4SCQL4.js.map +0 -1
- package/dist/cli/chunk-CFY2XLY6.js.map +0 -1
- package/dist/cli/chunk-CPOV2O73.js +0 -39
- package/dist/cli/chunk-CPOV2O73.js.map +0 -1
- package/dist/cli/chunk-DAEAAVDF.js +0 -199
- package/dist/cli/chunk-DAEAAVDF.js.map +0 -1
- package/dist/cli/chunk-F2AV2QDK.js +0 -16514
- package/dist/cli/chunk-F2AV2QDK.js.map +0 -1
- package/dist/cli/chunk-H4OLWRSX.js.map +0 -1
- package/dist/cli/chunk-IEA6JOIP.js +0 -5430
- package/dist/cli/chunk-IEA6JOIP.js.map +0 -1
- package/dist/cli/chunk-L7W3HJZQ.js +0 -46
- package/dist/cli/chunk-LN27AKV3.js +0 -26
- package/dist/cli/chunk-LN27AKV3.js.map +0 -1
- package/dist/cli/chunk-LTXADNCO.js.map +0 -1
- package/dist/cli/chunk-MHGPBJ2T.js.map +0 -1
- package/dist/cli/chunk-ORM6PK57.js.map +0 -1
- package/dist/cli/chunk-SXLJBFIV.js.map +0 -1
- package/dist/cli/chunk-YJFKFTAL.js.map +0 -1
- package/dist/cli/code-X3M6ENTQ.js +0 -150
- package/dist/cli/code-X3M6ENTQ.js.map +0 -1
- package/dist/cli/desktop-ZTMHQR2Y.js.map +0 -1
- package/dist/cli/diff-YASCB7PU.js +0 -153
- package/dist/cli/doctor-XCN5ETVP.js +0 -24
- package/dist/cli/mcp-inspect-H4D2HSJP.js.map +0 -1
- package/dist/cli/prompt-RSIHN62V.js +0 -14
- package/dist/cli/run-BLZPTRDX.js.map +0 -1
- package/dist/cli/server-DRFPXXSH.js.map +0 -1
- package/dist/cli/sessions-BOWFPTXT.js.map +0 -1
- package/dist/cli/setup-FQL2JJC2.js +0 -516
- package/dist/cli/setup-FQL2JJC2.js.map +0 -1
- package/dist/cli/stats-5RJCATCE.js +0 -12
- package/dist/cli/stats-5RJCATCE.js.map +0 -1
- package/dist/cli/update-GUCWB4UN.js +0 -13
- package/dist/cli/update-GUCWB4UN.js.map +0 -1
- package/dist/cli/version-XQXYSJ5L.js +0 -30
- /package/dist/cli/{.-3G6VX5S7.js.map → chat-TH7VNNCJ.js.map} +0 -0
- /package/dist/cli/{.-6YRPB2C7.js.map → chunk-TUK7OWJA.js.map} +0 -0
- /package/dist/cli/{.-EYSVINK3.js.map → doctor-CC5CLOGG.js.map} +0 -0
- /package/dist/cli/{chat-ZAGX52RV.js.map → prompt-2D7ID24X.js.map} +0 -0
- /package/dist/cli/{doctor-XCN5ETVP.js.map → stats-DPUBZNVX.js.map} +0 -0
- /package/dist/cli/{prompt-RSIHN62V.js.map → update-6ITLPRDV.js.map} +0 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
3
|
+
|
|
4
|
+
// src/tokenizer.ts
|
|
5
|
+
import { existsSync, readFileSync } from "fs";
|
|
6
|
+
import { createRequire } from "module";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { gunzipSync } from "zlib";
|
|
10
|
+
function buildByteToChar() {
|
|
11
|
+
const result = new Array(256);
|
|
12
|
+
const bs = [];
|
|
13
|
+
for (let b = 33; b <= 126; b++) bs.push(b);
|
|
14
|
+
for (let b = 161; b <= 172; b++) bs.push(b);
|
|
15
|
+
for (let b = 174; b <= 255; b++) bs.push(b);
|
|
16
|
+
const cs = bs.slice();
|
|
17
|
+
let n = 0;
|
|
18
|
+
for (let b = 0; b < 256; b++) {
|
|
19
|
+
if (!bs.includes(b)) {
|
|
20
|
+
bs.push(b);
|
|
21
|
+
cs.push(256 + n);
|
|
22
|
+
n++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (let i = 0; i < bs.length; i++) {
|
|
26
|
+
result[bs[i]] = String.fromCodePoint(cs[i]);
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
var cached = null;
|
|
31
|
+
function resolveDataPath() {
|
|
32
|
+
if (process.env.REASONIX_TOKENIZER_PATH) return process.env.REASONIX_TOKENIZER_PATH;
|
|
33
|
+
const candidates = [];
|
|
34
|
+
try {
|
|
35
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
candidates.push(join(here, "..", "data", "deepseek-tokenizer.json.gz"));
|
|
37
|
+
candidates.push(join(here, "..", "..", "data", "deepseek-tokenizer.json.gz"));
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const req = createRequire(import.meta.url);
|
|
42
|
+
candidates.push(
|
|
43
|
+
join(dirname(req.resolve("reasonix/package.json")), "data", "deepseek-tokenizer.json.gz")
|
|
44
|
+
);
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
for (const p of candidates) {
|
|
48
|
+
if (existsSync(p)) return p;
|
|
49
|
+
}
|
|
50
|
+
return candidates[0] ?? join(process.cwd(), "data", "deepseek-tokenizer.json.gz");
|
|
51
|
+
}
|
|
52
|
+
function loadTokenizer() {
|
|
53
|
+
if (cached) return cached;
|
|
54
|
+
const buf = readFileSync(resolveDataPath());
|
|
55
|
+
const json = gunzipSync(buf).toString("utf8");
|
|
56
|
+
const data = JSON.parse(json);
|
|
57
|
+
const mergeRank = /* @__PURE__ */ new Map();
|
|
58
|
+
for (let i = 0; i < data.model.merges.length; i++) {
|
|
59
|
+
mergeRank.set(data.model.merges[i], i);
|
|
60
|
+
}
|
|
61
|
+
const splitRegexes = [];
|
|
62
|
+
for (const p of data.pre_tokenizer.pretokenizers) {
|
|
63
|
+
if (p.type === "Split") {
|
|
64
|
+
splitRegexes.push(new RegExp(p.pattern.Regex, "gu"));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const addedMap = /* @__PURE__ */ new Map();
|
|
68
|
+
const addedContents = [];
|
|
69
|
+
for (const t of data.added_tokens) {
|
|
70
|
+
if (!t.special) {
|
|
71
|
+
addedMap.set(t.content, t.id);
|
|
72
|
+
addedContents.push(t.content);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
addedContents.sort((a, b) => b.length - a.length);
|
|
76
|
+
const addedPattern = addedContents.length ? new RegExp(addedContents.map(escapeRegex).join("|"), "g") : null;
|
|
77
|
+
cached = {
|
|
78
|
+
vocab: data.model.vocab,
|
|
79
|
+
mergeRank,
|
|
80
|
+
splitRegexes,
|
|
81
|
+
byteToChar: buildByteToChar(),
|
|
82
|
+
addedPattern,
|
|
83
|
+
addedMap
|
|
84
|
+
};
|
|
85
|
+
return cached;
|
|
86
|
+
}
|
|
87
|
+
function escapeRegex(s) {
|
|
88
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
89
|
+
}
|
|
90
|
+
function applySplit(chunks, re) {
|
|
91
|
+
const out = [];
|
|
92
|
+
for (const chunk of chunks) {
|
|
93
|
+
if (!chunk) continue;
|
|
94
|
+
re.lastIndex = 0;
|
|
95
|
+
let last = 0;
|
|
96
|
+
for (const m of chunk.matchAll(re)) {
|
|
97
|
+
const idx = m.index ?? 0;
|
|
98
|
+
if (idx > last) out.push(chunk.slice(last, idx));
|
|
99
|
+
if (m[0].length > 0) out.push(m[0]);
|
|
100
|
+
last = idx + m[0].length;
|
|
101
|
+
}
|
|
102
|
+
if (last < chunk.length) out.push(chunk.slice(last));
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
function byteLevelEncode(s, byteToChar) {
|
|
107
|
+
const bytes = new TextEncoder().encode(s);
|
|
108
|
+
let out = "";
|
|
109
|
+
for (let i = 0; i < bytes.length; i++) out += byteToChar[bytes[i]];
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
function bpeEncode(piece, mergeRank) {
|
|
113
|
+
if (piece.length <= 1) return piece ? [piece] : [];
|
|
114
|
+
let word = Array.from(piece);
|
|
115
|
+
while (true) {
|
|
116
|
+
let bestIdx = -1;
|
|
117
|
+
let bestRank = Number.POSITIVE_INFINITY;
|
|
118
|
+
for (let i = 0; i < word.length - 1; i++) {
|
|
119
|
+
const pair = `${word[i]} ${word[i + 1]}`;
|
|
120
|
+
const rank = mergeRank.get(pair);
|
|
121
|
+
if (rank !== void 0 && rank < bestRank) {
|
|
122
|
+
bestRank = rank;
|
|
123
|
+
bestIdx = i;
|
|
124
|
+
if (rank === 0) break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (bestIdx < 0) break;
|
|
128
|
+
word = [
|
|
129
|
+
...word.slice(0, bestIdx),
|
|
130
|
+
word[bestIdx] + word[bestIdx + 1],
|
|
131
|
+
...word.slice(bestIdx + 2)
|
|
132
|
+
];
|
|
133
|
+
if (word.length === 1) break;
|
|
134
|
+
}
|
|
135
|
+
return word;
|
|
136
|
+
}
|
|
137
|
+
function encode(text) {
|
|
138
|
+
if (!text) return [];
|
|
139
|
+
const t = loadTokenizer();
|
|
140
|
+
const ids = [];
|
|
141
|
+
const process2 = (segment) => {
|
|
142
|
+
if (!segment) return;
|
|
143
|
+
let chunks = [segment];
|
|
144
|
+
for (const re of t.splitRegexes) chunks = applySplit(chunks, re);
|
|
145
|
+
for (const chunk of chunks) {
|
|
146
|
+
if (!chunk) continue;
|
|
147
|
+
const byteLevel = byteLevelEncode(chunk, t.byteToChar);
|
|
148
|
+
const pieces = bpeEncode(byteLevel, t.mergeRank);
|
|
149
|
+
for (const p of pieces) {
|
|
150
|
+
const id = t.vocab[p];
|
|
151
|
+
if (id !== void 0) ids.push(id);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
if (t.addedPattern) {
|
|
156
|
+
t.addedPattern.lastIndex = 0;
|
|
157
|
+
let last = 0;
|
|
158
|
+
for (const m of text.matchAll(t.addedPattern)) {
|
|
159
|
+
const idx = m.index ?? 0;
|
|
160
|
+
if (idx > last) process2(text.slice(last, idx));
|
|
161
|
+
const id = t.addedMap.get(m[0]);
|
|
162
|
+
if (id !== void 0) ids.push(id);
|
|
163
|
+
last = idx + m[0].length;
|
|
164
|
+
}
|
|
165
|
+
if (last < text.length) process2(text.slice(last));
|
|
166
|
+
} else {
|
|
167
|
+
process2(text);
|
|
168
|
+
}
|
|
169
|
+
return ids;
|
|
170
|
+
}
|
|
171
|
+
function countTokens(text) {
|
|
172
|
+
return encode(text).length;
|
|
173
|
+
}
|
|
174
|
+
var DEFAULT_BOUNDED_TOKENIZE_CHARS = 2 * 1024;
|
|
175
|
+
function countTokensBounded(text, maxChars = DEFAULT_BOUNDED_TOKENIZE_CHARS) {
|
|
176
|
+
if (text.length === 0) return 0;
|
|
177
|
+
const cap = Math.floor(maxChars);
|
|
178
|
+
if (cap > 0 && text.length <= cap) return countTokens(text);
|
|
179
|
+
if (cap <= 0) return Math.max(1, Math.ceil(text.length * 0.3));
|
|
180
|
+
const headChars = Math.ceil(cap / 2);
|
|
181
|
+
const tailChars = Math.floor(cap / 2);
|
|
182
|
+
const head = text.slice(0, headChars);
|
|
183
|
+
const tail = tailChars > 0 ? text.slice(-tailChars) : "";
|
|
184
|
+
const sampleChars = head.length + tail.length;
|
|
185
|
+
const sampleTokens = countTokens(head) + countTokens(tail);
|
|
186
|
+
const ratio = sampleChars > 0 ? sampleTokens / sampleChars : 0.3;
|
|
187
|
+
return Math.max(1, Math.ceil(text.length * ratio));
|
|
188
|
+
}
|
|
189
|
+
var BOS = "<\uFF5Cbegin\u2581of\u2581sentence\uFF5C>";
|
|
190
|
+
var EOS = "<\uFF5Cend\u2581of\u2581sentence\uFF5C>";
|
|
191
|
+
var USER_SP = "<\uFF5CUser\uFF5C>";
|
|
192
|
+
var ASSISTANT_SP = "<\uFF5CAssistant\uFF5C>";
|
|
193
|
+
var THINK_START = "<think>";
|
|
194
|
+
var THINK_END = "</think>";
|
|
195
|
+
var DSML = "\uFF5CDSML\uFF5C";
|
|
196
|
+
var TC_BEGIN = `<${DSML}tool_calls>`;
|
|
197
|
+
var TC_END = `</${DSML}tool_calls>`;
|
|
198
|
+
var INVOKE_BEGIN = `<${DSML}invoke name="`;
|
|
199
|
+
var INVOKE_END = `</${DSML}invoke>`;
|
|
200
|
+
var PARAM_TEMPLATE = `<${DSML}parameter name="{key}" string="{is_str}">{value}</${DSML}parameter>`;
|
|
201
|
+
var TOOL_RESULT_TEMPLATE = "<tool_result>{content}</tool_result>";
|
|
202
|
+
var toolsTemplateCache = /* @__PURE__ */ new WeakMap();
|
|
203
|
+
function renderTools(tools) {
|
|
204
|
+
const cached2 = toolsTemplateCache.get(tools);
|
|
205
|
+
if (cached2 !== void 0) return cached2;
|
|
206
|
+
const schemas = tools.map((t) => {
|
|
207
|
+
const fn = t.function ?? t;
|
|
208
|
+
return JSON.stringify(fn);
|
|
209
|
+
}).join("\n");
|
|
210
|
+
const rendered = `## Tools
|
|
211
|
+
|
|
212
|
+
You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<${DSML}tool_calls>" block like the following:
|
|
213
|
+
|
|
214
|
+
<${DSML}tool_calls>
|
|
215
|
+
<${DSML}invoke name="$TOOL_NAME">
|
|
216
|
+
<${DSML}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</${DSML}parameter>
|
|
217
|
+
...
|
|
218
|
+
</${DSML}invoke>
|
|
219
|
+
<${DSML}invoke name="$TOOL_NAME2">
|
|
220
|
+
...
|
|
221
|
+
</${DSML}invoke>
|
|
222
|
+
</${DSML}tool_calls>
|
|
223
|
+
|
|
224
|
+
String parameters should be specified as is and set \`string="true"\`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set \`string="false"\`.
|
|
225
|
+
|
|
226
|
+
If thinking_mode is enabled (triggered by ${THINK_START}), you MUST output your complete reasoning inside ${THINK_START}...${THINK_END} BEFORE any tool calls or final response.
|
|
227
|
+
|
|
228
|
+
Otherwise, output directly after ${THINK_END} with tool calls or final response.
|
|
229
|
+
|
|
230
|
+
### Available Tool Schemas
|
|
231
|
+
|
|
232
|
+
${schemas}
|
|
233
|
+
|
|
234
|
+
You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.`;
|
|
235
|
+
toolsTemplateCache.set(tools, rendered);
|
|
236
|
+
return rendered;
|
|
237
|
+
}
|
|
238
|
+
function encodeArgumentsToDsml(argsJson) {
|
|
239
|
+
let args;
|
|
240
|
+
try {
|
|
241
|
+
args = JSON.parse(argsJson);
|
|
242
|
+
} catch {
|
|
243
|
+
args = { arguments: argsJson };
|
|
244
|
+
}
|
|
245
|
+
return Object.entries(args).map(
|
|
246
|
+
([k, v]) => PARAM_TEMPLATE.replace("{key}", k).replace("{is_str}", typeof v === "string" ? "true" : "false").replace("{value}", typeof v === "string" ? v : JSON.stringify(v))
|
|
247
|
+
).join("\n");
|
|
248
|
+
}
|
|
249
|
+
function renderToolCallsDsml(toolCalls) {
|
|
250
|
+
const invokes = toolCalls.map((tc) => {
|
|
251
|
+
const name = tc.function?.name ?? "";
|
|
252
|
+
const argsJson = tc.function?.arguments ?? "{}";
|
|
253
|
+
return `${INVOKE_BEGIN + name}">
|
|
254
|
+
${encodeArgumentsToDsml(argsJson)}
|
|
255
|
+
${INVOKE_END}`;
|
|
256
|
+
}).join("\n");
|
|
257
|
+
return `
|
|
258
|
+
|
|
259
|
+
${TC_BEGIN}
|
|
260
|
+
${invokes}
|
|
261
|
+
${TC_END}`;
|
|
262
|
+
}
|
|
263
|
+
function mergeToolMessages(messages) {
|
|
264
|
+
const merged = [];
|
|
265
|
+
for (const msg of messages) {
|
|
266
|
+
const role = msg.role ?? "user";
|
|
267
|
+
if (role === "tool") {
|
|
268
|
+
const toolBlock = TOOL_RESULT_TEMPLATE.replace("{content}", msg.content ?? "");
|
|
269
|
+
const last = merged[merged.length - 1];
|
|
270
|
+
if (last && last.role === "user" && Array.isArray(last._toolBlocks) && Array.isArray(last._textParts)) {
|
|
271
|
+
last._toolBlocks.push(toolBlock);
|
|
272
|
+
last.content = `${last._textParts.join("\n\n")}
|
|
273
|
+
|
|
274
|
+
${last._toolBlocks.join("\n")}`.replace(
|
|
275
|
+
/^\n\n/,
|
|
276
|
+
""
|
|
277
|
+
);
|
|
278
|
+
} else {
|
|
279
|
+
merged.push({
|
|
280
|
+
role: "user",
|
|
281
|
+
content: toolBlock,
|
|
282
|
+
_textParts: [],
|
|
283
|
+
_toolBlocks: [toolBlock]
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
} else if (role === "user") {
|
|
287
|
+
const text = msg.content ?? "";
|
|
288
|
+
const last = merged[merged.length - 1];
|
|
289
|
+
if (last && last.role === "user" && Array.isArray(last._toolBlocks) && Array.isArray(last._textParts)) {
|
|
290
|
+
last._textParts.push(text);
|
|
291
|
+
last.content = `${last._textParts.join("\n\n")}
|
|
292
|
+
|
|
293
|
+
${last._toolBlocks.join("\n\n")}`.replace(
|
|
294
|
+
/^\n\n/,
|
|
295
|
+
""
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
merged.push({
|
|
299
|
+
...msg,
|
|
300
|
+
role: "user",
|
|
301
|
+
content: text,
|
|
302
|
+
_textParts: [text],
|
|
303
|
+
_toolBlocks: []
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
merged.push({ ...msg });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const m of merged) {
|
|
311
|
+
m._textParts = void 0;
|
|
312
|
+
m._toolBlocks = void 0;
|
|
313
|
+
}
|
|
314
|
+
return merged;
|
|
315
|
+
}
|
|
316
|
+
function dropThinkingMessages(messages) {
|
|
317
|
+
let lastUserIdx = -1;
|
|
318
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
319
|
+
const role = messages[i].role;
|
|
320
|
+
if (role === "user" || role === "developer") {
|
|
321
|
+
lastUserIdx = i;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (lastUserIdx < 0) return messages;
|
|
326
|
+
const result = [];
|
|
327
|
+
for (let i = 0; i < messages.length; i++) {
|
|
328
|
+
const msg = messages[i];
|
|
329
|
+
if (i < lastUserIdx && msg.role === "developer") continue;
|
|
330
|
+
if (i < lastUserIdx && msg.role === "assistant") {
|
|
331
|
+
result.push({ ...msg, reasoning_content: null });
|
|
332
|
+
} else {
|
|
333
|
+
result.push(msg);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
}
|
|
338
|
+
function formatDeepSeekPrompt(messages, drop_thinking = false) {
|
|
339
|
+
if (messages.length === 0) return ASSISTANT_SP + THINK_END;
|
|
340
|
+
let msgs = messages;
|
|
341
|
+
if (drop_thinking) {
|
|
342
|
+
msgs = dropThinkingMessages(msgs);
|
|
343
|
+
}
|
|
344
|
+
const merged = mergeToolMessages(msgs);
|
|
345
|
+
let prompt = BOS;
|
|
346
|
+
for (let i = 0; i < merged.length; i++) {
|
|
347
|
+
const msg = merged[i];
|
|
348
|
+
const role = msg.role ?? "user";
|
|
349
|
+
const nextRole = i + 1 < merged.length ? merged[i + 1].role ?? "user" : null;
|
|
350
|
+
if (role === "system") {
|
|
351
|
+
prompt += msg.content ?? "";
|
|
352
|
+
} else if (role === "user") {
|
|
353
|
+
prompt += USER_SP + (msg.content ?? "");
|
|
354
|
+
if (nextRole === "assistant" || nextRole === null) {
|
|
355
|
+
prompt += ASSISTANT_SP + THINK_END;
|
|
356
|
+
}
|
|
357
|
+
} else if (role === "assistant") {
|
|
358
|
+
if (msg.reasoning_content) {
|
|
359
|
+
prompt += THINK_START + msg.reasoning_content + THINK_END;
|
|
360
|
+
}
|
|
361
|
+
if (msg.content) prompt += msg.content;
|
|
362
|
+
const tcs = msg.tool_calls;
|
|
363
|
+
if (Array.isArray(tcs) && tcs.length > 0) {
|
|
364
|
+
prompt += renderToolCallsDsml(tcs);
|
|
365
|
+
}
|
|
366
|
+
prompt += EOS;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return prompt;
|
|
370
|
+
}
|
|
371
|
+
function estimateConversationTokens(messages, drop_thinking = false) {
|
|
372
|
+
if (messages.length === 0) return 0;
|
|
373
|
+
return countTokensBounded(formatDeepSeekPrompt(messages, drop_thinking));
|
|
374
|
+
}
|
|
375
|
+
function estimateRequestTokens(messages, toolSpecs, drop_thinking = false) {
|
|
376
|
+
let total = estimateConversationTokens(messages, drop_thinking);
|
|
377
|
+
if (toolSpecs && toolSpecs.length > 0) {
|
|
378
|
+
total += countTokensBounded(renderTools(toolSpecs));
|
|
379
|
+
}
|
|
380
|
+
return total;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export {
|
|
384
|
+
resolveDataPath,
|
|
385
|
+
countTokens,
|
|
386
|
+
countTokensBounded,
|
|
387
|
+
estimateConversationTokens,
|
|
388
|
+
estimateRequestTokens
|
|
389
|
+
};
|
|
390
|
+
//# sourceMappingURL=chunk-6OWJV3YW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/tokenizer.ts"],"sourcesContent":["/** Encode-only DeepSeek V4 tokenizer port. Applies V4 chat template so token count tracks API `prompt_tokens`. */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { gunzipSync } from \"node:zlib\";\n\ninterface AddedToken {\n id: number;\n content: string;\n special: boolean;\n normalized: boolean;\n}\n\ninterface SplitPretokenizer {\n type: \"Split\";\n pattern: { Regex: string };\n behavior: \"Isolated\" | \"Removed\" | string;\n invert: boolean;\n}\n\ninterface ByteLevelPretokenizer {\n type: \"ByteLevel\";\n add_prefix_space: boolean;\n trim_offsets: boolean;\n use_regex: boolean;\n}\n\ntype Pretokenizer = SplitPretokenizer | ByteLevelPretokenizer;\n\ninterface TokenizerData {\n added_tokens: AddedToken[];\n pre_tokenizer: {\n type: \"Sequence\";\n pretokenizers: Pretokenizer[];\n };\n model: {\n type: \"BPE\";\n vocab: Record<string, number>;\n merges: string[];\n };\n}\n\ninterface LoadedTokenizer {\n vocab: Record<string, number>;\n mergeRank: Map<string, number>;\n splitRegexes: RegExp[];\n byteToChar: string[];\n /** Non-special added tokens only — special tokens in user text tokenize byte-by-byte (HF default). */\n addedPattern: RegExp | null;\n addedMap: Map<string, number>;\n}\n\n/** GPT-2 byte→unicode map; lets byte-level BPE vocab serialize as readable JSON strings. */\nfunction buildByteToChar(): string[] {\n const result: string[] = new Array(256);\n const bs: number[] = [];\n for (let b = 33; b <= 126; b++) bs.push(b);\n for (let b = 161; b <= 172; b++) bs.push(b);\n for (let b = 174; b <= 255; b++) bs.push(b);\n const cs = bs.slice();\n let n = 0;\n for (let b = 0; b < 256; b++) {\n if (!bs.includes(b)) {\n bs.push(b);\n cs.push(256 + n);\n n++;\n }\n }\n for (let i = 0; i < bs.length; i++) {\n result[bs[i]!] = String.fromCodePoint(cs[i]!);\n }\n return result;\n}\n\nlet cached: LoadedTokenizer | null = null;\n\n/** Two ../data candidates needed: dist/index.js AND dist/cli/index.js resolve to different roots. */\nexport function resolveDataPath(): string {\n if (process.env.REASONIX_TOKENIZER_PATH) return process.env.REASONIX_TOKENIZER_PATH;\n const candidates: string[] = [];\n try {\n const here = dirname(fileURLToPath(import.meta.url));\n candidates.push(join(here, \"..\", \"data\", \"deepseek-tokenizer.json.gz\"));\n candidates.push(join(here, \"..\", \"..\", \"data\", \"deepseek-tokenizer.json.gz\"));\n } catch {\n /* import.meta.url unavailable — skip to the package resolution step. */\n }\n try {\n const req = createRequire(import.meta.url);\n candidates.push(\n join(dirname(req.resolve(\"reasonix/package.json\")), \"data\", \"deepseek-tokenizer.json.gz\"),\n );\n } catch {\n /* Not installed as `reasonix/` — the earlier candidates still may hit. */\n }\n for (const p of candidates) {\n if (existsSync(p)) return p;\n }\n // Nothing exists — return the first candidate anyway so readFileSync\n // surfaces a concrete path in the ENOENT message (better than silent miss).\n return candidates[0] ?? join(process.cwd(), \"data\", \"deepseek-tokenizer.json.gz\");\n}\n\nfunction loadTokenizer(): LoadedTokenizer {\n if (cached) return cached;\n const buf = readFileSync(resolveDataPath());\n const json = gunzipSync(buf).toString(\"utf8\");\n const data = JSON.parse(json) as TokenizerData;\n\n const mergeRank = new Map<string, number>();\n for (let i = 0; i < data.model.merges.length; i++) {\n mergeRank.set(data.model.merges[i]!, i);\n }\n\n const splitRegexes: RegExp[] = [];\n for (const p of data.pre_tokenizer.pretokenizers) {\n if (p.type === \"Split\") {\n // All three Split rules use Isolated — matches become their own\n // pre-tokens and so do the in-between stretches. The ByteLevel\n // stage in the Sequence does no extra splitting here\n // (use_regex:false), so our 3 Split regexes are the whole story.\n splitRegexes.push(new RegExp(p.pattern.Regex, \"gu\"));\n }\n }\n\n const addedMap = new Map<string, number>();\n const addedContents: string[] = [];\n for (const t of data.added_tokens) {\n if (!t.special) {\n addedMap.set(t.content, t.id);\n addedContents.push(t.content);\n }\n }\n // Longest-first ensures greedy matching doesn't lose a longer token\n // to a shorter prefix (e.g. `<think>` before `<`).\n addedContents.sort((a, b) => b.length - a.length);\n const addedPattern = addedContents.length\n ? new RegExp(addedContents.map(escapeRegex).join(\"|\"), \"g\")\n : null;\n\n cached = {\n vocab: data.model.vocab,\n mergeRank,\n splitRegexes,\n byteToChar: buildByteToChar(),\n addedPattern,\n addedMap,\n };\n return cached;\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\nfunction applySplit(chunks: string[], re: RegExp): string[] {\n const out: string[] = [];\n for (const chunk of chunks) {\n if (!chunk) continue;\n // Reset lastIndex — reusing a /g regex across matchAll iterations\n // is safe (matchAll internally advances), but across different\n // input strings we want a clean start.\n re.lastIndex = 0;\n let last = 0;\n for (const m of chunk.matchAll(re)) {\n const idx = m.index ?? 0;\n if (idx > last) out.push(chunk.slice(last, idx));\n if (m[0].length > 0) out.push(m[0]);\n last = idx + m[0].length;\n }\n if (last < chunk.length) out.push(chunk.slice(last));\n }\n return out;\n}\n\n/** UTF-8 bytes of `s`, each mapped to its byte-level visible char. */\nfunction byteLevelEncode(s: string, byteToChar: string[]): string {\n const bytes = new TextEncoder().encode(s);\n let out = \"\";\n for (let i = 0; i < bytes.length; i++) out += byteToChar[bytes[i]!];\n return out;\n}\n\nfunction bpeEncode(piece: string, mergeRank: Map<string, number>): string[] {\n if (piece.length <= 1) return piece ? [piece] : [];\n let word: string[] = Array.from(piece);\n while (true) {\n let bestIdx = -1;\n let bestRank = Number.POSITIVE_INFINITY;\n for (let i = 0; i < word.length - 1; i++) {\n const pair = `${word[i]} ${word[i + 1]}`;\n const rank = mergeRank.get(pair);\n if (rank !== undefined && rank < bestRank) {\n bestRank = rank;\n bestIdx = i;\n if (rank === 0) break; // 0 is already the best possible\n }\n }\n if (bestIdx < 0) break;\n word = [\n ...word.slice(0, bestIdx),\n word[bestIdx]! + word[bestIdx + 1]!,\n ...word.slice(bestIdx + 2),\n ];\n if (word.length === 1) break;\n }\n return word;\n}\n\nexport function encode(text: string): number[] {\n if (!text) return [];\n const t = loadTokenizer();\n const ids: number[] = [];\n\n const process = (segment: string) => {\n if (!segment) return;\n let chunks: string[] = [segment];\n for (const re of t.splitRegexes) chunks = applySplit(chunks, re);\n for (const chunk of chunks) {\n if (!chunk) continue;\n const byteLevel = byteLevelEncode(chunk, t.byteToChar);\n const pieces = bpeEncode(byteLevel, t.mergeRank);\n for (const p of pieces) {\n const id = t.vocab[p];\n // If not in vocab we silently skip: shouldn't happen for\n // byte-level BPE (every single byte has its own vocab entry),\n // but if a future tokenizer update breaks that invariant we'd\n // rather under-count than throw from a UI gauge.\n if (id !== undefined) ids.push(id);\n }\n }\n };\n\n if (t.addedPattern) {\n t.addedPattern.lastIndex = 0;\n let last = 0;\n for (const m of text.matchAll(t.addedPattern)) {\n const idx = m.index ?? 0;\n if (idx > last) process(text.slice(last, idx));\n const id = t.addedMap.get(m[0]);\n if (id !== undefined) ids.push(id);\n last = idx + m[0].length;\n }\n if (last < text.length) process(text.slice(last));\n } else {\n process(text);\n }\n return ids;\n}\n\nexport function countTokens(text: string): number {\n return encode(text).length;\n}\n\nexport const DEFAULT_BOUNDED_TOKENIZE_CHARS = 2 * 1024;\n\nexport function countTokensBounded(\n text: string,\n maxChars = DEFAULT_BOUNDED_TOKENIZE_CHARS,\n): number {\n if (text.length === 0) return 0;\n const cap = Math.floor(maxChars);\n if (cap > 0 && text.length <= cap) return countTokens(text);\n if (cap <= 0) return Math.max(1, Math.ceil(text.length * 0.3));\n\n const headChars = Math.ceil(cap / 2);\n const tailChars = Math.floor(cap / 2);\n const head = text.slice(0, headChars);\n const tail = tailChars > 0 ? text.slice(-tailChars) : \"\";\n const sampleChars = head.length + tail.length;\n const sampleTokens = countTokens(head) + countTokens(tail);\n const ratio = sampleChars > 0 ? sampleTokens / sampleChars : 0.3;\n return Math.max(1, Math.ceil(text.length * ratio));\n}\n\nconst BOS = \"<|begin▁of▁sentence|>\";\nconst EOS = \"<|end▁of▁sentence|>\";\nconst USER_SP = \"<|User|>\";\nconst ASSISTANT_SP = \"<|Assistant|>\";\nconst THINK_START = \"<think>\";\nconst THINK_END = \"</think>\";\n\nconst DSML = \"|DSML|\";\nconst TC_BEGIN = `<${DSML}tool_calls>`;\nconst TC_END = `</${DSML}tool_calls>`;\nconst INVOKE_BEGIN = `<${DSML}invoke name=\"`;\nconst INVOKE_END = `</${DSML}invoke>`;\nconst PARAM_TEMPLATE = `<${DSML}parameter name=\"{key}\" string=\"{is_str}\">{value}</${DSML}parameter>`;\nconst TOOL_RESULT_TEMPLATE = \"<tool_result>{content}</tool_result>\";\n\n/** Keyed by `ImmutablePrefix._toolSpecs` identity — stable for the prefix's lifetime. */\nconst toolsTemplateCache = new WeakMap<ReadonlyArray<unknown>, string>();\n\nfunction renderTools(tools: ReadonlyArray<unknown>): string {\n const cached = toolsTemplateCache.get(tools);\n if (cached !== undefined) return cached;\n\n const schemas = tools\n .map((t) => {\n const fn = (t as { function?: unknown }).function ?? t;\n return JSON.stringify(fn);\n })\n .join(\"\\n\");\n const rendered = `## Tools\\n\\nYou have access to a set of tools to help answer the user's question. You can invoke tools by writing a \\\"<${DSML}tool_calls>\" block like the following:\\n\\n<${DSML}tool_calls>\\n<${DSML}invoke name=\"$TOOL_NAME\">\\n<${DSML}parameter name=\"$PARAMETER_NAME\" string=\"true|false\">$PARAMETER_VALUE</${DSML}parameter>\\n...\\n</${DSML}invoke>\\n<${DSML}invoke name=\"$TOOL_NAME2\">\\n...\\n</${DSML}invoke>\\n</${DSML}tool_calls>\\n\\nString parameters should be specified as is and set \\`string=\"true\"\\`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set \\`string=\"false\"\\`.\\n\\nIf thinking_mode is enabled (triggered by ${THINK_START}), you MUST output your complete reasoning inside ${THINK_START}...${THINK_END} BEFORE any tool calls or final response.\\n\\nOtherwise, output directly after ${THINK_END} with tool calls or final response.\\n\\n### Available Tool Schemas\\n\\n${schemas}\\n\\nYou MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.`;\n\n toolsTemplateCache.set(tools, rendered);\n return rendered;\n}\n\ninterface ToolCall {\n function?: { name?: string; arguments?: string };\n [k: string]: unknown;\n}\n\ninterface V4Message {\n role?: string;\n content?: string | null;\n tool_calls?: ToolCall[];\n tool_call_id?: string;\n reasoning_content?: string | null;\n _toolBlocks?: string[];\n _textParts?: string[];\n}\n\nfunction encodeArgumentsToDsml(argsJson: string): string {\n let args: Record<string, unknown>;\n try {\n args = JSON.parse(argsJson) as Record<string, unknown>;\n } catch {\n args = { arguments: argsJson };\n }\n return Object.entries(args)\n .map(([k, v]) =>\n PARAM_TEMPLATE.replace(\"{key}\", k)\n .replace(\"{is_str}\", typeof v === \"string\" ? \"true\" : \"false\")\n .replace(\"{value}\", typeof v === \"string\" ? v : JSON.stringify(v)),\n )\n .join(\"\\n\");\n}\n\nfunction renderToolCallsDsml(toolCalls: ToolCall[]): string {\n const invokes = toolCalls\n .map((tc) => {\n const name = tc.function?.name ?? \"\";\n const argsJson = tc.function?.arguments ?? \"{}\";\n return `${INVOKE_BEGIN + name}\">\\n${encodeArgumentsToDsml(argsJson)}\\n${INVOKE_END}`;\n })\n .join(\"\\n\");\n return `\\n\\n${TC_BEGIN}\\n${invokes}\\n${TC_END}`;\n}\n\nfunction mergeToolMessages(messages: V4Message[]): V4Message[] {\n const merged: V4Message[] = [];\n for (const msg of messages) {\n const role = msg.role ?? \"user\";\n if (role === \"tool\") {\n const toolBlock = TOOL_RESULT_TEMPLATE.replace(\"{content}\", msg.content ?? \"\");\n const last = merged[merged.length - 1];\n if (\n last &&\n last.role === \"user\" &&\n Array.isArray(last._toolBlocks) &&\n Array.isArray(last._textParts)\n ) {\n last._toolBlocks.push(toolBlock);\n last.content = `${last._textParts.join(\"\\n\\n\")}\\n\\n${last._toolBlocks.join(\"\\n\")}`.replace(\n /^\\n\\n/,\n \"\",\n );\n } else {\n merged.push({\n role: \"user\",\n content: toolBlock,\n _textParts: [],\n _toolBlocks: [toolBlock],\n });\n }\n } else if (role === \"user\") {\n const text = msg.content ?? \"\";\n const last = merged[merged.length - 1];\n if (\n last &&\n last.role === \"user\" &&\n Array.isArray(last._toolBlocks) &&\n Array.isArray(last._textParts)\n ) {\n last._textParts.push(text);\n last.content =\n `${last._textParts.join(\"\\n\\n\")}\\n\\n${last._toolBlocks.join(\"\\n\\n\")}`.replace(\n /^\\n\\n/,\n \"\",\n );\n } else {\n merged.push({\n ...msg,\n role: \"user\",\n content: text,\n _textParts: [text],\n _toolBlocks: [],\n });\n }\n } else {\n merged.push({ ...msg });\n }\n }\n for (const m of merged) {\n m._textParts = undefined;\n m._toolBlocks = undefined;\n }\n return merged;\n}\n\n/** Drop `reasoning_content` from assistant messages before the last user/developer message. Matches Python `_drop_thinking_messages`. */\nfunction dropThinkingMessages(messages: V4Message[]): V4Message[] {\n let lastUserIdx = -1;\n for (let i = messages.length - 1; i >= 0; i--) {\n const role = messages[i]!.role;\n if (role === \"user\" || role === \"developer\") {\n lastUserIdx = i;\n break;\n }\n }\n if (lastUserIdx < 0) return messages;\n\n // Match Python `_drop_thinking_messages`:\n // - developer messages before lastUserIdx are dropped entirely\n // - assistant messages before lastUserIdx keep content & tool_calls\n // but have reasoning_content stripped\n const result: V4Message[] = [];\n for (let i = 0; i < messages.length; i++) {\n const msg = messages[i]!;\n if (i < lastUserIdx && msg.role === \"developer\") continue;\n if (i < lastUserIdx && msg.role === \"assistant\") {\n result.push({ ...msg, reasoning_content: null });\n } else {\n result.push(msg);\n }\n }\n return result;\n}\n\n/** Apply DeepSeek V4 chat template. Matches `encoding_dsv4.py`: tool results merged into user messages, assistant tool_calls in DSML, generation suffix appended. */\nexport function formatDeepSeekPrompt(\n messages: Array<{\n role?: string;\n content?: string | null;\n tool_calls?: unknown;\n tool_call_id?: string;\n reasoning_content?: string | null;\n }>,\n drop_thinking = false,\n): string {\n if (messages.length === 0) return ASSISTANT_SP + THINK_END;\n\n let msgs = messages as V4Message[];\n if (drop_thinking) {\n msgs = dropThinkingMessages(msgs);\n }\n const merged = mergeToolMessages(msgs);\n\n let prompt = BOS;\n\n for (let i = 0; i < merged.length; i++) {\n const msg = merged[i]!;\n const role = msg.role ?? \"user\";\n const nextRole = i + 1 < merged.length ? (merged[i + 1]!.role ?? \"user\") : null;\n\n if (role === \"system\") {\n prompt += msg.content ?? \"\";\n } else if (role === \"user\") {\n prompt += USER_SP + (msg.content ?? \"\");\n if (nextRole === \"assistant\" || nextRole === null) {\n prompt += ASSISTANT_SP + THINK_END;\n }\n } else if (role === \"assistant\") {\n if (msg.reasoning_content) {\n prompt += THINK_START + msg.reasoning_content + THINK_END;\n }\n if (msg.content) prompt += msg.content;\n const tcs = msg.tool_calls;\n if (Array.isArray(tcs) && tcs.length > 0) {\n prompt += renderToolCallsDsml(tcs);\n }\n prompt += EOS;\n }\n }\n\n return prompt;\n}\n\n/** Token-count the FULL conversation as the API would see it: wraps messages in V4 chat template, then encodes once. */\nexport function estimateConversationTokens(\n messages: Array<{\n role?: string;\n content?: string | null;\n tool_calls?: unknown;\n tool_call_id?: string;\n reasoning_content?: string | null;\n }>,\n drop_thinking = false,\n): number {\n if (messages.length === 0) return 0;\n return countTokensBounded(formatDeepSeekPrompt(messages, drop_thinking));\n}\n\n/** Total request tokens (messages + tool specs) as the API counts them. Tool specs rendered via V4 TOOLS_TEMPLATE and added to message token count. */\nexport function estimateRequestTokens(\n messages: Array<{\n role?: string;\n content?: string | null;\n tool_calls?: unknown;\n tool_call_id?: string;\n reasoning_content?: string | null;\n }>,\n toolSpecs?: ReadonlyArray<unknown> | null,\n drop_thinking = false,\n): number {\n let total = estimateConversationTokens(messages, drop_thinking);\n if (toolSpecs && toolSpecs.length > 0) {\n total += countTokensBounded(renderTools(toolSpecs));\n }\n return total;\n}\n\n/** Exposed for tests — resets the lazy-load singleton. */\nexport function _resetForTests(): void {\n cached = null;\n}\n"],"mappings":"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","names":["process","cached"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
2
3
|
|
|
3
4
|
// src/memory/session.ts
|
|
4
5
|
import { execFileSync } from "child_process";
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
writeFileSync
|
|
16
17
|
} from "fs";
|
|
17
18
|
import { homedir } from "os";
|
|
18
|
-
import { dirname, join } from "path";
|
|
19
|
+
import { dirname, join, posix as posixPath, win32 as win32Path } from "path";
|
|
19
20
|
function detectGitBranch(cwd) {
|
|
20
21
|
try {
|
|
21
22
|
const out = execFileSync("git", ["branch", "--show-current"], {
|
|
@@ -138,8 +139,19 @@ function listSessions() {
|
|
|
138
139
|
return [];
|
|
139
140
|
}
|
|
140
141
|
}
|
|
142
|
+
function normalizeWorkspace(p, platform = process.platform) {
|
|
143
|
+
if (typeof p !== "string" || p.length === 0) return "";
|
|
144
|
+
if (platform === "win32") {
|
|
145
|
+
const resolved = win32Path.resolve(p);
|
|
146
|
+
return resolved.replace(/\\/g, "/").replace(/^([A-Z]):/i, (_, d) => `${d.toLowerCase()}:`);
|
|
147
|
+
}
|
|
148
|
+
return posixPath.resolve(p);
|
|
149
|
+
}
|
|
141
150
|
function listSessionsForWorkspace(workspace) {
|
|
142
|
-
|
|
151
|
+
const want = normalizeWorkspace(workspace);
|
|
152
|
+
return listSessions().filter(
|
|
153
|
+
(s) => typeof s.meta.workspace === "string" && normalizeWorkspace(s.meta.workspace) === want
|
|
154
|
+
);
|
|
143
155
|
}
|
|
144
156
|
function metaPath(name) {
|
|
145
157
|
return join(sessionsDir(), `${sanitizeName(name)}.meta.json`);
|
|
@@ -266,4 +278,4 @@ export {
|
|
|
266
278
|
rewriteSession,
|
|
267
279
|
archiveSession
|
|
268
280
|
};
|
|
269
|
-
//# sourceMappingURL=chunk-
|
|
281
|
+
//# sourceMappingURL=chunk-6PBZN4VI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/memory/session.ts"],"sourcesContent":["/** JSONL append-only message log under `~/.reasonix/sessions/`; concurrent-write safe. */\n\nimport { execFileSync } from \"node:child_process\";\nimport {\n appendFileSync,\n chmodSync,\n existsSync,\n mkdirSync,\n readFileSync,\n readdirSync,\n renameSync,\n statSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, posix as posixPath, win32 as win32Path } from \"node:path\";\nimport type { ChatMessage } from \"../types.js\";\n\n/** Best-effort git branch sniff; returns undefined if not a git repo or git missing. */\nexport function detectGitBranch(cwd: string): string | undefined {\n try {\n const out = execFileSync(\"git\", [\"branch\", \"--show-current\"], {\n cwd,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n timeout: 800,\n encoding: \"utf8\",\n }).trim();\n return out || undefined;\n } catch {\n return undefined;\n }\n}\n\nexport interface SessionInfo {\n name: string;\n path: string;\n size: number;\n messageCount: number;\n mtime: Date;\n meta: SessionMeta;\n}\n\nexport interface SessionMeta {\n branch?: string;\n summary?: string;\n totalCostUsd?: number;\n turnCount?: number;\n /** Absolute path of the workspace root the session was created/used in. */\n workspace?: string;\n /** Wallet currency at last save — used to format `totalCostUsd` in the picker without re-fetching balance. */\n balanceCurrency?: string;\n /** Cumulative cache hit / miss tokens across the session — survives resume so /status cache% isn't 0 on a fresh boot. */\n cacheHitTokens?: number;\n cacheMissTokens?: number;\n /** Last turn's promptTokens — lets /status render the context bar before the next turn fires. */\n lastPromptTokens?: number;\n}\n\nexport function sessionsDir(): string {\n return join(homedir(), \".reasonix\", \"sessions\");\n}\n\nexport function sessionPath(name: string): string {\n return join(sessionsDir(), `${sanitizeName(name)}.jsonl`);\n}\n\nexport function sanitizeName(name: string): string {\n const cleaned = name.replace(/[^\\w\\-\\u4e00-\\u9fa5]/g, \"_\").slice(0, 64);\n return cleaned || \"default\";\n}\n\n/** Sortable timestamp `YYYYMMDDHHmm` — used as a session-name suffix. */\nexport function timestampSuffix(): string {\n return new Date().toISOString().replace(/[^\\d]/g, \"\").slice(0, 12);\n}\n\n/** Unique name for an in-app \"new session\" — strips a trailing 12/14-digit timestamp from the current name and re-stamps with seconds precision so back-to-back clicks don't collide. */\nexport function freshSessionName(currentName: string | undefined): string {\n const base = currentName ? currentName.replace(/-\\d{12,14}$/, \"\") : \"default\";\n const stamp = new Date().toISOString().replace(/[^\\d]/g, \"\").slice(0, 14);\n return `${base || \"default\"}-${stamp}`;\n}\n\n/** Names of `.jsonl` sessions starting with `prefix`, newest-first by filename. */\nexport function findSessionsByPrefix(prefix: string): string[] {\n const dir = sessionsDir();\n if (!existsSync(dir)) return [];\n try {\n const files = readdirSync(dir)\n .filter((f) => f.endsWith(\".jsonl\") && !f.endsWith(\".events.jsonl\") && f.startsWith(prefix))\n .sort()\n .reverse();\n return files.map((f) => f.replace(/\\.jsonl$/, \"\"));\n } catch {\n return [];\n }\n}\n\nexport interface SessionPreview {\n messageCount: number;\n lastActive: Date;\n}\n\n/** Resolve launch-time session: forceNew → timestamped suffix; else latest `${name}-*` if any, else base. Preview returned only on the default branch when messages exist. */\nexport function resolveSession(\n sessionName: string | undefined,\n forceNew?: boolean,\n forceResume?: boolean,\n): { resolved: string | undefined; preview: SessionPreview | undefined } {\n let resolved = sessionName;\n let preview: SessionPreview | undefined;\n\n if (sessionName && forceNew) {\n resolved = `${sessionName}-${timestampSuffix()}`;\n } else if (sessionName && !forceResume) {\n let sessionToCheck = sessionName;\n const prefixed = findSessionsByPrefix(`${sessionName}-`);\n if (prefixed.length > 0) {\n sessionToCheck = prefixed[0]!;\n }\n const prior = loadSessionMessages(sessionToCheck);\n if (prior.length > 0) {\n resolved = sessionToCheck;\n const p = sessionPath(sessionToCheck);\n const mtime = existsSync(p) ? statSync(p).mtime : new Date();\n preview = { messageCount: prior.length, lastActive: mtime };\n }\n } else if (sessionName && forceResume) {\n const prefixed = findSessionsByPrefix(`${sessionName}-`);\n if (prefixed.length > 0) {\n resolved = prefixed[0]!;\n }\n }\n\n return { resolved, preview };\n}\n\nexport function loadSessionMessages(name: string): ChatMessage[] {\n const path = sessionPath(name);\n if (!existsSync(path)) return [];\n try {\n const raw = readFileSync(path, \"utf8\");\n const out: ChatMessage[] = [];\n for (const line of raw.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n const msg = JSON.parse(trimmed) as ChatMessage;\n if (msg && typeof msg === \"object\" && \"role\" in msg) out.push(msg);\n } catch {\n /* skip malformed line */\n }\n }\n return out;\n } catch {\n return [];\n }\n}\n\nexport function appendSessionMessage(name: string, message: ChatMessage): void {\n const path = sessionPath(name);\n mkdirSync(dirname(path), { recursive: true });\n appendFileSync(path, `${JSON.stringify(message)}\\n`, \"utf8\");\n try {\n chmodSync(path, 0o600);\n } catch {\n /* chmod not supported on this platform */\n }\n}\n\nexport function listSessions(): SessionInfo[] {\n const dir = sessionsDir();\n if (!existsSync(dir)) return [];\n try {\n // Exclude `.events.jsonl` sidecars — they share the .jsonl suffix.\n const files = readdirSync(dir).filter(\n (f) => f.endsWith(\".jsonl\") && !f.endsWith(\".events.jsonl\"),\n );\n return files\n .map((file) => {\n const path = join(dir, file);\n const stat = statSync(path);\n const name = file.replace(/\\.jsonl$/, \"\");\n const messageCount = countLines(path);\n return {\n name,\n path,\n size: stat.size,\n messageCount,\n mtime: stat.mtime,\n meta: loadSessionMeta(name),\n };\n })\n .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n } catch {\n return [];\n }\n}\n\n/** Canonical form for workspace path comparisons — Windows drive-case + separator drift between session writes (yesterday) and reads (today) used to hide sessions from the sidebar. Issue #878. */\nexport function normalizeWorkspace(\n p: string | undefined,\n platform: NodeJS.Platform = process.platform,\n): string {\n if (typeof p !== \"string\" || p.length === 0) return \"\";\n if (platform === \"win32\") {\n const resolved = win32Path.resolve(p);\n return resolved\n .replace(/\\\\/g, \"/\")\n .replace(/^([A-Z]):/i, (_, d: string) => `${d.toLowerCase()}:`);\n }\n return posixPath.resolve(p);\n}\n\n/** Sessions without `meta.workspace` are still hidden — resume by name still works. */\nexport function listSessionsForWorkspace(workspace: string): SessionInfo[] {\n const want = normalizeWorkspace(workspace);\n return listSessions().filter(\n (s) => typeof s.meta.workspace === \"string\" && normalizeWorkspace(s.meta.workspace) === want,\n );\n}\n\nfunction metaPath(name: string): string {\n return join(sessionsDir(), `${sanitizeName(name)}.meta.json`);\n}\n\nexport function loadSessionMeta(name: string): SessionMeta {\n const p = metaPath(name);\n if (!existsSync(p)) return {};\n try {\n const raw = JSON.parse(readFileSync(p, \"utf8\")) as SessionMeta;\n return raw && typeof raw === \"object\" ? raw : {};\n } catch {\n return {};\n }\n}\n\nexport function patchSessionMeta(name: string, patch: Partial<SessionMeta>): SessionMeta {\n const cur = loadSessionMeta(name);\n const next: SessionMeta = { ...cur, ...patch };\n const p = metaPath(name);\n mkdirSync(dirname(p), { recursive: true });\n writeFileSync(p, JSON.stringify(next), \"utf8\");\n try {\n chmodSync(p, 0o600);\n } catch {\n /* chmod not supported */\n }\n return next;\n}\n\n/** Renames the JSONL plus all known sidecars together; returns false if target already exists. */\nexport function renameSession(oldName: string, newName: string): boolean {\n const safeOld = sanitizeName(oldName);\n const safeNew = sanitizeName(newName);\n if (safeOld === safeNew) return false;\n const oldJsonl = sessionPath(oldName);\n const newJsonl = sessionPath(newName);\n if (!existsSync(oldJsonl) || existsSync(newJsonl)) return false;\n renameSync(oldJsonl, newJsonl);\n for (const ext of [\".events.jsonl\", \".meta.json\", \".pending.json\", \".plan.json\"]) {\n const oldP = oldJsonl.replace(/\\.jsonl$/, ext);\n const newP = newJsonl.replace(/\\.jsonl$/, ext);\n if (existsSync(oldP)) {\n try {\n renameSync(oldP, newP);\n } catch {\n /* sidecar rename failed — leave the jsonl rename in place */\n }\n }\n }\n return true;\n}\n\n/** Best-effort: per-file delete errors are swallowed so partial pruning still finishes. */\nexport function pruneStaleSessions(daysOld = 90): string[] {\n const cutoff = Date.now() - daysOld * 24 * 60 * 60 * 1000;\n const deleted: string[] = [];\n for (const s of listSessions()) {\n if (s.mtime.getTime() < cutoff) {\n if (deleteSession(s.name)) deleted.push(s.name);\n }\n }\n return deleted;\n}\n\nexport function deleteSession(name: string): boolean {\n const path = sessionPath(name);\n try {\n unlinkSync(path);\n for (const ext of [\".events.jsonl\", \".pending.json\", \".meta.json\", \".plan.json\"]) {\n const sidecar = path.replace(/\\.jsonl$/, ext);\n try {\n unlinkSync(sidecar);\n } catch {\n /* expected when the sidecar doesn't exist */\n }\n }\n return true;\n } catch {\n return false;\n }\n}\n\n/** Non-atomic truncate+write window is acceptable — a concurrent crash leaves the session file empty, same end state as the user deleting it. */\nexport function rewriteSession(name: string, messages: ChatMessage[]): void {\n const path = sessionPath(name);\n mkdirSync(dirname(path), { recursive: true });\n const body = messages.map((m) => JSON.stringify(m)).join(\"\\n\");\n writeFileSync(path, body ? `${body}\\n` : \"\", \"utf8\");\n try {\n chmodSync(path, 0o600);\n } catch {\n /* chmod not supported */\n }\n}\n\n/** Rotate the live jsonl + sidecars to `<name>__archive_<ts>` so /new doesn't destroy history. Returns the archive name, or null if there was nothing to archive. */\nexport function archiveSession(name: string): string | null {\n const path = sessionPath(name);\n if (!existsSync(path)) return null;\n try {\n if (statSync(path).size === 0) return null;\n } catch {\n return null;\n }\n for (let attempt = 0; attempt < 5; attempt++) {\n const target = `${name}__archive_${timestampSuffix()}${attempt > 0 ? `_${attempt}` : \"\"}`;\n if (renameSession(name, target)) return target;\n }\n return null;\n}\n\nfunction countLines(path: string): number {\n try {\n const raw = readFileSync(path, \"utf8\");\n return raw.split(/\\r?\\n/).filter((l) => l.trim()).length;\n } catch {\n return 0;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __cr } from 'node:module'; if (typeof globalThis.require === 'undefined') { globalThis.require = __cr(import.meta.url); }
|
|
2
3
|
import {
|
|
3
4
|
t
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-CGDR2ELH.js";
|
|
5
6
|
|
|
6
7
|
// src/hooks.ts
|
|
7
8
|
import { spawn } from "child_process";
|
|
@@ -205,4 +206,4 @@ export {
|
|
|
205
206
|
formatHookOutcomeMessage,
|
|
206
207
|
runHooks
|
|
207
208
|
};
|
|
208
|
-
//# sourceMappingURL=chunk-
|
|
209
|
+
//# sourceMappingURL=chunk-7CIGMZT3.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hooks.ts"],"sourcesContent":["/** Shell-command hooks; project scope first, then global. Exit 0=pass, 2=block on Pre*, other=warn. */\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { t } from \"./i18n/index.js\";\n\nexport type HookEvent = \"PreToolUse\" | \"PostToolUse\" | \"UserPromptSubmit\" | \"Stop\";\n\n/** All four events as a const array — drives slash listing + validation. */\nexport const HOOK_EVENTS: readonly HookEvent[] = [\n \"PreToolUse\",\n \"PostToolUse\",\n \"UserPromptSubmit\",\n \"Stop\",\n] as const;\n\n/** Only the gating events can block the loop. */\nconst BLOCKING_EVENTS: ReadonlySet<HookEvent> = new Set([\"PreToolUse\", \"UserPromptSubmit\"]);\n\n/** Per-event default timeout. Tool/prompt hooks gate progress, so they're tight. */\nconst DEFAULT_TIMEOUTS_MS: Record<HookEvent, number> = {\n PreToolUse: 5_000,\n UserPromptSubmit: 5_000,\n PostToolUse: 30_000,\n Stop: 30_000,\n};\n\nexport type HookScope = \"project\" | \"global\";\n\nexport interface HookConfig {\n /** Anchored regex; `\"*\"` / omitted = every tool. Pre/PostToolUse only. */\n match?: string;\n /** Shell command to run. Spawned through the platform shell. */\n command: string;\n /** Optional human description — surfaced in `/hooks`. */\n description?: string;\n /** Per-hook timeout override in ms. */\n timeout?: number;\n /** Defaults: project scope → project root; global scope → process.cwd(). */\n cwd?: string;\n}\n\n/** Shape of `<scope>/.reasonix/settings.json` — only `hooks` for now. */\nexport interface HookSettings {\n hooks?: Partial<Record<HookEvent, HookConfig[]>>;\n}\n\n/** A loaded hook with its origin scope baked in (used for ordering and `/hooks`). */\nexport interface ResolvedHook extends HookConfig {\n event: HookEvent;\n scope: HookScope;\n /** Absolute path to the settings.json the hook came from. */\n source: string;\n}\n\n/** Outcome of a single hook invocation. */\nexport interface HookOutcome {\n /** Which hook fired. */\n hook: ResolvedHook;\n /** pass=exit 0; block=exit 2 on blocking event; warn=other non-zero; timeout=killed; error=spawn failed. */\n decision: \"pass\" | \"block\" | \"warn\" | \"timeout\" | \"error\";\n exitCode: number | null;\n /** Captured stdout (trimmed). May be empty. */\n stdout: string;\n /** Captured stderr (trimmed). The block / warn message comes from here. */\n stderr: string;\n durationMs: number;\n /** Output crossed the per-stream byte cap; surfaced so user knows we kept less than the script wrote. */\n truncated?: boolean;\n}\n\n/** Aggregate report for `runHooks`. */\nexport interface HookReport {\n event: HookEvent;\n outcomes: HookOutcome[];\n /** True iff at least one outcome was a `block` — only meaningful for blocking events. */\n blocked: boolean;\n}\n\nexport const HOOK_SETTINGS_FILENAME = \"settings.json\";\nexport const HOOK_SETTINGS_DIRNAME = \".reasonix\";\n\n/** Where the global settings.json lives. Equivalent to `~/.reasonix/settings.json`. */\nexport function globalSettingsPath(homeDirOverride?: string): string {\n return join(homeDirOverride ?? homedir(), HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);\n}\n\n/** Where the project settings.json lives for a given root. */\nexport function projectSettingsPath(projectRoot: string): string {\n return join(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);\n}\n\nfunction readSettingsFile(path: string): HookSettings | null {\n if (!existsSync(path)) return null;\n try {\n const raw = readFileSync(path, \"utf8\");\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") return parsed as HookSettings;\n } catch {\n /* malformed JSON → treat as no hooks; do NOT throw, the user\n * shouldn't lose the whole CLI to a typo in their settings */\n }\n return null;\n}\n\n/** Project hooks fire before global; within a scope, array order. */\nexport interface LoadHookSettingsOptions {\n /** Absolute project root, if any. Without it, only global hooks load. */\n projectRoot?: string;\n /** Override `~` for tests. */\n homeDir?: string;\n}\n\nexport function loadHooks(opts: LoadHookSettingsOptions = {}): ResolvedHook[] {\n const out: ResolvedHook[] = [];\n if (opts.projectRoot) {\n const projPath = projectSettingsPath(opts.projectRoot);\n const settings = readSettingsFile(projPath);\n if (settings) appendResolved(out, settings, \"project\", projPath);\n }\n const globalPath = globalSettingsPath(opts.homeDir);\n const settings = readSettingsFile(globalPath);\n if (settings) appendResolved(out, settings, \"global\", globalPath);\n return out;\n}\n\nfunction appendResolved(\n out: ResolvedHook[],\n settings: HookSettings,\n scope: HookScope,\n source: string,\n): void {\n if (!settings.hooks) return;\n for (const event of HOOK_EVENTS) {\n const list = settings.hooks[event];\n if (!Array.isArray(list)) continue;\n for (const cfg of list) {\n if (!cfg || typeof cfg.command !== \"string\" || cfg.command.trim() === \"\") continue;\n out.push({ ...cfg, event, scope, source });\n }\n }\n}\n\n/** Match field is an ANCHORED regex — `\"file\"` won't trigger on `read_file`; use `\".*file\"`. */\nexport function matchesTool(hook: ResolvedHook, toolName: string): boolean {\n if (hook.event !== \"PreToolUse\" && hook.event !== \"PostToolUse\") return true;\n const m = hook.match;\n if (!m || m === \"*\") return true;\n try {\n const re = new RegExp(`^(?:${m})$`);\n return re.test(toolName);\n } catch {\n /* malformed regex → don't fire (safer than firing on every tool) */\n return false;\n }\n}\n\n/** Payload envelope passed to hook stdin. */\nexport interface HookPayload {\n event: HookEvent;\n cwd: string;\n toolName?: string;\n toolArgs?: unknown;\n toolResult?: string;\n prompt?: string;\n lastAssistantText?: string;\n turn?: number;\n}\n\n/** Test seam — same shape as Node's spawn but returns a Promise of the raw outcome bits. */\nexport interface HookSpawnInput {\n command: string;\n cwd: string;\n stdin: string;\n timeoutMs: number;\n}\n\nexport interface HookSpawnResult {\n exitCode: number | null;\n stdout: string;\n stderr: string;\n timedOut: boolean;\n /** True iff spawn() itself failed (ENOENT, EACCES, …). */\n spawnError?: Error;\n /** Output capped at byte limit — hook ran to completion but consumers see clipped view. */\n truncated?: boolean;\n}\n\n/** Per-stream cap — bounds heap exposure to a runaway child between spawn and timeout. */\nconst HOOK_OUTPUT_CAP_BYTES = 256 * 1024;\n\nexport type HookSpawner = (input: HookSpawnInput) => Promise<HookSpawnResult>;\n\n/** `shell: true` — hook is a shell command by contract; pipes / `&&` / env expansion must work. */\nfunction defaultSpawner(input: HookSpawnInput): Promise<HookSpawnResult> {\n return new Promise<HookSpawnResult>((resolve) => {\n const child = spawn(input.command, {\n cwd: input.cwd,\n shell: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n // Collect raw bytes per stream and decode once at close so a\n // multi-byte UTF-8 sequence split across data chunks doesn't\n // corrupt — same approach shell.ts uses for run_command output.\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let stdoutBytes = 0;\n let stderrBytes = 0;\n let truncated = false;\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n child.kill(\"SIGTERM\");\n // SIGTERM may not land on Windows for shell children — followed\n // by a hard kill a moment later if the process is still around.\n setTimeout(() => {\n try {\n child.kill(\"SIGKILL\");\n } catch {\n /* already gone */\n }\n }, 500);\n }, input.timeoutMs);\n\n const onChunk = (kind: \"stdout\" | \"stderr\", chunk: Buffer) => {\n const target = kind === \"stdout\" ? stdoutChunks : stderrChunks;\n const seen = kind === \"stdout\" ? stdoutBytes : stderrBytes;\n if (seen >= HOOK_OUTPUT_CAP_BYTES) {\n truncated = true;\n return;\n }\n const remaining = HOOK_OUTPUT_CAP_BYTES - seen;\n if (chunk.length > remaining) {\n target.push(chunk.subarray(0, remaining));\n if (kind === \"stdout\") stdoutBytes = HOOK_OUTPUT_CAP_BYTES;\n else stderrBytes = HOOK_OUTPUT_CAP_BYTES;\n truncated = true;\n } else {\n target.push(chunk);\n if (kind === \"stdout\") stdoutBytes += chunk.length;\n else stderrBytes += chunk.length;\n }\n };\n child.stdout.on(\"data\", (chunk: Buffer) => onChunk(\"stdout\", chunk));\n child.stderr.on(\"data\", (chunk: Buffer) => onChunk(\"stderr\", chunk));\n child.once(\"error\", (err) => {\n clearTimeout(timer);\n resolve({\n exitCode: null,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n timedOut: false,\n spawnError: err,\n truncated: truncated || undefined,\n });\n });\n child.once(\"close\", (code) => {\n clearTimeout(timer);\n resolve({\n exitCode: code,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\").trim(),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\").trim(),\n timedOut,\n truncated: truncated || undefined,\n });\n });\n\n try {\n child.stdin.write(input.stdin);\n child.stdin.end();\n } catch {\n /* stdin write can race with spawn errors; the close handler\n * still fires with exit 0/null */\n }\n });\n}\n\nexport function formatHookOutcomeMessage(outcome: HookOutcome): string {\n if (outcome.decision === \"pass\") return \"\";\n const detail = (outcome.stderr || outcome.stdout || \"\").trim();\n const tag = `${outcome.hook.scope}/${outcome.hook.event}`;\n const cmd =\n outcome.hook.command.length > 60\n ? `${outcome.hook.command.slice(0, 60)}…`\n : outcome.hook.command;\n const truncTag = outcome.truncated ? t(\"hooks.truncated\") : \"\";\n const decision = t(`hooks.decision${capitalize(outcome.decision)}`);\n return detail\n ? t(\"hooks.headWithDetail\", { tag, cmd, decision, truncTag, detail })\n : t(\"hooks.head\", { tag, cmd, decision, truncTag });\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\nexport function decideOutcome(\n event: HookEvent,\n raw: HookSpawnResult,\n): \"pass\" | \"block\" | \"warn\" | \"timeout\" | \"error\" {\n if (raw.spawnError) return \"error\";\n if (raw.timedOut) return BLOCKING_EVENTS.has(event) ? \"block\" : \"warn\";\n if (raw.exitCode === 0) return \"pass\";\n if (raw.exitCode === 2 && BLOCKING_EVENTS.has(event)) return \"block\";\n return \"warn\";\n}\n\nexport interface RunHooksOptions {\n payload: HookPayload;\n hooks: ResolvedHook[];\n /** Test seam — defaults to a real `spawn`. */\n spawner?: HookSpawner;\n}\n\n/** Stops at first `block` so a gating hook can prevent later hooks running against a phantom success. */\nexport async function runHooks(opts: RunHooksOptions): Promise<HookReport> {\n const spawner = opts.spawner ?? defaultSpawner;\n const event = opts.payload.event;\n const toolName = opts.payload.toolName ?? \"\";\n const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));\n\n const outcomes: HookOutcome[] = [];\n let blocked = false;\n const stdin = `${JSON.stringify(opts.payload)}\\n`;\n\n for (const hook of matching) {\n const start = Date.now();\n const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];\n const cwd = hook.cwd ?? opts.payload.cwd;\n const raw = await spawner({ command: hook.command, cwd, stdin, timeoutMs });\n const decision = decideOutcome(event, raw);\n outcomes.push({\n hook,\n decision,\n exitCode: raw.exitCode,\n stdout: raw.stdout,\n stderr:\n raw.stderr ||\n (raw.spawnError ? raw.spawnError.message : \"\") ||\n (raw.timedOut ? `hook timed out after ${timeoutMs}ms` : \"\"),\n durationMs: Date.now() - start,\n truncated: raw.truncated,\n });\n if (decision === \"block\") {\n blocked = true;\n break;\n }\n }\n\n return { event, outcomes, blocked };\n}\n"],"mappings":"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","names":["settings"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hooks.ts"],"sourcesContent":["/** Shell-command hooks; project scope first, then global. Exit 0=pass, 2=block on Pre*, other=warn. */\n\nimport { spawn } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { t } from \"./i18n/index.js\";\n\nexport type HookEvent = \"PreToolUse\" | \"PostToolUse\" | \"UserPromptSubmit\" | \"Stop\";\n\n/** All four events as a const array — drives slash listing + validation. */\nexport const HOOK_EVENTS: readonly HookEvent[] = [\n \"PreToolUse\",\n \"PostToolUse\",\n \"UserPromptSubmit\",\n \"Stop\",\n] as const;\n\n/** Only the gating events can block the loop. */\nconst BLOCKING_EVENTS: ReadonlySet<HookEvent> = new Set([\"PreToolUse\", \"UserPromptSubmit\"]);\n\n/** Per-event default timeout. Tool/prompt hooks gate progress, so they're tight. */\nconst DEFAULT_TIMEOUTS_MS: Record<HookEvent, number> = {\n PreToolUse: 5_000,\n UserPromptSubmit: 5_000,\n PostToolUse: 30_000,\n Stop: 30_000,\n};\n\nexport type HookScope = \"project\" | \"global\";\n\nexport interface HookConfig {\n /** Anchored regex; `\"*\"` / omitted = every tool. Pre/PostToolUse only. */\n match?: string;\n /** Shell command to run. Spawned through the platform shell. */\n command: string;\n /** Optional human description — surfaced in `/hooks`. */\n description?: string;\n /** Per-hook timeout override in ms. */\n timeout?: number;\n /** Defaults: project scope → project root; global scope → process.cwd(). */\n cwd?: string;\n}\n\n/** Shape of `<scope>/.reasonix/settings.json` — only `hooks` for now. */\nexport interface HookSettings {\n hooks?: Partial<Record<HookEvent, HookConfig[]>>;\n}\n\n/** A loaded hook with its origin scope baked in (used for ordering and `/hooks`). */\nexport interface ResolvedHook extends HookConfig {\n event: HookEvent;\n scope: HookScope;\n /** Absolute path to the settings.json the hook came from. */\n source: string;\n}\n\n/** Outcome of a single hook invocation. */\nexport interface HookOutcome {\n /** Which hook fired. */\n hook: ResolvedHook;\n /** pass=exit 0; block=exit 2 on blocking event; warn=other non-zero; timeout=killed; error=spawn failed. */\n decision: \"pass\" | \"block\" | \"warn\" | \"timeout\" | \"error\";\n exitCode: number | null;\n /** Captured stdout (trimmed). May be empty. */\n stdout: string;\n /** Captured stderr (trimmed). The block / warn message comes from here. */\n stderr: string;\n durationMs: number;\n /** Output crossed the per-stream byte cap; surfaced so user knows we kept less than the script wrote. */\n truncated?: boolean;\n}\n\n/** Aggregate report for `runHooks`. */\nexport interface HookReport {\n event: HookEvent;\n outcomes: HookOutcome[];\n /** True iff at least one outcome was a `block` — only meaningful for blocking events. */\n blocked: boolean;\n}\n\nexport const HOOK_SETTINGS_FILENAME = \"settings.json\";\nexport const HOOK_SETTINGS_DIRNAME = \".reasonix\";\n\n/** Where the global settings.json lives. Equivalent to `~/.reasonix/settings.json`. */\nexport function globalSettingsPath(homeDirOverride?: string): string {\n return join(homeDirOverride ?? homedir(), HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);\n}\n\n/** Where the project settings.json lives for a given root. */\nexport function projectSettingsPath(projectRoot: string): string {\n return join(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);\n}\n\nfunction readSettingsFile(path: string): HookSettings | null {\n if (!existsSync(path)) return null;\n try {\n const raw = readFileSync(path, \"utf8\");\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === \"object\") return parsed as HookSettings;\n } catch {\n /* malformed JSON → treat as no hooks; do NOT throw, the user\n * shouldn't lose the whole CLI to a typo in their settings */\n }\n return null;\n}\n\n/** Project hooks fire before global; within a scope, array order. */\nexport interface LoadHookSettingsOptions {\n /** Absolute project root, if any. Without it, only global hooks load. */\n projectRoot?: string;\n /** Override `~` for tests. */\n homeDir?: string;\n}\n\nexport function loadHooks(opts: LoadHookSettingsOptions = {}): ResolvedHook[] {\n const out: ResolvedHook[] = [];\n if (opts.projectRoot) {\n const projPath = projectSettingsPath(opts.projectRoot);\n const settings = readSettingsFile(projPath);\n if (settings) appendResolved(out, settings, \"project\", projPath);\n }\n const globalPath = globalSettingsPath(opts.homeDir);\n const settings = readSettingsFile(globalPath);\n if (settings) appendResolved(out, settings, \"global\", globalPath);\n return out;\n}\n\nfunction appendResolved(\n out: ResolvedHook[],\n settings: HookSettings,\n scope: HookScope,\n source: string,\n): void {\n if (!settings.hooks) return;\n for (const event of HOOK_EVENTS) {\n const list = settings.hooks[event];\n if (!Array.isArray(list)) continue;\n for (const cfg of list) {\n if (!cfg || typeof cfg.command !== \"string\" || cfg.command.trim() === \"\") continue;\n out.push({ ...cfg, event, scope, source });\n }\n }\n}\n\n/** Match field is an ANCHORED regex — `\"file\"` won't trigger on `read_file`; use `\".*file\"`. */\nexport function matchesTool(hook: ResolvedHook, toolName: string): boolean {\n if (hook.event !== \"PreToolUse\" && hook.event !== \"PostToolUse\") return true;\n const m = hook.match;\n if (!m || m === \"*\") return true;\n try {\n const re = new RegExp(`^(?:${m})$`);\n return re.test(toolName);\n } catch {\n /* malformed regex → don't fire (safer than firing on every tool) */\n return false;\n }\n}\n\n/** Payload envelope passed to hook stdin. */\nexport interface HookPayload {\n event: HookEvent;\n cwd: string;\n toolName?: string;\n toolArgs?: unknown;\n toolResult?: string;\n prompt?: string;\n lastAssistantText?: string;\n turn?: number;\n}\n\n/** Test seam — same shape as Node's spawn but returns a Promise of the raw outcome bits. */\nexport interface HookSpawnInput {\n command: string;\n cwd: string;\n stdin: string;\n timeoutMs: number;\n}\n\nexport interface HookSpawnResult {\n exitCode: number | null;\n stdout: string;\n stderr: string;\n timedOut: boolean;\n /** True iff spawn() itself failed (ENOENT, EACCES, …). */\n spawnError?: Error;\n /** Output capped at byte limit — hook ran to completion but consumers see clipped view. */\n truncated?: boolean;\n}\n\n/** Per-stream cap — bounds heap exposure to a runaway child between spawn and timeout. */\nconst HOOK_OUTPUT_CAP_BYTES = 256 * 1024;\n\nexport type HookSpawner = (input: HookSpawnInput) => Promise<HookSpawnResult>;\n\n/** `shell: true` — hook is a shell command by contract; pipes / `&&` / env expansion must work. */\nfunction defaultSpawner(input: HookSpawnInput): Promise<HookSpawnResult> {\n return new Promise<HookSpawnResult>((resolve) => {\n const child = spawn(input.command, {\n cwd: input.cwd,\n shell: true,\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n // Collect raw bytes per stream and decode once at close so a\n // multi-byte UTF-8 sequence split across data chunks doesn't\n // corrupt — same approach shell.ts uses for run_command output.\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let stdoutBytes = 0;\n let stderrBytes = 0;\n let truncated = false;\n let timedOut = false;\n const timer = setTimeout(() => {\n timedOut = true;\n child.kill(\"SIGTERM\");\n // SIGTERM may not land on Windows for shell children — followed\n // by a hard kill a moment later if the process is still around.\n setTimeout(() => {\n try {\n child.kill(\"SIGKILL\");\n } catch {\n /* already gone */\n }\n }, 500);\n }, input.timeoutMs);\n\n const onChunk = (kind: \"stdout\" | \"stderr\", chunk: Buffer) => {\n const target = kind === \"stdout\" ? stdoutChunks : stderrChunks;\n const seen = kind === \"stdout\" ? stdoutBytes : stderrBytes;\n if (seen >= HOOK_OUTPUT_CAP_BYTES) {\n truncated = true;\n return;\n }\n const remaining = HOOK_OUTPUT_CAP_BYTES - seen;\n if (chunk.length > remaining) {\n target.push(chunk.subarray(0, remaining));\n if (kind === \"stdout\") stdoutBytes = HOOK_OUTPUT_CAP_BYTES;\n else stderrBytes = HOOK_OUTPUT_CAP_BYTES;\n truncated = true;\n } else {\n target.push(chunk);\n if (kind === \"stdout\") stdoutBytes += chunk.length;\n else stderrBytes += chunk.length;\n }\n };\n child.stdout.on(\"data\", (chunk: Buffer) => onChunk(\"stdout\", chunk));\n child.stderr.on(\"data\", (chunk: Buffer) => onChunk(\"stderr\", chunk));\n child.once(\"error\", (err) => {\n clearTimeout(timer);\n resolve({\n exitCode: null,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\"),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\"),\n timedOut: false,\n spawnError: err,\n truncated: truncated || undefined,\n });\n });\n child.once(\"close\", (code) => {\n clearTimeout(timer);\n resolve({\n exitCode: code,\n stdout: Buffer.concat(stdoutChunks).toString(\"utf8\").trim(),\n stderr: Buffer.concat(stderrChunks).toString(\"utf8\").trim(),\n timedOut,\n truncated: truncated || undefined,\n });\n });\n\n try {\n child.stdin.write(input.stdin);\n child.stdin.end();\n } catch {\n /* stdin write can race with spawn errors; the close handler\n * still fires with exit 0/null */\n }\n });\n}\n\nexport function formatHookOutcomeMessage(outcome: HookOutcome): string {\n if (outcome.decision === \"pass\") return \"\";\n const detail = (outcome.stderr || outcome.stdout || \"\").trim();\n const tag = `${outcome.hook.scope}/${outcome.hook.event}`;\n const cmd =\n outcome.hook.command.length > 60\n ? `${outcome.hook.command.slice(0, 60)}…`\n : outcome.hook.command;\n const truncTag = outcome.truncated ? t(\"hooks.truncated\") : \"\";\n const decision = t(`hooks.decision${capitalize(outcome.decision)}`);\n return detail\n ? t(\"hooks.headWithDetail\", { tag, cmd, decision, truncTag, detail })\n : t(\"hooks.head\", { tag, cmd, decision, truncTag });\n}\n\nfunction capitalize(s: string): string {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n\nexport function decideOutcome(\n event: HookEvent,\n raw: HookSpawnResult,\n): \"pass\" | \"block\" | \"warn\" | \"timeout\" | \"error\" {\n if (raw.spawnError) return \"error\";\n if (raw.timedOut) return BLOCKING_EVENTS.has(event) ? \"block\" : \"warn\";\n if (raw.exitCode === 0) return \"pass\";\n if (raw.exitCode === 2 && BLOCKING_EVENTS.has(event)) return \"block\";\n return \"warn\";\n}\n\nexport interface RunHooksOptions {\n payload: HookPayload;\n hooks: ResolvedHook[];\n /** Test seam — defaults to a real `spawn`. */\n spawner?: HookSpawner;\n}\n\n/** Stops at first `block` so a gating hook can prevent later hooks running against a phantom success. */\nexport async function runHooks(opts: RunHooksOptions): Promise<HookReport> {\n const spawner = opts.spawner ?? defaultSpawner;\n const event = opts.payload.event;\n const toolName = opts.payload.toolName ?? \"\";\n const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));\n\n const outcomes: HookOutcome[] = [];\n let blocked = false;\n const stdin = `${JSON.stringify(opts.payload)}\\n`;\n\n for (const hook of matching) {\n const start = Date.now();\n const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];\n const cwd = hook.cwd ?? opts.payload.cwd;\n const raw = await spawner({ command: hook.command, cwd, stdin, timeoutMs });\n const decision = decideOutcome(event, raw);\n outcomes.push({\n hook,\n decision,\n exitCode: raw.exitCode,\n stdout: raw.stdout,\n stderr:\n raw.stderr ||\n (raw.spawnError ? raw.spawnError.message : \"\") ||\n (raw.timedOut ? `hook timed out after ${timeoutMs}ms` : \"\"),\n durationMs: Date.now() - start,\n truncated: raw.truncated,\n });\n if (decision === \"block\") {\n blocked = true;\n break;\n }\n }\n\n return { event, outcomes, blocked };\n}\n"],"mappings":";;;;;;;AAEA,SAAS,aAAa;AACtB,SAAS,YAAY,oBAAoB;AACzC,SAAS,eAAe;AACxB,SAAS,YAAY;AAMd,IAAM,cAAoC;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGA,IAAM,kBAA0C,oBAAI,IAAI,CAAC,cAAc,kBAAkB,CAAC;AAG1F,IAAM,sBAAiD;AAAA,EACrD,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb,MAAM;AACR;AAsDO,IAAM,yBAAyB;AAC/B,IAAM,wBAAwB;AAG9B,SAAS,mBAAmB,iBAAkC;AACnE,SAAO,KAAK,mBAAmB,QAAQ,GAAG,uBAAuB,sBAAsB;AACzF;AAGO,SAAS,oBAAoB,aAA6B;AAC/D,SAAO,KAAK,aAAa,uBAAuB,sBAAsB;AACxE;AAEA,SAAS,iBAAiB,MAAmC;AAC3D,MAAI,CAAC,WAAW,IAAI,EAAG,QAAO;AAC9B,MAAI;AACF,UAAM,MAAM,aAAa,MAAM,MAAM;AACrC,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,UAAU,OAAO,WAAW,SAAU,QAAO;AAAA,EACnD,QAAQ;AAAA,EAGR;AACA,SAAO;AACT;AAUO,SAAS,UAAU,OAAgC,CAAC,GAAmB;AAC5E,QAAM,MAAsB,CAAC;AAC7B,MAAI,KAAK,aAAa;AACpB,UAAM,WAAW,oBAAoB,KAAK,WAAW;AACrD,UAAMA,YAAW,iBAAiB,QAAQ;AAC1C,QAAIA,UAAU,gBAAe,KAAKA,WAAU,WAAW,QAAQ;AAAA,EACjE;AACA,QAAM,aAAa,mBAAmB,KAAK,OAAO;AAClD,QAAM,WAAW,iBAAiB,UAAU;AAC5C,MAAI,SAAU,gBAAe,KAAK,UAAU,UAAU,UAAU;AAChE,SAAO;AACT;AAEA,SAAS,eACP,KACA,UACA,OACA,QACM;AACN,MAAI,CAAC,SAAS,MAAO;AACrB,aAAW,SAAS,aAAa;AAC/B,UAAM,OAAO,SAAS,MAAM,KAAK;AACjC,QAAI,CAAC,MAAM,QAAQ,IAAI,EAAG;AAC1B,eAAW,OAAO,MAAM;AACtB,UAAI,CAAC,OAAO,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,KAAK,MAAM,GAAI;AAC1E,UAAI,KAAK,EAAE,GAAG,KAAK,OAAO,OAAO,OAAO,CAAC;AAAA,IAC3C;AAAA,EACF;AACF;AAGO,SAAS,YAAY,MAAoB,UAA2B;AACzE,MAAI,KAAK,UAAU,gBAAgB,KAAK,UAAU,cAAe,QAAO;AACxE,QAAM,IAAI,KAAK;AACf,MAAI,CAAC,KAAK,MAAM,IAAK,QAAO;AAC5B,MAAI;AACF,UAAM,KAAK,IAAI,OAAO,OAAO,CAAC,IAAI;AAClC,WAAO,GAAG,KAAK,QAAQ;AAAA,EACzB,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAkCA,IAAM,wBAAwB,MAAM;AAKpC,SAAS,eAAe,OAAiD;AACvE,SAAO,IAAI,QAAyB,CAAC,YAAY;AAC/C,UAAM,QAAQ,MAAM,MAAM,SAAS;AAAA,MACjC,KAAK,MAAM;AAAA,MACX,OAAO;AAAA,MACP,OAAO,CAAC,QAAQ,QAAQ,MAAM;AAAA,IAChC,CAAC;AAID,UAAM,eAAyB,CAAC;AAChC,UAAM,eAAyB,CAAC;AAChC,QAAI,cAAc;AAClB,QAAI,cAAc;AAClB,QAAI,YAAY;AAChB,QAAI,WAAW;AACf,UAAM,QAAQ,WAAW,MAAM;AAC7B,iBAAW;AACX,YAAM,KAAK,SAAS;AAGpB,iBAAW,MAAM;AACf,YAAI;AACF,gBAAM,KAAK,SAAS;AAAA,QACtB,QAAQ;AAAA,QAER;AAAA,MACF,GAAG,GAAG;AAAA,IACR,GAAG,MAAM,SAAS;AAElB,UAAM,UAAU,CAAC,MAA2B,UAAkB;AAC5D,YAAM,SAAS,SAAS,WAAW,eAAe;AAClD,YAAM,OAAO,SAAS,WAAW,cAAc;AAC/C,UAAI,QAAQ,uBAAuB;AACjC,oBAAY;AACZ;AAAA,MACF;AACA,YAAM,YAAY,wBAAwB;AAC1C,UAAI,MAAM,SAAS,WAAW;AAC5B,eAAO,KAAK,MAAM,SAAS,GAAG,SAAS,CAAC;AACxC,YAAI,SAAS,SAAU,eAAc;AAAA,YAChC,eAAc;AACnB,oBAAY;AAAA,MACd,OAAO;AACL,eAAO,KAAK,KAAK;AACjB,YAAI,SAAS,SAAU,gBAAe,MAAM;AAAA,YACvC,gBAAe,MAAM;AAAA,MAC5B;AAAA,IACF;AACA,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAkB,QAAQ,UAAU,KAAK,CAAC;AACnE,UAAM,OAAO,GAAG,QAAQ,CAAC,UAAkB,QAAQ,UAAU,KAAK,CAAC;AACnE,UAAM,KAAK,SAAS,CAAC,QAAQ;AAC3B,mBAAa,KAAK;AAClB,cAAQ;AAAA,QACN,UAAU;AAAA,QACV,QAAQ,OAAO,OAAO,YAAY,EAAE,SAAS,MAAM;AAAA,QACnD,QAAQ,OAAO,OAAO,YAAY,EAAE,SAAS,MAAM;AAAA,QACnD,UAAU;AAAA,QACV,YAAY;AAAA,QACZ,WAAW,aAAa;AAAA,MAC1B,CAAC;AAAA,IACH,CAAC;AACD,UAAM,KAAK,SAAS,CAAC,SAAS;AAC5B,mBAAa,KAAK;AAClB,cAAQ;AAAA,QACN,UAAU;AAAA,QACV,QAAQ,OAAO,OAAO,YAAY,EAAE,SAAS,MAAM,EAAE,KAAK;AAAA,QAC1D,QAAQ,OAAO,OAAO,YAAY,EAAE,SAAS,MAAM,EAAE,KAAK;AAAA,QAC1D;AAAA,QACA,WAAW,aAAa;AAAA,MAC1B,CAAC;AAAA,IACH,CAAC;AAED,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,KAAK;AAC7B,YAAM,MAAM,IAAI;AAAA,IAClB,QAAQ;AAAA,IAGR;AAAA,EACF,CAAC;AACH;AAEO,SAAS,yBAAyB,SAA8B;AACrE,MAAI,QAAQ,aAAa,OAAQ,QAAO;AACxC,QAAM,UAAU,QAAQ,UAAU,QAAQ,UAAU,IAAI,KAAK;AAC7D,QAAM,MAAM,GAAG,QAAQ,KAAK,KAAK,IAAI,QAAQ,KAAK,KAAK;AACvD,QAAM,MACJ,QAAQ,KAAK,QAAQ,SAAS,KAC1B,GAAG,QAAQ,KAAK,QAAQ,MAAM,GAAG,EAAE,CAAC,WACpC,QAAQ,KAAK;AACnB,QAAM,WAAW,QAAQ,YAAY,EAAE,iBAAiB,IAAI;AAC5D,QAAM,WAAW,EAAE,iBAAiB,WAAW,QAAQ,QAAQ,CAAC,EAAE;AAClE,SAAO,SACH,EAAE,wBAAwB,EAAE,KAAK,KAAK,UAAU,UAAU,OAAO,CAAC,IAClE,EAAE,cAAc,EAAE,KAAK,KAAK,UAAU,SAAS,CAAC;AACtD;AAEA,SAAS,WAAW,GAAmB;AACrC,SAAO,EAAE,OAAO,CAAC,EAAE,YAAY,IAAI,EAAE,MAAM,CAAC;AAC9C;AAEO,SAAS,cACd,OACA,KACiD;AACjD,MAAI,IAAI,WAAY,QAAO;AAC3B,MAAI,IAAI,SAAU,QAAO,gBAAgB,IAAI,KAAK,IAAI,UAAU;AAChE,MAAI,IAAI,aAAa,EAAG,QAAO;AAC/B,MAAI,IAAI,aAAa,KAAK,gBAAgB,IAAI,KAAK,EAAG,QAAO;AAC7D,SAAO;AACT;AAUA,eAAsB,SAAS,MAA4C;AACzE,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,QAAQ,KAAK,QAAQ;AAC3B,QAAM,WAAW,KAAK,QAAQ,YAAY;AAC1C,QAAM,WAAW,KAAK,MAAM,OAAO,CAAC,MAAM,EAAE,UAAU,SAAS,YAAY,GAAG,QAAQ,CAAC;AAEvF,QAAM,WAA0B,CAAC;AACjC,MAAI,UAAU;AACd,QAAM,QAAQ,GAAG,KAAK,UAAU,KAAK,OAAO,CAAC;AAAA;AAE7C,aAAW,QAAQ,UAAU;AAC3B,UAAM,QAAQ,KAAK,IAAI;AACvB,UAAM,YAAY,KAAK,WAAW,oBAAoB,KAAK;AAC3D,UAAM,MAAM,KAAK,OAAO,KAAK,QAAQ;AACrC,UAAM,MAAM,MAAM,QAAQ,EAAE,SAAS,KAAK,SAAS,KAAK,OAAO,UAAU,CAAC;AAC1E,UAAM,WAAW,cAAc,OAAO,GAAG;AACzC,aAAS,KAAK;AAAA,MACZ;AAAA,MACA;AAAA,MACA,UAAU,IAAI;AAAA,MACd,QAAQ,IAAI;AAAA,MACZ,QACE,IAAI,WACH,IAAI,aAAa,IAAI,WAAW,UAAU,QAC1C,IAAI,WAAW,wBAAwB,SAAS,OAAO;AAAA,MAC1D,YAAY,KAAK,IAAI,IAAI;AAAA,MACzB,WAAW,IAAI;AAAA,IACjB,CAAC;AACD,QAAI,aAAa,SAAS;AACxB,gBAAU;AACV;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,UAAU,QAAQ;AACpC;","names":["settings"]}
|