harnesstrim 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.mjs +186 -15
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -22747,11 +22747,23 @@ function pluginIndex(plugin) {
22747
22747
  (e) => typeof e === "string" && e.includes(OPENCODE_PLUGIN_NAME) || Array.isArray(e) && typeof e[0] === "string" && e[0].includes(OPENCODE_PLUGIN_NAME)
22748
22748
  );
22749
22749
  }
22750
+ function legacyPluginIndex(plugin) {
22751
+ return plugin.findIndex(
22752
+ (entry) => typeof entry === "object" && entry !== null && typeof entry.name === "string" && entry.name.includes(OPENCODE_PLUGIN_NAME)
22753
+ );
22754
+ }
22750
22755
  function planOpencodeInstall(config2, adapterConfig) {
22751
22756
  const base = typeof config2 === "object" && config2 !== null ? { ...config2 } : {};
22752
22757
  const plugin = Array.isArray(base.plugin) ? [...base.plugin] : [];
22753
22758
  const desired = adapterConfig ? [OPENCODE_PLUGIN_NAME, adapterConfig] : OPENCODE_PLUGIN_NAME;
22754
22759
  const idx = pluginIndex(plugin);
22760
+ const legacyIdx = idx === -1 ? legacyPluginIndex(plugin) : -1;
22761
+ if (legacyIdx !== -1) {
22762
+ const legacy = plugin[legacyIdx];
22763
+ const options = legacy.options;
22764
+ plugin[legacyIdx] = adapterConfig ? desired : typeof options === "object" && options !== null && !Array.isArray(options) ? [OPENCODE_PLUGIN_NAME, options] : OPENCODE_PLUGIN_NAME;
22765
+ return { nextConfig: { ...base, plugin }, alreadyInstalled: true, changed: true };
22766
+ }
22755
22767
  if (idx === -1) {
22756
22768
  plugin.push(desired);
22757
22769
  return { nextConfig: { ...base, plugin }, alreadyInstalled: false, changed: true };
@@ -22801,7 +22813,56 @@ import path6 from "node:path";
22801
22813
 
22802
22814
  // ../adapter-codex/src/index.ts
22803
22815
  import path3 from "node:path";
22816
+
22817
+ // ../adapter-codex/src/hook.ts
22818
+ init_src();
22819
+ function reduceCodexPayload(rawJson, minLength) {
22820
+ const extracted = extractToolOutput(rawJson);
22821
+ if (extracted === null) return { response: "{}", event: null };
22822
+ const result = reduceAuto(extracted.output, minLength);
22823
+ if (!result.changed) return { response: "{}", event: null };
22824
+ const response = JSON.stringify({
22825
+ decision: "block",
22826
+ reason: `HarnessTrim reduced ${extracted.toolName} output (${result.reducer}):
22827
+
22828
+ ${result.output}`
22829
+ });
22830
+ return {
22831
+ response,
22832
+ event: {
22833
+ tool: extracted.toolName,
22834
+ reducer: result.reducer,
22835
+ beforeChars: extracted.output.length,
22836
+ afterChars: result.output.length
22837
+ }
22838
+ };
22839
+ }
22840
+ function extractToolOutput(rawJson) {
22841
+ let payload;
22842
+ try {
22843
+ payload = JSON.parse(rawJson);
22844
+ } catch {
22845
+ return null;
22846
+ }
22847
+ if (typeof payload !== "object" || payload === null) return null;
22848
+ const p = payload;
22849
+ const output = extractOutputText(p.tool_response);
22850
+ return output === null ? null : { toolName: typeof p.tool_name === "string" ? p.tool_name : "unknown", output };
22851
+ }
22852
+ function extractOutputText(response) {
22853
+ if (typeof response === "string") return response;
22854
+ if (typeof response !== "object" || response === null) return null;
22855
+ const r = response;
22856
+ for (const key of ["stdout", "output", "content"]) {
22857
+ if (typeof r[key] === "string") return r[key];
22858
+ }
22859
+ return null;
22860
+ }
22861
+
22862
+ // ../adapter-codex/src/index.ts
22804
22863
  var HARNESSTRIM_MARKER = "harnesstrim:begin";
22864
+ var CODEX_HOOK_COMMAND = "harnesstrim hook codex --metrics .harnesstrim/metrics.jsonl";
22865
+ var CODEX_HOOK_MATCHER = "^Bash$";
22805
22866
  var REDUCE_INSTRUCTION_SNIPPET = `<!-- ${HARNESSTRIM_MARKER} -->
22806
22867
  ## Token economy (HarnessTrim)
22807
22868
 
@@ -22814,6 +22875,45 @@ This keeps failures, errors, assertions, and summaries while dropping passing-te
22814
22875
  generated-file (lockfile/dist) diffs. Prefer the installed skills for output, review, and
22815
22876
  scaffolding discipline.
22816
22877
  <!-- harnesstrim:end -->`;
22878
+ function hasHarnessTrimHook(document) {
22879
+ const hooks = document.hooks;
22880
+ const post = hooks?.PostToolUse;
22881
+ if (!Array.isArray(post)) return false;
22882
+ return post.some(
22883
+ (entry) => Array.isArray(entry?.hooks) && entry.hooks.some((hook) => typeof hook?.command === "string" && hook.command.includes("harnesstrim hook codex"))
22884
+ );
22885
+ }
22886
+ function planCodexHookInstall(input) {
22887
+ let document = {};
22888
+ let action;
22889
+ if (input.hooksJsonContent === null) {
22890
+ action = "create";
22891
+ } else {
22892
+ let parsed;
22893
+ try {
22894
+ parsed = JSON.parse(input.hooksJsonContent);
22895
+ } catch {
22896
+ throw new Error(".codex/hooks.json is not valid JSON; refusing to overwrite it.");
22897
+ }
22898
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
22899
+ throw new Error(".codex/hooks.json must contain a JSON object; refusing to overwrite it.");
22900
+ }
22901
+ document = parsed;
22902
+ action = hasHarnessTrimHook(document) ? "present" : "patch";
22903
+ }
22904
+ if (action === "present") {
22905
+ return { hooksFile: path3.join(input.projectDir, ".codex", "hooks.json"), action, nextHooks: document };
22906
+ }
22907
+ const hooks = { ...document.hooks ?? {} };
22908
+ const post = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
22909
+ post.push({ matcher: CODEX_HOOK_MATCHER, hooks: [{ type: "command", command: CODEX_HOOK_COMMAND }] });
22910
+ hooks.PostToolUse = post;
22911
+ return {
22912
+ hooksFile: path3.join(input.projectDir, ".codex", "hooks.json"),
22913
+ action,
22914
+ nextHooks: { ...document, hooks }
22915
+ };
22916
+ }
22817
22917
  function planCodexInstall(input) {
22818
22918
  const skillsDest = path3.join(input.projectDir, ".codex", "skills");
22819
22919
  const existing = new Set(input.existingSkillNames);
@@ -22881,7 +22981,20 @@ function existingSkillNames(dest) {
22881
22981
  }
22882
22982
 
22883
22983
  // src/install-codex.ts
22884
- function runInstallCodex(dir, apply) {
22984
+ function readHooksJson(hooksPath) {
22985
+ try {
22986
+ return fs5.readFileSync(hooksPath, "utf8");
22987
+ } catch {
22988
+ return null;
22989
+ }
22990
+ }
22991
+ function applyHookPlan(plan, apply) {
22992
+ if (!apply || plan.action === "present") return false;
22993
+ fs5.mkdirSync(path6.dirname(plan.hooksFile), { recursive: true });
22994
+ fs5.writeFileSync(plan.hooksFile, JSON.stringify(plan.nextHooks, null, 2) + "\n");
22995
+ return true;
22996
+ }
22997
+ function runInstallCodex(dir, apply, hook = false) {
22885
22998
  const skillsSourceDir = resolveSkillsSourceDir();
22886
22999
  const skillNames = listShippedSkills(skillsSourceDir);
22887
23000
  const skillsDest = path6.join(dir, ".codex", "skills");
@@ -22899,6 +23012,9 @@ function runInstallCodex(dir, apply) {
22899
23012
  agentsMdContent,
22900
23013
  existingSkillNames: existingSkillNames(skillsDest)
22901
23014
  });
23015
+ const hooksPath = path6.join(dir, ".codex", "hooks.json");
23016
+ const hooksJsonContent = hook ? readHooksJson(hooksPath) : null;
23017
+ const hookPlan = hook ? planCodexHookInstall({ projectDir: dir, hooksJsonContent }) : null;
22902
23018
  const copied = [];
22903
23019
  let applied = false;
22904
23020
  if (apply) {
@@ -22912,9 +23028,20 @@ function runInstallCodex(dir, apply) {
22912
23028
  } else if (plan.instructionsAction === "append") {
22913
23029
  fs5.appendFileSync(plan.instructionsFile, "\n\n" + plan.instructionsSnippet + "\n");
22914
23030
  }
23031
+ if (hookPlan) applyHookPlan(hookPlan, true);
22915
23032
  applied = true;
22916
23033
  }
22917
- return { plan, applied, copied };
23034
+ return { plan, hookPlan, applied, copied };
23035
+ }
23036
+ function runInstallCodexGlobalHook(codexHome, apply) {
23037
+ const hooksPath = path6.join(codexHome, "hooks.json");
23038
+ const hookPlan = planCodexHookInstall({
23039
+ // The planner expects the directory that contains .codex; for a user-level config
23040
+ // the Codex home is itself that directory, so add its parent and use a normal path.
23041
+ projectDir: path6.dirname(codexHome),
23042
+ hooksJsonContent: readHooksJson(hooksPath)
23043
+ });
23044
+ return { hookPlan, applied: applyHookPlan(hookPlan, apply) };
22918
23045
  }
22919
23046
 
22920
23047
  // src/install-claude.ts
@@ -22924,7 +23051,7 @@ import path8 from "node:path";
22924
23051
  // ../adapter-claude/src/hook.ts
22925
23052
  init_src();
22926
23053
  function reduceClaudePayload(rawJson, minLength) {
22927
- const extracted = extractToolOutput(rawJson);
23054
+ const extracted = extractToolOutput2(rawJson);
22928
23055
  if (extracted === null) return { response: "{}", event: null };
22929
23056
  const result = reduceAuto(extracted.output, minLength);
22930
23057
  if (!result.changed) return { response: "{}", event: null };
@@ -22944,7 +23071,7 @@ function reduceClaudePayload(rawJson, minLength) {
22944
23071
  }
22945
23072
  };
22946
23073
  }
22947
- function extractToolOutput(rawJson) {
23074
+ function extractToolOutput2(rawJson) {
22948
23075
  let payload;
22949
23076
  try {
22950
23077
  payload = JSON.parse(rawJson);
@@ -22954,10 +23081,10 @@ function extractToolOutput(rawJson) {
22954
23081
  if (typeof payload !== "object" || payload === null) return null;
22955
23082
  const p = payload;
22956
23083
  const toolName = typeof p.tool_name === "string" ? p.tool_name : "unknown";
22957
- const output = extractOutputText(p);
23084
+ const output = extractOutputText2(p);
22958
23085
  return output === null ? null : { toolName, output };
22959
23086
  }
22960
- function extractOutputText(p) {
23087
+ function extractOutputText2(p) {
22961
23088
  if (typeof p.tool_output === "string") return p.tool_output;
22962
23089
  const resp = p.tool_response;
22963
23090
  if (typeof resp === "string") return resp;
@@ -22974,7 +23101,7 @@ function extractOutputText(p) {
22974
23101
  import path7 from "node:path";
22975
23102
  var HOOK_COMMAND = "harnesstrim hook claude";
22976
23103
  var HOOK_MATCHER = "Bash";
22977
- function hasHarnessTrimHook(settings) {
23104
+ function hasHarnessTrimHook2(settings) {
22978
23105
  const hooks = settings.hooks;
22979
23106
  const post = hooks?.PostToolUse;
22980
23107
  if (!Array.isArray(post)) return false;
@@ -23002,7 +23129,7 @@ function planClaudeInstall(input) {
23002
23129
  } catch {
23003
23130
  settings = {};
23004
23131
  }
23005
- action = hasHarnessTrimHook(settings) ? "present" : "patch";
23132
+ action = hasHarnessTrimHook2(settings) ? "present" : "patch";
23006
23133
  }
23007
23134
  const nextSettings = action === "present" ? settings : addHook(settings);
23008
23135
  return {
@@ -23327,12 +23454,42 @@ function renderCodexInstall(result, apply) {
23327
23454
  lines.push("");
23328
23455
  const instr = plan.instructionsAction === "present" ? "AGENTS.md already contains the HarnessTrim instruction (no change)." : plan.instructionsAction === "create" ? `AGENTS.md ${apply ? "created" : "would be created"} with the reduce-pipe instruction.` : `Reduce-pipe instruction ${apply ? "appended" : "would be appended"} to AGENTS.md.`;
23329
23456
  lines.push(instr);
23457
+ if (result.hookPlan) {
23458
+ lines.push("");
23459
+ if (result.hookPlan.action === "present") {
23460
+ lines.push(`${result.hookPlan.hooksFile}: HarnessTrim Bash PostToolUse hook already present (no change).`);
23461
+ } else {
23462
+ lines.push(
23463
+ `${result.hookPlan.hooksFile}: experimental Bash PostToolUse hook ${apply ? result.hookPlan.action === "create" ? "created" : "added" : "would be added"}.`
23464
+ );
23465
+ lines.push("It reduces simple Bash output automatically and records JSONL telemetry in .harnesstrim/metrics.jsonl.");
23466
+ if (!apply) {
23467
+ lines.push("Resulting hooks.json:");
23468
+ lines.push(JSON.stringify(result.hookPlan.nextHooks, null, 2));
23469
+ }
23470
+ }
23471
+ }
23330
23472
  if (!apply) {
23331
23473
  lines.push("");
23332
23474
  lines.push("Dry run \u2014 nothing written. Re-run with `--apply`.");
23333
23475
  }
23334
23476
  return lines.join("\n");
23335
23477
  }
23478
+ function renderCodexGlobalHookInstall(result, apply) {
23479
+ const lines = [`${apply ? "Installed" : "Would install"} global Codex Bash PostToolUse hook`, ""];
23480
+ if (result.hookPlan.action === "present") {
23481
+ lines.push(`${result.hookPlan.hooksFile}: HarnessTrim hook already present (no change).`);
23482
+ } else {
23483
+ lines.push(`${result.hookPlan.hooksFile}: experimental Bash PostToolUse hook ${apply ? result.hookPlan.action === "create" ? "created" : "added" : "would be added"}.`);
23484
+ lines.push("It applies in trusted projects and writes telemetry to each project's .harnesstrim/metrics.jsonl.");
23485
+ if (!apply) {
23486
+ lines.push("Resulting hooks.json:");
23487
+ lines.push(JSON.stringify(result.hookPlan.nextHooks, null, 2));
23488
+ }
23489
+ }
23490
+ if (!apply) lines.push("", "Dry run \u2014 nothing written. Re-run with `--apply`.");
23491
+ return lines.join("\n");
23492
+ }
23336
23493
  function renderClaudeInstall(result, apply) {
23337
23494
  const { plan } = result;
23338
23495
  const lines = [];
@@ -23444,8 +23601,10 @@ Usage:
23444
23601
  harnesstrim install opencode [dir] Wire the adapter into opencode.json (dry-run)
23445
23602
  --apply Actually write the change
23446
23603
  --preset <name> Bake a policy preset's adapter config in
23447
- harnesstrim install codex [dir] Install skills + AGENTS.md reduce-pipe (dry-run)
23604
+ harnesstrim install codex [dir] Install skills + AGENTS.md reduction guidance (dry-run)
23448
23605
  --apply Actually write the change
23606
+ --hook Also install the experimental Bash PostToolUse hook
23607
+ --global With --hook, install it once in ~/.codex (no project files)
23449
23608
  harnesstrim install claude [dir] Install skills + PostToolUse reducer hook (dry-run)
23450
23609
  --apply Actually write the change
23451
23610
  harnesstrim install hermes [dir] Install Hermes plugin (dry-run)
@@ -23454,6 +23613,8 @@ Usage:
23454
23613
  --apply Actually write the change
23455
23614
  harnesstrim hook claude [--metrics <path>]
23456
23615
  PostToolUse hook runtime; --metrics records a TrimEvent per reduction
23616
+ harnesstrim hook codex [--metrics <path>]
23617
+ PostToolUse runtime for Codex's experimental Bash hook
23457
23618
  harnesstrim preset list List policy presets
23458
23619
  harnesstrim preset show <name> Show a preset in detail
23459
23620
  harnesstrim metrics [path] Summarize adapter telemetry (JSONL)
@@ -23477,7 +23638,9 @@ async function main(argv) {
23477
23638
  stats: { type: "boolean" },
23478
23639
  "min-length": { type: "string" },
23479
23640
  log: { type: "string" },
23480
- metrics: { type: "string" }
23641
+ metrics: { type: "string" },
23642
+ hook: { type: "boolean" },
23643
+ global: { type: "boolean" }
23481
23644
  }
23482
23645
  });
23483
23646
  const [command, ...rest] = positionals;
@@ -23501,7 +23664,15 @@ async function main(argv) {
23501
23664
  return 0;
23502
23665
  }
23503
23666
  if (target === "codex") {
23504
- console.log(renderCodexInstall(runInstallCodex(dir, apply), apply));
23667
+ if (values.global === true) {
23668
+ if (values.hook !== true) {
23669
+ console.error("`harnesstrim install codex --global` requires `--hook`.");
23670
+ return 1;
23671
+ }
23672
+ console.log(renderCodexGlobalHookInstall(runInstallCodexGlobalHook(path14.join(os2.homedir(), ".codex"), apply), apply));
23673
+ return 0;
23674
+ }
23675
+ console.log(renderCodexInstall(runInstallCodex(dir, apply, values.hook === true), apply));
23505
23676
  return 0;
23506
23677
  }
23507
23678
  if (target === "claude") {
@@ -23521,12 +23692,12 @@ async function main(argv) {
23521
23692
  }
23522
23693
  case "hook": {
23523
23694
  const which = rest[0];
23524
- if (which !== "claude") {
23525
- console.error(`Unknown hook target: ${which ?? "(none)"}. Supported: claude.`);
23695
+ if (which !== "claude" && which !== "codex") {
23696
+ console.error(`Unknown hook target: ${which ?? "(none)"}. Supported: claude, codex.`);
23526
23697
  return 1;
23527
23698
  }
23528
23699
  const input = await readStdin();
23529
- const { response, event } = reduceClaudePayload(input);
23700
+ const { response, event } = which === "claude" ? reduceClaudePayload(input) : reduceCodexPayload(input);
23530
23701
  process.stdout.write(response);
23531
23702
  if (values.metrics && event) {
23532
23703
  try {
@@ -23534,7 +23705,7 @@ async function main(argv) {
23534
23705
  fs10.mkdirSync(path14.dirname(p), { recursive: true });
23535
23706
  fs10.appendFileSync(
23536
23707
  p,
23537
- JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), harness: "claude", ...event }) + "\n"
23708
+ JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), harness: which, ...event }) + "\n"
23538
23709
  );
23539
23710
  } catch {
23540
23711
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnesstrim",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "HarnessTrim CLI: doctor (diagnose token waste), install adapters, run benchmarks.",
5
5
  "license": "MIT",
6
6
  "type": "module",