@fusengine/harness 0.1.0 → 0.1.2
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/claude/index.d.mts +1 -35
- package/dist/adapters/claude/index.mjs +1 -65
- package/dist/adapters/cline/index.d.mts +23 -0
- package/dist/adapters/cline/index.mjs +2 -0
- package/dist/adapters/codex/index.d.mts +2 -0
- package/dist/adapters/codex/index.mjs +2 -0
- package/dist/adapters/cursor/index.d.mts +31 -0
- package/dist/adapters/cursor/index.mjs +2 -0
- package/dist/adapters/gemini/index.d.mts +23 -0
- package/dist/adapters/gemini/index.mjs +2 -0
- package/dist/cache/index.d.mts +1 -29
- package/dist/cache/index.mjs +1 -109
- package/dist/cache-DbPSJ9bC.mjs +110 -0
- package/dist/claude-DbzjbxmO.mjs +66 -0
- package/dist/cli/bin.d.mts +1 -0
- package/dist/cli/bin.mjs +92 -0
- package/dist/cli/index.d.mts +12 -0
- package/dist/cli/index.mjs +2 -0
- package/dist/cline-BxslHtBG.mjs +25 -0
- package/dist/config/index.d.mts +1 -36
- package/dist/config/index.mjs +1 -18
- package/dist/config-BG55s6HZ.mjs +20 -0
- package/dist/cursor-Bh7eh9y_.mjs +37 -0
- package/dist/detect/index.d.mts +1 -30
- package/dist/detect/index.mjs +2 -81
- package/dist/detect-la_KkjCS.mjs +1 -0
- package/dist/freshness/index.d.mts +1 -12
- package/dist/freshness/index.mjs +1 -63
- package/dist/freshness-DzGoBKKj.mjs +64 -0
- package/dist/gemini-SrK_fFAr.mjs +25 -0
- package/dist/harness-C8Nxxyn_.mjs +83 -0
- package/dist/harness-DwJskkz_.d.mts +31 -0
- package/dist/index-B-z0CCiU.d.mts +37 -0
- package/dist/index-B3Ve_bBu.d.mts +30 -0
- package/dist/index-BEM-mQMC.d.mts +30 -0
- package/dist/index-BKZ67WMa.d.mts +1 -0
- package/dist/index-BOBXQ91y.d.mts +12 -0
- package/dist/index-BVVgDSdq.d.mts +1 -0
- package/dist/index-BWK8slRi.d.mts +84 -0
- package/dist/index-C1vLIMwN.d.mts +20 -0
- package/dist/index-CPoF_hLP.d.mts +59 -0
- package/dist/index-D7MkQNdc.d.mts +80 -0
- package/dist/index-DqNKW8Ki.d.mts +44 -0
- package/dist/index-FrWgmkbP.d.mts +36 -0
- package/dist/index.d.mts +10 -10
- package/dist/index.mjs +9 -11
- package/dist/init/index.d.mts +34 -0
- package/dist/init/index.mjs +2 -0
- package/dist/memory/index.d.mts +1 -29
- package/dist/memory/index.mjs +1 -93
- package/dist/memory-BVNt4Ary.mjs +94 -0
- package/dist/policy/index.d.mts +1 -79
- package/dist/policy/index.mjs +1 -32
- package/dist/policy-C_pXmeNB.mjs +33 -0
- package/dist/prompt/index.mjs +1 -0
- package/dist/prompt-la_KkjCS.mjs +1 -0
- package/dist/refs/index.d.mts +1 -43
- package/dist/refs/index.mjs +1 -69
- package/dist/refs-Dj3AfgBE.mjs +70 -0
- package/dist/run-Cc98348q.mjs +94 -0
- package/dist/run-h_2LNEA8.mjs +32 -0
- package/dist/state/index.d.mts +1 -58
- package/dist/state/index.mjs +1 -103
- package/dist/state-Bc4wdnCG.mjs +104 -0
- package/dist/statusline/index.d.mts +1 -83
- package/dist/statusline/index.mjs +1 -147
- package/dist/statusline-D87eUNXl.mjs +148 -0
- package/dist/util/index.d.mts +1 -19
- package/dist/util/index.mjs +1 -0
- package/dist/util-la_KkjCS.mjs +1 -0
- package/package.json +13 -4
|
@@ -1,36 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
|
|
3
|
-
//#region src/adapters/claude/index.d.ts
|
|
4
|
-
/** Subset of the Claude Code hook stdin payload we consume. */
|
|
5
|
-
interface ClaudeHookInput {
|
|
6
|
-
hook_event_name?: string;
|
|
7
|
-
tool_name?: string;
|
|
8
|
-
tool_input?: {
|
|
9
|
-
file_path?: string;
|
|
10
|
-
content?: string;
|
|
11
|
-
new_string?: string;
|
|
12
|
-
command?: string;
|
|
13
|
-
};
|
|
14
|
-
cwd?: string;
|
|
15
|
-
}
|
|
16
|
-
/** Read & parse the Claude hook payload from stdin (empty object on bad input). */
|
|
17
|
-
declare function readClaudeInput(): Promise<ClaudeHookInput>;
|
|
18
|
-
/** A `deny` hook response for a given event. */
|
|
19
|
-
declare function denyResponse(event: string, reason: string): string;
|
|
20
|
-
/** An `additionalContext` injection response. */
|
|
21
|
-
declare function contextResponse(event: string, text: string): string;
|
|
22
|
-
/**
|
|
23
|
-
* Render a portable {@link Prompt} as a Claude Code hook response:
|
|
24
|
-
* `block` → `permissionDecision: deny`, `ask` → `permissionDecision: ask`
|
|
25
|
-
* (interactive confirm), `inform` → `additionalContext`.
|
|
26
|
-
*/
|
|
27
|
-
declare function toClaudeResponse(event: string, prompt: Prompt): string;
|
|
28
|
-
/**
|
|
29
|
-
* Run the bundled policy over a Claude payload and return the native response
|
|
30
|
-
* string (deny/ask/additionalContext), or null to allow.
|
|
31
|
-
*/
|
|
32
|
-
declare function guard(input: ClaudeHookInput): string | null;
|
|
33
|
-
/** @deprecated use {@link guard}. Kept for back-compat. */
|
|
34
|
-
declare const fileSizeGuard: typeof guard;
|
|
35
|
-
//#endregion
|
|
1
|
+
import { a as guard, i as fileSizeGuard, n as contextResponse, o as readClaudeInput, r as denyResponse, s as toClaudeResponse, t as ClaudeHookInput } from "../../index-FrWgmkbP.mjs";
|
|
36
2
|
export { ClaudeHookInput, contextResponse, denyResponse, fileSizeGuard, guard, readClaudeInput, toClaudeResponse };
|
|
@@ -1,66 +1,2 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
|
|
3
|
-
//#region src/adapters/claude/index.ts
|
|
4
|
-
/**
|
|
5
|
-
* Claude Code adapter — the thin Claude-only shim over the portable policy core.
|
|
6
|
-
* Reads the hook stdin payload and emits hookSpecificOutput responses.
|
|
7
|
-
*/
|
|
8
|
-
/** Read & parse the Claude hook payload from stdin (empty object on bad input). */
|
|
9
|
-
async function readClaudeInput() {
|
|
10
|
-
const text = await Bun.stdin.text();
|
|
11
|
-
if (!text.trim()) return {};
|
|
12
|
-
try {
|
|
13
|
-
const parsed = JSON.parse(text);
|
|
14
|
-
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
15
|
-
} catch {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
/** A `deny` hook response for a given event. */
|
|
20
|
-
function denyResponse(event, reason) {
|
|
21
|
-
return JSON.stringify({ hookSpecificOutput: {
|
|
22
|
-
hookEventName: event,
|
|
23
|
-
permissionDecision: "deny",
|
|
24
|
-
permissionDecisionReason: reason
|
|
25
|
-
} });
|
|
26
|
-
}
|
|
27
|
-
/** An `additionalContext` injection response. */
|
|
28
|
-
function contextResponse(event, text) {
|
|
29
|
-
return JSON.stringify({ hookSpecificOutput: {
|
|
30
|
-
hookEventName: event,
|
|
31
|
-
additionalContext: text
|
|
32
|
-
} });
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Render a portable {@link Prompt} as a Claude Code hook response:
|
|
36
|
-
* `block` → `permissionDecision: deny`, `ask` → `permissionDecision: ask`
|
|
37
|
-
* (interactive confirm), `inform` → `additionalContext`.
|
|
38
|
-
*/
|
|
39
|
-
function toClaudeResponse(event, prompt) {
|
|
40
|
-
const reason = formatPrompt(prompt);
|
|
41
|
-
if (prompt.kind === "block") return denyResponse(event, reason);
|
|
42
|
-
if (prompt.kind === "ask") return JSON.stringify({ hookSpecificOutput: {
|
|
43
|
-
hookEventName: event,
|
|
44
|
-
permissionDecision: "ask",
|
|
45
|
-
permissionDecisionReason: reason
|
|
46
|
-
} });
|
|
47
|
-
return contextResponse(event, reason);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Run the bundled policy over a Claude payload and return the native response
|
|
51
|
-
* string (deny/ask/additionalContext), or null to allow.
|
|
52
|
-
*/
|
|
53
|
-
function guard(input) {
|
|
54
|
-
const result = evaluate({
|
|
55
|
-
tool: input.tool_name ?? "Write",
|
|
56
|
-
filePath: input.tool_input?.file_path,
|
|
57
|
-
content: input.tool_input?.content ?? input.tool_input?.new_string,
|
|
58
|
-
command: input.tool_input?.command
|
|
59
|
-
});
|
|
60
|
-
if (result.decision === "allow" || !result.prompt) return null;
|
|
61
|
-
return toClaudeResponse(input.hook_event_name ?? "PreToolUse", result.prompt);
|
|
62
|
-
}
|
|
63
|
-
/** @deprecated use {@link guard}. Kept for back-compat. */
|
|
64
|
-
const fileSizeGuard = guard;
|
|
65
|
-
//#endregion
|
|
1
|
+
import { a as readClaudeInput, i as guard, n as denyResponse, o as toClaudeResponse, r as fileSizeGuard, t as contextResponse } from "../../claude-DbzjbxmO.mjs";
|
|
66
2
|
export { contextResponse, denyResponse, fileSizeGuard, guard, readClaudeInput, toClaudeResponse };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/adapters/cline/index.d.ts
|
|
2
|
+
/** `PreToolUse` stdin payload (subset). */
|
|
3
|
+
interface ClineHookInput {
|
|
4
|
+
hookName?: string;
|
|
5
|
+
preToolUse?: {
|
|
6
|
+
toolName?: string;
|
|
7
|
+
parameters?: {
|
|
8
|
+
path?: string;
|
|
9
|
+
content?: string;
|
|
10
|
+
command?: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Hook stdout response. */
|
|
15
|
+
interface ClineResponse {
|
|
16
|
+
cancel?: boolean;
|
|
17
|
+
errorMessage?: string;
|
|
18
|
+
contextModification?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Evaluate a tool use; cancel on a hard block, otherwise inject context. */
|
|
21
|
+
declare function preToolUse(input: ClineHookInput): ClineResponse;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ClineHookInput, ClineResponse, preToolUse };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/adapters/cursor/index.d.ts
|
|
2
|
+
/** `beforeShellExecution` stdin payload (subset). */
|
|
3
|
+
interface CursorShellPayload {
|
|
4
|
+
command?: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
workspace_roots?: string[];
|
|
7
|
+
hook_event_name?: string;
|
|
8
|
+
}
|
|
9
|
+
/** `afterFileEdit` stdin payload (subset). */
|
|
10
|
+
interface CursorEditPayload {
|
|
11
|
+
file_path?: string;
|
|
12
|
+
edits?: {
|
|
13
|
+
old_string: string;
|
|
14
|
+
new_string: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
/** `beforeShellExecution` stdout response. */
|
|
18
|
+
interface CursorResponse {
|
|
19
|
+
permission: "allow" | "deny" | "ask";
|
|
20
|
+
continue?: boolean;
|
|
21
|
+
userMessage?: string;
|
|
22
|
+
agentMessage?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Guard a shell command (git/install policies). */
|
|
25
|
+
declare function beforeShellExecution(payload: CursorShellPayload): CursorResponse;
|
|
26
|
+
/** Observe a file edit (Cursor cannot block here). Returns the verdict for logging. */
|
|
27
|
+
declare function afterFileEdit(payload: CursorEditPayload): {
|
|
28
|
+
violation: string | null;
|
|
29
|
+
};
|
|
30
|
+
//#endregion
|
|
31
|
+
export { CursorEditPayload, CursorResponse, CursorShellPayload, afterFileEdit, beforeShellExecution };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/adapters/gemini/index.d.ts
|
|
2
|
+
/** `BeforeTool` stdin payload (subset). */
|
|
3
|
+
interface GeminiHookInput {
|
|
4
|
+
tool_name?: string;
|
|
5
|
+
tool_input?: {
|
|
6
|
+
command?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
content?: string;
|
|
9
|
+
};
|
|
10
|
+
hook_event_name?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Hook stdout response. */
|
|
13
|
+
interface GeminiResponse {
|
|
14
|
+
decision?: "allow" | "deny";
|
|
15
|
+
reason?: string;
|
|
16
|
+
hookSpecificOutput?: {
|
|
17
|
+
additionalContext?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Evaluate a tool use; deny on a hard block, otherwise inject context. */
|
|
21
|
+
declare function beforeTool(input: GeminiHookInput): GeminiResponse;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { GeminiHookInput, GeminiResponse, beforeTool };
|
package/dist/cache/index.d.mts
CHANGED
|
@@ -1,30 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
/** Strip HTML entities + boilerplate, normalize blank lines, truncate to ~5KB. */
|
|
3
|
-
declare function compactMarkdown(content: string): string;
|
|
4
|
-
/** 8-char MD5 of `${toolName}::${query}`. */
|
|
5
|
-
declare function queryHash(toolName: string, query: string): string;
|
|
6
|
-
/** Bag-of-words Jaccard similarity strictly greater than `threshold`. */
|
|
7
|
-
declare function jaccardSimilar(a: string, b: string, threshold?: number): boolean;
|
|
8
|
-
//#endregion
|
|
9
|
-
//#region src/cache/io.d.ts
|
|
10
|
-
/** Read a JSON array from `path`; [] on missing/corrupt/non-array. */
|
|
11
|
-
declare function loadIndex(path: string): unknown[];
|
|
12
|
-
/** Summary of a cache index. */
|
|
13
|
-
interface IndexSummary {
|
|
14
|
-
total: number;
|
|
15
|
-
byTool: Record<string, number>;
|
|
16
|
-
oldestTs: string | null;
|
|
17
|
-
newestTs: string | null;
|
|
18
|
-
}
|
|
19
|
-
/** Summarize an index of `{ tool?, ts? }` entries. */
|
|
20
|
-
declare function summarizeIndex(index: unknown[]): IndexSummary;
|
|
21
|
-
//#endregion
|
|
22
|
-
//#region src/cache/mcp-response.d.ts
|
|
23
|
-
/**
|
|
24
|
-
* Extract usable markdown from an MCP `tool_response`: a string, a list of
|
|
25
|
-
* content blocks (non-text blocks skipped), or any JSON structure (fallback).
|
|
26
|
-
* Recurses up to depth 5 to guard against pathological/cyclic structures.
|
|
27
|
-
*/
|
|
28
|
-
declare function extractText(resp: unknown, depth?: number): string;
|
|
29
|
-
//#endregion
|
|
1
|
+
import { a as compactMarkdown, i as summarizeIndex, n as IndexSummary, o as jaccardSimilar, r as loadIndex, s as queryHash, t as extractText } from "../index-B3Ve_bBu.mjs";
|
|
30
2
|
export { IndexSummary, compactMarkdown, extractText, jaccardSimilar, loadIndex, queryHash, summarizeIndex };
|
package/dist/cache/index.mjs
CHANGED
|
@@ -1,110 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
//#region src/cache/compact.ts
|
|
4
|
-
const HTML_ENTITIES = {
|
|
5
|
-
"&": "&",
|
|
6
|
-
"<": "<",
|
|
7
|
-
">": ">",
|
|
8
|
-
""": "\"",
|
|
9
|
-
"'": "'",
|
|
10
|
-
"'": "'",
|
|
11
|
-
" ": " ",
|
|
12
|
-
"'": "'"
|
|
13
|
-
};
|
|
14
|
-
const BOILERPLATE = [
|
|
15
|
-
/^.*cookie.*(accept|consent|banner).*$/gim,
|
|
16
|
-
/^.*was this (helpful|page helpful|article helpful).*$/gim,
|
|
17
|
-
/^.*©\s*\d{4}.*all rights reserved.*$/gim,
|
|
18
|
-
/^\s*(home|about|contact|privacy|terms)\s*\|\s*.*$/gim,
|
|
19
|
-
/^.*subscribe to (our )?newsletter.*$/gim,
|
|
20
|
-
/^.*follow us on (twitter|facebook|linkedin).*$/gim
|
|
21
|
-
];
|
|
22
|
-
const MAX_BYTES = 5 * 1024;
|
|
23
|
-
function decodeEntities(text) {
|
|
24
|
-
let out = text;
|
|
25
|
-
for (const [ent, ch] of Object.entries(HTML_ENTITIES)) out = out.split(ent).join(ch);
|
|
26
|
-
out = out.replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => String.fromCodePoint(parseInt(h, 16)));
|
|
27
|
-
out = out.replace(/&#(\d+);/g, (_m, d) => String.fromCodePoint(parseInt(d, 10)));
|
|
28
|
-
return out;
|
|
29
|
-
}
|
|
30
|
-
/** Strip HTML entities + boilerplate, normalize blank lines, truncate to ~5KB. */
|
|
31
|
-
function compactMarkdown(content) {
|
|
32
|
-
let text = decodeEntities(content);
|
|
33
|
-
for (const re of BOILERPLATE) text = text.replace(re, "");
|
|
34
|
-
text = text.replace(/\n{3,}/g, "\n\n").trim();
|
|
35
|
-
const enc = new TextEncoder().encode(text);
|
|
36
|
-
if (enc.length > MAX_BYTES) {
|
|
37
|
-
const truncated = new TextDecoder().decode(enc.slice(0, MAX_BYTES));
|
|
38
|
-
text = `${truncated}\n\n[... truncated, ${text.slice(truncated.length).split("\n").length - 1} lines]`;
|
|
39
|
-
}
|
|
40
|
-
return text;
|
|
41
|
-
}
|
|
42
|
-
/** 8-char MD5 of `${toolName}::${query}`. */
|
|
43
|
-
function queryHash(toolName, query) {
|
|
44
|
-
return createHash("md5").update(`${toolName}::${query}`).digest("hex").slice(0, 8);
|
|
45
|
-
}
|
|
46
|
-
/** Bag-of-words Jaccard similarity strictly greater than `threshold`. */
|
|
47
|
-
function jaccardSimilar(a, b, threshold = .8) {
|
|
48
|
-
const ta = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
|
|
49
|
-
const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
|
|
50
|
-
if (ta.size === 0 || tb.size === 0) return false;
|
|
51
|
-
let inter = 0;
|
|
52
|
-
for (const t of ta) if (tb.has(t)) inter++;
|
|
53
|
-
const union = ta.size + tb.size - inter;
|
|
54
|
-
return inter / union > threshold;
|
|
55
|
-
}
|
|
56
|
-
//#endregion
|
|
57
|
-
//#region src/cache/io.ts
|
|
58
|
-
/** Read a JSON array from `path`; [] on missing/corrupt/non-array. */
|
|
59
|
-
function loadIndex(path) {
|
|
60
|
-
try {
|
|
61
|
-
if (!existsSync(path)) return [];
|
|
62
|
-
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
63
|
-
return Array.isArray(data) ? data : [];
|
|
64
|
-
} catch {
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/** Summarize an index of `{ tool?, ts? }` entries. */
|
|
69
|
-
function summarizeIndex(index) {
|
|
70
|
-
const byTool = {};
|
|
71
|
-
const timestamps = [];
|
|
72
|
-
for (const entry of index) {
|
|
73
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
74
|
-
const e = entry;
|
|
75
|
-
if (typeof e.tool === "string") byTool[e.tool] = (byTool[e.tool] ?? 0) + 1;
|
|
76
|
-
if (typeof e.ts === "string") timestamps.push(e.ts);
|
|
77
|
-
}
|
|
78
|
-
return {
|
|
79
|
-
total: index.length,
|
|
80
|
-
byTool,
|
|
81
|
-
oldestTs: timestamps.length ? timestamps.reduce((a, b) => a < b ? a : b) : null,
|
|
82
|
-
newestTs: timestamps.length ? timestamps.reduce((a, b) => a > b ? a : b) : null
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
//#endregion
|
|
86
|
-
//#region src/cache/mcp-response.ts
|
|
87
|
-
const MAX_DEPTH = 5;
|
|
88
|
-
/**
|
|
89
|
-
* Extract usable markdown from an MCP `tool_response`: a string, a list of
|
|
90
|
-
* content blocks (non-text blocks skipped), or any JSON structure (fallback).
|
|
91
|
-
* Recurses up to depth 5 to guard against pathological/cyclic structures.
|
|
92
|
-
*/
|
|
93
|
-
function extractText(resp, depth = 0) {
|
|
94
|
-
if (depth >= MAX_DEPTH) return "";
|
|
95
|
-
if (typeof resp === "string") return resp;
|
|
96
|
-
if (Array.isArray(resp)) {
|
|
97
|
-
const parts = resp.filter((b) => typeof b === "object" && b !== null).filter((b) => b.type === "text").map((b) => b.text ?? "");
|
|
98
|
-
if (parts.length) return parts.join("\n\n");
|
|
99
|
-
const joined = resp.filter((b) => Array.isArray(b) || typeof b === "object" && b !== null).map((b) => extractText(b, depth + 1)).filter(Boolean).join("\n\n");
|
|
100
|
-
if (joined) return joined;
|
|
101
|
-
}
|
|
102
|
-
if (!resp) return "";
|
|
103
|
-
try {
|
|
104
|
-
return JSON.stringify(resp);
|
|
105
|
-
} catch {
|
|
106
|
-
return "";
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
//#endregion
|
|
1
|
+
import { a as jaccardSimilar, i as compactMarkdown, n as loadIndex, o as queryHash, r as summarizeIndex, t as extractText } from "../cache-DbPSJ9bC.mjs";
|
|
110
2
|
export { compactMarkdown, extractText, jaccardSimilar, loadIndex, queryHash, summarizeIndex };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
//#region src/cache/compact.ts
|
|
4
|
+
const HTML_ENTITIES = {
|
|
5
|
+
"&": "&",
|
|
6
|
+
"<": "<",
|
|
7
|
+
">": ">",
|
|
8
|
+
""": "\"",
|
|
9
|
+
"'": "'",
|
|
10
|
+
"'": "'",
|
|
11
|
+
" ": " ",
|
|
12
|
+
"'": "'"
|
|
13
|
+
};
|
|
14
|
+
const BOILERPLATE = [
|
|
15
|
+
/^.*cookie.*(accept|consent|banner).*$/gim,
|
|
16
|
+
/^.*was this (helpful|page helpful|article helpful).*$/gim,
|
|
17
|
+
/^.*©\s*\d{4}.*all rights reserved.*$/gim,
|
|
18
|
+
/^\s*(home|about|contact|privacy|terms)\s*\|\s*.*$/gim,
|
|
19
|
+
/^.*subscribe to (our )?newsletter.*$/gim,
|
|
20
|
+
/^.*follow us on (twitter|facebook|linkedin).*$/gim
|
|
21
|
+
];
|
|
22
|
+
const MAX_BYTES = 5 * 1024;
|
|
23
|
+
function decodeEntities(text) {
|
|
24
|
+
let out = text;
|
|
25
|
+
for (const [ent, ch] of Object.entries(HTML_ENTITIES)) out = out.split(ent).join(ch);
|
|
26
|
+
out = out.replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => String.fromCodePoint(parseInt(h, 16)));
|
|
27
|
+
out = out.replace(/&#(\d+);/g, (_m, d) => String.fromCodePoint(parseInt(d, 10)));
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/** Strip HTML entities + boilerplate, normalize blank lines, truncate to ~5KB. */
|
|
31
|
+
function compactMarkdown(content) {
|
|
32
|
+
let text = decodeEntities(content);
|
|
33
|
+
for (const re of BOILERPLATE) text = text.replace(re, "");
|
|
34
|
+
text = text.replace(/\n{3,}/g, "\n\n").trim();
|
|
35
|
+
const enc = new TextEncoder().encode(text);
|
|
36
|
+
if (enc.length > MAX_BYTES) {
|
|
37
|
+
const truncated = new TextDecoder().decode(enc.slice(0, MAX_BYTES));
|
|
38
|
+
text = `${truncated}\n\n[... truncated, ${text.slice(truncated.length).split("\n").length - 1} lines]`;
|
|
39
|
+
}
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
/** 8-char MD5 of `${toolName}::${query}`. */
|
|
43
|
+
function queryHash(toolName, query) {
|
|
44
|
+
return createHash("md5").update(`${toolName}::${query}`).digest("hex").slice(0, 8);
|
|
45
|
+
}
|
|
46
|
+
/** Bag-of-words Jaccard similarity strictly greater than `threshold`. */
|
|
47
|
+
function jaccardSimilar(a, b, threshold = .8) {
|
|
48
|
+
const ta = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
|
|
49
|
+
const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
|
|
50
|
+
if (ta.size === 0 || tb.size === 0) return false;
|
|
51
|
+
let inter = 0;
|
|
52
|
+
for (const t of ta) if (tb.has(t)) inter++;
|
|
53
|
+
const union = ta.size + tb.size - inter;
|
|
54
|
+
return inter / union > threshold;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/cache/io.ts
|
|
58
|
+
/** Read a JSON array from `path`; [] on missing/corrupt/non-array. */
|
|
59
|
+
function loadIndex(path) {
|
|
60
|
+
try {
|
|
61
|
+
if (!existsSync(path)) return [];
|
|
62
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
63
|
+
return Array.isArray(data) ? data : [];
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Summarize an index of `{ tool?, ts? }` entries. */
|
|
69
|
+
function summarizeIndex(index) {
|
|
70
|
+
const byTool = {};
|
|
71
|
+
const timestamps = [];
|
|
72
|
+
for (const entry of index) {
|
|
73
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
74
|
+
const e = entry;
|
|
75
|
+
if (typeof e.tool === "string") byTool[e.tool] = (byTool[e.tool] ?? 0) + 1;
|
|
76
|
+
if (typeof e.ts === "string") timestamps.push(e.ts);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
total: index.length,
|
|
80
|
+
byTool,
|
|
81
|
+
oldestTs: timestamps.length ? timestamps.reduce((a, b) => a < b ? a : b) : null,
|
|
82
|
+
newestTs: timestamps.length ? timestamps.reduce((a, b) => a > b ? a : b) : null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/cache/mcp-response.ts
|
|
87
|
+
const MAX_DEPTH = 5;
|
|
88
|
+
/**
|
|
89
|
+
* Extract usable markdown from an MCP `tool_response`: a string, a list of
|
|
90
|
+
* content blocks (non-text blocks skipped), or any JSON structure (fallback).
|
|
91
|
+
* Recurses up to depth 5 to guard against pathological/cyclic structures.
|
|
92
|
+
*/
|
|
93
|
+
function extractText(resp, depth = 0) {
|
|
94
|
+
if (depth >= MAX_DEPTH) return "";
|
|
95
|
+
if (typeof resp === "string") return resp;
|
|
96
|
+
if (Array.isArray(resp)) {
|
|
97
|
+
const parts = resp.filter((b) => typeof b === "object" && b !== null).filter((b) => b.type === "text").map((b) => b.text ?? "");
|
|
98
|
+
if (parts.length) return parts.join("\n\n");
|
|
99
|
+
const joined = resp.filter((b) => Array.isArray(b) || typeof b === "object" && b !== null).map((b) => extractText(b, depth + 1)).filter(Boolean).join("\n\n");
|
|
100
|
+
if (joined) return joined;
|
|
101
|
+
}
|
|
102
|
+
if (!resp) return "";
|
|
103
|
+
try {
|
|
104
|
+
return JSON.stringify(resp);
|
|
105
|
+
} catch {
|
|
106
|
+
return "";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
export { jaccardSimilar as a, compactMarkdown as i, loadIndex as n, queryHash as o, summarizeIndex as r, extractText as t };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { t as evaluate } from "./evaluate-CsYyUucy.mjs";
|
|
2
|
+
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
|
+
//#region src/adapters/claude/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Claude Code adapter — the thin Claude-only shim over the portable policy core.
|
|
6
|
+
* Reads the hook stdin payload and emits hookSpecificOutput responses.
|
|
7
|
+
*/
|
|
8
|
+
/** Read & parse the Claude hook payload from stdin (empty object on bad input). */
|
|
9
|
+
async function readClaudeInput() {
|
|
10
|
+
const text = await Bun.stdin.text();
|
|
11
|
+
if (!text.trim()) return {};
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(text);
|
|
14
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** A `deny` hook response for a given event. */
|
|
20
|
+
function denyResponse(event, reason) {
|
|
21
|
+
return JSON.stringify({ hookSpecificOutput: {
|
|
22
|
+
hookEventName: event,
|
|
23
|
+
permissionDecision: "deny",
|
|
24
|
+
permissionDecisionReason: reason
|
|
25
|
+
} });
|
|
26
|
+
}
|
|
27
|
+
/** An `additionalContext` injection response. */
|
|
28
|
+
function contextResponse(event, text) {
|
|
29
|
+
return JSON.stringify({ hookSpecificOutput: {
|
|
30
|
+
hookEventName: event,
|
|
31
|
+
additionalContext: text
|
|
32
|
+
} });
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Render a portable {@link Prompt} as a Claude Code hook response:
|
|
36
|
+
* `block` → `permissionDecision: deny`, `ask` → `permissionDecision: ask`
|
|
37
|
+
* (interactive confirm), `inform` → `additionalContext`.
|
|
38
|
+
*/
|
|
39
|
+
function toClaudeResponse(event, prompt) {
|
|
40
|
+
const reason = formatPrompt(prompt);
|
|
41
|
+
if (prompt.kind === "block") return denyResponse(event, reason);
|
|
42
|
+
if (prompt.kind === "ask") return JSON.stringify({ hookSpecificOutput: {
|
|
43
|
+
hookEventName: event,
|
|
44
|
+
permissionDecision: "ask",
|
|
45
|
+
permissionDecisionReason: reason
|
|
46
|
+
} });
|
|
47
|
+
return contextResponse(event, reason);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Run the bundled policy over a Claude payload and return the native response
|
|
51
|
+
* string (deny/ask/additionalContext), or null to allow.
|
|
52
|
+
*/
|
|
53
|
+
function guard(input) {
|
|
54
|
+
const result = evaluate({
|
|
55
|
+
tool: input.tool_name ?? "Write",
|
|
56
|
+
filePath: input.tool_input?.file_path,
|
|
57
|
+
content: input.tool_input?.content ?? input.tool_input?.new_string,
|
|
58
|
+
command: input.tool_input?.command
|
|
59
|
+
});
|
|
60
|
+
if (result.decision === "allow" || !result.prompt) return null;
|
|
61
|
+
return toClaudeResponse(input.hook_event_name ?? "PreToolUse", result.prompt);
|
|
62
|
+
}
|
|
63
|
+
/** @deprecated use {@link guard}. Kept for back-compat. */
|
|
64
|
+
const fileSizeGuard = guard;
|
|
65
|
+
//#endregion
|
|
66
|
+
export { readClaudeInput as a, guard as i, denyResponse as n, toClaudeResponse as o, fileSizeGuard as r, contextResponse as t };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli/bin.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as detectHarness } from "../harness-C8Nxxyn_.mjs";
|
|
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-Cc98348q.mjs";
|
|
5
|
+
import { i as guard } from "../claude-DbzjbxmO.mjs";
|
|
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
|
|
48
|
+
//#region src/cli/bin.ts
|
|
49
|
+
/**
|
|
50
|
+
* harness — CLI for @fusengine/harness.
|
|
51
|
+
* harness check cli-mode: check staged files (pre-commit), exit non-zero on a violation
|
|
52
|
+
* harness init [id] write the wiring file for a harness (defaults to the detected one)
|
|
53
|
+
* harness hook <id> runtime: read a hook payload on stdin, route to the adapter, print the response
|
|
54
|
+
*/
|
|
55
|
+
async function readStdin() {
|
|
56
|
+
const chunks = [];
|
|
57
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
58
|
+
const text = Buffer.concat(chunks).toString("utf8").trim();
|
|
59
|
+
if (!text) return {};
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(text);
|
|
62
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
63
|
+
} catch {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const cmd = process.argv[2];
|
|
68
|
+
if (cmd === "hook") {
|
|
69
|
+
const outcome = dispatchHook(process.argv[3] ?? detectHarness().id, await readStdin());
|
|
70
|
+
if (outcome.stdout) process.stdout.write(outcome.stdout);
|
|
71
|
+
process.exit(outcome.exit);
|
|
72
|
+
} else if (cmd === "init") {
|
|
73
|
+
const id = process.argv[3] ?? detectHarness().id;
|
|
74
|
+
const file = initFor(id);
|
|
75
|
+
if (!file) {
|
|
76
|
+
process.stderr.write(`harness: no hook integration for "${id}" — use \`harness check\` in a pre-commit step\n`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write(`harness: wired ${id} -> ${writeInitFile(process.cwd(), file)}\n`);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
} else {
|
|
82
|
+
const files = stagedFiles();
|
|
83
|
+
if (files.length === 0) process.exit(0);
|
|
84
|
+
const violations = checkStaged(files, stagedContent);
|
|
85
|
+
if (violations.length > 0) {
|
|
86
|
+
process.stderr.write(`harness check: policy violations\n\n${violations.join("\n\n")}\n`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
//#endregion
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/cli/run.d.ts
|
|
2
|
+
/** Staged files (Added/Copied/Modified/Renamed). Uses `node:child_process` (Bun shell can hang on `git show`). */
|
|
3
|
+
declare function stagedFiles(): string[];
|
|
4
|
+
/** Read a file's staged (index) content — not the working-tree version. */
|
|
5
|
+
declare function stagedContent(path: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Evaluate staged code files against the policy core; return violation blocks.
|
|
8
|
+
* `read` is injected (the real impl is {@link stagedContent}) so this is pure + testable.
|
|
9
|
+
*/
|
|
10
|
+
declare function checkStaged(files: string[], read: (path: string) => string): string[];
|
|
11
|
+
//#endregion
|
|
12
|
+
export { checkStaged, stagedContent, stagedFiles };
|
|
@@ -0,0 +1,25 @@
|
|
|
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 };
|