harnesstrim 0.0.6 → 0.1.0

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
@@ -713,6 +713,22 @@ 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
+ changed: partial2.changed ?? true,
728
+ beforeTokens: partial2.beforeTokens ?? null,
729
+ afterTokens: partial2.afterTokens ?? null
730
+ };
731
+ }
716
732
  function pct(before, after) {
717
733
  if (before === 0) return 0;
718
734
  return Math.round((1 - after / before) * 1e3) / 10;
@@ -720,10 +736,30 @@ function pct(before, after) {
720
736
  function summarize(events) {
721
737
  let beforeChars = 0;
722
738
  let afterChars = 0;
739
+ let reduced = 0;
740
+ let passThrough = 0;
741
+ let reductionErrors = 0;
742
+ let grewChars = 0;
723
743
  const byReducerMap = /* @__PURE__ */ new Map();
744
+ const byHarnessMap = /* @__PURE__ */ new Map();
724
745
  for (const e of events) {
725
746
  beforeChars += e.beforeChars;
726
747
  afterChars += e.afterChars;
748
+ if (e.changed === false) {
749
+ passThrough++;
750
+ } else if (e.afterChars > e.beforeChars) {
751
+ reductionErrors++;
752
+ grewChars += e.afterChars - e.beforeChars;
753
+ } else {
754
+ reduced++;
755
+ }
756
+ const harness = e.harness ?? "unknown";
757
+ const h = byHarnessMap.get(harness) ?? { harness, count: 0, beforeChars: 0, afterChars: 0, savedChars: 0, reductionPct: 0 };
758
+ h.count += 1;
759
+ h.beforeChars += e.beforeChars;
760
+ h.afterChars += e.afterChars;
761
+ h.savedChars += e.beforeChars - e.afterChars;
762
+ byHarnessMap.set(harness, h);
727
763
  if (e.reducer === null) continue;
728
764
  const b = byReducerMap.get(e.reducer) ?? { reducer: e.reducer, count: 0, beforeChars: 0, afterChars: 0, savedChars: 0 };
729
765
  b.count += 1;
@@ -733,13 +769,20 @@ function summarize(events) {
733
769
  byReducerMap.set(e.reducer, b);
734
770
  }
735
771
  const byReducer = [...byReducerMap.values()].sort((a, b) => b.savedChars - a.savedChars);
772
+ const byHarness = [...byHarnessMap.values()].map((h) => ({ ...h, reductionPct: pct(h.beforeChars, h.afterChars) })).sort((a, b) => b.savedChars - a.savedChars);
736
773
  return {
737
774
  events: events.length,
738
775
  beforeChars,
739
776
  afterChars,
740
777
  savedChars: beforeChars - afterChars,
741
778
  reductionPct: pct(beforeChars, afterChars),
742
- byReducer
779
+ byReducer,
780
+ byHarness,
781
+ reduced,
782
+ passThrough,
783
+ passThroughRate: events.length === 0 ? 0 : Math.round(passThrough / events.length * 1e3) / 10,
784
+ reductionErrors,
785
+ grewChars
743
786
  };
744
787
  }
745
788
  function parseTrimEvents(jsonl) {
@@ -753,7 +796,7 @@ function parseTrimEvents(jsonl) {
753
796
  } catch {
754
797
  continue;
755
798
  }
756
- if (isTrimEvent(parsed)) out.push(parsed);
799
+ if (isTrimEvent(parsed)) out.push(normalize(parsed));
757
800
  }
758
801
  return out;
759
802
  }
@@ -762,9 +805,26 @@ function isTrimEvent(value) {
762
805
  const v = value;
763
806
  return typeof v.beforeChars === "number" && typeof v.afterChars === "number" && typeof v.tool === "string" && (typeof v.reducer === "string" || v.reducer === null);
764
807
  }
808
+ function normalize(v) {
809
+ return {
810
+ schemaVersion: typeof v.schemaVersion === "number" ? v.schemaVersion : 0,
811
+ eventId: typeof v.eventId === "string" ? v.eventId : "",
812
+ ts: v.ts,
813
+ harness: v.harness,
814
+ tool: v.tool,
815
+ reducer: v.reducer,
816
+ beforeChars: v.beforeChars,
817
+ afterChars: v.afterChars,
818
+ changed: typeof v.changed === "boolean" ? v.changed : true,
819
+ beforeTokens: typeof v.beforeTokens === "number" ? v.beforeTokens : null,
820
+ afterTokens: typeof v.afterTokens === "number" ? v.afterTokens : null
821
+ };
822
+ }
823
+ var TRIM_EVENT_SCHEMA_VERSION;
765
824
  var init_trim_event = __esm({
766
825
  "../core/src/metrics/trim-event.ts"() {
767
826
  "use strict";
827
+ TRIM_EVENT_SCHEMA_VERSION = 1;
768
828
  }
769
829
  });
770
830
 
@@ -1239,8 +1299,8 @@ var init_parseUtil = __esm({
1239
1299
  init_errors();
1240
1300
  init_en();
1241
1301
  makeIssue = (params) => {
1242
- const { data, path: path16, errorMaps, issueData } = params;
1243
- const fullPath = [...path16, ...issueData.path || []];
1302
+ const { data, path: path17, errorMaps, issueData } = params;
1303
+ const fullPath = [...path17, ...issueData.path || []];
1244
1304
  const fullIssue = {
1245
1305
  ...issueData,
1246
1306
  path: fullPath
@@ -1548,11 +1608,11 @@ var init_types2 = __esm({
1548
1608
  init_parseUtil();
1549
1609
  init_util();
1550
1610
  ParseInputLazyPath = class {
1551
- constructor(parent, value, path16, key) {
1611
+ constructor(parent, value, path17, key) {
1552
1612
  this._cachedPath = [];
1553
1613
  this.parent = parent;
1554
1614
  this.data = value;
1555
- this._path = path16;
1615
+ this._path = path17;
1556
1616
  this._key = key;
1557
1617
  }
1558
1618
  get path() {
@@ -5133,10 +5193,10 @@ function assignProp(target, prop, value) {
5133
5193
  configurable: true
5134
5194
  });
5135
5195
  }
5136
- function getElementAtPath(obj, path16) {
5137
- if (!path16)
5196
+ function getElementAtPath(obj, path17) {
5197
+ if (!path17)
5138
5198
  return obj;
5139
- return path16.reduce((acc, key) => acc?.[key], obj);
5199
+ return path17.reduce((acc, key) => acc?.[key], obj);
5140
5200
  }
5141
5201
  function promiseAllObject(promisesObj) {
5142
5202
  const keys = Object.keys(promisesObj);
@@ -5385,11 +5445,11 @@ function aborted(x, startIndex = 0) {
5385
5445
  }
5386
5446
  return false;
5387
5447
  }
5388
- function prefixIssues(path16, issues) {
5448
+ function prefixIssues(path17, issues) {
5389
5449
  return issues.map((iss) => {
5390
5450
  var _a;
5391
5451
  (_a = iss).path ?? (_a.path = []);
5392
- iss.path.unshift(path16);
5452
+ iss.path.unshift(path17);
5393
5453
  return iss;
5394
5454
  });
5395
5455
  }
@@ -16225,8 +16285,8 @@ var require_resolve = __commonJS({
16225
16285
  }
16226
16286
  return count;
16227
16287
  }
16228
- function getFullPath(resolver, id = "", normalize) {
16229
- if (normalize !== false)
16288
+ function getFullPath(resolver, id = "", normalize2) {
16289
+ if (normalize2 !== false)
16230
16290
  id = normalizeId(id);
16231
16291
  const p = resolver.parse(id);
16232
16292
  return _getFullPath(resolver, p);
@@ -17219,8 +17279,8 @@ var require_utils = __commonJS({
17219
17279
  }
17220
17280
  return ind;
17221
17281
  }
17222
- function removeDotSegments(path16) {
17223
- let input = path16;
17282
+ function removeDotSegments(path17) {
17283
+ let input = path17;
17224
17284
  const output = [];
17225
17285
  let nextSlash = -1;
17226
17286
  let len = 0;
@@ -17472,8 +17532,8 @@ var require_schemes = __commonJS({
17472
17532
  wsComponent.secure = void 0;
17473
17533
  }
17474
17534
  if (wsComponent.resourceName) {
17475
- const [path16, query] = wsComponent.resourceName.split("?");
17476
- wsComponent.path = path16 && path16 !== "/" ? path16 : void 0;
17535
+ const [path17, query] = wsComponent.resourceName.split("?");
17536
+ wsComponent.path = path17 && path17 !== "/" ? path17 : void 0;
17477
17537
  wsComponent.query = query;
17478
17538
  wsComponent.resourceName = void 0;
17479
17539
  }
@@ -17622,7 +17682,7 @@ var require_fast_uri = __commonJS({
17622
17682
  "use strict";
17623
17683
  var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
17624
17684
  var { SCHEMES, getSchemeHandler } = require_schemes();
17625
- function normalize(uri, options) {
17685
+ function normalize2(uri, options) {
17626
17686
  if (typeof uri === "string") {
17627
17687
  uri = /** @type {T} */
17628
17688
  normalizeString(uri, options);
@@ -17889,7 +17949,7 @@ var require_fast_uri = __commonJS({
17889
17949
  }
17890
17950
  var fastUri = {
17891
17951
  SCHEMES,
17892
- normalize,
17952
+ normalize: normalize2,
17893
17953
  resolve,
17894
17954
  resolveComponent,
17895
17955
  equal,
@@ -20866,12 +20926,12 @@ var require_dist = __commonJS({
20866
20926
  throw new Error(`Unknown format "${name}"`);
20867
20927
  return f;
20868
20928
  };
20869
- function addFormats(ajv, list, fs12, exportName) {
20929
+ function addFormats(ajv, list, fs13, exportName) {
20870
20930
  var _a;
20871
20931
  var _b;
20872
20932
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
20873
20933
  for (const f of list)
20874
- ajv.addFormat(f, fs12[f]);
20934
+ ajv.addFormat(f, fs13[f]);
20875
20935
  }
20876
20936
  module.exports = exports = formatsPlugin;
20877
20937
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -22632,34 +22692,48 @@ __export(server_exports, {
22632
22692
  runReduceTool: () => runReduceTool,
22633
22693
  startStdioServer: () => startStdioServer
22634
22694
  });
22635
- import fs10 from "node:fs";
22636
- import path14 from "node:path";
22695
+ import fs11 from "node:fs";
22696
+ import path15 from "node:path";
22637
22697
  function createFileSink(metricsPath) {
22638
22698
  return (event) => {
22639
22699
  try {
22640
- const p = path14.resolve(metricsPath);
22641
- fs10.mkdirSync(path14.dirname(p), { recursive: true });
22642
- fs10.appendFileSync(p, JSON.stringify(event) + "\n");
22700
+ const p = path15.resolve(metricsPath);
22701
+ fs11.mkdirSync(path15.dirname(p), { recursive: true });
22702
+ fs11.appendFileSync(p, JSON.stringify(event) + "\n");
22643
22703
  } catch {
22644
22704
  }
22645
22705
  };
22646
22706
  }
22647
- function runReduceTool(text, minLength, sink = noopSink) {
22707
+ function runReduceTool(text, minLength, sink = noopSink, trackPassThrough2 = true) {
22648
22708
  const result = reduceAuto(text, minLength);
22709
+ const threshold = minLength ?? DEFAULT_MIN_LENGTH;
22649
22710
  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
- });
22711
+ sink(
22712
+ makeTrimEvent({
22713
+ harness: "mcp",
22714
+ tool: "reduce",
22715
+ reducer: result.reducer,
22716
+ beforeChars: text.length,
22717
+ afterChars: result.output.length
22718
+ })
22719
+ );
22720
+ } else if (trackPassThrough2 && text.length >= threshold) {
22721
+ sink(
22722
+ makeTrimEvent({
22723
+ harness: "mcp",
22724
+ tool: "reduce",
22725
+ reducer: null,
22726
+ beforeChars: text.length,
22727
+ afterChars: text.length,
22728
+ changed: false
22729
+ })
22730
+ );
22658
22731
  }
22659
22732
  return { content: [{ type: "text", text: result.output }] };
22660
22733
  }
22661
22734
  function createServer(options = {}) {
22662
22735
  const sink = options.metricsPath ? createFileSink(options.metricsPath) : noopSink;
22736
+ const trackPassThrough2 = options.trackPassThrough !== false;
22663
22737
  const server = new McpServer({ name: "harnesstrim", version: "0.0.1" });
22664
22738
  server.registerTool(
22665
22739
  "reduce",
@@ -22671,13 +22745,15 @@ function createServer(options = {}) {
22671
22745
  minLength: external_exports.number().optional().describe("Skip reduction for inputs shorter than this many characters (default 400)")
22672
22746
  }
22673
22747
  },
22674
- async ({ text, minLength }) => runReduceTool(text, minLength, sink)
22748
+ async ({ text, minLength }) => runReduceTool(text, minLength, sink, trackPassThrough2)
22675
22749
  );
22676
22750
  return server;
22677
22751
  }
22678
22752
  async function startStdioServer(options = {}) {
22679
22753
  const metricsPath = options.metricsPath ?? process.env.HARNESSTRIM_TELEMETRY_PATH;
22680
- const server = createServer({ metricsPath });
22754
+ const envTrack = process.env.HARNESSTRIM_TRACK_PASSTHROUGH;
22755
+ const trackPassThrough2 = options.trackPassThrough ?? (envTrack !== void 0 ? envTrack !== "0" && envTrack !== "false" : true);
22756
+ const server = createServer({ metricsPath, trackPassThrough: trackPassThrough2 });
22681
22757
  const transport = new StdioServerTransport();
22682
22758
  await server.connect(transport);
22683
22759
  }
@@ -22698,8 +22774,8 @@ var init_server3 = __esm({
22698
22774
  // src/cli.ts
22699
22775
  init_src();
22700
22776
  import { parseArgs } from "node:util";
22701
- import fs11 from "node:fs";
22702
- import path15 from "node:path";
22777
+ import fs12 from "node:fs";
22778
+ import path16 from "node:path";
22703
22779
  import os4 from "node:os";
22704
22780
 
22705
22781
  // src/doctor.ts
@@ -22930,7 +23006,7 @@ function planOpencodeInstall(input) {
22930
23006
  const changed = wrapperChanged || pkgChanged || opencodeChanged;
22931
23007
  return { wrapperContent, packageJsonContent, opencodeJsonContent, alreadyInstalled, changed };
22932
23008
  }
22933
- function runInstallOpencode(dir, apply, presetName, installDeps = true) {
23009
+ function runInstallOpencode(dir, apply, presetName, installDeps = true, options = {}) {
22934
23010
  let preset;
22935
23011
  let adapterConfig = { ...DEFAULT_OPENCODE_ADAPTER_CONFIG };
22936
23012
  if (presetName) {
@@ -22938,19 +23014,22 @@ function runInstallOpencode(dir, apply, presetName, installDeps = true) {
22938
23014
  if (!preset) throw new Error(`Unknown preset: ${presetName}`);
22939
23015
  adapterConfig = { ...adapterConfig, ...preset.adapter };
22940
23016
  }
23017
+ if (options.mode !== void 0) adapterConfig.mode = options.mode;
23018
+ if (options.minLength !== void 0) adapterConfig.minLength = options.minLength;
23019
+ if (options.tools !== void 0) adapterConfig.toolFilter = options.tools;
22941
23020
  const wrapperPath = path2.join(dir, WRAPPER_REL);
22942
23021
  const packageJsonPath = path2.join(dir, PKG_REL);
22943
23022
  const opencodeJsonPath = path2.join(dir, OPENCODE_JSON);
22944
- const readOrNull = (p) => {
23023
+ const readOrNull2 = (p) => {
22945
23024
  try {
22946
23025
  return fs2.readFileSync(p, "utf8");
22947
23026
  } catch {
22948
23027
  return null;
22949
23028
  }
22950
23029
  };
22951
- const existingWrapper = readOrNull(wrapperPath);
22952
- const existingPackageJson = readOrNull(packageJsonPath);
22953
- const existingOpencodeJson = readOrNull(opencodeJsonPath);
23030
+ const existingWrapper = readOrNull2(wrapperPath);
23031
+ const existingPackageJson = readOrNull2(packageJsonPath);
23032
+ const existingOpencodeJson = readOrNull2(opencodeJsonPath);
22954
23033
  const plan = planOpencodeInstall({
22955
23034
  existingWrapper,
22956
23035
  existingPackageJson,
@@ -23007,9 +23086,15 @@ import path3 from "node:path";
23007
23086
  init_src();
23008
23087
  function reduceCodexPayload(rawJson, minLength) {
23009
23088
  const extracted = extractToolOutput(rawJson);
23010
- if (extracted === null) return { response: "{}", event: null };
23089
+ if (extracted === null) return { response: "{}", event: null, attempt: null };
23011
23090
  const result = reduceAuto(extracted.output, minLength);
23012
- if (!result.changed) return { response: "{}", event: null };
23091
+ if (!result.changed) {
23092
+ return {
23093
+ response: "{}",
23094
+ event: null,
23095
+ attempt: extracted.output.length >= (minLength ?? DEFAULT_MIN_LENGTH) ? { tool: extracted.toolName, beforeChars: extracted.output.length } : null
23096
+ };
23097
+ }
23013
23098
  const response = JSON.stringify({
23014
23099
  decision: "block",
23015
23100
  reason: `HarnessTrim reduced ${extracted.toolName} output (${result.reducer}):
@@ -23023,7 +23108,8 @@ ${result.output}`
23023
23108
  reducer: result.reducer,
23024
23109
  beforeChars: extracted.output.length,
23025
23110
  afterChars: result.output.length
23026
- }
23111
+ },
23112
+ attempt: null
23027
23113
  };
23028
23114
  }
23029
23115
  function extractToolOutput(rawJson) {
@@ -23133,6 +23219,7 @@ function planCodexHookInstall(input) {
23133
23219
  };
23134
23220
  }
23135
23221
  function planCodexInstall(input) {
23222
+ const includeInstructions = input.includeInstructions ?? true;
23136
23223
  const skillsDest = path3.join(input.projectDir, ".codex", "skills");
23137
23224
  const existing = new Set(input.existingSkillNames);
23138
23225
  const skills = input.skillNames.map((name) => ({
@@ -23142,7 +23229,9 @@ function planCodexInstall(input) {
23142
23229
  present: existing.has(name)
23143
23230
  }));
23144
23231
  let instructionsAction;
23145
- if (input.agentsMdContent === null) {
23232
+ if (!includeInstructions) {
23233
+ instructionsAction = "skip";
23234
+ } else if (input.agentsMdContent === null) {
23146
23235
  instructionsAction = "create";
23147
23236
  } else if (input.agentsMdContent.includes(HARNESSTRIM_MARKER)) {
23148
23237
  instructionsAction = "present";
@@ -23154,7 +23243,8 @@ function planCodexInstall(input) {
23154
23243
  skills,
23155
23244
  instructionsFile: path3.join(input.projectDir, "AGENTS.md"),
23156
23245
  instructionsAction,
23157
- instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET
23246
+ instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET,
23247
+ changed: skills.some((s) => !s.present) || instructionsAction !== "present" && instructionsAction !== "skip"
23158
23248
  };
23159
23249
  }
23160
23250
 
@@ -23219,7 +23309,7 @@ function applyHookPlan(plan, apply) {
23219
23309
  fs5.writeFileSync(plan.hooksFile, JSON.stringify(plan.nextHooks, null, 2) + "\n");
23220
23310
  return true;
23221
23311
  }
23222
- function runInstallCodex(dir, apply, hook = false) {
23312
+ function runInstallCodex(dir, apply, hook = false, options = {}) {
23223
23313
  const skillsSourceDir = resolveSkillsSourceDir();
23224
23314
  const skillNames = listShippedSkills(skillsSourceDir);
23225
23315
  const skillsDest = path6.join(dir, ".codex", "skills");
@@ -23235,14 +23325,16 @@ function runInstallCodex(dir, apply, hook = false) {
23235
23325
  skillsSourceDir,
23236
23326
  skillNames,
23237
23327
  agentsMdContent,
23238
- existingSkillNames: existingSkillNames(skillsDest)
23328
+ existingSkillNames: existingSkillNames(skillsDest),
23329
+ includeInstructions: options.includeInstructions
23239
23330
  });
23240
23331
  const hooksPath = path6.join(dir, ".codex", "hooks.json");
23241
23332
  const hooksJsonContent = hook ? readHooksJson(hooksPath) : null;
23242
23333
  const hookPlan = hook ? planCodexHookInstall({ projectDir: dir, hooksJsonContent, hookCommand: resolveCodexHookCommand() }) : null;
23243
23334
  const copied = [];
23335
+ const hookChanged = hookPlan !== null && hookPlan.action !== "present";
23244
23336
  let applied = false;
23245
- if (apply) {
23337
+ if (apply && (plan.changed || hookChanged)) {
23246
23338
  for (const skill of plan.skills) {
23247
23339
  if (skill.present) continue;
23248
23340
  fs5.cpSync(skill.from, skill.to, { recursive: true });
@@ -23253,7 +23345,7 @@ function runInstallCodex(dir, apply, hook = false) {
23253
23345
  } else if (plan.instructionsAction === "append") {
23254
23346
  fs5.appendFileSync(plan.instructionsFile, "\n\n" + plan.instructionsSnippet + "\n");
23255
23347
  }
23256
- if (hookPlan) applyHookPlan(hookPlan, true);
23348
+ if (hookPlan && hookChanged) applyHookPlan(hookPlan, true);
23257
23349
  applied = true;
23258
23350
  }
23259
23351
  return { plan, hookPlan, applied, copied };
@@ -23278,9 +23370,15 @@ import path8 from "node:path";
23278
23370
  init_src();
23279
23371
  function reduceClaudePayload(rawJson, minLength) {
23280
23372
  const extracted = extractToolOutput2(rawJson);
23281
- if (extracted === null) return { response: "{}", event: null };
23373
+ if (extracted === null) return { response: "{}", event: null, attempt: null };
23282
23374
  const result = reduceAuto(extracted.output, minLength);
23283
- if (!result.changed) return { response: "{}", event: null };
23375
+ if (!result.changed) {
23376
+ return {
23377
+ response: "{}",
23378
+ event: null,
23379
+ attempt: extracted.output.length >= (minLength ?? DEFAULT_MIN_LENGTH) ? { tool: extracted.toolName, beforeChars: extracted.output.length } : null
23380
+ };
23381
+ }
23284
23382
  const response = JSON.stringify({
23285
23383
  hookSpecificOutput: {
23286
23384
  hookEventName: "PostToolUse",
@@ -23294,7 +23392,8 @@ function reduceClaudePayload(rawJson, minLength) {
23294
23392
  reducer: result.reducer,
23295
23393
  beforeChars: extracted.output.length,
23296
23394
  afterChars: result.output.length
23297
- }
23395
+ },
23396
+ attempt: null
23298
23397
  };
23299
23398
  }
23300
23399
  function extractToolOutput2(rawJson) {
@@ -23349,6 +23448,8 @@ function hasHarnessTrimHook2(settings) {
23349
23448
  );
23350
23449
  }
23351
23450
  function planClaudeInstall(input) {
23451
+ const includeHook = input.includeHook ?? true;
23452
+ const includeInstructions = input.includeInstructions ?? true;
23352
23453
  const skillsDest = path7.join(input.projectDir, ".claude", "skills");
23353
23454
  const existing = new Set(input.existingSkillNames);
23354
23455
  const skills = input.skillNames.map((name) => ({
@@ -23370,8 +23471,12 @@ function planClaudeInstall(input) {
23370
23471
  }
23371
23472
  action = hasHarnessTrimHook2(settings) ? "present" : "patch";
23372
23473
  }
23373
- const nextSettings = action === "present" ? settings : addHook(settings);
23374
- const instructionsAction = input.claudeMdContent === null ? "create" : input.claudeMdContent.includes(HARNESSTRIM_MARKER2) ? "present" : "append";
23474
+ if (!includeHook) {
23475
+ action = "skip";
23476
+ settings = input.settingsJsonContent === null ? {} : settings;
23477
+ }
23478
+ const nextSettings = action === "present" || action === "skip" ? settings : addHook(settings);
23479
+ const instructionsAction = !includeInstructions ? "skip" : input.claudeMdContent === null ? "create" : input.claudeMdContent.includes(HARNESSTRIM_MARKER2) ? "present" : "append";
23375
23480
  return {
23376
23481
  skillsDest,
23377
23482
  skills,
@@ -23380,7 +23485,8 @@ function planClaudeInstall(input) {
23380
23485
  nextSettings,
23381
23486
  instructionsFile: path7.join(input.projectDir, "CLAUDE.md"),
23382
23487
  instructionsAction,
23383
- instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET2
23488
+ instructionsSnippet: REDUCE_INSTRUCTION_SNIPPET2,
23489
+ changed: skills.some((s) => !s.present) || action !== "present" && action !== "skip" || instructionsAction !== "present" && instructionsAction !== "skip"
23384
23490
  };
23385
23491
  }
23386
23492
  function addHook(settings) {
@@ -23394,7 +23500,7 @@ function addHook(settings) {
23394
23500
  }
23395
23501
 
23396
23502
  // src/install-claude.ts
23397
- function runInstallClaude(dir, apply) {
23503
+ function runInstallClaude(dir, apply, options = {}) {
23398
23504
  const skillsSourceDir = resolveSkillsSourceDir();
23399
23505
  const skillNames = listShippedSkills(skillsSourceDir);
23400
23506
  const skillsDest = path8.join(dir, ".claude", "skills");
@@ -23418,17 +23524,19 @@ function runInstallClaude(dir, apply) {
23418
23524
  skillNames,
23419
23525
  settingsJsonContent,
23420
23526
  claudeMdContent,
23421
- existingSkillNames: existingSkillNames(skillsDest)
23527
+ existingSkillNames: existingSkillNames(skillsDest),
23528
+ includeHook: options.includeHook,
23529
+ includeInstructions: options.includeInstructions
23422
23530
  });
23423
23531
  const copied = [];
23424
23532
  let applied = false;
23425
- if (apply) {
23533
+ if (apply && plan.changed) {
23426
23534
  for (const skill of plan.skills) {
23427
23535
  if (skill.present) continue;
23428
23536
  fs6.cpSync(skill.from, skill.to, { recursive: true });
23429
23537
  copied.push(skill.name);
23430
23538
  }
23431
- if (plan.settingsAction !== "present") {
23539
+ if (plan.settingsAction !== "present" && plan.settingsAction !== "skip") {
23432
23540
  fs6.mkdirSync(path8.dirname(plan.settingsFile), { recursive: true });
23433
23541
  fs6.writeFileSync(plan.settingsFile, JSON.stringify(plan.nextSettings, null, 2) + "\n");
23434
23542
  }
@@ -23499,12 +23607,16 @@ function runInstallPi(installDir, apply) {
23499
23607
  let applied = false;
23500
23608
  if (apply) {
23501
23609
  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);
23610
+ const sourceFiles = fs7.readdirSync(extensionSourceDir, { withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => entry.name);
23611
+ for (const existing of fs7.readdirSync(dest)) {
23612
+ if (existing !== ".installed" && !sourceFiles.includes(existing)) {
23613
+ fs7.rmSync(path10.join(dest, existing), { recursive: true, force: true });
23506
23614
  }
23507
23615
  }
23616
+ for (const entry of sourceFiles) {
23617
+ fs7.copyFileSync(path10.join(extensionSourceDir, entry), path10.join(dest, entry));
23618
+ copiedFiles.push(entry);
23619
+ }
23508
23620
  fs7.writeFileSync(path10.join(dest, ".installed"), markerFileContent());
23509
23621
  copiedFiles.push(".installed");
23510
23622
  applied = true;
@@ -23621,7 +23733,7 @@ function loadMetrics(filePath) {
23621
23733
  // package.json
23622
23734
  var package_default = {
23623
23735
  name: "harnesstrim",
23624
- version: "0.0.6",
23736
+ version: "0.1.0",
23625
23737
  description: "HarnessTrim CLI: doctor (diagnose token waste), install adapters, run benchmarks.",
23626
23738
  license: "MIT",
23627
23739
  type: "module",
@@ -23669,6 +23781,324 @@ function reducePipe(input, minLength) {
23669
23781
  return { ...result, beforeChars: input.length, afterChars: result.output.length };
23670
23782
  }
23671
23783
 
23784
+ // src/capabilities.ts
23785
+ var CAPABILITIES = {
23786
+ harnesses: {
23787
+ opencode: {
23788
+ adapter: "@harnesstrim/adapter-opencode",
23789
+ surfaces: [
23790
+ "tool.execute.after \u2014 slims noisy tool output in place before it enters context",
23791
+ "experimental.session.compacting \u2014 injects compaction-handoff guidance"
23792
+ ],
23793
+ narrowing: [
23794
+ { flag: "--mode active|dryrun|off", produces: "bake the reduction mode into the generated wrapper (active/dryrun/off)" },
23795
+ { flag: "--min-length <n>", produces: "leave tool outputs shorter than n chars untouched (overrides preset)" },
23796
+ { flag: "--tools <name,...>", produces: "confine reduction to a subset of tool families (e.g. bash,read)" },
23797
+ { flag: "--preset <name>", produces: "bake a policy preset's adapter config into the wrapper" }
23798
+ ],
23799
+ writeSet: [
23800
+ ".opencode/plugin/harnesstrim.ts",
23801
+ ".opencode/package.json",
23802
+ "opencode.json (cleans a stale adapter entry, never adds one)"
23803
+ ]
23804
+ },
23805
+ codex: {
23806
+ adapter: "@harnesstrim/adapter-codex",
23807
+ surfaces: [
23808
+ "PostToolUse Bash hook \u2014 deterministic reduction of simple Bash output (optional --hook)",
23809
+ "AGENTS.md reduce-pipe instruction \u2014 model pipes noisy output through `harnesstrim reduce`",
23810
+ "MCP reduce tool \u2014 deterministic, instruction-free reduction (separate `harnesstrim mcp`)"
23811
+ ],
23812
+ narrowing: [
23813
+ { flag: "--no-instructions", produces: "skills only \u2014 no AGENTS.md reduce-pipe instruction" },
23814
+ { flag: "--hook", produces: "also install the experimental Bash PostToolUse hook" },
23815
+ { flag: "--global", produces: "install the hook once in ~/.codex (with --hook)" }
23816
+ ],
23817
+ writeSet: [".codex/skills/", "AGENTS.md (marker-guarded snippet)", ".codex/hooks.json (with --hook)"]
23818
+ },
23819
+ claude: {
23820
+ adapter: "@harnesstrim/adapter-claude",
23821
+ surfaces: [
23822
+ "PostToolUse Bash hook \u2014 spec-correct updatedToolOutput (not honored by Claude Code 2.1.37\u20132.1.212)",
23823
+ "CLAUDE.md reduce-pipe instruction \u2014 the effective reduction path on current Claude Code"
23824
+ ],
23825
+ narrowing: [
23826
+ { flag: "--no-hook", produces: "skills only \u2014 no PostToolUse hook in .claude/settings.json" },
23827
+ { flag: "--no-instructions", produces: "skills only \u2014 no CLAUDE.md reduce-pipe instruction" }
23828
+ ],
23829
+ writeSet: [".claude/skills/", ".claude/settings.json", "CLAUDE.md (marker-guarded snippet)"]
23830
+ },
23831
+ hermes: {
23832
+ adapter: "@harnesstrim/adapter-hermes",
23833
+ surfaces: ["transform_tool_result \u2014 deterministic reduction before the result enters context"],
23834
+ narrowing: [],
23835
+ writeSet: [".hermes/plugins/harnesstrim/ (incl. .installed marker)"]
23836
+ },
23837
+ pi: {
23838
+ adapter: "@harnesstrim/adapter-pi",
23839
+ surfaces: ["tool_result \u2014 deterministic reduction of text chunks in structured results"],
23840
+ narrowing: [],
23841
+ writeSet: [".pi/extensions/harnesstrim/ or .pi/agent/extensions/harnesstrim/ (incl. .installed marker)"]
23842
+ }
23843
+ }
23844
+ };
23845
+ function getCapabilities(version2) {
23846
+ return { version: version2, ...CAPABILITIES };
23847
+ }
23848
+
23849
+ // src/uninstall.ts
23850
+ import fs10 from "node:fs";
23851
+ import path14 from "node:path";
23852
+ function readOrNull(p) {
23853
+ try {
23854
+ return fs10.readFileSync(p, "utf8");
23855
+ } catch {
23856
+ return null;
23857
+ }
23858
+ }
23859
+ function stripMarkedRegion(content, marker) {
23860
+ const begin = `<!-- ${marker} -->`;
23861
+ const end = "<!-- harnesstrim:end -->";
23862
+ const start = content.indexOf(begin);
23863
+ if (start === -1) return content;
23864
+ const endIdx = content.indexOf(end, start);
23865
+ if (endIdx === -1) return content;
23866
+ const after = content.slice(endIdx + end.length);
23867
+ const before = content.slice(0, start).replace(/\s+$/, "");
23868
+ const next = (before + "\n" + after.replace(/^\s+/, "")).replace(/\n{3,}/g, "\n\n").trim();
23869
+ return next;
23870
+ }
23871
+ function stripHookEntries(settings) {
23872
+ const hooks = settings.hooks;
23873
+ if (!hooks || !Array.isArray(hooks.PostToolUse)) return { next: settings, removed: false };
23874
+ const post = hooks.PostToolUse;
23875
+ const kept = post.filter((entry) => {
23876
+ const hooksArr = entry.hooks;
23877
+ if (!Array.isArray(hooksArr)) return true;
23878
+ return !hooksArr.some((h) => typeof h.command === "string" && h.command.includes("harnesstrim hook"));
23879
+ });
23880
+ if (kept.length === post.length) return { next: settings, removed: false };
23881
+ const nextHooks = { ...hooks };
23882
+ nextHooks.PostToolUse = kept;
23883
+ const next = { ...settings, hooks: nextHooks };
23884
+ return { next, removed: true };
23885
+ }
23886
+ function isSkillDir(pathName) {
23887
+ try {
23888
+ return fs10.statSync(pathName).isDirectory();
23889
+ } catch {
23890
+ return false;
23891
+ }
23892
+ }
23893
+ function markerPresent3(dest) {
23894
+ try {
23895
+ return fs10.statSync(path14.join(dest, ".installed")).isFile();
23896
+ } catch {
23897
+ return false;
23898
+ }
23899
+ }
23900
+ function skillRemovalActions(dir, destRel) {
23901
+ const sourceDir = resolveSkillsSourceDir();
23902
+ const shipped = listShippedSkills(sourceDir);
23903
+ const dest = path14.join(dir, destRel);
23904
+ const actions = [];
23905
+ for (const name of shipped) {
23906
+ const skillPath = path14.join(dest, name);
23907
+ if (isSkillDir(skillPath)) {
23908
+ actions.push({ type: "remove-dir", path: skillPath, note: `skill directory installed by HarnessTrim` });
23909
+ }
23910
+ }
23911
+ if (actions.length > 0 && isSkillDir(dest)) {
23912
+ const remaining = fs10.readdirSync(dest).filter((name) => !shipped.includes(name));
23913
+ if (remaining.length === 0) {
23914
+ actions.push({ type: "remove-dir", path: dest, note: "empty skills directory left by HarnessTrim" });
23915
+ }
23916
+ }
23917
+ return actions;
23918
+ }
23919
+ function planClaudeUninstall(dir) {
23920
+ const actions = [...skillRemovalActions(dir, ".claude/skills")];
23921
+ const settingsPath = path14.join(dir, ".claude", "settings.json");
23922
+ const settingsContent = readOrNull(settingsPath);
23923
+ if (settingsContent !== null) {
23924
+ try {
23925
+ const parsed = JSON.parse(settingsContent);
23926
+ const { next, removed } = stripHookEntries(parsed);
23927
+ if (removed) {
23928
+ actions.push({
23929
+ type: Object.keys(next).length === 0 ? "remove-file" : "write",
23930
+ path: settingsPath,
23931
+ note: "remove the HarnessTrim PostToolUse hook"
23932
+ });
23933
+ }
23934
+ } catch {
23935
+ }
23936
+ }
23937
+ const claudeMdPath = path14.join(dir, "CLAUDE.md");
23938
+ const claudeMd = readOrNull(claudeMdPath);
23939
+ if (claudeMd !== null && claudeMd.includes(HARNESSTRIM_MARKER2)) {
23940
+ const next = stripMarkedRegion(claudeMd, HARNESSTRIM_MARKER2);
23941
+ if (next !== null && next !== claudeMd) {
23942
+ actions.push({
23943
+ type: next.length === 0 ? "remove-file" : "write",
23944
+ path: claudeMdPath,
23945
+ note: "remove the marker-guarded HarnessTrim instruction"
23946
+ });
23947
+ }
23948
+ }
23949
+ return { harness: "claude", dir, changed: actions.length > 0, actions };
23950
+ }
23951
+ function planCodexUninstall(dir) {
23952
+ const actions = [...skillRemovalActions(dir, ".codex/skills")];
23953
+ const agentsPath = path14.join(dir, "AGENTS.md");
23954
+ const agents = readOrNull(agentsPath);
23955
+ if (agents !== null && agents.includes(HARNESSTRIM_MARKER)) {
23956
+ const next = stripMarkedRegion(agents, HARNESSTRIM_MARKER);
23957
+ if (next !== null && next !== agents) {
23958
+ actions.push({
23959
+ type: next.length === 0 ? "remove-file" : "write",
23960
+ path: agentsPath,
23961
+ note: "remove the marker-guarded HarnessTrim instruction"
23962
+ });
23963
+ }
23964
+ }
23965
+ const hooksPath = path14.join(dir, ".codex", "hooks.json");
23966
+ const hooksContent = readOrNull(hooksPath);
23967
+ if (hooksContent !== null) {
23968
+ try {
23969
+ const parsed = JSON.parse(hooksContent);
23970
+ const { next, removed } = stripHookEntries(parsed);
23971
+ if (removed) {
23972
+ actions.push({
23973
+ type: Object.keys(next).length === 0 ? "remove-file" : "write",
23974
+ path: hooksPath,
23975
+ note: "remove the HarnessTrim PostToolUse hook"
23976
+ });
23977
+ }
23978
+ } catch {
23979
+ }
23980
+ }
23981
+ return { harness: "codex", dir, changed: actions.length > 0, actions };
23982
+ }
23983
+ function planOpencodeUninstall(dir) {
23984
+ const actions = [];
23985
+ const wrapperPath = path14.join(dir, ".opencode", "plugin", "harnesstrim.ts");
23986
+ const wrapper = readOrNull(wrapperPath);
23987
+ if (wrapper !== null && wrapper.includes(OPENCODE_PLUGIN_NAME)) {
23988
+ actions.push({ type: "remove-file", path: wrapperPath, note: "HarnessTrim plugin wrapper" });
23989
+ const pluginDir = path14.join(dir, ".opencode", "plugin");
23990
+ if (fs10.existsSync(pluginDir)) {
23991
+ const entries = fs10.readdirSync(pluginDir).filter((name) => name !== "harnesstrim.ts");
23992
+ if (entries.length === 0) {
23993
+ actions.push({ type: "remove-dir", path: pluginDir, note: "empty plugin directory left by HarnessTrim" });
23994
+ }
23995
+ }
23996
+ }
23997
+ const pkgPath = path14.join(dir, ".opencode", "package.json");
23998
+ const pkgContent = readOrNull(pkgPath);
23999
+ if (pkgContent !== null) {
24000
+ try {
24001
+ const parsed = JSON.parse(pkgContent);
24002
+ const deps = parsed.dependencies;
24003
+ if (deps && typeof deps[OPENCODE_PLUGIN_NAME] === "string") {
24004
+ const nextDeps = { ...deps };
24005
+ delete nextDeps[OPENCODE_PLUGIN_NAME];
24006
+ const next = { ...parsed, dependencies: nextDeps };
24007
+ const onlyOurDep = Object.keys(nextDeps).length === 0 && Object.keys(parsed).length <= 1;
24008
+ actions.push({
24009
+ type: onlyOurDep ? "remove-file" : "write",
24010
+ path: pkgPath,
24011
+ note: onlyOurDep ? "remove .opencode/package.json (only declared the adapter)" : "drop the @harnesstrim/adapter-opencode dependency"
24012
+ });
24013
+ }
24014
+ } catch {
24015
+ }
24016
+ }
24017
+ return { harness: "opencode", dir, changed: actions.length > 0, actions };
24018
+ }
24019
+ function planHermesUninstall(dir) {
24020
+ const pluginDest = path14.join(dir, ".hermes", "plugins", "harnesstrim");
24021
+ const actions = [];
24022
+ if (markerPresent3(pluginDest)) {
24023
+ actions.push({ type: "remove-dir", path: pluginDest, note: "Hermes plugin installed by HarnessTrim" });
24024
+ }
24025
+ return { harness: "hermes", dir, changed: actions.length > 0, actions };
24026
+ }
24027
+ function planPiUninstall(dir) {
24028
+ const scope = path14.resolve(dir) === path14.resolve(process.env.HOME ?? "") ? "user" : "project";
24029
+ const dest = scope === "user" ? path14.join(dir, ".pi", "agent", "extensions", "harnesstrim") : path14.join(dir, ".pi", "extensions", "harnesstrim");
24030
+ const actions = [];
24031
+ if (markerPresent3(dest)) {
24032
+ actions.push({ type: "remove-dir", path: dest, note: "Pi extension installed by HarnessTrim" });
24033
+ }
24034
+ return { harness: "pi", dir, changed: actions.length > 0, actions };
24035
+ }
24036
+ function planUninstall(harness, dir) {
24037
+ switch (harness) {
24038
+ case "claude":
24039
+ return planClaudeUninstall(dir);
24040
+ case "codex":
24041
+ return planCodexUninstall(dir);
24042
+ case "opencode":
24043
+ return planOpencodeUninstall(dir);
24044
+ case "hermes":
24045
+ return planHermesUninstall(dir);
24046
+ case "pi":
24047
+ return planPiUninstall(dir);
24048
+ default:
24049
+ throw new Error(`Unknown uninstall target: ${harness}. Supported: opencode, codex, claude, hermes, pi.`);
24050
+ }
24051
+ }
24052
+ function runUninstall(harness, dir, apply) {
24053
+ const plan = planUninstall(harness, dir);
24054
+ if (!apply || !plan.changed) return { ...plan, applied: false };
24055
+ for (const action of plan.actions) {
24056
+ switch (action.type) {
24057
+ case "remove-file":
24058
+ fs10.rmSync(action.path, { force: true });
24059
+ break;
24060
+ case "remove-dir":
24061
+ fs10.rmSync(action.path, { recursive: true, force: true });
24062
+ break;
24063
+ case "write": {
24064
+ if (action.path.endsWith("settings.json") || action.path.endsWith("hooks.json")) {
24065
+ const raw = readOrNull(action.path);
24066
+ if (raw !== null) {
24067
+ try {
24068
+ const parsed = JSON.parse(raw);
24069
+ const { next } = stripHookEntries(parsed);
24070
+ fs10.writeFileSync(action.path, JSON.stringify(next, null, 2) + "\n");
24071
+ } catch {
24072
+ }
24073
+ }
24074
+ } else if (action.path.endsWith(path14.join(".opencode", "package.json"))) {
24075
+ const raw = readOrNull(action.path);
24076
+ if (raw !== null) {
24077
+ try {
24078
+ const parsed = JSON.parse(raw);
24079
+ const deps = { ...parsed.dependencies ?? {} };
24080
+ delete deps[OPENCODE_PLUGIN_NAME];
24081
+ fs10.writeFileSync(action.path, JSON.stringify({ ...parsed, dependencies: deps }, null, 2) + "\n");
24082
+ } catch {
24083
+ }
24084
+ }
24085
+ } else {
24086
+ const marker = action.path.endsWith("CLAUDE.md") ? HARNESSTRIM_MARKER2 : HARNESSTRIM_MARKER;
24087
+ const raw = readOrNull(action.path);
24088
+ if (raw !== null) {
24089
+ const next = stripMarkedRegion(raw, marker);
24090
+ if (next !== null) fs10.writeFileSync(action.path, next + "\n");
24091
+ }
24092
+ }
24093
+ break;
24094
+ }
24095
+ default:
24096
+ break;
24097
+ }
24098
+ }
24099
+ return { ...plan, applied: true };
24100
+ }
24101
+
23672
24102
  // src/render.ts
23673
24103
  var ICON = { warn: "!", info: "i", ok: "+" };
23674
24104
  function renderDoctor(report) {
@@ -23767,7 +24197,7 @@ function renderCodexInstall(result, apply) {
23767
24197
  lines.push(` ${s.name.padEnd(18)} ${state}`);
23768
24198
  }
23769
24199
  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.`;
24200
+ 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
24201
  lines.push(instr);
23772
24202
  if (result.hookPlan) {
23773
24203
  lines.push("");
@@ -23816,7 +24246,9 @@ function renderClaudeInstall(result, apply) {
23816
24246
  lines.push(` ${s.name.padEnd(18)} ${state}`);
23817
24247
  }
23818
24248
  lines.push("");
23819
- if (plan.settingsAction === "present") {
24249
+ if (plan.settingsAction === "skip") {
24250
+ lines.push(`${plan.settingsFile}: PostToolUse hook skipped (--no-hook); skills only.`);
24251
+ } else if (plan.settingsAction === "present") {
23820
24252
  lines.push(`${plan.settingsFile}: PostToolUse reducer hook already present (no change).`);
23821
24253
  } else {
23822
24254
  lines.push(
@@ -23829,7 +24261,9 @@ function renderClaudeInstall(result, apply) {
23829
24261
  }
23830
24262
  }
23831
24263
  lines.push("");
23832
- if (plan.instructionsAction === "present") {
24264
+ if (plan.instructionsAction === "skip") {
24265
+ lines.push(`${plan.instructionsFile}: reduce-pipe instruction skipped (--no-instructions); skills only.`);
24266
+ } else if (plan.instructionsAction === "present") {
23833
24267
  lines.push(`${plan.instructionsFile}: reduce-pipe instruction already present (no change).`);
23834
24268
  } else {
23835
24269
  lines.push(
@@ -23884,7 +24318,7 @@ function renderPiInstall(result, apply) {
23884
24318
  lines.push(` Copied: ${result.copiedFiles.join(", ")}`);
23885
24319
  } else {
23886
24320
  lines.push(` Source: ${plan.extensionSource}`);
23887
- lines.push(" (harnesstrim.ts + .installed marker)");
24321
+ lines.push(" (index.ts + .installed marker)");
23888
24322
  }
23889
24323
  lines.push("");
23890
24324
  lines.push("The extension hooks Pi's `tool_result` and needs `harnesstrim` on PATH.");
@@ -23908,18 +24342,217 @@ Enable it in the adapter (telemetry: true) to record reductions.`;
23908
24342
  const lines = [
23909
24343
  `harnesstrim metrics \u2014 ${result.path}`,
23910
24344
  "",
23911
- `Reductions: ${s.events}`,
23912
- `Chars: ${s.beforeChars} -> ${s.afterChars} (saved ${s.savedChars}, -${s.reductionPct}%)`,
23913
- "",
23914
- "By reducer:"
24345
+ `Attempts: ${s.events} (${s.reduced} reduced, ${s.passThrough} pass-through, ${s.reductionErrors} error)`,
24346
+ `Pass-through: ${s.passThroughRate}% of attempts unchanged`,
24347
+ `Chars: ${s.beforeChars} -> ${s.afterChars} (saved ${s.savedChars}, -${s.reductionPct}%)`
23915
24348
  ];
24349
+ if (s.reductionErrors > 0) {
24350
+ lines.push(`Reduction errors: ${s.reductionErrors} attempt(s) GREW the output (+${s.grewChars} chars) \u2014 investigate`);
24351
+ }
24352
+ lines.push("");
24353
+ lines.push("By reducer:");
23916
24354
  for (const b of s.byReducer) {
23917
24355
  const p = b.beforeChars === 0 ? 0 : Math.round(b.savedChars / b.beforeChars * 1e3) / 10;
23918
24356
  lines.push(` ${b.reducer.padEnd(20)} ${b.count}x saved ${b.savedChars} chars (-${p}%)`);
23919
24357
  }
24358
+ if (s.byHarness.length > 0) {
24359
+ lines.push("");
24360
+ lines.push("By harness:");
24361
+ for (const h of s.byHarness) {
24362
+ const p = h.beforeChars === 0 ? 0 : Math.round(h.savedChars / h.beforeChars * 1e3) / 10;
24363
+ const sign = p < 0 ? "" : "-";
24364
+ lines.push(` ${h.harness.padEnd(12)} ${h.count}x saved ${h.savedChars} chars (${sign}${Math.abs(p)}%)`);
24365
+ }
24366
+ }
24367
+ return lines.join("\n");
24368
+ }
24369
+ function renderUninstall(result, apply) {
24370
+ const lines = [`${apply ? "Uninstalled" : "Would uninstall"} ${result.harness} integration`, ""];
24371
+ if (!result.changed) {
24372
+ lines.push("Nothing to remove \u2014 no HarnessTrim files found in the install set.");
24373
+ return lines.join("\n");
24374
+ }
24375
+ for (const action of result.actions) {
24376
+ const verb = action.type === "remove-file" || action.type === "remove-dir" ? apply ? "removed" : "remove" : action.type === "write" ? apply ? "updated" : "update" : "clean";
24377
+ lines.push(` ${verb.padEnd(8)} ${action.path}`);
24378
+ if (action.note) lines.push(` (${action.note})`);
24379
+ }
24380
+ if (!apply) {
24381
+ lines.push("");
24382
+ lines.push("Dry run \u2014 nothing written. Re-run with `--apply`.");
24383
+ }
24384
+ lines.push("");
24385
+ lines.push("Only files HarnessTrim wrote are touched; marker-guarded regions and your");
24386
+ lines.push("other settings/hook entries are preserved.");
23920
24387
  return lines.join("\n");
23921
24388
  }
23922
24389
 
24390
+ // src/json.ts
24391
+ function doctorJson(report) {
24392
+ return JSON.stringify(report, null, 2);
24393
+ }
24394
+ function metricsJson(result) {
24395
+ return JSON.stringify(result, null, 2);
24396
+ }
24397
+ function opencodeInstallJson(result, apply) {
24398
+ const actions = [];
24399
+ if (result.changed) {
24400
+ actions.push(
24401
+ {
24402
+ type: "write",
24403
+ path: result.wrapperPath,
24404
+ note: "local plugin wrapper with the adapter options"
24405
+ },
24406
+ {
24407
+ type: "write",
24408
+ path: result.packageJsonPath,
24409
+ note: "declares @harnesstrim/adapter-opencode"
24410
+ }
24411
+ );
24412
+ if (result.opencodeJsonContent !== null) {
24413
+ actions.push({
24414
+ type: "clean",
24415
+ path: result.opencodeJsonPath,
24416
+ note: "remove the stale adapter entry (options live in the wrapper now)"
24417
+ });
24418
+ }
24419
+ }
24420
+ return {
24421
+ harness: "opencode",
24422
+ dryRun: !apply,
24423
+ changed: result.changed,
24424
+ alreadyInstalled: result.alreadyInstalled,
24425
+ applied: result.applied,
24426
+ actions,
24427
+ details: {
24428
+ preset: result.preset?.name ?? null,
24429
+ depsInstalled: result.depsInstalled,
24430
+ depsMessage: result.depsMessage ?? null
24431
+ }
24432
+ };
24433
+ }
24434
+ function codexInstallJson(result, apply) {
24435
+ const actions = result.plan.skills.map((s) => ({
24436
+ type: s.present ? "none" : "copy",
24437
+ path: s.to,
24438
+ from: s.from,
24439
+ note: s.present ? "already present" : "copy skill"
24440
+ }));
24441
+ if (result.plan.instructionsAction !== "present" && result.plan.instructionsAction !== "skip") {
24442
+ actions.push({
24443
+ type: result.plan.instructionsAction === "create" ? "write" : "append",
24444
+ path: result.plan.instructionsFile,
24445
+ note: `reduce-pipe instruction (${result.plan.instructionsAction})`
24446
+ });
24447
+ }
24448
+ if (result.hookPlan && result.hookPlan.action !== "present") {
24449
+ actions.push({
24450
+ type: "write",
24451
+ path: result.hookPlan.hooksFile,
24452
+ note: `Bash PostToolUse hook (${result.hookPlan.action})`
24453
+ });
24454
+ }
24455
+ const hookChanged = result.hookPlan !== null && result.hookPlan.action !== "present";
24456
+ return {
24457
+ harness: "codex",
24458
+ dryRun: !apply,
24459
+ changed: result.plan.changed || hookChanged,
24460
+ alreadyInstalled: !result.plan.changed && !hookChanged,
24461
+ applied: result.applied,
24462
+ actions,
24463
+ details: { hook: result.hookPlan ? result.hookPlan.action : "skipped" }
24464
+ };
24465
+ }
24466
+ function claudeInstallJson(result, apply) {
24467
+ const actions = result.plan.skills.map((s) => ({
24468
+ type: s.present ? "none" : "copy",
24469
+ path: s.to,
24470
+ from: s.from,
24471
+ note: s.present ? "already present" : "copy skill"
24472
+ }));
24473
+ if (result.plan.settingsAction !== "present" && result.plan.settingsAction !== "skip") {
24474
+ actions.push({
24475
+ type: result.plan.settingsAction === "create" ? "write" : "write",
24476
+ path: result.plan.settingsFile,
24477
+ note: `PostToolUse Bash hook (${result.plan.settingsAction})`
24478
+ });
24479
+ }
24480
+ if (result.plan.instructionsAction !== "present" && result.plan.instructionsAction !== "skip") {
24481
+ actions.push({
24482
+ type: result.plan.instructionsAction === "create" ? "write" : "append",
24483
+ path: result.plan.instructionsFile,
24484
+ note: `reduce-pipe instruction (${result.plan.instructionsAction})`
24485
+ });
24486
+ }
24487
+ return {
24488
+ harness: "claude",
24489
+ dryRun: !apply,
24490
+ changed: result.plan.changed,
24491
+ alreadyInstalled: !result.plan.changed,
24492
+ applied: result.applied,
24493
+ actions,
24494
+ details: {
24495
+ settingsAction: result.plan.settingsAction,
24496
+ instructionsAction: result.plan.instructionsAction
24497
+ }
24498
+ };
24499
+ }
24500
+ function hermesInstallJson(result, apply) {
24501
+ const actions = [
24502
+ {
24503
+ type: result.plan.alreadyInstalled ? "none" : "copy",
24504
+ path: result.plan.pluginDest,
24505
+ from: result.plan.pluginSource,
24506
+ note: result.plan.alreadyInstalled ? "already installed" : "copy Hermes plugin bundle"
24507
+ }
24508
+ ];
24509
+ return {
24510
+ harness: "hermes",
24511
+ dryRun: !apply,
24512
+ changed: !result.plan.alreadyInstalled,
24513
+ alreadyInstalled: result.plan.alreadyInstalled,
24514
+ applied: result.applied,
24515
+ actions,
24516
+ details: { enabled: result.enabled, enableMessage: result.enableMessage ?? null }
24517
+ };
24518
+ }
24519
+ function piInstallJson(result, apply) {
24520
+ const actions = [
24521
+ {
24522
+ type: result.plan.alreadyInstalled ? "none" : "copy",
24523
+ path: result.plan.extensionDest,
24524
+ from: result.plan.extensionSource,
24525
+ note: result.plan.alreadyInstalled ? "already installed" : "copy Pi extension bundle"
24526
+ }
24527
+ ];
24528
+ return {
24529
+ harness: "pi",
24530
+ dryRun: !apply,
24531
+ changed: !result.plan.alreadyInstalled,
24532
+ alreadyInstalled: result.plan.alreadyInstalled,
24533
+ applied: result.applied,
24534
+ actions
24535
+ };
24536
+ }
24537
+ function codexGlobalHookJson(result, apply) {
24538
+ const actions = [
24539
+ {
24540
+ type: result.hookPlan.action === "present" ? "none" : "write",
24541
+ path: result.hookPlan.hooksFile,
24542
+ note: `global Bash PostToolUse hook (${result.hookPlan.action})`
24543
+ }
24544
+ ];
24545
+ return {
24546
+ harness: "codex",
24547
+ dryRun: !apply,
24548
+ changed: result.hookPlan.action !== "present",
24549
+ alreadyInstalled: result.hookPlan.action === "present",
24550
+ applied: result.applied,
24551
+ actions,
24552
+ details: { scope: "global" }
24553
+ };
24554
+ }
24555
+
23923
24556
  // src/cli.ts
23924
24557
  var HELP = `harnesstrim \u2014 one token policy for coding harnesses
23925
24558
 
@@ -23928,16 +24561,25 @@ Usage:
23928
24561
  harnesstrim install opencode [dir] Wire the adapter into opencode.json (dry-run)
23929
24562
  --apply Actually write the change
23930
24563
  --preset <name> Bake a policy preset's adapter config in
24564
+ --mode <m> Override mode: active|dryrun|off
24565
+ --min-length <n> Override the reduction threshold (chars)
24566
+ --tools <list> Confine reduction to a subset of tool families
23931
24567
  harnesstrim install codex [dir] Install skills + AGENTS.md reduction guidance (dry-run)
23932
24568
  --apply Actually write the change
23933
24569
  --hook Also install the experimental Bash PostToolUse hook
24570
+ --no-instructions Skills only (no AGENTS.md reduce-pipe instruction)
23934
24571
  --global With --hook, install it once in ~/.codex (no project files)
23935
24572
  harnesstrim install claude [dir] Install skills + PostToolUse reducer hook (dry-run)
23936
24573
  --apply Actually write the change
24574
+ --no-hook Skills only (no PostToolUse hook)
24575
+ --no-instructions Skills only (no CLAUDE.md reduce-pipe instruction)
23937
24576
  harnesstrim install hermes [dir] Install Hermes plugin (dry-run)
23938
24577
  --apply Actually write the change
23939
24578
  harnesstrim install pi [dir] Install Pi tool_result extension (dry-run)
23940
24579
  --apply Actually write the change
24580
+ harnesstrim uninstall <harness> [dir] Remove only what install wrote (dry-run)
24581
+ --apply Actually write the change
24582
+ harnesstrim capabilities Print machine-readable per-harness capabilities (JSON)
23941
24583
  harnesstrim hook claude [--metrics <path>]
23942
24584
  PostToolUse hook runtime; --metrics records a TrimEvent per reduction
23943
24585
  harnesstrim hook codex [--metrics <path>]
@@ -23953,8 +24595,11 @@ Usage:
23953
24595
  harnesstrim bench Run the Tier A reducer micro-benchmark
23954
24596
  harnesstrim --version Print the installed version
23955
24597
 
24598
+ Flags:
24599
+ --json Print machine-readable JSON (doctor, metrics, install, uninstall)
24600
+
23956
24601
  Notes:
23957
- - install is dry-run by default; nothing is written without --apply.
24602
+ - install and uninstall are dry-run by default; nothing is written without --apply.
23958
24603
  - dir defaults to the current directory; metrics path defaults to ${DEFAULT_METRICS_PATH}.
23959
24604
  - reduce reads stdin and writes slimmed output to stdout, e.g. npm test 2>&1 | harnesstrim reduce`;
23960
24605
  async function main(argv) {
@@ -23971,7 +24616,12 @@ async function main(argv) {
23971
24616
  log: { type: "string" },
23972
24617
  metrics: { type: "string" },
23973
24618
  hook: { type: "boolean" },
23974
- global: { type: "boolean" }
24619
+ global: { type: "boolean" },
24620
+ "no-hook": { type: "boolean" },
24621
+ "no-instructions": { type: "boolean" },
24622
+ mode: { type: "string" },
24623
+ tools: { type: "string" },
24624
+ json: { type: "boolean" }
23975
24625
  }
23976
24626
  });
23977
24627
  const [command, ...rest] = positionals;
@@ -23986,16 +24636,34 @@ async function main(argv) {
23986
24636
  switch (command) {
23987
24637
  case "doctor": {
23988
24638
  const dir = rest[0] ?? process.cwd();
23989
- console.log(renderDoctor(inspect(dir)));
24639
+ const report = inspect(dir);
24640
+ if (values.json) {
24641
+ console.log(doctorJson(report));
24642
+ } else {
24643
+ console.log(renderDoctor(report));
24644
+ }
23990
24645
  return 0;
23991
24646
  }
23992
24647
  case "install": {
23993
24648
  const target = rest[0];
23994
24649
  const dir = rest[1] ?? (target === "hermes" ? os4.homedir() : process.cwd());
23995
24650
  const apply = values.apply === true;
24651
+ const asJson = values.json === true;
23996
24652
  if (target === "opencode") {
23997
- const result = runInstallOpencode(dir, apply, values.preset);
23998
- console.log(renderInstall(result, apply));
24653
+ const mode = parseModeFlag(values.mode);
24654
+ if (mode === void 0 && values.mode !== void 0) {
24655
+ console.error(`Invalid --mode: ${values.mode} (expected active, dryrun, or off).`);
24656
+ return 1;
24657
+ }
24658
+ const minLength = values["min-length"] !== void 0 ? Number(values["min-length"]) : void 0;
24659
+ if (minLength !== void 0 && !Number.isFinite(minLength)) {
24660
+ console.error(`Invalid --min-length: ${values["min-length"]}`);
24661
+ return 1;
24662
+ }
24663
+ const tools = values.tools !== void 0 ? splitTools(values.tools) : void 0;
24664
+ const result = runInstallOpencode(dir, apply, values.preset, true, { mode, minLength, tools });
24665
+ if (asJson) console.log(JSON.stringify(opencodeInstallJson(result, apply), null, 2));
24666
+ else console.log(renderInstall(result, apply));
23999
24667
  return 0;
24000
24668
  }
24001
24669
  if (target === "codex") {
@@ -24004,27 +24672,63 @@ async function main(argv) {
24004
24672
  console.error("`harnesstrim install codex --global` requires `--hook`.");
24005
24673
  return 1;
24006
24674
  }
24007
- console.log(renderCodexGlobalHookInstall(runInstallCodexGlobalHook(path15.join(os4.homedir(), ".codex"), apply), apply));
24675
+ const result2 = runInstallCodexGlobalHook(path16.join(os4.homedir(), ".codex"), apply);
24676
+ if (asJson) console.log(JSON.stringify(codexGlobalHookJson(result2, apply), null, 2));
24677
+ else console.log(renderCodexGlobalHookInstall(result2, apply));
24008
24678
  return 0;
24009
24679
  }
24010
- console.log(renderCodexInstall(runInstallCodex(dir, apply, values.hook === true), apply));
24680
+ const result = runInstallCodex(dir, apply, values.hook === true, {
24681
+ includeInstructions: values["no-instructions"] !== true
24682
+ });
24683
+ if (asJson) console.log(JSON.stringify(codexInstallJson(result, apply), null, 2));
24684
+ else console.log(renderCodexInstall(result, apply));
24011
24685
  return 0;
24012
24686
  }
24013
24687
  if (target === "claude") {
24014
- console.log(renderClaudeInstall(runInstallClaude(dir, apply), apply));
24688
+ const result = runInstallClaude(dir, apply, {
24689
+ includeHook: values["no-hook"] !== true,
24690
+ includeInstructions: values["no-instructions"] !== true
24691
+ });
24692
+ if (asJson) console.log(JSON.stringify(claudeInstallJson(result, apply), null, 2));
24693
+ else console.log(renderClaudeInstall(result, apply));
24015
24694
  return 0;
24016
24695
  }
24017
24696
  if (target === "hermes") {
24018
- console.log(renderHermesInstall(runInstallHermes(dir, apply), apply));
24697
+ const result = runInstallHermes(dir, apply);
24698
+ if (asJson) console.log(JSON.stringify(hermesInstallJson(result, apply), null, 2));
24699
+ else console.log(renderHermesInstall(result, apply));
24019
24700
  return 0;
24020
24701
  }
24021
24702
  if (target === "pi") {
24022
- console.log(renderPiInstall(runInstallPi(dir, apply), apply));
24703
+ const result = runInstallPi(dir, apply);
24704
+ if (asJson) console.log(JSON.stringify(piInstallJson(result, apply), null, 2));
24705
+ else console.log(renderPiInstall(result, apply));
24023
24706
  return 0;
24024
24707
  }
24025
24708
  console.error(`Unknown install target: ${target ?? "(none)"}. Supported: opencode, codex, claude, hermes, pi.`);
24026
24709
  return 1;
24027
24710
  }
24711
+ case "uninstall": {
24712
+ const target = rest[0];
24713
+ const dir = rest[1] ?? (target === "hermes" ? os4.homedir() : process.cwd());
24714
+ const apply = values.apply === true;
24715
+ try {
24716
+ const result = runUninstall(target, dir, apply);
24717
+ if (values.json) {
24718
+ console.log(JSON.stringify(result, null, 2));
24719
+ } else {
24720
+ console.log(renderUninstall(result, apply));
24721
+ }
24722
+ } catch (err) {
24723
+ console.error(err instanceof Error ? err.message : String(err));
24724
+ return 1;
24725
+ }
24726
+ return 0;
24727
+ }
24728
+ case "capabilities": {
24729
+ console.log(JSON.stringify(getCapabilities(package_default.version), null, 2));
24730
+ return 0;
24731
+ }
24028
24732
  case "hook": {
24029
24733
  const which = rest[0];
24030
24734
  if (which !== "claude" && which !== "codex") {
@@ -24032,22 +24736,45 @@ async function main(argv) {
24032
24736
  return 1;
24033
24737
  }
24034
24738
  const input = await readStdin();
24035
- const { response, event } = which === "claude" ? reduceClaudePayload(input) : reduceCodexPayload(input);
24739
+ const { response, event, attempt } = which === "claude" ? reduceClaudePayload(input) : reduceCodexPayload(input);
24036
24740
  process.stdout.write(response);
24037
- if (values.metrics && event) {
24741
+ if (values.metrics) {
24038
24742
  try {
24039
- const p = path15.resolve(values.metrics);
24040
- fs11.mkdirSync(path15.dirname(p), { recursive: true });
24041
- fs11.appendFileSync(
24042
- p,
24043
- JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), harness: which, ...event }) + "\n"
24044
- );
24743
+ const p = path16.resolve(values.metrics);
24744
+ fs12.mkdirSync(path16.dirname(p), { recursive: true });
24745
+ if (event) {
24746
+ fs12.appendFileSync(
24747
+ p,
24748
+ JSON.stringify(
24749
+ makeTrimEvent({
24750
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
24751
+ harness: which,
24752
+ ...event
24753
+ })
24754
+ ) + "\n"
24755
+ );
24756
+ } else if (attempt && trackPassThrough()) {
24757
+ fs12.appendFileSync(
24758
+ p,
24759
+ JSON.stringify(
24760
+ makeTrimEvent({
24761
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
24762
+ harness: which,
24763
+ tool: attempt.tool,
24764
+ reducer: null,
24765
+ beforeChars: attempt.beforeChars,
24766
+ afterChars: attempt.beforeChars,
24767
+ changed: false
24768
+ })
24769
+ ) + "\n"
24770
+ );
24771
+ }
24045
24772
  } catch {
24046
24773
  }
24047
24774
  }
24048
24775
  if (values.log) {
24049
24776
  try {
24050
- fs11.appendFileSync(
24777
+ fs12.appendFileSync(
24051
24778
  values.log,
24052
24779
  JSON.stringify({ inputChars: input.length, changed: response !== "{}", responseChars: response.length }) + "\n"
24053
24780
  );
@@ -24076,8 +24803,13 @@ async function main(argv) {
24076
24803
  return 1;
24077
24804
  }
24078
24805
  case "metrics": {
24079
- const path16 = rest[0] ?? DEFAULT_METRICS_PATH;
24080
- console.log(renderMetrics(loadMetrics(path16)));
24806
+ const path17 = rest[0] ?? DEFAULT_METRICS_PATH;
24807
+ const result = loadMetrics(path17);
24808
+ if (values.json) {
24809
+ console.log(metricsJson(result));
24810
+ } else {
24811
+ console.log(renderMetrics(result));
24812
+ }
24081
24813
  return 0;
24082
24814
  }
24083
24815
  case "reduce": {
@@ -24090,21 +24822,40 @@ async function main(argv) {
24090
24822
  const input = await readStdin();
24091
24823
  const result = reducePipe(input, minLength);
24092
24824
  process.stdout.write(result.output);
24093
- if (values.metrics && result.changed) {
24825
+ if (values.metrics) {
24094
24826
  try {
24095
- const p = path15.resolve(values.metrics);
24096
- fs11.mkdirSync(path15.dirname(p), { recursive: true });
24097
- fs11.appendFileSync(
24098
- 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"
24107
- );
24827
+ const p = path16.resolve(values.metrics);
24828
+ fs12.mkdirSync(path16.dirname(p), { recursive: true });
24829
+ if (result.changed) {
24830
+ fs12.appendFileSync(
24831
+ p,
24832
+ JSON.stringify(
24833
+ makeTrimEvent({
24834
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
24835
+ harness: "pipe",
24836
+ tool: "reduce",
24837
+ reducer: result.reducer,
24838
+ beforeChars: result.beforeChars,
24839
+ afterChars: result.afterChars
24840
+ })
24841
+ ) + "\n"
24842
+ );
24843
+ } else if (trackPassThrough() && input.length >= (minLength ?? DEFAULT_MIN_LENGTH)) {
24844
+ fs12.appendFileSync(
24845
+ p,
24846
+ JSON.stringify(
24847
+ makeTrimEvent({
24848
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
24849
+ harness: "pipe",
24850
+ tool: "reduce",
24851
+ reducer: null,
24852
+ beforeChars: input.length,
24853
+ afterChars: input.length,
24854
+ changed: false
24855
+ })
24856
+ ) + "\n"
24857
+ );
24858
+ }
24108
24859
  } catch {
24109
24860
  }
24110
24861
  }
@@ -24144,6 +24895,16 @@ async function main(argv) {
24144
24895
  return 1;
24145
24896
  }
24146
24897
  }
24898
+ function parseModeFlag(value) {
24899
+ return value === "active" || value === "dryrun" || value === "off" ? value : void 0;
24900
+ }
24901
+ function splitTools(value) {
24902
+ return value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
24903
+ }
24904
+ function trackPassThrough() {
24905
+ const v = process.env.HARNESSTRIM_TRACK_PASSTHROUGH;
24906
+ return v === void 0 || v !== "0" && v !== "false";
24907
+ }
24147
24908
  main(process.argv.slice(2)).then((code) => {
24148
24909
  process.exitCode = code;
24149
24910
  }).catch((err) => {