harnesstrim 0.0.2 → 0.0.4

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 +358 -82
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -22681,40 +22681,45 @@ function checkSkills(dir, findings) {
22681
22681
  }
22682
22682
  }
22683
22683
  function checkOpenCode(dir, findings) {
22684
- const raw = readIfExists(path.join(dir, "opencode.json"));
22685
- if (raw === null) {
22684
+ const hasOpencodeProject = readIfExists(path.join(dir, "opencode.json")) !== null || fs.existsSync(path.join(dir, ".opencode"));
22685
+ if (!hasOpencodeProject) {
22686
22686
  findings.push({
22687
22687
  severity: "info",
22688
- title: "No opencode.json found",
22689
- detail: "No OpenCode project config in this directory."
22688
+ title: "No OpenCode project config found",
22689
+ detail: "No opencode.json or .opencode/ in this directory."
22690
22690
  });
22691
22691
  return;
22692
22692
  }
22693
- let config2;
22694
- try {
22695
- config2 = JSON.parse(raw);
22696
- } catch {
22697
- findings.push({
22698
- severity: "warn",
22699
- title: "opencode.json is not valid JSON",
22700
- detail: "Could not parse opencode.json to check plugin wiring."
22701
- });
22702
- return;
22693
+ const wrapper = path.join(dir, ".opencode", "plugin", "harnesstrim.ts");
22694
+ const wrapperPresent = fs.existsSync(wrapper);
22695
+ const raw = readIfExists(path.join(dir, "opencode.json"));
22696
+ let staleEntry = false;
22697
+ if (raw !== null) {
22698
+ try {
22699
+ staleEntry = extractPluginNames(JSON.parse(raw)).some((p) => p.includes("@harnesstrim/adapter-opencode"));
22700
+ } catch {
22701
+ }
22703
22702
  }
22704
- const plugins = extractPluginNames(config2);
22705
- const installed = plugins.some((p) => p.includes("@harnesstrim/adapter-opencode"));
22706
- if (installed) {
22703
+ if (wrapperPresent) {
22707
22704
  findings.push({
22708
22705
  severity: "ok",
22709
- title: "HarnessTrim OpenCode adapter is wired in",
22710
- detail: "@harnesstrim/adapter-opencode is present in opencode.json plugins."
22706
+ title: "HarnessTrim OpenCode adapter is installed",
22707
+ detail: ".opencode/plugin/harnesstrim.ts loads the adapter (OpenCode auto-loads it)."
22711
22708
  });
22712
22709
  } else {
22713
22710
  findings.push({
22714
22711
  severity: "warn",
22715
- title: "HarnessTrim adapter not installed in opencode.json",
22712
+ title: "HarnessTrim adapter not installed for OpenCode",
22716
22713
  detail: "The OpenCode adapter would slim tool output automatically.",
22717
- suggestion: "Run `harnesstrim install opencode` to wire it in."
22714
+ suggestion: "Run `harnesstrim install opencode --apply` to install the local plugin."
22715
+ });
22716
+ }
22717
+ if (staleEntry && !wrapperPresent) {
22718
+ findings.push({
22719
+ severity: "warn",
22720
+ title: "Stale adapter entry in opencode.json",
22721
+ detail: "opencode.json lists @harnesstrim/adapter-opencode, but OpenCode's plugin field is a string array and ignores option tuples \u2014 this entry never loaded.",
22722
+ suggestion: "Run `harnesstrim install opencode --apply` to migrate to the local plugin wrapper."
22718
22723
  });
22719
22724
  }
22720
22725
  }
@@ -22741,58 +22746,151 @@ function inspect(dir) {
22741
22746
  init_src();
22742
22747
  import fs2 from "node:fs";
22743
22748
  import path2 from "node:path";
22749
+ import { spawnSync } from "node:child_process";
22744
22750
  var OPENCODE_PLUGIN_NAME = "@harnesstrim/adapter-opencode";
22745
- function pluginIndex(plugin) {
22746
- return plugin.findIndex(
22747
- (e) => typeof e === "string" && e.includes(OPENCODE_PLUGIN_NAME) || Array.isArray(e) && typeof e[0] === "string" && e[0].includes(OPENCODE_PLUGIN_NAME)
22748
- );
22751
+ var OPENCODE_ADAPTER_VERSION = "^0.0.2";
22752
+ var DEFAULT_OPENCODE_ADAPTER_CONFIG = {
22753
+ mode: "active",
22754
+ telemetry: true,
22755
+ telemetryPath: ".harnesstrim/metrics.jsonl"
22756
+ };
22757
+ var WRAPPER_REL = path2.join(".opencode", "plugin", "harnesstrim.ts");
22758
+ var PKG_REL = path2.join(".opencode", "package.json");
22759
+ var OPENCODE_JSON = "opencode.json";
22760
+ function buildOpencodeWrapper(adapterConfig) {
22761
+ const opts = JSON.stringify(adapterConfig, null, 2).replace(/\n/g, "\n ");
22762
+ return `// Generated by \`harnesstrim install opencode\`. Do not reference this plugin from
22763
+ // opencode.json \u2014 OpenCode's \`plugin\` field is a string array and cannot pass options,
22764
+ // so HarnessTrim options live here and OpenCode auto-loads this file from .opencode/plugin/.
22765
+ // The dependency is declared in ../package.json. Edit the options below freely.
22766
+ import { HarnessTrim } from "${OPENCODE_PLUGIN_NAME}";
22767
+
22768
+ export const HarnessTrimPlugin = async (input) =>
22769
+ HarnessTrim(input, ${opts});
22770
+ `;
22749
22771
  }
22750
- function planOpencodeInstall(config2, adapterConfig) {
22751
- const base = typeof config2 === "object" && config2 !== null ? { ...config2 } : {};
22752
- const plugin = Array.isArray(base.plugin) ? [...base.plugin] : [];
22753
- const desired = adapterConfig ? [OPENCODE_PLUGIN_NAME, adapterConfig] : OPENCODE_PLUGIN_NAME;
22754
- const idx = pluginIndex(plugin);
22755
- if (idx === -1) {
22756
- plugin.push(desired);
22757
- return { nextConfig: { ...base, plugin }, alreadyInstalled: false, changed: true };
22758
- }
22759
- if (adapterConfig && JSON.stringify(plugin[idx]) !== JSON.stringify(desired)) {
22760
- plugin[idx] = desired;
22761
- return { nextConfig: { ...base, plugin }, alreadyInstalled: true, changed: true };
22772
+ function buildOpencodePackageJson(existing) {
22773
+ let base = {};
22774
+ if (existing) {
22775
+ try {
22776
+ const parsed = JSON.parse(existing);
22777
+ if (parsed && typeof parsed === "object") base = parsed;
22778
+ } catch {
22779
+ }
22762
22780
  }
22763
- return { nextConfig: { ...base, plugin }, alreadyInstalled: true, changed: false };
22781
+ const deps = { ...base.dependencies };
22782
+ deps[OPENCODE_PLUGIN_NAME] = OPENCODE_ADAPTER_VERSION;
22783
+ return JSON.stringify({ ...base, dependencies: deps }, null, 2) + "\n";
22764
22784
  }
22765
- function runInstallOpencode(dir, apply, presetName) {
22785
+ function opencodeJsonReferencesAdapter(config2) {
22786
+ const plugin = config2.plugin;
22787
+ if (!Array.isArray(plugin)) return false;
22788
+ return plugin.some((e) => {
22789
+ if (typeof e === "string") return e.includes(OPENCODE_PLUGIN_NAME);
22790
+ if (Array.isArray(e)) return typeof e[0] === "string" && e[0].includes(OPENCODE_PLUGIN_NAME);
22791
+ if (e && typeof e === "object") {
22792
+ const name = e.name;
22793
+ return typeof name === "string" && name.includes(OPENCODE_PLUGIN_NAME);
22794
+ }
22795
+ return false;
22796
+ });
22797
+ }
22798
+ function cleanOpencodeJson(existing) {
22799
+ let config2;
22800
+ try {
22801
+ config2 = JSON.parse(existing);
22802
+ } catch {
22803
+ throw new Error(`${OPENCODE_JSON} exists but is not valid JSON`);
22804
+ }
22805
+ if (!opencodeJsonReferencesAdapter(config2)) return null;
22806
+ const plugin = config2.plugin.filter((e) => {
22807
+ if (typeof e === "string") return !e.includes(OPENCODE_PLUGIN_NAME);
22808
+ if (Array.isArray(e)) return !(typeof e[0] === "string" && e[0].includes(OPENCODE_PLUGIN_NAME));
22809
+ if (e && typeof e === "object") {
22810
+ const name = e.name;
22811
+ return !(typeof name === "string" && name.includes(OPENCODE_PLUGIN_NAME));
22812
+ }
22813
+ return true;
22814
+ });
22815
+ const next = { ...config2 };
22816
+ if (plugin.length === 0) delete next.plugin;
22817
+ else next.plugin = plugin;
22818
+ return { content: JSON.stringify(next, null, 2) + "\n" };
22819
+ }
22820
+ function planOpencodeInstall(input) {
22821
+ const adapterConfig = input.adapterConfig ?? DEFAULT_OPENCODE_ADAPTER_CONFIG;
22822
+ const wrapperContent = buildOpencodeWrapper(adapterConfig);
22823
+ const packageJsonContent = buildOpencodePackageJson(input.existingPackageJson);
22824
+ const opencodeClean = input.existingOpencodeJson ? cleanOpencodeJson(input.existingOpencodeJson) : null;
22825
+ const opencodeJsonContent = opencodeClean?.content ?? null;
22826
+ const wrapperChanged = input.existingWrapper !== wrapperContent;
22827
+ const pkgChanged = input.existingPackageJson !== packageJsonContent;
22828
+ const opencodeChanged = opencodeJsonContent !== null;
22829
+ const alreadyInstalled = input.existingWrapper !== null;
22830
+ const changed = wrapperChanged || pkgChanged || opencodeChanged;
22831
+ return { wrapperContent, packageJsonContent, opencodeJsonContent, alreadyInstalled, changed };
22832
+ }
22833
+ function runInstallOpencode(dir, apply, presetName, installDeps = true) {
22766
22834
  let preset;
22767
- let adapterConfig;
22835
+ let adapterConfig = { ...DEFAULT_OPENCODE_ADAPTER_CONFIG };
22768
22836
  if (presetName) {
22769
22837
  preset = getPreset(presetName);
22770
22838
  if (!preset) throw new Error(`Unknown preset: ${presetName}`);
22771
- adapterConfig = { ...preset.adapter };
22839
+ adapterConfig = { ...adapterConfig, ...preset.adapter };
22772
22840
  }
22773
- const configPath = path2.join(dir, "opencode.json");
22774
- let raw = null;
22775
- try {
22776
- raw = fs2.readFileSync(configPath, "utf8");
22777
- } catch {
22778
- raw = null;
22779
- }
22780
- const existed = raw !== null;
22781
- let config2 = {};
22782
- if (raw !== null) {
22841
+ const wrapperPath = path2.join(dir, WRAPPER_REL);
22842
+ const packageJsonPath = path2.join(dir, PKG_REL);
22843
+ const opencodeJsonPath = path2.join(dir, OPENCODE_JSON);
22844
+ const readOrNull = (p) => {
22783
22845
  try {
22784
- config2 = JSON.parse(raw);
22846
+ return fs2.readFileSync(p, "utf8");
22785
22847
  } catch {
22786
- throw new Error(`${configPath} exists but is not valid JSON`);
22848
+ return null;
22787
22849
  }
22788
- }
22789
- const plan = planOpencodeInstall(config2, adapterConfig);
22850
+ };
22851
+ const existingWrapper = readOrNull(wrapperPath);
22852
+ const existingPackageJson = readOrNull(packageJsonPath);
22853
+ const existingOpencodeJson = readOrNull(opencodeJsonPath);
22854
+ const plan = planOpencodeInstall({
22855
+ existingWrapper,
22856
+ existingPackageJson,
22857
+ existingOpencodeJson,
22858
+ adapterConfig
22859
+ });
22790
22860
  let applied = false;
22861
+ let depsInstalled = null;
22862
+ let depsMessage;
22791
22863
  if (apply && plan.changed) {
22792
- fs2.writeFileSync(configPath, JSON.stringify(plan.nextConfig, null, 2) + "\n");
22864
+ fs2.mkdirSync(path2.dirname(wrapperPath), { recursive: true });
22865
+ fs2.writeFileSync(wrapperPath, plan.wrapperContent);
22866
+ fs2.writeFileSync(packageJsonPath, plan.packageJsonContent);
22867
+ if (plan.opencodeJsonContent !== null) fs2.writeFileSync(opencodeJsonPath, plan.opencodeJsonContent);
22793
22868
  applied = true;
22869
+ if (installDeps) {
22870
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
22871
+ const res = spawnSync(npm, ["install", "--silent"], {
22872
+ cwd: path2.dirname(packageJsonPath),
22873
+ encoding: "utf8"
22874
+ });
22875
+ if (res.error || res.status !== 0) {
22876
+ depsInstalled = false;
22877
+ depsMessage = `Could not auto-install the adapter; run \`npm install\` in ${path2.dirname(packageJsonPath)}.`;
22878
+ } else {
22879
+ depsInstalled = true;
22880
+ }
22881
+ }
22794
22882
  }
22795
- return { configPath, existed, applied, preset, ...plan };
22883
+ return {
22884
+ ...plan,
22885
+ wrapperPath,
22886
+ packageJsonPath,
22887
+ opencodeJsonPath,
22888
+ existed: existingWrapper !== null,
22889
+ applied,
22890
+ depsInstalled,
22891
+ depsMessage,
22892
+ preset
22893
+ };
22796
22894
  }
22797
22895
 
22798
22896
  // src/install-codex.ts
@@ -22801,7 +22899,56 @@ import path6 from "node:path";
22801
22899
 
22802
22900
  // ../adapter-codex/src/index.ts
22803
22901
  import path3 from "node:path";
22902
+
22903
+ // ../adapter-codex/src/hook.ts
22904
+ init_src();
22905
+ function reduceCodexPayload(rawJson, minLength) {
22906
+ const extracted = extractToolOutput(rawJson);
22907
+ if (extracted === null) return { response: "{}", event: null };
22908
+ const result = reduceAuto(extracted.output, minLength);
22909
+ if (!result.changed) return { response: "{}", event: null };
22910
+ const response = JSON.stringify({
22911
+ decision: "block",
22912
+ reason: `HarnessTrim reduced ${extracted.toolName} output (${result.reducer}):
22913
+
22914
+ ${result.output}`
22915
+ });
22916
+ return {
22917
+ response,
22918
+ event: {
22919
+ tool: extracted.toolName,
22920
+ reducer: result.reducer,
22921
+ beforeChars: extracted.output.length,
22922
+ afterChars: result.output.length
22923
+ }
22924
+ };
22925
+ }
22926
+ function extractToolOutput(rawJson) {
22927
+ let payload;
22928
+ try {
22929
+ payload = JSON.parse(rawJson);
22930
+ } catch {
22931
+ return null;
22932
+ }
22933
+ if (typeof payload !== "object" || payload === null) return null;
22934
+ const p = payload;
22935
+ const output = extractOutputText(p.tool_response);
22936
+ return output === null ? null : { toolName: typeof p.tool_name === "string" ? p.tool_name : "unknown", output };
22937
+ }
22938
+ function extractOutputText(response) {
22939
+ if (typeof response === "string") return response;
22940
+ if (typeof response !== "object" || response === null) return null;
22941
+ const r = response;
22942
+ for (const key of ["stdout", "output", "content"]) {
22943
+ if (typeof r[key] === "string") return r[key];
22944
+ }
22945
+ return null;
22946
+ }
22947
+
22948
+ // ../adapter-codex/src/index.ts
22804
22949
  var HARNESSTRIM_MARKER = "harnesstrim:begin";
22950
+ var CODEX_HOOK_COMMAND = "harnesstrim hook codex --metrics .harnesstrim/metrics.jsonl";
22951
+ var CODEX_HOOK_MATCHER = "^Bash$";
22805
22952
  var REDUCE_INSTRUCTION_SNIPPET = `<!-- ${HARNESSTRIM_MARKER} -->
22806
22953
  ## Token economy (HarnessTrim)
22807
22954
 
@@ -22814,6 +22961,45 @@ This keeps failures, errors, assertions, and summaries while dropping passing-te
22814
22961
  generated-file (lockfile/dist) diffs. Prefer the installed skills for output, review, and
22815
22962
  scaffolding discipline.
22816
22963
  <!-- harnesstrim:end -->`;
22964
+ function hasHarnessTrimHook(document) {
22965
+ const hooks = document.hooks;
22966
+ const post = hooks?.PostToolUse;
22967
+ if (!Array.isArray(post)) return false;
22968
+ return post.some(
22969
+ (entry) => Array.isArray(entry?.hooks) && entry.hooks.some((hook) => typeof hook?.command === "string" && hook.command.includes("harnesstrim hook codex"))
22970
+ );
22971
+ }
22972
+ function planCodexHookInstall(input) {
22973
+ let document = {};
22974
+ let action;
22975
+ if (input.hooksJsonContent === null) {
22976
+ action = "create";
22977
+ } else {
22978
+ let parsed;
22979
+ try {
22980
+ parsed = JSON.parse(input.hooksJsonContent);
22981
+ } catch {
22982
+ throw new Error(".codex/hooks.json is not valid JSON; refusing to overwrite it.");
22983
+ }
22984
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
22985
+ throw new Error(".codex/hooks.json must contain a JSON object; refusing to overwrite it.");
22986
+ }
22987
+ document = parsed;
22988
+ action = hasHarnessTrimHook(document) ? "present" : "patch";
22989
+ }
22990
+ if (action === "present") {
22991
+ return { hooksFile: path3.join(input.projectDir, ".codex", "hooks.json"), action, nextHooks: document };
22992
+ }
22993
+ const hooks = { ...document.hooks ?? {} };
22994
+ const post = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
22995
+ post.push({ matcher: CODEX_HOOK_MATCHER, hooks: [{ type: "command", command: CODEX_HOOK_COMMAND }] });
22996
+ hooks.PostToolUse = post;
22997
+ return {
22998
+ hooksFile: path3.join(input.projectDir, ".codex", "hooks.json"),
22999
+ action,
23000
+ nextHooks: { ...document, hooks }
23001
+ };
23002
+ }
22817
23003
  function planCodexInstall(input) {
22818
23004
  const skillsDest = path3.join(input.projectDir, ".codex", "skills");
22819
23005
  const existing = new Set(input.existingSkillNames);
@@ -22881,7 +23067,20 @@ function existingSkillNames(dest) {
22881
23067
  }
22882
23068
 
22883
23069
  // src/install-codex.ts
22884
- function runInstallCodex(dir, apply) {
23070
+ function readHooksJson(hooksPath) {
23071
+ try {
23072
+ return fs5.readFileSync(hooksPath, "utf8");
23073
+ } catch {
23074
+ return null;
23075
+ }
23076
+ }
23077
+ function applyHookPlan(plan, apply) {
23078
+ if (!apply || plan.action === "present") return false;
23079
+ fs5.mkdirSync(path6.dirname(plan.hooksFile), { recursive: true });
23080
+ fs5.writeFileSync(plan.hooksFile, JSON.stringify(plan.nextHooks, null, 2) + "\n");
23081
+ return true;
23082
+ }
23083
+ function runInstallCodex(dir, apply, hook = false) {
22885
23084
  const skillsSourceDir = resolveSkillsSourceDir();
22886
23085
  const skillNames = listShippedSkills(skillsSourceDir);
22887
23086
  const skillsDest = path6.join(dir, ".codex", "skills");
@@ -22899,6 +23098,9 @@ function runInstallCodex(dir, apply) {
22899
23098
  agentsMdContent,
22900
23099
  existingSkillNames: existingSkillNames(skillsDest)
22901
23100
  });
23101
+ const hooksPath = path6.join(dir, ".codex", "hooks.json");
23102
+ const hooksJsonContent = hook ? readHooksJson(hooksPath) : null;
23103
+ const hookPlan = hook ? planCodexHookInstall({ projectDir: dir, hooksJsonContent }) : null;
22902
23104
  const copied = [];
22903
23105
  let applied = false;
22904
23106
  if (apply) {
@@ -22912,9 +23114,20 @@ function runInstallCodex(dir, apply) {
22912
23114
  } else if (plan.instructionsAction === "append") {
22913
23115
  fs5.appendFileSync(plan.instructionsFile, "\n\n" + plan.instructionsSnippet + "\n");
22914
23116
  }
23117
+ if (hookPlan) applyHookPlan(hookPlan, true);
22915
23118
  applied = true;
22916
23119
  }
22917
- return { plan, applied, copied };
23120
+ return { plan, hookPlan, applied, copied };
23121
+ }
23122
+ function runInstallCodexGlobalHook(codexHome, apply) {
23123
+ const hooksPath = path6.join(codexHome, "hooks.json");
23124
+ const hookPlan = planCodexHookInstall({
23125
+ // The planner expects the directory that contains .codex; for a user-level config
23126
+ // the Codex home is itself that directory, so add its parent and use a normal path.
23127
+ projectDir: path6.dirname(codexHome),
23128
+ hooksJsonContent: readHooksJson(hooksPath)
23129
+ });
23130
+ return { hookPlan, applied: applyHookPlan(hookPlan, apply) };
22918
23131
  }
22919
23132
 
22920
23133
  // src/install-claude.ts
@@ -22924,7 +23137,7 @@ import path8 from "node:path";
22924
23137
  // ../adapter-claude/src/hook.ts
22925
23138
  init_src();
22926
23139
  function reduceClaudePayload(rawJson, minLength) {
22927
- const extracted = extractToolOutput(rawJson);
23140
+ const extracted = extractToolOutput2(rawJson);
22928
23141
  if (extracted === null) return { response: "{}", event: null };
22929
23142
  const result = reduceAuto(extracted.output, minLength);
22930
23143
  if (!result.changed) return { response: "{}", event: null };
@@ -22944,7 +23157,7 @@ function reduceClaudePayload(rawJson, minLength) {
22944
23157
  }
22945
23158
  };
22946
23159
  }
22947
- function extractToolOutput(rawJson) {
23160
+ function extractToolOutput2(rawJson) {
22948
23161
  let payload;
22949
23162
  try {
22950
23163
  payload = JSON.parse(rawJson);
@@ -22954,10 +23167,10 @@ function extractToolOutput(rawJson) {
22954
23167
  if (typeof payload !== "object" || payload === null) return null;
22955
23168
  const p = payload;
22956
23169
  const toolName = typeof p.tool_name === "string" ? p.tool_name : "unknown";
22957
- const output = extractOutputText(p);
23170
+ const output = extractOutputText2(p);
22958
23171
  return output === null ? null : { toolName, output };
22959
23172
  }
22960
- function extractOutputText(p) {
23173
+ function extractOutputText2(p) {
22961
23174
  if (typeof p.tool_output === "string") return p.tool_output;
22962
23175
  const resp = p.tool_response;
22963
23176
  if (typeof resp === "string") return resp;
@@ -22974,7 +23187,7 @@ function extractOutputText(p) {
22974
23187
  import path7 from "node:path";
22975
23188
  var HOOK_COMMAND = "harnesstrim hook claude";
22976
23189
  var HOOK_MATCHER = "Bash";
22977
- function hasHarnessTrimHook(settings) {
23190
+ function hasHarnessTrimHook2(settings) {
22978
23191
  const hooks = settings.hooks;
22979
23192
  const post = hooks?.PostToolUse;
22980
23193
  if (!Array.isArray(post)) return false;
@@ -23002,7 +23215,7 @@ function planClaudeInstall(input) {
23002
23215
  } catch {
23003
23216
  settings = {};
23004
23217
  }
23005
- action = hasHarnessTrimHook(settings) ? "present" : "patch";
23218
+ action = hasHarnessTrimHook2(settings) ? "present" : "patch";
23006
23219
  }
23007
23220
  const nextSettings = action === "present" ? settings : addHook(settings);
23008
23221
  return {
@@ -23129,7 +23342,7 @@ function runInstallPi(installDir, apply) {
23129
23342
  // src/install-hermes.ts
23130
23343
  import fs8 from "node:fs";
23131
23344
  import path12 from "node:path";
23132
- import { spawnSync } from "node:child_process";
23345
+ import { spawnSync as spawnSync2 } from "node:child_process";
23133
23346
 
23134
23347
  // ../adapter-hermes/src/index.ts
23135
23348
  import path11 from "node:path";
@@ -23196,7 +23409,7 @@ function runInstallHermes(installDir, apply) {
23196
23409
  fs8.writeFileSync(path12.join(pluginDest, ".installed"), markerFileContent2());
23197
23410
  copiedFiles.push(".installed");
23198
23411
  applied = true;
23199
- const enable = spawnSync("hermes", ["plugins", "enable", HERMES_PLUGIN_NAME], {
23412
+ const enable = spawnSync2("hermes", ["plugins", "enable", HERMES_PLUGIN_NAME], {
23200
23413
  encoding: "utf8"
23201
23414
  });
23202
23415
  if (enable.error) {
@@ -23262,19 +23475,38 @@ function renderDoctor(report) {
23262
23475
  }
23263
23476
  function renderInstall(result, apply) {
23264
23477
  const lines = [];
23265
- if (result.alreadyInstalled) {
23266
- lines.push(`Already installed: ${result.configPath} already wires in the HarnessTrim adapter.`);
23478
+ const wrapper = result.wrapperPath;
23479
+ const pkg = result.packageJsonPath;
23480
+ if (!result.changed) {
23481
+ lines.push(`Already installed and up to date: ${wrapper} loads the HarnessTrim adapter.`);
23482
+ if (result.preset) {
23483
+ lines.push("");
23484
+ lines.push(renderPresetAdvisory(result.preset));
23485
+ }
23267
23486
  return lines.join("\n");
23268
23487
  }
23269
23488
  if (apply) {
23270
- lines.push(`Wrote ${result.configPath}:`);
23271
- lines.push(JSON.stringify(result.nextConfig, null, 2));
23272
- } else {
23273
- lines.push(`Dry run \u2014 no files changed. This is what \`--apply\` would write to ${result.configPath}:`);
23489
+ lines.push(`Installed the OpenCode adapter as a local plugin:`);
23490
+ lines.push(` \u2022 wrote ${wrapper} (auto-loaded by OpenCode from .opencode/plugin/)`);
23491
+ lines.push(` \u2022 wrote ${pkg} (declares @harnesstrim/adapter-opencode)`);
23492
+ if (result.opencodeJsonContent !== null) {
23493
+ lines.push(` \u2022 cleaned the adapter entry out of ${result.opencodeJsonPath} (options live in the wrapper now)`);
23494
+ }
23495
+ if (result.depsInstalled === true) lines.push(` \u2022 installed the .opencode dependency`);
23496
+ else if (result.depsInstalled === false) lines.push(` ! ${result.depsMessage}`);
23274
23497
  lines.push("");
23275
- lines.push(JSON.stringify(result.nextConfig, null, 2));
23498
+ lines.push("Reduction is active; telemetry writes to .harnesstrim/metrics.jsonl.");
23499
+ lines.push("Reload OpenCode so it loads the plugin, then check `harnesstrim metrics`.");
23500
+ } else {
23501
+ lines.push(`Dry run \u2014 no files changed. \`--apply\` would:`);
23502
+ lines.push(` \u2022 write ${wrapper} (local plugin wrapper with the adapter options)`);
23503
+ lines.push(` \u2022 write ${pkg} (declare @harnesstrim/adapter-opencode) and install it`);
23504
+ if (result.opencodeJsonContent !== null) {
23505
+ lines.push(` \u2022 remove the stale adapter entry from ${result.opencodeJsonPath}`);
23506
+ }
23276
23507
  lines.push("");
23277
- lines.push("Re-run with `--apply` to write it.");
23508
+ lines.push("OpenCode's `plugin` config can't pass options, so the adapter is installed as a");
23509
+ lines.push("local plugin file instead. Re-run with `--apply` to write it.");
23278
23510
  }
23279
23511
  if (result.preset) {
23280
23512
  lines.push("");
@@ -23327,12 +23559,42 @@ function renderCodexInstall(result, apply) {
23327
23559
  lines.push("");
23328
23560
  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
23561
  lines.push(instr);
23562
+ if (result.hookPlan) {
23563
+ lines.push("");
23564
+ if (result.hookPlan.action === "present") {
23565
+ lines.push(`${result.hookPlan.hooksFile}: HarnessTrim Bash PostToolUse hook already present (no change).`);
23566
+ } else {
23567
+ lines.push(
23568
+ `${result.hookPlan.hooksFile}: experimental Bash PostToolUse hook ${apply ? result.hookPlan.action === "create" ? "created" : "added" : "would be added"}.`
23569
+ );
23570
+ lines.push("It reduces simple Bash output automatically and records JSONL telemetry in .harnesstrim/metrics.jsonl.");
23571
+ if (!apply) {
23572
+ lines.push("Resulting hooks.json:");
23573
+ lines.push(JSON.stringify(result.hookPlan.nextHooks, null, 2));
23574
+ }
23575
+ }
23576
+ }
23330
23577
  if (!apply) {
23331
23578
  lines.push("");
23332
23579
  lines.push("Dry run \u2014 nothing written. Re-run with `--apply`.");
23333
23580
  }
23334
23581
  return lines.join("\n");
23335
23582
  }
23583
+ function renderCodexGlobalHookInstall(result, apply) {
23584
+ const lines = [`${apply ? "Installed" : "Would install"} global Codex Bash PostToolUse hook`, ""];
23585
+ if (result.hookPlan.action === "present") {
23586
+ lines.push(`${result.hookPlan.hooksFile}: HarnessTrim hook already present (no change).`);
23587
+ } else {
23588
+ lines.push(`${result.hookPlan.hooksFile}: experimental Bash PostToolUse hook ${apply ? result.hookPlan.action === "create" ? "created" : "added" : "would be added"}.`);
23589
+ lines.push("It applies in trusted projects and writes telemetry to each project's .harnesstrim/metrics.jsonl.");
23590
+ if (!apply) {
23591
+ lines.push("Resulting hooks.json:");
23592
+ lines.push(JSON.stringify(result.hookPlan.nextHooks, null, 2));
23593
+ }
23594
+ }
23595
+ if (!apply) lines.push("", "Dry run \u2014 nothing written. Re-run with `--apply`.");
23596
+ return lines.join("\n");
23597
+ }
23336
23598
  function renderClaudeInstall(result, apply) {
23337
23599
  const { plan } = result;
23338
23600
  const lines = [];
@@ -23444,8 +23706,10 @@ Usage:
23444
23706
  harnesstrim install opencode [dir] Wire the adapter into opencode.json (dry-run)
23445
23707
  --apply Actually write the change
23446
23708
  --preset <name> Bake a policy preset's adapter config in
23447
- harnesstrim install codex [dir] Install skills + AGENTS.md reduce-pipe (dry-run)
23709
+ harnesstrim install codex [dir] Install skills + AGENTS.md reduction guidance (dry-run)
23448
23710
  --apply Actually write the change
23711
+ --hook Also install the experimental Bash PostToolUse hook
23712
+ --global With --hook, install it once in ~/.codex (no project files)
23449
23713
  harnesstrim install claude [dir] Install skills + PostToolUse reducer hook (dry-run)
23450
23714
  --apply Actually write the change
23451
23715
  harnesstrim install hermes [dir] Install Hermes plugin (dry-run)
@@ -23454,6 +23718,8 @@ Usage:
23454
23718
  --apply Actually write the change
23455
23719
  harnesstrim hook claude [--metrics <path>]
23456
23720
  PostToolUse hook runtime; --metrics records a TrimEvent per reduction
23721
+ harnesstrim hook codex [--metrics <path>]
23722
+ PostToolUse runtime for Codex's experimental Bash hook
23457
23723
  harnesstrim preset list List policy presets
23458
23724
  harnesstrim preset show <name> Show a preset in detail
23459
23725
  harnesstrim metrics [path] Summarize adapter telemetry (JSONL)
@@ -23477,7 +23743,9 @@ async function main(argv) {
23477
23743
  stats: { type: "boolean" },
23478
23744
  "min-length": { type: "string" },
23479
23745
  log: { type: "string" },
23480
- metrics: { type: "string" }
23746
+ metrics: { type: "string" },
23747
+ hook: { type: "boolean" },
23748
+ global: { type: "boolean" }
23481
23749
  }
23482
23750
  });
23483
23751
  const [command, ...rest] = positionals;
@@ -23501,7 +23769,15 @@ async function main(argv) {
23501
23769
  return 0;
23502
23770
  }
23503
23771
  if (target === "codex") {
23504
- console.log(renderCodexInstall(runInstallCodex(dir, apply), apply));
23772
+ if (values.global === true) {
23773
+ if (values.hook !== true) {
23774
+ console.error("`harnesstrim install codex --global` requires `--hook`.");
23775
+ return 1;
23776
+ }
23777
+ console.log(renderCodexGlobalHookInstall(runInstallCodexGlobalHook(path14.join(os2.homedir(), ".codex"), apply), apply));
23778
+ return 0;
23779
+ }
23780
+ console.log(renderCodexInstall(runInstallCodex(dir, apply, values.hook === true), apply));
23505
23781
  return 0;
23506
23782
  }
23507
23783
  if (target === "claude") {
@@ -23521,12 +23797,12 @@ async function main(argv) {
23521
23797
  }
23522
23798
  case "hook": {
23523
23799
  const which = rest[0];
23524
- if (which !== "claude") {
23525
- console.error(`Unknown hook target: ${which ?? "(none)"}. Supported: claude.`);
23800
+ if (which !== "claude" && which !== "codex") {
23801
+ console.error(`Unknown hook target: ${which ?? "(none)"}. Supported: claude, codex.`);
23526
23802
  return 1;
23527
23803
  }
23528
23804
  const input = await readStdin();
23529
- const { response, event } = reduceClaudePayload(input);
23805
+ const { response, event } = which === "claude" ? reduceClaudePayload(input) : reduceCodexPayload(input);
23530
23806
  process.stdout.write(response);
23531
23807
  if (values.metrics && event) {
23532
23808
  try {
@@ -23534,7 +23810,7 @@ async function main(argv) {
23534
23810
  fs10.mkdirSync(path14.dirname(p), { recursive: true });
23535
23811
  fs10.appendFileSync(
23536
23812
  p,
23537
- JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), harness: "claude", ...event }) + "\n"
23813
+ JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), harness: which, ...event }) + "\n"
23538
23814
  );
23539
23815
  } catch {
23540
23816
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnesstrim",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "HarnessTrim CLI: doctor (diagnose token waste), install adapters, run benchmarks.",
5
5
  "license": "MIT",
6
6
  "type": "module",