harnesstrim 0.0.3 → 0.0.5

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 +310 -115
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -1166,8 +1166,8 @@ var init_parseUtil = __esm({
1166
1166
  init_errors();
1167
1167
  init_en();
1168
1168
  makeIssue = (params) => {
1169
- const { data, path: path15, errorMaps, issueData } = params;
1170
- const fullPath = [...path15, ...issueData.path || []];
1169
+ const { data, path: path16, errorMaps, issueData } = params;
1170
+ const fullPath = [...path16, ...issueData.path || []];
1171
1171
  const fullIssue = {
1172
1172
  ...issueData,
1173
1173
  path: fullPath
@@ -1475,11 +1475,11 @@ var init_types2 = __esm({
1475
1475
  init_parseUtil();
1476
1476
  init_util();
1477
1477
  ParseInputLazyPath = class {
1478
- constructor(parent, value, path15, key) {
1478
+ constructor(parent, value, path16, key) {
1479
1479
  this._cachedPath = [];
1480
1480
  this.parent = parent;
1481
1481
  this.data = value;
1482
- this._path = path15;
1482
+ this._path = path16;
1483
1483
  this._key = key;
1484
1484
  }
1485
1485
  get path() {
@@ -5060,10 +5060,10 @@ function assignProp(target, prop, value) {
5060
5060
  configurable: true
5061
5061
  });
5062
5062
  }
5063
- function getElementAtPath(obj, path15) {
5064
- if (!path15)
5063
+ function getElementAtPath(obj, path16) {
5064
+ if (!path16)
5065
5065
  return obj;
5066
- return path15.reduce((acc, key) => acc?.[key], obj);
5066
+ return path16.reduce((acc, key) => acc?.[key], obj);
5067
5067
  }
5068
5068
  function promiseAllObject(promisesObj) {
5069
5069
  const keys = Object.keys(promisesObj);
@@ -5312,11 +5312,11 @@ function aborted(x, startIndex = 0) {
5312
5312
  }
5313
5313
  return false;
5314
5314
  }
5315
- function prefixIssues(path15, issues) {
5315
+ function prefixIssues(path16, issues) {
5316
5316
  return issues.map((iss) => {
5317
5317
  var _a;
5318
5318
  (_a = iss).path ?? (_a.path = []);
5319
- iss.path.unshift(path15);
5319
+ iss.path.unshift(path16);
5320
5320
  return iss;
5321
5321
  });
5322
5322
  }
@@ -17146,8 +17146,8 @@ var require_utils = __commonJS({
17146
17146
  }
17147
17147
  return ind;
17148
17148
  }
17149
- function removeDotSegments(path15) {
17150
- let input = path15;
17149
+ function removeDotSegments(path16) {
17150
+ let input = path16;
17151
17151
  const output = [];
17152
17152
  let nextSlash = -1;
17153
17153
  let len = 0;
@@ -17399,8 +17399,8 @@ var require_schemes = __commonJS({
17399
17399
  wsComponent.secure = void 0;
17400
17400
  }
17401
17401
  if (wsComponent.resourceName) {
17402
- const [path15, query] = wsComponent.resourceName.split("?");
17403
- wsComponent.path = path15 && path15 !== "/" ? path15 : void 0;
17402
+ const [path16, query] = wsComponent.resourceName.split("?");
17403
+ wsComponent.path = path16 && path16 !== "/" ? path16 : void 0;
17404
17404
  wsComponent.query = query;
17405
17405
  wsComponent.resourceName = void 0;
17406
17406
  }
@@ -20793,12 +20793,12 @@ var require_dist = __commonJS({
20793
20793
  throw new Error(`Unknown format "${name}"`);
20794
20794
  return f;
20795
20795
  };
20796
- function addFormats(ajv, list, fs11, exportName) {
20796
+ function addFormats(ajv, list, fs12, exportName) {
20797
20797
  var _a;
20798
20798
  var _b;
20799
20799
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
20800
20800
  for (const f of list)
20801
- ajv.addFormat(f, fs11[f]);
20801
+ ajv.addFormat(f, fs12[f]);
20802
20802
  }
20803
20803
  module.exports = exports = formatsPlugin;
20804
20804
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -22554,15 +22554,39 @@ var init_stdio2 = __esm({
22554
22554
  // ../mcp/src/server.ts
22555
22555
  var server_exports = {};
22556
22556
  __export(server_exports, {
22557
+ createFileSink: () => createFileSink,
22557
22558
  createServer: () => createServer,
22558
22559
  runReduceTool: () => runReduceTool,
22559
22560
  startStdioServer: () => startStdioServer
22560
22561
  });
22561
- function runReduceTool(text, minLength) {
22562
+ import fs10 from "node:fs";
22563
+ import path14 from "node:path";
22564
+ function createFileSink(metricsPath) {
22565
+ return (event) => {
22566
+ try {
22567
+ const p = path14.resolve(metricsPath);
22568
+ fs10.mkdirSync(path14.dirname(p), { recursive: true });
22569
+ fs10.appendFileSync(p, JSON.stringify(event) + "\n");
22570
+ } catch {
22571
+ }
22572
+ };
22573
+ }
22574
+ function runReduceTool(text, minLength, sink = noopSink) {
22562
22575
  const result = reduceAuto(text, minLength);
22576
+ if (result.changed) {
22577
+ sink({
22578
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
22579
+ harness: "mcp",
22580
+ tool: "reduce",
22581
+ reducer: result.reducer,
22582
+ beforeChars: text.length,
22583
+ afterChars: result.output.length
22584
+ });
22585
+ }
22563
22586
  return { content: [{ type: "text", text: result.output }] };
22564
22587
  }
22565
- function createServer() {
22588
+ function createServer(options = {}) {
22589
+ const sink = options.metricsPath ? createFileSink(options.metricsPath) : noopSink;
22566
22590
  const server = new McpServer({ name: "harnesstrim", version: "0.0.1" });
22567
22591
  server.registerTool(
22568
22592
  "reduce",
@@ -22574,16 +22598,17 @@ function createServer() {
22574
22598
  minLength: external_exports.number().optional().describe("Skip reduction for inputs shorter than this many characters (default 400)")
22575
22599
  }
22576
22600
  },
22577
- async ({ text, minLength }) => runReduceTool(text, minLength)
22601
+ async ({ text, minLength }) => runReduceTool(text, minLength, sink)
22578
22602
  );
22579
22603
  return server;
22580
22604
  }
22581
- async function startStdioServer() {
22582
- const server = createServer();
22605
+ async function startStdioServer(options = {}) {
22606
+ const metricsPath = options.metricsPath ?? process.env.HARNESSTRIM_TELEMETRY_PATH;
22607
+ const server = createServer({ metricsPath });
22583
22608
  const transport = new StdioServerTransport();
22584
22609
  await server.connect(transport);
22585
22610
  }
22586
- var REDUCE_DESCRIPTION;
22611
+ var noopSink, REDUCE_DESCRIPTION;
22587
22612
  var init_server3 = __esm({
22588
22613
  "../mcp/src/server.ts"() {
22589
22614
  "use strict";
@@ -22591,6 +22616,8 @@ var init_server3 = __esm({
22591
22616
  init_stdio2();
22592
22617
  init_zod();
22593
22618
  init_src();
22619
+ noopSink = () => {
22620
+ };
22594
22621
  REDUCE_DESCRIPTION = "Slim noisy text to its signal: keeps failures, errors, assertions and summaries while dropping passing-test noise and generated-file (lockfile/dist) diffs. Pass test-runner output or a git diff and use the returned text instead of the raw output. Deterministic and idempotent; returns the input unchanged if no reducer matches or it is too short.";
22595
22622
  }
22596
22623
  });
@@ -22598,8 +22625,8 @@ var init_server3 = __esm({
22598
22625
  // src/cli.ts
22599
22626
  init_src();
22600
22627
  import { parseArgs } from "node:util";
22601
- import fs10 from "node:fs";
22602
- import path14 from "node:path";
22628
+ import fs11 from "node:fs";
22629
+ import path15 from "node:path";
22603
22630
  import os2 from "node:os";
22604
22631
 
22605
22632
  // src/doctor.ts
@@ -22681,40 +22708,45 @@ function checkSkills(dir, findings) {
22681
22708
  }
22682
22709
  }
22683
22710
  function checkOpenCode(dir, findings) {
22684
- const raw = readIfExists(path.join(dir, "opencode.json"));
22685
- if (raw === null) {
22711
+ const hasOpencodeProject = readIfExists(path.join(dir, "opencode.json")) !== null || fs.existsSync(path.join(dir, ".opencode"));
22712
+ if (!hasOpencodeProject) {
22686
22713
  findings.push({
22687
22714
  severity: "info",
22688
- title: "No opencode.json found",
22689
- detail: "No OpenCode project config in this directory."
22715
+ title: "No OpenCode project config found",
22716
+ detail: "No opencode.json or .opencode/ in this directory."
22690
22717
  });
22691
22718
  return;
22692
22719
  }
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;
22720
+ const wrapper = path.join(dir, ".opencode", "plugin", "harnesstrim.ts");
22721
+ const wrapperPresent = fs.existsSync(wrapper);
22722
+ const raw = readIfExists(path.join(dir, "opencode.json"));
22723
+ let staleEntry = false;
22724
+ if (raw !== null) {
22725
+ try {
22726
+ staleEntry = extractPluginNames(JSON.parse(raw)).some((p) => p.includes("@harnesstrim/adapter-opencode"));
22727
+ } catch {
22728
+ }
22703
22729
  }
22704
- const plugins = extractPluginNames(config2);
22705
- const installed = plugins.some((p) => p.includes("@harnesstrim/adapter-opencode"));
22706
- if (installed) {
22730
+ if (wrapperPresent) {
22707
22731
  findings.push({
22708
22732
  severity: "ok",
22709
- title: "HarnessTrim OpenCode adapter is wired in",
22710
- detail: "@harnesstrim/adapter-opencode is present in opencode.json plugins."
22733
+ title: "HarnessTrim OpenCode adapter is installed",
22734
+ detail: ".opencode/plugin/harnesstrim.ts loads the adapter (OpenCode auto-loads it)."
22711
22735
  });
22712
22736
  } else {
22713
22737
  findings.push({
22714
22738
  severity: "warn",
22715
- title: "HarnessTrim adapter not installed in opencode.json",
22739
+ title: "HarnessTrim adapter not installed for OpenCode",
22716
22740
  detail: "The OpenCode adapter would slim tool output automatically.",
22717
- suggestion: "Run `harnesstrim install opencode` to wire it in."
22741
+ suggestion: "Run `harnesstrim install opencode --apply` to install the local plugin."
22742
+ });
22743
+ }
22744
+ if (staleEntry && !wrapperPresent) {
22745
+ findings.push({
22746
+ severity: "warn",
22747
+ title: "Stale adapter entry in opencode.json",
22748
+ 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.",
22749
+ suggestion: "Run `harnesstrim install opencode --apply` to migrate to the local plugin wrapper."
22718
22750
  });
22719
22751
  }
22720
22752
  }
@@ -22741,70 +22773,151 @@ function inspect(dir) {
22741
22773
  init_src();
22742
22774
  import fs2 from "node:fs";
22743
22775
  import path2 from "node:path";
22776
+ import { spawnSync } from "node:child_process";
22744
22777
  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
- );
22778
+ var OPENCODE_ADAPTER_VERSION = "^0.0.2";
22779
+ var DEFAULT_OPENCODE_ADAPTER_CONFIG = {
22780
+ mode: "active",
22781
+ telemetry: true,
22782
+ telemetryPath: ".harnesstrim/metrics.jsonl"
22783
+ };
22784
+ var WRAPPER_REL = path2.join(".opencode", "plugin", "harnesstrim.ts");
22785
+ var PKG_REL = path2.join(".opencode", "package.json");
22786
+ var OPENCODE_JSON = "opencode.json";
22787
+ function buildOpencodeWrapper(adapterConfig) {
22788
+ const opts = JSON.stringify(adapterConfig, null, 2).replace(/\n/g, "\n ");
22789
+ return `// Generated by \`harnesstrim install opencode\`. Do not reference this plugin from
22790
+ // opencode.json \u2014 OpenCode's \`plugin\` field is a string array and cannot pass options,
22791
+ // so HarnessTrim options live here and OpenCode auto-loads this file from .opencode/plugin/.
22792
+ // The dependency is declared in ../package.json. Edit the options below freely.
22793
+ import { HarnessTrim } from "${OPENCODE_PLUGIN_NAME}";
22794
+
22795
+ export const HarnessTrimPlugin = async (input) =>
22796
+ HarnessTrim(input, ${opts});
22797
+ `;
22749
22798
  }
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
- );
22799
+ function buildOpencodePackageJson(existing) {
22800
+ let base = {};
22801
+ if (existing) {
22802
+ try {
22803
+ const parsed = JSON.parse(existing);
22804
+ if (parsed && typeof parsed === "object") base = parsed;
22805
+ } catch {
22806
+ }
22807
+ }
22808
+ const deps = { ...base.dependencies };
22809
+ deps[OPENCODE_PLUGIN_NAME] = OPENCODE_ADAPTER_VERSION;
22810
+ return JSON.stringify({ ...base, dependencies: deps }, null, 2) + "\n";
22811
+ }
22812
+ function opencodeJsonReferencesAdapter(config2) {
22813
+ const plugin = config2.plugin;
22814
+ if (!Array.isArray(plugin)) return false;
22815
+ return plugin.some((e) => {
22816
+ if (typeof e === "string") return e.includes(OPENCODE_PLUGIN_NAME);
22817
+ if (Array.isArray(e)) return typeof e[0] === "string" && e[0].includes(OPENCODE_PLUGIN_NAME);
22818
+ if (e && typeof e === "object") {
22819
+ const name = e.name;
22820
+ return typeof name === "string" && name.includes(OPENCODE_PLUGIN_NAME);
22821
+ }
22822
+ return false;
22823
+ });
22754
22824
  }
22755
- function planOpencodeInstall(config2, adapterConfig) {
22756
- const base = typeof config2 === "object" && config2 !== null ? { ...config2 } : {};
22757
- const plugin = Array.isArray(base.plugin) ? [...base.plugin] : [];
22758
- const desired = adapterConfig ? [OPENCODE_PLUGIN_NAME, adapterConfig] : OPENCODE_PLUGIN_NAME;
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
- }
22767
- if (idx === -1) {
22768
- plugin.push(desired);
22769
- return { nextConfig: { ...base, plugin }, alreadyInstalled: false, changed: true };
22770
- }
22771
- if (adapterConfig && JSON.stringify(plugin[idx]) !== JSON.stringify(desired)) {
22772
- plugin[idx] = desired;
22773
- return { nextConfig: { ...base, plugin }, alreadyInstalled: true, changed: true };
22774
- }
22775
- return { nextConfig: { ...base, plugin }, alreadyInstalled: true, changed: false };
22776
- }
22777
- function runInstallOpencode(dir, apply, presetName) {
22825
+ function cleanOpencodeJson(existing) {
22826
+ let config2;
22827
+ try {
22828
+ config2 = JSON.parse(existing);
22829
+ } catch {
22830
+ throw new Error(`${OPENCODE_JSON} exists but is not valid JSON`);
22831
+ }
22832
+ if (!opencodeJsonReferencesAdapter(config2)) return null;
22833
+ const plugin = config2.plugin.filter((e) => {
22834
+ if (typeof e === "string") return !e.includes(OPENCODE_PLUGIN_NAME);
22835
+ if (Array.isArray(e)) return !(typeof e[0] === "string" && e[0].includes(OPENCODE_PLUGIN_NAME));
22836
+ if (e && typeof e === "object") {
22837
+ const name = e.name;
22838
+ return !(typeof name === "string" && name.includes(OPENCODE_PLUGIN_NAME));
22839
+ }
22840
+ return true;
22841
+ });
22842
+ const next = { ...config2 };
22843
+ if (plugin.length === 0) delete next.plugin;
22844
+ else next.plugin = plugin;
22845
+ return { content: JSON.stringify(next, null, 2) + "\n" };
22846
+ }
22847
+ function planOpencodeInstall(input) {
22848
+ const adapterConfig = input.adapterConfig ?? DEFAULT_OPENCODE_ADAPTER_CONFIG;
22849
+ const wrapperContent = buildOpencodeWrapper(adapterConfig);
22850
+ const packageJsonContent = buildOpencodePackageJson(input.existingPackageJson);
22851
+ const opencodeClean = input.existingOpencodeJson ? cleanOpencodeJson(input.existingOpencodeJson) : null;
22852
+ const opencodeJsonContent = opencodeClean?.content ?? null;
22853
+ const wrapperChanged = input.existingWrapper !== wrapperContent;
22854
+ const pkgChanged = input.existingPackageJson !== packageJsonContent;
22855
+ const opencodeChanged = opencodeJsonContent !== null;
22856
+ const alreadyInstalled = input.existingWrapper !== null;
22857
+ const changed = wrapperChanged || pkgChanged || opencodeChanged;
22858
+ return { wrapperContent, packageJsonContent, opencodeJsonContent, alreadyInstalled, changed };
22859
+ }
22860
+ function runInstallOpencode(dir, apply, presetName, installDeps = true) {
22778
22861
  let preset;
22779
- let adapterConfig;
22862
+ let adapterConfig = { ...DEFAULT_OPENCODE_ADAPTER_CONFIG };
22780
22863
  if (presetName) {
22781
22864
  preset = getPreset(presetName);
22782
22865
  if (!preset) throw new Error(`Unknown preset: ${presetName}`);
22783
- adapterConfig = { ...preset.adapter };
22784
- }
22785
- const configPath = path2.join(dir, "opencode.json");
22786
- let raw = null;
22787
- try {
22788
- raw = fs2.readFileSync(configPath, "utf8");
22789
- } catch {
22790
- raw = null;
22866
+ adapterConfig = { ...adapterConfig, ...preset.adapter };
22791
22867
  }
22792
- const existed = raw !== null;
22793
- let config2 = {};
22794
- if (raw !== null) {
22868
+ const wrapperPath = path2.join(dir, WRAPPER_REL);
22869
+ const packageJsonPath = path2.join(dir, PKG_REL);
22870
+ const opencodeJsonPath = path2.join(dir, OPENCODE_JSON);
22871
+ const readOrNull = (p) => {
22795
22872
  try {
22796
- config2 = JSON.parse(raw);
22873
+ return fs2.readFileSync(p, "utf8");
22797
22874
  } catch {
22798
- throw new Error(`${configPath} exists but is not valid JSON`);
22875
+ return null;
22799
22876
  }
22800
- }
22801
- const plan = planOpencodeInstall(config2, adapterConfig);
22877
+ };
22878
+ const existingWrapper = readOrNull(wrapperPath);
22879
+ const existingPackageJson = readOrNull(packageJsonPath);
22880
+ const existingOpencodeJson = readOrNull(opencodeJsonPath);
22881
+ const plan = planOpencodeInstall({
22882
+ existingWrapper,
22883
+ existingPackageJson,
22884
+ existingOpencodeJson,
22885
+ adapterConfig
22886
+ });
22802
22887
  let applied = false;
22888
+ let depsInstalled = null;
22889
+ let depsMessage;
22803
22890
  if (apply && plan.changed) {
22804
- fs2.writeFileSync(configPath, JSON.stringify(plan.nextConfig, null, 2) + "\n");
22891
+ fs2.mkdirSync(path2.dirname(wrapperPath), { recursive: true });
22892
+ fs2.writeFileSync(wrapperPath, plan.wrapperContent);
22893
+ fs2.writeFileSync(packageJsonPath, plan.packageJsonContent);
22894
+ if (plan.opencodeJsonContent !== null) fs2.writeFileSync(opencodeJsonPath, plan.opencodeJsonContent);
22805
22895
  applied = true;
22896
+ if (installDeps) {
22897
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
22898
+ const res = spawnSync(npm, ["install", "--silent"], {
22899
+ cwd: path2.dirname(packageJsonPath),
22900
+ encoding: "utf8"
22901
+ });
22902
+ if (res.error || res.status !== 0) {
22903
+ depsInstalled = false;
22904
+ depsMessage = `Could not auto-install the adapter; run \`npm install\` in ${path2.dirname(packageJsonPath)}.`;
22905
+ } else {
22906
+ depsInstalled = true;
22907
+ }
22908
+ }
22806
22909
  }
22807
- return { configPath, existed, applied, preset, ...plan };
22910
+ return {
22911
+ ...plan,
22912
+ wrapperPath,
22913
+ packageJsonPath,
22914
+ opencodeJsonPath,
22915
+ existed: existingWrapper !== null,
22916
+ applied,
22917
+ depsInstalled,
22918
+ depsMessage,
22919
+ preset
22920
+ };
22808
22921
  }
22809
22922
 
22810
22923
  // src/install-codex.ts
@@ -23101,6 +23214,19 @@ function extractOutputText2(p) {
23101
23214
  import path7 from "node:path";
23102
23215
  var HOOK_COMMAND = "harnesstrim hook claude";
23103
23216
  var HOOK_MATCHER = "Bash";
23217
+ var HARNESSTRIM_MARKER2 = "harnesstrim:begin";
23218
+ var REDUCE_INSTRUCTION_SNIPPET2 = `<!-- ${HARNESSTRIM_MARKER2} -->
23219
+ ## Token economy (HarnessTrim)
23220
+
23221
+ When a shell command produces long, noisy output \u2014 test runners, \`git diff\`, build logs,
23222
+ large file dumps \u2014 pipe it through the reducer so only the signal enters context:
23223
+
23224
+ <your command> 2>&1 | harnesstrim reduce --metrics .harnesstrim/metrics.jsonl
23225
+
23226
+ This keeps failures, errors, assertions, and summaries while dropping passing-test noise and
23227
+ generated-file (lockfile/dist) diffs, and records what was saved. Prefer the installed skills
23228
+ for output, review, and scaffolding discipline.
23229
+ <!-- harnesstrim:end -->`;
23104
23230
  function hasHarnessTrimHook2(settings) {
23105
23231
  const hooks = settings.hooks;
23106
23232
  const post = hooks?.PostToolUse;
@@ -23132,12 +23258,16 @@ function planClaudeInstall(input) {
23132
23258
  action = hasHarnessTrimHook2(settings) ? "present" : "patch";
23133
23259
  }
23134
23260
  const nextSettings = action === "present" ? settings : addHook(settings);
23261
+ const instructionsAction = input.claudeMdContent === null ? "create" : input.claudeMdContent.includes(HARNESSTRIM_MARKER2) ? "present" : "append";
23135
23262
  return {
23136
23263
  skillsDest,
23137
23264
  skills,
23138
23265
  settingsFile: path7.join(input.projectDir, ".claude", "settings.json"),
23139
23266
  settingsAction: action,
23140
- nextSettings
23267
+ nextSettings,
23268
+ instructionsFile: path7.join(input.projectDir, "CLAUDE.md"),
23269
+ instructionsAction,
23270
+ instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET2
23141
23271
  };
23142
23272
  }
23143
23273
  function addHook(settings) {
@@ -23162,11 +23292,19 @@ function runInstallClaude(dir, apply) {
23162
23292
  } catch {
23163
23293
  settingsJsonContent = null;
23164
23294
  }
23295
+ const claudeMdPath = path8.join(dir, "CLAUDE.md");
23296
+ let claudeMdContent = null;
23297
+ try {
23298
+ claudeMdContent = fs6.readFileSync(claudeMdPath, "utf8");
23299
+ } catch {
23300
+ claudeMdContent = null;
23301
+ }
23165
23302
  const plan = planClaudeInstall({
23166
23303
  projectDir: dir,
23167
23304
  skillsSourceDir,
23168
23305
  skillNames,
23169
23306
  settingsJsonContent,
23307
+ claudeMdContent,
23170
23308
  existingSkillNames: existingSkillNames(skillsDest)
23171
23309
  });
23172
23310
  const copied = [];
@@ -23181,6 +23319,11 @@ function runInstallClaude(dir, apply) {
23181
23319
  fs6.mkdirSync(path8.dirname(plan.settingsFile), { recursive: true });
23182
23320
  fs6.writeFileSync(plan.settingsFile, JSON.stringify(plan.nextSettings, null, 2) + "\n");
23183
23321
  }
23322
+ if (plan.instructionsAction === "create") {
23323
+ fs6.writeFileSync(plan.instructionsFile, plan.instructionsSnippet + "\n");
23324
+ } else if (plan.instructionsAction === "append") {
23325
+ fs6.appendFileSync(plan.instructionsFile, "\n\n" + plan.instructionsSnippet + "\n");
23326
+ }
23184
23327
  applied = true;
23185
23328
  }
23186
23329
  return { plan, applied, copied };
@@ -23256,7 +23399,7 @@ function runInstallPi(installDir, apply) {
23256
23399
  // src/install-hermes.ts
23257
23400
  import fs8 from "node:fs";
23258
23401
  import path12 from "node:path";
23259
- import { spawnSync } from "node:child_process";
23402
+ import { spawnSync as spawnSync2 } from "node:child_process";
23260
23403
 
23261
23404
  // ../adapter-hermes/src/index.ts
23262
23405
  import path11 from "node:path";
@@ -23323,7 +23466,7 @@ function runInstallHermes(installDir, apply) {
23323
23466
  fs8.writeFileSync(path12.join(pluginDest, ".installed"), markerFileContent2());
23324
23467
  copiedFiles.push(".installed");
23325
23468
  applied = true;
23326
- const enable = spawnSync("hermes", ["plugins", "enable", HERMES_PLUGIN_NAME], {
23469
+ const enable = spawnSync2("hermes", ["plugins", "enable", HERMES_PLUGIN_NAME], {
23327
23470
  encoding: "utf8"
23328
23471
  });
23329
23472
  if (enable.error) {
@@ -23389,19 +23532,38 @@ function renderDoctor(report) {
23389
23532
  }
23390
23533
  function renderInstall(result, apply) {
23391
23534
  const lines = [];
23392
- if (result.alreadyInstalled) {
23393
- lines.push(`Already installed: ${result.configPath} already wires in the HarnessTrim adapter.`);
23535
+ const wrapper = result.wrapperPath;
23536
+ const pkg = result.packageJsonPath;
23537
+ if (!result.changed) {
23538
+ lines.push(`Already installed and up to date: ${wrapper} loads the HarnessTrim adapter.`);
23539
+ if (result.preset) {
23540
+ lines.push("");
23541
+ lines.push(renderPresetAdvisory(result.preset));
23542
+ }
23394
23543
  return lines.join("\n");
23395
23544
  }
23396
23545
  if (apply) {
23397
- lines.push(`Wrote ${result.configPath}:`);
23398
- lines.push(JSON.stringify(result.nextConfig, null, 2));
23399
- } else {
23400
- lines.push(`Dry run \u2014 no files changed. This is what \`--apply\` would write to ${result.configPath}:`);
23546
+ lines.push(`Installed the OpenCode adapter as a local plugin:`);
23547
+ lines.push(` \u2022 wrote ${wrapper} (auto-loaded by OpenCode from .opencode/plugin/)`);
23548
+ lines.push(` \u2022 wrote ${pkg} (declares @harnesstrim/adapter-opencode)`);
23549
+ if (result.opencodeJsonContent !== null) {
23550
+ lines.push(` \u2022 cleaned the adapter entry out of ${result.opencodeJsonPath} (options live in the wrapper now)`);
23551
+ }
23552
+ if (result.depsInstalled === true) lines.push(` \u2022 installed the .opencode dependency`);
23553
+ else if (result.depsInstalled === false) lines.push(` ! ${result.depsMessage}`);
23401
23554
  lines.push("");
23402
- lines.push(JSON.stringify(result.nextConfig, null, 2));
23555
+ lines.push("Reduction is active; telemetry writes to .harnesstrim/metrics.jsonl.");
23556
+ lines.push("Reload OpenCode so it loads the plugin, then check `harnesstrim metrics`.");
23557
+ } else {
23558
+ lines.push(`Dry run \u2014 no files changed. \`--apply\` would:`);
23559
+ lines.push(` \u2022 write ${wrapper} (local plugin wrapper with the adapter options)`);
23560
+ lines.push(` \u2022 write ${pkg} (declare @harnesstrim/adapter-opencode) and install it`);
23561
+ if (result.opencodeJsonContent !== null) {
23562
+ lines.push(` \u2022 remove the stale adapter entry from ${result.opencodeJsonPath}`);
23563
+ }
23403
23564
  lines.push("");
23404
- lines.push("Re-run with `--apply` to write it.");
23565
+ lines.push("OpenCode's `plugin` config can't pass options, so the adapter is installed as a");
23566
+ lines.push("local plugin file instead. Re-run with `--apply` to write it.");
23405
23567
  }
23406
23568
  if (result.preset) {
23407
23569
  lines.push("");
@@ -23514,7 +23676,19 @@ function renderClaudeInstall(result, apply) {
23514
23676
  }
23515
23677
  }
23516
23678
  lines.push("");
23517
- lines.push("Note: the hook command is `harnesstrim hook claude` \u2014 ensure harnesstrim is on PATH.");
23679
+ if (plan.instructionsAction === "present") {
23680
+ lines.push(`${plan.instructionsFile}: reduce-pipe instruction already present (no change).`);
23681
+ } else {
23682
+ lines.push(
23683
+ `${plan.instructionsFile}: reduce-pipe instruction ${apply ? plan.instructionsAction === "create" ? "created" : "appended" : "would be added"}.`
23684
+ );
23685
+ }
23686
+ lines.push("");
23687
+ lines.push("Note: `harnesstrim` must be on PATH (used by both the hook and the reduce pipe).");
23688
+ lines.push(
23689
+ "The CLAUDE.md instruction is the effective path today: current Claude Code versions don't apply"
23690
+ );
23691
+ lines.push("the hook's updatedToolOutput, so piping through `harnesstrim reduce` is what saves tokens.");
23518
23692
  if (!apply) lines.push("Dry run \u2014 nothing written. Re-run with `--apply`.");
23519
23693
  return lines.join("\n");
23520
23694
  }
@@ -23618,8 +23792,11 @@ Usage:
23618
23792
  harnesstrim preset list List policy presets
23619
23793
  harnesstrim preset show <name> Show a preset in detail
23620
23794
  harnesstrim metrics [path] Summarize adapter telemetry (JSONL)
23621
- harnesstrim reduce [--stats] Slim stdin -> stdout (pipe noisy command output)
23622
- harnesstrim mcp Start the MCP server (stdio) exposing a reduce tool
23795
+ harnesstrim reduce [--stats] [--metrics <path>]
23796
+ Slim stdin -> stdout (pipe noisy command output);
23797
+ --metrics records a TrimEvent per reduction
23798
+ harnesstrim mcp [--metrics <path>] Start the MCP server (stdio) exposing a reduce tool;
23799
+ --metrics records a TrimEvent per reduction
23623
23800
  harnesstrim bench Run the Tier A reducer micro-benchmark
23624
23801
  harnesstrim --help Show this help
23625
23802
 
@@ -23669,7 +23846,7 @@ async function main(argv) {
23669
23846
  console.error("`harnesstrim install codex --global` requires `--hook`.");
23670
23847
  return 1;
23671
23848
  }
23672
- console.log(renderCodexGlobalHookInstall(runInstallCodexGlobalHook(path14.join(os2.homedir(), ".codex"), apply), apply));
23849
+ console.log(renderCodexGlobalHookInstall(runInstallCodexGlobalHook(path15.join(os2.homedir(), ".codex"), apply), apply));
23673
23850
  return 0;
23674
23851
  }
23675
23852
  console.log(renderCodexInstall(runInstallCodex(dir, apply, values.hook === true), apply));
@@ -23701,9 +23878,9 @@ async function main(argv) {
23701
23878
  process.stdout.write(response);
23702
23879
  if (values.metrics && event) {
23703
23880
  try {
23704
- const p = path14.resolve(values.metrics);
23705
- fs10.mkdirSync(path14.dirname(p), { recursive: true });
23706
- fs10.appendFileSync(
23881
+ const p = path15.resolve(values.metrics);
23882
+ fs11.mkdirSync(path15.dirname(p), { recursive: true });
23883
+ fs11.appendFileSync(
23707
23884
  p,
23708
23885
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), harness: which, ...event }) + "\n"
23709
23886
  );
@@ -23712,7 +23889,7 @@ async function main(argv) {
23712
23889
  }
23713
23890
  if (values.log) {
23714
23891
  try {
23715
- fs10.appendFileSync(
23892
+ fs11.appendFileSync(
23716
23893
  values.log,
23717
23894
  JSON.stringify({ inputChars: input.length, changed: response !== "{}", responseChars: response.length }) + "\n"
23718
23895
  );
@@ -23741,8 +23918,8 @@ async function main(argv) {
23741
23918
  return 1;
23742
23919
  }
23743
23920
  case "metrics": {
23744
- const path15 = rest[0] ?? DEFAULT_METRICS_PATH;
23745
- console.log(renderMetrics(loadMetrics(path15)));
23921
+ const path16 = rest[0] ?? DEFAULT_METRICS_PATH;
23922
+ console.log(renderMetrics(loadMetrics(path16)));
23746
23923
  return 0;
23747
23924
  }
23748
23925
  case "reduce": {
@@ -23755,6 +23932,24 @@ async function main(argv) {
23755
23932
  const input = await readStdin();
23756
23933
  const result = reducePipe(input, minLength);
23757
23934
  process.stdout.write(result.output);
23935
+ if (values.metrics && result.changed) {
23936
+ try {
23937
+ const p = path15.resolve(values.metrics);
23938
+ fs11.mkdirSync(path15.dirname(p), { recursive: true });
23939
+ fs11.appendFileSync(
23940
+ p,
23941
+ JSON.stringify({
23942
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
23943
+ harness: "pipe",
23944
+ tool: "reduce",
23945
+ reducer: result.reducer,
23946
+ beforeChars: result.beforeChars,
23947
+ afterChars: result.afterChars
23948
+ }) + "\n"
23949
+ );
23950
+ } catch {
23951
+ }
23952
+ }
23758
23953
  if (values.stats) {
23759
23954
  const note = result.changed ? `${result.reducer}: ${result.beforeChars} -> ${result.afterChars} chars` : "no reduction (no reducer matched or below min-length)";
23760
23955
  console.error(`[harnesstrim reduce] ${note}`);
@@ -23763,7 +23958,7 @@ async function main(argv) {
23763
23958
  }
23764
23959
  case "mcp": {
23765
23960
  const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server3(), server_exports));
23766
- await startStdioServer2();
23961
+ await startStdioServer2(values.metrics ? { metricsPath: values.metrics } : {});
23767
23962
  await new Promise(() => {
23768
23963
  });
23769
23964
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnesstrim",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "HarnessTrim CLI: doctor (diagnose token waste), install adapters, run benchmarks.",
5
5
  "license": "MIT",
6
6
  "type": "module",