harness-wingman 0.1.0 → 0.3.0

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.
@@ -253,23 +253,82 @@ var init_dist = __esm({
253
253
  }
254
254
  });
255
255
 
256
+ // packages/adapter-cc/dist/walk.js
257
+ import { readdirSync } from "node:fs";
258
+ import { join as join5, parse, resolve } from "node:path";
259
+ function newWalkStats(entryBudget = MAX_WALK_ENTRIES) {
260
+ return { unknownErrors: 0, truncated: false, entriesLeft: entryBudget };
261
+ }
262
+ function safeReaddir(dir, stats) {
263
+ try {
264
+ return readdirSync(dir, { withFileTypes: true });
265
+ } catch (e) {
266
+ const code = e.code;
267
+ if (stats && (code === void 0 || !TOLERATED_CODES.has(code)))
268
+ stats.unknownErrors += 1;
269
+ return [];
270
+ }
271
+ }
272
+ function walkFiles(dir, fileName, stats = newWalkStats()) {
273
+ const found = [];
274
+ const visit = (d, depth) => {
275
+ const entries = safeReaddir(d, stats).sort((a, b) => a.name.localeCompare(b.name));
276
+ for (const entry of entries) {
277
+ if (stats.entriesLeft <= 0) {
278
+ stats.truncated = true;
279
+ return;
280
+ }
281
+ stats.entriesLeft -= 1;
282
+ const p = join5(d, entry.name);
283
+ if (entry.isDirectory()) {
284
+ if (entry.name.startsWith(".") || SKIP_DIR_NAMES.has(entry.name))
285
+ continue;
286
+ if (depth < MAX_WALK_DEPTH)
287
+ visit(p, depth + 1);
288
+ if (stats.truncated)
289
+ return;
290
+ } else if (entry.name === fileName) {
291
+ found.push(p);
292
+ }
293
+ }
294
+ };
295
+ visit(dir, 1);
296
+ return found;
297
+ }
298
+ function listMdFiles(dir, stats) {
299
+ return safeReaddir(dir, stats).map((e) => e.name).filter((n) => n.endsWith(".md")).sort().map((n) => join5(dir, n));
300
+ }
301
+ function isBareProjectDir(projectDir, homeDir) {
302
+ const r = resolve(projectDir);
303
+ return r === resolve(homeDir) || r === parse(r).root;
304
+ }
305
+ var MAX_WALK_DEPTH, MAX_WALK_ENTRIES, SKIP_DIR_NAMES, TOLERATED_CODES;
306
+ var init_walk = __esm({
307
+ "packages/adapter-cc/dist/walk.js"() {
308
+ "use strict";
309
+ MAX_WALK_DEPTH = 6;
310
+ MAX_WALK_ENTRIES = 5e4;
311
+ SKIP_DIR_NAMES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage", "Library"]);
312
+ TOLERATED_CODES = /* @__PURE__ */ new Set(["EPERM", "EACCES", "ENOENT"]);
313
+ }
314
+ });
315
+
256
316
  // packages/adapter-cc/dist/detect.js
257
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
317
+ import { existsSync as existsSync2, readFileSync as readFileSync3, statSync } from "node:fs";
258
318
  import { homedir as homedir3 } from "node:os";
259
- import { join as join4 } from "node:path";
319
+ import { join as join6 } from "node:path";
260
320
  function probeVersion(homeDir) {
261
- const root = join4(homeDir, ".claude", "projects");
262
- if (!existsSync2(root))
263
- return void 0;
321
+ const root = join6(homeDir, ".claude", "projects");
264
322
  let latest;
265
- for (const entry of readdirSync(root, { withFileTypes: true })) {
323
+ for (const entry of safeReaddir(root)) {
266
324
  if (!entry.isDirectory())
267
325
  continue;
268
- const dir = join4(root, entry.name);
269
- for (const name of readdirSync(dir)) {
326
+ const dir = join6(root, entry.name);
327
+ for (const sub of safeReaddir(dir)) {
328
+ const name = sub.name;
270
329
  if (!name.endsWith(".jsonl"))
271
330
  continue;
272
- const file = join4(dir, name);
331
+ const file = join6(dir, name);
273
332
  const mtimeMs = statSync(file).mtimeMs;
274
333
  if (!latest || mtimeMs > latest.mtimeMs)
275
334
  latest = { file, mtimeMs };
@@ -293,7 +352,7 @@ function probeVersion(homeDir) {
293
352
  }
294
353
  async function detectCc(options) {
295
354
  const homeDir = options?.homeDir ?? homedir3();
296
- const installed = existsSync2(join4(homeDir, ".claude")) || existsSync2(join4(homeDir, ".claude.json"));
355
+ const installed = existsSync2(join6(homeDir, ".claude")) || existsSync2(join6(homeDir, ".claude.json"));
297
356
  if (!installed)
298
357
  return { installed: false };
299
358
  const version = probeVersion(homeDir);
@@ -302,14 +361,15 @@ async function detectCc(options) {
302
361
  var init_detect = __esm({
303
362
  "packages/adapter-cc/dist/detect.js"() {
304
363
  "use strict";
364
+ init_walk();
305
365
  }
306
366
  });
307
367
 
308
368
  // packages/adapter-cc/dist/scan.js
309
369
  import { Buffer as Buffer2 } from "node:buffer";
310
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
370
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
311
371
  import { homedir as homedir4 } from "node:os";
312
- import { basename, dirname as dirname3, join as join5, resolve, sep } from "node:path";
372
+ import { basename, dirname as dirname3, join as join7, resolve as resolve2, sep } from "node:path";
313
373
  function readJsonFile(file) {
314
374
  const text = readFileSync4(file, "utf8");
315
375
  try {
@@ -325,34 +385,11 @@ function recordField(v, key) {
325
385
  const rec = asRecord(v);
326
386
  return rec ? asRecord(rec[key]) : void 0;
327
387
  }
328
- function walkFiles(dir, fileName, skipDotDirs, found = []) {
329
- if (!existsSync3(dir))
330
- return found;
331
- const entries = readdirSync2(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
332
- for (const entry of entries) {
333
- const p = join5(dir, entry.name);
334
- if (entry.isDirectory()) {
335
- if (IGNORED_WALK_DIRS.has(entry.name))
336
- continue;
337
- if (skipDotDirs && entry.name.startsWith("."))
338
- continue;
339
- walkFiles(p, fileName, skipDotDirs, found);
340
- } else if (entry.name === fileName) {
341
- found.push(p);
342
- }
343
- }
344
- return found;
345
- }
346
- function listMdFiles(dir) {
347
- if (!existsSync3(dir))
348
- return [];
349
- return readdirSync2(dir).filter((n) => n.endsWith(".md")).sort().map((n) => join5(dir, n));
350
- }
351
388
  function readSettingsChain(homeDir, projectDir) {
352
389
  const chain = [
353
- { file: join5(homeDir, ".claude", "settings.json"), source: "user" },
354
- { file: join5(projectDir, ".claude", "settings.json"), source: "project" },
355
- { file: join5(projectDir, ".claude", "settings.local.json"), source: "local" }
390
+ { file: join7(homeDir, ".claude", "settings.json"), source: "user" },
391
+ { file: join7(projectDir, ".claude", "settings.json"), source: "project" },
392
+ { file: join7(projectDir, ".claude", "settings.local.json"), source: "local" }
356
393
  ];
357
394
  const out = [];
358
395
  for (const { file, source } of chain) {
@@ -409,14 +446,20 @@ function ruleItem(file, source) {
409
446
  }
410
447
  function buildRules(homeDir, projectDir) {
411
448
  const items = [];
412
- const userClaudeMd = join5(homeDir, ".claude", "CLAUDE.md");
449
+ const userClaudeMd = join7(homeDir, ".claude", "CLAUDE.md");
413
450
  if (existsSync3(userClaudeMd))
414
451
  items.push(ruleItem(userClaudeMd, "user"));
415
- for (const f of listMdFiles(join5(homeDir, ".claude", "rules")))
452
+ for (const f of listMdFiles(join7(homeDir, ".claude", "rules")))
416
453
  items.push(ruleItem(f, "user"));
417
- for (const f of walkFiles(projectDir, "CLAUDE.md", true))
418
- items.push(ruleItem(f, "project"));
419
- for (const f of listMdFiles(join5(projectDir, ".claude", "rules"))) {
454
+ if (isBareProjectDir(projectDir, homeDir)) {
455
+ const direct = join7(projectDir, "CLAUDE.md");
456
+ if (existsSync3(direct))
457
+ items.push(ruleItem(direct, "project"));
458
+ } else {
459
+ for (const f of walkFiles(projectDir, "CLAUDE.md"))
460
+ items.push(ruleItem(f, "project"));
461
+ }
462
+ for (const f of listMdFiles(join7(projectDir, ".claude", "rules"))) {
420
463
  items.push(ruleItem(f, "project"));
421
464
  }
422
465
  return items;
@@ -439,17 +482,19 @@ function parseSkillFrontmatter(content) {
439
482
  return out;
440
483
  }
441
484
  function buildSkills(homeDir, projectDir, overrides) {
442
- const userRoot = join5(homeDir, ".claude", "skills");
443
- const syncedRoot = join5(userRoot, "synced");
485
+ const userRoot = join7(homeDir, ".claude", "skills");
486
+ const syncedRoot = join7(userRoot, "synced");
444
487
  const roots = [
445
488
  { root: userRoot, source: "user", exclude: syncedRoot },
446
- { root: join5(projectDir, ".claude", "skills"), source: "project" },
447
- { root: join5(homeDir, ".claude", "plugins"), source: "plugin" },
489
+ // projectDir 为家目录时,项目 skills 根与 user 根是同一目录,再走一遍会把每个
490
+ // skill 重复列为 project 层;bare root 无项目上下文 不设项目 skills
491
+ ...isBareProjectDir(projectDir, homeDir) ? [] : [{ root: join7(projectDir, ".claude", "skills"), source: "project" }],
492
+ { root: join7(homeDir, ".claude", "plugins"), source: "plugin" },
448
493
  { root: syncedRoot, source: "synced" }
449
494
  ];
450
495
  const items = [];
451
496
  for (const { root, source, exclude } of roots) {
452
- for (const skillFile of walkFiles(root, "SKILL.md", false)) {
497
+ for (const skillFile of walkFiles(root, "SKILL.md")) {
453
498
  if (exclude && (skillFile === exclude || skillFile.startsWith(exclude + sep)))
454
499
  continue;
455
500
  const fm = parseSkillFrontmatter(readFileSync4(skillFile, "utf8"));
@@ -473,11 +518,11 @@ function buildSkills(homeDir, projectDir, overrides) {
473
518
  }
474
519
  function buildTools(homeDir, projectDir) {
475
520
  const items = [];
476
- const claudeJsonPath = join5(homeDir, ".claude.json");
521
+ const claudeJsonPath = join7(homeDir, ".claude.json");
477
522
  let claudeJson;
478
523
  if (existsSync3(claudeJsonPath))
479
524
  claudeJson = asRecord(readJsonFile(claudeJsonPath));
480
- const projectEntry = recordField(claudeJson?.projects, resolve(projectDir));
525
+ const projectEntry = recordField(claudeJson?.projects, resolve2(projectDir));
481
526
  const disabledRaw = projectEntry?.disabledMcpServers;
482
527
  const disabled = new Set(Array.isArray(disabledRaw) ? disabledRaw.filter((v) => typeof v === "string") : []);
483
528
  const mcpItem = (name, source, nativePath) => ({
@@ -499,7 +544,7 @@ function buildTools(homeDir, projectDir) {
499
544
  for (const name of Object.keys(localServers).sort()) {
500
545
  items.push(mcpItem(name, "local", claudeJsonPath));
501
546
  }
502
- const mcpJsonPath = join5(projectDir, ".mcp.json");
547
+ const mcpJsonPath = join7(projectDir, ".mcp.json");
503
548
  if (existsSync3(mcpJsonPath)) {
504
549
  const projectServers = recordField(readJsonFile(mcpJsonPath), "mcpServers") ?? {};
505
550
  for (const name of Object.keys(projectServers).sort()) {
@@ -540,7 +585,7 @@ function buildGates(chain) {
540
585
  return items;
541
586
  }
542
587
  function projectSlug(projectDir) {
543
- return resolve(projectDir).replace(/[^a-zA-Z0-9]/g, "-");
588
+ return resolve2(projectDir).replace(/[^a-zA-Z0-9]/g, "-");
544
589
  }
545
590
  function lastContextTotal(file) {
546
591
  let total;
@@ -567,12 +612,10 @@ function lastContextTotal(file) {
567
612
  return total;
568
613
  }
569
614
  function buildHistory(homeDir, projectDir) {
570
- const dir = join5(homeDir, ".claude", "projects", projectSlug(projectDir));
571
- if (!existsSync3(dir))
572
- return [];
615
+ const dir = join7(homeDir, ".claude", "projects", projectSlug(projectDir));
573
616
  const items = [];
574
- for (const name of readdirSync2(dir).filter((n) => n.endsWith(".jsonl")).sort()) {
575
- const file = join5(dir, name);
617
+ for (const name of safeReaddir(dir).map((e) => e.name).filter((n) => n.endsWith(".jsonl")).sort()) {
618
+ const file = join7(dir, name);
576
619
  const total = lastContextTotal(file);
577
620
  if (total === void 0)
578
621
  continue;
@@ -662,20 +705,19 @@ async function scanCc(options) {
662
705
  }
663
706
  };
664
707
  }
665
- var IGNORED_WALK_DIRS;
666
708
  var init_scan = __esm({
667
709
  "packages/adapter-cc/dist/scan.js"() {
668
710
  "use strict";
669
711
  init_dist();
670
712
  init_detect();
671
- IGNORED_WALK_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "coverage"]);
713
+ init_walk();
672
714
  }
673
715
  });
674
716
 
675
717
  // packages/adapter-cc/dist/actions.js
676
718
  import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
677
719
  import { homedir as homedir5 } from "node:os";
678
- import { basename as basename2, dirname as dirname4, join as join6, resolve as resolve2 } from "node:path";
720
+ import { basename as basename2, dirname as dirname4, join as join8, resolve as resolve3 } from "node:path";
679
721
  function asRecord2(v) {
680
722
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
681
723
  }
@@ -699,17 +741,17 @@ function deepEqual(a, b) {
699
741
  }
700
742
  function backupDirPath(homeDir) {
701
743
  const ts = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
702
- const root = join6(homeDir, ".wingman", "backups");
703
- let dir = join6(root, ts);
744
+ const root = join8(homeDir, ".wingman", "backups");
745
+ let dir = join8(root, ts);
704
746
  for (let n = 2; existsSync4(dir); n++)
705
- dir = join6(root, `${ts}-${n}`);
747
+ dir = join8(root, `${ts}-${n}`);
706
748
  return dir;
707
749
  }
708
750
  function readSettingsSnapshots(homeDir, projectDir) {
709
751
  const files = [
710
- join6(homeDir, ".claude", "settings.json"),
711
- join6(projectDir, ".claude", "settings.json"),
712
- join6(projectDir, ".claude", "settings.local.json")
752
+ join8(homeDir, ".claude", "settings.json"),
753
+ join8(projectDir, ".claude", "settings.json"),
754
+ join8(projectDir, ".claude", "settings.local.json")
713
755
  ];
714
756
  const out = [];
715
757
  for (const file of files) {
@@ -738,7 +780,7 @@ function overridesRecord(s) {
738
780
  return rec;
739
781
  }
740
782
  function skillTargetFile(item, homeDir, projectDir) {
741
- return item.source === "project" ? join6(projectDir, ".claude", "settings.json") : join6(homeDir, ".claude", "settings.json");
783
+ return item.source === "project" ? join8(projectDir, ".claude", "settings.json") : join8(homeDir, ".claude", "settings.json");
742
784
  }
743
785
  function planSkillToggle(item, enable, homeDir, projectDir) {
744
786
  const name = item.nativeName;
@@ -791,7 +833,7 @@ function planSkillToggle(item, enable, homeDir, projectDir) {
791
833
  });
792
834
  }
793
835
  function planMcpToggle(name, enable, homeDir, projectDir) {
794
- const file = join6(homeDir, ".claude.json");
836
+ const file = join8(homeDir, ".claude.json");
795
837
  if (!existsSync4(file)) {
796
838
  throw new Error(`\u62D2\u7EDD:\u627E\u4E0D\u5230 ${file}(MCP \u8FDE\u63A5\u5F00\u5173\u6309\u9879\u76EE\u5B58\u653E\u4E8E\u8BE5\u6587\u4EF6)`);
797
839
  }
@@ -806,7 +848,7 @@ function planMcpToggle(name, enable, homeDir, projectDir) {
806
848
  if (!root)
807
849
  throw new Error(`\u62D2\u5199:${file} \u9876\u5C42\u4E0D\u662F JSON \u5BF9\u8C61,\u4E0D\u731C\u683C\u5F0F`);
808
850
  const projects = asRecord2(root.projects);
809
- const projectKey = resolve2(projectDir);
851
+ const projectKey = resolve3(projectDir);
810
852
  const entry = projects ? asRecord2(projects[projectKey]) : void 0;
811
853
  if (!projects || !entry) {
812
854
  throw new Error(`\u62D2\u7EDD:${file} \u7684 projects \u91CC\u6CA1\u6709\u672C\u9879\u76EE(${projectKey})\u7684\u8BB0\u5F55;MCP \u8FDE\u63A5\u5F00\u5173\u6309\u9879\u76EE\u5B58\u653E,\u8BF7\u5148\u5728\u8BE5\u9879\u76EE\u91CC\u8FD0\u884C\u8FC7 Claude Code`);
@@ -872,36 +914,40 @@ function collectRefSources(homeDir, projectDir, exclude) {
872
914
  if (!seen.has(file) && existsSync4(file))
873
915
  push(file, readFileSync5(file, "utf8"));
874
916
  };
875
- pushFile(join6(homeDir, ".claude", "CLAUDE.md"));
876
- for (const f of listMdFiles(join6(homeDir, ".claude", "rules")))
877
- pushFile(f);
878
- for (const f of walkFiles(projectDir, "CLAUDE.md", true))
917
+ const bareRoot = isBareProjectDir(projectDir, homeDir);
918
+ pushFile(join8(homeDir, ".claude", "CLAUDE.md"));
919
+ for (const f of listMdFiles(join8(homeDir, ".claude", "rules")))
879
920
  pushFile(f);
880
- for (const f of listMdFiles(join6(projectDir, ".claude", "rules")))
921
+ if (bareRoot)
922
+ pushFile(join8(projectDir, "CLAUDE.md"));
923
+ else
924
+ for (const f of walkFiles(projectDir, "CLAUDE.md"))
925
+ pushFile(f);
926
+ for (const f of listMdFiles(join8(projectDir, ".claude", "rules")))
881
927
  pushFile(f);
882
928
  const skillRoots = [
883
- join6(homeDir, ".claude", "skills"),
884
- join6(projectDir, ".claude", "skills"),
885
- join6(homeDir, ".claude", "plugins")
929
+ join8(homeDir, ".claude", "skills"),
930
+ ...bareRoot ? [] : [join8(projectDir, ".claude", "skills")],
931
+ join8(homeDir, ".claude", "plugins")
886
932
  ];
887
933
  for (const root of skillRoots) {
888
- for (const f of walkFiles(root, "SKILL.md", false)) {
934
+ for (const f of walkFiles(root, "SKILL.md")) {
889
935
  if (f !== exclude.skillFile)
890
936
  pushFile(f);
891
937
  }
892
938
  }
893
939
  for (const dir of [
894
- join6(homeDir, ".claude", "agents"),
895
- join6(projectDir, ".claude", "agents"),
896
- join6(homeDir, ".claude", "commands"),
897
- join6(projectDir, ".claude", "commands")
940
+ join8(homeDir, ".claude", "agents"),
941
+ join8(projectDir, ".claude", "agents"),
942
+ join8(homeDir, ".claude", "commands"),
943
+ join8(projectDir, ".claude", "commands")
898
944
  ]) {
899
945
  for (const f of listMdFiles(dir))
900
946
  pushFile(f);
901
947
  }
902
948
  for (const s of readSettingsSnapshots(homeDir, projectDir))
903
949
  push(s.file, s.raw);
904
- for (const file of [join6(projectDir, ".mcp.json"), join6(homeDir, ".claude.json")]) {
950
+ for (const file of [join8(projectDir, ".mcp.json"), join8(homeDir, ".claude.json")]) {
905
951
  if (seen.has(file) || !existsSync4(file))
906
952
  continue;
907
953
  const raw = readFileSync5(file, "utf8");
@@ -1029,9 +1075,9 @@ async function applyActionCc(plan, _options) {
1029
1075
  target: w.file
1030
1076
  }));
1031
1077
  toBackup.forEach((w, i) => {
1032
- writeFileSync3(join6(plan.backupDir, `${i}-${basename2(w.file)}`), w.before ?? "");
1078
+ writeFileSync3(join8(plan.backupDir, `${i}-${basename2(w.file)}`), w.before ?? "");
1033
1079
  });
1034
- writeFileSync3(join6(plan.backupDir, "manifest.json"), serialize({
1080
+ writeFileSync3(join8(plan.backupDir, "manifest.json"), serialize({
1035
1081
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1036
1082
  harness: "cc",
1037
1083
  action: plan.action,
@@ -1088,6 +1134,7 @@ var init_actions = __esm({
1088
1134
  "use strict";
1089
1135
  init_dist();
1090
1136
  init_scan();
1137
+ init_walk();
1091
1138
  MCP_PREFIX = "mcpServers.";
1092
1139
  KEY_DEF = /"([^"\\]+)"\s*:\s*\{/;
1093
1140
  }
@@ -1096,7 +1143,7 @@ var init_actions = __esm({
1096
1143
  // packages/adapter-cc/dist/hooks.js
1097
1144
  import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "node:fs";
1098
1145
  import { homedir as homedir6 } from "node:os";
1099
- import { dirname as dirname5, join as join7 } from "node:path";
1146
+ import { dirname as dirname5, join as join9 } from "node:path";
1100
1147
  function wingmanUrl(port) {
1101
1148
  return `http://127.0.0.1:${port}${SIGNATURE}`;
1102
1149
  }
@@ -1108,10 +1155,10 @@ function isWingmanHandler(h) {
1108
1155
  return rec?.type === "http" && typeof rec.url === "string" && rec.url.includes(SIGNATURE);
1109
1156
  }
1110
1157
  function wingmanRoot(homeDir) {
1111
- return join7(homeDir, ".wingman");
1158
+ return join9(homeDir, ".wingman");
1112
1159
  }
1113
1160
  function statePath(homeDir) {
1114
- return join7(wingmanRoot(homeDir), "state", "cc-hooks.json");
1161
+ return join9(wingmanRoot(homeDir), "state", "cc-hooks.json");
1115
1162
  }
1116
1163
  function readState(homeDir) {
1117
1164
  const file = statePath(homeDir);
@@ -1138,18 +1185,18 @@ function readState(homeDir) {
1138
1185
  }
1139
1186
  return rec;
1140
1187
  }
1141
- function writeState(homeDir, state) {
1188
+ function writeState(homeDir, state2) {
1142
1189
  const file = statePath(homeDir);
1143
1190
  mkdirSync3(dirname5(file), { recursive: true });
1144
- writeFileSync4(file, `${JSON.stringify(state, null, 2)}
1191
+ writeFileSync4(file, `${JSON.stringify(state2, null, 2)}
1145
1192
  `);
1146
1193
  }
1147
1194
  function makeBackupDir(homeDir) {
1148
1195
  const ts = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
1149
- const root = join7(wingmanRoot(homeDir), "backups");
1150
- let dir = join7(root, ts);
1196
+ const root = join9(wingmanRoot(homeDir), "backups");
1197
+ let dir = join9(root, ts);
1151
1198
  for (let n = 2; existsSync5(dir); n++)
1152
- dir = join7(root, `${ts}-${n}`);
1199
+ dir = join9(root, `${ts}-${n}`);
1153
1200
  mkdirSync3(dir, { recursive: true });
1154
1201
  return dir;
1155
1202
  }
@@ -1259,7 +1306,7 @@ function deepEqual2(a, b) {
1259
1306
  return false;
1260
1307
  }
1261
1308
  function targetPath(homeDir) {
1262
- return join7(homeDir, ".claude", "settings.json");
1309
+ return join9(homeDir, ".claude", "settings.json");
1263
1310
  }
1264
1311
  async function installHooksCc(port, options) {
1265
1312
  const homeDir = options?.homeDir ?? homedir6();
@@ -1281,7 +1328,7 @@ async function installHooksCc(port, options) {
1281
1328
  }
1282
1329
  const backupDir = makeBackupDir(homeDir);
1283
1330
  if (bytes !== null)
1284
- writeFileSync4(join7(backupDir, "settings.json"), bytes);
1331
+ writeFileSync4(join9(backupDir, "settings.json"), bytes);
1285
1332
  const prev = readState(homeDir);
1286
1333
  const now = (/* @__PURE__ */ new Date()).toISOString();
1287
1334
  writeState(homeDir, prev ? { ...prev, port, installedAt: now } : { target, original: bytes, backupDir, port, installedAt: now });
@@ -1300,7 +1347,7 @@ async function installHooksCc(port, options) {
1300
1347
  async function uninstallHooksCc(options) {
1301
1348
  const homeDir = options?.homeDir ?? homedir6();
1302
1349
  const target = targetPath(homeDir);
1303
- const state = readState(homeDir);
1350
+ const state2 = readState(homeDir);
1304
1351
  if (!existsSync5(target)) {
1305
1352
  rmSync2(statePath(homeDir), { force: true });
1306
1353
  return;
@@ -1313,15 +1360,15 @@ async function uninstallHooksCc(options) {
1313
1360
  }
1314
1361
  const backupDir = makeBackupDir(homeDir);
1315
1362
  if (bytes !== null)
1316
- writeFileSync4(join7(backupDir, "settings.json"), bytes);
1317
- if (state && state.original !== null) {
1318
- const originalParsed = asRecord3(JSON.parse(state.original));
1363
+ writeFileSync4(join9(backupDir, "settings.json"), bytes);
1364
+ if (state2 && state2.original !== null) {
1365
+ const originalParsed = asRecord3(JSON.parse(state2.original));
1319
1366
  if (originalParsed && deepEqual2(normalizeForCompare(settings), normalizeForCompare(originalParsed))) {
1320
- writeFileSync4(target, state.original);
1367
+ writeFileSync4(target, state2.original);
1321
1368
  rmSync2(statePath(homeDir), { force: true });
1322
1369
  return;
1323
1370
  }
1324
- } else if (state && state.original === null) {
1371
+ } else if (state2 && state2.original === null) {
1325
1372
  if (deepEqual2(normalizeForCompare(settings), {})) {
1326
1373
  rmSync2(target, { force: true });
1327
1374
  rmSync2(statePath(homeDir), { force: true });
@@ -1584,33 +1631,33 @@ function parseString(ctx) {
1584
1631
  }
1585
1632
  let parsed = "";
1586
1633
  let sliceStart = ctx.p;
1587
- let state = 0;
1634
+ let state2 = 0;
1588
1635
  for (; ctx.p < ctx.s.length; ctx.p++) {
1589
1636
  c = ctx.s.charCodeAt(ctx.p);
1590
1637
  if (isMultiline && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)) {
1591
- state = state && 3;
1638
+ state2 = state2 && 3;
1592
1639
  } else if (c < 32 && c !== 9 || c === 127) {
1593
1640
  throw new TomlError("control characters are not allowed in strings", {
1594
1641
  toml: ctx.s,
1595
1642
  ptr: ctx.p
1596
1643
  });
1597
- } else if ((!state || state === 3) && c === first && (!isMultiline || ctx.s.charCodeAt(ctx.p + 1) === first && ctx.s.charCodeAt(ctx.p + 2) === first)) {
1644
+ } else if ((!state2 || state2 === 3) && c === first && (!isMultiline || ctx.s.charCodeAt(ctx.p + 1) === first && ctx.s.charCodeAt(ctx.p + 2) === first)) {
1598
1645
  if (isMultiline) {
1599
1646
  if (ctx.s.charCodeAt(ctx.p + 3) === first)
1600
1647
  ctx.p++;
1601
1648
  if (ctx.s.charCodeAt(ctx.p + 3) === first)
1602
1649
  ctx.p++;
1603
1650
  }
1604
- if (!state)
1651
+ if (!state2)
1605
1652
  parsed += ctx.s.slice(sliceStart, ctx.p);
1606
1653
  ctx.p += isMultiline ? 3 : 1;
1607
1654
  return parsed;
1608
- } else if (!state) {
1655
+ } else if (!state2) {
1609
1656
  if (!isLiteral && c === 92) {
1610
1657
  parsed += ctx.s.slice(sliceStart, sliceStart = ctx.p);
1611
- state = 1;
1658
+ state2 = 1;
1612
1659
  }
1613
- } else if (state === 1) {
1660
+ } else if (state2 === 1) {
1614
1661
  if (c === 120 || c === 117 || c === 85) {
1615
1662
  let value = 0;
1616
1663
  let len = c === 120 ? 2 : c === 117 ? 4 : 8;
@@ -1635,9 +1682,9 @@ function parseString(ctx) {
1635
1682
  }
1636
1683
  parsed += String.fromCodePoint(value);
1637
1684
  sliceStart = ctx.p + 1;
1638
- state = 0;
1685
+ state2 = 0;
1639
1686
  } else if (c === 32 || c === 9) {
1640
- state = 2;
1687
+ state2 = 2;
1641
1688
  } else {
1642
1689
  if (c === 98)
1643
1690
  parsed += "\b";
@@ -1658,16 +1705,16 @@ function parseString(ctx) {
1658
1705
  else
1659
1706
  throw new TomlError("unrecognized escape sequence", { toml: ctx.s, ptr: ctx.p });
1660
1707
  sliceStart = ctx.p + 1;
1661
- state = 0;
1708
+ state2 = 0;
1662
1709
  }
1663
1710
  } else if (c !== 32 && c !== 9) {
1664
- if (state === 2) {
1711
+ if (state2 === 2) {
1665
1712
  throw new TomlError("invalid escape: only line-ending whitespace may be escaped", {
1666
1713
  toml: ctx.s,
1667
1714
  ptr: sliceStart
1668
1715
  });
1669
1716
  }
1670
- state = !isLiteral && c === 92 ? 1 : 0;
1717
+ state2 = !isLiteral && c === 92 ? 1 : 0;
1671
1718
  sliceStart = ctx.p;
1672
1719
  }
1673
1720
  }
@@ -1930,15 +1977,15 @@ function peekTable(key, table, meta, type) {
1930
1977
  let m = meta;
1931
1978
  let k;
1932
1979
  let hasOwn = false;
1933
- let state;
1980
+ let state2;
1934
1981
  for (let i = 0; i < key.length; i++) {
1935
1982
  if (i) {
1936
1983
  t = hasOwn ? t[k] : t[k] = {};
1937
- m = (state = m[k]).c;
1938
- if (type === 0 && (state.t === 1 || state.t === 2)) {
1984
+ m = (state2 = m[k]).c;
1985
+ if (type === 0 && (state2.t === 1 || state2.t === 2)) {
1939
1986
  return null;
1940
1987
  }
1941
- if (state.t === 2) {
1988
+ if (state2.t === 2) {
1942
1989
  let l = t.length - 1;
1943
1990
  t = t[l];
1944
1991
  m = m[l].c;
@@ -1961,30 +2008,30 @@ function peekTable(key, table, meta, type) {
1961
2008
  };
1962
2009
  }
1963
2010
  }
1964
- state = m[k];
1965
- if (state.t !== type && !(type === 1 && state.t === 3)) {
2011
+ state2 = m[k];
2012
+ if (state2.t !== type && !(type === 1 && state2.t === 3)) {
1966
2013
  return null;
1967
2014
  }
1968
2015
  if (type === 2) {
1969
- if (!state.d) {
1970
- state.d = true;
2016
+ if (!state2.d) {
2017
+ state2.d = true;
1971
2018
  t[k] = [];
1972
2019
  }
1973
2020
  t[k].push(t = {});
1974
- state.c[state.i++] = state = { t: 1, d: false, i: 0, c: {} };
2021
+ state2.c[state2.i++] = state2 = { t: 1, d: false, i: 0, c: {} };
1975
2022
  }
1976
- if (state.d) {
2023
+ if (state2.d) {
1977
2024
  return null;
1978
2025
  }
1979
- state.d = true;
2026
+ state2.d = true;
1980
2027
  if (type === 1) {
1981
2028
  t = hasOwn ? t[k] : t[k] = {};
1982
2029
  } else if (type === 0 && hasOwn) {
1983
2030
  return null;
1984
2031
  }
1985
- return [k, t, state.c];
2032
+ return [k, t, state2.c];
1986
2033
  }
1987
- function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
2034
+ function parse2(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
1988
2035
  let ctx = { s: toml, p: 0, d: maxDepth };
1989
2036
  let res = {};
1990
2037
  let meta = {};
@@ -2077,14 +2124,14 @@ var init_dist3 = __esm({
2077
2124
 
2078
2125
  // packages/adapter-codex/dist/shared.js
2079
2126
  import { homedir as homedir7 } from "node:os";
2080
- import { join as join8 } from "node:path";
2127
+ import { join as join10 } from "node:path";
2081
2128
  function resolvePaths(options) {
2082
2129
  const homeDir = options?.homeDir ?? homedir7();
2083
2130
  const projectDir = options?.projectDir ?? process.cwd();
2084
- return { homeDir, projectDir, codexHome: join8(homeDir, ".codex") };
2131
+ return { homeDir, projectDir, codexHome: join10(homeDir, ".codex") };
2085
2132
  }
2086
2133
  function wingmanStateRoot(paths) {
2087
- return join8(paths.homeDir, ".wingman");
2134
+ return join10(paths.homeDir, ".wingman");
2088
2135
  }
2089
2136
  function isTable(value) {
2090
2137
  return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
@@ -2393,8 +2440,8 @@ var init_toml_edit = __esm({
2393
2440
  });
2394
2441
 
2395
2442
  // packages/adapter-codex/dist/actions.js
2396
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync3, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
2397
- import { basename as basename3, dirname as dirname6, join as join9 } from "node:path";
2443
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
2444
+ import { basename as basename3, dirname as dirname6, join as join11 } from "node:path";
2398
2445
  async function planAction(action, options) {
2399
2446
  const paths = resolvePaths(options);
2400
2447
  const resolved = resolveToggle(action, paths);
@@ -2423,7 +2470,7 @@ async function applyAction(plan, options) {
2423
2470
  error: "plan.writes \u4E0D\u662F\u672C adapter \u4EA7\u51FA\u7684\u5F62\u72B6(\u5E94\u6070\u4E3A\u4E00\u5904 modify),\u62D2\u7EDD\u6267\u884C"
2424
2471
  };
2425
2472
  }
2426
- const backupsRoot = join9(wingmanStateRoot(paths), "backups");
2473
+ const backupsRoot = join11(wingmanStateRoot(paths), "backups");
2427
2474
  if (dirname6(plan.backupDir) !== backupsRoot) {
2428
2475
  return { ok: false, error: `backupDir \u4E0D\u5728 wingman \u5907\u4EFD\u6839(${backupsRoot})\u4E0B,\u62D2\u7EDD\u6267\u884C` };
2429
2476
  }
@@ -2439,7 +2486,7 @@ async function applyAction(plan, options) {
2439
2486
  return { ok: false, error: "plan \u4E0E\u672C adapter \u7684\u63A8\u6F14\u4E0D\u4E00\u81F4,\u62D2\u7EDD\u6267\u884C;\u8BF7\u91CD\u65B0 plan" };
2440
2487
  }
2441
2488
  mkdirSync4(plan.backupDir, { recursive: true });
2442
- writeFileSync5(join9(plan.backupDir, basename3(write.file)), currentRaw);
2489
+ writeFileSync5(join11(plan.backupDir, basename3(write.file)), currentRaw);
2443
2490
  writeFileSync5(write.file, write.after);
2444
2491
  return { ok: true, backupDir: plan.backupDir };
2445
2492
  } catch (error) {
@@ -2448,10 +2495,10 @@ async function applyAction(plan, options) {
2448
2495
  }
2449
2496
  function nextBackupDir(paths) {
2450
2497
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
2451
- const base = join9(wingmanStateRoot(paths), "backups");
2452
- let dir = join9(base, stamp);
2498
+ const base = join11(wingmanStateRoot(paths), "backups");
2499
+ let dir = join11(base, stamp);
2453
2500
  for (let n = 1; existsSync6(dir); n += 1)
2454
- dir = join9(base, `${stamp}-${n}`);
2501
+ dir = join11(base, `${stamp}-${n}`);
2455
2502
  return dir;
2456
2503
  }
2457
2504
  function resolveToggle(action, paths) {
@@ -2465,13 +2512,13 @@ function resolveToggle(action, paths) {
2465
2512
  const slot = m[1];
2466
2513
  const source = m[2];
2467
2514
  const nativeName = m[3];
2468
- const file = source === "user" ? join9(paths.codexHome, "config.toml") : join9(paths.projectDir, ".codex", "config.toml");
2515
+ const file = source === "user" ? join11(paths.codexHome, "config.toml") : join11(paths.projectDir, ".codex", "config.toml");
2469
2516
  if (!existsSync6(file))
2470
2517
  throw new Error(`itemId \u627E\u4E0D\u5230:\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728(${file})`);
2471
2518
  const before = readFileSync7(file, "utf8");
2472
2519
  let table;
2473
2520
  try {
2474
- table = parse(before);
2521
+ table = parse2(before);
2475
2522
  } catch (error) {
2476
2523
  throw new Error(`Codex config.toml \u89E3\u6790\u5931\u8D25,\u62D2\u7EDD\u52A8\u4F5C(${file}):${error.message}`);
2477
2524
  }
@@ -2679,7 +2726,7 @@ function readToolList(entry, key, serverId, file) {
2679
2726
  function verifyEdit(table, edit, file) {
2680
2727
  let reparsed;
2681
2728
  try {
2682
- reparsed = parse(edit.after);
2729
+ reparsed = parse2(edit.after);
2683
2730
  } catch (error) {
2684
2731
  throw new Error(`\u7F16\u8F91\u7ED3\u679C\u4E0D\u662F\u5408\u6CD5 TOML,\u5DF2\u653E\u5F03\u5199\u5165(${file}):${error.message}`);
2685
2732
  }
@@ -2691,13 +2738,13 @@ function verifyEdit(table, edit, file) {
2691
2738
  }
2692
2739
  function collectCorpus(paths) {
2693
2740
  const candidates = [
2694
- join9(paths.codexHome, "AGENTS.md"),
2695
- join9(paths.projectDir, "AGENTS.md"),
2696
- join9(paths.codexHome, "config.toml"),
2697
- join9(paths.projectDir, ".codex", "config.toml"),
2698
- join9(paths.codexHome, "hooks.json"),
2699
- join9(paths.projectDir, ".codex", "hooks.json"),
2700
- ...listFilesRecursive(join9(paths.codexHome, "prompts"))
2741
+ join11(paths.codexHome, "AGENTS.md"),
2742
+ join11(paths.projectDir, "AGENTS.md"),
2743
+ join11(paths.codexHome, "config.toml"),
2744
+ join11(paths.projectDir, ".codex", "config.toml"),
2745
+ join11(paths.codexHome, "hooks.json"),
2746
+ join11(paths.projectDir, ".codex", "hooks.json"),
2747
+ ...listFilesRecursive(join11(paths.codexHome, "prompts"))
2701
2748
  ];
2702
2749
  const sources = [];
2703
2750
  for (const file of candidates) {
@@ -2714,13 +2761,13 @@ function collectCorpus(paths) {
2714
2761
  function listFilesRecursive(dir) {
2715
2762
  let entries;
2716
2763
  try {
2717
- entries = readdirSync3(dir, { withFileTypes: true });
2764
+ entries = readdirSync2(dir, { withFileTypes: true });
2718
2765
  } catch {
2719
2766
  return [];
2720
2767
  }
2721
2768
  const files = [];
2722
2769
  for (const entry of [...entries].sort((a, b) => a.name.localeCompare(b.name))) {
2723
- const path = join9(dir, entry.name);
2770
+ const path = join11(dir, entry.name);
2724
2771
  if (entry.isDirectory())
2725
2772
  files.push(...listFilesRecursive(path));
2726
2773
  else if (entry.isFile())
@@ -2787,15 +2834,15 @@ var init_actions2 = __esm({
2787
2834
 
2788
2835
  // packages/adapter-codex/dist/hooks.js
2789
2836
  import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync8, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
2790
- import { basename as basename4, dirname as dirname7, join as join10 } from "node:path";
2837
+ import { basename as basename4, dirname as dirname7, join as join12 } from "node:path";
2791
2838
  async function installHooks(port, options) {
2792
2839
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
2793
2840
  throw new Error(`\u975E\u6CD5\u7AEF\u53E3:${port}`);
2794
2841
  }
2795
2842
  const paths = resolvePaths(options);
2796
2843
  const root = wingmanStateRoot(paths);
2797
- const scriptPath = join10(root, FORWARD_SCRIPT_NAME);
2798
- const configPath = join10(paths.codexHome, "config.toml");
2844
+ const scriptPath = join12(root, FORWARD_SCRIPT_NAME);
2845
+ const configPath = join12(paths.codexHome, "config.toml");
2799
2846
  const raw = existsSync7(configPath) ? readFileSync8(configPath) : void 0;
2800
2847
  const text = raw?.toString("utf8") ?? "";
2801
2848
  const { remainder, block } = splitManagedBlock(text, configPath);
@@ -2827,17 +2874,17 @@ async function installHooks(port, options) {
2827
2874
  async function uninstallHooks(options) {
2828
2875
  const paths = resolvePaths(options);
2829
2876
  const root = wingmanStateRoot(paths);
2830
- const scriptPath = join10(root, FORWARD_SCRIPT_NAME);
2877
+ const scriptPath = join12(root, FORWARD_SCRIPT_NAME);
2831
2878
  const statePath2 = hooksStatePath(root);
2832
- const configPath = join10(paths.codexHome, "config.toml");
2833
- const state = readState2(statePath2);
2879
+ const configPath = join12(paths.codexHome, "config.toml");
2880
+ const state2 = readState2(statePath2);
2834
2881
  const raw = existsSync7(configPath) ? readFileSync8(configPath) : void 0;
2835
2882
  if (raw !== void 0) {
2836
2883
  const text = raw.toString("utf8");
2837
2884
  const { remainder, block } = splitManagedBlock(text, configPath);
2838
2885
  if (block !== void 0) {
2839
2886
  backupFile(root, configPath, raw);
2840
- const restored = restoreTarget(remainder, state);
2887
+ const restored = restoreTarget(remainder, state2);
2841
2888
  if (restored === null)
2842
2889
  rmSync3(configPath);
2843
2890
  else
@@ -2848,13 +2895,13 @@ async function uninstallHooks(options) {
2848
2895
  rmSync3(statePath2, { force: true });
2849
2896
  }
2850
2897
  function hasManagedHooks(paths) {
2851
- const configPath = join10(paths.codexHome, "config.toml");
2898
+ const configPath = join12(paths.codexHome, "config.toml");
2852
2899
  if (!existsSync7(configPath))
2853
2900
  return false;
2854
2901
  return readFileSync8(configPath, "utf8").includes(MANAGED_BLOCK_START);
2855
2902
  }
2856
- function restoreTarget(remainder, state) {
2857
- const original = state?.original;
2903
+ function restoreTarget(remainder, state2) {
2904
+ const original = state2?.original;
2858
2905
  if (original === void 0)
2859
2906
  return remainder;
2860
2907
  if (!original.existed) {
@@ -2862,7 +2909,7 @@ function restoreTarget(remainder, state) {
2862
2909
  }
2863
2910
  const originalBytes = Buffer.from(original.base64 ?? "", "base64");
2864
2911
  try {
2865
- if (deepEqual3(parse(remainder), parse(originalBytes.toString("utf8"))))
2912
+ if (deepEqual3(parse2(remainder), parse2(originalBytes.toString("utf8"))))
2866
2913
  return originalBytes;
2867
2914
  } catch {
2868
2915
  }
@@ -2897,7 +2944,7 @@ function splitManagedBlock(text, configPath) {
2897
2944
  function assertWritable(remainder, configPath) {
2898
2945
  let table;
2899
2946
  try {
2900
- table = parse(remainder);
2947
+ table = parse2(remainder);
2901
2948
  } catch (error) {
2902
2949
  throw new Error(`Codex config.toml \u89E3\u6790\u5931\u8D25,\u62D2\u7EDD\u5199\u5165(${configPath}):${error.message}`);
2903
2950
  }
@@ -2914,15 +2961,15 @@ function assertWritable(remainder, configPath) {
2914
2961
  }
2915
2962
  function backupFile(root, filePath, contents) {
2916
2963
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-");
2917
- let dir = join10(root, "backups", stamp);
2964
+ let dir = join12(root, "backups", stamp);
2918
2965
  for (let n = 1; existsSync7(dir); n += 1)
2919
- dir = join10(root, "backups", `${stamp}-${n}`);
2966
+ dir = join12(root, "backups", `${stamp}-${n}`);
2920
2967
  mkdirSync5(dir, { recursive: true });
2921
- writeFileSync6(join10(dir, basename4(filePath)), contents);
2968
+ writeFileSync6(join12(dir, basename4(filePath)), contents);
2922
2969
  return dir;
2923
2970
  }
2924
2971
  function hooksStatePath(root) {
2925
- return join10(root, "state", "codex-hooks.json");
2972
+ return join12(root, "state", "codex-hooks.json");
2926
2973
  }
2927
2974
  function readState2(statePath2) {
2928
2975
  if (!existsSync7(statePath2))
@@ -2933,9 +2980,9 @@ function readState2(statePath2) {
2933
2980
  return void 0;
2934
2981
  }
2935
2982
  }
2936
- function writeState2(statePath2, state) {
2983
+ function writeState2(statePath2, state2) {
2937
2984
  mkdirSync5(dirname7(statePath2), { recursive: true });
2938
- writeFileSync6(statePath2, `${JSON.stringify(state, null, 2)}
2985
+ writeFileSync6(statePath2, `${JSON.stringify(state2, null, 2)}
2939
2986
  `);
2940
2987
  }
2941
2988
  function deepEqual3(a, b) {
@@ -2987,8 +3034,8 @@ process.stdin.on('end', async () => {
2987
3034
  });
2988
3035
 
2989
3036
  // packages/adapter-codex/dist/scan.js
2990
- import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
2991
- import { dirname as dirname8, isAbsolute, join as join11, resolve as resolve3 } from "node:path";
3037
+ import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
3038
+ import { dirname as dirname8, isAbsolute, join as join13, resolve as resolve4 } from "node:path";
2992
3039
  async function detect(options) {
2993
3040
  const { codexHome } = resolvePaths(options);
2994
3041
  return { installed: existsSync8(codexHome) };
@@ -3028,8 +3075,8 @@ async function scan(options) {
3028
3075
  }
3029
3076
  function loadConfigLayers(paths) {
3030
3077
  const candidates = [
3031
- ["user", join11(paths.codexHome, "config.toml")],
3032
- ["project", join11(paths.projectDir, ".codex", "config.toml")]
3078
+ ["user", join13(paths.codexHome, "config.toml")],
3079
+ ["project", join13(paths.projectDir, ".codex", "config.toml")]
3033
3080
  ];
3034
3081
  const layers = [];
3035
3082
  for (const [source, path] of candidates) {
@@ -3037,7 +3084,7 @@ function loadConfigLayers(paths) {
3037
3084
  continue;
3038
3085
  const text = readFileSync9(path, "utf8");
3039
3086
  try {
3040
- layers.push({ source, path, table: parse(text) });
3087
+ layers.push({ source, path, table: parse2(text) });
3041
3088
  } catch (error) {
3042
3089
  throw new Error(`Codex config.toml \u89E3\u6790\u5931\u8D25(${path}):${error.message}`);
3043
3090
  }
@@ -3046,8 +3093,8 @@ function loadConfigLayers(paths) {
3046
3093
  }
3047
3094
  function collectRules(out, paths, layers) {
3048
3095
  const agentsChain = [
3049
- ["user", join11(paths.codexHome, "AGENTS.md")],
3050
- ["project", join11(paths.projectDir, "AGENTS.md")]
3096
+ ["user", join13(paths.codexHome, "AGENTS.md")],
3097
+ ["project", join13(paths.projectDir, "AGENTS.md")]
3051
3098
  ];
3052
3099
  for (const [source, file] of agentsChain) {
3053
3100
  const size = fileSize(file);
@@ -3082,7 +3129,7 @@ function collectRules(out, paths, layers) {
3082
3129
  }
3083
3130
  const instructionsFile = layer.table.model_instructions_file;
3084
3131
  if (typeof instructionsFile === "string") {
3085
- const filePath = isAbsolute(instructionsFile) ? instructionsFile : resolve3(dirname8(layer.path), instructionsFile);
3132
+ const filePath = isAbsolute(instructionsFile) ? instructionsFile : resolve4(dirname8(layer.path), instructionsFile);
3086
3133
  const item = {
3087
3134
  id: `codex:rules:${layer.source}:model_instructions_file`,
3088
3135
  slot: "rules",
@@ -3191,7 +3238,7 @@ function collectGates(out, layer) {
3191
3238
  }
3192
3239
  }
3193
3240
  function collectHistory(out, paths, layers) {
3194
- const historyFile = join11(paths.codexHome, "history.jsonl");
3241
+ const historyFile = join13(paths.codexHome, "history.jsonl");
3195
3242
  const historyBytes = fileSize(historyFile);
3196
3243
  if (historyBytes !== void 0) {
3197
3244
  out.push({
@@ -3206,7 +3253,7 @@ function collectHistory(out, paths, layers) {
3206
3253
  });
3207
3254
  }
3208
3255
  const configured = layers.find((layer) => layer.source === "user")?.table.log_dir;
3209
- const logDir = typeof configured === "string" ? expandPath(configured, paths) : join11(paths.codexHome, "log");
3256
+ const logDir = typeof configured === "string" ? expandPath(configured, paths) : join13(paths.codexHome, "log");
3210
3257
  if (isDirectory(logDir)) {
3211
3258
  out.push({
3212
3259
  id: "codex:history:user:log_dir",
@@ -3222,8 +3269,8 @@ function collectHistory(out, paths, layers) {
3222
3269
  }
3223
3270
  function expandPath(value, paths) {
3224
3271
  if (value === "~" || value.startsWith("~/"))
3225
- return join11(paths.homeDir, value.slice(2));
3226
- return isAbsolute(value) ? value : resolve3(paths.codexHome, value);
3272
+ return join13(paths.homeDir, value.slice(2));
3273
+ return isAbsolute(value) ? value : resolve4(paths.codexHome, value);
3227
3274
  }
3228
3275
  function assertUniqueIds(slots) {
3229
3276
  const seen = /* @__PURE__ */ new Set();
@@ -3253,8 +3300,8 @@ function isDirectory(path) {
3253
3300
  }
3254
3301
  function directoryBytes(dir) {
3255
3302
  let total = 0;
3256
- for (const entry of readdirSync4(dir, { withFileTypes: true })) {
3257
- const entryPath = join11(dir, entry.name);
3303
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
3304
+ const entryPath = join13(dir, entry.name);
3258
3305
  if (entry.isDirectory())
3259
3306
  total += directoryBytes(entryPath);
3260
3307
  else if (entry.isFile())
@@ -9087,11 +9134,11 @@ var require_lexer = __commonJS({
9087
9134
  hasChars(n) {
9088
9135
  return this.pos + n <= this.buffer.length;
9089
9136
  }
9090
- setNext(state) {
9137
+ setNext(state2) {
9091
9138
  this.buffer = this.buffer.substring(this.pos);
9092
9139
  this.pos = 0;
9093
9140
  this.lineEndPos = null;
9094
- this.next = state;
9141
+ this.next = state2;
9095
9142
  return null;
9096
9143
  }
9097
9144
  peek(n) {
@@ -10527,7 +10574,7 @@ var require_public_api = __commonJS({
10527
10574
  }
10528
10575
  return doc;
10529
10576
  }
10530
- function parse3(src, reviver, options) {
10577
+ function parse4(src, reviver, options) {
10531
10578
  let _reviver = void 0;
10532
10579
  if (typeof reviver === "function") {
10533
10580
  _reviver = reviver;
@@ -10568,7 +10615,7 @@ var require_public_api = __commonJS({
10568
10615
  return value.toString(options);
10569
10616
  return new Document.Document(value, _replacer, options).toString(options);
10570
10617
  }
10571
- exports.parse = parse3;
10618
+ exports.parse = parse4;
10572
10619
  exports.parseAllDocuments = parseAllDocuments;
10573
10620
  exports.parseDocument = parseDocument3;
10574
10621
  exports.stringify = stringify2;
@@ -10837,20 +10884,28 @@ var init_dump_config = __esm({
10837
10884
  import { Buffer as Buffer3 } from "node:buffer";
10838
10885
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
10839
10886
  import { homedir as homedir8 } from "node:os";
10840
- import { dirname as dirname9, join as join12 } from "node:path";
10887
+ import { dirname as dirname9, join as join14 } from "node:path";
10888
+ function resolveDshHome(options) {
10889
+ if (options?.homeDir !== void 0)
10890
+ return join14(options.homeDir, ".dsh");
10891
+ const envHome = process.env.DSH_HOME;
10892
+ if (envHome !== void 0 && envHome !== "")
10893
+ return envHome;
10894
+ return join14(homedir8(), ".dsh");
10895
+ }
10841
10896
  function resolveDshHookPaths(options) {
10842
10897
  const home = options?.homeDir ?? homedir8();
10843
- const dshHome = join12(home, ".dsh");
10844
- const wingmanRoot2 = join12(home, ".wingman");
10845
- const hooksDir = join12(wingmanRoot2, "dsh");
10898
+ const dshHome = resolveDshHome(options);
10899
+ const wingmanRoot2 = join14(home, ".wingman");
10900
+ const hooksDir = join14(wingmanRoot2, "dsh");
10846
10901
  return {
10847
10902
  dshHome,
10848
- patchFile: join12(dshHome, "cordis.patch.yml"),
10903
+ patchFile: join14(dshHome, "cordis.patch.yml"),
10849
10904
  wingmanRoot: wingmanRoot2,
10850
- hooksConfigFile: join12(hooksDir, "hooks-claude-code.json"),
10851
- forwarderFile: join12(hooksDir, "forward.mjs"),
10852
- stateFile: join12(wingmanRoot2, "state", "dsh-hooks.json"),
10853
- backupsRoot: join12(wingmanRoot2, "backups")
10905
+ hooksConfigFile: join14(hooksDir, "hooks-claude-code.json"),
10906
+ forwarderFile: join14(hooksDir, "forward.mjs"),
10907
+ stateFile: join14(wingmanRoot2, "state", "dsh-hooks.json"),
10908
+ backupsRoot: join14(wingmanRoot2, "backups")
10854
10909
  };
10855
10910
  }
10856
10911
  function yamlSingleQuote(value) {
@@ -10973,20 +11028,20 @@ async function readState3(paths) {
10973
11028
  const buffer = await readIfExists(paths.stateFile);
10974
11029
  if (buffer === void 0)
10975
11030
  return void 0;
10976
- const state = JSON.parse(buffer.toString("utf8"));
10977
- if (state.version !== 1)
10978
- throw new Error(`\u672A\u77E5\u7684\u63A5\u7EBF\u72B6\u6001\u7248\u672C:${String(state.version)},\u4E0D\u731C`);
10979
- return state;
11031
+ const state2 = JSON.parse(buffer.toString("utf8"));
11032
+ if (state2.version !== 1)
11033
+ throw new Error(`\u672A\u77E5\u7684\u63A5\u7EBF\u72B6\u6001\u7248\u672C:${String(state2.version)},\u4E0D\u731C`);
11034
+ return state2;
10980
11035
  }
10981
- async function writeState3(paths, state) {
11036
+ async function writeState3(paths, state2) {
10982
11037
  await mkdir(dirname9(paths.stateFile), { recursive: true });
10983
- await writeFile(paths.stateFile, `${JSON.stringify(state, void 0, 2)}
11038
+ await writeFile(paths.stateFile, `${JSON.stringify(state2, void 0, 2)}
10984
11039
  `, "utf8");
10985
11040
  }
10986
11041
  async function backupPatchFile(paths, bytes) {
10987
- const dir = join12(paths.backupsRoot, (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"));
11042
+ const dir = join14(paths.backupsRoot, (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-"));
10988
11043
  await mkdir(dir, { recursive: true });
10989
- await writeFile(join12(dir, "cordis.patch.yml"), bytes);
11044
+ await writeFile(join14(dir, "cordis.patch.yml"), bytes);
10990
11045
  return dir;
10991
11046
  }
10992
11047
  async function writeOwnedFiles(paths, url) {
@@ -11046,13 +11101,13 @@ async function installDshHooks(port, options) {
11046
11101
  }
11047
11102
  async function uninstallDshHooks(options) {
11048
11103
  const paths = resolveDshHookPaths(options);
11049
- const state = await readState3(paths);
11104
+ const state2 = await readState3(paths);
11050
11105
  const currentBuffer = await readIfExists(paths.patchFile);
11051
11106
  if (currentBuffer !== void 0) {
11052
11107
  const currentText = decodeUtf8Strict(currentBuffer, paths.patchFile);
11053
11108
  if (currentText.includes(HOOKS_MARKER_BEGIN_PREFIX)) {
11054
11109
  const surgical = stripLiveHooksBlocks(currentText, paths.patchFile);
11055
- const restored = resolveRestoredText(surgical, state, paths);
11110
+ const restored = resolveRestoredText(surgical, state2, paths);
11056
11111
  if (restored !== currentText) {
11057
11112
  await backupPatchFile(paths, currentBuffer);
11058
11113
  if (restored === void 0)
@@ -11066,9 +11121,9 @@ async function uninstallDshHooks(options) {
11066
11121
  await rm(paths.forwarderFile, { force: true });
11067
11122
  await rm(paths.stateFile, { force: true });
11068
11123
  }
11069
- function resolveRestoredText(surgical, state, paths) {
11070
- if (state?.original.existed === true) {
11071
- const originalText = Buffer3.from(state.original.bytesBase64, "base64").toString("utf8");
11124
+ function resolveRestoredText(surgical, state2, paths) {
11125
+ if (state2?.original.existed === true) {
11126
+ const originalText = Buffer3.from(state2.original.bytesBase64, "base64").toString("utf8");
11072
11127
  const surgicalEntries = tryParsePatchEntries(surgical, paths.patchFile);
11073
11128
  const originalEntries = tryParsePatchEntries(originalText, paths.patchFile);
11074
11129
  if (surgicalEntries !== void 0 && originalEntries !== void 0 && JSON.stringify(surgicalEntries) === JSON.stringify(originalEntries)) {
@@ -11138,7 +11193,7 @@ if (url !== undefined) {
11138
11193
  import { Buffer as Buffer4 } from "node:buffer";
11139
11194
  import { randomBytes } from "node:crypto";
11140
11195
  import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
11141
- import { dirname as dirname10, isAbsolute as isAbsolute2, join as join13, relative } from "node:path";
11196
+ import { dirname as dirname10, isAbsolute as isAbsolute2, join as join15, relative } from "node:path";
11142
11197
  function buildToggleBlock(instanceId, flags) {
11143
11198
  const lines = [`${TOGGLE_BEGIN_PREFIX}${instanceId}${TOGGLE_BEGIN_SUFFIX}`];
11144
11199
  if (flags.fileCreated)
@@ -11429,7 +11484,7 @@ async function planDshToggle(action, dump, options) {
11429
11484
  harness: "dsh",
11430
11485
  action: toggle,
11431
11486
  writes: [change],
11432
- backupDir: join13(paths.backupsRoot, backupDirName()),
11487
+ backupDir: join15(paths.backupsRoot, backupDirName()),
11433
11488
  references: scanDshReferences(paths.patchFile, text, dump, row),
11434
11489
  // 生效时机:patch 层在 boot 时组装(composeProfile),保证下次启动生效;钉定版本
11435
11490
  // 还会 watchUserPatches 热重载家目录层,但该监听失败被静默吞掉(suppressShutdownError)
@@ -11489,7 +11544,7 @@ async function applyDshToggle(plan, options) {
11489
11544
  }
11490
11545
  await mkdir2(plan.backupDir, { recursive: true });
11491
11546
  if (buffer !== void 0) {
11492
- await writeFile2(join13(plan.backupDir, "cordis.patch.yml"), buffer);
11547
+ await writeFile2(join15(plan.backupDir, "cordis.patch.yml"), buffer);
11493
11548
  }
11494
11549
  if (change.kind === "delete") {
11495
11550
  await rm2(paths.patchFile);
@@ -11528,70 +11583,95 @@ __export(dist_exports3, {
11528
11583
  createAdapter: () => createAdapter3,
11529
11584
  detectDshLiveWiring: () => detectDshLiveWiring,
11530
11585
  parseDumpConfig: () => parseDumpConfig,
11586
+ resolveDshHome: () => resolveDshHome,
11531
11587
  resolveDshHookPaths: () => resolveDshHookPaths
11532
11588
  });
11533
- import { spawn } from "node:child_process";
11534
- import { readFileSync as readFileSync10 } from "node:fs";
11589
+ import { execFile } from "node:child_process";
11590
+ import { existsSync as existsSync9, readFileSync as readFileSync10 } from "node:fs";
11535
11591
  import { readFile as readFile2 } from "node:fs/promises";
11536
11592
  import { createRequire } from "node:module";
11537
- import { dirname as dirname11, join as join14 } from "node:path";
11538
- function resolvePinnedDsh() {
11593
+ import { join as join16 } from "node:path";
11594
+ import { promisify } from "node:util";
11595
+ function resolvePinnedDshVersion() {
11539
11596
  try {
11540
11597
  const require2 = createRequire(import.meta.url);
11541
- const pkgJsonPath = require2.resolve("@deepseek-ai/dsh/package.json");
11542
- const pkg = JSON.parse(readFileSync10(pkgJsonPath, "utf8"));
11543
- const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.dsh;
11544
- if (typeof pkg.version !== "string" || typeof binRel !== "string")
11545
- return void 0;
11546
- return { version: pkg.version, binPath: join14(dirname11(pkgJsonPath), binRel) };
11598
+ const pkg = JSON.parse(readFileSync10(require2.resolve("@deepseek-ai/dsh/package.json"), "utf8"));
11599
+ return typeof pkg.version === "string" ? pkg.version : void 0;
11547
11600
  } catch {
11548
11601
  return void 0;
11549
11602
  }
11550
11603
  }
11551
- async function execDumpConfig(profile) {
11552
- const pinned = resolvePinnedDsh();
11553
- if (!pinned) {
11554
- throw new Error("\u672A\u627E\u5230\u9489\u5B9A\u7684 @deepseek-ai/dsh CLI:\u771F\u673A\u626B\u63CF\u4F9D\u8D56 devDependencies \u9489\u5B9A\u7248\u672C;\u6D4B\u8BD5\u8BF7\u7528 dumpConfigPath \u6CE8\u5165 fixtures");
11555
- }
11556
- return await new Promise((resolve6, reject) => {
11557
- const child = spawn(process.execPath, [pinned.binPath, "--profile", profile, "--dump-config"], {
11558
- stdio: ["ignore", "pipe", "pipe"]
11559
- });
11560
- let stdout = "";
11561
- let stderr = "";
11562
- child.stdout.setEncoding("utf8");
11563
- child.stderr.setEncoding("utf8");
11564
- child.stdout.on("data", (chunk) => {
11565
- stdout += chunk;
11566
- });
11567
- child.stderr.on("data", (chunk) => {
11568
- stderr += chunk;
11569
- });
11570
- child.on("error", reject);
11571
- child.on("close", (code) => {
11572
- if (code === 0)
11573
- resolve6(stdout);
11574
- else
11575
- reject(new Error(`dsh --dump-config \u9000\u51FA\u7801 ${String(code)}:${stderr.slice(0, 2e3)}`));
11604
+ async function execDumpConfigViaPath(profile) {
11605
+ try {
11606
+ const { stdout } = await execFileAsync("dsh", ["--profile", profile, "--dump-config"], {
11607
+ timeout: 1e4,
11608
+ maxBuffer: 16 * 1024 * 1024
11576
11609
  });
11577
- });
11610
+ return stdout;
11611
+ } catch (error) {
11612
+ const message = error instanceof Error ? error.message : String(error);
11613
+ throw new Error(`dsh --dump-config \u6267\u884C\u5931\u8D25(PATH \u4E0A\u65E0 dsh\u3001\u8D85\u65F6\u6216\u975E\u96F6\u9000\u51FA):${message}`);
11614
+ }
11615
+ }
11616
+ function degradedConfig(wired, version) {
11617
+ const config = {
11618
+ harness: "dsh",
11619
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
11620
+ slots: {
11621
+ base: [
11622
+ {
11623
+ id: "dsh:base:bundle:dsh-cli-unavailable",
11624
+ slot: "base",
11625
+ nativeName: "dsh \u5DF2\u68C0\u6D4B\u5230;\u8BFB\u53D6\u914D\u7F6E\u9700 dsh CLI \u53EF\u7528(PATH \u4E0A\u6267\u884C dsh --dump-config \u5931\u8D25)",
11626
+ nativePath: "",
11627
+ toggleable: false,
11628
+ enabled: true,
11629
+ source: "bundle"
11630
+ }
11631
+ ],
11632
+ rules: [],
11633
+ skills: [],
11634
+ tools: [],
11635
+ gates: [],
11636
+ history: []
11637
+ },
11638
+ capabilities: { live: wired ? "full" : "none", actions: [] }
11639
+ };
11640
+ if (version !== void 0)
11641
+ config.harnessVersion = version;
11642
+ return config;
11578
11643
  }
11579
11644
  function createAdapter3(options = {}) {
11580
11645
  const profile = options.profile ?? "web";
11581
11646
  const dumpConfigPath = options.dumpConfigPath;
11647
+ const runDump = options.dumpRunner ?? execDumpConfigViaPath;
11648
+ const loadDumpText = async () => dumpConfigPath !== void 0 ? await readFile2(dumpConfigPath, "utf8") : await runDump(profile);
11582
11649
  return {
11583
11650
  harness: "dsh",
11584
- // dsh 的检测依据 = 钉定 CLI 是否可解析(不读 $HOME,不跑命令),ScanOptions 不参与
11585
- async detect(_options) {
11586
- const pinned = resolvePinnedDsh();
11587
- return pinned ? { installed: true, version: pinned.version } : { installed: false };
11651
+ // 检测与 CC/Codex 同构:探用户 dsh 家目录(homeDir/.dsh $DSH_HOME → ~/.dsh)是否存在。
11652
+ // 版本尽力:钉定 devDependency 可解析时用其版本(仓库开发形态);发布产物无此依赖
11653
+ // 省略,不跑命令探版本。
11654
+ async detect(options2) {
11655
+ if (!existsSync9(resolveDshHome(options2)))
11656
+ return { installed: false };
11657
+ const version = resolvePinnedDshVersion();
11658
+ return version !== void 0 ? { installed: true, version } : { installed: true };
11588
11659
  },
11589
11660
  // anatomy 唯一来源 = dump-config 输出;homeDir 只用于接线检测(hermetic 注入)
11590
11661
  async scan(options2) {
11591
- const text = dumpConfigPath !== void 0 ? await readFile2(dumpConfigPath, "utf8") : await execDumpConfig(profile);
11592
- const slots = buildSlots(parseDumpConfig(text));
11593
- const pinned = resolvePinnedDsh();
11594
11662
  const wired = await detectDshLiveWiring(options2);
11663
+ const version = resolvePinnedDshVersion();
11664
+ let text;
11665
+ if (dumpConfigPath !== void 0) {
11666
+ text = await readFile2(dumpConfigPath, "utf8");
11667
+ } else {
11668
+ try {
11669
+ text = await runDump(profile);
11670
+ } catch {
11671
+ return degradedConfig(wired, version);
11672
+ }
11673
+ }
11674
+ const slots = buildSlots(parseDumpConfig(text));
11595
11675
  const config = {
11596
11676
  harness: "dsh",
11597
11677
  scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11599,13 +11679,13 @@ function createAdapter3(options = {}) {
11599
11679
  // M3:toggle 已落地(patch row 开关);open_editor 不适用(dump 条目无 nativePath)
11600
11680
  capabilities: { live: wired ? "full" : "none", actions: ["toggle"] }
11601
11681
  };
11602
- if (pinned)
11603
- config.harnessVersion = pinned.version;
11682
+ if (version !== void 0)
11683
+ config.harnessVersion = version;
11604
11684
  return config;
11605
11685
  },
11606
11686
  async planAction(action, options2) {
11607
- const text = dumpConfigPath !== void 0 ? await readFile2(dumpConfigPath, "utf8") : await execDumpConfig(profile);
11608
- const file = dumpConfigPath ?? join14(resolveDshHookPaths(options2).dshHome, `dump-config(${profile})`);
11687
+ const text = await loadDumpText();
11688
+ const file = dumpConfigPath ?? join16(resolveDshHookPaths(options2).dshHome, `dump-config(${profile})`);
11609
11689
  return await planDshToggle(action, { text, file }, options2);
11610
11690
  },
11611
11691
  async applyAction(plan, options2) {
@@ -11619,6 +11699,7 @@ function createAdapter3(options = {}) {
11619
11699
  }
11620
11700
  };
11621
11701
  }
11702
+ var execFileAsync;
11622
11703
  var init_dist5 = __esm({
11623
11704
  "packages/adapter-dsh/dist/index.js"() {
11624
11705
  "use strict";
@@ -11627,13 +11708,23 @@ var init_dist5 = __esm({
11627
11708
  init_toggle();
11628
11709
  init_dump_config();
11629
11710
  init_hooks3();
11711
+ execFileAsync = promisify(execFile);
11630
11712
  }
11631
11713
  });
11632
11714
 
11633
11715
  // apps/cli/dist/main.js
11634
11716
  init_dist();
11635
- import { dirname as dirname12, resolve as resolve5 } from "node:path";
11636
- import { fileURLToPath as fileURLToPath2 } from "node:url";
11717
+ import { readFileSync as readFileSync12 } from "node:fs";
11718
+ import { dirname as dirname12, resolve as resolve6 } from "node:path";
11719
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
11720
+
11721
+ // packages/fixtures/dist/index.js
11722
+ import { dirname, join } from "node:path";
11723
+ import { fileURLToPath } from "node:url";
11724
+ var fixturesRoot = join(dirname(dirname(fileURLToPath(import.meta.url))), "data");
11725
+ function fixturePath(...segments) {
11726
+ return join(fixturesRoot, ...segments);
11727
+ }
11637
11728
 
11638
11729
  // packages/live/dist/receiver.js
11639
11730
  init_dist();
@@ -11646,6 +11737,7 @@ function createReceiver(options = {}) {
11646
11737
  buffer.push(event);
11647
11738
  if (buffer.length > bufferSize)
11648
11739
  buffer.shift();
11740
+ options.onEvent?.(event);
11649
11741
  const data = frame(event);
11650
11742
  for (const subscriber of subscribers)
11651
11743
  subscriber.res.write(data);
@@ -11733,10 +11825,10 @@ function isHookPayload(value) {
11733
11825
  return typeof name === "string" && name.length > 0;
11734
11826
  }
11735
11827
  function readBody(req) {
11736
- return new Promise((resolve6, reject) => {
11828
+ return new Promise((resolve7, reject) => {
11737
11829
  const chunks = [];
11738
11830
  req.on("data", (chunk) => chunks.push(chunk));
11739
- req.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
11831
+ req.on("end", () => resolve7(Buffer.concat(chunks).toString("utf8")));
11740
11832
  req.on("error", reject);
11741
11833
  });
11742
11834
  }
@@ -11749,7 +11841,7 @@ function sendText(res, status, body) {
11749
11841
  import { verify } from "node:crypto";
11750
11842
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
11751
11843
  import { homedir } from "node:os";
11752
- import { join } from "node:path";
11844
+ import { join as join2 } from "node:path";
11753
11845
  var OFFLINE_GRACE_DAYS = 14;
11754
11846
  var PROD_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAyU4yO0SJcflt/CV77wqVxIwCkM52CYGC6qrPGP8dMME=\n-----END PUBLIC KEY-----\n";
11755
11847
  function checkLicense(options = {}) {
@@ -11763,7 +11855,7 @@ function licenseStatus(options = {}) {
11763
11855
  const home = options.homeDir ?? homedir();
11764
11856
  let raw;
11765
11857
  try {
11766
- raw = readFileSync(join(home, ".wingman", "license.json"), "utf8");
11858
+ raw = readFileSync(join2(home, ".wingman", "license.json"), "utf8");
11767
11859
  } catch {
11768
11860
  return { state: "none", detail: "\u672A\u5B89\u88C5 license(anatomy \u4E0E\u5B9E\u51B5\u514D\u8D39\u53EF\u7528;\u5199\u52A8\u4F5C\u9700\u8BA2\u9605)" };
11769
11861
  }
@@ -11832,9 +11924,9 @@ function installLicense(raw, options = {}) {
11832
11924
  return { ok: false, error: status.detail };
11833
11925
  }
11834
11926
  }
11835
- const dir = join(options.homeDir ?? homedir(), ".wingman");
11836
- const licensePath = join(dir, "license.json");
11837
- const tmpPath = join(dir, `.license.json.tmp-${process.pid}`);
11927
+ const dir = join2(options.homeDir ?? homedir(), ".wingman");
11928
+ const licensePath = join2(dir, "license.json");
11929
+ const tmpPath = join2(dir, `.license.json.tmp-${process.pid}`);
11838
11930
  try {
11839
11931
  mkdirSync(dir, { recursive: true });
11840
11932
  writeFileSync(tmpPath, raw);
@@ -11851,12 +11943,12 @@ function installLicense(raw, options = {}) {
11851
11943
  // apps/cli/dist/license-refresh.js
11852
11944
  import { readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
11853
11945
  import { homedir as homedir2 } from "node:os";
11854
- import { dirname, join as join2 } from "node:path";
11946
+ import { dirname as dirname2, join as join3 } from "node:path";
11855
11947
  var DEFAULT_LICENSE_SERVER = "https://api.getwingman.dev";
11856
11948
  async function refreshLicense(options = {}) {
11857
11949
  const home = options.homeDir ?? homedir2();
11858
11950
  const baseUrl = options.baseUrl ?? process.env.WINGMAN_LICENSE_SERVER ?? DEFAULT_LICENSE_SERVER;
11859
- const licensePath = join2(home, ".wingman", "license.json");
11951
+ const licensePath = join3(home, ".wingman", "license.json");
11860
11952
  let raw;
11861
11953
  try {
11862
11954
  raw = readFileSync2(licensePath, "utf8");
@@ -11898,7 +11990,7 @@ async function refreshLicense(options = {}) {
11898
11990
  }
11899
11991
  const next = `${JSON.stringify({ token: entitlement.token, signature: entitlement.signature })}
11900
11992
  `;
11901
- const tmpPath = join2(dirname(licensePath), `.license.json.tmp-${process.pid}`);
11993
+ const tmpPath = join3(dirname2(licensePath), `.license.json.tmp-${process.pid}`);
11902
11994
  try {
11903
11995
  writeFileSync2(tmpPath, next);
11904
11996
  renameSync2(tmpPath, licensePath);
@@ -11917,18 +12009,1184 @@ function expSuffix(token) {
11917
12009
  }
11918
12010
  }
11919
12011
 
11920
- // apps/cli/dist/registry.js
11921
- import { existsSync as existsSync9 } from "node:fs";
12012
+ // apps/cli/dist/narrative-service.js
12013
+ init_dist();
12014
+ import { readdir } from "node:fs/promises";
12015
+ import { join as join4 } from "node:path";
11922
12016
 
11923
- // packages/fixtures/dist/index.js
11924
- import { dirname as dirname2, join as join3 } from "node:path";
11925
- import { fileURLToPath } from "node:url";
11926
- var fixturesRoot = join3(dirname2(dirname2(fileURLToPath(import.meta.url))), "data");
11927
- function fixturePath(...segments) {
11928
- return join3(fixturesRoot, ...segments);
12017
+ // packages/narrative/dist/attribute.js
12018
+ function extractSteps(lines) {
12019
+ const steps = [];
12020
+ const byToolUseId = /* @__PURE__ */ new Map();
12021
+ for (const line of lines) {
12022
+ if (line.kind === "assistant") {
12023
+ for (const block of line.blocks) {
12024
+ if (block.kind !== "tool_use")
12025
+ continue;
12026
+ const step = {
12027
+ toolUseId: block.id,
12028
+ toolName: block.name,
12029
+ toolInput: block.input,
12030
+ isError: false
12031
+ };
12032
+ if (line.timestamp !== void 0)
12033
+ step.ts = line.timestamp;
12034
+ if (line.agentId !== void 0)
12035
+ step.agentId = line.agentId;
12036
+ steps.push(step);
12037
+ byToolUseId.set(block.id, step);
12038
+ }
12039
+ } else if (line.kind === "user") {
12040
+ for (const result of line.toolResults) {
12041
+ const step = byToolUseId.get(result.toolUseId);
12042
+ if (step === void 0)
12043
+ continue;
12044
+ if (line.timestamp !== void 0)
12045
+ step.endTs = line.timestamp;
12046
+ if (result.isError)
12047
+ step.isError = true;
12048
+ }
12049
+ }
12050
+ }
12051
+ return steps;
12052
+ }
12053
+ function deriveSubagentIntervals(lines) {
12054
+ const agentSteps = extractSteps(lines).filter((step) => step.toolName === "Agent");
12055
+ return agentSteps.map((step) => {
12056
+ const interval = { agentId: step.toolUseId };
12057
+ if (step.ts !== void 0)
12058
+ interval.startTs = step.ts;
12059
+ if (step.endTs !== void 0)
12060
+ interval.endTs = step.endTs;
12061
+ return interval;
12062
+ });
12063
+ }
12064
+ function attributeSteps(steps, timeline, intervals = []) {
12065
+ return steps.map((step) => ({ step, attribution: attributeOne(step, timeline, intervals) }));
12066
+ }
12067
+ function attributeOne(step, timeline, intervals) {
12068
+ const ts = step.ts;
12069
+ if (ts === void 0)
12070
+ return { kind: "other", reason: "no-timestamp" };
12071
+ const active = timeline.filter((entry) => isInProgressAt(entry, ts));
12072
+ if (active.length === 0)
12073
+ return { kind: "other", reason: "no-active-todo" };
12074
+ const only = active[0];
12075
+ if (active.length === 1 && only !== void 0)
12076
+ return { kind: "todo", todoId: only.id };
12077
+ if (step.agentId !== void 0) {
12078
+ const todoIds = /* @__PURE__ */ new Set();
12079
+ for (const interval of intervals) {
12080
+ if (interval.agentId !== step.agentId)
12081
+ continue;
12082
+ const todoId = resolveIntervalTodo(interval, timeline);
12083
+ if (todoId !== void 0)
12084
+ todoIds.add(todoId);
12085
+ }
12086
+ if (todoIds.size === 1) {
12087
+ const [todoId] = todoIds;
12088
+ if (todoId !== void 0 && active.some((entry) => entry.id === todoId)) {
12089
+ return { kind: "todo", todoId };
12090
+ }
12091
+ }
12092
+ }
12093
+ return { kind: "other", reason: "ambiguous" };
12094
+ }
12095
+ function resolveIntervalTodo(interval, timeline) {
12096
+ const start = interval.startTs;
12097
+ if (start === void 0)
12098
+ return void 0;
12099
+ const active = timeline.filter((entry) => isInProgressAt(entry, start));
12100
+ const only = active[0];
12101
+ return active.length === 1 && only !== void 0 ? only.id : void 0;
12102
+ }
12103
+ function isInProgressAt(entry, ts) {
12104
+ return entry.spans.some((span) => span.status === "in_progress" && span.from !== void 0 && span.from <= ts && (span.to === void 0 || ts < span.to));
12105
+ }
12106
+
12107
+ // packages/narrative/dist/parse.js
12108
+ function parseTranscriptLine(line) {
12109
+ const trimmed = line.trim();
12110
+ if (trimmed === "")
12111
+ return { kind: "unknown", reason: "\u7A7A\u884C", rawText: line };
12112
+ let value;
12113
+ try {
12114
+ value = JSON.parse(trimmed);
12115
+ } catch {
12116
+ return { kind: "unknown", reason: "JSON \u89E3\u6790\u5931\u8D25", rawText: line };
12117
+ }
12118
+ if (!isRecord2(value))
12119
+ return { kind: "unknown", reason: "\u884C\u4E0D\u662F JSON \u5BF9\u8C61", rawText: line };
12120
+ const type = value.type;
12121
+ if (typeof type !== "string") {
12122
+ return { kind: "unknown", reason: "\u7F3A type \u5B57\u6BB5\u6216\u975E\u5B57\u7B26\u4E32", rawText: line };
12123
+ }
12124
+ const common = extractCommon(value);
12125
+ if (type === "user")
12126
+ return parseUser(value, common);
12127
+ if (type === "assistant")
12128
+ return parseAssistant(value, common);
12129
+ if (type === "attachment") {
12130
+ const attachment = {
12131
+ kind: "attachment",
12132
+ ...common,
12133
+ attachment: isRecord2(value.attachment) ? value.attachment : {}
12134
+ };
12135
+ return attachment;
12136
+ }
12137
+ return { kind: "other", type, ...common };
12138
+ }
12139
+ function extractCommon(raw) {
12140
+ const common = { raw };
12141
+ if (typeof raw.uuid === "string")
12142
+ common.uuid = raw.uuid;
12143
+ if (typeof raw.parentUuid === "string")
12144
+ common.parentUuid = raw.parentUuid;
12145
+ if (typeof raw.timestamp === "string")
12146
+ common.timestamp = raw.timestamp;
12147
+ if (typeof raw.sessionId === "string")
12148
+ common.sessionId = raw.sessionId;
12149
+ if (typeof raw.agentId === "string")
12150
+ common.agentId = raw.agentId;
12151
+ if (typeof raw.isSidechain === "boolean")
12152
+ common.isSidechain = raw.isSidechain;
12153
+ return common;
12154
+ }
12155
+ function parseUser(raw, common) {
12156
+ const line = { kind: "user", ...common, toolResults: [] };
12157
+ const message = isRecord2(raw.message) ? raw.message : void 0;
12158
+ const content = message?.content;
12159
+ if (typeof content === "string") {
12160
+ line.text = content;
12161
+ } else if (Array.isArray(content)) {
12162
+ const texts = [];
12163
+ for (const block of content) {
12164
+ if (!isRecord2(block))
12165
+ continue;
12166
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
12167
+ const result = {
12168
+ toolUseId: block.tool_use_id,
12169
+ // 实测:成功结果常无 is_error 字段;仅显式 true 视为失败
12170
+ isError: block.is_error === true,
12171
+ content: block.content
12172
+ };
12173
+ line.toolResults.push(result);
12174
+ } else if (block.type === "text" && typeof block.text === "string") {
12175
+ texts.push(block.text);
12176
+ }
12177
+ }
12178
+ if (texts.length > 0)
12179
+ line.text = texts.join("\n");
12180
+ }
12181
+ if ("toolUseResult" in raw)
12182
+ line.toolUseResult = raw.toolUseResult;
12183
+ return line;
12184
+ }
12185
+ function parseAssistant(raw, common) {
12186
+ const line = { kind: "assistant", ...common, blocks: [] };
12187
+ const message = isRecord2(raw.message) ? raw.message : void 0;
12188
+ if (message === void 0)
12189
+ return line;
12190
+ if (typeof message.model === "string")
12191
+ line.model = message.model;
12192
+ if (typeof message.id === "string")
12193
+ line.messageId = message.id;
12194
+ if (isRecord2(message.usage))
12195
+ line.usage = message.usage;
12196
+ const content = message.content;
12197
+ if (!Array.isArray(content))
12198
+ return line;
12199
+ for (const block of content) {
12200
+ if (!isRecord2(block))
12201
+ continue;
12202
+ line.blocks.push(parseAssistantBlock(block));
12203
+ }
12204
+ return line;
12205
+ }
12206
+ function parseAssistantBlock(block) {
12207
+ if (block.type === "text" && typeof block.text === "string") {
12208
+ return { kind: "text", text: block.text };
12209
+ }
12210
+ if (block.type === "thinking" && typeof block.thinking === "string") {
12211
+ return { kind: "thinking", thinking: block.thinking };
12212
+ }
12213
+ if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
12214
+ return {
12215
+ kind: "tool_use",
12216
+ id: block.id,
12217
+ name: block.name,
12218
+ input: isRecord2(block.input) ? block.input : {}
12219
+ };
12220
+ }
12221
+ return {
12222
+ kind: "other",
12223
+ type: typeof block.type === "string" ? block.type : "unknown",
12224
+ raw: block
12225
+ };
12226
+ }
12227
+ function isRecord2(value) {
12228
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12229
+ }
12230
+
12231
+ // packages/narrative/dist/phase.js
12232
+ var PHASE_LABELS = {
12233
+ inspect: "\u68C0\u67E5",
12234
+ modify: "\u4FEE\u6539",
12235
+ verify: "\u9A8C\u8BC1",
12236
+ other: "\u5176\u4ED6"
12237
+ };
12238
+ var PHASE_TOOL_RULES = {
12239
+ Read: "inspect",
12240
+ Grep: "inspect",
12241
+ Glob: "inspect",
12242
+ WebFetch: "inspect",
12243
+ Edit: "modify",
12244
+ Write: "modify",
12245
+ NotebookEdit: "modify"
12246
+ };
12247
+ var VERIFY_COMMAND_WORDS = [
12248
+ "test",
12249
+ "vitest",
12250
+ "jest",
12251
+ "pytest",
12252
+ "playwright",
12253
+ "build",
12254
+ "lint",
12255
+ "biome",
12256
+ "eslint",
12257
+ "tsc",
12258
+ "typecheck",
12259
+ "check"
12260
+ ];
12261
+ var VERIFY_COMMAND_RE = new RegExp(`\\b(?:${VERIFY_COMMAND_WORDS.join("|")})\\b`);
12262
+ function classifyPhase(toolName, toolInput) {
12263
+ const ruled = PHASE_TOOL_RULES[toolName];
12264
+ if (ruled !== void 0)
12265
+ return ruled;
12266
+ if (toolName === "Bash") {
12267
+ const command = toolInput?.command;
12268
+ if (typeof command === "string" && VERIFY_COMMAND_RE.test(command))
12269
+ return "verify";
12270
+ }
12271
+ return "other";
12272
+ }
12273
+ function consolidatePhases(steps) {
12274
+ const nodes = [];
12275
+ for (const step of steps) {
12276
+ const phase = classifyPhase(step.toolName, step.toolInput);
12277
+ const last = nodes[nodes.length - 1];
12278
+ if (last !== void 0 && last.phase === phase) {
12279
+ last.steps.push(step);
12280
+ if (step.isError)
12281
+ last.failed = true;
12282
+ } else {
12283
+ const node = { phase, steps: [step], failed: step.isError };
12284
+ nodes.push(node);
12285
+ }
12286
+ }
12287
+ for (const node of nodes) {
12288
+ const first = node.steps[0];
12289
+ const lastStep = node.steps[node.steps.length - 1];
12290
+ if (first?.ts !== void 0)
12291
+ node.startTs = first.ts;
12292
+ const end = lastStep?.endTs ?? lastStep?.ts;
12293
+ if (end !== void 0)
12294
+ node.endTs = end;
12295
+ if (node.startTs !== void 0 && node.endTs !== void 0) {
12296
+ const start = Date.parse(node.startTs);
12297
+ const endMs = Date.parse(node.endTs);
12298
+ if (Number.isFinite(start) && Number.isFinite(endMs) && endMs >= start) {
12299
+ node.durationMs = endMs - start;
12300
+ }
12301
+ }
12302
+ }
12303
+ return nodes;
12304
+ }
12305
+
12306
+ // packages/narrative/dist/tail.js
12307
+ import { open, stat } from "node:fs/promises";
12308
+ var NEWLINE = 10;
12309
+ function followTranscript(path, onLine, options = {}) {
12310
+ const intervalMs = options.intervalMs ?? 500;
12311
+ let offset = 0;
12312
+ let remainder = Buffer.alloc(0);
12313
+ let closed = false;
12314
+ let timer;
12315
+ async function poll() {
12316
+ try {
12317
+ const stats = await stat(path);
12318
+ if (stats.size < offset) {
12319
+ offset = 0;
12320
+ remainder = Buffer.alloc(0);
12321
+ }
12322
+ if (stats.size > offset) {
12323
+ const handle2 = await open(path, "r");
12324
+ try {
12325
+ const length = stats.size - offset;
12326
+ const buffer = Buffer.alloc(length);
12327
+ const { bytesRead } = await handle2.read(buffer, 0, length, offset);
12328
+ offset += bytesRead;
12329
+ emitLines(buffer.subarray(0, bytesRead));
12330
+ } finally {
12331
+ await handle2.close();
12332
+ }
12333
+ }
12334
+ } catch {
12335
+ }
12336
+ if (!closed)
12337
+ timer = setTimeout(poll, intervalMs);
12338
+ }
12339
+ function emitLines(chunk) {
12340
+ let data = remainder.length > 0 ? Buffer.concat([remainder, chunk]) : chunk;
12341
+ let newlineAt = data.indexOf(NEWLINE);
12342
+ while (newlineAt !== -1) {
12343
+ const line = data.subarray(0, newlineAt).toString("utf8");
12344
+ data = data.subarray(newlineAt + 1);
12345
+ if (!closed)
12346
+ onLine(line);
12347
+ newlineAt = data.indexOf(NEWLINE);
12348
+ }
12349
+ remainder = Buffer.from(data);
12350
+ }
12351
+ void poll();
12352
+ return {
12353
+ close() {
12354
+ closed = true;
12355
+ if (timer !== void 0)
12356
+ clearTimeout(timer);
12357
+ }
12358
+ };
12359
+ }
12360
+
12361
+ // packages/narrative/dist/textlist.js
12362
+ var CHECKLIST_LINE = /^\s*[-*]\s+\[([ xX])\]\s+(.+?)\s*$/;
12363
+ function parseTextChecklist(text) {
12364
+ const items = [];
12365
+ for (const line of text.split("\n")) {
12366
+ const match = CHECKLIST_LINE.exec(line);
12367
+ if (match === null)
12368
+ continue;
12369
+ const [, mark, content] = match;
12370
+ if (content === void 0 || content === "")
12371
+ continue;
12372
+ items.push({ content, checked: mark !== " " });
12373
+ }
12374
+ return items;
12375
+ }
12376
+
12377
+ // packages/narrative/dist/todos.js
12378
+ var TODO_STATUSES = ["pending", "in_progress", "completed"];
12379
+ function isTodoStatus(value) {
12380
+ return typeof value === "string" && TODO_STATUSES.includes(value);
12381
+ }
12382
+ function extractTodoSnapshots(lines) {
12383
+ const snapshots = [];
12384
+ const tasks = /* @__PURE__ */ new Map();
12385
+ const createResultIds = buildTaskCreateResultIndex(lines);
12386
+ for (const line of lines) {
12387
+ if (line.kind !== "assistant")
12388
+ continue;
12389
+ for (const block of line.blocks) {
12390
+ if (block.kind !== "tool_use")
12391
+ continue;
12392
+ let changed = false;
12393
+ if (block.name === "TodoWrite") {
12394
+ changed = applyTodoWrite(block.input, tasks);
12395
+ } else if (block.name === "TaskCreate") {
12396
+ changed = applyTaskCreate(block.input, block.id, createResultIds, tasks);
12397
+ } else if (block.name === "TaskUpdate") {
12398
+ changed = applyTaskUpdate(block.input, tasks);
12399
+ }
12400
+ if (changed)
12401
+ snapshots.push(snapshotOf(tasks, line.timestamp));
12402
+ }
12403
+ }
12404
+ return snapshots;
12405
+ }
12406
+ function buildTaskCreateResultIndex(lines) {
12407
+ const index = /* @__PURE__ */ new Map();
12408
+ for (const line of lines) {
12409
+ if (line.kind !== "user")
12410
+ continue;
12411
+ const result = line.toolUseResult;
12412
+ if (!isRecord2(result) || !isRecord2(result.task) || typeof result.task.id !== "string")
12413
+ continue;
12414
+ for (const toolResult of line.toolResults)
12415
+ index.set(toolResult.toolUseId, result.task.id);
12416
+ }
12417
+ return index;
12418
+ }
12419
+ function applyTodoWrite(input, tasks) {
12420
+ const todos = input.todos;
12421
+ if (!Array.isArray(todos))
12422
+ return false;
12423
+ tasks.clear();
12424
+ for (const raw of todos) {
12425
+ if (!isRecord2(raw) || typeof raw.content !== "string" || !isTodoStatus(raw.status))
12426
+ continue;
12427
+ const item = { id: raw.content, content: raw.content, status: raw.status };
12428
+ if (typeof raw.activeForm === "string")
12429
+ item.activeForm = raw.activeForm;
12430
+ tasks.set(item.id, item);
12431
+ }
12432
+ return true;
12433
+ }
12434
+ function applyTaskCreate(input, toolUseId, createResultIds, tasks) {
12435
+ if (typeof input.subject !== "string")
12436
+ return false;
12437
+ const id = createResultIds.get(toolUseId) ?? toolUseId;
12438
+ const item = { id, content: input.subject, status: "pending" };
12439
+ if (typeof input.activeForm === "string")
12440
+ item.activeForm = input.activeForm;
12441
+ tasks.set(id, item);
12442
+ return true;
12443
+ }
12444
+ function applyTaskUpdate(input, tasks) {
12445
+ if (typeof input.taskId !== "string")
12446
+ return false;
12447
+ const existing = tasks.get(input.taskId);
12448
+ if (existing === void 0)
12449
+ return false;
12450
+ if (isTodoStatus(input.status) && input.status !== existing.status) {
12451
+ existing.status = input.status;
12452
+ return true;
12453
+ }
12454
+ return false;
12455
+ }
12456
+ function snapshotOf(tasks, ts) {
12457
+ const snapshot = { todos: [...tasks.values()].map((item) => ({ ...item })) };
12458
+ if (ts !== void 0)
12459
+ snapshot.ts = ts;
12460
+ return snapshot;
12461
+ }
12462
+ function deriveTodoTimeline(snapshots) {
12463
+ const entries = /* @__PURE__ */ new Map();
12464
+ for (const snapshot of snapshots) {
12465
+ const present = /* @__PURE__ */ new Set();
12466
+ for (const todo of snapshot.todos) {
12467
+ present.add(todo.id);
12468
+ const existing = entries.get(todo.id);
12469
+ if (existing === void 0) {
12470
+ const entry = {
12471
+ id: todo.id,
12472
+ content: todo.content,
12473
+ spans: [newSpan(todo.status, snapshot.ts)]
12474
+ };
12475
+ if (todo.activeForm !== void 0)
12476
+ entry.activeForm = todo.activeForm;
12477
+ entries.set(todo.id, entry);
12478
+ continue;
12479
+ }
12480
+ existing.content = todo.content;
12481
+ if (todo.activeForm !== void 0)
12482
+ existing.activeForm = todo.activeForm;
12483
+ const last = existing.spans[existing.spans.length - 1];
12484
+ if (last !== void 0 && last.to === void 0 && last.status !== todo.status) {
12485
+ if (snapshot.ts !== void 0)
12486
+ last.to = snapshot.ts;
12487
+ existing.spans.push(newSpan(todo.status, snapshot.ts));
12488
+ }
12489
+ }
12490
+ for (const [id, entry] of entries) {
12491
+ if (present.has(id))
12492
+ continue;
12493
+ const last = entry.spans[entry.spans.length - 1];
12494
+ if (last !== void 0 && last.to === void 0 && snapshot.ts !== void 0) {
12495
+ last.to = snapshot.ts;
12496
+ }
12497
+ }
12498
+ }
12499
+ return [...entries.values()];
12500
+ }
12501
+ function newSpan(status, from) {
12502
+ const span = { status };
12503
+ if (from !== void 0)
12504
+ span.from = from;
12505
+ return span;
12506
+ }
12507
+
12508
+ // packages/narrative/dist/usage.js
12509
+ function turnUsage(line) {
12510
+ const usage = line.usage;
12511
+ if (usage === void 0)
12512
+ return void 0;
12513
+ const inputTokens = finiteNumber(usage.input_tokens);
12514
+ const cacheCreationInputTokens = finiteNumber(usage.cache_creation_input_tokens);
12515
+ const cacheReadInputTokens = finiteNumber(usage.cache_read_input_tokens);
12516
+ const outputTokens = finiteNumber(usage.output_tokens);
12517
+ if (inputTokens === void 0 || cacheCreationInputTokens === void 0 || cacheReadInputTokens === void 0 || outputTokens === void 0) {
12518
+ return void 0;
12519
+ }
12520
+ const result = {
12521
+ inputTokens,
12522
+ cacheCreationInputTokens,
12523
+ cacheReadInputTokens,
12524
+ outputTokens,
12525
+ contextTotal: {
12526
+ kind: "exact",
12527
+ value: inputTokens + cacheCreationInputTokens + cacheReadInputTokens
12528
+ }
12529
+ };
12530
+ const breakdown = usage.cache_creation;
12531
+ if (isRecord2(breakdown)) {
12532
+ const fiveMinute = finiteNumber(breakdown.ephemeral_5m_input_tokens);
12533
+ const oneHour = finiteNumber(breakdown.ephemeral_1h_input_tokens);
12534
+ if (fiveMinute !== void 0)
12535
+ result.cacheCreation5mTokens = fiveMinute;
12536
+ if (oneHour !== void 0)
12537
+ result.cacheCreation1hTokens = oneHour;
12538
+ }
12539
+ return result;
12540
+ }
12541
+ function finiteNumber(value) {
12542
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
12543
+ }
12544
+ var MODEL_PRICES = {
12545
+ "claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 },
12546
+ "claude-mythos-5": { inputPerMTok: 10, outputPerMTok: 50 },
12547
+ "claude-opus-5": { inputPerMTok: 5, outputPerMTok: 25 },
12548
+ "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
12549
+ "claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
12550
+ "claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
12551
+ "claude-sonnet-5": { inputPerMTok: 3, outputPerMTok: 15 },
12552
+ "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
12553
+ "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
12554
+ };
12555
+ var CACHE_READ_MULTIPLIER = 0.1;
12556
+ var CACHE_WRITE_5M_MULTIPLIER = 1.25;
12557
+ var CACHE_WRITE_1H_MULTIPLIER = 2;
12558
+ function costUsd(usage, model) {
12559
+ const price = MODEL_PRICES[model];
12560
+ if (price === void 0)
12561
+ return void 0;
12562
+ const fiveMinute = usage.cacheCreation5mTokens;
12563
+ const oneHour = usage.cacheCreation1hTokens;
12564
+ const cacheWriteTokensPriced = fiveMinute !== void 0 || oneHour !== void 0 ? (fiveMinute ?? 0) * CACHE_WRITE_5M_MULTIPLIER + (oneHour ?? 0) * CACHE_WRITE_1H_MULTIPLIER : usage.cacheCreationInputTokens * CACHE_WRITE_5M_MULTIPLIER;
12565
+ return (usage.inputTokens * price.inputPerMTok + usage.cacheReadInputTokens * price.inputPerMTok * CACHE_READ_MULTIPLIER + cacheWriteTokensPriced * price.inputPerMTok + usage.outputTokens * price.outputPerMTok) / 1e6;
12566
+ }
12567
+
12568
+ // apps/cli/dist/narrative-service.js
12569
+ var MAX_SESSIONS = 8;
12570
+ var ENDED_AFTER_STOP_MS = 10 * 6e4;
12571
+ var TODO_TOOLS = /* @__PURE__ */ new Set(["TaskCreate", "TaskUpdate", "TodoWrite"]);
12572
+ var DETAIL_LIMIT = 80;
12573
+ var QUOTE_LIMIT = 160;
12574
+ var ERROR_BRIEF_LIMIT = 60;
12575
+ function createNarrativeService(options = {}) {
12576
+ const follow = options.followTranscripts ?? true;
12577
+ const intervalMs = options.intervalMs ?? 500;
12578
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
12579
+ const harnesses = /* @__PURE__ */ new Map();
12580
+ let closed = false;
12581
+ function sessionsOf(harness) {
12582
+ let sessions = harnesses.get(harness);
12583
+ if (sessions === void 0) {
12584
+ sessions = /* @__PURE__ */ new Map();
12585
+ harnesses.set(harness, sessions);
12586
+ }
12587
+ return sessions;
12588
+ }
12589
+ function newSession(id) {
12590
+ return {
12591
+ id,
12592
+ lines: [],
12593
+ follows: /* @__PURE__ */ new Map(),
12594
+ hookIntervals: [],
12595
+ waiting: null,
12596
+ waitEpisodes: [],
12597
+ dirty: true
12598
+ };
12599
+ }
12600
+ function closeSession(session) {
12601
+ for (const handle2 of session.follows.values())
12602
+ handle2.close();
12603
+ session.follows.clear();
12604
+ if (session.subagentTimer !== void 0)
12605
+ clearInterval(session.subagentTimer);
12606
+ session.subagentTimer = void 0;
12607
+ }
12608
+ function evictOne(sessions, nowIso) {
12609
+ let victimId;
12610
+ let victimAt = "\uFFFF";
12611
+ let victimEnded = false;
12612
+ for (const [id, session] of sessions) {
12613
+ const ended = isEnded(session, nowIso);
12614
+ const at = lastActivityOf(session);
12615
+ const better = victimEnded === ended ? at < victimAt : ended;
12616
+ if (victimId === void 0 || better) {
12617
+ victimId = id;
12618
+ victimAt = at;
12619
+ victimEnded = ended;
12620
+ }
12621
+ }
12622
+ if (victimId !== void 0) {
12623
+ const victim = sessions.get(victimId);
12624
+ if (victim)
12625
+ closeSession(victim);
12626
+ sessions.delete(victimId);
12627
+ }
12628
+ }
12629
+ function sessionOf(harness, id) {
12630
+ const sessions = sessionsOf(harness);
12631
+ let session = sessions.get(id);
12632
+ if (session === void 0) {
12633
+ if (sessions.size >= MAX_SESSIONS)
12634
+ evictOne(sessions, now().toISOString());
12635
+ session = newSession(id);
12636
+ sessions.set(id, session);
12637
+ }
12638
+ return session;
12639
+ }
12640
+ function resetTranscript(session, path) {
12641
+ closeSession(session);
12642
+ session.lines = [];
12643
+ session.hookIntervals = [];
12644
+ session.mockNote = void 0;
12645
+ session.transcriptPath = path;
12646
+ session.dirty = true;
12647
+ }
12648
+ function startFollowing(session, path) {
12649
+ if (closed || session.follows.has(path))
12650
+ return;
12651
+ session.follows.set(path, followTranscript(path, (line) => {
12652
+ session.lines.push(parseTranscriptLine(line));
12653
+ session.dirty = true;
12654
+ }, { intervalMs }));
12655
+ }
12656
+ function startSubagentDiscovery(session, transcriptPath) {
12657
+ const sessionDir = transcriptPath.replace(/\.jsonl$/, "");
12658
+ if (sessionDir === transcriptPath)
12659
+ return;
12660
+ const subagentsDir = join4(sessionDir, "subagents");
12661
+ const poll = async () => {
12662
+ try {
12663
+ const entries = await readdir(subagentsDir);
12664
+ for (const entry of entries) {
12665
+ if (/^agent-.*\.jsonl$/.test(entry))
12666
+ startFollowing(session, join4(subagentsDir, entry));
12667
+ }
12668
+ } catch {
12669
+ }
12670
+ };
12671
+ session.subagentTimer = setInterval(() => void poll(), intervalMs);
12672
+ void poll();
12673
+ }
12674
+ function handleEvent(event) {
12675
+ if (closed || !HARNESS_IDS.includes(event.harness))
12676
+ return;
12677
+ const payload = event.payload;
12678
+ const name = payload.hook_event_name;
12679
+ const transcriptPath = typeof payload.transcript_path === "string" ? payload.transcript_path : void 0;
12680
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : void 0;
12681
+ const key = sessionId ?? transcriptPath ?? "__nosession__";
12682
+ const session = sessionOf(event.harness, key);
12683
+ session.lastActivity = event.received_at;
12684
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : void 0;
12685
+ if (cwd !== void 0) {
12686
+ const base = pathBasename(cwd);
12687
+ if (base !== "")
12688
+ session.label = base;
12689
+ }
12690
+ if (follow && transcriptPath !== void 0) {
12691
+ if (session.transcriptPath !== transcriptPath) {
12692
+ resetTranscript(session, transcriptPath);
12693
+ startFollowing(session, transcriptPath);
12694
+ startSubagentDiscovery(session, transcriptPath);
12695
+ }
12696
+ }
12697
+ if (name === "SubagentStart") {
12698
+ const agentId = agentIdOf(payload);
12699
+ if (agentId !== void 0) {
12700
+ session.hookIntervals.push({ agentId, startTs: event.received_at });
12701
+ }
12702
+ } else if (name === "SubagentStop") {
12703
+ const agentId = agentIdOf(payload);
12704
+ if (agentId !== void 0) {
12705
+ const open2 = [...session.hookIntervals].reverse().find((i) => i.agentId === agentId && i.endTs === void 0);
12706
+ if (open2)
12707
+ open2.endTs = event.received_at;
12708
+ }
12709
+ }
12710
+ if (name === "PreToolUse" && typeof payload.tool_name === "string") {
12711
+ session.pendingHookTool = {
12712
+ toolName: payload.tool_name,
12713
+ detail: hookToolDetail(payload.tool_input),
12714
+ since: event.received_at
12715
+ };
12716
+ } else if (name === "PostToolUse" && typeof payload.tool_name === "string" || name === "Stop" || name === "SessionEnd") {
12717
+ session.pendingHookTool = void 0;
12718
+ }
12719
+ if (name === "PermissionRequest" || name === "Notification") {
12720
+ const kind = name === "PermissionRequest" ? "permission" : "notification";
12721
+ const message = waitingMessage(payload, kind === "permission" ? "\u7B49\u5F85\u6388\u6743" : "\u7B49\u5F85\u56DE\u5E94");
12722
+ if (session.waiting === null) {
12723
+ session.waiting = { kind, message, since: event.received_at };
12724
+ } else if (kind === "permission" || session.waiting.kind === kind) {
12725
+ session.waiting = { kind, message, since: session.waiting.since };
12726
+ }
12727
+ } else {
12728
+ if (session.waiting !== null) {
12729
+ session.waitEpisodes.push({
12730
+ ...session.waiting,
12731
+ until: event.received_at,
12732
+ resolvedBy: name
12733
+ });
12734
+ session.waiting = null;
12735
+ }
12736
+ }
12737
+ if (name === "SessionEnd") {
12738
+ session.endedAt = event.received_at;
12739
+ } else if (name === "Stop") {
12740
+ session.stopAt = event.received_at;
12741
+ } else {
12742
+ session.stopAt = void 0;
12743
+ }
12744
+ session.lastEvent = { name, at: event.received_at };
12745
+ session.dirty = true;
12746
+ }
12747
+ function seedFixture(harness, rawLines, mockNote) {
12748
+ const lines = rawLines.filter((l) => l.trim() !== "").map((l) => parseTranscriptLine(l));
12749
+ const sessionLine = lines.find((l) => l.kind !== "unknown" && typeof l.sessionId === "string");
12750
+ const id = sessionLine?.sessionId ?? "__fixture__";
12751
+ const session = sessionOf(harness, id);
12752
+ session.lines = lines;
12753
+ session.mockNote = mockNote;
12754
+ for (const line of lines) {
12755
+ if (line.kind === "unknown")
12756
+ continue;
12757
+ const cwd = line.raw.cwd;
12758
+ if (typeof cwd === "string") {
12759
+ const base = pathBasename(cwd);
12760
+ if (base !== "")
12761
+ session.label = base;
12762
+ break;
12763
+ }
12764
+ }
12765
+ session.dirty = true;
12766
+ }
12767
+ function deriveCached(harness, session) {
12768
+ if (!session.dirty && session.cached !== void 0)
12769
+ return session.cached;
12770
+ session.cached = derive(harness, session, now().toISOString());
12771
+ session.dirty = false;
12772
+ return session.cached;
12773
+ }
12774
+ function getOverview(harness) {
12775
+ const nowIso = now().toISOString();
12776
+ const sessions = sessionsOf(harness);
12777
+ const infos = [...sessions.values()].map((session) => ({
12778
+ id: session.id,
12779
+ label: session.label ?? session.id.slice(0, 8),
12780
+ lastActivity: lastActivityOf(session),
12781
+ waiting: session.waiting !== null,
12782
+ ended: isEnded(session, nowIso)
12783
+ })).sort((a, b) => a.lastActivity < b.lastActivity ? 1 : a.lastActivity > b.lastActivity ? -1 : 0);
12784
+ const states = {};
12785
+ for (const [id, session] of sessions)
12786
+ states[id] = deriveCached(harness, session);
12787
+ return { harness, sessions: infos, states };
12788
+ }
12789
+ function getState(harness) {
12790
+ const overview = getOverview(harness);
12791
+ const first = overview.sessions[0];
12792
+ if (first !== void 0) {
12793
+ const state2 = overview.states[first.id];
12794
+ if (state2 !== void 0)
12795
+ return state2;
12796
+ }
12797
+ return emptyState(harness, now().toISOString());
12798
+ }
12799
+ function close() {
12800
+ closed = true;
12801
+ for (const sessions of harnesses.values()) {
12802
+ for (const session of sessions.values())
12803
+ closeSession(session);
12804
+ }
12805
+ }
12806
+ return { handleEvent, getState, getOverview, seedFixture, close };
12807
+ }
12808
+ function lastActivityOf(session) {
12809
+ if (session.lastActivity !== void 0)
12810
+ return session.lastActivity;
12811
+ let latest = "";
12812
+ for (const line of session.lines) {
12813
+ if (line.kind === "unknown")
12814
+ continue;
12815
+ const ts = line.timestamp;
12816
+ if (ts !== void 0 && ts > latest)
12817
+ latest = ts;
12818
+ }
12819
+ return latest;
12820
+ }
12821
+ function isEnded(session, nowIso) {
12822
+ if (session.endedAt !== void 0)
12823
+ return true;
12824
+ if (session.stopAt === void 0)
12825
+ return false;
12826
+ const last = Date.parse(lastActivityOf(session));
12827
+ const nowMs = Date.parse(nowIso);
12828
+ if (!Number.isFinite(last) || !Number.isFinite(nowMs))
12829
+ return false;
12830
+ return nowMs - last >= ENDED_AFTER_STOP_MS;
12831
+ }
12832
+ function emptyState(harness, nowIso) {
12833
+ return {
12834
+ harness,
12835
+ sessionId: null,
12836
+ transcriptPath: null,
12837
+ todos: [],
12838
+ traces: [],
12839
+ turnUsages: [],
12840
+ currentAction: null,
12841
+ waiting: null,
12842
+ agentQuote: null,
12843
+ lastPrompt: null,
12844
+ updatedAt: nowIso
12845
+ };
12846
+ }
12847
+ function pathBasename(path) {
12848
+ const parts = path.split(/[\\/]/).filter((p) => p !== "");
12849
+ return parts[parts.length - 1] ?? "";
12850
+ }
12851
+ function agentIdOf(payload) {
12852
+ const id = payload.agent_id ?? payload.agentId;
12853
+ return typeof id === "string" && id !== "" ? id : void 0;
12854
+ }
12855
+ function waitingMessage(payload, fallback) {
12856
+ if (typeof payload.message === "string" && payload.message !== "")
12857
+ return payload.message;
12858
+ if (typeof payload.tool_name === "string" && payload.tool_name !== "") {
12859
+ return `${fallback}:${payload.tool_name}`;
12860
+ }
12861
+ return fallback;
12862
+ }
12863
+ function derive(harness, session, nowIso) {
12864
+ const lines = sortedByTimestamp(session.lines);
12865
+ const timeline = deriveTodoTimeline(extractTodoSnapshots(lines));
12866
+ const allSteps = extractSteps(lines);
12867
+ const steps = allSteps.filter((s) => !TODO_TOOLS.has(s.toolName));
12868
+ const intervals = [...deriveSubagentIntervals(lines), ...session.hookIntervals];
12869
+ const attributed = attributeSteps(steps, timeline, intervals);
12870
+ const results = collectResults(lines);
12871
+ const buckets = /* @__PURE__ */ new Map();
12872
+ for (const { step, attribution } of attributed) {
12873
+ const key = attribution.kind === "todo" ? attribution.todoId : null;
12874
+ const bucket = buckets.get(key);
12875
+ if (bucket)
12876
+ bucket.push(step);
12877
+ else
12878
+ buckets.set(key, [step]);
12879
+ }
12880
+ const waitNodes = /* @__PURE__ */ new Map();
12881
+ const pushWait = (episode, open2) => {
12882
+ const key = todoInProgressAt(timeline, episode.since);
12883
+ const node = {
12884
+ phase: "wait",
12885
+ label: episode.kind === "permission" ? "\u7B49\u5F85\u6388\u6743" : "\u7B49\u5F85\u56DE\u5E94",
12886
+ steps: [],
12887
+ startTs: episode.since,
12888
+ failed: false,
12889
+ wait: { kind: episode.kind, message: episode.message }
12890
+ };
12891
+ if (!open2) {
12892
+ const closed = episode;
12893
+ node.endTs = closed.until;
12894
+ const ms = Date.parse(closed.until) - Date.parse(closed.since);
12895
+ if (Number.isFinite(ms) && ms >= 0)
12896
+ node.durationMs = ms;
12897
+ if (node.wait)
12898
+ node.wait.resolvedBy = closed.resolvedBy;
12899
+ }
12900
+ const list = waitNodes.get(key);
12901
+ if (list)
12902
+ list.push(node);
12903
+ else
12904
+ waitNodes.set(key, [node]);
12905
+ };
12906
+ for (const episode of session.waitEpisodes)
12907
+ pushWait(episode, false);
12908
+ if (session.waiting !== null)
12909
+ pushWait(session.waiting, true);
12910
+ const traces = [];
12911
+ const agentsByTodo = /* @__PURE__ */ new Map();
12912
+ const orderedKeys = timeline.map((entry) => entry.id).filter((id) => buckets.has(id) || waitNodes.has(id));
12913
+ if (buckets.has(null) || waitNodes.has(null))
12914
+ orderedKeys.push(null);
12915
+ for (const key of orderedKeys) {
12916
+ const bucketSteps = buckets.get(key) ?? [];
12917
+ const nodes = consolidatePhases(bucketSteps).map((node) => ({
12918
+ phase: node.phase,
12919
+ label: PHASE_LABELS[node.phase],
12920
+ steps: node.steps.map((step) => stepView(step, results)),
12921
+ ...node.startTs !== void 0 ? { startTs: node.startTs } : {},
12922
+ ...node.endTs !== void 0 ? { endTs: node.endTs } : {},
12923
+ ...node.durationMs !== void 0 ? { durationMs: node.durationMs } : {},
12924
+ failed: node.failed,
12925
+ ...nodeEstTokens(node.steps, results)
12926
+ }));
12927
+ for (const waitNode of waitNodes.get(key) ?? [])
12928
+ insertByStartTs(nodes, waitNode);
12929
+ traces.push({ todoId: key, nodes });
12930
+ if (key !== null) {
12931
+ const agents = /* @__PURE__ */ new Set();
12932
+ for (const step of bucketSteps)
12933
+ if (step.agentId !== void 0)
12934
+ agents.add(step.agentId);
12935
+ agentsByTodo.set(key, agents);
12936
+ }
12937
+ }
12938
+ const todos = timeline.map((entry) => {
12939
+ const last = entry.spans[entry.spans.length - 1];
12940
+ return {
12941
+ id: entry.id,
12942
+ content: entry.content,
12943
+ ...entry.activeForm !== void 0 ? { activeForm: entry.activeForm } : {},
12944
+ status: last?.status ?? "pending",
12945
+ agents: [...agentsByTodo.get(entry.id) ?? []]
12946
+ };
12947
+ });
12948
+ const textTodos = todos.length === 0 ? deriveTextTodos(lines) : void 0;
12949
+ return {
12950
+ harness,
12951
+ sessionId: session.id,
12952
+ transcriptPath: session.transcriptPath ?? null,
12953
+ ...session.mockNote !== void 0 ? { __mock_note: session.mockNote } : {},
12954
+ todos,
12955
+ ...textTodos !== void 0 && textTodos.length > 0 ? { textTodos } : {},
12956
+ traces,
12957
+ turnUsages: deriveTurnUsages(lines),
12958
+ currentAction: deriveCurrentAction(steps, session.lastEvent, session.pendingHookTool),
12959
+ waiting: session.waiting,
12960
+ agentQuote: deriveAgentQuote(lines),
12961
+ lastPrompt: deriveLastPrompt(lines),
12962
+ updatedAt: nowIso
12963
+ };
12964
+ }
12965
+ function todoInProgressAt(timeline, ts) {
12966
+ const active = timeline.filter((entry) => entry.spans.some((span) => span.status === "in_progress" && span.from !== void 0 && span.from <= ts && (span.to === void 0 || ts < span.to)));
12967
+ const only = active[0];
12968
+ return active.length === 1 && only !== void 0 ? only.id : null;
12969
+ }
12970
+ function insertByStartTs(nodes, node) {
12971
+ const ts = node.startTs;
12972
+ if (ts !== void 0) {
12973
+ for (let i = 0; i < nodes.length; i++) {
12974
+ const at = nodes[i]?.startTs;
12975
+ if (at !== void 0 && at > ts) {
12976
+ nodes.splice(i, 0, node);
12977
+ return;
12978
+ }
12979
+ }
12980
+ }
12981
+ nodes.push(node);
12982
+ }
12983
+ function deriveTextTodos(lines) {
12984
+ for (let i = lines.length - 1; i >= 0; i--) {
12985
+ const line = lines[i];
12986
+ if (line === void 0 || line.kind !== "assistant" || line.agentId !== void 0)
12987
+ continue;
12988
+ for (let j = line.blocks.length - 1; j >= 0; j--) {
12989
+ const block = line.blocks[j];
12990
+ if (block === void 0 || block.kind !== "text")
12991
+ continue;
12992
+ const items = parseTextChecklist(block.text);
12993
+ if (items.length > 0)
12994
+ return items;
12995
+ }
12996
+ }
12997
+ return void 0;
12998
+ }
12999
+ function sortedByTimestamp(lines) {
13000
+ return [...lines].sort((a, b) => {
13001
+ const ta = a.kind === "unknown" ? void 0 : a.timestamp;
13002
+ const tb = b.kind === "unknown" ? void 0 : b.timestamp;
13003
+ if (ta === void 0 || tb === void 0)
13004
+ return 0;
13005
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
13006
+ });
13007
+ }
13008
+ function collectResults(lines) {
13009
+ const results = /* @__PURE__ */ new Map();
13010
+ for (const line of lines) {
13011
+ if (line.kind !== "user")
13012
+ continue;
13013
+ for (const result of line.toolResults) {
13014
+ if (result.content === void 0)
13015
+ continue;
13016
+ const info = {};
13017
+ try {
13018
+ info.bytes = Buffer.byteLength(JSON.stringify(result.content), "utf8");
13019
+ } catch {
13020
+ }
13021
+ const brief = errorBrief(result.content);
13022
+ if (brief !== void 0)
13023
+ info.brief = brief;
13024
+ results.set(result.toolUseId, info);
13025
+ }
13026
+ }
13027
+ return results;
13028
+ }
13029
+ function stepView(step, results) {
13030
+ const view = {
13031
+ toolName: step.toolName,
13032
+ detail: stepDetail(step),
13033
+ isError: step.isError
13034
+ };
13035
+ if (step.ts !== void 0)
13036
+ view.ts = step.ts;
13037
+ if (step.endTs !== void 0)
13038
+ view.endTs = step.endTs;
13039
+ if (step.agentId !== void 0)
13040
+ view.agentId = step.agentId;
13041
+ if (step.isError) {
13042
+ const brief = results.get(step.toolUseId)?.brief;
13043
+ if (brief !== void 0)
13044
+ view.errorBrief = brief;
13045
+ }
13046
+ const est = stepEstTokens(step, results);
13047
+ if (est !== void 0)
13048
+ view.estTokens = est;
13049
+ return view;
13050
+ }
13051
+ function stepEstTokens(step, results) {
13052
+ let bytes = 0;
13053
+ try {
13054
+ bytes += Buffer.byteLength(JSON.stringify(step.toolInput), "utf8");
13055
+ } catch {
13056
+ }
13057
+ bytes += results.get(step.toolUseId)?.bytes ?? 0;
13058
+ if (bytes === 0)
13059
+ return void 0;
13060
+ return estimateTokens(bytes).value;
13061
+ }
13062
+ function nodeEstTokens(steps, results) {
13063
+ let sum = 0;
13064
+ let counted = false;
13065
+ for (const step of steps) {
13066
+ const est = stepEstTokens(step, results);
13067
+ if (est !== void 0) {
13068
+ sum += est;
13069
+ counted = true;
13070
+ }
13071
+ }
13072
+ return counted ? { estTokens: sum } : {};
13073
+ }
13074
+ var DETAIL_KEYS = ["command", "file_path", "description", "subject", "pattern", "prompt"];
13075
+ function inputDetail(input) {
13076
+ for (const key of DETAIL_KEYS) {
13077
+ const value = input[key];
13078
+ if (typeof value === "string" && value !== "")
13079
+ return truncate(value, DETAIL_LIMIT);
13080
+ }
13081
+ return "";
13082
+ }
13083
+ function stepDetail(step) {
13084
+ return inputDetail(step.toolInput);
13085
+ }
13086
+ function hookToolDetail(input) {
13087
+ if (typeof input !== "object" || input === null || Array.isArray(input))
13088
+ return "";
13089
+ return inputDetail(input);
13090
+ }
13091
+ function deriveTurnUsages(lines) {
13092
+ const byMessage = /* @__PURE__ */ new Map();
13093
+ let anonymous = 0;
13094
+ for (const line of lines) {
13095
+ if (line.kind !== "assistant")
13096
+ continue;
13097
+ const usage = turnUsage(line);
13098
+ if (usage === void 0)
13099
+ continue;
13100
+ const key = line.messageId ?? `__anon_${anonymous++}`;
13101
+ const view = {
13102
+ ...line.messageId !== void 0 ? { messageId: line.messageId } : {},
13103
+ ...line.model !== void 0 ? { model: line.model } : {},
13104
+ inputTokens: usage.inputTokens,
13105
+ cacheCreationInputTokens: usage.cacheCreationInputTokens,
13106
+ cacheReadInputTokens: usage.cacheReadInputTokens,
13107
+ outputTokens: usage.outputTokens,
13108
+ contextTotal: usage.contextTotal,
13109
+ ...line.timestamp !== void 0 ? { ts: line.timestamp } : {}
13110
+ };
13111
+ if (line.model !== void 0) {
13112
+ const cost = costUsd(usage, line.model);
13113
+ if (cost !== void 0)
13114
+ view.costUsd = cost;
13115
+ }
13116
+ const existing = byMessage.get(key);
13117
+ if (existing?.ts !== void 0)
13118
+ view.ts = existing.ts;
13119
+ byMessage.set(key, view);
13120
+ }
13121
+ return [...byMessage.values()];
13122
+ }
13123
+ function deriveCurrentAction(steps, lastEvent, pendingHookTool) {
13124
+ const open2 = [...steps].reverse().find((s) => s.endTs === void 0);
13125
+ if (open2) {
13126
+ const action = { kind: "tool", toolName: open2.toolName, detail: stepDetail(open2) };
13127
+ if (open2.ts !== void 0)
13128
+ action.since = open2.ts;
13129
+ return action;
13130
+ }
13131
+ if (pendingHookTool) {
13132
+ return {
13133
+ kind: "tool",
13134
+ toolName: pendingHookTool.toolName,
13135
+ detail: pendingHookTool.detail,
13136
+ since: pendingHookTool.since
13137
+ };
13138
+ }
13139
+ if (lastEvent)
13140
+ return { kind: "event", name: lastEvent.name, at: lastEvent.at };
13141
+ return null;
13142
+ }
13143
+ function deriveAgentQuote(lines) {
13144
+ for (let i = lines.length - 1; i >= 0; i--) {
13145
+ const line = lines[i];
13146
+ if (line === void 0 || line.kind !== "assistant" || line.agentId !== void 0)
13147
+ continue;
13148
+ for (let j = line.blocks.length - 1; j >= 0; j--) {
13149
+ const block = line.blocks[j];
13150
+ if (block !== void 0 && block.kind === "text" && block.text.trim() !== "") {
13151
+ return truncate(block.text.trim(), QUOTE_LIMIT);
13152
+ }
13153
+ }
13154
+ }
13155
+ return null;
13156
+ }
13157
+ function deriveLastPrompt(lines) {
13158
+ for (let i = lines.length - 1; i >= 0; i--) {
13159
+ const line = lines[i];
13160
+ if (line === void 0 || line.kind !== "user" || line.agentId !== void 0)
13161
+ continue;
13162
+ if (line.toolResults.length > 0)
13163
+ continue;
13164
+ if (line.text === void 0 || line.text.trim() === "")
13165
+ continue;
13166
+ return truncate(line.text.trim(), DETAIL_LIMIT);
13167
+ }
13168
+ return null;
13169
+ }
13170
+ function truncate(text, limit) {
13171
+ return text.length <= limit ? text : `${text.slice(0, limit - 1)}\u2026`;
13172
+ }
13173
+ function errorBrief(content) {
13174
+ if (typeof content === "string") {
13175
+ const first = content.split("\n", 1)[0]?.trim();
13176
+ return first ? truncate(first, ERROR_BRIEF_LIMIT) : void 0;
13177
+ }
13178
+ if (Array.isArray(content)) {
13179
+ for (const block of content) {
13180
+ if (typeof block === "object" && block !== null && block.type === "text" && typeof block.text === "string") {
13181
+ return errorBrief(block.text);
13182
+ }
13183
+ }
13184
+ }
13185
+ return void 0;
11929
13186
  }
11930
13187
 
11931
13188
  // apps/cli/dist/registry.js
13189
+ import { existsSync as existsSync10 } from "node:fs";
11932
13190
  async function importFactory(harness) {
11933
13191
  const mod = await importAdapterModule(harness);
11934
13192
  if (typeof mod.createAdapter !== "function") {
@@ -11947,7 +13205,7 @@ function importAdapterModule(harness) {
11947
13205
  }
11948
13206
  }
11949
13207
  function fixtureScanOptions(harness) {
11950
- if (!existsSync9(fixturePath())) {
13208
+ if (!existsSync10(fixturePath())) {
11951
13209
  throw new Error("--fixtures \u9700\u8981\u4ED3\u5E93\u5185\u7684 packages/fixtures \u6570\u636E;\u53D1\u5E03\u5B89\u88C5\u5305/npx \u4EA7\u7269\u4E0D\u5305\u542B\u6837\u4F8B");
11952
13210
  }
11953
13211
  switch (harness) {
@@ -11956,7 +13214,7 @@ function fixtureScanOptions(harness) {
11956
13214
  case "codex":
11957
13215
  return { homeDir: fixturePath("codex", "home") };
11958
13216
  case "dsh":
11959
- return {};
13217
+ return { homeDir: fixturePath("dsh", "home") };
11960
13218
  }
11961
13219
  }
11962
13220
  async function loadHarness(harness, mode) {
@@ -11971,6 +13229,94 @@ async function loadHarness(harness, mode) {
11971
13229
 
11972
13230
  // apps/cli/dist/scan.js
11973
13231
  init_dist();
13232
+
13233
+ // apps/cli/dist/telemetry.js
13234
+ import { readFileSync as readFileSync11 } from "node:fs";
13235
+ import { dirname as dirname11, join as join17 } from "node:path";
13236
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
13237
+ var DEFAULT_TELEMETRY_SERVER = "https://api.getwingman.dev";
13238
+ var SEND_INTERVAL_MS = 24 * 60 * 60 * 1e3;
13239
+ var SEND_TIMEOUT_MS = 3e3;
13240
+ var MAX_ERROR_TYPES = 10;
13241
+ function emptyState2() {
13242
+ return {
13243
+ scans: 0,
13244
+ toggleApplies: 0,
13245
+ detected: { cc: false, codex: false, dsh: false },
13246
+ liveWired: { cc: false, codex: false, dsh: false },
13247
+ errorTypes: /* @__PURE__ */ new Set()
13248
+ };
13249
+ }
13250
+ var state = emptyState2();
13251
+ function recordDetect(harness, installed) {
13252
+ state.detected[harness] = installed;
13253
+ }
13254
+ function recordScan(config) {
13255
+ state.scans += 1;
13256
+ state.detected[config.harness] = true;
13257
+ state.liveWired[config.harness] = config.capabilities.live !== "none";
13258
+ }
13259
+ function recordHookInstall(harness) {
13260
+ state.liveWired[harness] = true;
13261
+ }
13262
+ function recordApply() {
13263
+ state.toggleApplies += 1;
13264
+ }
13265
+ function recordError(error) {
13266
+ if (state.errorTypes.size >= MAX_ERROR_TYPES)
13267
+ return;
13268
+ state.errorTypes.add(error instanceof Error ? error.constructor.name : typeof error);
13269
+ }
13270
+ function buildPayload(options) {
13271
+ return {
13272
+ app_version: options.version,
13273
+ platform: process.platform,
13274
+ arch: process.arch,
13275
+ harnesses: { ...state.detected },
13276
+ counts: {
13277
+ scans: state.scans,
13278
+ live_wired: Object.values(state.liveWired).filter(Boolean).length,
13279
+ toggle_applies: state.toggleApplies
13280
+ },
13281
+ license_state: options.licenseState,
13282
+ error_types: [...state.errorTypes].sort()
13283
+ };
13284
+ }
13285
+ function appVersion() {
13286
+ if (typeof __WINGMAN_VERSION__ === "string")
13287
+ return __WINGMAN_VERSION__;
13288
+ try {
13289
+ const pkgPath = join17(dirname11(fileURLToPath2(import.meta.url)), "..", "package.json");
13290
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
13291
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
13292
+ } catch {
13293
+ return "unknown";
13294
+ }
13295
+ }
13296
+ async function sendMetrics(payload, baseUrl) {
13297
+ const base = baseUrl ?? process.env.WINGMAN_TELEMETRY_SERVER ?? DEFAULT_TELEMETRY_SERVER;
13298
+ try {
13299
+ await fetch(`${base}/metrics`, {
13300
+ method: "POST",
13301
+ headers: { "content-type": "application/json" },
13302
+ body: JSON.stringify(payload),
13303
+ signal: AbortSignal.timeout(SEND_TIMEOUT_MS)
13304
+ });
13305
+ } catch {
13306
+ }
13307
+ }
13308
+ function startTelemetry(options = {}) {
13309
+ const send = () => {
13310
+ const licenseState = licenseStatus(options.homeDir === void 0 ? {} : { homeDir: options.homeDir }).state;
13311
+ void sendMetrics(buildPayload({ version: appVersion(), licenseState }), options.baseUrl);
13312
+ };
13313
+ send();
13314
+ const timer = setInterval(send, options.intervalMs ?? SEND_INTERVAL_MS);
13315
+ timer.unref();
13316
+ return () => clearInterval(timer);
13317
+ }
13318
+
13319
+ // apps/cli/dist/scan.js
11974
13320
  async function scanValidated(adapter, options) {
11975
13321
  const config = await adapter.scan(options);
11976
13322
  const errors = validateUnifiedConfig(config);
@@ -11978,6 +13324,7 @@ async function scanValidated(adapter, options) {
11978
13324
  throw new Error(`${adapter.harness} \u7684 scan \u8F93\u51FA\u4E0D\u7B26\u5408 UnifiedConfig \u5408\u540C:
11979
13325
  - ${errors.join("\n- ")}`);
11980
13326
  }
13327
+ recordScan(config);
11981
13328
  return config;
11982
13329
  }
11983
13330
 
@@ -11985,7 +13332,152 @@ async function scanValidated(adapter, options) {
11985
13332
  init_dist();
11986
13333
  import { readFile as readFile3 } from "node:fs/promises";
11987
13334
  import { createServer } from "node:http";
11988
- import { extname, resolve as resolve4, sep as sep2 } from "node:path";
13335
+ import { extname, resolve as resolve5, sep as sep2 } from "node:path";
13336
+
13337
+ // apps/cli/dist/suggest.js
13338
+ init_dist();
13339
+ var WAITING_TIMEOUT_MS = 6e4;
13340
+ var HISTORY_BLOAT_RATIO = 0.5;
13341
+ var HISTORY_BLOAT_MIN_CONTEXT = 5e4;
13342
+ function mcpServerName(nativeName) {
13343
+ for (const prefix of ["mcpServers.", "mcp_servers."]) {
13344
+ if (nativeName.startsWith(prefix) && nativeName.length > prefix.length) {
13345
+ return nativeName.slice(prefix.length);
13346
+ }
13347
+ }
13348
+ return void 0;
13349
+ }
13350
+ function displayName(item) {
13351
+ return mcpServerName(item.nativeName) ?? item.nativeName;
13352
+ }
13353
+ function fmtTok(value) {
13354
+ if (value < 1e3)
13355
+ return String(value);
13356
+ return `${(Math.round(value / 100) / 10).toFixed(1)}k`;
13357
+ }
13358
+ function fmtTokens(tokens) {
13359
+ const n = fmtTok(tokens.value);
13360
+ return tokens.kind === "estimate" ? `\u7EA6 ${n}` : n;
13361
+ }
13362
+ function fmtDuration(ms) {
13363
+ const seconds = Math.round(ms / 1e3);
13364
+ if (seconds < 60)
13365
+ return `${seconds}s`;
13366
+ const minutes = Math.floor(seconds / 60);
13367
+ return `${minutes}m${seconds % 60 === 0 ? "" : ` ${seconds % 60}s`}`;
13368
+ }
13369
+ function residentTotal(config) {
13370
+ let sum = 0;
13371
+ let counted = false;
13372
+ let kind = "exact";
13373
+ for (const items of Object.values(config.slots)) {
13374
+ for (const item of items) {
13375
+ if (!item.enabled || !item.tokens)
13376
+ continue;
13377
+ sum += item.tokens.value;
13378
+ counted = true;
13379
+ if (item.tokens.kind === "estimate")
13380
+ kind = "estimate";
13381
+ }
13382
+ }
13383
+ return counted ? { kind, value: sum } : void 0;
13384
+ }
13385
+ function stepToolNames(state2) {
13386
+ const names = [];
13387
+ for (const trace of state2.traces) {
13388
+ for (const node of trace.nodes) {
13389
+ for (const step of node.steps)
13390
+ names.push(step.toolName);
13391
+ }
13392
+ }
13393
+ return names;
13394
+ }
13395
+ function deriveSuggestions(state2, config, nowMs) {
13396
+ const suggestions = [];
13397
+ if (state2.waiting !== null) {
13398
+ const sinceMs = Date.parse(state2.waiting.since);
13399
+ if (Number.isFinite(sinceMs) && nowMs - sinceMs > WAITING_TIMEOUT_MS) {
13400
+ suggestions.push({
13401
+ id: "waiting-timeout",
13402
+ rule: "waiting-timeout",
13403
+ text: `\u5DF2\u7B49\u5F85 ${fmtDuration(nowMs - sinceMs)}:${state2.waiting.message}`,
13404
+ source: `\u6765\u6E90:${state2.waiting.kind === "permission" ? "PermissionRequest" : "Notification"} \u4E8B\u4EF6,${state2.waiting.since} \u8D77\u672A\u51B3;\u9608\u503C 60s`
13405
+ });
13406
+ }
13407
+ }
13408
+ if (config !== void 0) {
13409
+ const refSources = [
13410
+ ...state2.lastPrompt !== null ? [{ file: "\u7528\u6237\u63D0\u793A", text: state2.lastPrompt }] : [],
13411
+ ...state2.agentQuote !== null ? [{ file: "agent \u81EA\u8FF0", text: state2.agentQuote }] : []
13412
+ ];
13413
+ if (refSources.length > 0) {
13414
+ for (const items of Object.values(config.slots)) {
13415
+ for (const item of items) {
13416
+ if (item.enabled || !item.toggleable)
13417
+ continue;
13418
+ const name = displayName(item);
13419
+ const hits = scanReferences(refSources, name);
13420
+ const hit = hits[0];
13421
+ if (hit === void 0)
13422
+ continue;
13423
+ suggestions.push({
13424
+ id: `reenable:${item.id}`,
13425
+ rule: "reenable",
13426
+ text: `\u68C0\u6D4B\u5230\u63D0\u53CA ${name}(\u5DF2\u5173),\u53EF\u4E00\u952E\u6253\u5F00`,
13427
+ source: `\u547D\u4E2D:${hit.file} \u2014\u2014 \u300C${hit.context}\u300D(scanReferences \u6574\u8BCD\u5339\u914D)`,
13428
+ action: { itemId: item.id, enable: true }
13429
+ });
13430
+ }
13431
+ }
13432
+ }
13433
+ const toolNames = stepToolNames(state2);
13434
+ if (toolNames.length > 0 && state2.turnUsages.length > 0) {
13435
+ for (const item of config.slots.tools) {
13436
+ if (!item.enabled || !item.toggleable)
13437
+ continue;
13438
+ const server = mcpServerName(item.nativeName);
13439
+ if (server === void 0)
13440
+ continue;
13441
+ const used = toolNames.some((n) => n.startsWith(`mcp__${server}__`));
13442
+ if (used)
13443
+ continue;
13444
+ const saving = item.tokens ? `,\u5173\u6389\u53EF\u7701 ${fmtTokens(item.tokens)} tok(\u5E38\u9A7B\u4F30\u7B97)` : ",\u53EF\u5173";
13445
+ suggestions.push({
13446
+ id: `idle-mcp:${item.id}`,
13447
+ rule: "idle-mcp",
13448
+ text: `${server} \u672C session \u672A\u4F7F\u7528${saving}`,
13449
+ source: [
13450
+ `\u5224\u5B9A:\u672C session \u5DE5\u5177\u6B65\u9AA4\u96F6\u547D\u4E2D mcp__${server}__*(transcript \u89C2\u5BDF)`,
13451
+ `\u6761\u76EE:${item.nativePath !== "" ? item.nativePath : item.id}`,
13452
+ item.tokens ? "\u7701\u989D = \u6761\u76EE\u5E38\u9A7B token(\u4F30\u7B97\u6807\u300C\u7EA6\u300D);\u5173\u505C\u8D70 plan/apply,\u5199\u524D\u5907\u4EFD\u53EF\u56DE\u9000" : "\u8BE5 harness \u672A\u63D0\u4F9B\u6B64\u6761\u76EE\u7684\u5E38\u9A7B\u5360\u7528\u6570,\u4E0D\u7F16\u7701\u989D;\u5173\u505C\u8D70 plan/apply,\u5199\u524D\u5907\u4EFD\u53EF\u56DE\u9000"
13453
+ ].join("\n"),
13454
+ action: { itemId: item.id, enable: false }
13455
+ });
13456
+ }
13457
+ }
13458
+ const lastTurn = state2.turnUsages[state2.turnUsages.length - 1];
13459
+ const resident = residentTotal(config);
13460
+ if (lastTurn !== void 0 && resident !== void 0) {
13461
+ const ctx = lastTurn.contextTotal.value;
13462
+ const history = ctx - resident.value;
13463
+ if (ctx >= HISTORY_BLOAT_MIN_CONTEXT && history / ctx > HISTORY_BLOAT_RATIO) {
13464
+ const pct = Math.round(history / ctx * 100);
13465
+ suggestions.push({
13466
+ id: "history-bloat",
13467
+ rule: "history-bloat",
13468
+ text: `\u5BF9\u8BDD\u5386\u53F2\u7EA6\u5360\u672C\u8F6E context ${pct}%(\u7EA6 ${fmtTok(history)} / ${fmtTok(ctx)}),\u5F00\u65B0 session \u53EF\u590D\u4F4D`,
13469
+ source: [
13470
+ "\u4F30\u7B97 = \u672C\u8F6E usage \u5408\u8BA1(harness \u81EA\u62A5,\u7CBE\u786E)\u2212 \u914D\u7F6E\u5E38\u9A7B\u5408\u8BA1(\u4F30\u7B97,\u6807\u300C\u7EA6\u300D)",
13471
+ `\u9608\u503C:context \u2265 ${fmtTok(HISTORY_BLOAT_MIN_CONTEXT)} \u4E14\u5386\u53F2\u5360\u6BD4 > 50%`
13472
+ ].join("\n")
13473
+ });
13474
+ }
13475
+ }
13476
+ }
13477
+ return suggestions;
13478
+ }
13479
+
13480
+ // apps/cli/dist/server.js
11989
13481
  var CONTENT_TYPES = {
11990
13482
  ".html": "text/html; charset=utf-8",
11991
13483
  ".js": "text/javascript; charset=utf-8",
@@ -11997,10 +13489,13 @@ var CONTENT_TYPES = {
11997
13489
  ".ico": "image/x-icon",
11998
13490
  ".woff2": "font/woff2"
11999
13491
  };
13492
+ var ANATOMY_CACHE_TTL_MS = 3e4;
12000
13493
  function createWingmanServer(options) {
12001
13494
  const warn = options.warn ?? ((message) => console.error(message));
13495
+ const runtime = { anatomy: null };
12002
13496
  return createServer((req, res) => {
12003
- handle(req, res, options, warn).catch((error) => {
13497
+ handle(req, res, options, warn, runtime).catch((error) => {
13498
+ recordError(error);
12004
13499
  warn(`[wingman] \u8BF7\u6C42\u5904\u7406\u5931\u8D25:${errorMessage(error)}`);
12005
13500
  if (!res.headersSent)
12006
13501
  sendText2(res, 500, "internal error");
@@ -12018,7 +13513,7 @@ function listen(server, port) {
12018
13513
  });
12019
13514
  });
12020
13515
  }
12021
- async function handle(req, res, options, warn) {
13516
+ async function handle(req, res, options, warn, runtime) {
12022
13517
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
12023
13518
  if (options.receiver && url.pathname === "/hook") {
12024
13519
  options.receiver.handleHook(req, res);
@@ -12033,7 +13528,7 @@ async function handle(req, res, options, warn) {
12033
13528
  return;
12034
13529
  }
12035
13530
  if (url.pathname === "/api/action/apply" && req.method === "POST") {
12036
- await handleActionApply(req, res, options);
13531
+ await handleActionApply(req, res, options, runtime);
12037
13532
  return;
12038
13533
  }
12039
13534
  if (url.pathname === "/api/license/install" && req.method === "POST") {
@@ -12045,8 +13540,34 @@ async function handle(req, res, options, warn) {
12045
13540
  return;
12046
13541
  }
12047
13542
  if (url.pathname === "/api/license") {
12048
- const { state, detail } = licenseStatus(options.licenseOptions);
12049
- sendJson(res, 200, { state, detail });
13543
+ const { state: state2, detail } = licenseStatus(options.licenseOptions);
13544
+ sendJson(res, 200, { state: state2, detail });
13545
+ return;
13546
+ }
13547
+ if (url.pathname === "/api/narrative") {
13548
+ if (!options.narrative) {
13549
+ sendText2(res, 404, "not found");
13550
+ return;
13551
+ }
13552
+ const harness = url.searchParams.get("harness");
13553
+ if (typeof harness !== "string" || !HARNESS_IDS.includes(harness)) {
13554
+ sendJson(res, 400, {
13555
+ error: `harness \u9700\u8981 ${HARNESS_IDS.join("|")}(?harness=<id>),\u5F97\u5230 ${String(harness)}`
13556
+ });
13557
+ return;
13558
+ }
13559
+ const overview = options.narrative.getOverview(harness);
13560
+ if (overview.sessions.length > 0) {
13561
+ const config = (await cachedAnatomy(options, warn, runtime)).find((c) => c.harness === harness);
13562
+ const nowMs = Date.now();
13563
+ const states = {};
13564
+ for (const [id, state2] of Object.entries(overview.states)) {
13565
+ const suggestions = deriveSuggestions(state2, config, nowMs);
13566
+ states[id] = suggestions.length > 0 ? { ...state2, suggestions } : state2;
13567
+ }
13568
+ overview.states = states;
13569
+ }
13570
+ sendJson(res, 200, overview);
12050
13571
  return;
12051
13572
  }
12052
13573
  if (url.pathname === "/api/anatomy") {
@@ -12061,20 +13582,75 @@ async function handle(req, res, options, warn) {
12061
13582
  }
12062
13583
  await serveStatic(url.pathname, res, options.panelDistDir);
12063
13584
  }
13585
+ async function cachedAnatomy(options, warn, runtime) {
13586
+ const nowMs = Date.now();
13587
+ if (runtime.anatomy !== null && nowMs - runtime.anatomy.at < ANATOMY_CACHE_TTL_MS) {
13588
+ return runtime.anatomy.configs;
13589
+ }
13590
+ const configs = await scanAll(options.harnesses, warn);
13591
+ runtime.anatomy = { at: nowMs, configs };
13592
+ return configs;
13593
+ }
12064
13594
  async function scanAll(harnesses, warn) {
12065
13595
  const configs = [];
12066
13596
  for (const { harness, adapter, scanOptions } of harnesses) {
13597
+ let installed;
12067
13598
  try {
12068
13599
  const detected = await adapter.detect(scanOptions);
12069
- if (!detected.installed)
12070
- continue;
13600
+ recordDetect(harness, detected.installed);
13601
+ installed = detected.installed;
13602
+ } catch (error) {
13603
+ recordError(error);
13604
+ warn(`[wingman] ${harness} detect \u5931\u8D25,\u5DF2\u8DF3\u8FC7:${errorMessage(error)}`);
13605
+ continue;
13606
+ }
13607
+ if (!installed)
13608
+ continue;
13609
+ try {
12071
13610
  configs.push(await scanValidated(adapter, scanOptions));
12072
13611
  } catch (error) {
12073
- warn(`[wingman] ${harness} \u626B\u63CF\u5931\u8D25,\u5DF2\u8DF3\u8FC7:${errorMessage(error)}`);
13612
+ recordError(error);
13613
+ warn(`[wingman] ${harness} \u626B\u63CF\u5931\u8D25,\u964D\u7EA7\u663E\u793A:${errorMessage(error)}`);
13614
+ configs.push(degradedScanConfig(harness, error));
12074
13615
  }
12075
13616
  }
12076
13617
  return configs;
12077
13618
  }
13619
+ var DEGRADED_SOURCE = {
13620
+ cc: "bundled",
13621
+ codex: "user",
13622
+ dsh: "bundle"
13623
+ };
13624
+ function degradedScanConfig(harness, error) {
13625
+ return {
13626
+ harness,
13627
+ scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
13628
+ slots: {
13629
+ base: [
13630
+ {
13631
+ id: `${harness}:base:${DEGRADED_SOURCE[harness]}:scan-failed`,
13632
+ slot: "base",
13633
+ nativeName: `\u8BFB\u53D6\u914D\u7F6E\u5931\u8D25:${errorSummary(error)}`,
13634
+ nativePath: "",
13635
+ toggleable: false,
13636
+ enabled: true,
13637
+ source: DEGRADED_SOURCE[harness]
13638
+ }
13639
+ ],
13640
+ rules: [],
13641
+ skills: [],
13642
+ tools: [],
13643
+ gates: [],
13644
+ history: []
13645
+ },
13646
+ capabilities: { live: "none", actions: [] }
13647
+ };
13648
+ }
13649
+ function errorSummary(error) {
13650
+ const first = errorMessage(error).split("\n", 1)[0] ?? "";
13651
+ const scrubbed = first.replace(/(?:[A-Za-z]:)?[\\/][^\s'"()|,;]+/g, "\u2026").trim();
13652
+ return scrubbed.length <= 120 ? scrubbed : `${scrubbed.slice(0, 119)}\u2026`;
13653
+ }
12078
13654
  async function handleActionPlan(req, res, options) {
12079
13655
  const body = await readJsonBody(req);
12080
13656
  if (body === void 0) {
@@ -12098,7 +13674,7 @@ async function handleActionPlan(req, res, options) {
12098
13674
  sendJson(res, 400, { error: errorMessage(error) });
12099
13675
  }
12100
13676
  }
12101
- async function handleActionApply(req, res, options) {
13677
+ async function handleActionApply(req, res, options, runtime) {
12102
13678
  if (options.readonlyActions) {
12103
13679
  sendJson(res, 403, { error: "fixtures \u6A21\u5F0F\u53EA\u8BFB:\u52A8\u4F5C\u53EA\u5230 plan \u4E3A\u6B62,\u4E0D\u5199\u4EFB\u4F55\u6587\u4EF6" });
12104
13680
  return;
@@ -12124,8 +13700,13 @@ async function handleActionApply(req, res, options) {
12124
13700
  }
12125
13701
  try {
12126
13702
  const result = await loaded.adapter.applyAction(plan, loaded.scanOptions);
13703
+ if (result.ok) {
13704
+ recordApply();
13705
+ runtime.anatomy = null;
13706
+ }
12127
13707
  sendJson(res, 200, result);
12128
13708
  } catch (error) {
13709
+ recordError(error);
12129
13710
  sendJson(res, 400, { error: errorMessage(error) });
12130
13711
  }
12131
13712
  }
@@ -12208,8 +13789,8 @@ async function serveStatic(pathname, res, panelDistDir) {
12208
13789
  return;
12209
13790
  }
12210
13791
  const rel = decoded === "/" ? "/index.html" : decoded;
12211
- const distDir = resolve4(panelDistDir);
12212
- const filePath = resolve4(distDir, `.${rel}`);
13792
+ const distDir = resolve5(panelDistDir);
13793
+ const filePath = resolve5(distDir, `.${rel}`);
12213
13794
  if (filePath !== distDir && !filePath.startsWith(distDir + sep2)) {
12214
13795
  sendText2(res, 403, "forbidden");
12215
13796
  return;
@@ -12326,17 +13907,30 @@ async function runServe(args) {
12326
13907
  `);
12327
13908
  }
12328
13909
  }
12329
- const panelDistDir = panelDist === void 0 ? resolve5(dirname12(fileURLToPath2(import.meta.url)), "../../panel/dist") : resolve5(panelDist);
13910
+ const panelDistDir = panelDist === void 0 ? resolve6(dirname12(fileURLToPath3(import.meta.url)), "../../panel/dist") : resolve6(panelDist);
13911
+ const narrative = createNarrativeService({ followTranscripts: !fixtures });
13912
+ if (fixtures) {
13913
+ try {
13914
+ const raw = readFileSync12(fixturePath("cc", "sessions", "session-todos.jsonl"), "utf8");
13915
+ narrative.seedFixture("cc", raw.split("\n"), "MOCK \u2014 fixtures \u6F14\u793A:\u53D9\u4E8B\u6570\u636E\u6765\u81EA packages/fixtures \u7684 session-todos.jsonl \u6837\u4F8B\u4F1A\u8BDD,\u975E\u672C\u673A\u771F\u5B9E transcript\u3002");
13916
+ } catch (error) {
13917
+ const message = error instanceof Error ? error.message : String(error);
13918
+ process.stderr.write(`[wingman] fixtures \u53D9\u4E8B\u6837\u4F8B\u52A0\u8F7D\u5931\u8D25,\u5DF2\u8DF3\u8FC7:${message}
13919
+ `);
13920
+ }
13921
+ }
12330
13922
  const server = createWingmanServer({
12331
13923
  harnesses,
12332
13924
  panelDistDir,
12333
- receiver: createReceiver(),
12334
- readonlyActions: fixtures
13925
+ receiver: createReceiver({ onEvent: narrative.handleEvent }),
13926
+ readonlyActions: fixtures,
13927
+ narrative
12335
13928
  });
12336
13929
  const actualPort = await listen(server, port);
13930
+ startTelemetry();
12337
13931
  const mode = fixtures ? "(fixtures \u6A21\u5F0F:\u6570\u636E\u6765\u81EA @wingman/fixtures \u6837\u4F8B,\u975E\u672C\u673A\u914D\u7F6E)" : "";
12338
13932
  process.stdout.write(`Wingman \u9762\u677F:http://127.0.0.1:${actualPort}${mode}
12339
- \u53EA\u7ED1 127.0.0.1,\u6570\u636E\u4E0D\u51FA\u672C\u673A\u3002Ctrl+C \u9000\u51FA\u3002
13933
+ \u53EA\u7ED1 127.0.0.1,\u9762\u677F\u4E0E\u6570\u636E\u90FD\u5728\u672C\u673A;\u533F\u540D\u4F7F\u7528\u7EDF\u8BA1\u4E0D\u542B\u5185\u5BB9\u4E0E\u8DEF\u5F84,\u660E\u7EC6 getwingman.dev/data\u3002Ctrl+C \u9000\u51FA\u3002
12340
13934
  `);
12341
13935
  return 0;
12342
13936
  }
@@ -12364,6 +13958,7 @@ async function runHook(args) {
12364
13958
  const { adapter } = await loadHarness(harness, { fixtures: false });
12365
13959
  if (action === "install") {
12366
13960
  await adapter.installHooks(port);
13961
+ recordHookInstall(harness);
12367
13962
  process.stdout.write(`wingman:${harness} \u5B9E\u51B5\u63A5\u7EBF\u5B8C\u6210(POST http://127.0.0.1:${port}/hook?harness=${harness})\u3002
12368
13963
  \u5199\u524D\u5DF2\u5907\u4EFD;\u91CD\u590D\u6267\u884C\u5E42\u7B49;\u89E3\u7EBF:wingman hook uninstall ${harness}
12369
13964
  `);