harness-wingman 0.3.0 → 0.4.1

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";
@@ -11767,7 +11936,7 @@ function createReceiver(options = {}) {
11767
11936
  }
11768
11937
  accept({ harness, received_at: (/* @__PURE__ */ new Date()).toISOString(), payload: parsed });
11769
11938
  sendText(res, 200, "ok");
11770
- }, () => sendText(res, 400, "\u8BFB\u53D6\u8BF7\u6C42\u4F53\u5931\u8D25"));
11939
+ }, (error) => sendText(res, 400, error instanceof Error ? error.message : "\u8BFB\u53D6\u8BF7\u6C42\u4F53\u5931\u8D25"));
11771
11940
  }
11772
11941
  function handleEvents(req, res) {
11773
11942
  if (req.method !== "GET") {
@@ -11824,10 +11993,20 @@ function isHookPayload(value) {
11824
11993
  const name = value.hook_event_name;
11825
11994
  return typeof name === "string" && name.length > 0;
11826
11995
  }
11996
+ var HOOK_BODY_LIMIT = 1024 * 1024;
11827
11997
  function readBody(req) {
11828
11998
  return new Promise((resolve7, reject) => {
11829
11999
  const chunks = [];
11830
- req.on("data", (chunk) => chunks.push(chunk));
12000
+ let size = 0;
12001
+ req.on("data", (chunk) => {
12002
+ size += chunk.length;
12003
+ if (size > HOOK_BODY_LIMIT) {
12004
+ reject(new Error("body \u8D85\u8FC7 1MB \u4E0A\u9650"));
12005
+ req.destroy();
12006
+ return;
12007
+ }
12008
+ chunks.push(chunk);
12009
+ });
11831
12010
  req.on("end", () => resolve7(Buffer.concat(chunks).toString("utf8")));
11832
12011
  req.on("error", reject);
11833
12012
  });
@@ -11886,7 +12065,7 @@ function verifyEntitlement(raw, now, publicKeyPem) {
11886
12065
  let claims;
11887
12066
  try {
11888
12067
  const decoded = JSON.parse(Buffer.from(parsed.token, "base64url").toString("utf8"));
11889
- 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))) {
11890
12069
  return { state: "invalid", detail: "license \u8F7D\u8377\u5B57\u6BB5\u4E0D\u5B8C\u6574" };
11891
12070
  }
11892
12071
  claims = { sub: decoded.sub, plan: decoded.plan, iat: decoded.iat, exp: decoded.exp };
@@ -11894,10 +12073,22 @@ function verifyEntitlement(raw, now, publicKeyPem) {
11894
12073
  return { state: "invalid", detail: "license \u8F7D\u8377\u65E0\u6CD5\u89E3\u7801" };
11895
12074
  }
11896
12075
  const exp = Date.parse(claims.exp);
11897
- const graceEnd = exp + OFFLINE_GRACE_DAYS * 24 * 60 * 60 * 1e3;
12076
+ const isTrial = claims.plan === "trial";
11898
12077
  if (now.getTime() < exp) {
11899
- 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
+ };
11900
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;
11901
12092
  if (now.getTime() < graceEnd) {
11902
12093
  return {
11903
12094
  state: "grace",
@@ -11955,7 +12146,8 @@ async function refreshLicense(options = {}) {
11955
12146
  } catch {
11956
12147
  return {
11957
12148
  ok: false,
11958
- 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"
11959
12151
  };
11960
12152
  }
11961
12153
  let response;
@@ -11967,43 +12159,91 @@ async function refreshLicense(options = {}) {
11967
12159
  signal: AbortSignal.timeout(15e3)
11968
12160
  });
11969
12161
  } catch {
11970
- 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
+ };
11971
12167
  }
11972
12168
  if (!response.ok) {
11973
12169
  let detail = `\u7B7E\u53D1\u7AEF\u8FD4\u56DE HTTP ${response.status}`;
12170
+ let inactive = false;
11974
12171
  try {
11975
12172
  const body = await response.json();
11976
12173
  if (typeof body.error === "string")
11977
12174
  detail = body.error;
12175
+ inactive = body.reason === "inactive";
11978
12176
  } catch {
11979
12177
  }
11980
- 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
+ };
11981
12183
  }
11982
12184
  let entitlement;
11983
12185
  try {
11984
12186
  entitlement = await response.json();
11985
12187
  } catch {
11986
- 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
+ };
11987
12193
  }
11988
12194
  if (typeof entitlement.token !== "string" || typeof entitlement.signature !== "string") {
11989
- 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
+ };
11990
12200
  }
11991
12201
  const next = `${JSON.stringify({ token: entitlement.token, signature: entitlement.signature })}
11992
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
+ }
11993
12229
  const tmpPath = join3(dirname2(licensePath), `.license.json.tmp-${process.pid}`);
11994
12230
  try {
11995
12231
  writeFileSync2(tmpPath, next);
11996
12232
  renameSync2(tmpPath, licensePath);
11997
12233
  } catch (error) {
11998
12234
  const message = error instanceof Error ? error.message : String(error);
11999
- return { ok: false, message: `\u5199\u5165 ${licensePath} \u5931\u8D25:${message}` };
12235
+ return { ok: false, message: `\u5199\u5165 ${licensePath} \u5931\u8D25:${message}`, reason: "write" };
12000
12236
  }
12001
12237
  return { ok: true, message: `license \u5DF2\u7EED\u671F${expSuffix(entitlement.token)}` };
12002
12238
  }
12003
12239
  function expSuffix(token) {
12004
12240
  try {
12005
12241
  const claims = JSON.parse(Buffer.from(token, "base64url").toString("utf8"));
12006
- 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}`;
12007
12247
  } catch {
12008
12248
  return "";
12009
12249
  }
@@ -12104,6 +12344,246 @@ function isInProgressAt(entry, ts) {
12104
12344
  return entry.spans.some((span) => span.status === "in_progress" && span.from !== void 0 && span.from <= ts && (span.to === void 0 || ts < span.to));
12105
12345
  }
12106
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
+
12107
12587
  // packages/narrative/dist/parse.js
12108
12588
  function parseTranscriptLine(line) {
12109
12589
  const trimmed = line.trim();
@@ -12115,7 +12595,7 @@ function parseTranscriptLine(line) {
12115
12595
  } catch {
12116
12596
  return { kind: "unknown", reason: "JSON \u89E3\u6790\u5931\u8D25", rawText: line };
12117
12597
  }
12118
- if (!isRecord2(value))
12598
+ if (!isRecord3(value))
12119
12599
  return { kind: "unknown", reason: "\u884C\u4E0D\u662F JSON \u5BF9\u8C61", rawText: line };
12120
12600
  const type = value.type;
12121
12601
  if (typeof type !== "string") {
@@ -12130,7 +12610,7 @@ function parseTranscriptLine(line) {
12130
12610
  const attachment = {
12131
12611
  kind: "attachment",
12132
12612
  ...common,
12133
- attachment: isRecord2(value.attachment) ? value.attachment : {}
12613
+ attachment: isRecord3(value.attachment) ? value.attachment : {}
12134
12614
  };
12135
12615
  return attachment;
12136
12616
  }
@@ -12154,14 +12634,14 @@ function extractCommon(raw) {
12154
12634
  }
12155
12635
  function parseUser(raw, common) {
12156
12636
  const line = { kind: "user", ...common, toolResults: [] };
12157
- const message = isRecord2(raw.message) ? raw.message : void 0;
12637
+ const message = isRecord3(raw.message) ? raw.message : void 0;
12158
12638
  const content = message?.content;
12159
12639
  if (typeof content === "string") {
12160
12640
  line.text = content;
12161
12641
  } else if (Array.isArray(content)) {
12162
12642
  const texts = [];
12163
12643
  for (const block of content) {
12164
- if (!isRecord2(block))
12644
+ if (!isRecord3(block))
12165
12645
  continue;
12166
12646
  if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
12167
12647
  const result = {
@@ -12184,20 +12664,20 @@ function parseUser(raw, common) {
12184
12664
  }
12185
12665
  function parseAssistant(raw, common) {
12186
12666
  const line = { kind: "assistant", ...common, blocks: [] };
12187
- const message = isRecord2(raw.message) ? raw.message : void 0;
12667
+ const message = isRecord3(raw.message) ? raw.message : void 0;
12188
12668
  if (message === void 0)
12189
12669
  return line;
12190
12670
  if (typeof message.model === "string")
12191
12671
  line.model = message.model;
12192
12672
  if (typeof message.id === "string")
12193
12673
  line.messageId = message.id;
12194
- if (isRecord2(message.usage))
12674
+ if (isRecord3(message.usage))
12195
12675
  line.usage = message.usage;
12196
12676
  const content = message.content;
12197
12677
  if (!Array.isArray(content))
12198
12678
  return line;
12199
12679
  for (const block of content) {
12200
- if (!isRecord2(block))
12680
+ if (!isRecord3(block))
12201
12681
  continue;
12202
12682
  line.blocks.push(parseAssistantBlock(block));
12203
12683
  }
@@ -12215,7 +12695,7 @@ function parseAssistantBlock(block) {
12215
12695
  kind: "tool_use",
12216
12696
  id: block.id,
12217
12697
  name: block.name,
12218
- input: isRecord2(block.input) ? block.input : {}
12698
+ input: isRecord3(block.input) ? block.input : {}
12219
12699
  };
12220
12700
  }
12221
12701
  return {
@@ -12224,7 +12704,7 @@ function parseAssistantBlock(block) {
12224
12704
  raw: block
12225
12705
  };
12226
12706
  }
12227
- function isRecord2(value) {
12707
+ function isRecord3(value) {
12228
12708
  return typeof value === "object" && value !== null && !Array.isArray(value);
12229
12709
  }
12230
12710
 
@@ -12409,7 +12889,7 @@ function buildTaskCreateResultIndex(lines) {
12409
12889
  if (line.kind !== "user")
12410
12890
  continue;
12411
12891
  const result = line.toolUseResult;
12412
- if (!isRecord2(result) || !isRecord2(result.task) || typeof result.task.id !== "string")
12892
+ if (!isRecord3(result) || !isRecord3(result.task) || typeof result.task.id !== "string")
12413
12893
  continue;
12414
12894
  for (const toolResult of line.toolResults)
12415
12895
  index.set(toolResult.toolUseId, result.task.id);
@@ -12422,7 +12902,7 @@ function applyTodoWrite(input, tasks) {
12422
12902
  return false;
12423
12903
  tasks.clear();
12424
12904
  for (const raw of todos) {
12425
- if (!isRecord2(raw) || typeof raw.content !== "string" || !isTodoStatus(raw.status))
12905
+ if (!isRecord3(raw) || typeof raw.content !== "string" || !isTodoStatus(raw.status))
12426
12906
  continue;
12427
12907
  const item = { id: raw.content, content: raw.content, status: raw.status };
12428
12908
  if (typeof raw.activeForm === "string")
@@ -12528,7 +13008,7 @@ function turnUsage(line) {
12528
13008
  }
12529
13009
  };
12530
13010
  const breakdown = usage.cache_creation;
12531
- if (isRecord2(breakdown)) {
13011
+ if (isRecord3(breakdown)) {
12532
13012
  const fiveMinute = finiteNumber(breakdown.ephemeral_5m_input_tokens);
12533
13013
  const oneHour = finiteNumber(breakdown.ephemeral_1h_input_tokens);
12534
13014
  if (fiveMinute !== void 0)
@@ -12577,7 +13057,33 @@ function createNarrativeService(options = {}) {
12577
13057
  const intervalMs = options.intervalMs ?? 500;
12578
13058
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
12579
13059
  const harnesses = /* @__PURE__ */ new Map();
13060
+ const listeners = /* @__PURE__ */ new Set();
13061
+ const pendingNotifications = /* @__PURE__ */ new Set();
13062
+ let notificationQueued = false;
12580
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
+ }
12581
13087
  function sessionsOf(harness) {
12582
13088
  let sessions = harnesses.get(harness);
12583
13089
  if (sessions === void 0) {
@@ -12590,6 +13096,7 @@ function createNarrativeService(options = {}) {
12590
13096
  return {
12591
13097
  id,
12592
13098
  lines: [],
13099
+ codexLines: [],
12593
13100
  follows: /* @__PURE__ */ new Map(),
12594
13101
  hookIntervals: [],
12595
13102
  waiting: null,
@@ -12640,20 +13147,27 @@ function createNarrativeService(options = {}) {
12640
13147
  function resetTranscript(session, path) {
12641
13148
  closeSession(session);
12642
13149
  session.lines = [];
13150
+ session.codexLines = [];
12643
13151
  session.hookIntervals = [];
13152
+ session.hookPrompt = void 0;
13153
+ session.hookPromptAt = void 0;
12644
13154
  session.mockNote = void 0;
12645
13155
  session.transcriptPath = path;
12646
13156
  session.dirty = true;
12647
13157
  }
12648
- function startFollowing(session, path) {
13158
+ function startFollowing(harness, session, path) {
12649
13159
  if (closed || session.follows.has(path))
12650
13160
  return;
12651
13161
  session.follows.set(path, followTranscript(path, (line) => {
12652
- session.lines.push(parseTranscriptLine(line));
13162
+ if (harness === "codex")
13163
+ session.codexLines.push(parseCodexRolloutLine(line));
13164
+ else
13165
+ session.lines.push(parseTranscriptLine(line));
12653
13166
  session.dirty = true;
13167
+ notify(harness);
12654
13168
  }, { intervalMs }));
12655
13169
  }
12656
- function startSubagentDiscovery(session, transcriptPath) {
13170
+ function startSubagentDiscovery(harness, session, transcriptPath) {
12657
13171
  const sessionDir = transcriptPath.replace(/\.jsonl$/, "");
12658
13172
  if (sessionDir === transcriptPath)
12659
13173
  return;
@@ -12662,8 +13176,9 @@ function createNarrativeService(options = {}) {
12662
13176
  try {
12663
13177
  const entries = await readdir(subagentsDir);
12664
13178
  for (const entry of entries) {
12665
- if (/^agent-.*\.jsonl$/.test(entry))
12666
- startFollowing(session, join4(subagentsDir, entry));
13179
+ if (/^agent-.*\.jsonl$/.test(entry)) {
13180
+ startFollowing(harness, session, join4(subagentsDir, entry));
13181
+ }
12667
13182
  }
12668
13183
  } catch {
12669
13184
  }
@@ -12690,8 +13205,17 @@ function createNarrativeService(options = {}) {
12690
13205
  if (follow && transcriptPath !== void 0) {
12691
13206
  if (session.transcriptPath !== transcriptPath) {
12692
13207
  resetTranscript(session, transcriptPath);
12693
- startFollowing(session, transcriptPath);
12694
- 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;
12695
13219
  }
12696
13220
  }
12697
13221
  if (name === "SubagentStart") {
@@ -12743,13 +13267,34 @@ function createNarrativeService(options = {}) {
12743
13267
  }
12744
13268
  session.lastEvent = { name, at: event.received_at };
12745
13269
  session.dirty = true;
13270
+ notify(event.harness);
12746
13271
  }
12747
13272
  function seedFixture(harness, rawLines, mockNote) {
12748
- 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));
12749
13293
  const sessionLine = lines.find((l) => l.kind !== "unknown" && typeof l.sessionId === "string");
12750
13294
  const id = sessionLine?.sessionId ?? "__fixture__";
12751
13295
  const session = sessionOf(harness, id);
12752
13296
  session.lines = lines;
13297
+ session.codexLines = [];
12753
13298
  session.mockNote = mockNote;
12754
13299
  for (const line of lines) {
12755
13300
  if (line.kind === "unknown")
@@ -12763,6 +13308,7 @@ function createNarrativeService(options = {}) {
12763
13308
  }
12764
13309
  }
12765
13310
  session.dirty = true;
13311
+ notify(harness);
12766
13312
  }
12767
13313
  function deriveCached(harness, session) {
12768
13314
  if (!session.dirty && session.cached !== void 0)
@@ -12790,9 +13336,9 @@ function createNarrativeService(options = {}) {
12790
13336
  const overview = getOverview(harness);
12791
13337
  const first = overview.sessions[0];
12792
13338
  if (first !== void 0) {
12793
- const state2 = overview.states[first.id];
12794
- if (state2 !== void 0)
12795
- return state2;
13339
+ const state = overview.states[first.id];
13340
+ if (state !== void 0)
13341
+ return state;
12796
13342
  }
12797
13343
  return emptyState(harness, now().toISOString());
12798
13344
  }
@@ -12802,8 +13348,17 @@ function createNarrativeService(options = {}) {
12802
13348
  for (const session of sessions.values())
12803
13349
  closeSession(session);
12804
13350
  }
13351
+ pendingNotifications.clear();
13352
+ listeners.clear();
13353
+ }
13354
+ function subscribe(listener) {
13355
+ if (closed)
13356
+ return () => {
13357
+ };
13358
+ listeners.add(listener);
13359
+ return () => listeners.delete(listener);
12805
13360
  }
12806
- return { handleEvent, getState, getOverview, seedFixture, close };
13361
+ return { handleEvent, getState, getOverview, seedFixture, subscribe, close };
12807
13362
  }
12808
13363
  function lastActivityOf(session) {
12809
13364
  if (session.lastActivity !== void 0)
@@ -12816,6 +13371,13 @@ function lastActivityOf(session) {
12816
13371
  if (ts !== void 0 && ts > latest)
12817
13372
  latest = ts;
12818
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
+ }
12819
13381
  return latest;
12820
13382
  }
12821
13383
  function isEnded(session, nowIso) {
@@ -12861,7 +13423,8 @@ function waitingMessage(payload, fallback) {
12861
13423
  return fallback;
12862
13424
  }
12863
13425
  function derive(harness, session, nowIso) {
12864
- const lines = sortedByTimestamp(session.lines);
13426
+ const codexLines = sortedCodexByTimestamp(session.codexLines);
13427
+ const lines = harness === "codex" ? adaptCodexNarrativeLines(codexLines) : sortedByTimestamp(session.lines);
12865
13428
  const timeline = deriveTodoTimeline(extractTodoSnapshots(lines));
12866
13429
  const allSteps = extractSteps(lines);
12867
13430
  const steps = allSteps.filter((s) => !TODO_TOOLS.has(s.toolName));
@@ -12954,11 +13517,12 @@ function derive(harness, session, nowIso) {
12954
13517
  todos,
12955
13518
  ...textTodos !== void 0 && textTodos.length > 0 ? { textTodos } : {},
12956
13519
  traces,
12957
- turnUsages: deriveTurnUsages(lines),
13520
+ turnUsages: harness === "codex" ? deriveCodexTurnUsages(codexLines) : deriveTurnUsages(lines),
12958
13521
  currentAction: deriveCurrentAction(steps, session.lastEvent, session.pendingHookTool),
12959
13522
  waiting: session.waiting,
12960
13523
  agentQuote: deriveAgentQuote(lines),
12961
- lastPrompt: deriveLastPrompt(lines),
13524
+ // transcript 是会话事实源;hook prompt 只在 tail 尚未读到/格式暂不可读时兜底。
13525
+ lastPrompt: deriveLastPrompt(lines, session.hookPrompt, session.hookPromptAt),
12962
13526
  updatedAt: nowIso
12963
13527
  };
12964
13528
  }
@@ -13005,6 +13569,109 @@ function sortedByTimestamp(lines) {
13005
13569
  return ta < tb ? -1 : ta > tb ? 1 : 0;
13006
13570
  });
13007
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
+ }
13008
13675
  function collectResults(lines) {
13009
13676
  const results = /* @__PURE__ */ new Map();
13010
13677
  for (const line of lines) {
@@ -13071,7 +13738,15 @@ function nodeEstTokens(steps, results) {
13071
13738
  }
13072
13739
  return counted ? { estTokens: sum } : {};
13073
13740
  }
13074
- 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
+ ];
13075
13750
  function inputDetail(input) {
13076
13751
  for (const key of DETAIL_KEYS) {
13077
13752
  const value = input[key];
@@ -13120,6 +13795,131 @@ function deriveTurnUsages(lines) {
13120
13795
  }
13121
13796
  return [...byMessage.values()];
13122
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
+ }
13123
13923
  function deriveCurrentAction(steps, lastEvent, pendingHookTool) {
13124
13924
  const open2 = [...steps].reverse().find((s) => s.endTs === void 0);
13125
13925
  if (open2) {
@@ -13154,7 +13954,7 @@ function deriveAgentQuote(lines) {
13154
13954
  }
13155
13955
  return null;
13156
13956
  }
13157
- function deriveLastPrompt(lines) {
13957
+ function deriveLastPrompt(lines, hookPrompt, hookPromptAt) {
13158
13958
  for (let i = lines.length - 1; i >= 0; i--) {
13159
13959
  const line = lines[i];
13160
13960
  if (line === void 0 || line.kind !== "user" || line.agentId !== void 0)
@@ -13163,9 +13963,12 @@ function deriveLastPrompt(lines) {
13163
13963
  continue;
13164
13964
  if (line.text === void 0 || line.text.trim() === "")
13165
13965
  continue;
13966
+ if (hookPrompt !== void 0 && hookPromptAt !== void 0 && line.timestamp !== void 0 && hookPromptAt > line.timestamp) {
13967
+ return hookPrompt;
13968
+ }
13166
13969
  return truncate(line.text.trim(), DETAIL_LIMIT);
13167
13970
  }
13168
- return null;
13971
+ return hookPrompt ?? null;
13169
13972
  }
13170
13973
  function truncate(text, limit) {
13171
13974
  return text.length <= limit ? text : `${text.slice(0, limit - 1)}\u2026`;
@@ -13227,96 +14030,65 @@ async function loadHarness(harness, mode) {
13227
14030
  };
13228
14031
  }
13229
14032
 
13230
- // apps/cli/dist/scan.js
13231
- init_dist();
13232
-
13233
- // apps/cli/dist/telemetry.js
13234
- 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";
13235
14037
  import { dirname as dirname11, join as join17 } from "node:path";
13236
- import { fileURLToPath as fileURLToPath2 } from "node:url";
13237
- var DEFAULT_TELEMETRY_SERVER = "https://api.getwingman.dev";
13238
- var SEND_INTERVAL_MS = 24 * 60 * 60 * 1e3;
13239
- var SEND_TIMEOUT_MS = 3e3;
13240
- var MAX_ERROR_TYPES = 10;
13241
- function emptyState2() {
13242
- return {
13243
- scans: 0,
13244
- toggleApplies: 0,
13245
- detected: { cc: false, codex: false, dsh: false },
13246
- liveWired: { cc: false, codex: false, dsh: false },
13247
- errorTypes: /* @__PURE__ */ new Set()
13248
- };
13249
- }
13250
- var state = emptyState2();
13251
- function recordDetect(harness, installed) {
13252
- state.detected[harness] = installed;
13253
- }
13254
- function recordScan(config) {
13255
- state.scans += 1;
13256
- state.detected[config.harness] = true;
13257
- state.liveWired[config.harness] = config.capabilities.live !== "none";
14038
+ function createHookToken() {
14039
+ return randomBytes2(32).toString("base64url");
13258
14040
  }
13259
- function recordHookInstall(harness) {
13260
- state.liveWired[harness] = true;
13261
- }
13262
- function recordApply() {
13263
- state.toggleApplies += 1;
13264
- }
13265
- function recordError(error) {
13266
- if (state.errorTypes.size >= MAX_ERROR_TYPES)
13267
- return;
13268
- state.errorTypes.add(error instanceof Error ? error.constructor.name : typeof error);
13269
- }
13270
- function buildPayload(options) {
13271
- return {
13272
- app_version: options.version,
13273
- platform: process.platform,
13274
- arch: process.arch,
13275
- harnesses: { ...state.detected },
13276
- counts: {
13277
- scans: state.scans,
13278
- live_wired: Object.values(state.liveWired).filter(Boolean).length,
13279
- toggle_applies: state.toggleApplies
13280
- },
13281
- license_state: options.licenseState,
13282
- error_types: [...state.errorTypes].sort()
13283
- };
13284
- }
13285
- function appVersion() {
13286
- if (typeof __WINGMAN_VERSION__ === "string")
13287
- return __WINGMAN_VERSION__;
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;
13288
14053
  try {
13289
- const pkgPath = join17(dirname11(fileURLToPath2(import.meta.url)), "..", "package.json");
13290
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
13291
- return typeof pkg.version === "string" ? pkg.version : "unknown";
13292
- } catch {
13293
- return "unknown";
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;
13294
14074
  }
13295
14075
  }
13296
- async function sendMetrics(payload, baseUrl) {
13297
- const base = baseUrl ?? process.env.WINGMAN_TELEMETRY_SERVER ?? DEFAULT_TELEMETRY_SERVER;
14076
+ function removeServeRuntimeState(registration) {
14077
+ if (registration === void 0)
14078
+ return;
13298
14079
  try {
13299
- await fetch(`${base}/metrics`, {
13300
- method: "POST",
13301
- headers: { "content-type": "application/json" },
13302
- body: JSON.stringify(payload),
13303
- signal: AbortSignal.timeout(SEND_TIMEOUT_MS)
13304
- });
14080
+ const current = readFileSync11(registration.path);
14081
+ if (current.equals(registration.contents))
14082
+ rmSync4(registration.path, { force: true });
13305
14083
  } catch {
13306
14084
  }
13307
14085
  }
13308
- function startTelemetry(options = {}) {
13309
- const send = () => {
13310
- const licenseState = licenseStatus(options.homeDir === void 0 ? {} : { homeDir: options.homeDir }).state;
13311
- void sendMetrics(buildPayload({ version: appVersion(), licenseState }), options.baseUrl);
13312
- };
13313
- send();
13314
- const timer = setInterval(send, options.intervalMs ?? SEND_INTERVAL_MS);
13315
- timer.unref();
13316
- return () => clearInterval(timer);
14086
+ function errorMessage(error) {
14087
+ return error instanceof Error ? error.message : String(error);
13317
14088
  }
13318
14089
 
13319
14090
  // apps/cli/dist/scan.js
14091
+ init_dist();
13320
14092
  async function scanValidated(adapter, options) {
13321
14093
  const config = await adapter.scan(options);
13322
14094
  const errors = validateUnifiedConfig(config);
@@ -13324,12 +14096,12 @@ async function scanValidated(adapter, options) {
13324
14096
  throw new Error(`${adapter.harness} \u7684 scan \u8F93\u51FA\u4E0D\u7B26\u5408 UnifiedConfig \u5408\u540C:
13325
14097
  - ${errors.join("\n- ")}`);
13326
14098
  }
13327
- recordScan(config);
13328
14099
  return config;
13329
14100
  }
13330
14101
 
13331
14102
  // apps/cli/dist/server.js
13332
14103
  init_dist();
14104
+ import { createHmac, timingSafeEqual } from "node:crypto";
13333
14105
  import { readFile as readFile3 } from "node:fs/promises";
13334
14106
  import { createServer } from "node:http";
13335
14107
  import { extname, resolve as resolve5, sep as sep2 } from "node:path";
@@ -13382,9 +14154,9 @@ function residentTotal(config) {
13382
14154
  }
13383
14155
  return counted ? { kind, value: sum } : void 0;
13384
14156
  }
13385
- function stepToolNames(state2) {
14157
+ function stepToolNames(state) {
13386
14158
  const names = [];
13387
- for (const trace of state2.traces) {
14159
+ for (const trace of state.traces) {
13388
14160
  for (const node of trace.nodes) {
13389
14161
  for (const step of node.steps)
13390
14162
  names.push(step.toolName);
@@ -13392,23 +14164,23 @@ function stepToolNames(state2) {
13392
14164
  }
13393
14165
  return names;
13394
14166
  }
13395
- function deriveSuggestions(state2, config, nowMs) {
14167
+ function deriveSuggestions(state, config, nowMs) {
13396
14168
  const suggestions = [];
13397
- if (state2.waiting !== null) {
13398
- const sinceMs = Date.parse(state2.waiting.since);
14169
+ if (state.waiting !== null) {
14170
+ const sinceMs = Date.parse(state.waiting.since);
13399
14171
  if (Number.isFinite(sinceMs) && nowMs - sinceMs > WAITING_TIMEOUT_MS) {
13400
14172
  suggestions.push({
13401
14173
  id: "waiting-timeout",
13402
14174
  rule: "waiting-timeout",
13403
- text: `\u5DF2\u7B49\u5F85 ${fmtDuration(nowMs - sinceMs)}:${state2.waiting.message}`,
13404
- source: `\u6765\u6E90:${state2.waiting.kind === "permission" ? "PermissionRequest" : "Notification"} \u4E8B\u4EF6,${state2.waiting.since} \u8D77\u672A\u51B3;\u9608\u503C 60s`
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`
13405
14177
  });
13406
14178
  }
13407
14179
  }
13408
14180
  if (config !== void 0) {
13409
14181
  const refSources = [
13410
- ...state2.lastPrompt !== null ? [{ file: "\u7528\u6237\u63D0\u793A", text: state2.lastPrompt }] : [],
13411
- ...state2.agentQuote !== null ? [{ file: "agent \u81EA\u8FF0", text: state2.agentQuote }] : []
14182
+ ...state.lastPrompt !== null ? [{ file: "\u7528\u6237\u63D0\u793A", text: state.lastPrompt }] : [],
14183
+ ...state.agentQuote !== null ? [{ file: "agent \u81EA\u8FF0", text: state.agentQuote }] : []
13412
14184
  ];
13413
14185
  if (refSources.length > 0) {
13414
14186
  for (const items of Object.values(config.slots)) {
@@ -13430,8 +14202,9 @@ function deriveSuggestions(state2, config, nowMs) {
13430
14202
  }
13431
14203
  }
13432
14204
  }
13433
- const toolNames = stepToolNames(state2);
13434
- 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) {
13435
14208
  for (const item of config.slots.tools) {
13436
14209
  if (!item.enabled || !item.toggleable)
13437
14210
  continue;
@@ -13455,9 +14228,9 @@ function deriveSuggestions(state2, config, nowMs) {
13455
14228
  });
13456
14229
  }
13457
14230
  }
13458
- const lastTurn = state2.turnUsages[state2.turnUsages.length - 1];
14231
+ const lastTurn = state.turnUsages[state.turnUsages.length - 1];
13459
14232
  const resident = residentTotal(config);
13460
- if (lastTurn !== void 0 && resident !== void 0) {
14233
+ if (lastTurn?.contextTotal !== void 0 && resident !== void 0) {
13461
14234
  const ctx = lastTurn.contextTotal.value;
13462
14235
  const history = ctx - resident.value;
13463
14236
  if (ctx >= HISTORY_BLOAT_MIN_CONTEXT && history / ctx > HISTORY_BLOAT_RATIO) {
@@ -13490,13 +14263,16 @@ var CONTENT_TYPES = {
13490
14263
  ".woff2": "font/woff2"
13491
14264
  };
13492
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";
13493
14270
  function createWingmanServer(options) {
13494
14271
  const warn = options.warn ?? ((message) => console.error(message));
13495
14272
  const runtime = { anatomy: null };
13496
14273
  return createServer((req, res) => {
13497
14274
  handle(req, res, options, warn, runtime).catch((error) => {
13498
- recordError(error);
13499
- warn(`[wingman] \u8BF7\u6C42\u5904\u7406\u5931\u8D25:${errorMessage(error)}`);
14275
+ warn(`[wingman] \u8BF7\u6C42\u5904\u7406\u5931\u8D25:${errorMessage2(error)}`);
13500
14276
  if (!res.headersSent)
13501
14277
  sendText2(res, 500, "internal error");
13502
14278
  else
@@ -13515,7 +14291,35 @@ function listen(server, port) {
13515
14291
  }
13516
14292
  async function handle(req, res, options, warn, runtime) {
13517
14293
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
14294
+ if (req.method !== "GET") {
14295
+ const origin = req.headers.origin;
14296
+ if (typeof origin === "string" && origin !== "" && !/^https?:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin)) {
14297
+ sendJson(res, 403, { error: `\u62D2\u7EDD\u5F02\u6E90\u5199\u8BF7\u6C42(Origin: ${origin})` });
14298
+ return;
14299
+ }
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
+ }
13518
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
+ }
13519
14323
  options.receiver.handleHook(req, res);
13520
14324
  return;
13521
14325
  }
@@ -13523,6 +14327,14 @@ async function handle(req, res, options, warn, runtime) {
13523
14327
  options.receiver.handleEvents(req, res);
13524
14328
  return;
13525
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
+ }
13526
14338
  if (url.pathname === "/api/action/plan" && req.method === "POST") {
13527
14339
  await handleActionPlan(req, res, options);
13528
14340
  return;
@@ -13535,13 +14347,24 @@ async function handle(req, res, options, warn, runtime) {
13535
14347
  await handleLicenseInstall(req, res, options);
13536
14348
  return;
13537
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
+ }
13538
14362
  if (req.method !== "GET") {
13539
14363
  sendText2(res, 405, "method not allowed");
13540
14364
  return;
13541
14365
  }
13542
14366
  if (url.pathname === "/api/license") {
13543
- const { state: state2, detail } = licenseStatus(options.licenseOptions);
13544
- sendJson(res, 200, { state: state2, detail });
14367
+ sendJson(res, 200, publicLicenseStatus(licenseStatus(options.licenseOptions)));
13545
14368
  return;
13546
14369
  }
13547
14370
  if (url.pathname === "/api/narrative") {
@@ -13561,9 +14384,9 @@ async function handle(req, res, options, warn, runtime) {
13561
14384
  const config = (await cachedAnatomy(options, warn, runtime)).find((c) => c.harness === harness);
13562
14385
  const nowMs = Date.now();
13563
14386
  const states = {};
13564
- for (const [id, state2] of Object.entries(overview.states)) {
13565
- const suggestions = deriveSuggestions(state2, config, nowMs);
13566
- states[id] = suggestions.length > 0 ? { ...state2, suggestions } : state2;
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;
13567
14390
  }
13568
14391
  overview.states = states;
13569
14392
  }
@@ -13582,6 +14405,40 @@ async function handle(req, res, options, warn, runtime) {
13582
14405
  }
13583
14406
  await serveStatic(url.pathname, res, options.panelDistDir);
13584
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
+ }
13585
14442
  async function cachedAnatomy(options, warn, runtime) {
13586
14443
  const nowMs = Date.now();
13587
14444
  if (runtime.anatomy !== null && nowMs - runtime.anatomy.at < ANATOMY_CACHE_TTL_MS) {
@@ -13597,11 +14454,9 @@ async function scanAll(harnesses, warn) {
13597
14454
  let installed;
13598
14455
  try {
13599
14456
  const detected = await adapter.detect(scanOptions);
13600
- recordDetect(harness, detected.installed);
13601
14457
  installed = detected.installed;
13602
14458
  } catch (error) {
13603
- recordError(error);
13604
- warn(`[wingman] ${harness} detect \u5931\u8D25,\u5DF2\u8DF3\u8FC7:${errorMessage(error)}`);
14459
+ warn(`[wingman] ${harness} detect \u5931\u8D25,\u5DF2\u8DF3\u8FC7:${errorMessage2(error)}`);
13605
14460
  continue;
13606
14461
  }
13607
14462
  if (!installed)
@@ -13609,8 +14464,7 @@ async function scanAll(harnesses, warn) {
13609
14464
  try {
13610
14465
  configs.push(await scanValidated(adapter, scanOptions));
13611
14466
  } catch (error) {
13612
- recordError(error);
13613
- 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)}`);
13614
14468
  configs.push(degradedScanConfig(harness, error));
13615
14469
  }
13616
14470
  }
@@ -13647,7 +14501,7 @@ function degradedScanConfig(harness, error) {
13647
14501
  };
13648
14502
  }
13649
14503
  function errorSummary(error) {
13650
- const first = errorMessage(error).split("\n", 1)[0] ?? "";
14504
+ const first = errorMessage2(error).split("\n", 1)[0] ?? "";
13651
14505
  const scrubbed = first.replace(/(?:[A-Za-z]:)?[\\/][^\s'"()|,;]+/g, "\u2026").trim();
13652
14506
  return scrubbed.length <= 120 ? scrubbed : `${scrubbed.slice(0, 119)}\u2026`;
13653
14507
  }
@@ -13671,7 +14525,7 @@ async function handleActionPlan(req, res, options) {
13671
14525
  const plan = await loaded.adapter.planAction(action, loaded.scanOptions);
13672
14526
  sendJson(res, 200, plan);
13673
14527
  } catch (error) {
13674
- sendJson(res, 400, { error: errorMessage(error) });
14528
+ sendJson(res, 400, { error: errorMessage2(error) });
13675
14529
  }
13676
14530
  }
13677
14531
  async function handleActionApply(req, res, options, runtime) {
@@ -13701,13 +14555,11 @@ async function handleActionApply(req, res, options, runtime) {
13701
14555
  try {
13702
14556
  const result = await loaded.adapter.applyAction(plan, loaded.scanOptions);
13703
14557
  if (result.ok) {
13704
- recordApply();
13705
14558
  runtime.anatomy = null;
13706
14559
  }
13707
14560
  sendJson(res, 200, result);
13708
14561
  } catch (error) {
13709
- recordError(error);
13710
- sendJson(res, 400, { error: errorMessage(error) });
14562
+ sendJson(res, 400, { error: errorMessage2(error) });
13711
14563
  }
13712
14564
  }
13713
14565
  var LICENSE_BODY_LIMIT = 64 * 1024;
@@ -13722,7 +14574,44 @@ async function handleLicenseInstall(req, res, options) {
13722
14574
  sendJson(res, 400, { error: result.error });
13723
14575
  return;
13724
14576
  }
13725
- 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;
13726
14615
  }
13727
14616
  function readTextBody(req, maxBytes) {
13728
14617
  return new Promise((resolveBody) => {
@@ -13813,7 +14702,7 @@ function sendText2(res, status, body) {
13813
14702
  res.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
13814
14703
  res.end(body);
13815
14704
  }
13816
- function errorMessage(error) {
14705
+ function errorMessage2(error) {
13817
14706
  return error instanceof Error ? error.message : String(error);
13818
14707
  }
13819
14708
 
@@ -13897,6 +14786,7 @@ ${counts}
13897
14786
  }
13898
14787
  async function runServe(args) {
13899
14788
  const { port, fixtures, panelDist } = parseServeArgs(args);
14789
+ const hookToken = createHookToken();
13900
14790
  const harnesses = [];
13901
14791
  for (const id of HARNESS_IDS) {
13902
14792
  try {
@@ -13907,7 +14797,7 @@ async function runServe(args) {
13907
14797
  `);
13908
14798
  }
13909
14799
  }
13910
- 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);
13911
14801
  const narrative = createNarrativeService({ followTranscripts: !fixtures });
13912
14802
  if (fixtures) {
13913
14803
  try {
@@ -13924,13 +14814,28 @@ async function runServe(args) {
13924
14814
  panelDistDir,
13925
14815
  receiver: createReceiver({ onEvent: narrative.handleEvent }),
13926
14816
  readonlyActions: fixtures,
13927
- narrative
14817
+ narrative,
14818
+ hookToken
13928
14819
  });
13929
14820
  const actualPort = await listen(server, port);
13930
- 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
+ });
13931
14836
  const mode = fixtures ? "(fixtures \u6A21\u5F0F:\u6570\u636E\u6765\u81EA @wingman/fixtures \u6837\u4F8B,\u975E\u672C\u673A\u914D\u7F6E)" : "";
13932
14837
  process.stdout.write(`Wingman \u9762\u677F:http://127.0.0.1:${actualPort}${mode}
13933
- \u53EA\u7ED1 127.0.0.1,\u9762\u677F\u4E0E\u6570\u636E\u90FD\u5728\u672C\u673A;\u533F\u540D\u4F7F\u7528\u7EDF\u8BA1\u4E0D\u542B\u5185\u5BB9\u4E0E\u8DEF\u5F84,\u660E\u7EC6 getwingman.dev/data\u3002Ctrl+C \u9000\u51FA\u3002
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
13934
14839
  `);
13935
14840
  return 0;
13936
14841
  }
@@ -13958,10 +14863,13 @@ async function runHook(args) {
13958
14863
  const { adapter } = await loadHarness(harness, { fixtures: false });
13959
14864
  if (action === "install") {
13960
14865
  await adapter.installHooks(port);
13961
- recordHookInstall(harness);
13962
- 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
13963
14870
  \u5199\u524D\u5DF2\u5907\u4EFD;\u91CD\u590D\u6267\u884C\u5E42\u7B49;\u89E3\u7EBF:wingman hook uninstall ${harness}
13964
14871
  `);
14872
+ }
13965
14873
  } else {
13966
14874
  await adapter.uninstallHooks();
13967
14875
  process.stdout.write(`wingman:${harness} \u5DF2\u89E3\u7EBF,\u914D\u7F6E\u5DF2\u6062\u590D\u3002