harnesstrim 0.0.5 → 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.
package/dist/cli.mjs CHANGED
@@ -581,6 +581,75 @@ ${input.slice(response.index)}`;
581
581
  }
582
582
  });
583
583
 
584
+ // ../core/src/reducers/lint-output-slim.ts
585
+ function isLintLine(line) {
586
+ return LINT_LINE_RE.test(line);
587
+ }
588
+ var MARKER_PREFIX7, LINT_LINE_RE, MAX_RULES_IN_MARKER, lintOutputSlim;
589
+ var init_lint_output_slim = __esm({
590
+ "../core/src/reducers/lint-output-slim.ts"() {
591
+ "use strict";
592
+ MARKER_PREFIX7 = "[harnesstrim:lint-output-slim]";
593
+ LINT_LINE_RE = /^[\w.\/\\-]+:\d+:\d+\s+(warning|error)\s+([\w@.\/-]+)/;
594
+ MAX_RULES_IN_MARKER = 8;
595
+ lintOutputSlim = {
596
+ name: "lint-output-slim",
597
+ reduce(input) {
598
+ const lines = input.split(/\r?\n/);
599
+ const out = [];
600
+ let droppedTotal = 0;
601
+ let i = 0;
602
+ while (i < lines.length) {
603
+ const line = lines[i];
604
+ if (!isLintLine(line)) {
605
+ out.push(line);
606
+ i++;
607
+ continue;
608
+ }
609
+ const counts = [];
610
+ const byKey = /* @__PURE__ */ new Map();
611
+ let runEnd = i;
612
+ while (runEnd < lines.length && isLintLine(lines[runEnd])) {
613
+ const m = LINT_LINE_RE.exec(lines[runEnd]);
614
+ const severity = m[1] === "error" ? "error" : "warning";
615
+ const rule = m[2];
616
+ const key = `${severity}:${rule}`;
617
+ const existing = byKey.get(key);
618
+ if (existing) {
619
+ existing.count++;
620
+ } else {
621
+ const entry = { severity, rule, count: 1 };
622
+ byKey.set(key, entry);
623
+ counts.push(entry);
624
+ }
625
+ runEnd++;
626
+ }
627
+ const runLength = runEnd - i;
628
+ if (runLength >= 2) {
629
+ const parts = counts.slice(0, MAX_RULES_IN_MARKER).map(
630
+ (c) => `${c.rule} \xD7${c.count}`
631
+ );
632
+ const truncated = counts.length > MAX_RULES_IN_MARKER;
633
+ const suffix = truncated ? `, +${counts.length - MAX_RULES_IN_MARKER} more rule(s)` : "";
634
+ const severities = counts.some((c) => c.severity === "error") && counts.some((c) => c.severity === "warning") ? "error(s) and warning(s)" : counts.some((c) => c.severity === "error") ? "error(s)" : "warning(s)";
635
+ out.push(`${MARKER_PREFIX7} omitted ${runLength} lint line(s) (${severities}: ${parts.join(", ")}${suffix})`);
636
+ droppedTotal += runLength;
637
+ } else {
638
+ for (let j = i; j < runEnd; j++) out.push(lines[j]);
639
+ }
640
+ i = runEnd;
641
+ }
642
+ const output = out.join("\n");
643
+ return {
644
+ output,
645
+ changed: droppedTotal > 0,
646
+ note: droppedTotal > 0 ? `dropped ${droppedTotal} lint noise line(s)` : void 0
647
+ };
648
+ }
649
+ };
650
+ }
651
+ });
652
+
584
653
  // ../core/src/reducers/index.ts
585
654
  var init_reducers = __esm({
586
655
  "../core/src/reducers/index.ts"() {
@@ -592,6 +661,7 @@ var init_reducers = __esm({
592
661
  init_json_output_slim();
593
662
  init_file_listing_slim();
594
663
  init_cron_output_slim();
664
+ init_lint_output_slim();
595
665
  }
596
666
  });
597
667
 
@@ -600,6 +670,7 @@ function pickReducer(text) {
600
670
  if (GIT_DIFF_RE.test(text)) return gitDiffSlim;
601
671
  if (TEST_OUTPUT_RE.test(text)) return testOutputSlim;
602
672
  if (CRON_OUTPUT_RE.test(text) && text.length >= 400) return cronOutputSlim;
673
+ if (LINT_OUTPUT_RE.test(text) && text.length >= 400) return lintOutputSlim;
603
674
  if (JSON_RE.test(text) && text.length >= 400) return jsonOutputSlim;
604
675
  if (FILE_LISTING_RE.test(text) && text.length >= 400) return fileListingSlim;
605
676
  if (LONG_TEXT_RE.test(text) && text.length >= 1e3) return genericTextSlim;
@@ -619,7 +690,7 @@ function reduceAuto(text, minLength = DEFAULT_MIN_LENGTH) {
619
690
  }
620
691
  return { ...result, reducer: reducer.name };
621
692
  }
622
- var DEFAULT_MIN_LENGTH, GIT_DIFF_RE, TEST_OUTPUT_RE, JSON_RE, FILE_LISTING_RE, CRON_OUTPUT_RE, LONG_TEXT_RE;
693
+ var DEFAULT_MIN_LENGTH, GIT_DIFF_RE, TEST_OUTPUT_RE, JSON_RE, FILE_LISTING_RE, CRON_OUTPUT_RE, LINT_OUTPUT_RE, LONG_TEXT_RE;
623
694
  var init_dispatch = __esm({
624
695
  "../core/src/dispatch.ts"() {
625
696
  "use strict";
@@ -629,17 +700,34 @@ var init_dispatch = __esm({
629
700
  init_json_output_slim();
630
701
  init_file_listing_slim();
631
702
  init_cron_output_slim();
703
+ init_lint_output_slim();
632
704
  DEFAULT_MIN_LENGTH = 400;
633
705
  GIT_DIFF_RE = /^diff --git /m;
634
706
  TEST_OUTPUT_RE = /\b\d+\s+(passed|failed)\b|^(PASS|FAIL)\s|::\w.*\b(PASSED|FAILED)\b|=+\s*(FAILURES|short test summary)/im;
635
707
  JSON_RE = /^\s*[\[{]/m;
636
708
  FILE_LISTING_RE = /(?:^total\s+\d+|^[\-bcdlsp][\-r][\-w][\-xs\-][\-r][\-w][\-xs\-][\-r][\-w][\-xs\-]|^\.\/(?:\.|[^.\s])|^\s*(?:├──|└──|│\s+)|^[\w.\/\-]+\.[a-zA-Z]{1,4}:\d+\|)/m;
637
709
  CRON_OUTPUT_RE = /^# Cron Job:.*\n[\s\S]*^## Prompt\s*$[\s\S]*^## Response\s*$/m;
710
+ LINT_OUTPUT_RE = /^[\w.\/\\-]+:\d+:\d+\s+(?:warning|error)\s+[\w@.\/-]+\s/m;
638
711
  LONG_TEXT_RE = /^#{1,4}\s.*\n(?:(?!^#{1,4}\s|^diff --git |^```).*\n){5,}/m;
639
712
  }
640
713
  });
641
714
 
642
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
+ }
643
731
  function pct(before, after) {
644
732
  if (before === 0) return 0;
645
733
  return Math.round((1 - after / before) * 1e3) / 10;
@@ -680,7 +768,7 @@ function parseTrimEvents(jsonl) {
680
768
  } catch {
681
769
  continue;
682
770
  }
683
- if (isTrimEvent(parsed)) out.push(parsed);
771
+ if (isTrimEvent(parsed)) out.push(normalize(parsed));
684
772
  }
685
773
  return out;
686
774
  }
@@ -689,9 +777,25 @@ function isTrimEvent(value) {
689
777
  const v = value;
690
778
  return typeof v.beforeChars === "number" && typeof v.afterChars === "number" && typeof v.tool === "string" && (typeof v.reducer === "string" || v.reducer === null);
691
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;
692
795
  var init_trim_event = __esm({
693
796
  "../core/src/metrics/trim-event.ts"() {
694
797
  "use strict";
798
+ TRIM_EVENT_SCHEMA_VERSION = 1;
695
799
  }
696
800
  });
697
801
 
@@ -1166,8 +1270,8 @@ var init_parseUtil = __esm({
1166
1270
  init_errors();
1167
1271
  init_en();
1168
1272
  makeIssue = (params) => {
1169
- const { data, path: path16, errorMaps, issueData } = params;
1170
- const fullPath = [...path16, ...issueData.path || []];
1273
+ const { data, path: path17, errorMaps, issueData } = params;
1274
+ const fullPath = [...path17, ...issueData.path || []];
1171
1275
  const fullIssue = {
1172
1276
  ...issueData,
1173
1277
  path: fullPath
@@ -1475,11 +1579,11 @@ var init_types2 = __esm({
1475
1579
  init_parseUtil();
1476
1580
  init_util();
1477
1581
  ParseInputLazyPath = class {
1478
- constructor(parent, value, path16, key) {
1582
+ constructor(parent, value, path17, key) {
1479
1583
  this._cachedPath = [];
1480
1584
  this.parent = parent;
1481
1585
  this.data = value;
1482
- this._path = path16;
1586
+ this._path = path17;
1483
1587
  this._key = key;
1484
1588
  }
1485
1589
  get path() {
@@ -5060,10 +5164,10 @@ function assignProp(target, prop, value) {
5060
5164
  configurable: true
5061
5165
  });
5062
5166
  }
5063
- function getElementAtPath(obj, path16) {
5064
- if (!path16)
5167
+ function getElementAtPath(obj, path17) {
5168
+ if (!path17)
5065
5169
  return obj;
5066
- return path16.reduce((acc, key) => acc?.[key], obj);
5170
+ return path17.reduce((acc, key) => acc?.[key], obj);
5067
5171
  }
5068
5172
  function promiseAllObject(promisesObj) {
5069
5173
  const keys = Object.keys(promisesObj);
@@ -5312,11 +5416,11 @@ function aborted(x, startIndex = 0) {
5312
5416
  }
5313
5417
  return false;
5314
5418
  }
5315
- function prefixIssues(path16, issues) {
5419
+ function prefixIssues(path17, issues) {
5316
5420
  return issues.map((iss) => {
5317
5421
  var _a;
5318
5422
  (_a = iss).path ?? (_a.path = []);
5319
- iss.path.unshift(path16);
5423
+ iss.path.unshift(path17);
5320
5424
  return iss;
5321
5425
  });
5322
5426
  }
@@ -16152,8 +16256,8 @@ var require_resolve = __commonJS({
16152
16256
  }
16153
16257
  return count;
16154
16258
  }
16155
- function getFullPath(resolver, id = "", normalize) {
16156
- if (normalize !== false)
16259
+ function getFullPath(resolver, id = "", normalize2) {
16260
+ if (normalize2 !== false)
16157
16261
  id = normalizeId(id);
16158
16262
  const p = resolver.parse(id);
16159
16263
  return _getFullPath(resolver, p);
@@ -17146,8 +17250,8 @@ var require_utils = __commonJS({
17146
17250
  }
17147
17251
  return ind;
17148
17252
  }
17149
- function removeDotSegments(path16) {
17150
- let input = path16;
17253
+ function removeDotSegments(path17) {
17254
+ let input = path17;
17151
17255
  const output = [];
17152
17256
  let nextSlash = -1;
17153
17257
  let len = 0;
@@ -17399,8 +17503,8 @@ var require_schemes = __commonJS({
17399
17503
  wsComponent.secure = void 0;
17400
17504
  }
17401
17505
  if (wsComponent.resourceName) {
17402
- const [path16, query] = wsComponent.resourceName.split("?");
17403
- wsComponent.path = path16 && path16 !== "/" ? path16 : void 0;
17506
+ const [path17, query] = wsComponent.resourceName.split("?");
17507
+ wsComponent.path = path17 && path17 !== "/" ? path17 : void 0;
17404
17508
  wsComponent.query = query;
17405
17509
  wsComponent.resourceName = void 0;
17406
17510
  }
@@ -17549,7 +17653,7 @@ var require_fast_uri = __commonJS({
17549
17653
  "use strict";
17550
17654
  var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
17551
17655
  var { SCHEMES, getSchemeHandler } = require_schemes();
17552
- function normalize(uri, options) {
17656
+ function normalize2(uri, options) {
17553
17657
  if (typeof uri === "string") {
17554
17658
  uri = /** @type {T} */
17555
17659
  normalizeString(uri, options);
@@ -17816,7 +17920,7 @@ var require_fast_uri = __commonJS({
17816
17920
  }
17817
17921
  var fastUri = {
17818
17922
  SCHEMES,
17819
- normalize,
17923
+ normalize: normalize2,
17820
17924
  resolve,
17821
17925
  resolveComponent,
17822
17926
  equal,
@@ -20793,12 +20897,12 @@ var require_dist = __commonJS({
20793
20897
  throw new Error(`Unknown format "${name}"`);
20794
20898
  return f;
20795
20899
  };
20796
- function addFormats(ajv, list, fs12, exportName) {
20900
+ function addFormats(ajv, list, fs13, exportName) {
20797
20901
  var _a;
20798
20902
  var _b;
20799
20903
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
20800
20904
  for (const f of list)
20801
- ajv.addFormat(f, fs12[f]);
20905
+ ajv.addFormat(f, fs13[f]);
20802
20906
  }
20803
20907
  module.exports = exports = formatsPlugin;
20804
20908
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -22559,14 +22663,14 @@ __export(server_exports, {
22559
22663
  runReduceTool: () => runReduceTool,
22560
22664
  startStdioServer: () => startStdioServer
22561
22665
  });
22562
- import fs10 from "node:fs";
22563
- import path14 from "node:path";
22666
+ import fs11 from "node:fs";
22667
+ import path15 from "node:path";
22564
22668
  function createFileSink(metricsPath) {
22565
22669
  return (event) => {
22566
22670
  try {
22567
- const p = path14.resolve(metricsPath);
22568
- fs10.mkdirSync(path14.dirname(p), { recursive: true });
22569
- 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");
22570
22674
  } catch {
22571
22675
  }
22572
22676
  };
@@ -22574,14 +22678,15 @@ function createFileSink(metricsPath) {
22574
22678
  function runReduceTool(text, minLength, sink = noopSink) {
22575
22679
  const result = reduceAuto(text, minLength);
22576
22680
  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
- });
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
+ );
22585
22690
  }
22586
22691
  return { content: [{ type: "text", text: result.output }] };
22587
22692
  }
@@ -22625,9 +22730,9 @@ var init_server3 = __esm({
22625
22730
  // src/cli.ts
22626
22731
  init_src();
22627
22732
  import { parseArgs } from "node:util";
22628
- import fs11 from "node:fs";
22629
- import path15 from "node:path";
22630
- import os2 from "node:os";
22733
+ import fs12 from "node:fs";
22734
+ import path16 from "node:path";
22735
+ import os4 from "node:os";
22631
22736
 
22632
22737
  // src/doctor.ts
22633
22738
  import fs from "node:fs";
@@ -22857,7 +22962,7 @@ function planOpencodeInstall(input) {
22857
22962
  const changed = wrapperChanged || pkgChanged || opencodeChanged;
22858
22963
  return { wrapperContent, packageJsonContent, opencodeJsonContent, alreadyInstalled, changed };
22859
22964
  }
22860
- function runInstallOpencode(dir, apply, presetName, installDeps = true) {
22965
+ function runInstallOpencode(dir, apply, presetName, installDeps = true, options = {}) {
22861
22966
  let preset;
22862
22967
  let adapterConfig = { ...DEFAULT_OPENCODE_ADAPTER_CONFIG };
22863
22968
  if (presetName) {
@@ -22865,19 +22970,22 @@ function runInstallOpencode(dir, apply, presetName, installDeps = true) {
22865
22970
  if (!preset) throw new Error(`Unknown preset: ${presetName}`);
22866
22971
  adapterConfig = { ...adapterConfig, ...preset.adapter };
22867
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;
22868
22976
  const wrapperPath = path2.join(dir, WRAPPER_REL);
22869
22977
  const packageJsonPath = path2.join(dir, PKG_REL);
22870
22978
  const opencodeJsonPath = path2.join(dir, OPENCODE_JSON);
22871
- const readOrNull = (p) => {
22979
+ const readOrNull2 = (p) => {
22872
22980
  try {
22873
22981
  return fs2.readFileSync(p, "utf8");
22874
22982
  } catch {
22875
22983
  return null;
22876
22984
  }
22877
22985
  };
22878
- const existingWrapper = readOrNull(wrapperPath);
22879
- const existingPackageJson = readOrNull(packageJsonPath);
22880
- const existingOpencodeJson = readOrNull(opencodeJsonPath);
22986
+ const existingWrapper = readOrNull2(wrapperPath);
22987
+ const existingPackageJson = readOrNull2(packageJsonPath);
22988
+ const existingOpencodeJson = readOrNull2(opencodeJsonPath);
22881
22989
  const plan = planOpencodeInstall({
22882
22990
  existingWrapper,
22883
22991
  existingPackageJson,
@@ -22897,7 +23005,9 @@ function runInstallOpencode(dir, apply, presetName, installDeps = true) {
22897
23005
  const npm = process.platform === "win32" ? "npm.cmd" : "npm";
22898
23006
  const res = spawnSync(npm, ["install", "--silent"], {
22899
23007
  cwd: path2.dirname(packageJsonPath),
22900
- encoding: "utf8"
23008
+ encoding: "utf8",
23009
+ input: "",
23010
+ timeout: 12e4
22901
23011
  });
22902
23012
  if (res.error || res.status !== 0) {
22903
23013
  depsInstalled = false;
@@ -22922,6 +23032,7 @@ function runInstallOpencode(dir, apply, presetName, installDeps = true) {
22922
23032
 
22923
23033
  // src/install-codex.ts
22924
23034
  import fs5 from "node:fs";
23035
+ import os from "node:os";
22925
23036
  import path6 from "node:path";
22926
23037
 
22927
23038
  // ../adapter-codex/src/index.ts
@@ -22993,9 +23104,31 @@ function hasHarnessTrimHook(document) {
22993
23104
  const post = hooks?.PostToolUse;
22994
23105
  if (!Array.isArray(post)) return false;
22995
23106
  return post.some(
22996
- (entry) => Array.isArray(entry?.hooks) && entry.hooks.some((hook) => typeof hook?.command === "string" && hook.command.includes("harnesstrim hook codex"))
23107
+ (entry) => Array.isArray(entry?.hooks) && entry.hooks.some(
23108
+ (hook) => typeof hook?.command === "string" && hook.command.includes("harnesstrim") && hook.command.includes("hook codex")
23109
+ )
23110
+ );
23111
+ }
23112
+ function hasExactHarnessTrimHookCommand(document, command) {
23113
+ const hooks = document.hooks;
23114
+ const post = hooks?.PostToolUse;
23115
+ return Array.isArray(post) && post.some(
23116
+ (entry) => Array.isArray(entry?.hooks) && entry.hooks.some((hook) => hook?.command === command)
22997
23117
  );
22998
23118
  }
23119
+ function replaceHarnessTrimHookCommand(document, command) {
23120
+ const hooks = document.hooks;
23121
+ const post = hooks?.PostToolUse;
23122
+ if (!Array.isArray(post)) return;
23123
+ for (const entry of post) {
23124
+ if (!Array.isArray(entry?.hooks)) continue;
23125
+ for (const hook of entry.hooks) {
23126
+ if (typeof hook?.command === "string" && hook.command.includes("harnesstrim") && hook.command.includes("hook codex")) {
23127
+ hook.command = command;
23128
+ }
23129
+ }
23130
+ }
23131
+ }
22999
23132
  function planCodexHookInstall(input) {
23000
23133
  let document = {};
23001
23134
  let action;
@@ -23012,14 +23145,21 @@ function planCodexHookInstall(input) {
23012
23145
  throw new Error(".codex/hooks.json must contain a JSON object; refusing to overwrite it.");
23013
23146
  }
23014
23147
  document = parsed;
23015
- action = hasHarnessTrimHook(document) ? "present" : "patch";
23148
+ action = hasHarnessTrimHook(document) && (!input.hookCommand || hasExactHarnessTrimHookCommand(document, input.hookCommand)) ? "present" : "patch";
23016
23149
  }
23017
23150
  if (action === "present") {
23018
23151
  return { hooksFile: path3.join(input.projectDir, ".codex", "hooks.json"), action, nextHooks: document };
23019
23152
  }
23153
+ if (hasHarnessTrimHook(document)) {
23154
+ replaceHarnessTrimHookCommand(document, input.hookCommand ?? CODEX_HOOK_COMMAND);
23155
+ return { hooksFile: path3.join(input.projectDir, ".codex", "hooks.json"), action, nextHooks: document };
23156
+ }
23020
23157
  const hooks = { ...document.hooks ?? {} };
23021
23158
  const post = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
23022
- post.push({ matcher: CODEX_HOOK_MATCHER, hooks: [{ type: "command", command: CODEX_HOOK_COMMAND }] });
23159
+ post.push({
23160
+ matcher: CODEX_HOOK_MATCHER,
23161
+ hooks: [{ type: "command", command: input.hookCommand ?? CODEX_HOOK_COMMAND }]
23162
+ });
23023
23163
  hooks.PostToolUse = post;
23024
23164
  return {
23025
23165
  hooksFile: path3.join(input.projectDir, ".codex", "hooks.json"),
@@ -23028,6 +23168,7 @@ function planCodexHookInstall(input) {
23028
23168
  };
23029
23169
  }
23030
23170
  function planCodexInstall(input) {
23171
+ const includeInstructions = input.includeInstructions ?? true;
23031
23172
  const skillsDest = path3.join(input.projectDir, ".codex", "skills");
23032
23173
  const existing = new Set(input.existingSkillNames);
23033
23174
  const skills = input.skillNames.map((name) => ({
@@ -23037,7 +23178,9 @@ function planCodexInstall(input) {
23037
23178
  present: existing.has(name)
23038
23179
  }));
23039
23180
  let instructionsAction;
23040
- if (input.agentsMdContent === null) {
23181
+ if (!includeInstructions) {
23182
+ instructionsAction = "skip";
23183
+ } else if (input.agentsMdContent === null) {
23041
23184
  instructionsAction = "create";
23042
23185
  } else if (input.agentsMdContent.includes(HARNESSTRIM_MARKER)) {
23043
23186
  instructionsAction = "present";
@@ -23049,7 +23192,8 @@ function planCodexInstall(input) {
23049
23192
  skills,
23050
23193
  instructionsFile: path3.join(input.projectDir, "AGENTS.md"),
23051
23194
  instructionsAction,
23052
- instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET
23195
+ instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET,
23196
+ changed: skills.some((s) => !s.present) || instructionsAction !== "present" && instructionsAction !== "skip"
23053
23197
  };
23054
23198
  }
23055
23199
 
@@ -23094,6 +23238,13 @@ function existingSkillNames(dest) {
23094
23238
  }
23095
23239
 
23096
23240
  // src/install-codex.ts
23241
+ function resolveCodexHookCommand() {
23242
+ if (process.platform !== "win32") return void 0;
23243
+ const pnpmHome = process.env.PNPM_HOME ?? path6.join(process.env.LOCALAPPDATA ?? path6.join(os.homedir(), "AppData", "Local"), "pnpm");
23244
+ const shim = path6.join(pnpmHome, "harnesstrim.CMD");
23245
+ if (!fs5.existsSync(shim)) return void 0;
23246
+ return `"${shim}" hook codex --metrics .harnesstrim/metrics.jsonl`;
23247
+ }
23097
23248
  function readHooksJson(hooksPath) {
23098
23249
  try {
23099
23250
  return fs5.readFileSync(hooksPath, "utf8");
@@ -23107,7 +23258,7 @@ function applyHookPlan(plan, apply) {
23107
23258
  fs5.writeFileSync(plan.hooksFile, JSON.stringify(plan.nextHooks, null, 2) + "\n");
23108
23259
  return true;
23109
23260
  }
23110
- function runInstallCodex(dir, apply, hook = false) {
23261
+ function runInstallCodex(dir, apply, hook = false, options = {}) {
23111
23262
  const skillsSourceDir = resolveSkillsSourceDir();
23112
23263
  const skillNames = listShippedSkills(skillsSourceDir);
23113
23264
  const skillsDest = path6.join(dir, ".codex", "skills");
@@ -23123,14 +23274,16 @@ function runInstallCodex(dir, apply, hook = false) {
23123
23274
  skillsSourceDir,
23124
23275
  skillNames,
23125
23276
  agentsMdContent,
23126
- existingSkillNames: existingSkillNames(skillsDest)
23277
+ existingSkillNames: existingSkillNames(skillsDest),
23278
+ includeInstructions: options.includeInstructions
23127
23279
  });
23128
23280
  const hooksPath = path6.join(dir, ".codex", "hooks.json");
23129
23281
  const hooksJsonContent = hook ? readHooksJson(hooksPath) : null;
23130
- const hookPlan = hook ? planCodexHookInstall({ projectDir: dir, hooksJsonContent }) : null;
23282
+ const hookPlan = hook ? planCodexHookInstall({ projectDir: dir, hooksJsonContent, hookCommand: resolveCodexHookCommand() }) : null;
23131
23283
  const copied = [];
23284
+ const hookChanged = hookPlan !== null && hookPlan.action !== "present";
23132
23285
  let applied = false;
23133
- if (apply) {
23286
+ if (apply && (plan.changed || hookChanged)) {
23134
23287
  for (const skill of plan.skills) {
23135
23288
  if (skill.present) continue;
23136
23289
  fs5.cpSync(skill.from, skill.to, { recursive: true });
@@ -23141,7 +23294,7 @@ function runInstallCodex(dir, apply, hook = false) {
23141
23294
  } else if (plan.instructionsAction === "append") {
23142
23295
  fs5.appendFileSync(plan.instructionsFile, "\n\n" + plan.instructionsSnippet + "\n");
23143
23296
  }
23144
- if (hookPlan) applyHookPlan(hookPlan, true);
23297
+ if (hookPlan && hookChanged) applyHookPlan(hookPlan, true);
23145
23298
  applied = true;
23146
23299
  }
23147
23300
  return { plan, hookPlan, applied, copied };
@@ -23152,7 +23305,8 @@ function runInstallCodexGlobalHook(codexHome, apply) {
23152
23305
  // The planner expects the directory that contains .codex; for a user-level config
23153
23306
  // the Codex home is itself that directory, so add its parent and use a normal path.
23154
23307
  projectDir: path6.dirname(codexHome),
23155
- hooksJsonContent: readHooksJson(hooksPath)
23308
+ hooksJsonContent: readHooksJson(hooksPath),
23309
+ hookCommand: resolveCodexHookCommand()
23156
23310
  });
23157
23311
  return { hookPlan, applied: applyHookPlan(hookPlan, apply) };
23158
23312
  }
@@ -23236,6 +23390,8 @@ function hasHarnessTrimHook2(settings) {
23236
23390
  );
23237
23391
  }
23238
23392
  function planClaudeInstall(input) {
23393
+ const includeHook = input.includeHook ?? true;
23394
+ const includeInstructions = input.includeInstructions ?? true;
23239
23395
  const skillsDest = path7.join(input.projectDir, ".claude", "skills");
23240
23396
  const existing = new Set(input.existingSkillNames);
23241
23397
  const skills = input.skillNames.map((name) => ({
@@ -23257,8 +23413,12 @@ function planClaudeInstall(input) {
23257
23413
  }
23258
23414
  action = hasHarnessTrimHook2(settings) ? "present" : "patch";
23259
23415
  }
23260
- const nextSettings = action === "present" ? settings : addHook(settings);
23261
- 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";
23262
23422
  return {
23263
23423
  skillsDest,
23264
23424
  skills,
@@ -23267,7 +23427,8 @@ function planClaudeInstall(input) {
23267
23427
  nextSettings,
23268
23428
  instructionsFile: path7.join(input.projectDir, "CLAUDE.md"),
23269
23429
  instructionsAction,
23270
- instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET2
23430
+ instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET2,
23431
+ changed: skills.some((s) => !s.present) || action !== "present" && action !== "skip" || instructionsAction !== "present" && instructionsAction !== "skip"
23271
23432
  };
23272
23433
  }
23273
23434
  function addHook(settings) {
@@ -23281,7 +23442,7 @@ function addHook(settings) {
23281
23442
  }
23282
23443
 
23283
23444
  // src/install-claude.ts
23284
- function runInstallClaude(dir, apply) {
23445
+ function runInstallClaude(dir, apply, options = {}) {
23285
23446
  const skillsSourceDir = resolveSkillsSourceDir();
23286
23447
  const skillNames = listShippedSkills(skillsSourceDir);
23287
23448
  const skillsDest = path8.join(dir, ".claude", "skills");
@@ -23305,17 +23466,19 @@ function runInstallClaude(dir, apply) {
23305
23466
  skillNames,
23306
23467
  settingsJsonContent,
23307
23468
  claudeMdContent,
23308
- existingSkillNames: existingSkillNames(skillsDest)
23469
+ existingSkillNames: existingSkillNames(skillsDest),
23470
+ includeHook: options.includeHook,
23471
+ includeInstructions: options.includeInstructions
23309
23472
  });
23310
23473
  const copied = [];
23311
23474
  let applied = false;
23312
- if (apply) {
23475
+ if (apply && plan.changed) {
23313
23476
  for (const skill of plan.skills) {
23314
23477
  if (skill.present) continue;
23315
23478
  fs6.cpSync(skill.from, skill.to, { recursive: true });
23316
23479
  copied.push(skill.name);
23317
23480
  }
23318
- if (plan.settingsAction !== "present") {
23481
+ if (plan.settingsAction !== "present" && plan.settingsAction !== "skip") {
23319
23482
  fs6.mkdirSync(path8.dirname(plan.settingsFile), { recursive: true });
23320
23483
  fs6.writeFileSync(plan.settingsFile, JSON.stringify(plan.nextSettings, null, 2) + "\n");
23321
23484
  }
@@ -23332,12 +23495,13 @@ function runInstallClaude(dir, apply) {
23332
23495
  // src/install-pi.ts
23333
23496
  import fs7 from "node:fs";
23334
23497
  import path10 from "node:path";
23498
+ import os2 from "node:os";
23335
23499
 
23336
23500
  // ../adapter-pi/src/index.ts
23337
23501
  import path9 from "node:path";
23338
23502
  var PI_EXTENSION_NAME = "harnesstrim";
23339
23503
  function planPiInstall(input) {
23340
- const extensionDest = path9.join(input.installDir, ".pi", "extensions", PI_EXTENSION_NAME);
23504
+ const extensionDest = input.scope === "user" ? path9.join(input.installDir, ".pi", "agent", "extensions", PI_EXTENSION_NAME) : path9.join(input.installDir, ".pi", "extensions", PI_EXTENSION_NAME);
23341
23505
  return {
23342
23506
  extensionDest,
23343
23507
  extensionSource: input.extensionSourceDir,
@@ -23372,23 +23536,29 @@ function markerPresent(dest) {
23372
23536
  }
23373
23537
  function runInstallPi(installDir, apply) {
23374
23538
  const extensionSourceDir = resolvePiExtensionSourceDir();
23375
- const dest = path10.join(installDir, ".pi", "extensions", "harnesstrim");
23539
+ const scope = path10.resolve(installDir) === path10.resolve(os2.homedir()) ? "user" : "project";
23540
+ const dest = scope === "user" ? path10.join(installDir, ".pi", "agent", "extensions", "harnesstrim") : path10.join(installDir, ".pi", "extensions", "harnesstrim");
23376
23541
  const plan = planPiInstall({
23377
23542
  installDir,
23378
23543
  extensionSourceDir,
23379
23544
  extensionDirExists: dirExists(dest),
23380
- markerPresent: markerPresent(dest)
23545
+ markerPresent: markerPresent(dest),
23546
+ scope
23381
23547
  });
23382
23548
  const copiedFiles = [];
23383
23549
  let applied = false;
23384
- if (apply && !plan.alreadyInstalled) {
23550
+ if (apply) {
23385
23551
  fs7.mkdirSync(dest, { recursive: true });
23386
- for (const entry of fs7.readdirSync(extensionSourceDir, { withFileTypes: true })) {
23387
- if (!entry.isDirectory()) {
23388
- fs7.copyFileSync(path10.join(extensionSourceDir, entry.name), path10.join(dest, entry.name));
23389
- 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 });
23390
23556
  }
23391
23557
  }
23558
+ for (const entry of sourceFiles) {
23559
+ fs7.copyFileSync(path10.join(extensionSourceDir, entry), path10.join(dest, entry));
23560
+ copiedFiles.push(entry);
23561
+ }
23392
23562
  fs7.writeFileSync(path10.join(dest, ".installed"), markerFileContent());
23393
23563
  copiedFiles.push(".installed");
23394
23564
  applied = true;
@@ -23485,8 +23655,8 @@ function runInstallHermes(installDir, apply) {
23485
23655
  init_src();
23486
23656
  import fs9 from "node:fs";
23487
23657
  import path13 from "node:path";
23488
- import os from "node:os";
23489
- var HERMES_METRICS_PATH = path13.join(os.homedir(), ".hermes", "harnesstrim-metrics.jsonl");
23658
+ import os3 from "node:os";
23659
+ var HERMES_METRICS_PATH = path13.join(os3.homedir(), ".hermes", "harnesstrim-metrics.jsonl");
23490
23660
  var LOCAL_METRICS_PATH = ".harnesstrim/metrics.jsonl";
23491
23661
  var DEFAULT_METRICS_PATH = fs9.existsSync(HERMES_METRICS_PATH) ? HERMES_METRICS_PATH : LOCAL_METRICS_PATH;
23492
23662
  function loadMetrics(filePath) {
@@ -23502,9 +23672,46 @@ function loadMetrics(filePath) {
23502
23672
  return { path: filePath, found: true, summary: summarize(parseTrimEvents(raw)) };
23503
23673
  }
23504
23674
 
23675
+ // package.json
23676
+ var package_default = {
23677
+ name: "harnesstrim",
23678
+ version: "0.0.7",
23679
+ description: "HarnessTrim CLI: doctor (diagnose token waste), install adapters, run benchmarks.",
23680
+ license: "MIT",
23681
+ type: "module",
23682
+ bin: {
23683
+ harnesstrim: "./dist/cli.mjs"
23684
+ },
23685
+ files: [
23686
+ "dist",
23687
+ "assets"
23688
+ ],
23689
+ publishConfig: {
23690
+ access: "public"
23691
+ },
23692
+ devDependencies: {
23693
+ "@harnesstrim/adapter-claude": "workspace:*",
23694
+ "@harnesstrim/adapter-codex": "workspace:*",
23695
+ "@harnesstrim/adapter-hermes": "workspace:*",
23696
+ "@harnesstrim/adapter-pi": "workspace:*",
23697
+ "@harnesstrim/benchmarks": "workspace:*",
23698
+ "@harnesstrim/core": "workspace:*",
23699
+ "@harnesstrim/mcp": "workspace:*",
23700
+ esbuild: "^0.25.0"
23701
+ },
23702
+ scripts: {
23703
+ build: "node build.mjs",
23704
+ prepare: "node build.mjs",
23705
+ prepack: "node build.mjs",
23706
+ test: 'node --test "src/**/*.test.ts"',
23707
+ typecheck: "tsc -p tsconfig.json"
23708
+ }
23709
+ };
23710
+
23505
23711
  // src/reduce.ts
23506
23712
  init_src();
23507
23713
  async function readStdin() {
23714
+ if (process.stdin.isTTY) return "";
23508
23715
  const chunks = [];
23509
23716
  for await (const chunk of process.stdin) {
23510
23717
  chunks.push(chunk);
@@ -23516,6 +23723,324 @@ function reducePipe(input, minLength) {
23516
23723
  return { ...result, beforeChars: input.length, afterChars: result.output.length };
23517
23724
  }
23518
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
+
23519
24044
  // src/render.ts
23520
24045
  var ICON = { warn: "!", info: "i", ok: "+" };
23521
24046
  function renderDoctor(report) {
@@ -23614,7 +24139,7 @@ function renderCodexInstall(result, apply) {
23614
24139
  lines.push(` ${s.name.padEnd(18)} ${state}`);
23615
24140
  }
23616
24141
  lines.push("");
23617
- 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.`;
23618
24143
  lines.push(instr);
23619
24144
  if (result.hookPlan) {
23620
24145
  lines.push("");
@@ -23663,7 +24188,9 @@ function renderClaudeInstall(result, apply) {
23663
24188
  lines.push(` ${s.name.padEnd(18)} ${state}`);
23664
24189
  }
23665
24190
  lines.push("");
23666
- 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") {
23667
24194
  lines.push(`${plan.settingsFile}: PostToolUse reducer hook already present (no change).`);
23668
24195
  } else {
23669
24196
  lines.push(
@@ -23676,7 +24203,9 @@ function renderClaudeInstall(result, apply) {
23676
24203
  }
23677
24204
  }
23678
24205
  lines.push("");
23679
- 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") {
23680
24209
  lines.push(`${plan.instructionsFile}: reduce-pipe instruction already present (no change).`);
23681
24210
  } else {
23682
24211
  lines.push(
@@ -23731,7 +24260,7 @@ function renderPiInstall(result, apply) {
23731
24260
  lines.push(` Copied: ${result.copiedFiles.join(", ")}`);
23732
24261
  } else {
23733
24262
  lines.push(` Source: ${plan.extensionSource}`);
23734
- lines.push(" (harnesstrim.ts + .installed marker)");
24263
+ lines.push(" (index.ts + .installed marker)");
23735
24264
  }
23736
24265
  lines.push("");
23737
24266
  lines.push("The extension hooks Pi's `tool_result` and needs `harnesstrim` on PATH.");
@@ -23766,6 +24295,192 @@ Enable it in the adapter (telemetry: true) to record reductions.`;
23766
24295
  }
23767
24296
  return lines.join("\n");
23768
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
+ }
23769
24484
 
23770
24485
  // src/cli.ts
23771
24486
  var HELP = `harnesstrim \u2014 one token policy for coding harnesses
@@ -23775,16 +24490,25 @@ Usage:
23775
24490
  harnesstrim install opencode [dir] Wire the adapter into opencode.json (dry-run)
23776
24491
  --apply Actually write the change
23777
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
23778
24496
  harnesstrim install codex [dir] Install skills + AGENTS.md reduction guidance (dry-run)
23779
24497
  --apply Actually write the change
23780
24498
  --hook Also install the experimental Bash PostToolUse hook
24499
+ --no-instructions Skills only (no AGENTS.md reduce-pipe instruction)
23781
24500
  --global With --hook, install it once in ~/.codex (no project files)
23782
24501
  harnesstrim install claude [dir] Install skills + PostToolUse reducer hook (dry-run)
23783
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)
23784
24505
  harnesstrim install hermes [dir] Install Hermes plugin (dry-run)
23785
24506
  --apply Actually write the change
23786
24507
  harnesstrim install pi [dir] Install Pi tool_result extension (dry-run)
23787
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)
23788
24512
  harnesstrim hook claude [--metrics <path>]
23789
24513
  PostToolUse hook runtime; --metrics records a TrimEvent per reduction
23790
24514
  harnesstrim hook codex [--metrics <path>]
@@ -23798,10 +24522,13 @@ Usage:
23798
24522
  harnesstrim mcp [--metrics <path>] Start the MCP server (stdio) exposing a reduce tool;
23799
24523
  --metrics records a TrimEvent per reduction
23800
24524
  harnesstrim bench Run the Tier A reducer micro-benchmark
23801
- harnesstrim --help Show this help
24525
+ harnesstrim --version Print the installed version
24526
+
24527
+ Flags:
24528
+ --json Print machine-readable JSON (doctor, metrics, install, uninstall)
23802
24529
 
23803
24530
  Notes:
23804
- - 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.
23805
24532
  - dir defaults to the current directory; metrics path defaults to ${DEFAULT_METRICS_PATH}.
23806
24533
  - reduce reads stdin and writes slimmed output to stdout, e.g. npm test 2>&1 | harnesstrim reduce`;
23807
24534
  async function main(argv) {
@@ -23810,6 +24537,7 @@ async function main(argv) {
23810
24537
  allowPositionals: true,
23811
24538
  options: {
23812
24539
  help: { type: "boolean", short: "h" },
24540
+ version: { type: "boolean", short: "v" },
23813
24541
  apply: { type: "boolean" },
23814
24542
  preset: { type: "string" },
23815
24543
  stats: { type: "boolean" },
@@ -23817,10 +24545,19 @@ async function main(argv) {
23817
24545
  log: { type: "string" },
23818
24546
  metrics: { type: "string" },
23819
24547
  hook: { type: "boolean" },
23820
- 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" }
23821
24554
  }
23822
24555
  });
23823
24556
  const [command, ...rest] = positionals;
24557
+ if (values.version) {
24558
+ console.log(package_default.version);
24559
+ return 0;
24560
+ }
23824
24561
  if (values.help || !command) {
23825
24562
  console.log(HELP);
23826
24563
  return 0;
@@ -23828,16 +24565,34 @@ async function main(argv) {
23828
24565
  switch (command) {
23829
24566
  case "doctor": {
23830
24567
  const dir = rest[0] ?? process.cwd();
23831
- 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
+ }
23832
24574
  return 0;
23833
24575
  }
23834
24576
  case "install": {
23835
24577
  const target = rest[0];
23836
- const dir = rest[1] ?? (target === "hermes" ? os2.homedir() : process.cwd());
24578
+ const dir = rest[1] ?? (target === "hermes" ? os4.homedir() : process.cwd());
23837
24579
  const apply = values.apply === true;
24580
+ const asJson = values.json === true;
23838
24581
  if (target === "opencode") {
23839
- const result = runInstallOpencode(dir, apply, values.preset);
23840
- 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));
23841
24596
  return 0;
23842
24597
  }
23843
24598
  if (target === "codex") {
@@ -23846,27 +24601,63 @@ async function main(argv) {
23846
24601
  console.error("`harnesstrim install codex --global` requires `--hook`.");
23847
24602
  return 1;
23848
24603
  }
23849
- console.log(renderCodexGlobalHookInstall(runInstallCodexGlobalHook(path15.join(os2.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));
23850
24607
  return 0;
23851
24608
  }
23852
- 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));
23853
24614
  return 0;
23854
24615
  }
23855
24616
  if (target === "claude") {
23856
- 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));
23857
24623
  return 0;
23858
24624
  }
23859
24625
  if (target === "hermes") {
23860
- 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));
23861
24629
  return 0;
23862
24630
  }
23863
24631
  if (target === "pi") {
23864
- 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));
23865
24635
  return 0;
23866
24636
  }
23867
24637
  console.error(`Unknown install target: ${target ?? "(none)"}. Supported: opencode, codex, claude, hermes, pi.`);
23868
24638
  return 1;
23869
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
+ }
23870
24661
  case "hook": {
23871
24662
  const which = rest[0];
23872
24663
  if (which !== "claude" && which !== "codex") {
@@ -23878,18 +24669,24 @@ async function main(argv) {
23878
24669
  process.stdout.write(response);
23879
24670
  if (values.metrics && event) {
23880
24671
  try {
23881
- const p = path15.resolve(values.metrics);
23882
- fs11.mkdirSync(path15.dirname(p), { recursive: true });
23883
- fs11.appendFileSync(
24672
+ const p = path16.resolve(values.metrics);
24673
+ fs12.mkdirSync(path16.dirname(p), { recursive: true });
24674
+ fs12.appendFileSync(
23884
24675
  p,
23885
- 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"
23886
24683
  );
23887
24684
  } catch {
23888
24685
  }
23889
24686
  }
23890
24687
  if (values.log) {
23891
24688
  try {
23892
- fs11.appendFileSync(
24689
+ fs12.appendFileSync(
23893
24690
  values.log,
23894
24691
  JSON.stringify({ inputChars: input.length, changed: response !== "{}", responseChars: response.length }) + "\n"
23895
24692
  );
@@ -23918,8 +24715,13 @@ async function main(argv) {
23918
24715
  return 1;
23919
24716
  }
23920
24717
  case "metrics": {
23921
- const path16 = rest[0] ?? DEFAULT_METRICS_PATH;
23922
- 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
+ }
23923
24725
  return 0;
23924
24726
  }
23925
24727
  case "reduce": {
@@ -23934,18 +24736,20 @@ async function main(argv) {
23934
24736
  process.stdout.write(result.output);
23935
24737
  if (values.metrics && result.changed) {
23936
24738
  try {
23937
- const p = path15.resolve(values.metrics);
23938
- fs11.mkdirSync(path15.dirname(p), { recursive: true });
23939
- fs11.appendFileSync(
24739
+ const p = path16.resolve(values.metrics);
24740
+ fs12.mkdirSync(path16.dirname(p), { recursive: true });
24741
+ fs12.appendFileSync(
23940
24742
  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"
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"
23949
24753
  );
23950
24754
  } catch {
23951
24755
  }
@@ -23986,6 +24790,12 @@ async function main(argv) {
23986
24790
  return 1;
23987
24791
  }
23988
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
+ }
23989
24799
  main(process.argv.slice(2)).then((code) => {
23990
24800
  process.exitCode = code;
23991
24801
  }).catch((err) => {