@fusengine/harness 0.1.6 → 0.1.8
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/adapters/cline/index.mjs +24 -1
- package/dist/adapters/cursor/index.mjs +36 -1
- package/dist/adapters/gemini/index.mjs +24 -1
- package/dist/cli/bin.mjs +10 -48
- package/dist/handle-BGe0QZvQ.mjs +224 -0
- package/dist/init/index.d.mts +16 -15
- package/dist/init/index.mjs +1 -1
- package/dist/run-Cdp2Ef9B.mjs +128 -0
- package/dist/runtime/index.d.mts +46 -1
- package/dist/runtime/index.mjs +2 -108
- package/package.json +1 -1
- package/dist/cline-BxslHtBG.mjs +0 -25
- package/dist/cursor-Bh7eh9y_.mjs +0 -37
- package/dist/gemini-SrK_fFAr.mjs +0 -25
- package/dist/run-Cc98348q.mjs +0 -94
|
@@ -1,2 +1,25 @@
|
|
|
1
|
-
import { t as
|
|
1
|
+
import { t as evaluate } from "../../evaluate-CsYyUucy.mjs";
|
|
2
|
+
import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
|
|
3
|
+
//#region src/adapters/cline/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Cline adapter (hook-mode). Schema per docs.cline.bot / .clinerules/hooks (2026):
|
|
6
|
+
* `PreToolUse` blocks via `{ cancel: true }`; it cannot modify tool parameters.
|
|
7
|
+
*/
|
|
8
|
+
/** Evaluate a tool use; cancel on a hard block, otherwise inject context. */
|
|
9
|
+
function preToolUse(input) {
|
|
10
|
+
const t = input.preToolUse;
|
|
11
|
+
const r = evaluate({
|
|
12
|
+
tool: t?.toolName ?? "write_to_file",
|
|
13
|
+
filePath: t?.parameters?.path,
|
|
14
|
+
content: t?.parameters?.content,
|
|
15
|
+
command: t?.parameters?.command
|
|
16
|
+
});
|
|
17
|
+
if (r.decision === "allow" || !r.prompt) return {};
|
|
18
|
+
const msg = formatPrompt(r.prompt);
|
|
19
|
+
return r.prompt.kind === "block" ? {
|
|
20
|
+
cancel: true,
|
|
21
|
+
errorMessage: msg
|
|
22
|
+
} : { contextModification: msg };
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
2
25
|
export { preToolUse };
|
|
@@ -1,2 +1,37 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as evaluate } from "../../evaluate-CsYyUucy.mjs";
|
|
2
|
+
import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
|
|
3
|
+
//#region src/adapters/cursor/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Cursor adapter (hook-mode). Schemas per cursor.com/docs/hooks (2026):
|
|
6
|
+
* `beforeShellExecution` can block; `afterFileEdit` is observe-only.
|
|
7
|
+
*/
|
|
8
|
+
function toPermission(kind) {
|
|
9
|
+
return kind === "block" ? "deny" : kind === "ask" ? "ask" : "allow";
|
|
10
|
+
}
|
|
11
|
+
/** Guard a shell command (git/install policies). */
|
|
12
|
+
function beforeShellExecution(payload) {
|
|
13
|
+
const r = evaluate({
|
|
14
|
+
tool: "Bash",
|
|
15
|
+
command: payload.command
|
|
16
|
+
});
|
|
17
|
+
if (r.decision === "allow" || !r.prompt) return { permission: "allow" };
|
|
18
|
+
const msg = formatPrompt(r.prompt);
|
|
19
|
+
return {
|
|
20
|
+
permission: toPermission(r.prompt.kind),
|
|
21
|
+
continue: false,
|
|
22
|
+
userMessage: msg,
|
|
23
|
+
agentMessage: msg
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Observe a file edit (Cursor cannot block here). Returns the verdict for logging. */
|
|
27
|
+
function afterFileEdit(payload) {
|
|
28
|
+
const content = payload.edits?.map((e) => e.new_string).join("\n") ?? "";
|
|
29
|
+
const r = evaluate({
|
|
30
|
+
tool: "Edit",
|
|
31
|
+
filePath: payload.file_path,
|
|
32
|
+
content
|
|
33
|
+
});
|
|
34
|
+
return { violation: r.decision === "deny" ? r.message : null };
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
2
37
|
export { afterFileEdit, beforeShellExecution };
|
|
@@ -1,2 +1,25 @@
|
|
|
1
|
-
import { t as
|
|
1
|
+
import { t as evaluate } from "../../evaluate-CsYyUucy.mjs";
|
|
2
|
+
import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
|
|
3
|
+
//#region src/adapters/gemini/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Gemini CLI adapter (hook-mode). Schema per google-gemini/gemini-cli docs/hooks (2026):
|
|
6
|
+
* `BeforeTool` blocks via `{ decision: "deny", reason }` (or exit 2).
|
|
7
|
+
*/
|
|
8
|
+
/** Evaluate a tool use; deny on a hard block, otherwise inject context. */
|
|
9
|
+
function beforeTool(input) {
|
|
10
|
+
const i = input.tool_input;
|
|
11
|
+
const r = evaluate({
|
|
12
|
+
tool: input.tool_name ?? "write_file",
|
|
13
|
+
filePath: i?.path,
|
|
14
|
+
content: i?.content,
|
|
15
|
+
command: i?.command
|
|
16
|
+
});
|
|
17
|
+
if (r.decision === "allow" || !r.prompt) return {};
|
|
18
|
+
const msg = formatPrompt(r.prompt);
|
|
19
|
+
return r.prompt.kind === "block" ? {
|
|
20
|
+
decision: "deny",
|
|
21
|
+
reason: msg
|
|
22
|
+
} : { hookSpecificOutput: { additionalContext: msg } };
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
2
25
|
export { beforeTool };
|
package/dist/cli/bin.mjs
CHANGED
|
@@ -1,50 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as detectHarness } from "../harness-C8Nxxyn_.mjs";
|
|
3
3
|
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-h_2LNEA8.mjs";
|
|
4
|
-
import { n as writeInitFile, t as initFor } from "../run-
|
|
5
|
-
import {
|
|
6
|
-
import { n as beforeShellExecution, t as afterFileEdit } from "../cursor-Bh7eh9y_.mjs";
|
|
7
|
-
import { t as preToolUse } from "../cline-BxslHtBG.mjs";
|
|
8
|
-
import { t as beforeTool } from "../gemini-SrK_fFAr.mjs";
|
|
9
|
-
//#region src/cli/hook.ts
|
|
10
|
-
/**
|
|
11
|
-
* Route a harness hook payload to its adapter and produce the native response.
|
|
12
|
-
* The deny/ask decision lives in `stdout` (the harness parses it); exit stays 0.
|
|
13
|
-
*/
|
|
14
|
-
function dispatchHook(id, payload) {
|
|
15
|
-
switch (id) {
|
|
16
|
-
case "claude-code":
|
|
17
|
-
case "codex": return {
|
|
18
|
-
stdout: guard(payload) ?? "",
|
|
19
|
-
exit: 0
|
|
20
|
-
};
|
|
21
|
-
case "cursor":
|
|
22
|
-
if (payload.hook_event_name === "afterFileEdit") {
|
|
23
|
-
afterFileEdit(payload);
|
|
24
|
-
return {
|
|
25
|
-
stdout: "",
|
|
26
|
-
exit: 0
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
return {
|
|
30
|
-
stdout: JSON.stringify(beforeShellExecution(payload)),
|
|
31
|
-
exit: 0
|
|
32
|
-
};
|
|
33
|
-
case "cline": return {
|
|
34
|
-
stdout: JSON.stringify(preToolUse(payload)),
|
|
35
|
-
exit: 0
|
|
36
|
-
};
|
|
37
|
-
case "gemini-cli": return {
|
|
38
|
-
stdout: JSON.stringify(beforeTool(payload)),
|
|
39
|
-
exit: 0
|
|
40
|
-
};
|
|
41
|
-
default: return {
|
|
42
|
-
stdout: "",
|
|
43
|
-
exit: 0
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
//#endregion
|
|
4
|
+
import { n as writeInitFile, t as initFor } from "../run-Cdp2Ef9B.mjs";
|
|
5
|
+
import { t as handleHook } from "../handle-BGe0QZvQ.mjs";
|
|
48
6
|
//#region src/cli/bin.ts
|
|
49
7
|
/**
|
|
50
8
|
* harness — CLI for @fusengine/harness.
|
|
@@ -66,17 +24,21 @@ async function readStdin() {
|
|
|
66
24
|
}
|
|
67
25
|
const cmd = process.argv[2];
|
|
68
26
|
if (cmd === "hook") {
|
|
69
|
-
const outcome =
|
|
27
|
+
const outcome = await handleHook(process.argv[3] ?? detectHarness().id, await readStdin(), {
|
|
28
|
+
now: Date.now(),
|
|
29
|
+
cwd: process.cwd()
|
|
30
|
+
});
|
|
70
31
|
if (outcome.stdout) process.stdout.write(outcome.stdout);
|
|
71
32
|
process.exit(outcome.exit);
|
|
72
33
|
} else if (cmd === "init") {
|
|
73
34
|
const id = process.argv[3] ?? detectHarness().id;
|
|
74
|
-
const
|
|
75
|
-
if (!
|
|
35
|
+
const files = initFor(id);
|
|
36
|
+
if (!files) {
|
|
76
37
|
process.stderr.write(`harness: no hook integration for "${id}" — use \`harness check\` in a pre-commit step\n`);
|
|
77
38
|
process.exit(1);
|
|
78
39
|
}
|
|
79
|
-
|
|
40
|
+
const written = files.map((f) => writeInitFile(process.cwd(), f));
|
|
41
|
+
process.stdout.write(`harness: wired ${id} -> ${written.join(", ")}\n`);
|
|
80
42
|
process.exit(0);
|
|
81
43
|
} else {
|
|
82
44
|
const files = stagedFiles();
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { l as detectFramework, t as evaluate } from "./evaluate-CsYyUucy.mjs";
|
|
2
|
+
import { r as evaluateApex } from "./apex-gGrHzvM2.mjs";
|
|
3
|
+
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
4
|
+
import { a as recordAgent, n as saveTrack, o as recordDoc, r as agentsFresh, s as recordRefRead, t as loadTrack } from "./store-BWvwnnf6.mjs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
//#region src/runtime/activity.ts
|
|
8
|
+
/** Read tools across harnesses (Claude `Read`, Gemini/Cline `read_file`, …). */
|
|
9
|
+
const READ_TOOLS = /* @__PURE__ */ new Set([
|
|
10
|
+
"Read",
|
|
11
|
+
"read_file",
|
|
12
|
+
"read_many_files"
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Map a live tool-use to the activity to record, or null when nothing is
|
|
16
|
+
* tracked. Works across harnesses — tool names are globally distinct:
|
|
17
|
+
* - MCP doc calls (`context7` / `exa`, any separator) → `doc`
|
|
18
|
+
* - `Task` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
|
|
19
|
+
* - a read tool opening a `.md` reference → `ref`
|
|
20
|
+
*/
|
|
21
|
+
function activityFor(event) {
|
|
22
|
+
if (/context7|exa/i.test(event.tool)) return {
|
|
23
|
+
kind: "doc",
|
|
24
|
+
framework: event.framework,
|
|
25
|
+
sessionId: event.sessionId,
|
|
26
|
+
source: /exa/i.test(event.tool) ? "exa" : "context7"
|
|
27
|
+
};
|
|
28
|
+
if (event.tool === "Task") {
|
|
29
|
+
const name = String(event.input?.subagent_type ?? "").split(":").pop() ?? "";
|
|
30
|
+
return name ? {
|
|
31
|
+
kind: "agent",
|
|
32
|
+
name,
|
|
33
|
+
ts: event.now
|
|
34
|
+
} : null;
|
|
35
|
+
}
|
|
36
|
+
if (READ_TOOLS.has(event.tool)) {
|
|
37
|
+
const path = String(event.input?.file_path ?? event.input?.path ?? "");
|
|
38
|
+
if (path.endsWith(".md")) return {
|
|
39
|
+
kind: "ref",
|
|
40
|
+
path
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/runtime/gate.ts
|
|
47
|
+
/** Prior agents the freshness gate requires before a code edit. */
|
|
48
|
+
const REQUIRED_AGENTS = ["explore-codebase", "research-expert"];
|
|
49
|
+
/** Default freshness window for {@link REQUIRED_AGENTS} (4 min, the APEX TTL). */
|
|
50
|
+
const DEFAULT_WINDOW_MS = 24e4;
|
|
51
|
+
/**
|
|
52
|
+
* Full gate: the stateless guards (file-size, git) first, then the stateful
|
|
53
|
+
* APEX gates fed from the session track. Returns the first blocking prompt, or
|
|
54
|
+
* null to allow. APEX gates apply only to code edits (a `filePath`).
|
|
55
|
+
*/
|
|
56
|
+
async function gate(input) {
|
|
57
|
+
const quick = evaluate({
|
|
58
|
+
tool: input.tool,
|
|
59
|
+
filePath: input.filePath,
|
|
60
|
+
content: input.content,
|
|
61
|
+
command: input.command
|
|
62
|
+
});
|
|
63
|
+
if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
|
|
64
|
+
if (!input.filePath) return null;
|
|
65
|
+
const track = await loadTrack(input.trackFile);
|
|
66
|
+
return evaluateApex({
|
|
67
|
+
sessionId: input.sessionId,
|
|
68
|
+
framework: input.framework,
|
|
69
|
+
filePath: input.filePath,
|
|
70
|
+
content: input.content ?? "",
|
|
71
|
+
authorizations: track.authorizations,
|
|
72
|
+
refs: input.refs,
|
|
73
|
+
refsRead: track.refsRead,
|
|
74
|
+
agentsFresh: agentsFresh(track, [...REQUIRED_AGENTS], input.windowMs ?? 24e4, input.now)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/runtime/normalize.ts
|
|
79
|
+
function str(v) {
|
|
80
|
+
return typeof v === "string" ? v : void 0;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Normalize a harness hook payload into a uniform event. Handles Cline's nested
|
|
84
|
+
* `preToolUse`/`postToolUse` shape and the top-level `tool_name`/`tool_input`
|
|
85
|
+
* shape used by Claude, Codex, Gemini, and Cursor.
|
|
86
|
+
*/
|
|
87
|
+
function normalizeEvent(id, payload) {
|
|
88
|
+
if (id === "cline") {
|
|
89
|
+
const post = payload.postToolUse;
|
|
90
|
+
const node = post ?? payload.preToolUse ?? {};
|
|
91
|
+
const params = node.parameters ?? {};
|
|
92
|
+
return {
|
|
93
|
+
phase: post ? "post" : "pre",
|
|
94
|
+
tool: str(node.toolName) ?? "",
|
|
95
|
+
input: params,
|
|
96
|
+
sessionId: str(payload.taskId) ?? "",
|
|
97
|
+
filePath: str(params.path),
|
|
98
|
+
content: str(params.content),
|
|
99
|
+
command: str(params.command)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const event = str(payload.hook_event_name) ?? "";
|
|
103
|
+
const input = payload.tool_input ?? payload;
|
|
104
|
+
return {
|
|
105
|
+
phase: /post|after/i.test(event) ? "post" : "pre",
|
|
106
|
+
tool: str(payload.tool_name) ?? "",
|
|
107
|
+
input,
|
|
108
|
+
sessionId: str(payload.session_id) ?? str(payload.conversation_id) ?? "",
|
|
109
|
+
filePath: str(input.file_path) ?? str(input.path) ?? str(payload.file_path),
|
|
110
|
+
content: str(input.content) ?? str(input.new_string),
|
|
111
|
+
command: str(input.command) ?? str(payload.command)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/runtime/paths.ts
|
|
116
|
+
/** Path to a session's track file (under a per-tool base dir). */
|
|
117
|
+
function trackFile(sessionId, baseDir = join(tmpdir(), "fuse-harness")) {
|
|
118
|
+
return join(baseDir, `track-${sessionId.replace(/[^A-Za-z0-9_-]/g, "_") || "default"}.json`);
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/runtime/record.ts
|
|
122
|
+
/** Apply an activity to a session's track and persist it (PostToolUse path). */
|
|
123
|
+
async function recordActivity(file, activity) {
|
|
124
|
+
const track = await loadTrack(file);
|
|
125
|
+
await saveTrack(file, activity.kind === "agent" ? recordAgent(track, activity.name, activity.ts) : activity.kind === "doc" ? recordDoc(track, activity.framework, activity.sessionId, activity.source) : recordRefRead(track, activity.path));
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/runtime/respond.ts
|
|
129
|
+
/**
|
|
130
|
+
* Map a portable {@link Prompt} to a harness's native hook response. `block`
|
|
131
|
+
* denies; anything else asks/injects context. (Codex/Cursor parse but ignore
|
|
132
|
+
* `ask` — they only honor deny.)
|
|
133
|
+
*/
|
|
134
|
+
function respond(id, prompt) {
|
|
135
|
+
const message = formatPrompt(prompt);
|
|
136
|
+
const deny = prompt.kind === "block";
|
|
137
|
+
switch (id) {
|
|
138
|
+
case "claude-code":
|
|
139
|
+
case "codex": return JSON.stringify({ hookSpecificOutput: {
|
|
140
|
+
hookEventName: "PreToolUse",
|
|
141
|
+
permissionDecision: deny ? "deny" : "ask",
|
|
142
|
+
permissionDecisionReason: message
|
|
143
|
+
} });
|
|
144
|
+
case "gemini-cli": return JSON.stringify(deny ? {
|
|
145
|
+
decision: "deny",
|
|
146
|
+
reason: message
|
|
147
|
+
} : { hookSpecificOutput: { additionalContext: message } });
|
|
148
|
+
case "cursor": return JSON.stringify({
|
|
149
|
+
permission: deny ? "deny" : "ask",
|
|
150
|
+
continue: false,
|
|
151
|
+
userMessage: message,
|
|
152
|
+
agentMessage: message
|
|
153
|
+
});
|
|
154
|
+
case "cline": return JSON.stringify(deny ? {
|
|
155
|
+
cancel: true,
|
|
156
|
+
errorMessage: message
|
|
157
|
+
} : { contextModification: message });
|
|
158
|
+
default: return "";
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
//#region src/runtime/storage.ts
|
|
163
|
+
/** Per-harness config dir (relative to the project root) where state lives. */
|
|
164
|
+
const STATE_DIR = {
|
|
165
|
+
"claude-code": ".claude",
|
|
166
|
+
codex: ".codex",
|
|
167
|
+
cursor: ".cursor",
|
|
168
|
+
"gemini-cli": ".gemini",
|
|
169
|
+
cline: ".clinerules"
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Directory where a harness's fuse-harness track files live — under that
|
|
173
|
+
* harness's own config dir (`.claude`, `.codex`, …) so state sits next to its
|
|
174
|
+
* hooks. Falls back to `.fuse-harness` for harnesses without a known config dir.
|
|
175
|
+
*/
|
|
176
|
+
function harnessTrackDir(id, projectRoot) {
|
|
177
|
+
return join(projectRoot, STATE_DIR[id] ?? ".fuse-harness", "harness");
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/runtime/handle.ts
|
|
181
|
+
/**
|
|
182
|
+
* The full hook handler: on a PRE event it gates the tool-use (stateless guards
|
|
183
|
+
* then APEX gates from the session track) and returns the native response; on a
|
|
184
|
+
* POST event it records the activity into the track. The loop that makes the
|
|
185
|
+
* package behave like the Claude plugin, on any harness.
|
|
186
|
+
*/
|
|
187
|
+
async function handleHook(id, payload, opts) {
|
|
188
|
+
const event = normalizeEvent(id, payload);
|
|
189
|
+
const file = trackFile(event.sessionId, harnessTrackDir(id, opts.cwd));
|
|
190
|
+
const framework = detectFramework(event.filePath ?? "", event.content ?? "");
|
|
191
|
+
if (event.phase === "post") {
|
|
192
|
+
const activity = activityFor({
|
|
193
|
+
tool: event.tool,
|
|
194
|
+
input: event.input,
|
|
195
|
+
sessionId: event.sessionId,
|
|
196
|
+
framework,
|
|
197
|
+
now: opts.now
|
|
198
|
+
});
|
|
199
|
+
if (activity) await recordActivity(file, activity);
|
|
200
|
+
return {
|
|
201
|
+
stdout: "",
|
|
202
|
+
exit: 0
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const prompt = await gate({
|
|
206
|
+
sessionId: event.sessionId,
|
|
207
|
+
framework,
|
|
208
|
+
tool: event.tool,
|
|
209
|
+
filePath: event.filePath,
|
|
210
|
+
content: event.content,
|
|
211
|
+
command: event.command,
|
|
212
|
+
now: opts.now,
|
|
213
|
+
trackFile: file
|
|
214
|
+
});
|
|
215
|
+
return prompt ? {
|
|
216
|
+
stdout: respond(id, prompt),
|
|
217
|
+
exit: 0
|
|
218
|
+
} : {
|
|
219
|
+
stdout: "",
|
|
220
|
+
exit: 0
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
export { trackFile as a, REQUIRED_AGENTS as c, recordActivity as i, gate as l, harnessTrackDir as n, normalizeEvent as o, respond as r, DEFAULT_WINDOW_MS as s, handleHook as t, activityFor as u };
|
package/dist/init/index.d.mts
CHANGED
|
@@ -2,8 +2,9 @@ import { t as HarnessId } from "../harness-DwJskkz_.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/init/templates.d.ts
|
|
4
4
|
/**
|
|
5
|
-
* Wiring file templates per harness,
|
|
6
|
-
*
|
|
5
|
+
* Wiring file templates per harness, registering `harness hook <id>` for BOTH
|
|
6
|
+
* phases: PRE (gate the edit) and POST (catch-all → `activityFor` filters what
|
|
7
|
+
* to record). Formats verified against each harness's 2026 hook docs.
|
|
7
8
|
*/
|
|
8
9
|
/** A file to write during `harness init`. */
|
|
9
10
|
interface InitFile {
|
|
@@ -11,23 +12,23 @@ interface InitFile {
|
|
|
11
12
|
content: string;
|
|
12
13
|
executable?: boolean;
|
|
13
14
|
}
|
|
14
|
-
/** Claude Code: PreToolUse(Write|Edit)
|
|
15
|
-
declare function claudeInit(command: string): InitFile;
|
|
16
|
-
/** Codex CLI: `.codex/hooks.json`
|
|
17
|
-
declare function codexInit(command: string): InitFile;
|
|
18
|
-
/** Cursor: `.cursor/hooks.json` (version 1) — shell + file-edit
|
|
19
|
-
declare function cursorInit(command: string): InitFile;
|
|
20
|
-
/** Gemini CLI: `.gemini/settings.json` — BeforeTool
|
|
21
|
-
declare function geminiInit(command: string): InitFile;
|
|
22
|
-
/** Cline:
|
|
23
|
-
declare function clineInit(command: string): InitFile;
|
|
15
|
+
/** Claude Code: `.claude/settings.json` — PreToolUse(Write|Edit) + PostToolUse(all). */
|
|
16
|
+
declare function claudeInit(command: string): InitFile[];
|
|
17
|
+
/** Codex CLI: `.codex/hooks.json` (Claude-compatible shape). */
|
|
18
|
+
declare function codexInit(command: string): InitFile[];
|
|
19
|
+
/** Cursor: `.cursor/hooks.json` (version 1) — shell + tool gate + file-edit observe. */
|
|
20
|
+
declare function cursorInit(command: string): InitFile[];
|
|
21
|
+
/** Gemini CLI: `.gemini/settings.json` — BeforeTool(edits) + AfterTool(all, regex). */
|
|
22
|
+
declare function geminiInit(command: string): InitFile[];
|
|
23
|
+
/** Cline: executable `.clinerules/hooks/PreToolUse` + `PostToolUse` piping stdin. */
|
|
24
|
+
declare function clineInit(command: string): InitFile[];
|
|
24
25
|
//#endregion
|
|
25
26
|
//#region src/init/run.d.ts
|
|
26
27
|
/**
|
|
27
|
-
* Build the wiring file for a harness, or null when it has no hook
|
|
28
|
-
* (cli-mode harnesses use `harness check` in a pre-commit step
|
|
28
|
+
* Build the wiring file(s) for a harness, or null when it has no hook
|
|
29
|
+
* integration (cli-mode harnesses use `harness check` in a pre-commit step).
|
|
29
30
|
*/
|
|
30
|
-
declare function initFor(id: HarnessId, command?: string): InitFile | null;
|
|
31
|
+
declare function initFor(id: HarnessId, command?: string): InitFile[] | null;
|
|
31
32
|
/** Write an {@link InitFile} under `root` (creates dirs; `chmod +x` when executable). */
|
|
32
33
|
declare function writeInitFile(root: string, file: InitFile): string;
|
|
33
34
|
//#endregion
|
package/dist/init/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as codexInit, i as clineInit, n as writeInitFile, o as cursorInit, r as claudeInit, s as geminiInit, t as initFor } from "../run-
|
|
1
|
+
import { a as codexInit, i as clineInit, n as writeInitFile, o as cursorInit, r as claudeInit, s as geminiInit, t as initFor } from "../run-Cdp2Ef9B.mjs";
|
|
2
2
|
export { claudeInit, clineInit, codexInit, cursorInit, geminiInit, initFor, writeInitFile };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
//#region src/init/templates.ts
|
|
4
|
+
function json(obj) {
|
|
5
|
+
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
6
|
+
}
|
|
7
|
+
/** Claude Code: `.claude/settings.json` — PreToolUse(Write|Edit) + PostToolUse(all). */
|
|
8
|
+
function claudeInit(command) {
|
|
9
|
+
return [{
|
|
10
|
+
path: ".claude/settings.json",
|
|
11
|
+
content: json({ hooks: {
|
|
12
|
+
PreToolUse: [{
|
|
13
|
+
matcher: "Write|Edit",
|
|
14
|
+
hooks: [{
|
|
15
|
+
type: "command",
|
|
16
|
+
command
|
|
17
|
+
}]
|
|
18
|
+
}],
|
|
19
|
+
PostToolUse: [{
|
|
20
|
+
matcher: "",
|
|
21
|
+
hooks: [{
|
|
22
|
+
type: "command",
|
|
23
|
+
command
|
|
24
|
+
}]
|
|
25
|
+
}]
|
|
26
|
+
} })
|
|
27
|
+
}];
|
|
28
|
+
}
|
|
29
|
+
/** Codex CLI: `.codex/hooks.json` (Claude-compatible shape). */
|
|
30
|
+
function codexInit(command) {
|
|
31
|
+
return [{
|
|
32
|
+
path: ".codex/hooks.json",
|
|
33
|
+
content: json({ hooks: {
|
|
34
|
+
PreToolUse: [{
|
|
35
|
+
matcher: "Bash|apply_patch",
|
|
36
|
+
hooks: [{
|
|
37
|
+
type: "command",
|
|
38
|
+
command
|
|
39
|
+
}]
|
|
40
|
+
}],
|
|
41
|
+
PostToolUse: [{
|
|
42
|
+
matcher: "",
|
|
43
|
+
hooks: [{
|
|
44
|
+
type: "command",
|
|
45
|
+
command
|
|
46
|
+
}]
|
|
47
|
+
}]
|
|
48
|
+
} })
|
|
49
|
+
}];
|
|
50
|
+
}
|
|
51
|
+
/** Cursor: `.cursor/hooks.json` (version 1) — shell + tool gate + file-edit observe. */
|
|
52
|
+
function cursorInit(command) {
|
|
53
|
+
return [{
|
|
54
|
+
path: ".cursor/hooks.json",
|
|
55
|
+
content: json({
|
|
56
|
+
version: 1,
|
|
57
|
+
hooks: {
|
|
58
|
+
beforeShellExecution: [{ command }],
|
|
59
|
+
preToolUse: [{ command }],
|
|
60
|
+
afterFileEdit: [{ command }]
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}];
|
|
64
|
+
}
|
|
65
|
+
/** Gemini CLI: `.gemini/settings.json` — BeforeTool(edits) + AfterTool(all, regex). */
|
|
66
|
+
function geminiInit(command) {
|
|
67
|
+
return [{
|
|
68
|
+
path: ".gemini/settings.json",
|
|
69
|
+
content: json({ hooks: {
|
|
70
|
+
BeforeTool: [{
|
|
71
|
+
matcher: "write_file|edit_file|replace",
|
|
72
|
+
hooks: [{
|
|
73
|
+
type: "command",
|
|
74
|
+
command,
|
|
75
|
+
timeout: 3e4
|
|
76
|
+
}]
|
|
77
|
+
}],
|
|
78
|
+
AfterTool: [{
|
|
79
|
+
matcher: ".*",
|
|
80
|
+
hooks: [{
|
|
81
|
+
type: "command",
|
|
82
|
+
command,
|
|
83
|
+
timeout: 3e4
|
|
84
|
+
}]
|
|
85
|
+
}]
|
|
86
|
+
} })
|
|
87
|
+
}];
|
|
88
|
+
}
|
|
89
|
+
/** Cline: executable `.clinerules/hooks/PreToolUse` + `PostToolUse` piping stdin. */
|
|
90
|
+
function clineInit(command) {
|
|
91
|
+
const script = (phase) => `#!/usr/bin/env bash\n# fuse-harness Cline ${phase} hook\ncat | ${command}\n`;
|
|
92
|
+
return [{
|
|
93
|
+
path: ".clinerules/hooks/PreToolUse",
|
|
94
|
+
content: script("pre"),
|
|
95
|
+
executable: true
|
|
96
|
+
}, {
|
|
97
|
+
path: ".clinerules/hooks/PostToolUse",
|
|
98
|
+
content: script("post"),
|
|
99
|
+
executable: true
|
|
100
|
+
}];
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/init/run.ts
|
|
104
|
+
const RUNNERS = {
|
|
105
|
+
"claude-code": claudeInit,
|
|
106
|
+
codex: codexInit,
|
|
107
|
+
cursor: cursorInit,
|
|
108
|
+
"gemini-cli": geminiInit,
|
|
109
|
+
cline: clineInit
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Build the wiring file(s) for a harness, or null when it has no hook
|
|
113
|
+
* integration (cli-mode harnesses use `harness check` in a pre-commit step).
|
|
114
|
+
*/
|
|
115
|
+
function initFor(id, command = `npx harness hook ${id}`) {
|
|
116
|
+
const make = RUNNERS[id];
|
|
117
|
+
return make ? make(command) : null;
|
|
118
|
+
}
|
|
119
|
+
/** Write an {@link InitFile} under `root` (creates dirs; `chmod +x` when executable). */
|
|
120
|
+
function writeInitFile(root, file) {
|
|
121
|
+
const full = join(root, file.path);
|
|
122
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
123
|
+
writeFileSync(full, file.content);
|
|
124
|
+
if (file.executable) chmodSync(full, 493);
|
|
125
|
+
return full;
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { codexInit as a, clineInit as i, writeInitFile as n, cursorInit as o, claudeInit as r, geminiInit as s, initFor as t };
|
package/dist/runtime/index.d.mts
CHANGED
|
@@ -76,4 +76,49 @@ interface GateInput {
|
|
|
76
76
|
*/
|
|
77
77
|
declare function gate(input: GateInput): Promise<Prompt | null>;
|
|
78
78
|
//#endregion
|
|
79
|
-
|
|
79
|
+
//#region src/runtime/normalize.d.ts
|
|
80
|
+
/** A hook event normalized across harnesses. */
|
|
81
|
+
interface NormalizedEvent {
|
|
82
|
+
phase: "pre" | "post";
|
|
83
|
+
tool: string;
|
|
84
|
+
input: Record<string, unknown>;
|
|
85
|
+
sessionId: string;
|
|
86
|
+
filePath?: string;
|
|
87
|
+
content?: string;
|
|
88
|
+
command?: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Normalize a harness hook payload into a uniform event. Handles Cline's nested
|
|
92
|
+
* `preToolUse`/`postToolUse` shape and the top-level `tool_name`/`tool_input`
|
|
93
|
+
* shape used by Claude, Codex, Gemini, and Cursor.
|
|
94
|
+
*/
|
|
95
|
+
declare function normalizeEvent(id: string, payload: Record<string, unknown>): NormalizedEvent;
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/runtime/respond.d.ts
|
|
98
|
+
/**
|
|
99
|
+
* Map a portable {@link Prompt} to a harness's native hook response. `block`
|
|
100
|
+
* denies; anything else asks/injects context. (Codex/Cursor parse but ignore
|
|
101
|
+
* `ask` — they only honor deny.)
|
|
102
|
+
*/
|
|
103
|
+
declare function respond(id: string, prompt: Prompt): string;
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/runtime/handle.d.ts
|
|
106
|
+
/** Options for {@link handleHook} (caller supplies the clock + project root). */
|
|
107
|
+
interface HandleOptions {
|
|
108
|
+
now: number;
|
|
109
|
+
cwd: string;
|
|
110
|
+
}
|
|
111
|
+
/** What the hook bin should print + exit with. */
|
|
112
|
+
interface HandleOutcome {
|
|
113
|
+
stdout: string;
|
|
114
|
+
exit: number;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The full hook handler: on a PRE event it gates the tool-use (stateless guards
|
|
118
|
+
* then APEX gates from the session track) and returns the native response; on a
|
|
119
|
+
* POST event it records the activity into the track. The loop that makes the
|
|
120
|
+
* package behave like the Claude plugin, on any harness.
|
|
121
|
+
*/
|
|
122
|
+
declare function handleHook(id: string, payload: Record<string, unknown>, opts: HandleOptions): Promise<HandleOutcome>;
|
|
123
|
+
//#endregion
|
|
124
|
+
export { Activity, DEFAULT_WINDOW_MS, GateInput, HandleOptions, HandleOutcome, NormalizedEvent, REQUIRED_AGENTS, ToolEvent, activityFor, gate, handleHook, harnessTrackDir, normalizeEvent, recordActivity, respond, trackFile };
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -1,108 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
|
|
3
|
-
import { a as recordAgent, n as saveTrack, o as recordDoc, r as agentsFresh, s as recordRefRead, t as loadTrack } from "../store-BWvwnnf6.mjs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
//#region src/runtime/paths.ts
|
|
7
|
-
/** Path to a session's track file (under a per-tool base dir). */
|
|
8
|
-
function trackFile(sessionId, baseDir = join(tmpdir(), "fuse-harness")) {
|
|
9
|
-
return join(baseDir, `track-${sessionId.replace(/[^A-Za-z0-9_-]/g, "_") || "default"}.json`);
|
|
10
|
-
}
|
|
11
|
-
//#endregion
|
|
12
|
-
//#region src/runtime/storage.ts
|
|
13
|
-
/** Per-harness config dir (relative to the project root) where state lives. */
|
|
14
|
-
const STATE_DIR = {
|
|
15
|
-
"claude-code": ".claude",
|
|
16
|
-
codex: ".codex",
|
|
17
|
-
cursor: ".cursor",
|
|
18
|
-
"gemini-cli": ".gemini",
|
|
19
|
-
cline: ".clinerules"
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* Directory where a harness's fuse-harness track files live — under that
|
|
23
|
-
* harness's own config dir (`.claude`, `.codex`, …) so state sits next to its
|
|
24
|
-
* hooks. Falls back to `.fuse-harness` for harnesses without a known config dir.
|
|
25
|
-
*/
|
|
26
|
-
function harnessTrackDir(id, projectRoot) {
|
|
27
|
-
return join(projectRoot, STATE_DIR[id] ?? ".fuse-harness", "harness");
|
|
28
|
-
}
|
|
29
|
-
//#endregion
|
|
30
|
-
//#region src/runtime/record.ts
|
|
31
|
-
/** Apply an activity to a session's track and persist it (PostToolUse path). */
|
|
32
|
-
async function recordActivity(file, activity) {
|
|
33
|
-
const track = await loadTrack(file);
|
|
34
|
-
await saveTrack(file, activity.kind === "agent" ? recordAgent(track, activity.name, activity.ts) : activity.kind === "doc" ? recordDoc(track, activity.framework, activity.sessionId, activity.source) : recordRefRead(track, activity.path));
|
|
35
|
-
}
|
|
36
|
-
//#endregion
|
|
37
|
-
//#region src/runtime/activity.ts
|
|
38
|
-
/** Read tools across harnesses (Claude `Read`, Gemini/Cline `read_file`, …). */
|
|
39
|
-
const READ_TOOLS = /* @__PURE__ */ new Set([
|
|
40
|
-
"Read",
|
|
41
|
-
"read_file",
|
|
42
|
-
"read_many_files"
|
|
43
|
-
]);
|
|
44
|
-
/**
|
|
45
|
-
* Map a live tool-use to the activity to record, or null when nothing is
|
|
46
|
-
* tracked. Works across harnesses — tool names are globally distinct:
|
|
47
|
-
* - MCP doc calls (`context7` / `exa`, any separator) → `doc`
|
|
48
|
-
* - `Task` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
|
|
49
|
-
* - a read tool opening a `.md` reference → `ref`
|
|
50
|
-
*/
|
|
51
|
-
function activityFor(event) {
|
|
52
|
-
if (/context7|exa/i.test(event.tool)) return {
|
|
53
|
-
kind: "doc",
|
|
54
|
-
framework: event.framework,
|
|
55
|
-
sessionId: event.sessionId,
|
|
56
|
-
source: /exa/i.test(event.tool) ? "exa" : "context7"
|
|
57
|
-
};
|
|
58
|
-
if (event.tool === "Task") {
|
|
59
|
-
const name = String(event.input?.subagent_type ?? "").split(":").pop() ?? "";
|
|
60
|
-
return name ? {
|
|
61
|
-
kind: "agent",
|
|
62
|
-
name,
|
|
63
|
-
ts: event.now
|
|
64
|
-
} : null;
|
|
65
|
-
}
|
|
66
|
-
if (READ_TOOLS.has(event.tool)) {
|
|
67
|
-
const path = String(event.input?.file_path ?? event.input?.path ?? "");
|
|
68
|
-
if (path.endsWith(".md")) return {
|
|
69
|
-
kind: "ref",
|
|
70
|
-
path
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
//#endregion
|
|
76
|
-
//#region src/runtime/gate.ts
|
|
77
|
-
/** Prior agents the freshness gate requires before a code edit. */
|
|
78
|
-
const REQUIRED_AGENTS = ["explore-codebase", "research-expert"];
|
|
79
|
-
/** Default freshness window for {@link REQUIRED_AGENTS} (4 min, the APEX TTL). */
|
|
80
|
-
const DEFAULT_WINDOW_MS = 24e4;
|
|
81
|
-
/**
|
|
82
|
-
* Full gate: the stateless guards (file-size, git) first, then the stateful
|
|
83
|
-
* APEX gates fed from the session track. Returns the first blocking prompt, or
|
|
84
|
-
* null to allow. APEX gates apply only to code edits (a `filePath`).
|
|
85
|
-
*/
|
|
86
|
-
async function gate(input) {
|
|
87
|
-
const quick = evaluate({
|
|
88
|
-
tool: input.tool,
|
|
89
|
-
filePath: input.filePath,
|
|
90
|
-
content: input.content,
|
|
91
|
-
command: input.command
|
|
92
|
-
});
|
|
93
|
-
if (quick.decision !== "allow" && quick.prompt) return quick.prompt;
|
|
94
|
-
if (!input.filePath) return null;
|
|
95
|
-
const track = await loadTrack(input.trackFile);
|
|
96
|
-
return evaluateApex({
|
|
97
|
-
sessionId: input.sessionId,
|
|
98
|
-
framework: input.framework,
|
|
99
|
-
filePath: input.filePath,
|
|
100
|
-
content: input.content ?? "",
|
|
101
|
-
authorizations: track.authorizations,
|
|
102
|
-
refs: input.refs,
|
|
103
|
-
refsRead: track.refsRead,
|
|
104
|
-
agentsFresh: agentsFresh(track, [...REQUIRED_AGENTS], input.windowMs ?? 24e4, input.now)
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
//#endregion
|
|
108
|
-
export { DEFAULT_WINDOW_MS, REQUIRED_AGENTS, activityFor, gate, harnessTrackDir, recordActivity, trackFile };
|
|
1
|
+
import { a as trackFile, c as REQUIRED_AGENTS, i as recordActivity, l as gate, n as harnessTrackDir, o as normalizeEvent, r as respond, s as DEFAULT_WINDOW_MS, t as handleHook, u as activityFor } from "../handle-BGe0QZvQ.mjs";
|
|
2
|
+
export { DEFAULT_WINDOW_MS, REQUIRED_AGENTS, activityFor, gate, handleHook, harnessTrackDir, normalizeEvent, recordActivity, respond, trackFile };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusengine/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.ts",
|
package/dist/cline-BxslHtBG.mjs
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { t as evaluate } from "./evaluate-CsYyUucy.mjs";
|
|
2
|
-
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
|
-
//#region src/adapters/cline/index.ts
|
|
4
|
-
/**
|
|
5
|
-
* Cline adapter (hook-mode). Schema per docs.cline.bot / .clinerules/hooks (2026):
|
|
6
|
-
* `PreToolUse` blocks via `{ cancel: true }`; it cannot modify tool parameters.
|
|
7
|
-
*/
|
|
8
|
-
/** Evaluate a tool use; cancel on a hard block, otherwise inject context. */
|
|
9
|
-
function preToolUse(input) {
|
|
10
|
-
const t = input.preToolUse;
|
|
11
|
-
const r = evaluate({
|
|
12
|
-
tool: t?.toolName ?? "write_to_file",
|
|
13
|
-
filePath: t?.parameters?.path,
|
|
14
|
-
content: t?.parameters?.content,
|
|
15
|
-
command: t?.parameters?.command
|
|
16
|
-
});
|
|
17
|
-
if (r.decision === "allow" || !r.prompt) return {};
|
|
18
|
-
const msg = formatPrompt(r.prompt);
|
|
19
|
-
return r.prompt.kind === "block" ? {
|
|
20
|
-
cancel: true,
|
|
21
|
-
errorMessage: msg
|
|
22
|
-
} : { contextModification: msg };
|
|
23
|
-
}
|
|
24
|
-
//#endregion
|
|
25
|
-
export { preToolUse as t };
|
package/dist/cursor-Bh7eh9y_.mjs
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { t as evaluate } from "./evaluate-CsYyUucy.mjs";
|
|
2
|
-
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
|
-
//#region src/adapters/cursor/index.ts
|
|
4
|
-
/**
|
|
5
|
-
* Cursor adapter (hook-mode). Schemas per cursor.com/docs/hooks (2026):
|
|
6
|
-
* `beforeShellExecution` can block; `afterFileEdit` is observe-only.
|
|
7
|
-
*/
|
|
8
|
-
function toPermission(kind) {
|
|
9
|
-
return kind === "block" ? "deny" : kind === "ask" ? "ask" : "allow";
|
|
10
|
-
}
|
|
11
|
-
/** Guard a shell command (git/install policies). */
|
|
12
|
-
function beforeShellExecution(payload) {
|
|
13
|
-
const r = evaluate({
|
|
14
|
-
tool: "Bash",
|
|
15
|
-
command: payload.command
|
|
16
|
-
});
|
|
17
|
-
if (r.decision === "allow" || !r.prompt) return { permission: "allow" };
|
|
18
|
-
const msg = formatPrompt(r.prompt);
|
|
19
|
-
return {
|
|
20
|
-
permission: toPermission(r.prompt.kind),
|
|
21
|
-
continue: false,
|
|
22
|
-
userMessage: msg,
|
|
23
|
-
agentMessage: msg
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
/** Observe a file edit (Cursor cannot block here). Returns the verdict for logging. */
|
|
27
|
-
function afterFileEdit(payload) {
|
|
28
|
-
const content = payload.edits?.map((e) => e.new_string).join("\n") ?? "";
|
|
29
|
-
const r = evaluate({
|
|
30
|
-
tool: "Edit",
|
|
31
|
-
filePath: payload.file_path,
|
|
32
|
-
content
|
|
33
|
-
});
|
|
34
|
-
return { violation: r.decision === "deny" ? r.message : null };
|
|
35
|
-
}
|
|
36
|
-
//#endregion
|
|
37
|
-
export { beforeShellExecution as n, afterFileEdit as t };
|
package/dist/gemini-SrK_fFAr.mjs
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { t as evaluate } from "./evaluate-CsYyUucy.mjs";
|
|
2
|
-
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
|
-
//#region src/adapters/gemini/index.ts
|
|
4
|
-
/**
|
|
5
|
-
* Gemini CLI adapter (hook-mode). Schema per google-gemini/gemini-cli docs/hooks (2026):
|
|
6
|
-
* `BeforeTool` blocks via `{ decision: "deny", reason }` (or exit 2).
|
|
7
|
-
*/
|
|
8
|
-
/** Evaluate a tool use; deny on a hard block, otherwise inject context. */
|
|
9
|
-
function beforeTool(input) {
|
|
10
|
-
const i = input.tool_input;
|
|
11
|
-
const r = evaluate({
|
|
12
|
-
tool: input.tool_name ?? "write_file",
|
|
13
|
-
filePath: i?.path,
|
|
14
|
-
content: i?.content,
|
|
15
|
-
command: i?.command
|
|
16
|
-
});
|
|
17
|
-
if (r.decision === "allow" || !r.prompt) return {};
|
|
18
|
-
const msg = formatPrompt(r.prompt);
|
|
19
|
-
return r.prompt.kind === "block" ? {
|
|
20
|
-
decision: "deny",
|
|
21
|
-
reason: msg
|
|
22
|
-
} : { hookSpecificOutput: { additionalContext: msg } };
|
|
23
|
-
}
|
|
24
|
-
//#endregion
|
|
25
|
-
export { beforeTool as t };
|
package/dist/run-Cc98348q.mjs
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
//#region src/init/templates.ts
|
|
4
|
-
function json(obj) {
|
|
5
|
-
return `${JSON.stringify(obj, null, 2)}\n`;
|
|
6
|
-
}
|
|
7
|
-
/** Claude Code: PreToolUse(Write|Edit) -> command. Target: `.claude/settings.json`. */
|
|
8
|
-
function claudeInit(command) {
|
|
9
|
-
return {
|
|
10
|
-
path: ".claude/settings.json",
|
|
11
|
-
content: json({ hooks: { PreToolUse: [{
|
|
12
|
-
matcher: "Write|Edit",
|
|
13
|
-
hooks: [{
|
|
14
|
-
type: "command",
|
|
15
|
-
command
|
|
16
|
-
}]
|
|
17
|
-
}] } })
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/** Codex CLI: `.codex/hooks.json` — PreToolUse (Claude-compatible shape). */
|
|
21
|
-
function codexInit(command) {
|
|
22
|
-
return {
|
|
23
|
-
path: ".codex/hooks.json",
|
|
24
|
-
content: json({ hooks: { PreToolUse: [{
|
|
25
|
-
matcher: "Bash|apply_patch",
|
|
26
|
-
hooks: [{
|
|
27
|
-
type: "command",
|
|
28
|
-
command
|
|
29
|
-
}]
|
|
30
|
-
}] } })
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
/** Cursor: `.cursor/hooks.json` (version 1) — shell + file-edit hooks. */
|
|
34
|
-
function cursorInit(command) {
|
|
35
|
-
return {
|
|
36
|
-
path: ".cursor/hooks.json",
|
|
37
|
-
content: json({
|
|
38
|
-
version: 1,
|
|
39
|
-
hooks: {
|
|
40
|
-
beforeShellExecution: [{ command }],
|
|
41
|
-
afterFileEdit: [{ command }]
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
/** Gemini CLI: `.gemini/settings.json` — BeforeTool, timeout in ms. */
|
|
47
|
-
function geminiInit(command) {
|
|
48
|
-
return {
|
|
49
|
-
path: ".gemini/settings.json",
|
|
50
|
-
content: json({ hooks: { BeforeTool: [{
|
|
51
|
-
matcher: "write_file|edit_file|replace",
|
|
52
|
-
hooks: [{
|
|
53
|
-
type: "command",
|
|
54
|
-
command,
|
|
55
|
-
timeout: 3e4
|
|
56
|
-
}]
|
|
57
|
-
}] } })
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
/** Cline: an executable `.clinerules/hooks/PreToolUse` that pipes stdin to the command. */
|
|
61
|
-
function clineInit(command) {
|
|
62
|
-
return {
|
|
63
|
-
path: ".clinerules/hooks/PreToolUse",
|
|
64
|
-
content: `#!/usr/bin/env bash\n# fuse-harness Cline hook — evaluate each tool use\ncat | ${command}\n`,
|
|
65
|
-
executable: true
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
//#endregion
|
|
69
|
-
//#region src/init/run.ts
|
|
70
|
-
const RUNNERS = {
|
|
71
|
-
"claude-code": claudeInit,
|
|
72
|
-
codex: codexInit,
|
|
73
|
-
cursor: cursorInit,
|
|
74
|
-
"gemini-cli": geminiInit,
|
|
75
|
-
cline: clineInit
|
|
76
|
-
};
|
|
77
|
-
/**
|
|
78
|
-
* Build the wiring file for a harness, or null when it has no hook integration
|
|
79
|
-
* (cli-mode harnesses use `harness check` in a pre-commit step instead).
|
|
80
|
-
*/
|
|
81
|
-
function initFor(id, command = `npx harness hook ${id}`) {
|
|
82
|
-
const make = RUNNERS[id];
|
|
83
|
-
return make ? make(command) : null;
|
|
84
|
-
}
|
|
85
|
-
/** Write an {@link InitFile} under `root` (creates dirs; `chmod +x` when executable). */
|
|
86
|
-
function writeInitFile(root, file) {
|
|
87
|
-
const full = join(root, file.path);
|
|
88
|
-
mkdirSync(dirname(full), { recursive: true });
|
|
89
|
-
writeFileSync(full, file.content);
|
|
90
|
-
if (file.executable) chmodSync(full, 493);
|
|
91
|
-
return full;
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
|
-
export { codexInit as a, clineInit as i, writeInitFile as n, cursorInit as o, claudeInit as r, geminiInit as s, initFor as t };
|