jinzd-ai-cli 0.4.232 → 0.4.234

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/{batch-D6XYKHHA.js → batch-T3FRX4B6.js} +2 -2
  2. package/dist/{chat-index-O56HDGFI.js → chat-index-7HXBWQFH.js} +2 -2
  3. package/dist/{chat-index-WSI7ULRQ.js → chat-index-WYXYD7YP.js} +1 -1
  4. package/dist/{chunk-4KM53GLG.js → chunk-2QQKS56X.js} +1 -1
  5. package/dist/{chunk-EPDW73H7.js → chunk-3KTSJ2EL.js} +1 -1
  6. package/dist/{chunk-KIQ2ICMV.js → chunk-5DTEEWYC.js} +4 -4
  7. package/dist/{chunk-NVUCDUXE.js → chunk-5MYPIQ3Y.js} +11 -5
  8. package/dist/{chunk-YDGKT6RI.js → chunk-5VPRZMZB.js} +2 -2
  9. package/dist/{chunk-RRMLRCPW.js → chunk-6MJMHHRK.js} +4 -4
  10. package/dist/{chunk-2FZACYUE.js → chunk-AFT6VFMI.js} +1 -1
  11. package/dist/{chunk-TJVM535A.js → chunk-E5RLXMSU.js} +6 -6
  12. package/dist/{chunk-XVR4YKRC.js → chunk-ETJ5X6V7.js} +1 -1
  13. package/dist/{chunk-6VSAKQTN.js → chunk-II6RZRTF.js} +1 -1
  14. package/dist/{chunk-ZWU7FWHO.js → chunk-JMP3LJC2.js} +38 -91
  15. package/dist/chunk-KCEO2XJ4.js +420 -0
  16. package/dist/{chunk-LKTYTDEJ.js → chunk-L5PNAQ6O.js} +1 -1
  17. package/dist/{chunk-ODYTXQ26.js → chunk-O3XSFPYH.js} +35 -3
  18. package/dist/{chunk-62R7CACG.js → chunk-QYQI7ZWK.js} +1 -1
  19. package/dist/{chunk-2HWL45PG.js → chunk-TAXIG7HH.js} +1 -1
  20. package/dist/{chunk-LF3OUJHK.js → chunk-UO3XWZFE.js} +302 -4
  21. package/dist/{ci-OR57Z7QF.js → ci-GJ53GPLA.js} +6 -6
  22. package/dist/{ci-format-DMMS2ZFK.js → ci-format-AXE4QCI6.js} +2 -2
  23. package/dist/{constants-7GZKINR5.js → constants-DC4EXFSB.js} +1 -1
  24. package/dist/{doctor-cli-ZD37GAZD.js → doctor-cli-HKREND6Q.js} +6 -6
  25. package/dist/electron-server.js +78 -99
  26. package/dist/{hub-VFYFUSM6.js → hub-LTTKRGZC.js} +2 -2
  27. package/dist/index.js +24 -24
  28. package/dist/{persist-I3JI27OK.js → persist-AMH5SQ4W.js} +3 -3
  29. package/dist/{pr-YF6TXJYE.js → pr-4VODJVRV.js} +6 -6
  30. package/dist/{run-tests-HEDDC6DJ.js → run-tests-G5BNTMWQ.js} +2 -2
  31. package/dist/{run-tests-BNZZDCTU.js → run-tests-QR2PYR2P.js} +1 -1
  32. package/dist/{server-HCDI2EID.js → server-64WZVZZN.js} +17 -17
  33. package/dist/{server-C52P6IDO.js → server-EO6VQHKL.js} +7 -7
  34. package/dist/{task-orchestrator-QIEHYRGK.js → task-orchestrator-4INCZMLT.js} +7 -7
  35. package/dist/{usage-4AAMJBBM.js → usage-T3HFMNNV.js} +2 -2
  36. package/package.json +1 -1
  37. package/dist/chunk-B5TYJO7V.js +0 -123
@@ -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
+ };
@@ -8,7 +8,7 @@ import {
8
8
  CONFIG_FILE_NAME,
9
9
  HISTORY_DIR_NAME,
10
10
  PLUGINS_DIR_NAME
11
- } from "./chunk-EPDW73H7.js";
11
+ } from "./chunk-3KTSJ2EL.js";
12
12
  import {
13
13
  atomicWriteFileSync
14
14
  } from "./chunk-IW3Q7AE5.js";
@@ -466,7 +466,10 @@ function peelMetaNarration(content) {
466
466
  var META_NARRATION_HARD_MARKERS = [
467
467
  /\[⚠️\s*CONTENT GENERATION MODE\]/,
468
468
  /CONTENT_ONLY_STREAM_REMINDER\b/,
469
- /<system-reminder>/i
469
+ /<system-reminder>/i,
470
+ // → `path` — AI wraps content with "已写好 → `file.md`" before the document body.
471
+ // Documents never use this conversational file-path-pointer syntax.
472
+ /→\s*`[^`]{2,}`/u
470
473
  ];
471
474
  var META_NARRATION_HEURISTICS = [
472
475
  /\bthe user (?:is asking me|wants me|is requesting|expects me)\b/i,
@@ -477,7 +480,24 @@ var META_NARRATION_HEURISTICS = [
477
480
  /\bActually,?\s+I\b/i,
478
481
  /\bI need to be honest with the user\b/i,
479
482
  /\bI(?:'m| am) in a special mode\b/i,
480
- /\bGiven that I cannot\b/i
483
+ /\bGiven that I cannot\b/i,
484
+ // ── 中文 ── (need 2+ total hits across all patterns to trigger)
485
+ /(?:已经?|已)\s*(?:写[好完]|生成[好完]|创建[好完]|准备[好完]|搞[好定]|弄[好完])[了啦]?/u,
486
+ /(?:需要你|请你|麻烦你|还要你|你看)\s*(?:确认|决定|选择|帮忙|看[一看下]|过目)/u,
487
+ /(?:总结|概括|归纳|整理)[一下]?(?:核心|主要|关键)?(?:思路|要点|内容|设计)/u,
488
+ /(?:随时|有问题|有需要|需要调整|需要修改|需要补充)\s*(?:联系|找我|告诉|沟通|调整|修改|补充|说)/u,
489
+ /^(?:.{0,40})(?:好的[,,,!\s]|嗯[,,,!\s]|收到[了]?[,,,!\s]|了解[了]?[,,,!\s]|明白[了]?[,,,!\s])/mu,
490
+ /(?:我来|让我|容我)\s*(?:整理|梳理|归纳|想想|看看|重新|检查|核实)/u,
491
+ // 过渡语(需要 2+ hits):"以下是:" / "如下:" 作为独立行。文档中可作小节标题,
492
+ // 但配合另一个对话模式(如"好的")时是典型的 AI 内容包装。
493
+ /^.{0,60}(?:以下是|下面是|如下)\s*[::,,]\s*$/mu
494
+ ];
495
+ var CHINESE_TAIL_CLOSING_MARKERS = [
496
+ /(?:需要你|请你|麻烦你|你看)\s*(?:确认|决定|选择|帮忙|看[一看下]|过目|觉得|认为)/u,
497
+ /(?:随时|有问题|有需要|需要调整|需要修改)\s*(?:联系|找我|告诉|沟通|调整|修改)/u,
498
+ /(?:如果|假如|假设)\s*(?:有[没缺][有]?|需要|觉得|认为).{0,30}?(?:随时|可以|直接|尽管)\s*(?:联系|找我|告诉|说|调整|修改)/u,
499
+ /(?:以上|先这样|暂时|目前).{0,10}$/mu,
500
+ /(?:几个|一些|以下)\s*(?:关键)?(?:问题|疑问|想法|建议).{0,30}$/mu
481
501
  ];
482
502
  function detectMetaNarration(content) {
483
503
  if (!content) return null;
@@ -485,7 +505,19 @@ function detectMetaNarration(content) {
485
505
  for (const re of META_NARRATION_HARD_MARKERS) {
486
506
  if (re.test(head)) return re.source;
487
507
  }
488
- if (/^#{1,3}\s+\S/m.test(head)) return null;
508
+ if (content.length > 500) {
509
+ const tail = content.slice(-500);
510
+ let tailHits = 0;
511
+ let tailFirst = "";
512
+ for (const re of CHINESE_TAIL_CLOSING_MARKERS) {
513
+ if (re.test(tail)) {
514
+ tailHits++;
515
+ if (!tailFirst) tailFirst = re.source;
516
+ if (tailHits >= 2) return `meta-narration-tail:${tailFirst}`;
517
+ }
518
+ }
519
+ }
520
+ if (/^#{1,3}\s+\S/m.test(content.slice(0, 200))) return null;
489
521
  let hits = 0;
490
522
  let firstMatch = "";
491
523
  for (const re of META_NARRATION_HEURISTICS) {
@@ -3,7 +3,7 @@ import {
3
3
  detectsHallucinatedFileOp,
4
4
  repairToolCallArguments,
5
5
  schemaToJsonSchema
6
- } from "./chunk-ODYTXQ26.js";
6
+ } from "./chunk-O3XSFPYH.js";
7
7
  import {
8
8
  AuthError,
9
9
  ProviderError,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  VERSION
4
- } from "./chunk-EPDW73H7.js";
4
+ } from "./chunk-3KTSJ2EL.js";
5
5
 
6
6
  // src/cli/ci-format.ts
7
7
  var DEFAULT_CI_THRESHOLDS = {