harness-wingman 0.4.0 → 0.4.2

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.
@@ -48,6 +48,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
48
48
  mod
49
49
  ));
50
50
 
51
+ // packages/core/dist/feedback-url.js
52
+ var init_feedback_url = __esm({
53
+ "packages/core/dist/feedback-url.js"() {
54
+ "use strict";
55
+ }
56
+ });
57
+
51
58
  // packages/core/dist/refscan.js
52
59
  function scanReferences(sources, name) {
53
60
  if (name.length === 0)
@@ -228,6 +235,7 @@ var SLOT_IDS, HARNESS_IDS, ACTION_KINDS, SOURCES_BY_HARNESS, CONFIG_KEYS, ITEM_K
228
235
  var init_dist = __esm({
229
236
  "packages/core/dist/index.js"() {
230
237
  "use strict";
238
+ init_feedback_url();
231
239
  init_refscan();
232
240
  SLOT_IDS = ["base", "rules", "skills", "tools", "gates", "history"];
233
241
  HARNESS_IDS = ["cc", "codex", "dsh"];
@@ -1185,10 +1193,10 @@ function readState(homeDir) {
1185
1193
  }
1186
1194
  return rec;
1187
1195
  }
1188
- function writeState(homeDir, state2) {
1196
+ function writeState(homeDir, state) {
1189
1197
  const file = statePath(homeDir);
1190
1198
  mkdirSync3(dirname5(file), { recursive: true });
1191
- writeFileSync4(file, `${JSON.stringify(state2, null, 2)}
1199
+ writeFileSync4(file, `${JSON.stringify(state, null, 2)}
1192
1200
  `);
1193
1201
  }
1194
1202
  function makeBackupDir(homeDir) {
@@ -1347,7 +1355,7 @@ async function installHooksCc(port, options) {
1347
1355
  async function uninstallHooksCc(options) {
1348
1356
  const homeDir = options?.homeDir ?? homedir6();
1349
1357
  const target = targetPath(homeDir);
1350
- const state2 = readState(homeDir);
1358
+ const state = readState(homeDir);
1351
1359
  if (!existsSync5(target)) {
1352
1360
  rmSync2(statePath(homeDir), { force: true });
1353
1361
  return;
@@ -1361,14 +1369,14 @@ async function uninstallHooksCc(options) {
1361
1369
  const backupDir = makeBackupDir(homeDir);
1362
1370
  if (bytes !== null)
1363
1371
  writeFileSync4(join9(backupDir, "settings.json"), bytes);
1364
- if (state2 && state2.original !== null) {
1365
- const originalParsed = asRecord3(JSON.parse(state2.original));
1372
+ if (state && state.original !== null) {
1373
+ const originalParsed = asRecord3(JSON.parse(state.original));
1366
1374
  if (originalParsed && deepEqual2(normalizeForCompare(settings), normalizeForCompare(originalParsed))) {
1367
- writeFileSync4(target, state2.original);
1375
+ writeFileSync4(target, state.original);
1368
1376
  rmSync2(statePath(homeDir), { force: true });
1369
1377
  return;
1370
1378
  }
1371
- } else if (state2 && state2.original === null) {
1379
+ } else if (state && state.original === null) {
1372
1380
  if (deepEqual2(normalizeForCompare(settings), {})) {
1373
1381
  rmSync2(target, { force: true });
1374
1382
  rmSync2(statePath(homeDir), { force: true });
@@ -1631,33 +1639,33 @@ function parseString(ctx) {
1631
1639
  }
1632
1640
  let parsed = "";
1633
1641
  let sliceStart = ctx.p;
1634
- let state2 = 0;
1642
+ let state = 0;
1635
1643
  for (; ctx.p < ctx.s.length; ctx.p++) {
1636
1644
  c = ctx.s.charCodeAt(ctx.p);
1637
1645
  if (isMultiline && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)) {
1638
- state2 = state2 && 3;
1646
+ state = state && 3;
1639
1647
  } else if (c < 32 && c !== 9 || c === 127) {
1640
1648
  throw new TomlError("control characters are not allowed in strings", {
1641
1649
  toml: ctx.s,
1642
1650
  ptr: ctx.p
1643
1651
  });
1644
- } else if ((!state2 || state2 === 3) && c === first && (!isMultiline || ctx.s.charCodeAt(ctx.p + 1) === first && ctx.s.charCodeAt(ctx.p + 2) === first)) {
1652
+ } else if ((!state || state === 3) && c === first && (!isMultiline || ctx.s.charCodeAt(ctx.p + 1) === first && ctx.s.charCodeAt(ctx.p + 2) === first)) {
1645
1653
  if (isMultiline) {
1646
1654
  if (ctx.s.charCodeAt(ctx.p + 3) === first)
1647
1655
  ctx.p++;
1648
1656
  if (ctx.s.charCodeAt(ctx.p + 3) === first)
1649
1657
  ctx.p++;
1650
1658
  }
1651
- if (!state2)
1659
+ if (!state)
1652
1660
  parsed += ctx.s.slice(sliceStart, ctx.p);
1653
1661
  ctx.p += isMultiline ? 3 : 1;
1654
1662
  return parsed;
1655
- } else if (!state2) {
1663
+ } else if (!state) {
1656
1664
  if (!isLiteral && c === 92) {
1657
1665
  parsed += ctx.s.slice(sliceStart, sliceStart = ctx.p);
1658
- state2 = 1;
1666
+ state = 1;
1659
1667
  }
1660
- } else if (state2 === 1) {
1668
+ } else if (state === 1) {
1661
1669
  if (c === 120 || c === 117 || c === 85) {
1662
1670
  let value = 0;
1663
1671
  let len = c === 120 ? 2 : c === 117 ? 4 : 8;
@@ -1682,9 +1690,9 @@ function parseString(ctx) {
1682
1690
  }
1683
1691
  parsed += String.fromCodePoint(value);
1684
1692
  sliceStart = ctx.p + 1;
1685
- state2 = 0;
1693
+ state = 0;
1686
1694
  } else if (c === 32 || c === 9) {
1687
- state2 = 2;
1695
+ state = 2;
1688
1696
  } else {
1689
1697
  if (c === 98)
1690
1698
  parsed += "\b";
@@ -1705,16 +1713,16 @@ function parseString(ctx) {
1705
1713
  else
1706
1714
  throw new TomlError("unrecognized escape sequence", { toml: ctx.s, ptr: ctx.p });
1707
1715
  sliceStart = ctx.p + 1;
1708
- state2 = 0;
1716
+ state = 0;
1709
1717
  }
1710
1718
  } else if (c !== 32 && c !== 9) {
1711
- if (state2 === 2) {
1719
+ if (state === 2) {
1712
1720
  throw new TomlError("invalid escape: only line-ending whitespace may be escaped", {
1713
1721
  toml: ctx.s,
1714
1722
  ptr: sliceStart
1715
1723
  });
1716
1724
  }
1717
- state2 = !isLiteral && c === 92 ? 1 : 0;
1725
+ state = !isLiteral && c === 92 ? 1 : 0;
1718
1726
  sliceStart = ctx.p;
1719
1727
  }
1720
1728
  }
@@ -1977,15 +1985,15 @@ function peekTable(key, table, meta, type) {
1977
1985
  let m = meta;
1978
1986
  let k;
1979
1987
  let hasOwn = false;
1980
- let state2;
1988
+ let state;
1981
1989
  for (let i = 0; i < key.length; i++) {
1982
1990
  if (i) {
1983
1991
  t = hasOwn ? t[k] : t[k] = {};
1984
- m = (state2 = m[k]).c;
1985
- if (type === 0 && (state2.t === 1 || state2.t === 2)) {
1992
+ m = (state = m[k]).c;
1993
+ if (type === 0 && (state.t === 1 || state.t === 2)) {
1986
1994
  return null;
1987
1995
  }
1988
- if (state2.t === 2) {
1996
+ if (state.t === 2) {
1989
1997
  let l = t.length - 1;
1990
1998
  t = t[l];
1991
1999
  m = m[l].c;
@@ -2008,28 +2016,28 @@ function peekTable(key, table, meta, type) {
2008
2016
  };
2009
2017
  }
2010
2018
  }
2011
- state2 = m[k];
2012
- if (state2.t !== type && !(type === 1 && state2.t === 3)) {
2019
+ state = m[k];
2020
+ if (state.t !== type && !(type === 1 && state.t === 3)) {
2013
2021
  return null;
2014
2022
  }
2015
2023
  if (type === 2) {
2016
- if (!state2.d) {
2017
- state2.d = true;
2024
+ if (!state.d) {
2025
+ state.d = true;
2018
2026
  t[k] = [];
2019
2027
  }
2020
2028
  t[k].push(t = {});
2021
- state2.c[state2.i++] = state2 = { t: 1, d: false, i: 0, c: {} };
2029
+ state.c[state.i++] = state = { t: 1, d: false, i: 0, c: {} };
2022
2030
  }
2023
- if (state2.d) {
2031
+ if (state.d) {
2024
2032
  return null;
2025
2033
  }
2026
- state2.d = true;
2034
+ state.d = true;
2027
2035
  if (type === 1) {
2028
2036
  t = hasOwn ? t[k] : t[k] = {};
2029
2037
  } else if (type === 0 && hasOwn) {
2030
2038
  return null;
2031
2039
  }
2032
- return [k, t, state2.c];
2040
+ return [k, t, state.c];
2033
2041
  }
2034
2042
  function parse2(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
2035
2043
  let ctx = { s: toml, p: 0, d: maxDepth };
@@ -2846,16 +2854,30 @@ async function installHooks(port, options) {
2846
2854
  const raw = existsSync7(configPath) ? readFileSync8(configPath) : void 0;
2847
2855
  const text = raw?.toString("utf8") ?? "";
2848
2856
  const { remainder, block } = splitManagedBlock(text, configPath);
2849
- const newBlock = buildManagedBlock(scriptPath, port);
2857
+ const newBlock = buildManagedBlock(scriptPath);
2858
+ if (block !== void 0 && !isKnownManagedBlock(block, scriptPath)) {
2859
+ throw new Error(`Wingman Codex hook \u53D7\u7BA1\u5757\u5DF2\u88AB\u4FEE\u6539\u6216\u6B8B\u7F3A(${configPath}),\u62D2\u7EDD\u8C0E\u62A5\u63A5\u7EBF\u6210\u529F;\u8BF7\u5148\u4ECE <wingman\u6839>/backups/ \u6062\u590D\u5B8C\u6574\u5757,\u6216\u4EBA\u5DE5\u6838\u5BF9\u540E\u79FB\u9664\u8BE5\u53D7\u7BA1\u5757\u518D\u91CD\u65B0\u5B89\u88C5\u5E76\u4FE1\u4EFB`);
2860
+ }
2850
2861
  mkdirSync5(root, { recursive: true });
2851
2862
  writeFileSync6(scriptPath, FORWARD_SCRIPT);
2852
- if (block === newBlock)
2863
+ const statePath2 = hooksStatePath(root);
2864
+ const prior = readState2(statePath2);
2865
+ if (block !== void 0) {
2866
+ if (prior?.port !== port || prior.configPath !== configPath || prior.forwardScriptPath !== scriptPath) {
2867
+ writeState2(statePath2, {
2868
+ ...prior,
2869
+ version: 1,
2870
+ port,
2871
+ configPath,
2872
+ forwardScriptPath: scriptPath,
2873
+ installedAt: prior?.installedAt ?? (/* @__PURE__ */ new Date()).toISOString()
2874
+ });
2875
+ }
2853
2876
  return;
2877
+ }
2854
2878
  assertWritable(remainder, configPath);
2855
2879
  const backupDir = raw === void 0 ? void 0 : backupFile(root, configPath, raw);
2856
- const statePath2 = hooksStatePath(root);
2857
- const prior = readState2(statePath2);
2858
- const original = prior?.original ?? (block === void 0 ? raw === void 0 ? { existed: false } : { existed: true, base64: raw.toString("base64") } : void 0);
2880
+ const original = prior?.original ?? (raw === void 0 ? { existed: false } : { existed: true, base64: raw.toString("base64") });
2859
2881
  writeState2(statePath2, {
2860
2882
  version: 1,
2861
2883
  port,
@@ -2877,14 +2899,17 @@ async function uninstallHooks(options) {
2877
2899
  const scriptPath = join12(root, FORWARD_SCRIPT_NAME);
2878
2900
  const statePath2 = hooksStatePath(root);
2879
2901
  const configPath = join12(paths.codexHome, "config.toml");
2880
- const state2 = readState2(statePath2);
2902
+ const state = readState2(statePath2);
2881
2903
  const raw = existsSync7(configPath) ? readFileSync8(configPath) : void 0;
2882
2904
  if (raw !== void 0) {
2883
2905
  const text = raw.toString("utf8");
2884
2906
  const { remainder, block } = splitManagedBlock(text, configPath);
2885
2907
  if (block !== void 0) {
2908
+ if (!isKnownManagedBlock(block, scriptPath)) {
2909
+ throw new Error(`Wingman Codex hook \u53D7\u7BA1\u5757\u5DF2\u88AB\u4FEE\u6539\u6216\u6B8B\u7F3A(${configPath}),\u62D2\u7EDD\u81EA\u52A8\u5220\u9664;\u8BF7\u5148\u4ECE <wingman\u6839>/backups/ \u6062\u590D\u5B8C\u6574\u5757,\u6216\u4EBA\u5DE5\u6838\u5BF9\u540E\u79FB\u9664\u8BE5\u53D7\u7BA1\u5757`);
2910
+ }
2886
2911
  backupFile(root, configPath, raw);
2887
- const restored = restoreTarget(remainder, state2);
2912
+ const restored = restoreTarget(remainder, state);
2888
2913
  if (restored === null)
2889
2914
  rmSync3(configPath);
2890
2915
  else
@@ -2898,10 +2923,15 @@ function hasManagedHooks(paths) {
2898
2923
  const configPath = join12(paths.codexHome, "config.toml");
2899
2924
  if (!existsSync7(configPath))
2900
2925
  return false;
2901
- return readFileSync8(configPath, "utf8").includes(MANAGED_BLOCK_START);
2926
+ try {
2927
+ const { block } = splitManagedBlock(readFileSync8(configPath, "utf8"), configPath);
2928
+ return block !== void 0 && isKnownManagedBlock(block, join12(wingmanStateRoot(paths), FORWARD_SCRIPT_NAME));
2929
+ } catch {
2930
+ return false;
2931
+ }
2902
2932
  }
2903
- function restoreTarget(remainder, state2) {
2904
- const original = state2?.original;
2933
+ function restoreTarget(remainder, state) {
2934
+ const original = state?.original;
2905
2935
  if (original === void 0)
2906
2936
  return remainder;
2907
2937
  if (!original.existed) {
@@ -2915,8 +2945,11 @@ function restoreTarget(remainder, state2) {
2915
2945
  }
2916
2946
  return remainder;
2917
2947
  }
2918
- function buildManagedBlock(scriptPath, port) {
2919
- const command = tomlBasicString(`node "${scriptPath}" ${port}`);
2948
+ function buildManagedBlock(scriptPath) {
2949
+ return buildManagedBlockForCommand(`node "${scriptPath}"`);
2950
+ }
2951
+ function buildManagedBlockForCommand(rawCommand) {
2952
+ const command = tomlBasicString(rawCommand);
2920
2953
  const handler = `[{ hooks = [{ type = "command", command = ${command} }] }]`;
2921
2954
  const lines = [MANAGED_BLOCK_START, "[hooks]"];
2922
2955
  for (const event of HOOK_EVENTS)
@@ -2925,6 +2958,38 @@ function buildManagedBlock(scriptPath, port) {
2925
2958
  return `${lines.join("\n")}
2926
2959
  `;
2927
2960
  }
2961
+ function isKnownManagedBlock(block, scriptPath) {
2962
+ if (block === buildManagedBlock(scriptPath))
2963
+ return true;
2964
+ let table;
2965
+ try {
2966
+ table = parse2(block);
2967
+ } catch {
2968
+ return false;
2969
+ }
2970
+ const hooks = table.hooks;
2971
+ if (!isTable(hooks))
2972
+ return false;
2973
+ const first = hooks[HOOK_EVENTS[0]];
2974
+ if (!Array.isArray(first) || first.length !== 1 || !isTable(first[0]))
2975
+ return false;
2976
+ const handlers = first[0].hooks;
2977
+ if (!Array.isArray(handlers) || handlers.length !== 1 || !isTable(handlers[0]))
2978
+ return false;
2979
+ const command = handlers[0].command;
2980
+ if (typeof command !== "string")
2981
+ return false;
2982
+ const prefix = `node "${scriptPath}" `;
2983
+ if (!command.startsWith(prefix))
2984
+ return false;
2985
+ const legacyPortText = command.slice(prefix.length);
2986
+ if (!/^[1-9]\d{0,4}$/.test(legacyPortText))
2987
+ return false;
2988
+ const legacyPort = Number(legacyPortText);
2989
+ if (legacyPort > 65535 || String(legacyPort) !== legacyPortText)
2990
+ return false;
2991
+ return block === buildManagedBlockForCommand(command);
2992
+ }
2928
2993
  function splitManagedBlock(text, configPath) {
2929
2994
  const start = text.indexOf(MANAGED_BLOCK_START);
2930
2995
  const endMark = text.indexOf(MANAGED_BLOCK_END);
@@ -2980,9 +3045,9 @@ function readState2(statePath2) {
2980
3045
  return void 0;
2981
3046
  }
2982
3047
  }
2983
- function writeState2(statePath2, state2) {
3048
+ function writeState2(statePath2, state) {
2984
3049
  mkdirSync5(dirname7(statePath2), { recursive: true });
2985
- writeFileSync6(statePath2, `${JSON.stringify(state2, null, 2)}
3050
+ writeFileSync6(statePath2, `${JSON.stringify(state, null, 2)}
2986
3051
  `);
2987
3052
  }
2988
3053
  function deepEqual3(a, b) {
@@ -3013,19 +3078,123 @@ var init_hooks2 = __esm({
3013
3078
  FORWARD_SCRIPT_NAME = "codex-forward.mjs";
3014
3079
  FORWARD_SCRIPT = `#!/usr/bin/env node
3015
3080
  // wingman codex hook \u8F6C\u53D1\u811A\u672C(\u81EA\u52A8\u751F\u6210,\`wingman hook uninstall codex\` \u65F6\u5220\u9664)\u3002
3016
- // \u8BFB stdin \u5168\u91CF(codex hook \u7684 CC \u57FA\u51C6\u5F62\u72B6 JSON)\u2192 \u539F\u6837 POST \u672C\u673A wingman \u9762\u677F;
3017
- // 3s \u8D85\u65F6,\u4EFB\u4F55\u5931\u8D25\u9759\u9ED8\u9000\u51FA 0,\u7EDD\u4E0D\u963B\u585E harness(payload \u4E0D\u6539\u5199,\u7EDF\u4E00\u5B9E\u51B5\u534F\u8BAE \xA72)\u3002
3018
- const port = process.argv[2];
3081
+ // \u8BFB stdin \u5168\u91CF(codex hook \u7684 CC \u57FA\u51C6\u5F62\u72B6 JSON)\u2192 \u539F\u6837 POST \u672C\u673A wingman \u9762\u677F\u3002
3082
+ // \u53EA\u8BA4 serve \u5C31\u7EEA\u540E\u5199\u4E0B\u7684 v2 \u8FD0\u884C\u6001\uFF1B\u5148\u505A HMAC \u8EAB\u4EFD\u63A2\u6D4B\uFF0C\u9A8C\u8BC1\u901A\u8FC7\u624D\u53D1\u9001\u4F1A\u8BDD\u5185\u5BB9\u3002
3083
+ // \u5019\u9009\u6709\u754C,\u8EAB\u4EFD\u63A2\u6D4B\u5E76\u884C\u4E14\u5171\u4EAB\u603B\u7F51\u7EDC\u9884\u7B97;\u4EFB\u4F55\u5931\u8D25\u9759\u9ED8\u9000\u51FA 0,\u7EDD\u4E0D\u963B\u585E harness\u3002
3084
+ import { readdir, readFile } from 'node:fs/promises';
3085
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
3086
+ import { dirname, join } from 'node:path';
3087
+ import { fileURLToPath } from 'node:url';
3088
+
3089
+ const root = dirname(fileURLToPath(import.meta.url));
3090
+ const stateRoot = join(root, 'state');
3091
+ const stateName = /^serve-(\\d+)-(\\d+)-[A-Za-z0-9_-]+\\.json$/;
3092
+ const maxCandidates = 5;
3093
+ const probeTimeoutMs = 1000;
3094
+ const postTimeoutMs = 3000;
3095
+ const totalNetworkBudgetMs = 4500;
3096
+
3097
+ function validPort(value) {
3098
+ return Number.isInteger(value) && value > 0 && value <= 65535;
3099
+ }
3100
+
3101
+ function validToken(value) {
3102
+ return typeof value === 'string' && /^[A-Za-z0-9_-]{32,128}$/.test(value);
3103
+ }
3104
+
3105
+ async function candidates() {
3106
+ try {
3107
+ const dir = join(stateRoot, 'serve');
3108
+ const names = await readdir(dir);
3109
+ const selected = names
3110
+ .flatMap((name) => {
3111
+ const match = stateName.exec(name);
3112
+ return match === null ? [] : [{ name, startedAtMs: Number(match[1]) }];
3113
+ })
3114
+ .filter((entry) => Number.isSafeInteger(entry.startedAtMs) && entry.startedAtMs >= 0)
3115
+ .sort((a, b) => b.startedAtMs - a.startedAtMs)
3116
+ .slice(0, maxCandidates);
3117
+ const states = await Promise.all(
3118
+ selected.map(async ({ name }) => {
3119
+ try {
3120
+ const state = JSON.parse(await readFile(join(dir, name), 'utf8'));
3121
+ return state.version === 2 && validPort(state.port) && validToken(state.token)
3122
+ ? { port: state.port, token: state.token }
3123
+ : undefined;
3124
+ } catch {
3125
+ return undefined;
3126
+ }
3127
+ }),
3128
+ );
3129
+ return states.filter(Boolean);
3130
+ } catch {
3131
+ return [];
3132
+ }
3133
+ }
3134
+
3135
+ function sameProof(expected, actual) {
3136
+ if (typeof actual !== 'string') return false;
3137
+ const left = Buffer.from(expected);
3138
+ const right = Buffer.from(actual);
3139
+ return left.length === right.length && timingSafeEqual(left, right);
3140
+ }
3141
+
3142
+ async function verified(candidate) {
3143
+ const nonce = randomBytes(18).toString('base64url');
3144
+ const expected = createHmac('sha256', candidate.token).update(nonce).digest('base64url');
3145
+ try {
3146
+ const response = await fetch(
3147
+ \`http://127.0.0.1:\${candidate.port}/api/hook-target?nonce=\${nonce}\`,
3148
+ // \u51B7\u542F\u52A8\u6216\u9AD8\u8D1F\u8F7D\u673A\u5668\u4E0A Node/SEA \u53EF\u80FD\u6570\u767E\u6BEB\u79D2\u624D\u8C03\u5EA6\u5230\u672C\u673A\u54CD\u5E94\uFF1B
3149
+ // \u4ECD\u7ED9\u6BCF\u4E2A\u5019\u9009\u72EC\u7ACB\u4E0A\u9650\uFF0C\u4F46\u4E0D\u8981\u56E0 400ms \u8FC7\u7D27\u800C\u8BEF\u4E22\u771F\u5B9E hook\u3002
3150
+ // \u7981\u6B62\u8DDF\u968F\u91CD\u5B9A\u5411\uFF1A\u5426\u5219\u590D\u7528\u7AEF\u53E3\u53EF\u628A\u63A2\u6D4B\u8F6C\u53D1\u7ED9\u771F\u6B63 Wingman\u3001\u518D\u9A97\u53D6\u6B63\u6587\u3002
3151
+ { signal: AbortSignal.timeout(probeTimeoutMs), redirect: 'error' },
3152
+ );
3153
+ return response.status === 204 && sameProof(expected, response.headers.get('x-wingman-hook-proof'));
3154
+ } catch {
3155
+ return false;
3156
+ }
3157
+ }
3158
+
3019
3159
  const chunks = [];
3020
3160
  process.stdin.on('data', (chunk) => chunks.push(chunk));
3021
3161
  process.stdin.on('end', async () => {
3022
3162
  try {
3023
- await fetch(\`http://127.0.0.1:\${port}/hook?harness=codex\`, {
3024
- method: 'POST',
3025
- headers: { 'content-type': 'application/json' },
3026
- body: Buffer.concat(chunks),
3027
- signal: AbortSignal.timeout(3000),
3163
+ const body = Buffer.concat(chunks);
3164
+ const networkStartedAt = performance.now();
3165
+ const available = await candidates();
3166
+ // \u8EAB\u4EFD\u63A2\u6D4B\u4E0D\u542B\u6B63\u6587\uFF0C\u53EF\u5B89\u5168\u5E76\u884C\uFF1B\u5171\u4EAB deadline \u907F\u514D\u5E95\u5C42 fetch
3167
+ // \u5BF9\u591A\u4E2A\u6302\u8D77 socket \u9010\u4E2A\u5EF6\u8FDF\u8D85\u65F6\u3002\u53EA\u8BB0\u5F55 deadline \u524D\u5DF2\u5B8C\u6210\u9A8C\u8BC1\u7684\u5019\u9009\u3002
3168
+ const checks = Array(available.length);
3169
+ const probes = available.map(async (candidate, index) => {
3170
+ if (await verified(candidate)) checks[index] = candidate;
3028
3171
  });
3172
+ let deadline;
3173
+ await Promise.race([
3174
+ Promise.all(probes),
3175
+ new Promise((resolve) => {
3176
+ deadline = setTimeout(resolve, probeTimeoutMs + 250);
3177
+ }),
3178
+ ]);
3179
+ clearTimeout(deadline);
3180
+ for (const candidate of checks.filter(Boolean)) {
3181
+ const remainingMs = totalNetworkBudgetMs - (performance.now() - networkStartedAt);
3182
+ if (remainingMs <= 0) break;
3183
+ try {
3184
+ const response = await fetch(\`http://127.0.0.1:\${candidate.port}/hook?harness=codex\`, {
3185
+ method: 'POST',
3186
+ headers: {
3187
+ 'content-type': 'application/json',
3188
+ 'x-wingman-hook-token': candidate.token,
3189
+ },
3190
+ body,
3191
+ // \u5DF2\u9A8C\u8BC1\u4E3A\u672C\u5B9E\u4F8B\u540E\u7ED9\u6B63\u6587\u63A5\u6536\u7559\u8DB3\u65F6\u95F4\uFF1B\u5927 payload/\u6162\u78C1\u76D8\u4E0D\u5E94\u88AB 1s \u8BEF\u6740\u3002
3192
+ signal: AbortSignal.timeout(Math.max(1, Math.min(postTimeoutMs, Math.ceil(remainingMs)))),
3193
+ redirect: 'error',
3194
+ });
3195
+ if (response.ok) break;
3196
+ } catch {}
3197
+ }
3029
3198
  } catch {}
3030
3199
  process.exit(0);
3031
3200
  });
@@ -9134,11 +9303,11 @@ var require_lexer = __commonJS({
9134
9303
  hasChars(n) {
9135
9304
  return this.pos + n <= this.buffer.length;
9136
9305
  }
9137
- setNext(state2) {
9306
+ setNext(state) {
9138
9307
  this.buffer = this.buffer.substring(this.pos);
9139
9308
  this.pos = 0;
9140
9309
  this.lineEndPos = null;
9141
- this.next = state2;
9310
+ this.next = state;
9142
9311
  return null;
9143
9312
  }
9144
9313
  peek(n) {
@@ -11028,14 +11197,14 @@ async function readState3(paths) {
11028
11197
  const buffer = await readIfExists(paths.stateFile);
11029
11198
  if (buffer === void 0)
11030
11199
  return void 0;
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;
11200
+ const state = JSON.parse(buffer.toString("utf8"));
11201
+ if (state.version !== 1)
11202
+ throw new Error(`\u672A\u77E5\u7684\u63A5\u7EBF\u72B6\u6001\u7248\u672C:${String(state.version)},\u4E0D\u731C`);
11203
+ return state;
11035
11204
  }
11036
- async function writeState3(paths, state2) {
11205
+ async function writeState3(paths, state) {
11037
11206
  await mkdir(dirname9(paths.stateFile), { recursive: true });
11038
- await writeFile(paths.stateFile, `${JSON.stringify(state2, void 0, 2)}
11207
+ await writeFile(paths.stateFile, `${JSON.stringify(state, void 0, 2)}
11039
11208
  `, "utf8");
11040
11209
  }
11041
11210
  async function backupPatchFile(paths, bytes) {
@@ -11101,13 +11270,13 @@ async function installDshHooks(port, options) {
11101
11270
  }
11102
11271
  async function uninstallDshHooks(options) {
11103
11272
  const paths = resolveDshHookPaths(options);
11104
- const state2 = await readState3(paths);
11273
+ const state = await readState3(paths);
11105
11274
  const currentBuffer = await readIfExists(paths.patchFile);
11106
11275
  if (currentBuffer !== void 0) {
11107
11276
  const currentText = decodeUtf8Strict(currentBuffer, paths.patchFile);
11108
11277
  if (currentText.includes(HOOKS_MARKER_BEGIN_PREFIX)) {
11109
11278
  const surgical = stripLiveHooksBlocks(currentText, paths.patchFile);
11110
- const restored = resolveRestoredText(surgical, state2, paths);
11279
+ const restored = resolveRestoredText(surgical, state, paths);
11111
11280
  if (restored !== currentText) {
11112
11281
  await backupPatchFile(paths, currentBuffer);
11113
11282
  if (restored === void 0)
@@ -11121,9 +11290,9 @@ async function uninstallDshHooks(options) {
11121
11290
  await rm(paths.forwarderFile, { force: true });
11122
11291
  await rm(paths.stateFile, { force: true });
11123
11292
  }
11124
- function resolveRestoredText(surgical, state2, paths) {
11125
- if (state2?.original.existed === true) {
11126
- const originalText = Buffer3.from(state2.original.bytesBase64, "base64").toString("utf8");
11293
+ function resolveRestoredText(surgical, state, paths) {
11294
+ if (state?.original.existed === true) {
11295
+ const originalText = Buffer3.from(state.original.bytesBase64, "base64").toString("utf8");
11127
11296
  const surgicalEntries = tryParsePatchEntries(surgical, paths.patchFile);
11128
11297
  const originalEntries = tryParsePatchEntries(originalText, paths.patchFile);
11129
11298
  if (surgicalEntries !== void 0 && originalEntries !== void 0 && JSON.stringify(surgicalEntries) === JSON.stringify(originalEntries)) {
@@ -11716,7 +11885,7 @@ var init_dist5 = __esm({
11716
11885
  init_dist();
11717
11886
  import { readFileSync as readFileSync12 } from "node:fs";
11718
11887
  import { dirname as dirname12, resolve as resolve6 } from "node:path";
11719
- import { fileURLToPath as fileURLToPath3 } from "node:url";
11888
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
11720
11889
 
11721
11890
  // packages/fixtures/dist/index.js
11722
11891
  import { dirname, join } from "node:path";
@@ -11896,7 +12065,7 @@ function verifyEntitlement(raw, now, publicKeyPem) {
11896
12065
  let claims;
11897
12066
  try {
11898
12067
  const decoded = JSON.parse(Buffer.from(parsed.token, "base64url").toString("utf8"));
11899
- if (typeof decoded.sub !== "string" || typeof decoded.plan !== "string" || typeof decoded.iat !== "string" || typeof decoded.exp !== "string" || Number.isNaN(Date.parse(decoded.exp))) {
12068
+ if (typeof decoded.sub !== "string" || decoded.plan !== "trial" && decoded.plan !== "subscription" || typeof decoded.iat !== "string" || typeof decoded.exp !== "string" || Number.isNaN(Date.parse(decoded.exp))) {
11900
12069
  return { state: "invalid", detail: "license \u8F7D\u8377\u5B57\u6BB5\u4E0D\u5B8C\u6574" };
11901
12070
  }
11902
12071
  claims = { sub: decoded.sub, plan: decoded.plan, iat: decoded.iat, exp: decoded.exp };
@@ -11904,10 +12073,22 @@ function verifyEntitlement(raw, now, publicKeyPem) {
11904
12073
  return { state: "invalid", detail: "license \u8F7D\u8377\u65E0\u6CD5\u89E3\u7801" };
11905
12074
  }
11906
12075
  const exp = Date.parse(claims.exp);
11907
- const graceEnd = exp + OFFLINE_GRACE_DAYS * 24 * 60 * 60 * 1e3;
12076
+ const isTrial = claims.plan === "trial";
11908
12077
  if (now.getTime() < exp) {
11909
- return { state: "active", detail: `\u8BA2\u9605\u6709\u6548\u81F3 ${claims.exp}`, claims };
12078
+ return {
12079
+ state: "active",
12080
+ detail: `${isTrial ? "\u8BD5\u7528" : "\u8BA2\u9605"}\u6709\u6548\u81F3 ${claims.exp}`,
12081
+ claims
12082
+ };
11910
12083
  }
12084
+ if (isTrial) {
12085
+ return {
12086
+ state: "expired",
12087
+ detail: `\u8BD5\u7528\u5DF2\u4E8E ${claims.exp} \u5230\u671F,\u5199\u52A8\u4F5C\u5DF2\u9501;\u8BA2\u9605\u540E\u5728\u9762\u677F\u70B9\u300C\u5237\u65B0\u6388\u6743\u300D\u5373\u53EF\u6062\u590D`,
12088
+ claims
12089
+ };
12090
+ }
12091
+ const graceEnd = exp + OFFLINE_GRACE_DAYS * 24 * 60 * 60 * 1e3;
11911
12092
  if (now.getTime() < graceEnd) {
11912
12093
  return {
11913
12094
  state: "grace",
@@ -11965,7 +12146,8 @@ async function refreshLicense(options = {}) {
11965
12146
  } catch {
11966
12147
  return {
11967
12148
  ok: false,
11968
- message: `\u672A\u627E\u5230 ${licensePath}\u2014\u2014\u5148\u8BA2\u9605,\u4ECE\u4ED8\u6B3E\u540E\u7684\u6FC0\u6D3B\u9875\u4E0B\u8F7D license.json \u653E\u8FDB\u53BB`
12149
+ message: `\u672A\u627E\u5230 ${licensePath}\u2014\u2014\u5148\u8BA2\u9605,\u4ECE\u4ED8\u6B3E\u540E\u7684\u6FC0\u6D3B\u9875\u4E0B\u8F7D license.json \u653E\u8FDB\u53BB`,
12150
+ reason: "missing"
11969
12151
  };
11970
12152
  }
11971
12153
  let response;
@@ -11977,43 +12159,91 @@ async function refreshLicense(options = {}) {
11977
12159
  signal: AbortSignal.timeout(15e3)
11978
12160
  });
11979
12161
  } catch {
11980
- return { ok: false, message: "\u8054\u7F51\u5931\u8D25,\u7A0D\u540E\u518D\u8BD5(\u79BB\u7EBF\u5BBD\u9650\u671F\u5185\u4E0D\u5F71\u54CD\u4F7F\u7528)" };
12162
+ return {
12163
+ ok: false,
12164
+ message: "\u8054\u7F51\u5931\u8D25,\u7A0D\u540E\u518D\u8BD5;\u5F53\u524D\u6388\u6743\u72B6\u6001\u4FDD\u6301\u4E0D\u53D8",
12165
+ reason: "network"
12166
+ };
11981
12167
  }
11982
12168
  if (!response.ok) {
11983
12169
  let detail = `\u7B7E\u53D1\u7AEF\u8FD4\u56DE HTTP ${response.status}`;
12170
+ let inactive = false;
11984
12171
  try {
11985
12172
  const body = await response.json();
11986
12173
  if (typeof body.error === "string")
11987
12174
  detail = body.error;
12175
+ inactive = body.reason === "inactive";
11988
12176
  } catch {
11989
12177
  }
11990
- return { ok: false, message: `\u7EED\u671F\u88AB\u62D2:${detail}` };
12178
+ return {
12179
+ ok: false,
12180
+ message: `\u7EED\u671F\u88AB\u62D2:${detail}`,
12181
+ reason: inactive ? "inactive" : "server"
12182
+ };
11991
12183
  }
11992
12184
  let entitlement;
11993
12185
  try {
11994
12186
  entitlement = await response.json();
11995
12187
  } catch {
11996
- return { ok: false, message: "\u7B7E\u53D1\u7AEF\u8FD4\u56DE\u7684\u4E0D\u662F\u5408\u6CD5 JSON;\u539F license \u672A\u52A8" };
12188
+ return {
12189
+ ok: false,
12190
+ message: "\u7B7E\u53D1\u7AEF\u8FD4\u56DE\u7684\u4E0D\u662F\u5408\u6CD5 JSON;\u539F license \u672A\u52A8",
12191
+ reason: "invalid-response"
12192
+ };
11997
12193
  }
11998
12194
  if (typeof entitlement.token !== "string" || typeof entitlement.signature !== "string") {
11999
- return { ok: false, message: "\u7B7E\u53D1\u7AEF\u8FD4\u56DE\u7F3A token/signature;\u539F license \u672A\u52A8" };
12195
+ return {
12196
+ ok: false,
12197
+ message: "\u7B7E\u53D1\u7AEF\u8FD4\u56DE\u7F3A token/signature;\u539F license \u672A\u52A8",
12198
+ reason: "invalid-response"
12199
+ };
12000
12200
  }
12001
12201
  const next = `${JSON.stringify({ token: entitlement.token, signature: entitlement.signature })}
12002
12202
  `;
12203
+ const publicKeyPem = options.publicKeyPem ?? PROD_PUBLIC_KEY_PEM;
12204
+ if (publicKeyPem !== "") {
12205
+ const status = verifyEntitlement(next, options.now ?? /* @__PURE__ */ new Date(), publicKeyPem);
12206
+ if (status.state !== "active" && status.state !== "grace") {
12207
+ return {
12208
+ ok: false,
12209
+ message: `\u7B7E\u53D1\u7AEF\u8FD4\u56DE\u7684 license \u65E0\u6548:${status.detail};\u539F license \u672A\u52A8`,
12210
+ reason: "invalid-response"
12211
+ };
12212
+ }
12213
+ }
12214
+ try {
12215
+ if (readFileSync2(licensePath, "utf8") !== raw) {
12216
+ return {
12217
+ ok: false,
12218
+ message: "\u5237\u65B0\u671F\u95F4\u672C\u673A\u6388\u6743\u5DF2\u88AB\u66F4\u6362;\u5DF2\u4FDD\u7559\u8F83\u65B0\u7684\u6587\u4EF6,\u8BF7\u518D\u70B9\u4E00\u6B21\u5237\u65B0",
12219
+ reason: "changed"
12220
+ };
12221
+ }
12222
+ } catch {
12223
+ return {
12224
+ ok: false,
12225
+ message: "\u5237\u65B0\u671F\u95F4\u672C\u673A\u6388\u6743\u5DF2\u88AB\u66F4\u6362;\u5F53\u524D\u6587\u4EF6\u672A\u88AB\u8986\u76D6",
12226
+ reason: "changed"
12227
+ };
12228
+ }
12003
12229
  const tmpPath = join3(dirname2(licensePath), `.license.json.tmp-${process.pid}`);
12004
12230
  try {
12005
12231
  writeFileSync2(tmpPath, next);
12006
12232
  renameSync2(tmpPath, licensePath);
12007
12233
  } catch (error) {
12008
12234
  const message = error instanceof Error ? error.message : String(error);
12009
- return { ok: false, message: `\u5199\u5165 ${licensePath} \u5931\u8D25:${message}` };
12235
+ return { ok: false, message: `\u5199\u5165 ${licensePath} \u5931\u8D25:${message}`, reason: "write" };
12010
12236
  }
12011
12237
  return { ok: true, message: `license \u5DF2\u7EED\u671F${expSuffix(entitlement.token)}` };
12012
12238
  }
12013
12239
  function expSuffix(token) {
12014
12240
  try {
12015
12241
  const claims = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
12016
- return typeof claims.exp === "string" ? `,\u8BA2\u9605\u6709\u6548\u81F3 ${claims.exp}` : "";
12242
+ if (typeof claims.exp !== "string")
12243
+ return "";
12244
+ if (claims.plan !== "trial" && claims.plan !== "subscription")
12245
+ return "";
12246
+ return `,${claims.plan === "trial" ? "\u8BD5\u7528" : "\u8BA2\u9605"}\u6709\u6548\u81F3 ${claims.exp}`;
12017
12247
  } catch {
12018
12248
  return "";
12019
12249
  }
@@ -12114,6 +12344,246 @@ function isInProgressAt(entry, ts) {
12114
12344
  return entry.spans.some((span) => span.status === "in_progress" && span.from !== void 0 && span.from <= ts && (span.to === void 0 || ts < span.to));
12115
12345
  }
12116
12346
 
12347
+ // packages/narrative/dist/codex.js
12348
+ function parseCodexRolloutLine(line) {
12349
+ const trimmed = line.trim();
12350
+ if (trimmed === "")
12351
+ return { kind: "unknown", reason: "\u7A7A\u884C", rawText: line };
12352
+ let value;
12353
+ try {
12354
+ value = JSON.parse(trimmed);
12355
+ } catch {
12356
+ return { kind: "unknown", reason: "JSON \u89E3\u6790\u5931\u8D25", rawText: line };
12357
+ }
12358
+ if (!isRecord2(value))
12359
+ return { kind: "unknown", reason: "\u884C\u4E0D\u662F JSON \u5BF9\u8C61", rawText: line };
12360
+ if (typeof value.type !== "string") {
12361
+ return { kind: "unknown", reason: "\u7F3A type \u5B57\u6BB5\u6216\u975E\u5B57\u7B26\u4E32", rawText: line };
12362
+ }
12363
+ const timestamp = optionalString(value.timestamp);
12364
+ const payload = isRecord2(value.payload) ? value.payload : void 0;
12365
+ if (value.type === "session_meta")
12366
+ return parseSessionMeta(payload, timestamp);
12367
+ if (value.type === "turn_context")
12368
+ return parseTurnContext(payload, timestamp);
12369
+ if (value.type === "event_msg")
12370
+ return parseEventMessage(payload, timestamp);
12371
+ if (value.type === "response_item")
12372
+ return parseResponseItem(payload, timestamp);
12373
+ return otherLine(value.type, payload, timestamp);
12374
+ }
12375
+ function parseSessionMeta(payload, timestamp) {
12376
+ const line = { kind: "session_meta" };
12377
+ assignString(line, "timestamp", timestamp ?? optionalString(payload?.timestamp));
12378
+ assignString(line, "threadId", optionalString(payload?.id));
12379
+ assignString(line, "sessionId", optionalString(payload?.session_id));
12380
+ assignString(line, "parentThreadId", optionalString(payload?.parent_thread_id));
12381
+ assignString(line, "cwd", optionalString(payload?.cwd));
12382
+ assignString(line, "cliVersion", optionalString(payload?.cli_version));
12383
+ assignString(line, "modelProvider", optionalString(payload?.model_provider));
12384
+ return line;
12385
+ }
12386
+ function parseTurnContext(payload, timestamp) {
12387
+ const line = { kind: "turn_context" };
12388
+ assignString(line, "timestamp", timestamp);
12389
+ assignString(line, "turnId", optionalString(payload?.turn_id));
12390
+ assignString(line, "model", optionalString(payload?.model));
12391
+ assignString(line, "cwd", optionalString(payload?.cwd));
12392
+ return line;
12393
+ }
12394
+ function parseEventMessage(payload, timestamp) {
12395
+ const payloadType = optionalString(payload?.type);
12396
+ if (payloadType === "user_message") {
12397
+ const line = { kind: "user" };
12398
+ assignString(line, "timestamp", timestamp);
12399
+ assignString(line, "text", optionalString(payload?.message));
12400
+ return line;
12401
+ }
12402
+ if (payloadType === "agent_message") {
12403
+ const line = { kind: "assistant", source: "event_msg" };
12404
+ assignString(line, "timestamp", timestamp);
12405
+ assignString(line, "text", optionalString(payload?.message));
12406
+ assignString(line, "phase", optionalString(payload?.phase));
12407
+ return line;
12408
+ }
12409
+ if (payloadType === "task_started") {
12410
+ const line = { kind: "task_started" };
12411
+ assignString(line, "timestamp", timestamp);
12412
+ assignString(line, "turnId", optionalString(payload?.turn_id));
12413
+ return line;
12414
+ }
12415
+ if (payloadType === "task_complete") {
12416
+ const line = { kind: "task_complete" };
12417
+ assignString(line, "timestamp", timestamp);
12418
+ assignString(line, "turnId", optionalString(payload?.turn_id));
12419
+ return line;
12420
+ }
12421
+ if (payloadType === "turn_aborted") {
12422
+ const line = { kind: "turn_aborted" };
12423
+ assignString(line, "timestamp", timestamp);
12424
+ assignString(line, "turnId", optionalString(payload?.turn_id));
12425
+ return line;
12426
+ }
12427
+ if (payloadType === "token_count" && payload !== void 0) {
12428
+ return parseTokenCount(payload, timestamp);
12429
+ }
12430
+ return otherLine("event_msg", payload, timestamp);
12431
+ }
12432
+ function parseResponseItem(payload, timestamp) {
12433
+ const payloadType = optionalString(payload?.type);
12434
+ if (payloadType === "message" && payload?.role === "assistant") {
12435
+ const line = { kind: "assistant", source: "response_item" };
12436
+ assignString(line, "timestamp", timestamp);
12437
+ assignString(line, "text", extractText(payload.content, "output_text"));
12438
+ assignString(line, "phase", optionalString(payload.phase));
12439
+ assignString(line, "messageId", optionalString(payload.id));
12440
+ assignString(line, "turnId", extractTurnId(payload));
12441
+ return line;
12442
+ }
12443
+ if (payload !== void 0 && (payloadType === "function_call" || payloadType === "custom_tool_call")) {
12444
+ const call = { kind: "tool_call", callType: payloadType };
12445
+ assignString(call, "timestamp", timestamp);
12446
+ assignString(call, "callId", optionalString(payload.call_id));
12447
+ assignString(call, "itemId", optionalString(payload.id));
12448
+ assignString(call, "name", optionalString(payload.name));
12449
+ assignString(call, "namespace", optionalString(payload.namespace));
12450
+ assignString(call, "status", optionalString(payload.status));
12451
+ assignString(call, "turnId", extractTurnId(payload));
12452
+ const input = payloadType === "function_call" ? parseJsonString(payload.arguments) : payload.input;
12453
+ if (input !== void 0)
12454
+ call.input = input;
12455
+ return call;
12456
+ }
12457
+ if (payload !== void 0 && (payloadType === "function_call_output" || payloadType === "custom_tool_call_output")) {
12458
+ const result = { kind: "tool_result", resultType: payloadType };
12459
+ assignString(result, "timestamp", timestamp);
12460
+ assignString(result, "callId", optionalString(payload.call_id));
12461
+ assignString(result, "itemId", optionalString(payload.id));
12462
+ assignString(result, "turnId", extractTurnId(payload));
12463
+ if (payload.output !== void 0) {
12464
+ result.output = payload.output;
12465
+ if (codexToolResultIsError(payload.output))
12466
+ result.isError = true;
12467
+ }
12468
+ return result;
12469
+ }
12470
+ return otherLine("response_item", payload, timestamp);
12471
+ }
12472
+ function parseTokenCount(payload, timestamp) {
12473
+ const line = { kind: "token_count" };
12474
+ assignString(line, "timestamp", timestamp);
12475
+ const info = isRecord2(payload.info) ? payload.info : void 0;
12476
+ const lastRaw = isRecord2(info?.last_token_usage) ? info.last_token_usage : void 0;
12477
+ const totalRaw = isRecord2(info?.total_token_usage) ? info.total_token_usage : void 0;
12478
+ const lastUsage = parseCodexTokenUsage(lastRaw);
12479
+ const totalUsage = parseCodexTokenUsage(totalRaw);
12480
+ if (lastUsage !== void 0)
12481
+ line.lastUsage = lastUsage;
12482
+ if (totalUsage !== void 0)
12483
+ line.totalUsage = totalUsage;
12484
+ const modelContextWindow = tokenInteger(info?.model_context_window);
12485
+ if (modelContextWindow !== void 0)
12486
+ line.modelContextWindow = modelContextWindow;
12487
+ return line;
12488
+ }
12489
+ function parseCodexTokenUsage(value) {
12490
+ if (value === void 0)
12491
+ return void 0;
12492
+ const usage = { kind: "exact" };
12493
+ assignToken(usage, "inputTokens", value.input_tokens);
12494
+ assignToken(usage, "cachedInputTokens", value.cached_input_tokens);
12495
+ if (value.cache_write_input_tokens === void 0)
12496
+ usage.cacheWriteInputTokens = 0;
12497
+ else
12498
+ assignToken(usage, "cacheWriteInputTokens", value.cache_write_input_tokens);
12499
+ assignToken(usage, "outputTokens", value.output_tokens);
12500
+ assignToken(usage, "reasoningOutputTokens", value.reasoning_output_tokens);
12501
+ assignToken(usage, "totalTokens", value.total_tokens);
12502
+ return Object.keys(usage).length > 1 ? usage : void 0;
12503
+ }
12504
+ function codexToolResultIsError(output) {
12505
+ return explicitResultFailure(output, 0);
12506
+ }
12507
+ function explicitResultFailure(value, depth) {
12508
+ if (depth > 2)
12509
+ return false;
12510
+ if (typeof value === "string") {
12511
+ try {
12512
+ return explicitResultFailure(JSON.parse(value), depth + 1);
12513
+ } catch {
12514
+ return false;
12515
+ }
12516
+ }
12517
+ if (Array.isArray(value)) {
12518
+ return value.some((block) => {
12519
+ if (!isRecord2(block))
12520
+ return false;
12521
+ if (block.isError === true || block.is_error === true)
12522
+ return true;
12523
+ return (block.type === "text" || block.type === "input_text") && typeof block.text === "string" ? explicitResultFailure(block.text, depth + 1) : false;
12524
+ });
12525
+ }
12526
+ if (!isRecord2(value))
12527
+ return false;
12528
+ if (value.isError === true || value.is_error === true)
12529
+ return true;
12530
+ if (explicitExitFailure(value.exit_code))
12531
+ return true;
12532
+ const metadata = isRecord2(value.metadata) ? value.metadata : void 0;
12533
+ return explicitExitFailure(metadata?.exit_code);
12534
+ }
12535
+ function explicitExitFailure(value) {
12536
+ return typeof value === "number" && Number.isSafeInteger(value) && value !== 0;
12537
+ }
12538
+ function otherLine(type, payload, timestamp) {
12539
+ const line = { kind: "other", type };
12540
+ assignString(line, "timestamp", timestamp);
12541
+ assignString(line, "payloadType", optionalString(payload?.type));
12542
+ return line;
12543
+ }
12544
+ function extractText(value, blockType) {
12545
+ if (!Array.isArray(value))
12546
+ return void 0;
12547
+ const texts = [];
12548
+ for (const block of value) {
12549
+ if (!isRecord2(block) || block.type !== blockType || typeof block.text !== "string")
12550
+ continue;
12551
+ texts.push(block.text);
12552
+ }
12553
+ return texts.length > 0 ? texts.join("\n") : void 0;
12554
+ }
12555
+ function extractTurnId(payload) {
12556
+ const metadata = isRecord2(payload.internal_chat_message_metadata_passthrough) ? payload.internal_chat_message_metadata_passthrough : void 0;
12557
+ return optionalString(metadata?.turn_id);
12558
+ }
12559
+ function parseJsonString(value) {
12560
+ if (typeof value !== "string")
12561
+ return value;
12562
+ try {
12563
+ return JSON.parse(value);
12564
+ } catch {
12565
+ return value;
12566
+ }
12567
+ }
12568
+ function tokenInteger(value) {
12569
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
12570
+ }
12571
+ function optionalString(value) {
12572
+ return typeof value === "string" ? value : void 0;
12573
+ }
12574
+ function assignString(target, key, value) {
12575
+ if (value !== void 0)
12576
+ target[key] = value;
12577
+ }
12578
+ function assignToken(usage, key, value) {
12579
+ const parsed = tokenInteger(value);
12580
+ if (parsed !== void 0)
12581
+ usage[key] = parsed;
12582
+ }
12583
+ function isRecord2(value) {
12584
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12585
+ }
12586
+
12117
12587
  // packages/narrative/dist/parse.js
12118
12588
  function parseTranscriptLine(line) {
12119
12589
  const trimmed = line.trim();
@@ -12125,7 +12595,7 @@ function parseTranscriptLine(line) {
12125
12595
  } catch {
12126
12596
  return { kind: "unknown", reason: "JSON \u89E3\u6790\u5931\u8D25", rawText: line };
12127
12597
  }
12128
- if (!isRecord2(value))
12598
+ if (!isRecord3(value))
12129
12599
  return { kind: "unknown", reason: "\u884C\u4E0D\u662F JSON \u5BF9\u8C61", rawText: line };
12130
12600
  const type = value.type;
12131
12601
  if (typeof type !== "string") {
@@ -12140,7 +12610,7 @@ function parseTranscriptLine(line) {
12140
12610
  const attachment = {
12141
12611
  kind: "attachment",
12142
12612
  ...common,
12143
- attachment: isRecord2(value.attachment) ? value.attachment : {}
12613
+ attachment: isRecord3(value.attachment) ? value.attachment : {}
12144
12614
  };
12145
12615
  return attachment;
12146
12616
  }
@@ -12164,14 +12634,14 @@ function extractCommon(raw) {
12164
12634
  }
12165
12635
  function parseUser(raw, common) {
12166
12636
  const line = { kind: "user", ...common, toolResults: [] };
12167
- const message = isRecord2(raw.message) ? raw.message : void 0;
12637
+ const message = isRecord3(raw.message) ? raw.message : void 0;
12168
12638
  const content = message?.content;
12169
12639
  if (typeof content === "string") {
12170
12640
  line.text = content;
12171
12641
  } else if (Array.isArray(content)) {
12172
12642
  const texts = [];
12173
12643
  for (const block of content) {
12174
- if (!isRecord2(block))
12644
+ if (!isRecord3(block))
12175
12645
  continue;
12176
12646
  if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
12177
12647
  const result = {
@@ -12194,20 +12664,20 @@ function parseUser(raw, common) {
12194
12664
  }
12195
12665
  function parseAssistant(raw, common) {
12196
12666
  const line = { kind: "assistant", ...common, blocks: [] };
12197
- const message = isRecord2(raw.message) ? raw.message : void 0;
12667
+ const message = isRecord3(raw.message) ? raw.message : void 0;
12198
12668
  if (message === void 0)
12199
12669
  return line;
12200
12670
  if (typeof message.model === "string")
12201
12671
  line.model = message.model;
12202
12672
  if (typeof message.id === "string")
12203
12673
  line.messageId = message.id;
12204
- if (isRecord2(message.usage))
12674
+ if (isRecord3(message.usage))
12205
12675
  line.usage = message.usage;
12206
12676
  const content = message.content;
12207
12677
  if (!Array.isArray(content))
12208
12678
  return line;
12209
12679
  for (const block of content) {
12210
- if (!isRecord2(block))
12680
+ if (!isRecord3(block))
12211
12681
  continue;
12212
12682
  line.blocks.push(parseAssistantBlock(block));
12213
12683
  }
@@ -12225,7 +12695,7 @@ function parseAssistantBlock(block) {
12225
12695
  kind: "tool_use",
12226
12696
  id: block.id,
12227
12697
  name: block.name,
12228
- input: isRecord2(block.input) ? block.input : {}
12698
+ input: isRecord3(block.input) ? block.input : {}
12229
12699
  };
12230
12700
  }
12231
12701
  return {
@@ -12234,7 +12704,7 @@ function parseAssistantBlock(block) {
12234
12704
  raw: block
12235
12705
  };
12236
12706
  }
12237
- function isRecord2(value) {
12707
+ function isRecord3(value) {
12238
12708
  return typeof value === "object" && value !== null && !Array.isArray(value);
12239
12709
  }
12240
12710
 
@@ -12419,7 +12889,7 @@ function buildTaskCreateResultIndex(lines) {
12419
12889
  if (line.kind !== "user")
12420
12890
  continue;
12421
12891
  const result = line.toolUseResult;
12422
- if (!isRecord2(result) || !isRecord2(result.task) || typeof result.task.id !== "string")
12892
+ if (!isRecord3(result) || !isRecord3(result.task) || typeof result.task.id !== "string")
12423
12893
  continue;
12424
12894
  for (const toolResult of line.toolResults)
12425
12895
  index.set(toolResult.toolUseId, result.task.id);
@@ -12432,7 +12902,7 @@ function applyTodoWrite(input, tasks) {
12432
12902
  return false;
12433
12903
  tasks.clear();
12434
12904
  for (const raw of todos) {
12435
- if (!isRecord2(raw) || typeof raw.content !== "string" || !isTodoStatus(raw.status))
12905
+ if (!isRecord3(raw) || typeof raw.content !== "string" || !isTodoStatus(raw.status))
12436
12906
  continue;
12437
12907
  const item = { id: raw.content, content: raw.content, status: raw.status };
12438
12908
  if (typeof raw.activeForm === "string")
@@ -12538,7 +13008,7 @@ function turnUsage(line) {
12538
13008
  }
12539
13009
  };
12540
13010
  const breakdown = usage.cache_creation;
12541
- if (isRecord2(breakdown)) {
13011
+ if (isRecord3(breakdown)) {
12542
13012
  const fiveMinute = finiteNumber(breakdown.ephemeral_5m_input_tokens);
12543
13013
  const oneHour = finiteNumber(breakdown.ephemeral_1h_input_tokens);
12544
13014
  if (fiveMinute !== void 0)
@@ -12587,7 +13057,33 @@ function createNarrativeService(options = {}) {
12587
13057
  const intervalMs = options.intervalMs ?? 500;
12588
13058
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
12589
13059
  const harnesses = /* @__PURE__ */ new Map();
13060
+ const listeners = /* @__PURE__ */ new Set();
13061
+ const pendingNotifications = /* @__PURE__ */ new Set();
13062
+ let notificationQueued = false;
12590
13063
  let closed = false;
13064
+ function notify(harness) {
13065
+ pendingNotifications.add(harness);
13066
+ if (notificationQueued)
13067
+ return;
13068
+ notificationQueued = true;
13069
+ queueMicrotask(() => {
13070
+ notificationQueued = false;
13071
+ if (closed) {
13072
+ pendingNotifications.clear();
13073
+ return;
13074
+ }
13075
+ const harnesses2 = [...pendingNotifications];
13076
+ pendingNotifications.clear();
13077
+ for (const changed of harnesses2) {
13078
+ for (const listener of listeners) {
13079
+ try {
13080
+ listener(changed);
13081
+ } catch {
13082
+ }
13083
+ }
13084
+ }
13085
+ });
13086
+ }
12591
13087
  function sessionsOf(harness) {
12592
13088
  let sessions = harnesses.get(harness);
12593
13089
  if (sessions === void 0) {
@@ -12600,6 +13096,7 @@ function createNarrativeService(options = {}) {
12600
13096
  return {
12601
13097
  id,
12602
13098
  lines: [],
13099
+ codexLines: [],
12603
13100
  follows: /* @__PURE__ */ new Map(),
12604
13101
  hookIntervals: [],
12605
13102
  waiting: null,
@@ -12650,20 +13147,27 @@ function createNarrativeService(options = {}) {
12650
13147
  function resetTranscript(session, path) {
12651
13148
  closeSession(session);
12652
13149
  session.lines = [];
13150
+ session.codexLines = [];
12653
13151
  session.hookIntervals = [];
13152
+ session.hookPrompt = void 0;
13153
+ session.hookPromptAt = void 0;
12654
13154
  session.mockNote = void 0;
12655
13155
  session.transcriptPath = path;
12656
13156
  session.dirty = true;
12657
13157
  }
12658
- function startFollowing(session, path) {
13158
+ function startFollowing(harness, session, path) {
12659
13159
  if (closed || session.follows.has(path))
12660
13160
  return;
12661
13161
  session.follows.set(path, followTranscript(path, (line) => {
12662
- session.lines.push(parseTranscriptLine(line));
13162
+ if (harness === "codex")
13163
+ session.codexLines.push(parseCodexRolloutLine(line));
13164
+ else
13165
+ session.lines.push(parseTranscriptLine(line));
12663
13166
  session.dirty = true;
13167
+ notify(harness);
12664
13168
  }, { intervalMs }));
12665
13169
  }
12666
- function startSubagentDiscovery(session, transcriptPath) {
13170
+ function startSubagentDiscovery(harness, session, transcriptPath) {
12667
13171
  const sessionDir = transcriptPath.replace(/\.jsonl$/, "");
12668
13172
  if (sessionDir === transcriptPath)
12669
13173
  return;
@@ -12672,8 +13176,9 @@ function createNarrativeService(options = {}) {
12672
13176
  try {
12673
13177
  const entries = await readdir(subagentsDir);
12674
13178
  for (const entry of entries) {
12675
- if (/^agent-.*\.jsonl$/.test(entry))
12676
- startFollowing(session, join4(subagentsDir, entry));
13179
+ if (/^agent-.*\.jsonl$/.test(entry)) {
13180
+ startFollowing(harness, session, join4(subagentsDir, entry));
13181
+ }
12677
13182
  }
12678
13183
  } catch {
12679
13184
  }
@@ -12700,8 +13205,17 @@ function createNarrativeService(options = {}) {
12700
13205
  if (follow && transcriptPath !== void 0) {
12701
13206
  if (session.transcriptPath !== transcriptPath) {
12702
13207
  resetTranscript(session, transcriptPath);
12703
- startFollowing(session, transcriptPath);
12704
- startSubagentDiscovery(session, transcriptPath);
13208
+ startFollowing(event.harness, session, transcriptPath);
13209
+ if (event.harness === "cc") {
13210
+ startSubagentDiscovery(event.harness, session, transcriptPath);
13211
+ }
13212
+ }
13213
+ }
13214
+ if (name === "UserPromptSubmit" && typeof payload.prompt === "string") {
13215
+ const prompt = payload.prompt.trim();
13216
+ if (prompt !== "") {
13217
+ session.hookPrompt = truncate(prompt, DETAIL_LIMIT);
13218
+ session.hookPromptAt = event.received_at;
12705
13219
  }
12706
13220
  }
12707
13221
  if (name === "SubagentStart") {
@@ -12753,13 +13267,34 @@ function createNarrativeService(options = {}) {
12753
13267
  }
12754
13268
  session.lastEvent = { name, at: event.received_at };
12755
13269
  session.dirty = true;
13270
+ notify(event.harness);
12756
13271
  }
12757
13272
  function seedFixture(harness, rawLines, mockNote) {
12758
- const lines = rawLines.filter((l) => l.trim() !== "").map((l) => parseTranscriptLine(l));
13273
+ const sourceLines = rawLines.filter((line) => line.trim() !== "");
13274
+ if (harness === "codex") {
13275
+ const codexLines = sourceLines.map((line) => parseCodexRolloutLine(line));
13276
+ const meta = codexLines.find((line) => line.kind === "session_meta");
13277
+ const id2 = meta?.threadId ?? meta?.sessionId ?? "__fixture__";
13278
+ const session2 = sessionOf(harness, id2);
13279
+ session2.lines = [];
13280
+ session2.codexLines = codexLines;
13281
+ session2.mockNote = mockNote;
13282
+ const cwd = meta?.cwd ?? codexLines.find((line) => line.kind === "turn_context")?.cwd;
13283
+ if (cwd !== void 0) {
13284
+ const base = pathBasename(cwd);
13285
+ if (base !== "")
13286
+ session2.label = base;
13287
+ }
13288
+ session2.dirty = true;
13289
+ notify(harness);
13290
+ return;
13291
+ }
13292
+ const lines = sourceLines.map((line) => parseTranscriptLine(line));
12759
13293
  const sessionLine = lines.find((l) => l.kind !== "unknown" && typeof l.sessionId === "string");
12760
13294
  const id = sessionLine?.sessionId ?? "__fixture__";
12761
13295
  const session = sessionOf(harness, id);
12762
13296
  session.lines = lines;
13297
+ session.codexLines = [];
12763
13298
  session.mockNote = mockNote;
12764
13299
  for (const line of lines) {
12765
13300
  if (line.kind === "unknown")
@@ -12773,6 +13308,7 @@ function createNarrativeService(options = {}) {
12773
13308
  }
12774
13309
  }
12775
13310
  session.dirty = true;
13311
+ notify(harness);
12776
13312
  }
12777
13313
  function deriveCached(harness, session) {
12778
13314
  if (!session.dirty && session.cached !== void 0)
@@ -12800,9 +13336,9 @@ function createNarrativeService(options = {}) {
12800
13336
  const overview = getOverview(harness);
12801
13337
  const first = overview.sessions[0];
12802
13338
  if (first !== void 0) {
12803
- const state2 = overview.states[first.id];
12804
- if (state2 !== void 0)
12805
- return state2;
13339
+ const state = overview.states[first.id];
13340
+ if (state !== void 0)
13341
+ return state;
12806
13342
  }
12807
13343
  return emptyState(harness, now().toISOString());
12808
13344
  }
@@ -12812,8 +13348,17 @@ function createNarrativeService(options = {}) {
12812
13348
  for (const session of sessions.values())
12813
13349
  closeSession(session);
12814
13350
  }
13351
+ pendingNotifications.clear();
13352
+ listeners.clear();
12815
13353
  }
12816
- return { handleEvent, getState, getOverview, seedFixture, close };
13354
+ function subscribe(listener) {
13355
+ if (closed)
13356
+ return () => {
13357
+ };
13358
+ listeners.add(listener);
13359
+ return () => listeners.delete(listener);
13360
+ }
13361
+ return { handleEvent, getState, getOverview, seedFixture, subscribe, close };
12817
13362
  }
12818
13363
  function lastActivityOf(session) {
12819
13364
  if (session.lastActivity !== void 0)
@@ -12826,6 +13371,13 @@ function lastActivityOf(session) {
12826
13371
  if (ts !== void 0 && ts > latest)
12827
13372
  latest = ts;
12828
13373
  }
13374
+ for (const line of session.codexLines) {
13375
+ if (line.kind === "unknown")
13376
+ continue;
13377
+ const ts = line.timestamp;
13378
+ if (ts !== void 0 && ts > latest)
13379
+ latest = ts;
13380
+ }
12829
13381
  return latest;
12830
13382
  }
12831
13383
  function isEnded(session, nowIso) {
@@ -12871,7 +13423,8 @@ function waitingMessage(payload, fallback) {
12871
13423
  return fallback;
12872
13424
  }
12873
13425
  function derive(harness, session, nowIso) {
12874
- const lines = sortedByTimestamp(session.lines);
13426
+ const codexLines = sortedCodexByTimestamp(session.codexLines);
13427
+ const lines = harness === "codex" ? adaptCodexNarrativeLines(codexLines) : sortedByTimestamp(session.lines);
12875
13428
  const timeline = deriveTodoTimeline(extractTodoSnapshots(lines));
12876
13429
  const allSteps = extractSteps(lines);
12877
13430
  const steps = allSteps.filter((s) => !TODO_TOOLS.has(s.toolName));
@@ -12964,11 +13517,12 @@ function derive(harness, session, nowIso) {
12964
13517
  todos,
12965
13518
  ...textTodos !== void 0 && textTodos.length > 0 ? { textTodos } : {},
12966
13519
  traces,
12967
- turnUsages: deriveTurnUsages(lines),
13520
+ turnUsages: harness === "codex" ? deriveCodexTurnUsages(codexLines) : deriveTurnUsages(lines),
12968
13521
  currentAction: deriveCurrentAction(steps, session.lastEvent, session.pendingHookTool),
12969
13522
  waiting: session.waiting,
12970
13523
  agentQuote: deriveAgentQuote(lines),
12971
- lastPrompt: deriveLastPrompt(lines),
13524
+ // transcript 是会话事实源;hook prompt 只在 tail 尚未读到/格式暂不可读时兜底。
13525
+ lastPrompt: deriveLastPrompt(lines, session.hookPrompt, session.hookPromptAt),
12972
13526
  updatedAt: nowIso
12973
13527
  };
12974
13528
  }
@@ -13015,6 +13569,109 @@ function sortedByTimestamp(lines) {
13015
13569
  return ta < tb ? -1 : ta > tb ? 1 : 0;
13016
13570
  });
13017
13571
  }
13572
+ function sortedCodexByTimestamp(lines) {
13573
+ return [...lines].sort((a, b) => {
13574
+ const ta = a.kind === "unknown" ? void 0 : a.timestamp;
13575
+ const tb = b.kind === "unknown" ? void 0 : b.timestamp;
13576
+ if (ta === void 0 || tb === void 0)
13577
+ return 0;
13578
+ return ta < tb ? -1 : ta > tb ? 1 : 0;
13579
+ });
13580
+ }
13581
+ function adaptCodexNarrativeLines(lines) {
13582
+ const responseTexts = new Set(lines.flatMap((line) => line.kind === "assistant" && line.source === "response_item" && line.text !== void 0 ? [line.text] : []));
13583
+ const modelByTurn = /* @__PURE__ */ new Map();
13584
+ for (const line of lines) {
13585
+ if (line.kind === "turn_context" && line.turnId !== void 0 && line.model !== void 0) {
13586
+ modelByTurn.set(line.turnId, line.model);
13587
+ }
13588
+ }
13589
+ const adapted = [];
13590
+ let activeTurnId;
13591
+ for (const line of lines) {
13592
+ if (line.kind === "task_started") {
13593
+ activeTurnId = line.turnId;
13594
+ continue;
13595
+ }
13596
+ if (line.kind === "task_complete" || line.kind === "turn_aborted") {
13597
+ if (line.turnId === void 0 || line.turnId === activeTurnId)
13598
+ activeTurnId = void 0;
13599
+ continue;
13600
+ }
13601
+ if (line.kind === "turn_context" && line.turnId !== void 0) {
13602
+ activeTurnId = line.turnId;
13603
+ continue;
13604
+ }
13605
+ if (line.kind === "user") {
13606
+ adapted.push({
13607
+ kind: "user",
13608
+ ...line.timestamp !== void 0 ? { timestamp: line.timestamp } : {},
13609
+ ...line.text !== void 0 ? { text: line.text } : {},
13610
+ toolResults: [],
13611
+ raw: { source: "codex-rollout" }
13612
+ });
13613
+ continue;
13614
+ }
13615
+ if (line.kind === "assistant") {
13616
+ if (line.text === void 0 || line.source === "event_msg" && responseTexts.has(line.text)) {
13617
+ continue;
13618
+ }
13619
+ const turnId = line.turnId ?? activeTurnId;
13620
+ const model = turnId === void 0 ? void 0 : modelByTurn.get(turnId);
13621
+ adapted.push({
13622
+ kind: "assistant",
13623
+ ...line.timestamp !== void 0 ? { timestamp: line.timestamp } : {},
13624
+ ...line.messageId !== void 0 ? { messageId: line.messageId } : {},
13625
+ ...model !== void 0 ? { model } : {},
13626
+ blocks: [{ kind: "text", text: line.text }],
13627
+ raw: { source: "codex-rollout" }
13628
+ });
13629
+ continue;
13630
+ }
13631
+ if (line.kind === "tool_call") {
13632
+ if (line.callId === void 0 || line.name === void 0)
13633
+ continue;
13634
+ const turnId = line.turnId ?? activeTurnId;
13635
+ const model = turnId === void 0 ? void 0 : modelByTurn.get(turnId);
13636
+ adapted.push({
13637
+ kind: "assistant",
13638
+ ...line.timestamp !== void 0 ? { timestamp: line.timestamp } : {},
13639
+ ...model !== void 0 ? { model } : {},
13640
+ blocks: [
13641
+ {
13642
+ kind: "tool_use",
13643
+ id: line.callId,
13644
+ name: line.name,
13645
+ input: codexToolInput(line.input)
13646
+ }
13647
+ ],
13648
+ raw: { source: "codex-rollout" }
13649
+ });
13650
+ continue;
13651
+ }
13652
+ if (line.kind === "tool_result" && line.callId !== void 0) {
13653
+ adapted.push({
13654
+ kind: "user",
13655
+ ...line.timestamp !== void 0 ? { timestamp: line.timestamp } : {},
13656
+ toolResults: [
13657
+ {
13658
+ toolUseId: line.callId,
13659
+ isError: line.isError === true,
13660
+ content: line.output
13661
+ }
13662
+ ],
13663
+ raw: { source: "codex-rollout" }
13664
+ });
13665
+ }
13666
+ }
13667
+ return sortedByTimestamp(adapted);
13668
+ }
13669
+ function codexToolInput(value) {
13670
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
13671
+ return value;
13672
+ }
13673
+ return value === void 0 ? {} : { input: value };
13674
+ }
13018
13675
  function collectResults(lines) {
13019
13676
  const results = /* @__PURE__ */ new Map();
13020
13677
  for (const line of lines) {
@@ -13081,7 +13738,15 @@ function nodeEstTokens(steps, results) {
13081
13738
  }
13082
13739
  return counted ? { estTokens: sum } : {};
13083
13740
  }
13084
- var DETAIL_KEYS = ["command", "file_path", "description", "subject", "pattern", "prompt"];
13741
+ var DETAIL_KEYS = [
13742
+ "command",
13743
+ "file_path",
13744
+ "description",
13745
+ "subject",
13746
+ "pattern",
13747
+ "prompt",
13748
+ "input"
13749
+ ];
13085
13750
  function inputDetail(input) {
13086
13751
  for (const key of DETAIL_KEYS) {
13087
13752
  const value = input[key];
@@ -13130,6 +13795,131 @@ function deriveTurnUsages(lines) {
13130
13795
  }
13131
13796
  return [...byMessage.values()];
13132
13797
  }
13798
+ var ZERO_CODEX_USAGE = {
13799
+ inputTokens: 0,
13800
+ cachedInputTokens: 0,
13801
+ cacheWriteInputTokens: 0,
13802
+ outputTokens: 0,
13803
+ reasoningOutputTokens: 0,
13804
+ totalTokens: 0
13805
+ };
13806
+ function deriveCodexTurnUsages(lines) {
13807
+ const modelByTurn = /* @__PURE__ */ new Map();
13808
+ for (const line of lines) {
13809
+ if (line.kind === "turn_context" && line.turnId !== void 0 && line.model !== void 0) {
13810
+ modelByTurn.set(line.turnId, line.model);
13811
+ }
13812
+ }
13813
+ const turns = [];
13814
+ let latestTotal;
13815
+ let active;
13816
+ function finishActive() {
13817
+ const current = active;
13818
+ if (current?.terminal === void 0)
13819
+ return;
13820
+ active = void 0;
13821
+ const delta = current.latest === void 0 ? void 0 : subtractCodexUsage(current.latest, current.baseline);
13822
+ if (delta === void 0)
13823
+ return;
13824
+ if (delta.inputTokens === 0 && delta.outputTokens === 0 && (delta.totalTokens ?? 0) === 0) {
13825
+ return;
13826
+ }
13827
+ const inputTokens = delta.inputTokens - delta.cachedInputTokens - delta.cacheWriteInputTokens;
13828
+ if (inputTokens < 0)
13829
+ return;
13830
+ const model = current.turnId === void 0 ? void 0 : modelByTurn.get(current.turnId);
13831
+ const view = {
13832
+ ...current.turnId !== void 0 ? { messageId: current.turnId } : {},
13833
+ ...model !== void 0 ? { model } : {},
13834
+ inputTokens,
13835
+ cacheCreationInputTokens: delta.cacheWriteInputTokens,
13836
+ cacheReadInputTokens: delta.cachedInputTokens,
13837
+ outputTokens: delta.outputTokens,
13838
+ inputTotalTokens: delta.inputTokens,
13839
+ ...current.terminal === "aborted" ? { status: "aborted" } : current.terminal === "incomplete" ? { status: "incomplete" } : {},
13840
+ ...current.startedAt !== void 0 ? { ts: current.startedAt } : {}
13841
+ };
13842
+ if (delta.reasoningOutputTokens !== void 0 && delta.reasoningOutputTokens <= delta.outputTokens) {
13843
+ view.reasoningOutputTokens = delta.reasoningOutputTokens;
13844
+ }
13845
+ if (delta.totalTokens !== void 0)
13846
+ view.totalTokens = delta.totalTokens;
13847
+ turns.push(view);
13848
+ }
13849
+ for (const line of lines) {
13850
+ if (line.kind === "task_started") {
13851
+ if (active !== void 0 && active.terminal === void 0)
13852
+ active.terminal = "incomplete";
13853
+ finishActive();
13854
+ active = {
13855
+ ...line.turnId !== void 0 ? { turnId: line.turnId } : {},
13856
+ ...line.timestamp !== void 0 ? { startedAt: line.timestamp } : {},
13857
+ baseline: latestTotal ?? ZERO_CODEX_USAGE
13858
+ };
13859
+ continue;
13860
+ }
13861
+ if (line.kind === "token_count") {
13862
+ const snapshot = codexUsageSnapshot(line.totalUsage);
13863
+ if (snapshot === void 0)
13864
+ continue;
13865
+ latestTotal = snapshot;
13866
+ if (active !== void 0) {
13867
+ active.latest = snapshot;
13868
+ if (active.terminal !== void 0)
13869
+ finishActive();
13870
+ }
13871
+ continue;
13872
+ }
13873
+ if (line.kind !== "task_complete" && line.kind !== "turn_aborted" || active === void 0) {
13874
+ continue;
13875
+ }
13876
+ if (line.turnId !== void 0 && active.turnId !== void 0 && line.turnId !== active.turnId) {
13877
+ continue;
13878
+ }
13879
+ active.terminal = line.kind === "turn_aborted" ? "aborted" : "complete";
13880
+ }
13881
+ if (active !== void 0 && active.terminal === void 0)
13882
+ active.terminal = "incomplete";
13883
+ finishActive();
13884
+ return turns;
13885
+ }
13886
+ function codexUsageSnapshot(usage) {
13887
+ if (usage?.inputTokens === void 0 || usage.cachedInputTokens === void 0 || usage.cacheWriteInputTokens === void 0 || usage.outputTokens === void 0) {
13888
+ return void 0;
13889
+ }
13890
+ return {
13891
+ inputTokens: usage.inputTokens,
13892
+ cachedInputTokens: usage.cachedInputTokens,
13893
+ cacheWriteInputTokens: usage.cacheWriteInputTokens,
13894
+ outputTokens: usage.outputTokens,
13895
+ ...usage.reasoningOutputTokens !== void 0 ? { reasoningOutputTokens: usage.reasoningOutputTokens } : {},
13896
+ ...usage.totalTokens !== void 0 ? { totalTokens: usage.totalTokens } : {}
13897
+ };
13898
+ }
13899
+ function subtractCodexUsage(current, baseline) {
13900
+ const inputTokens = current.inputTokens - baseline.inputTokens;
13901
+ const cachedInputTokens = current.cachedInputTokens - baseline.cachedInputTokens;
13902
+ const cacheWriteInputTokens = current.cacheWriteInputTokens - baseline.cacheWriteInputTokens;
13903
+ const outputTokens = current.outputTokens - baseline.outputTokens;
13904
+ if (inputTokens < 0 || cachedInputTokens < 0 || cacheWriteInputTokens < 0 || outputTokens < 0) {
13905
+ return void 0;
13906
+ }
13907
+ const reasoningOutputTokens = optionalUsageDelta(current.reasoningOutputTokens, baseline.reasoningOutputTokens);
13908
+ const totalTokens = optionalUsageDelta(current.totalTokens, baseline.totalTokens);
13909
+ return {
13910
+ inputTokens,
13911
+ cachedInputTokens,
13912
+ cacheWriteInputTokens,
13913
+ outputTokens,
13914
+ ...reasoningOutputTokens !== void 0 ? { reasoningOutputTokens } : {},
13915
+ ...totalTokens !== void 0 ? { totalTokens } : {}
13916
+ };
13917
+ }
13918
+ function optionalUsageDelta(current, baseline) {
13919
+ if (current === void 0 || baseline === void 0 || current < baseline)
13920
+ return void 0;
13921
+ return current - baseline;
13922
+ }
13133
13923
  function deriveCurrentAction(steps, lastEvent, pendingHookTool) {
13134
13924
  const open2 = [...steps].reverse().find((s) => s.endTs === void 0);
13135
13925
  if (open2) {
@@ -13164,7 +13954,7 @@ function deriveAgentQuote(lines) {
13164
13954
  }
13165
13955
  return null;
13166
13956
  }
13167
- function deriveLastPrompt(lines) {
13957
+ function deriveLastPrompt(lines, hookPrompt, hookPromptAt) {
13168
13958
  for (let i = lines.length - 1; i >= 0; i--) {
13169
13959
  const line = lines[i];
13170
13960
  if (line === void 0 || line.kind !== "user" || line.agentId !== void 0)
@@ -13173,9 +13963,12 @@ function deriveLastPrompt(lines) {
13173
13963
  continue;
13174
13964
  if (line.text === void 0 || line.text.trim() === "")
13175
13965
  continue;
13966
+ if (hookPrompt !== void 0 && hookPromptAt !== void 0 && line.timestamp !== void 0 && hookPromptAt > line.timestamp) {
13967
+ return hookPrompt;
13968
+ }
13176
13969
  return truncate(line.text.trim(), DETAIL_LIMIT);
13177
13970
  }
13178
- return null;
13971
+ return hookPrompt ?? null;
13179
13972
  }
13180
13973
  function truncate(text, limit) {
13181
13974
  return text.length <= limit ? text : `${text.slice(0, limit - 1)}\u2026`;
@@ -13237,96 +14030,65 @@ async function loadHarness(harness, mode) {
13237
14030
  };
13238
14031
  }
13239
14032
 
13240
- // apps/cli/dist/scan.js
13241
- init_dist();
13242
-
13243
- // apps/cli/dist/telemetry.js
13244
- import { readFileSync as readFileSync11 } from "node:fs";
14033
+ // apps/cli/dist/runtime-state.js
14034
+ import { randomBytes as randomBytes2 } from "node:crypto";
14035
+ import { closeSync, mkdirSync as mkdirSync6, openSync, readFileSync as readFileSync11, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "node:fs";
14036
+ import { homedir as homedir9 } from "node:os";
13245
14037
  import { dirname as dirname11, join as join17 } from "node:path";
13246
- import { fileURLToPath as fileURLToPath2 } from "node:url";
13247
- var DEFAULT_TELEMETRY_SERVER = "https://api.getwingman.dev";
13248
- var SEND_INTERVAL_MS = 24 * 60 * 60 * 1e3;
13249
- var SEND_TIMEOUT_MS = 3e3;
13250
- var MAX_ERROR_TYPES = 10;
13251
- function emptyState2() {
13252
- return {
13253
- scans: 0,
13254
- toggleApplies: 0,
13255
- detected: { cc: false, codex: false, dsh: false },
13256
- liveWired: { cc: false, codex: false, dsh: false },
13257
- errorTypes: /* @__PURE__ */ new Set()
13258
- };
13259
- }
13260
- var state = emptyState2();
13261
- function recordDetect(harness, installed) {
13262
- state.detected[harness] = installed;
13263
- }
13264
- function recordScan(config) {
13265
- state.scans += 1;
13266
- state.detected[config.harness] = true;
13267
- state.liveWired[config.harness] = config.capabilities.live !== "none";
13268
- }
13269
- function recordHookInstall(harness) {
13270
- state.liveWired[harness] = true;
13271
- }
13272
- function recordApply() {
13273
- state.toggleApplies += 1;
13274
- }
13275
- function recordError(error) {
13276
- if (state.errorTypes.size >= MAX_ERROR_TYPES)
13277
- return;
13278
- state.errorTypes.add(error instanceof Error ? error.constructor.name : typeof error);
14038
+ function createHookToken() {
14039
+ return randomBytes2(32).toString("base64url");
13279
14040
  }
13280
- function buildPayload(options) {
13281
- return {
13282
- app_version: options.version,
13283
- platform: process.platform,
13284
- arch: process.arch,
13285
- harnesses: { ...state.detected },
13286
- counts: {
13287
- scans: state.scans,
13288
- live_wired: Object.values(state.liveWired).filter(Boolean).length,
13289
- toggle_applies: state.toggleApplies
13290
- },
13291
- license_state: options.licenseState,
13292
- error_types: [...state.errorTypes].sort()
13293
- };
13294
- }
13295
- function appVersion() {
13296
- if (typeof __WINGMAN_VERSION__ === "string")
13297
- return __WINGMAN_VERSION__;
14041
+ function writeServeRuntimeState(port, token, options = {}) {
14042
+ const warn = options.warn ?? (() => {
14043
+ });
14044
+ const startedAtMs = (options.now ?? Date.now)();
14045
+ const pid = options.pid ?? process.pid;
14046
+ const suffix = randomBytes2(8).toString("base64url");
14047
+ const owner = `serve-${startedAtMs}-${pid}-${suffix}`;
14048
+ const path = join17(options.homeDir ?? homedir9(), ".wingman", "state", "serve", `${owner}.json`);
14049
+ const tempPath = join17(dirname11(path), `.${owner}.tmp`);
14050
+ const contents = Buffer.from(`${JSON.stringify({ version: 2, owner, pid, port, startedAtMs, token })}
14051
+ `, "utf8");
14052
+ let fd;
13298
14053
  try {
13299
- const pkgPath = join17(dirname11(fileURLToPath2(import.meta.url)), "..", "package.json");
13300
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
13301
- return typeof pkg.version === "string" ? pkg.version : "unknown";
13302
- } catch {
13303
- return "unknown";
14054
+ mkdirSync6(dirname11(path), { recursive: true, mode: 448 });
14055
+ fd = openSync(tempPath, "wx", 384);
14056
+ writeFileSync7(fd, contents);
14057
+ closeSync(fd);
14058
+ fd = void 0;
14059
+ renameSync3(tempPath, path);
14060
+ return { path, contents };
14061
+ } catch (error) {
14062
+ if (fd !== void 0) {
14063
+ try {
14064
+ closeSync(fd);
14065
+ } catch {
14066
+ }
14067
+ }
14068
+ try {
14069
+ rmSync4(tempPath, { force: true });
14070
+ } catch {
14071
+ }
14072
+ warn(`Codex hook \u8FD0\u884C\u6001\u5199\u5165\u5931\u8D25,\u9762\u677F\u7EE7\u7EED\u8FD0\u884C:${errorMessage(error)}`);
14073
+ return void 0;
13304
14074
  }
13305
14075
  }
13306
- async function sendMetrics(payload, baseUrl) {
13307
- const base = baseUrl ?? process.env.WINGMAN_TELEMETRY_SERVER ?? DEFAULT_TELEMETRY_SERVER;
14076
+ function removeServeRuntimeState(registration) {
14077
+ if (registration === void 0)
14078
+ return;
13308
14079
  try {
13309
- await fetch(`${base}/metrics`, {
13310
- method: "POST",
13311
- headers: { "content-type": "application/json" },
13312
- body: JSON.stringify(payload),
13313
- signal: AbortSignal.timeout(SEND_TIMEOUT_MS)
13314
- });
14080
+ const current = readFileSync11(registration.path);
14081
+ if (current.equals(registration.contents))
14082
+ rmSync4(registration.path, { force: true });
13315
14083
  } catch {
13316
14084
  }
13317
14085
  }
13318
- function startTelemetry(options = {}) {
13319
- const send = () => {
13320
- const licenseState = licenseStatus(options.homeDir === void 0 ? {} : { homeDir: options.homeDir }).state;
13321
- void sendMetrics(buildPayload({ version: appVersion(), licenseState }), options.baseUrl);
13322
- };
13323
- send();
13324
- const timer = setInterval(send, options.intervalMs ?? SEND_INTERVAL_MS);
13325
- timer.unref();
13326
- return () => clearInterval(timer);
14086
+ function errorMessage(error) {
14087
+ return error instanceof Error ? error.message : String(error);
13327
14088
  }
13328
14089
 
13329
14090
  // apps/cli/dist/scan.js
14091
+ init_dist();
13330
14092
  async function scanValidated(adapter, options) {
13331
14093
  const config = await adapter.scan(options);
13332
14094
  const errors = validateUnifiedConfig(config);
@@ -13334,12 +14096,12 @@ async function scanValidated(adapter, options) {
13334
14096
  throw new Error(`${adapter.harness} \u7684 scan \u8F93\u51FA\u4E0D\u7B26\u5408 UnifiedConfig \u5408\u540C:
13335
14097
  - ${errors.join("\n- ")}`);
13336
14098
  }
13337
- recordScan(config);
13338
14099
  return config;
13339
14100
  }
13340
14101
 
13341
14102
  // apps/cli/dist/server.js
13342
14103
  init_dist();
14104
+ import { createHmac, timingSafeEqual } from "node:crypto";
13343
14105
  import { readFile as readFile3 } from "node:fs/promises";
13344
14106
  import { createServer } from "node:http";
13345
14107
  import { extname, resolve as resolve5, sep as sep2 } from "node:path";
@@ -13392,9 +14154,9 @@ function residentTotal(config) {
13392
14154
  }
13393
14155
  return counted ? { kind, value: sum } : void 0;
13394
14156
  }
13395
- function stepToolNames(state2) {
14157
+ function stepToolNames(state) {
13396
14158
  const names = [];
13397
- for (const trace of state2.traces) {
14159
+ for (const trace of state.traces) {
13398
14160
  for (const node of trace.nodes) {
13399
14161
  for (const step of node.steps)
13400
14162
  names.push(step.toolName);
@@ -13402,23 +14164,23 @@ function stepToolNames(state2) {
13402
14164
  }
13403
14165
  return names;
13404
14166
  }
13405
- function deriveSuggestions(state2, config, nowMs) {
14167
+ function deriveSuggestions(state, config, nowMs) {
13406
14168
  const suggestions = [];
13407
- if (state2.waiting !== null) {
13408
- const sinceMs = Date.parse(state2.waiting.since);
14169
+ if (state.waiting !== null) {
14170
+ const sinceMs = Date.parse(state.waiting.since);
13409
14171
  if (Number.isFinite(sinceMs) && nowMs - sinceMs > WAITING_TIMEOUT_MS) {
13410
14172
  suggestions.push({
13411
14173
  id: "waiting-timeout",
13412
14174
  rule: "waiting-timeout",
13413
- text: `\u5DF2\u7B49\u5F85 ${fmtDuration(nowMs - sinceMs)}:${state2.waiting.message}`,
13414
- source: `\u6765\u6E90:${state2.waiting.kind === "permission" ? "PermissionRequest" : "Notification"} \u4E8B\u4EF6,${state2.waiting.since} \u8D77\u672A\u51B3;\u9608\u503C 60s`
14175
+ text: `\u5DF2\u7B49\u5F85 ${fmtDuration(nowMs - sinceMs)}:${state.waiting.message}`,
14176
+ source: `\u6765\u6E90:${state.waiting.kind === "permission" ? "PermissionRequest" : "Notification"} \u4E8B\u4EF6,${state.waiting.since} \u8D77\u672A\u51B3;\u9608\u503C 60s`
13415
14177
  });
13416
14178
  }
13417
14179
  }
13418
14180
  if (config !== void 0) {
13419
14181
  const refSources = [
13420
- ...state2.lastPrompt !== null ? [{ file: "\u7528\u6237\u63D0\u793A", text: state2.lastPrompt }] : [],
13421
- ...state2.agentQuote !== null ? [{ file: "agent \u81EA\u8FF0", text: state2.agentQuote }] : []
14182
+ ...state.lastPrompt !== null ? [{ file: "\u7528\u6237\u63D0\u793A", text: state.lastPrompt }] : [],
14183
+ ...state.agentQuote !== null ? [{ file: "agent \u81EA\u8FF0", text: state.agentQuote }] : []
13422
14184
  ];
13423
14185
  if (refSources.length > 0) {
13424
14186
  for (const items of Object.values(config.slots)) {
@@ -13440,8 +14202,9 @@ function deriveSuggestions(state2, config, nowMs) {
13440
14202
  }
13441
14203
  }
13442
14204
  }
13443
- const toolNames = stepToolNames(state2);
13444
- if (toolNames.length > 0 && state2.turnUsages.length > 0) {
14205
+ const toolNames = stepToolNames(state);
14206
+ const hasCompletedTurn = state.turnUsages.some((turn) => turn.status === void 0);
14207
+ if (toolNames.length > 0 && hasCompletedTurn) {
13445
14208
  for (const item of config.slots.tools) {
13446
14209
  if (!item.enabled || !item.toggleable)
13447
14210
  continue;
@@ -13465,9 +14228,9 @@ function deriveSuggestions(state2, config, nowMs) {
13465
14228
  });
13466
14229
  }
13467
14230
  }
13468
- const lastTurn = state2.turnUsages[state2.turnUsages.length - 1];
14231
+ const lastTurn = state.turnUsages[state.turnUsages.length - 1];
13469
14232
  const resident = residentTotal(config);
13470
- if (lastTurn !== void 0 && resident !== void 0) {
14233
+ if (lastTurn?.contextTotal !== void 0 && resident !== void 0) {
13471
14234
  const ctx = lastTurn.contextTotal.value;
13472
14235
  const history = ctx - resident.value;
13473
14236
  if (ctx >= HISTORY_BLOAT_MIN_CONTEXT && history / ctx > HISTORY_BLOAT_RATIO) {
@@ -13500,13 +14263,16 @@ var CONTENT_TYPES = {
13500
14263
  ".woff2": "font/woff2"
13501
14264
  };
13502
14265
  var ANATOMY_CACHE_TTL_MS = 3e4;
14266
+ var HOOK_IDENTITY_PATH = "/api/hook-target";
14267
+ var HOOK_TOKEN_HEADER = "x-wingman-hook-token";
14268
+ var HOOK_PROOF_HEADER = "x-wingman-hook-proof";
14269
+ var PANEL_ACTION_HEADER = "x-wingman-panel-action";
13503
14270
  function createWingmanServer(options) {
13504
14271
  const warn = options.warn ?? ((message) => console.error(message));
13505
14272
  const runtime = { anatomy: null };
13506
14273
  return createServer((req, res) => {
13507
14274
  handle(req, res, options, warn, runtime).catch((error) => {
13508
- recordError(error);
13509
- warn(`[wingman] \u8BF7\u6C42\u5904\u7406\u5931\u8D25:${errorMessage(error)}`);
14275
+ warn(`[wingman] \u8BF7\u6C42\u5904\u7406\u5931\u8D25:${errorMessage2(error)}`);
13510
14276
  if (!res.headersSent)
13511
14277
  sendText2(res, 500, "internal error");
13512
14278
  else
@@ -13532,7 +14298,28 @@ async function handle(req, res, options, warn, runtime) {
13532
14298
  return;
13533
14299
  }
13534
14300
  }
14301
+ if (options.hookToken && url.pathname === HOOK_IDENTITY_PATH) {
14302
+ if (req.method !== "GET") {
14303
+ sendText2(res, 405, `method not allowed:${HOOK_IDENTITY_PATH} \u53EA\u63A5\u53D7 GET`);
14304
+ return;
14305
+ }
14306
+ const nonce = url.searchParams.get("nonce");
14307
+ if (nonce === null || !/^[A-Za-z0-9_-]{16,128}$/.test(nonce)) {
14308
+ sendText2(res, 400, "nonce \u9700\u8981 16-128 \u4F4D base64url \u5B57\u7B26");
14309
+ return;
14310
+ }
14311
+ res.writeHead(204, {
14312
+ [HOOK_PROOF_HEADER]: hookProof(options.hookToken, nonce),
14313
+ "cache-control": "no-store"
14314
+ });
14315
+ res.end();
14316
+ return;
14317
+ }
13535
14318
  if (options.receiver && url.pathname === "/hook") {
14319
+ if (url.searchParams.get("harness") === "codex" && options.hookToken !== void 0 && !sameToken(options.hookToken, req.headers[HOOK_TOKEN_HEADER])) {
14320
+ sendText2(res, 403, "Codex hook token \u65E0\u6548");
14321
+ return;
14322
+ }
13536
14323
  options.receiver.handleHook(req, res);
13537
14324
  return;
13538
14325
  }
@@ -13540,6 +14327,14 @@ async function handle(req, res, options, warn, runtime) {
13540
14327
  options.receiver.handleEvents(req, res);
13541
14328
  return;
13542
14329
  }
14330
+ if (options.narrative && url.pathname === "/api/narrative/events") {
14331
+ if (req.method !== "GET") {
14332
+ sendText2(res, 405, "method not allowed:/api/narrative/events \u53EA\u63A5\u53D7 GET");
14333
+ return;
14334
+ }
14335
+ handleNarrativeEvents(req, res, options.narrative);
14336
+ return;
14337
+ }
13543
14338
  if (url.pathname === "/api/action/plan" && req.method === "POST") {
13544
14339
  await handleActionPlan(req, res, options);
13545
14340
  return;
@@ -13552,13 +14347,24 @@ async function handle(req, res, options, warn, runtime) {
13552
14347
  await handleLicenseInstall(req, res, options);
13553
14348
  return;
13554
14349
  }
14350
+ if (url.pathname === "/api/license/refresh") {
14351
+ if (req.method !== "POST") {
14352
+ sendText2(res, 405, "method not allowed:/api/license/refresh \u53EA\u63A5\u53D7 POST");
14353
+ return;
14354
+ }
14355
+ if (req.headers[PANEL_ACTION_HEADER] !== "refresh-license") {
14356
+ sendJson(res, 403, { error: "\u5237\u65B0\u6388\u6743\u5FC5\u987B\u7531 Wingman \u9762\u677F\u4E2D\u7684\u6309\u94AE\u89E6\u53D1" });
14357
+ return;
14358
+ }
14359
+ await handleLicenseRefresh(res, options);
14360
+ return;
14361
+ }
13555
14362
  if (req.method !== "GET") {
13556
14363
  sendText2(res, 405, "method not allowed");
13557
14364
  return;
13558
14365
  }
13559
14366
  if (url.pathname === "/api/license") {
13560
- const { state: state2, detail } = licenseStatus(options.licenseOptions);
13561
- sendJson(res, 200, { state: state2, detail });
14367
+ sendJson(res, 200, publicLicenseStatus(licenseStatus(options.licenseOptions)));
13562
14368
  return;
13563
14369
  }
13564
14370
  if (url.pathname === "/api/narrative") {
@@ -13578,9 +14384,9 @@ async function handle(req, res, options, warn, runtime) {
13578
14384
  const config = (await cachedAnatomy(options, warn, runtime)).find((c) => c.harness === harness);
13579
14385
  const nowMs = Date.now();
13580
14386
  const states = {};
13581
- for (const [id, state2] of Object.entries(overview.states)) {
13582
- const suggestions = deriveSuggestions(state2, config, nowMs);
13583
- states[id] = suggestions.length > 0 ? { ...state2, suggestions } : state2;
14387
+ for (const [id, state] of Object.entries(overview.states)) {
14388
+ const suggestions = deriveSuggestions(state, config, nowMs);
14389
+ states[id] = suggestions.length > 0 ? { ...state, suggestions } : state;
13584
14390
  }
13585
14391
  overview.states = states;
13586
14392
  }
@@ -13599,6 +14405,40 @@ async function handle(req, res, options, warn, runtime) {
13599
14405
  }
13600
14406
  await serveStatic(url.pathname, res, options.panelDistDir);
13601
14407
  }
14408
+ function hookProof(token, nonce) {
14409
+ return createHmac("sha256", token).update(nonce).digest("base64url");
14410
+ }
14411
+ function sameToken(expected, actual) {
14412
+ if (typeof actual !== "string")
14413
+ return false;
14414
+ const left = Buffer.from(expected, "utf8");
14415
+ const right = Buffer.from(actual, "utf8");
14416
+ return left.length === right.length && timingSafeEqual(left, right);
14417
+ }
14418
+ function handleNarrativeEvents(_req, res, narrative) {
14419
+ res.writeHead(200, {
14420
+ "content-type": "text/event-stream",
14421
+ "cache-control": "no-cache",
14422
+ connection: "keep-alive"
14423
+ });
14424
+ res.on("error", () => {
14425
+ });
14426
+ res.write(": connected\n\n");
14427
+ const unsubscribe = narrative.subscribe((harness) => {
14428
+ if (!res.destroyed)
14429
+ res.write(`data: ${JSON.stringify({ harness })}
14430
+
14431
+ `);
14432
+ });
14433
+ const heartbeat = setInterval(() => {
14434
+ if (!res.destroyed)
14435
+ res.write(": heartbeat\n\n");
14436
+ }, 15e3);
14437
+ res.once("close", () => {
14438
+ clearInterval(heartbeat);
14439
+ unsubscribe();
14440
+ });
14441
+ }
13602
14442
  async function cachedAnatomy(options, warn, runtime) {
13603
14443
  const nowMs = Date.now();
13604
14444
  if (runtime.anatomy !== null && nowMs - runtime.anatomy.at < ANATOMY_CACHE_TTL_MS) {
@@ -13614,11 +14454,9 @@ async function scanAll(harnesses, warn) {
13614
14454
  let installed;
13615
14455
  try {
13616
14456
  const detected = await adapter.detect(scanOptions);
13617
- recordDetect(harness, detected.installed);
13618
14457
  installed = detected.installed;
13619
14458
  } catch (error) {
13620
- recordError(error);
13621
- warn(`[wingman] ${harness} detect \u5931\u8D25,\u5DF2\u8DF3\u8FC7:${errorMessage(error)}`);
14459
+ warn(`[wingman] ${harness} detect \u5931\u8D25,\u5DF2\u8DF3\u8FC7:${errorMessage2(error)}`);
13622
14460
  continue;
13623
14461
  }
13624
14462
  if (!installed)
@@ -13626,8 +14464,7 @@ async function scanAll(harnesses, warn) {
13626
14464
  try {
13627
14465
  configs.push(await scanValidated(adapter, scanOptions));
13628
14466
  } catch (error) {
13629
- recordError(error);
13630
- warn(`[wingman] ${harness} \u626B\u63CF\u5931\u8D25,\u964D\u7EA7\u663E\u793A:${errorMessage(error)}`);
14467
+ warn(`[wingman] ${harness} \u626B\u63CF\u5931\u8D25,\u964D\u7EA7\u663E\u793A:${errorMessage2(error)}`);
13631
14468
  configs.push(degradedScanConfig(harness, error));
13632
14469
  }
13633
14470
  }
@@ -13664,7 +14501,7 @@ function degradedScanConfig(harness, error) {
13664
14501
  };
13665
14502
  }
13666
14503
  function errorSummary(error) {
13667
- const first = errorMessage(error).split("\n", 1)[0] ?? "";
14504
+ const first = errorMessage2(error).split("\n", 1)[0] ?? "";
13668
14505
  const scrubbed = first.replace(/(?:[A-Za-z]:)?[\\/][^\s'"()|,;]+/g, "\u2026").trim();
13669
14506
  return scrubbed.length <= 120 ? scrubbed : `${scrubbed.slice(0, 119)}\u2026`;
13670
14507
  }
@@ -13688,7 +14525,7 @@ async function handleActionPlan(req, res, options) {
13688
14525
  const plan = await loaded.adapter.planAction(action, loaded.scanOptions);
13689
14526
  sendJson(res, 200, plan);
13690
14527
  } catch (error) {
13691
- sendJson(res, 400, { error: errorMessage(error) });
14528
+ sendJson(res, 400, { error: errorMessage2(error) });
13692
14529
  }
13693
14530
  }
13694
14531
  async function handleActionApply(req, res, options, runtime) {
@@ -13718,13 +14555,11 @@ async function handleActionApply(req, res, options, runtime) {
13718
14555
  try {
13719
14556
  const result = await loaded.adapter.applyAction(plan, loaded.scanOptions);
13720
14557
  if (result.ok) {
13721
- recordApply();
13722
14558
  runtime.anatomy = null;
13723
14559
  }
13724
14560
  sendJson(res, 200, result);
13725
14561
  } catch (error) {
13726
- recordError(error);
13727
- sendJson(res, 400, { error: errorMessage(error) });
14562
+ sendJson(res, 400, { error: errorMessage2(error) });
13728
14563
  }
13729
14564
  }
13730
14565
  var LICENSE_BODY_LIMIT = 64 * 1024;
@@ -13739,7 +14574,44 @@ async function handleLicenseInstall(req, res, options) {
13739
14574
  sendJson(res, 400, { error: result.error });
13740
14575
  return;
13741
14576
  }
13742
- sendJson(res, 200, { state: result.status.state, detail: result.status.detail });
14577
+ sendJson(res, 200, publicLicenseStatus(result.status));
14578
+ }
14579
+ async function handleLicenseRefresh(res, options) {
14580
+ const homeDir = options.licenseRefreshOptions?.homeDir ?? options.licenseOptions?.homeDir;
14581
+ const publicKeyPem = options.licenseRefreshOptions?.publicKeyPem ?? options.licenseOptions?.publicKeyPem;
14582
+ const now = options.licenseRefreshOptions?.now ?? options.licenseOptions?.now;
14583
+ const refreshOptions = {
14584
+ ...options.licenseRefreshOptions,
14585
+ ...homeDir === void 0 ? {} : { homeDir },
14586
+ ...publicKeyPem === void 0 ? {} : { publicKeyPem },
14587
+ ...now === void 0 ? {} : { now }
14588
+ };
14589
+ const result = await refreshLicense(refreshOptions);
14590
+ if (!result.ok) {
14591
+ sendJson(res, 400, {
14592
+ error: result.message,
14593
+ canSubscribe: result.reason === "inactive"
14594
+ });
14595
+ return;
14596
+ }
14597
+ const statusOptions = {
14598
+ ...options.licenseOptions,
14599
+ ...homeDir === void 0 ? {} : { homeDir },
14600
+ ...publicKeyPem === void 0 ? {} : { publicKeyPem },
14601
+ ...now === void 0 ? {} : { now }
14602
+ };
14603
+ sendJson(res, 200, {
14604
+ ...publicLicenseStatus(licenseStatus(statusOptions)),
14605
+ message: result.message
14606
+ });
14607
+ }
14608
+ function publicLicenseStatus(status) {
14609
+ const body = { state: status.state, detail: status.detail };
14610
+ if (status.claims) {
14611
+ body.plan = status.claims.plan;
14612
+ body.exp = status.claims.exp;
14613
+ }
14614
+ return body;
13743
14615
  }
13744
14616
  function readTextBody(req, maxBytes) {
13745
14617
  return new Promise((resolveBody) => {
@@ -13830,7 +14702,7 @@ function sendText2(res, status, body) {
13830
14702
  res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
13831
14703
  res.end(body);
13832
14704
  }
13833
- function errorMessage(error) {
14705
+ function errorMessage2(error) {
13834
14706
  return error instanceof Error ? error.message : String(error);
13835
14707
  }
13836
14708
 
@@ -13914,6 +14786,7 @@ ${counts}
13914
14786
  }
13915
14787
  async function runServe(args) {
13916
14788
  const { port, fixtures, panelDist } = parseServeArgs(args);
14789
+ const hookToken = createHookToken();
13917
14790
  const harnesses = [];
13918
14791
  for (const id of HARNESS_IDS) {
13919
14792
  try {
@@ -13924,7 +14797,7 @@ async function runServe(args) {
13924
14797
  `);
13925
14798
  }
13926
14799
  }
13927
- const panelDistDir = panelDist === void 0 ? resolve6(dirname12(fileURLToPath3(import.meta.url)), "../../panel/dist") : resolve6(panelDist);
14800
+ const panelDistDir = panelDist === void 0 ? resolve6(dirname12(fileURLToPath2(import.meta.url)), "../../panel/dist") : resolve6(panelDist);
13928
14801
  const narrative = createNarrativeService({ followTranscripts: !fixtures });
13929
14802
  if (fixtures) {
13930
14803
  try {
@@ -13941,13 +14814,28 @@ async function runServe(args) {
13941
14814
  panelDistDir,
13942
14815
  receiver: createReceiver({ onEvent: narrative.handleEvent }),
13943
14816
  readonlyActions: fixtures,
13944
- narrative
14817
+ narrative,
14818
+ hookToken
13945
14819
  });
13946
14820
  const actualPort = await listen(server, port);
13947
- startTelemetry();
14821
+ const runtimeRegistration = fixtures ? void 0 : writeServeRuntimeState(actualPort, hookToken, {
14822
+ warn: (message) => process.stderr.write(`[wingman] ${message}
14823
+ `)
14824
+ });
14825
+ const cleanupRuntime = () => removeServeRuntimeState(runtimeRegistration);
14826
+ server.once("close", cleanupRuntime);
14827
+ process.once("exit", cleanupRuntime);
14828
+ process.once("SIGINT", () => {
14829
+ cleanupRuntime();
14830
+ process.exit(130);
14831
+ });
14832
+ process.once("SIGTERM", () => {
14833
+ cleanupRuntime();
14834
+ process.exit(143);
14835
+ });
13948
14836
  const mode = fixtures ? "(fixtures \u6A21\u5F0F:\u6570\u636E\u6765\u81EA @wingman/fixtures \u6837\u4F8B,\u975E\u672C\u673A\u914D\u7F6E)" : "";
13949
14837
  process.stdout.write(`Wingman \u9762\u677F:http://127.0.0.1:${actualPort}${mode}
13950
- \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
14838
+ \u53EA\u7ED1 127.0.0.1,\u9762\u677F\u4E0E\u6570\u636E\u90FD\u5728\u672C\u673A;\u96F6\u9065\u6D4B\u2014\u2014\u552F\u4E00\u51FA\u95E8\u7684\u6570\u636E\u662F\u4F60\u5728\u9762\u677F\u9010\u6B21\u4E3B\u52A8\u5206\u4EAB\u7684\u6837\u672C,\u53E3\u5F84 getwingman.dev/data\u3002Ctrl+C \u9000\u51FA\u3002
13951
14839
  `);
13952
14840
  return 0;
13953
14841
  }
@@ -13975,10 +14863,13 @@ async function runHook(args) {
13975
14863
  const { adapter } = await loadHarness(harness, { fixtures: false });
13976
14864
  if (action === "install") {
13977
14865
  await adapter.installHooks(port);
13978
- recordHookInstall(harness);
13979
- process.stdout.write(`wingman:${harness} \u5B9E\u51B5\u63A5\u7EBF\u5B8C\u6210(POST http://127.0.0.1:${port}/hook?harness=${harness})\u3002
14866
+ if (harness === "codex") {
14867
+ process.stdout.write("wingman:codex \u914D\u7F6E\u5B8C\u6210\uFF1B\u9996\u6B21\u63A5\u7EBF\u8BF7\u5728 Codex \u8F93\u5165 /hooks\uFF0C\u5C06 Wingman hooks \u8BBE\u4E3A Trusted + Enabled\u3002\u6388\u6743\u540E\u65B0\u4E8B\u4EF6\u5E94\u8FDB\u5165 Wingman\uFF1B\u82E5\u5F53\u524D\u4EFB\u52A1\u4ECD\u65E0\u4E8B\u4EF6\uFF0C\u8BF7\u5B8C\u6574\u9000\u51FA\u91CD\u5F00\u540E\u7528\u65B0\u4EFB\u52A1\u9A8C\u8BC1\u3002\n\u5347\u7EA7\u65F6\u53EA\u8981\u53D7\u7BA1 command \u4FDD\u6301\u4E0D\u53D8\uFF0C\u65E0\u9700\u91CD\u590D\u4FE1\u4EFB\uFF1Bcommand \u53D8\u5316\u6216\u53D7\u7BA1\u5757\u6B8B\u7F3A/\u88AB\u4FEE\u6539\u65F6\u4F8B\u5916\u3002\n\u5199\u524D\u5DF2\u5907\u4EFD;\u91CD\u590D\u6267\u884C\u5E42\u7B49;\u89E3\u7EBF:wingman hook uninstall codex\n");
14868
+ } else {
14869
+ process.stdout.write(`wingman:${harness} \u5B9E\u51B5\u63A5\u7EBF\u5B8C\u6210(POST http://127.0.0.1:${port}/hook?harness=${harness})\u3002
13980
14870
  \u5199\u524D\u5DF2\u5907\u4EFD;\u91CD\u590D\u6267\u884C\u5E42\u7B49;\u89E3\u7EBF:wingman hook uninstall ${harness}
13981
14871
  `);
14872
+ }
13982
14873
  } else {
13983
14874
  await adapter.uninstallHooks();
13984
14875
  process.stdout.write(`wingman:${harness} \u5DF2\u89E3\u7EBF,\u914D\u7F6E\u5DF2\u6062\u590D\u3002