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