harnesstrim 0.0.6 → 0.0.7

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.
@@ -225,14 +225,19 @@ def _write_metric(tool: str, reducer: str | None, before: int, after: int) -> No
225
225
  directory lazily and swallows any error — telemetry must never crash the plugin.
226
226
  """
227
227
  import datetime as _dt
228
+ import uuid as _uuid
228
229
 
229
230
  event = {
231
+ "schemaVersion": 1,
232
+ "eventId": str(_uuid.uuid4()),
230
233
  "ts": _dt.datetime.now(_dt.timezone.utc).isoformat(),
231
234
  "harness": "hermes",
232
235
  "tool": tool,
233
236
  "reducer": reducer,
234
237
  "beforeChars": before,
235
238
  "afterChars": after,
239
+ "beforeTokens": None,
240
+ "afterTokens": None,
236
241
  }
237
242
  try:
238
243
  METRICS_PATH.parent.mkdir(parents=True, exist_ok=True)
@@ -29,6 +29,11 @@ const MODE = env.HARNESSTRIM_MODE ?? "dryrun";
29
29
  const MIN_LENGTH = Number(env.HARNESSTRIM_MINLENGTH ?? "400") || 400;
30
30
  const MARKER = "[harnesstrim";
31
31
 
32
+ /** True when a text chunk should not be reduced: too short, or already reduced. */
33
+ export function shouldSkip(text: string, minLength: number): boolean {
34
+ return text.length < minLength || text.includes(MARKER);
35
+ }
36
+
32
37
  function reduceViaCli(text: string): string | null {
33
38
  try {
34
39
  const r = spawnSync("harnesstrim", ["reduce", "--min-length", String(MIN_LENGTH)], {
@@ -54,7 +59,7 @@ export default function harnesstrim(pi: ExtensionAPI): void {
54
59
  const content = event.content.map((chunk) => {
55
60
  if (chunk.type !== "text" || typeof chunk.text !== "string") return chunk;
56
61
  const text = chunk.text;
57
- if (text.length < MIN_LENGTH || text.includes(MARKER)) return chunk;
62
+ if (shouldSkip(text, MIN_LENGTH)) return chunk;
58
63
 
59
64
  const reduced = reduceViaCli(text);
60
65
  if (!reduced || reduced.length >= text.length) return chunk;
package/dist/cli.mjs CHANGED
@@ -713,6 +713,21 @@ var init_dispatch = __esm({
713
713
  });
714
714
 
715
715
  // ../core/src/metrics/trim-event.ts
716
+ import { randomUUID } from "node:crypto";
717
+ function makeTrimEvent(partial2) {
718
+ return {
719
+ schemaVersion: TRIM_EVENT_SCHEMA_VERSION,
720
+ eventId: randomUUID(),
721
+ ts: partial2.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
722
+ harness: partial2.harness,
723
+ tool: partial2.tool,
724
+ reducer: partial2.reducer,
725
+ beforeChars: partial2.beforeChars,
726
+ afterChars: partial2.afterChars,
727
+ beforeTokens: partial2.beforeTokens ?? null,
728
+ afterTokens: partial2.afterTokens ?? null
729
+ };
730
+ }
716
731
  function pct(before, after) {
717
732
  if (before === 0) return 0;
718
733
  return Math.round((1 - after / before) * 1e3) / 10;
@@ -753,7 +768,7 @@ function parseTrimEvents(jsonl) {
753
768
  } catch {
754
769
  continue;
755
770
  }
756
- if (isTrimEvent(parsed)) out.push(parsed);
771
+ if (isTrimEvent(parsed)) out.push(normalize(parsed));
757
772
  }
758
773
  return out;
759
774
  }
@@ -762,9 +777,25 @@ function isTrimEvent(value) {
762
777
  const v = value;
763
778
  return typeof v.beforeChars === "number" && typeof v.afterChars === "number" && typeof v.tool === "string" && (typeof v.reducer === "string" || v.reducer === null);
764
779
  }
780
+ function normalize(v) {
781
+ return {
782
+ schemaVersion: typeof v.schemaVersion === "number" ? v.schemaVersion : 0,
783
+ eventId: typeof v.eventId === "string" ? v.eventId : "",
784
+ ts: v.ts,
785
+ harness: v.harness,
786
+ tool: v.tool,
787
+ reducer: v.reducer,
788
+ beforeChars: v.beforeChars,
789
+ afterChars: v.afterChars,
790
+ beforeTokens: typeof v.beforeTokens === "number" ? v.beforeTokens : null,
791
+ afterTokens: typeof v.afterTokens === "number" ? v.afterTokens : null
792
+ };
793
+ }
794
+ var TRIM_EVENT_SCHEMA_VERSION;
765
795
  var init_trim_event = __esm({
766
796
  "../core/src/metrics/trim-event.ts"() {
767
797
  "use strict";
798
+ TRIM_EVENT_SCHEMA_VERSION = 1;
768
799
  }
769
800
  });
770
801
 
@@ -1239,8 +1270,8 @@ var init_parseUtil = __esm({
1239
1270
  init_errors();
1240
1271
  init_en();
1241
1272
  makeIssue = (params) => {
1242
- const { data, path: path16, errorMaps, issueData } = params;
1243
- const fullPath = [...path16, ...issueData.path || []];
1273
+ const { data, path: path17, errorMaps, issueData } = params;
1274
+ const fullPath = [...path17, ...issueData.path || []];
1244
1275
  const fullIssue = {
1245
1276
  ...issueData,
1246
1277
  path: fullPath
@@ -1548,11 +1579,11 @@ var init_types2 = __esm({
1548
1579
  init_parseUtil();
1549
1580
  init_util();
1550
1581
  ParseInputLazyPath = class {
1551
- constructor(parent, value, path16, key) {
1582
+ constructor(parent, value, path17, key) {
1552
1583
  this._cachedPath = [];
1553
1584
  this.parent = parent;
1554
1585
  this.data = value;
1555
- this._path = path16;
1586
+ this._path = path17;
1556
1587
  this._key = key;
1557
1588
  }
1558
1589
  get path() {
@@ -5133,10 +5164,10 @@ function assignProp(target, prop, value) {
5133
5164
  configurable: true
5134
5165
  });
5135
5166
  }
5136
- function getElementAtPath(obj, path16) {
5137
- if (!path16)
5167
+ function getElementAtPath(obj, path17) {
5168
+ if (!path17)
5138
5169
  return obj;
5139
- return path16.reduce((acc, key) => acc?.[key], obj);
5170
+ return path17.reduce((acc, key) => acc?.[key], obj);
5140
5171
  }
5141
5172
  function promiseAllObject(promisesObj) {
5142
5173
  const keys = Object.keys(promisesObj);
@@ -5385,11 +5416,11 @@ function aborted(x, startIndex = 0) {
5385
5416
  }
5386
5417
  return false;
5387
5418
  }
5388
- function prefixIssues(path16, issues) {
5419
+ function prefixIssues(path17, issues) {
5389
5420
  return issues.map((iss) => {
5390
5421
  var _a;
5391
5422
  (_a = iss).path ?? (_a.path = []);
5392
- iss.path.unshift(path16);
5423
+ iss.path.unshift(path17);
5393
5424
  return iss;
5394
5425
  });
5395
5426
  }
@@ -16225,8 +16256,8 @@ var require_resolve = __commonJS({
16225
16256
  }
16226
16257
  return count;
16227
16258
  }
16228
- function getFullPath(resolver, id = "", normalize) {
16229
- if (normalize !== false)
16259
+ function getFullPath(resolver, id = "", normalize2) {
16260
+ if (normalize2 !== false)
16230
16261
  id = normalizeId(id);
16231
16262
  const p = resolver.parse(id);
16232
16263
  return _getFullPath(resolver, p);
@@ -17219,8 +17250,8 @@ var require_utils = __commonJS({
17219
17250
  }
17220
17251
  return ind;
17221
17252
  }
17222
- function removeDotSegments(path16) {
17223
- let input = path16;
17253
+ function removeDotSegments(path17) {
17254
+ let input = path17;
17224
17255
  const output = [];
17225
17256
  let nextSlash = -1;
17226
17257
  let len = 0;
@@ -17472,8 +17503,8 @@ var require_schemes = __commonJS({
17472
17503
  wsComponent.secure = void 0;
17473
17504
  }
17474
17505
  if (wsComponent.resourceName) {
17475
- const [path16, query] = wsComponent.resourceName.split("?");
17476
- wsComponent.path = path16 && path16 !== "/" ? path16 : void 0;
17506
+ const [path17, query] = wsComponent.resourceName.split("?");
17507
+ wsComponent.path = path17 && path17 !== "/" ? path17 : void 0;
17477
17508
  wsComponent.query = query;
17478
17509
  wsComponent.resourceName = void 0;
17479
17510
  }
@@ -17622,7 +17653,7 @@ var require_fast_uri = __commonJS({
17622
17653
  "use strict";
17623
17654
  var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
17624
17655
  var { SCHEMES, getSchemeHandler } = require_schemes();
17625
- function normalize(uri, options) {
17656
+ function normalize2(uri, options) {
17626
17657
  if (typeof uri === "string") {
17627
17658
  uri = /** @type {T} */
17628
17659
  normalizeString(uri, options);
@@ -17889,7 +17920,7 @@ var require_fast_uri = __commonJS({
17889
17920
  }
17890
17921
  var fastUri = {
17891
17922
  SCHEMES,
17892
- normalize,
17923
+ normalize: normalize2,
17893
17924
  resolve,
17894
17925
  resolveComponent,
17895
17926
  equal,
@@ -20866,12 +20897,12 @@ var require_dist = __commonJS({
20866
20897
  throw new Error(`Unknown format "${name}"`);
20867
20898
  return f;
20868
20899
  };
20869
- function addFormats(ajv, list, fs12, exportName) {
20900
+ function addFormats(ajv, list, fs13, exportName) {
20870
20901
  var _a;
20871
20902
  var _b;
20872
20903
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
20873
20904
  for (const f of list)
20874
- ajv.addFormat(f, fs12[f]);
20905
+ ajv.addFormat(f, fs13[f]);
20875
20906
  }
20876
20907
  module.exports = exports = formatsPlugin;
20877
20908
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -22632,14 +22663,14 @@ __export(server_exports, {
22632
22663
  runReduceTool: () => runReduceTool,
22633
22664
  startStdioServer: () => startStdioServer
22634
22665
  });
22635
- import fs10 from "node:fs";
22636
- import path14 from "node:path";
22666
+ import fs11 from "node:fs";
22667
+ import path15 from "node:path";
22637
22668
  function createFileSink(metricsPath) {
22638
22669
  return (event) => {
22639
22670
  try {
22640
- const p = path14.resolve(metricsPath);
22641
- fs10.mkdirSync(path14.dirname(p), { recursive: true });
22642
- fs10.appendFileSync(p, JSON.stringify(event) + "\n");
22671
+ const p = path15.resolve(metricsPath);
22672
+ fs11.mkdirSync(path15.dirname(p), { recursive: true });
22673
+ fs11.appendFileSync(p, JSON.stringify(event) + "\n");
22643
22674
  } catch {
22644
22675
  }
22645
22676
  };
@@ -22647,14 +22678,15 @@ function createFileSink(metricsPath) {
22647
22678
  function runReduceTool(text, minLength, sink = noopSink) {
22648
22679
  const result = reduceAuto(text, minLength);
22649
22680
  if (result.changed) {
22650
- sink({
22651
- ts: (/* @__PURE__ */ new Date()).toISOString(),
22652
- harness: "mcp",
22653
- tool: "reduce",
22654
- reducer: result.reducer,
22655
- beforeChars: text.length,
22656
- afterChars: result.output.length
22657
- });
22681
+ sink(
22682
+ makeTrimEvent({
22683
+ harness: "mcp",
22684
+ tool: "reduce",
22685
+ reducer: result.reducer,
22686
+ beforeChars: text.length,
22687
+ afterChars: result.output.length
22688
+ })
22689
+ );
22658
22690
  }
22659
22691
  return { content: [{ type: "text", text: result.output }] };
22660
22692
  }
@@ -22698,8 +22730,8 @@ var init_server3 = __esm({
22698
22730
  // src/cli.ts
22699
22731
  init_src();
22700
22732
  import { parseArgs } from "node:util";
22701
- import fs11 from "node:fs";
22702
- import path15 from "node:path";
22733
+ import fs12 from "node:fs";
22734
+ import path16 from "node:path";
22703
22735
  import os4 from "node:os";
22704
22736
 
22705
22737
  // src/doctor.ts
@@ -22930,7 +22962,7 @@ function planOpencodeInstall(input) {
22930
22962
  const changed = wrapperChanged || pkgChanged || opencodeChanged;
22931
22963
  return { wrapperContent, packageJsonContent, opencodeJsonContent, alreadyInstalled, changed };
22932
22964
  }
22933
- function runInstallOpencode(dir, apply, presetName, installDeps = true) {
22965
+ function runInstallOpencode(dir, apply, presetName, installDeps = true, options = {}) {
22934
22966
  let preset;
22935
22967
  let adapterConfig = { ...DEFAULT_OPENCODE_ADAPTER_CONFIG };
22936
22968
  if (presetName) {
@@ -22938,19 +22970,22 @@ function runInstallOpencode(dir, apply, presetName, installDeps = true) {
22938
22970
  if (!preset) throw new Error(`Unknown preset: ${presetName}`);
22939
22971
  adapterConfig = { ...adapterConfig, ...preset.adapter };
22940
22972
  }
22973
+ if (options.mode !== void 0) adapterConfig.mode = options.mode;
22974
+ if (options.minLength !== void 0) adapterConfig.minLength = options.minLength;
22975
+ if (options.tools !== void 0) adapterConfig.toolFilter = options.tools;
22941
22976
  const wrapperPath = path2.join(dir, WRAPPER_REL);
22942
22977
  const packageJsonPath = path2.join(dir, PKG_REL);
22943
22978
  const opencodeJsonPath = path2.join(dir, OPENCODE_JSON);
22944
- const readOrNull = (p) => {
22979
+ const readOrNull2 = (p) => {
22945
22980
  try {
22946
22981
  return fs2.readFileSync(p, "utf8");
22947
22982
  } catch {
22948
22983
  return null;
22949
22984
  }
22950
22985
  };
22951
- const existingWrapper = readOrNull(wrapperPath);
22952
- const existingPackageJson = readOrNull(packageJsonPath);
22953
- const existingOpencodeJson = readOrNull(opencodeJsonPath);
22986
+ const existingWrapper = readOrNull2(wrapperPath);
22987
+ const existingPackageJson = readOrNull2(packageJsonPath);
22988
+ const existingOpencodeJson = readOrNull2(opencodeJsonPath);
22954
22989
  const plan = planOpencodeInstall({
22955
22990
  existingWrapper,
22956
22991
  existingPackageJson,
@@ -23133,6 +23168,7 @@ function planCodexHookInstall(input) {
23133
23168
  };
23134
23169
  }
23135
23170
  function planCodexInstall(input) {
23171
+ const includeInstructions = input.includeInstructions ?? true;
23136
23172
  const skillsDest = path3.join(input.projectDir, ".codex", "skills");
23137
23173
  const existing = new Set(input.existingSkillNames);
23138
23174
  const skills = input.skillNames.map((name) => ({
@@ -23142,7 +23178,9 @@ function planCodexInstall(input) {
23142
23178
  present: existing.has(name)
23143
23179
  }));
23144
23180
  let instructionsAction;
23145
- if (input.agentsMdContent === null) {
23181
+ if (!includeInstructions) {
23182
+ instructionsAction = "skip";
23183
+ } else if (input.agentsMdContent === null) {
23146
23184
  instructionsAction = "create";
23147
23185
  } else if (input.agentsMdContent.includes(HARNESSTRIM_MARKER)) {
23148
23186
  instructionsAction = "present";
@@ -23154,7 +23192,8 @@ function planCodexInstall(input) {
23154
23192
  skills,
23155
23193
  instructionsFile: path3.join(input.projectDir, "AGENTS.md"),
23156
23194
  instructionsAction,
23157
- instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET
23195
+ instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET,
23196
+ changed: skills.some((s) => !s.present) || instructionsAction !== "present" && instructionsAction !== "skip"
23158
23197
  };
23159
23198
  }
23160
23199
 
@@ -23219,7 +23258,7 @@ function applyHookPlan(plan, apply) {
23219
23258
  fs5.writeFileSync(plan.hooksFile, JSON.stringify(plan.nextHooks, null, 2) + "\n");
23220
23259
  return true;
23221
23260
  }
23222
- function runInstallCodex(dir, apply, hook = false) {
23261
+ function runInstallCodex(dir, apply, hook = false, options = {}) {
23223
23262
  const skillsSourceDir = resolveSkillsSourceDir();
23224
23263
  const skillNames = listShippedSkills(skillsSourceDir);
23225
23264
  const skillsDest = path6.join(dir, ".codex", "skills");
@@ -23235,14 +23274,16 @@ function runInstallCodex(dir, apply, hook = false) {
23235
23274
  skillsSourceDir,
23236
23275
  skillNames,
23237
23276
  agentsMdContent,
23238
- existingSkillNames: existingSkillNames(skillsDest)
23277
+ existingSkillNames: existingSkillNames(skillsDest),
23278
+ includeInstructions: options.includeInstructions
23239
23279
  });
23240
23280
  const hooksPath = path6.join(dir, ".codex", "hooks.json");
23241
23281
  const hooksJsonContent = hook ? readHooksJson(hooksPath) : null;
23242
23282
  const hookPlan = hook ? planCodexHookInstall({ projectDir: dir, hooksJsonContent, hookCommand: resolveCodexHookCommand() }) : null;
23243
23283
  const copied = [];
23284
+ const hookChanged = hookPlan !== null && hookPlan.action !== "present";
23244
23285
  let applied = false;
23245
- if (apply) {
23286
+ if (apply && (plan.changed || hookChanged)) {
23246
23287
  for (const skill of plan.skills) {
23247
23288
  if (skill.present) continue;
23248
23289
  fs5.cpSync(skill.from, skill.to, { recursive: true });
@@ -23253,7 +23294,7 @@ function runInstallCodex(dir, apply, hook = false) {
23253
23294
  } else if (plan.instructionsAction === "append") {
23254
23295
  fs5.appendFileSync(plan.instructionsFile, "\n\n" + plan.instructionsSnippet + "\n");
23255
23296
  }
23256
- if (hookPlan) applyHookPlan(hookPlan, true);
23297
+ if (hookPlan && hookChanged) applyHookPlan(hookPlan, true);
23257
23298
  applied = true;
23258
23299
  }
23259
23300
  return { plan, hookPlan, applied, copied };
@@ -23349,6 +23390,8 @@ function hasHarnessTrimHook2(settings) {
23349
23390
  );
23350
23391
  }
23351
23392
  function planClaudeInstall(input) {
23393
+ const includeHook = input.includeHook ?? true;
23394
+ const includeInstructions = input.includeInstructions ?? true;
23352
23395
  const skillsDest = path7.join(input.projectDir, ".claude", "skills");
23353
23396
  const existing = new Set(input.existingSkillNames);
23354
23397
  const skills = input.skillNames.map((name) => ({
@@ -23370,8 +23413,12 @@ function planClaudeInstall(input) {
23370
23413
  }
23371
23414
  action = hasHarnessTrimHook2(settings) ? "present" : "patch";
23372
23415
  }
23373
- const nextSettings = action === "present" ? settings : addHook(settings);
23374
- const instructionsAction = input.claudeMdContent === null ? "create" : input.claudeMdContent.includes(HARNESSTRIM_MARKER2) ? "present" : "append";
23416
+ if (!includeHook) {
23417
+ action = "skip";
23418
+ settings = input.settingsJsonContent === null ? {} : settings;
23419
+ }
23420
+ const nextSettings = action === "present" || action === "skip" ? settings : addHook(settings);
23421
+ const instructionsAction = !includeInstructions ? "skip" : input.claudeMdContent === null ? "create" : input.claudeMdContent.includes(HARNESSTRIM_MARKER2) ? "present" : "append";
23375
23422
  return {
23376
23423
  skillsDest,
23377
23424
  skills,
@@ -23380,7 +23427,8 @@ function planClaudeInstall(input) {
23380
23427
  nextSettings,
23381
23428
  instructionsFile: path7.join(input.projectDir, "CLAUDE.md"),
23382
23429
  instructionsAction,
23383
- instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET2
23430
+ instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET2,
23431
+ changed: skills.some((s) => !s.present) || action !== "present" && action !== "skip" || instructionsAction !== "present" && instructionsAction !== "skip"
23384
23432
  };
23385
23433
  }
23386
23434
  function addHook(settings) {
@@ -23394,7 +23442,7 @@ function addHook(settings) {
23394
23442
  }
23395
23443
 
23396
23444
  // src/install-claude.ts
23397
- function runInstallClaude(dir, apply) {
23445
+ function runInstallClaude(dir, apply, options = {}) {
23398
23446
  const skillsSourceDir = resolveSkillsSourceDir();
23399
23447
  const skillNames = listShippedSkills(skillsSourceDir);
23400
23448
  const skillsDest = path8.join(dir, ".claude", "skills");
@@ -23418,17 +23466,19 @@ function runInstallClaude(dir, apply) {
23418
23466
  skillNames,
23419
23467
  settingsJsonContent,
23420
23468
  claudeMdContent,
23421
- existingSkillNames: existingSkillNames(skillsDest)
23469
+ existingSkillNames: existingSkillNames(skillsDest),
23470
+ includeHook: options.includeHook,
23471
+ includeInstructions: options.includeInstructions
23422
23472
  });
23423
23473
  const copied = [];
23424
23474
  let applied = false;
23425
- if (apply) {
23475
+ if (apply && plan.changed) {
23426
23476
  for (const skill of plan.skills) {
23427
23477
  if (skill.present) continue;
23428
23478
  fs6.cpSync(skill.from, skill.to, { recursive: true });
23429
23479
  copied.push(skill.name);
23430
23480
  }
23431
- if (plan.settingsAction !== "present") {
23481
+ if (plan.settingsAction !== "present" && plan.settingsAction !== "skip") {
23432
23482
  fs6.mkdirSync(path8.dirname(plan.settingsFile), { recursive: true });
23433
23483
  fs6.writeFileSync(plan.settingsFile, JSON.stringify(plan.nextSettings, null, 2) + "\n");
23434
23484
  }
@@ -23499,12 +23549,16 @@ function runInstallPi(installDir, apply) {
23499
23549
  let applied = false;
23500
23550
  if (apply) {
23501
23551
  fs7.mkdirSync(dest, { recursive: true });
23502
- for (const entry of fs7.readdirSync(extensionSourceDir, { withFileTypes: true })) {
23503
- if (!entry.isDirectory()) {
23504
- fs7.copyFileSync(path10.join(extensionSourceDir, entry.name), path10.join(dest, entry.name));
23505
- copiedFiles.push(entry.name);
23552
+ const sourceFiles = fs7.readdirSync(extensionSourceDir, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name);
23553
+ for (const existing of fs7.readdirSync(dest)) {
23554
+ if (existing !== ".installed" && !sourceFiles.includes(existing)) {
23555
+ fs7.rmSync(path10.join(dest, existing), { recursive: true, force: true });
23506
23556
  }
23507
23557
  }
23558
+ for (const entry of sourceFiles) {
23559
+ fs7.copyFileSync(path10.join(extensionSourceDir, entry), path10.join(dest, entry));
23560
+ copiedFiles.push(entry);
23561
+ }
23508
23562
  fs7.writeFileSync(path10.join(dest, ".installed"), markerFileContent());
23509
23563
  copiedFiles.push(".installed");
23510
23564
  applied = true;
@@ -23621,7 +23675,7 @@ function loadMetrics(filePath) {
23621
23675
  // package.json
23622
23676
  var package_default = {
23623
23677
  name: "harnesstrim",
23624
- version: "0.0.6",
23678
+ version: "0.0.7",
23625
23679
  description: "HarnessTrim CLI: doctor (diagnose token waste), install adapters, run benchmarks.",
23626
23680
  license: "MIT",
23627
23681
  type: "module",
@@ -23669,6 +23723,324 @@ function reducePipe(input, minLength) {
23669
23723
  return { ...result, beforeChars: input.length, afterChars: result.output.length };
23670
23724
  }
23671
23725
 
23726
+ // src/capabilities.ts
23727
+ var CAPABILITIES = {
23728
+ harnesses: {
23729
+ opencode: {
23730
+ adapter: "@harnesstrim/adapter-opencode",
23731
+ surfaces: [
23732
+ "tool.execute.after \u2014 slims noisy tool output in place before it enters context",
23733
+ "experimental.session.compacting \u2014 injects compaction-handoff guidance"
23734
+ ],
23735
+ narrowing: [
23736
+ { flag: "--mode active|dryrun|off", produces: "bake the reduction mode into the generated wrapper (active/dryrun/off)" },
23737
+ { flag: "--min-length <n>", produces: "leave tool outputs shorter than n chars untouched (overrides preset)" },
23738
+ { flag: "--tools <name,...>", produces: "confine reduction to a subset of tool families (e.g. bash,read)" },
23739
+ { flag: "--preset <name>", produces: "bake a policy preset's adapter config into the wrapper" }
23740
+ ],
23741
+ writeSet: [
23742
+ ".opencode/plugin/harnesstrim.ts",
23743
+ ".opencode/package.json",
23744
+ "opencode.json (cleans a stale adapter entry, never adds one)"
23745
+ ]
23746
+ },
23747
+ codex: {
23748
+ adapter: "@harnesstrim/adapter-codex",
23749
+ surfaces: [
23750
+ "PostToolUse Bash hook \u2014 deterministic reduction of simple Bash output (optional --hook)",
23751
+ "AGENTS.md reduce-pipe instruction \u2014 model pipes noisy output through `harnesstrim reduce`",
23752
+ "MCP reduce tool \u2014 deterministic, instruction-free reduction (separate `harnesstrim mcp`)"
23753
+ ],
23754
+ narrowing: [
23755
+ { flag: "--no-instructions", produces: "skills only \u2014 no AGENTS.md reduce-pipe instruction" },
23756
+ { flag: "--hook", produces: "also install the experimental Bash PostToolUse hook" },
23757
+ { flag: "--global", produces: "install the hook once in ~/.codex (with --hook)" }
23758
+ ],
23759
+ writeSet: [".codex/skills/", "AGENTS.md (marker-guarded snippet)", ".codex/hooks.json (with --hook)"]
23760
+ },
23761
+ claude: {
23762
+ adapter: "@harnesstrim/adapter-claude",
23763
+ surfaces: [
23764
+ "PostToolUse Bash hook \u2014 spec-correct updatedToolOutput (not honored by Claude Code 2.1.37\u20132.1.212)",
23765
+ "CLAUDE.md reduce-pipe instruction \u2014 the effective reduction path on current Claude Code"
23766
+ ],
23767
+ narrowing: [
23768
+ { flag: "--no-hook", produces: "skills only \u2014 no PostToolUse hook in .claude/settings.json" },
23769
+ { flag: "--no-instructions", produces: "skills only \u2014 no CLAUDE.md reduce-pipe instruction" }
23770
+ ],
23771
+ writeSet: [".claude/skills/", ".claude/settings.json", "CLAUDE.md (marker-guarded snippet)"]
23772
+ },
23773
+ hermes: {
23774
+ adapter: "@harnesstrim/adapter-hermes",
23775
+ surfaces: ["transform_tool_result \u2014 deterministic reduction before the result enters context"],
23776
+ narrowing: [],
23777
+ writeSet: [".hermes/plugins/harnesstrim/ (incl. .installed marker)"]
23778
+ },
23779
+ pi: {
23780
+ adapter: "@harnesstrim/adapter-pi",
23781
+ surfaces: ["tool_result \u2014 deterministic reduction of text chunks in structured results"],
23782
+ narrowing: [],
23783
+ writeSet: [".pi/extensions/harnesstrim/ or .pi/agent/extensions/harnesstrim/ (incl. .installed marker)"]
23784
+ }
23785
+ }
23786
+ };
23787
+ function getCapabilities(version2) {
23788
+ return { version: version2, ...CAPABILITIES };
23789
+ }
23790
+
23791
+ // src/uninstall.ts
23792
+ import fs10 from "node:fs";
23793
+ import path14 from "node:path";
23794
+ function readOrNull(p) {
23795
+ try {
23796
+ return fs10.readFileSync(p, "utf8");
23797
+ } catch {
23798
+ return null;
23799
+ }
23800
+ }
23801
+ function stripMarkedRegion(content, marker) {
23802
+ const begin = `<!-- ${marker} -->`;
23803
+ const end = "<!-- harnesstrim:end -->";
23804
+ const start = content.indexOf(begin);
23805
+ if (start === -1) return content;
23806
+ const endIdx = content.indexOf(end, start);
23807
+ if (endIdx === -1) return content;
23808
+ const after = content.slice(endIdx + end.length);
23809
+ const before = content.slice(0, start).replace(/\s+$/, "");
23810
+ const next = (before + "\n" + after.replace(/^\s+/, "")).replace(/\n{3,}/g, "\n\n").trim();
23811
+ return next;
23812
+ }
23813
+ function stripHookEntries(settings) {
23814
+ const hooks = settings.hooks;
23815
+ if (!hooks || !Array.isArray(hooks.PostToolUse)) return { next: settings, removed: false };
23816
+ const post = hooks.PostToolUse;
23817
+ const kept = post.filter((entry) => {
23818
+ const hooksArr = entry.hooks;
23819
+ if (!Array.isArray(hooksArr)) return true;
23820
+ return !hooksArr.some((h) => typeof h.command === "string" && h.command.includes("harnesstrim hook"));
23821
+ });
23822
+ if (kept.length === post.length) return { next: settings, removed: false };
23823
+ const nextHooks = { ...hooks };
23824
+ nextHooks.PostToolUse = kept;
23825
+ const next = { ...settings, hooks: nextHooks };
23826
+ return { next, removed: true };
23827
+ }
23828
+ function isSkillDir(pathName) {
23829
+ try {
23830
+ return fs10.statSync(pathName).isDirectory();
23831
+ } catch {
23832
+ return false;
23833
+ }
23834
+ }
23835
+ function markerPresent3(dest) {
23836
+ try {
23837
+ return fs10.statSync(path14.join(dest, ".installed")).isFile();
23838
+ } catch {
23839
+ return false;
23840
+ }
23841
+ }
23842
+ function skillRemovalActions(dir, destRel) {
23843
+ const sourceDir = resolveSkillsSourceDir();
23844
+ const shipped = listShippedSkills(sourceDir);
23845
+ const dest = path14.join(dir, destRel);
23846
+ const actions = [];
23847
+ for (const name of shipped) {
23848
+ const skillPath = path14.join(dest, name);
23849
+ if (isSkillDir(skillPath)) {
23850
+ actions.push({ type: "remove-dir", path: skillPath, note: `skill directory installed by HarnessTrim` });
23851
+ }
23852
+ }
23853
+ if (actions.length > 0 && isSkillDir(dest)) {
23854
+ const remaining = fs10.readdirSync(dest).filter((name) => !shipped.includes(name));
23855
+ if (remaining.length === 0) {
23856
+ actions.push({ type: "remove-dir", path: dest, note: "empty skills directory left by HarnessTrim" });
23857
+ }
23858
+ }
23859
+ return actions;
23860
+ }
23861
+ function planClaudeUninstall(dir) {
23862
+ const actions = [...skillRemovalActions(dir, ".claude/skills")];
23863
+ const settingsPath = path14.join(dir, ".claude", "settings.json");
23864
+ const settingsContent = readOrNull(settingsPath);
23865
+ if (settingsContent !== null) {
23866
+ try {
23867
+ const parsed = JSON.parse(settingsContent);
23868
+ const { next, removed } = stripHookEntries(parsed);
23869
+ if (removed) {
23870
+ actions.push({
23871
+ type: Object.keys(next).length === 0 ? "remove-file" : "write",
23872
+ path: settingsPath,
23873
+ note: "remove the HarnessTrim PostToolUse hook"
23874
+ });
23875
+ }
23876
+ } catch {
23877
+ }
23878
+ }
23879
+ const claudeMdPath = path14.join(dir, "CLAUDE.md");
23880
+ const claudeMd = readOrNull(claudeMdPath);
23881
+ if (claudeMd !== null && claudeMd.includes(HARNESSTRIM_MARKER2)) {
23882
+ const next = stripMarkedRegion(claudeMd, HARNESSTRIM_MARKER2);
23883
+ if (next !== null && next !== claudeMd) {
23884
+ actions.push({
23885
+ type: next.length === 0 ? "remove-file" : "write",
23886
+ path: claudeMdPath,
23887
+ note: "remove the marker-guarded HarnessTrim instruction"
23888
+ });
23889
+ }
23890
+ }
23891
+ return { harness: "claude", dir, changed: actions.length > 0, actions };
23892
+ }
23893
+ function planCodexUninstall(dir) {
23894
+ const actions = [...skillRemovalActions(dir, ".codex/skills")];
23895
+ const agentsPath = path14.join(dir, "AGENTS.md");
23896
+ const agents = readOrNull(agentsPath);
23897
+ if (agents !== null && agents.includes(HARNESSTRIM_MARKER)) {
23898
+ const next = stripMarkedRegion(agents, HARNESSTRIM_MARKER);
23899
+ if (next !== null && next !== agents) {
23900
+ actions.push({
23901
+ type: next.length === 0 ? "remove-file" : "write",
23902
+ path: agentsPath,
23903
+ note: "remove the marker-guarded HarnessTrim instruction"
23904
+ });
23905
+ }
23906
+ }
23907
+ const hooksPath = path14.join(dir, ".codex", "hooks.json");
23908
+ const hooksContent = readOrNull(hooksPath);
23909
+ if (hooksContent !== null) {
23910
+ try {
23911
+ const parsed = JSON.parse(hooksContent);
23912
+ const { next, removed } = stripHookEntries(parsed);
23913
+ if (removed) {
23914
+ actions.push({
23915
+ type: Object.keys(next).length === 0 ? "remove-file" : "write",
23916
+ path: hooksPath,
23917
+ note: "remove the HarnessTrim PostToolUse hook"
23918
+ });
23919
+ }
23920
+ } catch {
23921
+ }
23922
+ }
23923
+ return { harness: "codex", dir, changed: actions.length > 0, actions };
23924
+ }
23925
+ function planOpencodeUninstall(dir) {
23926
+ const actions = [];
23927
+ const wrapperPath = path14.join(dir, ".opencode", "plugin", "harnesstrim.ts");
23928
+ const wrapper = readOrNull(wrapperPath);
23929
+ if (wrapper !== null && wrapper.includes(OPENCODE_PLUGIN_NAME)) {
23930
+ actions.push({ type: "remove-file", path: wrapperPath, note: "HarnessTrim plugin wrapper" });
23931
+ const pluginDir = path14.join(dir, ".opencode", "plugin");
23932
+ if (fs10.existsSync(pluginDir)) {
23933
+ const entries = fs10.readdirSync(pluginDir).filter((name) => name !== "harnesstrim.ts");
23934
+ if (entries.length === 0) {
23935
+ actions.push({ type: "remove-dir", path: pluginDir, note: "empty plugin directory left by HarnessTrim" });
23936
+ }
23937
+ }
23938
+ }
23939
+ const pkgPath = path14.join(dir, ".opencode", "package.json");
23940
+ const pkgContent = readOrNull(pkgPath);
23941
+ if (pkgContent !== null) {
23942
+ try {
23943
+ const parsed = JSON.parse(pkgContent);
23944
+ const deps = parsed.dependencies;
23945
+ if (deps && typeof deps[OPENCODE_PLUGIN_NAME] === "string") {
23946
+ const nextDeps = { ...deps };
23947
+ delete nextDeps[OPENCODE_PLUGIN_NAME];
23948
+ const next = { ...parsed, dependencies: nextDeps };
23949
+ const onlyOurDep = Object.keys(nextDeps).length === 0 && Object.keys(parsed).length <= 1;
23950
+ actions.push({
23951
+ type: onlyOurDep ? "remove-file" : "write",
23952
+ path: pkgPath,
23953
+ note: onlyOurDep ? "remove .opencode/package.json (only declared the adapter)" : "drop the @harnesstrim/adapter-opencode dependency"
23954
+ });
23955
+ }
23956
+ } catch {
23957
+ }
23958
+ }
23959
+ return { harness: "opencode", dir, changed: actions.length > 0, actions };
23960
+ }
23961
+ function planHermesUninstall(dir) {
23962
+ const pluginDest = path14.join(dir, ".hermes", "plugins", "harnesstrim");
23963
+ const actions = [];
23964
+ if (markerPresent3(pluginDest)) {
23965
+ actions.push({ type: "remove-dir", path: pluginDest, note: "Hermes plugin installed by HarnessTrim" });
23966
+ }
23967
+ return { harness: "hermes", dir, changed: actions.length > 0, actions };
23968
+ }
23969
+ function planPiUninstall(dir) {
23970
+ const scope = path14.resolve(dir) === path14.resolve(process.env.HOME ?? "") ? "user" : "project";
23971
+ const dest = scope === "user" ? path14.join(dir, ".pi", "agent", "extensions", "harnesstrim") : path14.join(dir, ".pi", "extensions", "harnesstrim");
23972
+ const actions = [];
23973
+ if (markerPresent3(dest)) {
23974
+ actions.push({ type: "remove-dir", path: dest, note: "Pi extension installed by HarnessTrim" });
23975
+ }
23976
+ return { harness: "pi", dir, changed: actions.length > 0, actions };
23977
+ }
23978
+ function planUninstall(harness, dir) {
23979
+ switch (harness) {
23980
+ case "claude":
23981
+ return planClaudeUninstall(dir);
23982
+ case "codex":
23983
+ return planCodexUninstall(dir);
23984
+ case "opencode":
23985
+ return planOpencodeUninstall(dir);
23986
+ case "hermes":
23987
+ return planHermesUninstall(dir);
23988
+ case "pi":
23989
+ return planPiUninstall(dir);
23990
+ default:
23991
+ throw new Error(`Unknown uninstall target: ${harness}. Supported: opencode, codex, claude, hermes, pi.`);
23992
+ }
23993
+ }
23994
+ function runUninstall(harness, dir, apply) {
23995
+ const plan = planUninstall(harness, dir);
23996
+ if (!apply || !plan.changed) return { ...plan, applied: false };
23997
+ for (const action of plan.actions) {
23998
+ switch (action.type) {
23999
+ case "remove-file":
24000
+ fs10.rmSync(action.path, { force: true });
24001
+ break;
24002
+ case "remove-dir":
24003
+ fs10.rmSync(action.path, { recursive: true, force: true });
24004
+ break;
24005
+ case "write": {
24006
+ if (action.path.endsWith("settings.json") || action.path.endsWith("hooks.json")) {
24007
+ const raw = readOrNull(action.path);
24008
+ if (raw !== null) {
24009
+ try {
24010
+ const parsed = JSON.parse(raw);
24011
+ const { next } = stripHookEntries(parsed);
24012
+ fs10.writeFileSync(action.path, JSON.stringify(next, null, 2) + "\n");
24013
+ } catch {
24014
+ }
24015
+ }
24016
+ } else if (action.path.endsWith(path14.join(".opencode", "package.json"))) {
24017
+ const raw = readOrNull(action.path);
24018
+ if (raw !== null) {
24019
+ try {
24020
+ const parsed = JSON.parse(raw);
24021
+ const deps = { ...parsed.dependencies ?? {} };
24022
+ delete deps[OPENCODE_PLUGIN_NAME];
24023
+ fs10.writeFileSync(action.path, JSON.stringify({ ...parsed, dependencies: deps }, null, 2) + "\n");
24024
+ } catch {
24025
+ }
24026
+ }
24027
+ } else {
24028
+ const marker = action.path.endsWith("CLAUDE.md") ? HARNESSTRIM_MARKER2 : HARNESSTRIM_MARKER;
24029
+ const raw = readOrNull(action.path);
24030
+ if (raw !== null) {
24031
+ const next = stripMarkedRegion(raw, marker);
24032
+ if (next !== null) fs10.writeFileSync(action.path, next + "\n");
24033
+ }
24034
+ }
24035
+ break;
24036
+ }
24037
+ default:
24038
+ break;
24039
+ }
24040
+ }
24041
+ return { ...plan, applied: true };
24042
+ }
24043
+
23672
24044
  // src/render.ts
23673
24045
  var ICON = { warn: "!", info: "i", ok: "+" };
23674
24046
  function renderDoctor(report) {
@@ -23767,7 +24139,7 @@ function renderCodexInstall(result, apply) {
23767
24139
  lines.push(` ${s.name.padEnd(18)} ${state}`);
23768
24140
  }
23769
24141
  lines.push("");
23770
- 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.`;
24142
+ const instr = plan.instructionsAction === "skip" ? "AGENTS.md instruction skipped (--no-instructions); skills only." : 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.`;
23771
24143
  lines.push(instr);
23772
24144
  if (result.hookPlan) {
23773
24145
  lines.push("");
@@ -23816,7 +24188,9 @@ function renderClaudeInstall(result, apply) {
23816
24188
  lines.push(` ${s.name.padEnd(18)} ${state}`);
23817
24189
  }
23818
24190
  lines.push("");
23819
- if (plan.settingsAction === "present") {
24191
+ if (plan.settingsAction === "skip") {
24192
+ lines.push(`${plan.settingsFile}: PostToolUse hook skipped (--no-hook); skills only.`);
24193
+ } else if (plan.settingsAction === "present") {
23820
24194
  lines.push(`${plan.settingsFile}: PostToolUse reducer hook already present (no change).`);
23821
24195
  } else {
23822
24196
  lines.push(
@@ -23829,7 +24203,9 @@ function renderClaudeInstall(result, apply) {
23829
24203
  }
23830
24204
  }
23831
24205
  lines.push("");
23832
- if (plan.instructionsAction === "present") {
24206
+ if (plan.instructionsAction === "skip") {
24207
+ lines.push(`${plan.instructionsFile}: reduce-pipe instruction skipped (--no-instructions); skills only.`);
24208
+ } else if (plan.instructionsAction === "present") {
23833
24209
  lines.push(`${plan.instructionsFile}: reduce-pipe instruction already present (no change).`);
23834
24210
  } else {
23835
24211
  lines.push(
@@ -23884,7 +24260,7 @@ function renderPiInstall(result, apply) {
23884
24260
  lines.push(` Copied: ${result.copiedFiles.join(", ")}`);
23885
24261
  } else {
23886
24262
  lines.push(` Source: ${plan.extensionSource}`);
23887
- lines.push(" (harnesstrim.ts + .installed marker)");
24263
+ lines.push(" (index.ts + .installed marker)");
23888
24264
  }
23889
24265
  lines.push("");
23890
24266
  lines.push("The extension hooks Pi's `tool_result` and needs `harnesstrim` on PATH.");
@@ -23919,6 +24295,192 @@ Enable it in the adapter (telemetry: true) to record reductions.`;
23919
24295
  }
23920
24296
  return lines.join("\n");
23921
24297
  }
24298
+ function renderUninstall(result, apply) {
24299
+ const lines = [`${apply ? "Uninstalled" : "Would uninstall"} ${result.harness} integration`, ""];
24300
+ if (!result.changed) {
24301
+ lines.push("Nothing to remove \u2014 no HarnessTrim files found in the install set.");
24302
+ return lines.join("\n");
24303
+ }
24304
+ for (const action of result.actions) {
24305
+ const verb = action.type === "remove-file" || action.type === "remove-dir" ? apply ? "removed" : "remove" : action.type === "write" ? apply ? "updated" : "update" : "clean";
24306
+ lines.push(` ${verb.padEnd(8)} ${action.path}`);
24307
+ if (action.note) lines.push(` (${action.note})`);
24308
+ }
24309
+ if (!apply) {
24310
+ lines.push("");
24311
+ lines.push("Dry run \u2014 nothing written. Re-run with `--apply`.");
24312
+ }
24313
+ lines.push("");
24314
+ lines.push("Only files HarnessTrim wrote are touched; marker-guarded regions and your");
24315
+ lines.push("other settings/hook entries are preserved.");
24316
+ return lines.join("\n");
24317
+ }
24318
+
24319
+ // src/json.ts
24320
+ function doctorJson(report) {
24321
+ return JSON.stringify(report, null, 2);
24322
+ }
24323
+ function metricsJson(result) {
24324
+ return JSON.stringify(result, null, 2);
24325
+ }
24326
+ function opencodeInstallJson(result, apply) {
24327
+ const actions = [];
24328
+ if (result.changed) {
24329
+ actions.push(
24330
+ {
24331
+ type: "write",
24332
+ path: result.wrapperPath,
24333
+ note: "local plugin wrapper with the adapter options"
24334
+ },
24335
+ {
24336
+ type: "write",
24337
+ path: result.packageJsonPath,
24338
+ note: "declares @harnesstrim/adapter-opencode"
24339
+ }
24340
+ );
24341
+ if (result.opencodeJsonContent !== null) {
24342
+ actions.push({
24343
+ type: "clean",
24344
+ path: result.opencodeJsonPath,
24345
+ note: "remove the stale adapter entry (options live in the wrapper now)"
24346
+ });
24347
+ }
24348
+ }
24349
+ return {
24350
+ harness: "opencode",
24351
+ dryRun: !apply,
24352
+ changed: result.changed,
24353
+ alreadyInstalled: result.alreadyInstalled,
24354
+ applied: result.applied,
24355
+ actions,
24356
+ details: {
24357
+ preset: result.preset?.name ?? null,
24358
+ depsInstalled: result.depsInstalled,
24359
+ depsMessage: result.depsMessage ?? null
24360
+ }
24361
+ };
24362
+ }
24363
+ function codexInstallJson(result, apply) {
24364
+ const actions = result.plan.skills.map((s) => ({
24365
+ type: s.present ? "none" : "copy",
24366
+ path: s.to,
24367
+ from: s.from,
24368
+ note: s.present ? "already present" : "copy skill"
24369
+ }));
24370
+ if (result.plan.instructionsAction !== "present" && result.plan.instructionsAction !== "skip") {
24371
+ actions.push({
24372
+ type: result.plan.instructionsAction === "create" ? "write" : "append",
24373
+ path: result.plan.instructionsFile,
24374
+ note: `reduce-pipe instruction (${result.plan.instructionsAction})`
24375
+ });
24376
+ }
24377
+ if (result.hookPlan && result.hookPlan.action !== "present") {
24378
+ actions.push({
24379
+ type: "write",
24380
+ path: result.hookPlan.hooksFile,
24381
+ note: `Bash PostToolUse hook (${result.hookPlan.action})`
24382
+ });
24383
+ }
24384
+ const hookChanged = result.hookPlan !== null && result.hookPlan.action !== "present";
24385
+ return {
24386
+ harness: "codex",
24387
+ dryRun: !apply,
24388
+ changed: result.plan.changed || hookChanged,
24389
+ alreadyInstalled: !result.plan.changed && !hookChanged,
24390
+ applied: result.applied,
24391
+ actions,
24392
+ details: { hook: result.hookPlan ? result.hookPlan.action : "skipped" }
24393
+ };
24394
+ }
24395
+ function claudeInstallJson(result, apply) {
24396
+ const actions = result.plan.skills.map((s) => ({
24397
+ type: s.present ? "none" : "copy",
24398
+ path: s.to,
24399
+ from: s.from,
24400
+ note: s.present ? "already present" : "copy skill"
24401
+ }));
24402
+ if (result.plan.settingsAction !== "present" && result.plan.settingsAction !== "skip") {
24403
+ actions.push({
24404
+ type: result.plan.settingsAction === "create" ? "write" : "write",
24405
+ path: result.plan.settingsFile,
24406
+ note: `PostToolUse Bash hook (${result.plan.settingsAction})`
24407
+ });
24408
+ }
24409
+ if (result.plan.instructionsAction !== "present" && result.plan.instructionsAction !== "skip") {
24410
+ actions.push({
24411
+ type: result.plan.instructionsAction === "create" ? "write" : "append",
24412
+ path: result.plan.instructionsFile,
24413
+ note: `reduce-pipe instruction (${result.plan.instructionsAction})`
24414
+ });
24415
+ }
24416
+ return {
24417
+ harness: "claude",
24418
+ dryRun: !apply,
24419
+ changed: result.plan.changed,
24420
+ alreadyInstalled: !result.plan.changed,
24421
+ applied: result.applied,
24422
+ actions,
24423
+ details: {
24424
+ settingsAction: result.plan.settingsAction,
24425
+ instructionsAction: result.plan.instructionsAction
24426
+ }
24427
+ };
24428
+ }
24429
+ function hermesInstallJson(result, apply) {
24430
+ const actions = [
24431
+ {
24432
+ type: result.plan.alreadyInstalled ? "none" : "copy",
24433
+ path: result.plan.pluginDest,
24434
+ from: result.plan.pluginSource,
24435
+ note: result.plan.alreadyInstalled ? "already installed" : "copy Hermes plugin bundle"
24436
+ }
24437
+ ];
24438
+ return {
24439
+ harness: "hermes",
24440
+ dryRun: !apply,
24441
+ changed: !result.plan.alreadyInstalled,
24442
+ alreadyInstalled: result.plan.alreadyInstalled,
24443
+ applied: result.applied,
24444
+ actions,
24445
+ details: { enabled: result.enabled, enableMessage: result.enableMessage ?? null }
24446
+ };
24447
+ }
24448
+ function piInstallJson(result, apply) {
24449
+ const actions = [
24450
+ {
24451
+ type: result.plan.alreadyInstalled ? "none" : "copy",
24452
+ path: result.plan.extensionDest,
24453
+ from: result.plan.extensionSource,
24454
+ note: result.plan.alreadyInstalled ? "already installed" : "copy Pi extension bundle"
24455
+ }
24456
+ ];
24457
+ return {
24458
+ harness: "pi",
24459
+ dryRun: !apply,
24460
+ changed: !result.plan.alreadyInstalled,
24461
+ alreadyInstalled: result.plan.alreadyInstalled,
24462
+ applied: result.applied,
24463
+ actions
24464
+ };
24465
+ }
24466
+ function codexGlobalHookJson(result, apply) {
24467
+ const actions = [
24468
+ {
24469
+ type: result.hookPlan.action === "present" ? "none" : "write",
24470
+ path: result.hookPlan.hooksFile,
24471
+ note: `global Bash PostToolUse hook (${result.hookPlan.action})`
24472
+ }
24473
+ ];
24474
+ return {
24475
+ harness: "codex",
24476
+ dryRun: !apply,
24477
+ changed: result.hookPlan.action !== "present",
24478
+ alreadyInstalled: result.hookPlan.action === "present",
24479
+ applied: result.applied,
24480
+ actions,
24481
+ details: { scope: "global" }
24482
+ };
24483
+ }
23922
24484
 
23923
24485
  // src/cli.ts
23924
24486
  var HELP = `harnesstrim \u2014 one token policy for coding harnesses
@@ -23928,16 +24490,25 @@ Usage:
23928
24490
  harnesstrim install opencode [dir] Wire the adapter into opencode.json (dry-run)
23929
24491
  --apply Actually write the change
23930
24492
  --preset <name> Bake a policy preset's adapter config in
24493
+ --mode <m> Override mode: active|dryrun|off
24494
+ --min-length <n> Override the reduction threshold (chars)
24495
+ --tools <list> Confine reduction to a subset of tool families
23931
24496
  harnesstrim install codex [dir] Install skills + AGENTS.md reduction guidance (dry-run)
23932
24497
  --apply Actually write the change
23933
24498
  --hook Also install the experimental Bash PostToolUse hook
24499
+ --no-instructions Skills only (no AGENTS.md reduce-pipe instruction)
23934
24500
  --global With --hook, install it once in ~/.codex (no project files)
23935
24501
  harnesstrim install claude [dir] Install skills + PostToolUse reducer hook (dry-run)
23936
24502
  --apply Actually write the change
24503
+ --no-hook Skills only (no PostToolUse hook)
24504
+ --no-instructions Skills only (no CLAUDE.md reduce-pipe instruction)
23937
24505
  harnesstrim install hermes [dir] Install Hermes plugin (dry-run)
23938
24506
  --apply Actually write the change
23939
24507
  harnesstrim install pi [dir] Install Pi tool_result extension (dry-run)
23940
24508
  --apply Actually write the change
24509
+ harnesstrim uninstall <harness> [dir] Remove only what install wrote (dry-run)
24510
+ --apply Actually write the change
24511
+ harnesstrim capabilities Print machine-readable per-harness capabilities (JSON)
23941
24512
  harnesstrim hook claude [--metrics <path>]
23942
24513
  PostToolUse hook runtime; --metrics records a TrimEvent per reduction
23943
24514
  harnesstrim hook codex [--metrics <path>]
@@ -23953,8 +24524,11 @@ Usage:
23953
24524
  harnesstrim bench Run the Tier A reducer micro-benchmark
23954
24525
  harnesstrim --version Print the installed version
23955
24526
 
24527
+ Flags:
24528
+ --json Print machine-readable JSON (doctor, metrics, install, uninstall)
24529
+
23956
24530
  Notes:
23957
- - install is dry-run by default; nothing is written without --apply.
24531
+ - install and uninstall are dry-run by default; nothing is written without --apply.
23958
24532
  - dir defaults to the current directory; metrics path defaults to ${DEFAULT_METRICS_PATH}.
23959
24533
  - reduce reads stdin and writes slimmed output to stdout, e.g. npm test 2>&1 | harnesstrim reduce`;
23960
24534
  async function main(argv) {
@@ -23971,7 +24545,12 @@ async function main(argv) {
23971
24545
  log: { type: "string" },
23972
24546
  metrics: { type: "string" },
23973
24547
  hook: { type: "boolean" },
23974
- global: { type: "boolean" }
24548
+ global: { type: "boolean" },
24549
+ "no-hook": { type: "boolean" },
24550
+ "no-instructions": { type: "boolean" },
24551
+ mode: { type: "string" },
24552
+ tools: { type: "string" },
24553
+ json: { type: "boolean" }
23975
24554
  }
23976
24555
  });
23977
24556
  const [command, ...rest] = positionals;
@@ -23986,16 +24565,34 @@ async function main(argv) {
23986
24565
  switch (command) {
23987
24566
  case "doctor": {
23988
24567
  const dir = rest[0] ?? process.cwd();
23989
- console.log(renderDoctor(inspect(dir)));
24568
+ const report = inspect(dir);
24569
+ if (values.json) {
24570
+ console.log(doctorJson(report));
24571
+ } else {
24572
+ console.log(renderDoctor(report));
24573
+ }
23990
24574
  return 0;
23991
24575
  }
23992
24576
  case "install": {
23993
24577
  const target = rest[0];
23994
24578
  const dir = rest[1] ?? (target === "hermes" ? os4.homedir() : process.cwd());
23995
24579
  const apply = values.apply === true;
24580
+ const asJson = values.json === true;
23996
24581
  if (target === "opencode") {
23997
- const result = runInstallOpencode(dir, apply, values.preset);
23998
- console.log(renderInstall(result, apply));
24582
+ const mode = parseModeFlag(values.mode);
24583
+ if (mode === void 0 && values.mode !== void 0) {
24584
+ console.error(`Invalid --mode: ${values.mode} (expected active, dryrun, or off).`);
24585
+ return 1;
24586
+ }
24587
+ const minLength = values["min-length"] !== void 0 ? Number(values["min-length"]) : void 0;
24588
+ if (minLength !== void 0 && !Number.isFinite(minLength)) {
24589
+ console.error(`Invalid --min-length: ${values["min-length"]}`);
24590
+ return 1;
24591
+ }
24592
+ const tools = values.tools !== void 0 ? splitTools(values.tools) : void 0;
24593
+ const result = runInstallOpencode(dir, apply, values.preset, true, { mode, minLength, tools });
24594
+ if (asJson) console.log(JSON.stringify(opencodeInstallJson(result, apply), null, 2));
24595
+ else console.log(renderInstall(result, apply));
23999
24596
  return 0;
24000
24597
  }
24001
24598
  if (target === "codex") {
@@ -24004,27 +24601,63 @@ async function main(argv) {
24004
24601
  console.error("`harnesstrim install codex --global` requires `--hook`.");
24005
24602
  return 1;
24006
24603
  }
24007
- console.log(renderCodexGlobalHookInstall(runInstallCodexGlobalHook(path15.join(os4.homedir(), ".codex"), apply), apply));
24604
+ const result2 = runInstallCodexGlobalHook(path16.join(os4.homedir(), ".codex"), apply);
24605
+ if (asJson) console.log(JSON.stringify(codexGlobalHookJson(result2, apply), null, 2));
24606
+ else console.log(renderCodexGlobalHookInstall(result2, apply));
24008
24607
  return 0;
24009
24608
  }
24010
- console.log(renderCodexInstall(runInstallCodex(dir, apply, values.hook === true), apply));
24609
+ const result = runInstallCodex(dir, apply, values.hook === true, {
24610
+ includeInstructions: values["no-instructions"] !== true
24611
+ });
24612
+ if (asJson) console.log(JSON.stringify(codexInstallJson(result, apply), null, 2));
24613
+ else console.log(renderCodexInstall(result, apply));
24011
24614
  return 0;
24012
24615
  }
24013
24616
  if (target === "claude") {
24014
- console.log(renderClaudeInstall(runInstallClaude(dir, apply), apply));
24617
+ const result = runInstallClaude(dir, apply, {
24618
+ includeHook: values["no-hook"] !== true,
24619
+ includeInstructions: values["no-instructions"] !== true
24620
+ });
24621
+ if (asJson) console.log(JSON.stringify(claudeInstallJson(result, apply), null, 2));
24622
+ else console.log(renderClaudeInstall(result, apply));
24015
24623
  return 0;
24016
24624
  }
24017
24625
  if (target === "hermes") {
24018
- console.log(renderHermesInstall(runInstallHermes(dir, apply), apply));
24626
+ const result = runInstallHermes(dir, apply);
24627
+ if (asJson) console.log(JSON.stringify(hermesInstallJson(result, apply), null, 2));
24628
+ else console.log(renderHermesInstall(result, apply));
24019
24629
  return 0;
24020
24630
  }
24021
24631
  if (target === "pi") {
24022
- console.log(renderPiInstall(runInstallPi(dir, apply), apply));
24632
+ const result = runInstallPi(dir, apply);
24633
+ if (asJson) console.log(JSON.stringify(piInstallJson(result, apply), null, 2));
24634
+ else console.log(renderPiInstall(result, apply));
24023
24635
  return 0;
24024
24636
  }
24025
24637
  console.error(`Unknown install target: ${target ?? "(none)"}. Supported: opencode, codex, claude, hermes, pi.`);
24026
24638
  return 1;
24027
24639
  }
24640
+ case "uninstall": {
24641
+ const target = rest[0];
24642
+ const dir = rest[1] ?? (target === "hermes" ? os4.homedir() : process.cwd());
24643
+ const apply = values.apply === true;
24644
+ try {
24645
+ const result = runUninstall(target, dir, apply);
24646
+ if (values.json) {
24647
+ console.log(JSON.stringify(result, null, 2));
24648
+ } else {
24649
+ console.log(renderUninstall(result, apply));
24650
+ }
24651
+ } catch (err) {
24652
+ console.error(err instanceof Error ? err.message : String(err));
24653
+ return 1;
24654
+ }
24655
+ return 0;
24656
+ }
24657
+ case "capabilities": {
24658
+ console.log(JSON.stringify(getCapabilities(package_default.version), null, 2));
24659
+ return 0;
24660
+ }
24028
24661
  case "hook": {
24029
24662
  const which = rest[0];
24030
24663
  if (which !== "claude" && which !== "codex") {
@@ -24036,18 +24669,24 @@ async function main(argv) {
24036
24669
  process.stdout.write(response);
24037
24670
  if (values.metrics && event) {
24038
24671
  try {
24039
- const p = path15.resolve(values.metrics);
24040
- fs11.mkdirSync(path15.dirname(p), { recursive: true });
24041
- fs11.appendFileSync(
24672
+ const p = path16.resolve(values.metrics);
24673
+ fs12.mkdirSync(path16.dirname(p), { recursive: true });
24674
+ fs12.appendFileSync(
24042
24675
  p,
24043
- JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), harness: which, ...event }) + "\n"
24676
+ JSON.stringify(
24677
+ makeTrimEvent({
24678
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
24679
+ harness: which,
24680
+ ...event
24681
+ })
24682
+ ) + "\n"
24044
24683
  );
24045
24684
  } catch {
24046
24685
  }
24047
24686
  }
24048
24687
  if (values.log) {
24049
24688
  try {
24050
- fs11.appendFileSync(
24689
+ fs12.appendFileSync(
24051
24690
  values.log,
24052
24691
  JSON.stringify({ inputChars: input.length, changed: response !== "{}", responseChars: response.length }) + "\n"
24053
24692
  );
@@ -24076,8 +24715,13 @@ async function main(argv) {
24076
24715
  return 1;
24077
24716
  }
24078
24717
  case "metrics": {
24079
- const path16 = rest[0] ?? DEFAULT_METRICS_PATH;
24080
- console.log(renderMetrics(loadMetrics(path16)));
24718
+ const path17 = rest[0] ?? DEFAULT_METRICS_PATH;
24719
+ const result = loadMetrics(path17);
24720
+ if (values.json) {
24721
+ console.log(metricsJson(result));
24722
+ } else {
24723
+ console.log(renderMetrics(result));
24724
+ }
24081
24725
  return 0;
24082
24726
  }
24083
24727
  case "reduce": {
@@ -24092,18 +24736,20 @@ async function main(argv) {
24092
24736
  process.stdout.write(result.output);
24093
24737
  if (values.metrics && result.changed) {
24094
24738
  try {
24095
- const p = path15.resolve(values.metrics);
24096
- fs11.mkdirSync(path15.dirname(p), { recursive: true });
24097
- fs11.appendFileSync(
24739
+ const p = path16.resolve(values.metrics);
24740
+ fs12.mkdirSync(path16.dirname(p), { recursive: true });
24741
+ fs12.appendFileSync(
24098
24742
  p,
24099
- JSON.stringify({
24100
- ts: (/* @__PURE__ */ new Date()).toISOString(),
24101
- harness: "pipe",
24102
- tool: "reduce",
24103
- reducer: result.reducer,
24104
- beforeChars: result.beforeChars,
24105
- afterChars: result.afterChars
24106
- }) + "\n"
24743
+ JSON.stringify(
24744
+ makeTrimEvent({
24745
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
24746
+ harness: "pipe",
24747
+ tool: "reduce",
24748
+ reducer: result.reducer,
24749
+ beforeChars: result.beforeChars,
24750
+ afterChars: result.afterChars
24751
+ })
24752
+ ) + "\n"
24107
24753
  );
24108
24754
  } catch {
24109
24755
  }
@@ -24144,6 +24790,12 @@ async function main(argv) {
24144
24790
  return 1;
24145
24791
  }
24146
24792
  }
24793
+ function parseModeFlag(value) {
24794
+ return value === "active" || value === "dryrun" || value === "off" ? value : void 0;
24795
+ }
24796
+ function splitTools(value) {
24797
+ return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
24798
+ }
24147
24799
  main(process.argv.slice(2)).then((code) => {
24148
24800
  process.exitCode = code;
24149
24801
  }).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "harnesstrim",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "HarnessTrim CLI: doctor (diagnose token waste), install adapters, run benchmarks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,13 +16,13 @@
16
16
  },
17
17
  "devDependencies": {
18
18
  "esbuild": "^0.25.0",
19
- "@harnesstrim/adapter-claude": "0.0.1",
20
- "@harnesstrim/adapter-hermes": "0.0.1",
21
- "@harnesstrim/core": "0.0.2",
22
19
  "@harnesstrim/adapter-codex": "0.0.1",
20
+ "@harnesstrim/adapter-claude": "0.0.1",
21
+ "@harnesstrim/adapter-pi": "0.0.1",
23
22
  "@harnesstrim/benchmarks": "0.0.1",
24
23
  "@harnesstrim/mcp": "0.0.1",
25
- "@harnesstrim/adapter-pi": "0.0.1"
24
+ "@harnesstrim/adapter-hermes": "0.0.1",
25
+ "@harnesstrim/core": "0.0.2"
26
26
  },
27
27
  "scripts": {
28
28
  "build": "node build.mjs",