@fusengine/harness 0.1.39 → 0.1.41
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/README.md +7 -4
- package/dist/adapters/claude/index.d.mts +2 -2
- package/dist/adapters/claude/index.mjs +2 -2
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.d.mts +1 -1
- package/dist/adapters/codex/index.mjs +1 -1
- package/dist/adapters/cursor/index.mjs +1 -1
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/cache/index.d.mts +2 -2
- package/dist/cache/index.mjs +3 -3
- package/dist/cache-la_KkjCS.mjs +1 -0
- package/dist/{claude-ZgTC5yDS.mjs → claude-BnFrSfMo.mjs} +17 -3
- package/dist/cli/bin.mjs +70 -6
- package/dist/cli/index.mjs +1 -1
- package/dist/config/index.d.mts +2 -2
- package/dist/config/index.mjs +3 -2
- package/dist/{doc-helpers-Dd_x1-tZ.mjs → doc-helpers-BhzDmJ18.mjs} +8 -6
- package/dist/{doc-helpers-CG1nuf-c.d.mts → doc-helpers-D14nkD5D.d.mts} +5 -3
- package/dist/{dotenv-TLNMiSjP.mjs → dotenv-DGyLln7U.mjs} +1 -20
- package/dist/{evaluate-za4vT-p7.mjs → evaluate-B1n1ti0N.mjs} +89 -49
- package/dist/freshness/index.d.mts +1 -1
- package/dist/freshness/index.mjs +1 -1
- package/dist/{handle-t-lQuQc7.mjs → handle-VhWQxvyN.mjs} +612 -163
- package/dist/index-BX-xcvhY.d.mts +75 -0
- package/dist/{index-DLUfyYmU.d.mts → index-COBvvc3L.d.mts} +3 -1
- package/dist/{index-FrWgmkbP.d.mts → index-CwOdFBOr.d.mts} +11 -2
- package/dist/{index-CPoF_hLP.d.mts → index-D7GpOmkl.d.mts} +1 -1
- package/dist/{index-QzK2dv0V.d.mts → index-DTrjSNmI.d.mts} +38 -13
- package/dist/{index-DL8MxjuP.d.mts → index-mmRF3KNp.d.mts} +4 -4
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +9 -9
- package/dist/{loader-CyAoJv2W.mjs → loader-Bn-DbZmt.mjs} +1 -1
- package/dist/mcp-store-CDUVqtJ0.mjs +238 -0
- package/dist/policy/index.d.mts +2 -2
- package/dist/policy/index.mjs +3 -3
- package/dist/refs/index.d.mts +1 -1
- package/dist/refs/index.mjs +2 -2
- package/dist/{router-D8cVrI-s.mjs → router-BfX0hJg8.mjs} +13 -20
- package/dist/{run-DOARDruz.mjs → run-D-Ydrw3D.mjs} +1 -1
- package/dist/runtime/index.d.mts +33 -13
- package/dist/runtime/index.mjs +2 -2
- package/dist/{session-state-DxNkIBZ2.d.mts → session-state-Dzq6yrw7.d.mts} +1 -1
- package/dist/state/index.d.mts +1 -1
- package/dist/{store-7j02oGjt.mjs → store-3WBr37Xz.mjs} +1 -1
- package/dist/tracking/index.d.mts +1 -1
- package/dist/tracking/index.mjs +1 -1
- package/dist/ttl-BG55s6HZ.mjs +20 -0
- package/dist/{validate-osn-huLZ.mjs → validate-B3wkqEoN.mjs} +15 -9
- package/package.json +1 -1
- package/dist/cache-C9z9LclL.mjs +0 -56
- package/dist/index-DPkCX_AR.d.mts +0 -40
- package/dist/store-PrNPm6So.mjs +0 -89
package/dist/policy/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as evaluateApex, C as MAX_EXA_RESULTS, D as APEX_GATES, E as detectCreationIntent, F as detectProjectType, I as isApexCommand, L as requiredArchSkill, M as solidReadGate, N as DEV_KEYWORDS, O as brainstormGate, P as detectModularArchitecture, S as frameworkSolidGate, T as capVerbosity, _ as buildClaudeMdContext, a as firstHeading, b as skillTriggerGate, c as parseBodyDesc, d as PROJECT_INDICATORS, f as buildApexTaskContext, g as buildApexInstruction, h as DEV_VERBS, i as firstComment, j as freshnessGate, k as docConsultedGate, l as parseField, m as loadApexTaskState, n as missingSeoElements, o as parseEnrichment, p as buildApexTaskInjection, r as descFromText, s as parseEntry, t as isHtmlLike, u as EXCLUDE_DIRS, v as detectClaudeMdProjectType, w as MAX_TOKENS, x as SKILL_TRIGGERS, y as detectRequiredSkills } from "../validate-
|
|
2
|
-
import { A as
|
|
1
|
+
import { A as evaluateApex, C as MAX_EXA_RESULTS, D as APEX_GATES, E as detectCreationIntent, F as detectProjectType, I as isApexCommand, L as requiredArchSkill, M as solidReadGate, N as DEV_KEYWORDS, O as brainstormGate, P as detectModularArchitecture, S as frameworkSolidGate, T as capVerbosity, _ as buildClaudeMdContext, a as firstHeading, b as skillTriggerGate, c as parseBodyDesc, d as PROJECT_INDICATORS, f as buildApexTaskContext, g as buildApexInstruction, h as DEV_VERBS, i as firstComment, j as freshnessGate, k as docConsultedGate, l as parseField, m as loadApexTaskState, n as missingSeoElements, o as parseEnrichment, p as buildApexTaskInjection, r as descFromText, s as parseEntry, t as isHtmlLike, u as EXCLUDE_DIRS, v as detectClaudeMdProjectType, w as MAX_TOKENS, x as SKILL_TRIGGERS, y as detectRequiredSkills } from "../validate-B3wkqEoN.mjs";
|
|
2
|
+
import { A as matchPatterns, C as ASK_PATTERNS, D as GIT_BLOCKED, E as GIT_ASK, M as evaluateFileSize, N as detectFramework, O as PROJECT_INSTALL, S as protectedPathGuard, T as securityGuard, _ as CODE_REDIRECT, a as registerGuard, b as PROTECTED_FRAGMENTS, c as GO_DECL_RE, d as PY_MODEL_RE, f as SWIFT_PROTO_RE, g as CODE_MUTATORS, h as ASK_WRITERS, i as clearUserGuards, j as countLines, k as SYSTEM_INSTALL, l as JAVA_DECL_RE, m as interfaceSeparationGuard, n as FAIL_CLOSED, o as runGuards, p as TS_DECL_RE, r as GUARDS, s as installGuard, t as evaluate, u as PHP_DECL_RE, v as FILE_REDIRECT, w as CRITICAL_PATTERNS, x as PROTECTED_GIT_RE, y as bashWriteGuard } from "../evaluate-B1n1ti0N.mjs";
|
|
3
3
|
import "../policy-la_KkjCS.mjs";
|
|
4
|
-
export { APEX_GATES, ASK_PATTERNS, ASK_WRITERS, CODE_MUTATORS, CODE_REDIRECT, CRITICAL_PATTERNS, DEV_KEYWORDS, DEV_VERBS, EXCLUDE_DIRS, FAIL_CLOSED, GIT_ASK, GIT_BLOCKED, GO_DECL_RE, GUARDS, JAVA_DECL_RE, MAX_EXA_RESULTS, MAX_TOKENS, PHP_DECL_RE, PROJECT_INDICATORS, PROJECT_INSTALL, PROTECTED_FRAGMENTS, PY_MODEL_RE, SKILL_TRIGGERS, SWIFT_PROTO_RE, SYSTEM_INSTALL, TS_DECL_RE, bashWriteGuard, brainstormGate, buildApexInstruction, buildApexTaskContext, buildApexTaskInjection, buildClaudeMdContext, capVerbosity, clearUserGuards, countLines, descFromText, detectClaudeMdProjectType, detectCreationIntent, detectFramework, detectModularArchitecture, detectProjectType, detectRequiredSkills, docConsultedGate, evaluate, evaluateApex, evaluateFileSize, firstComment, firstHeading, frameworkSolidGate, freshnessGate, installGuard, interfaceSeparationGuard, isApexCommand, isHtmlLike, loadApexTaskState, matchPatterns, missingSeoElements, parseBodyDesc, parseEnrichment, parseEntry, parseField, protectedPathGuard, registerGuard, requiredArchSkill, runGuards, securityGuard, skillTriggerGate, solidReadGate };
|
|
4
|
+
export { APEX_GATES, ASK_PATTERNS, ASK_WRITERS, CODE_MUTATORS, CODE_REDIRECT, CRITICAL_PATTERNS, DEV_KEYWORDS, DEV_VERBS, EXCLUDE_DIRS, FAIL_CLOSED, FILE_REDIRECT, GIT_ASK, GIT_BLOCKED, GO_DECL_RE, GUARDS, JAVA_DECL_RE, MAX_EXA_RESULTS, MAX_TOKENS, PHP_DECL_RE, PROJECT_INDICATORS, PROJECT_INSTALL, PROTECTED_FRAGMENTS, PROTECTED_GIT_RE, PY_MODEL_RE, SKILL_TRIGGERS, SWIFT_PROTO_RE, SYSTEM_INSTALL, TS_DECL_RE, bashWriteGuard, brainstormGate, buildApexInstruction, buildApexTaskContext, buildApexTaskInjection, buildClaudeMdContext, capVerbosity, clearUserGuards, countLines, descFromText, detectClaudeMdProjectType, detectCreationIntent, detectFramework, detectModularArchitecture, detectProjectType, detectRequiredSkills, docConsultedGate, evaluate, evaluateApex, evaluateFileSize, firstComment, firstHeading, frameworkSolidGate, freshnessGate, installGuard, interfaceSeparationGuard, isApexCommand, isHtmlLike, loadApexTaskState, matchPatterns, missingSeoElements, parseBodyDesc, parseEnrichment, parseEntry, parseField, protectedPathGuard, registerGuard, requiredArchSkill, runGuards, securityGuard, skillTriggerGate, solidReadGate };
|
package/dist/refs/index.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { n as RouteResult, r as ScoredRef, t as RefMeta } from "../types-CY5qT2X1.mjs";
|
|
2
|
-
import { a as globToRe, i as scoreReferences, n as toRefMeta, o as parseFrontmatter, r as routeReferences, t as loadRefs } from "../index-
|
|
2
|
+
import { a as globToRe, i as scoreReferences, n as toRefMeta, o as parseFrontmatter, r as routeReferences, t as loadRefs } from "../index-mmRF3KNp.mjs";
|
|
3
3
|
export { RefMeta, RouteResult, ScoredRef, globToRe, loadRefs, parseFrontmatter, routeReferences, scoreReferences, toRefMeta };
|
package/dist/refs/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as parseFrontmatter, n as scoreReferences, r as globToRe, t as routeReferences } from "../router-
|
|
2
|
-
import { n as toRefMeta, t as loadRefs } from "../loader-
|
|
1
|
+
import { i as parseFrontmatter, n as scoreReferences, r as globToRe, t as routeReferences } from "../router-BfX0hJg8.mjs";
|
|
2
|
+
import { n as toRefMeta, t as loadRefs } from "../loader-Bn-DbZmt.mjs";
|
|
3
3
|
import "../refs-la_KkjCS.mjs";
|
|
4
4
|
export { globToRe, loadRefs, parseFrontmatter, routeReferences, scoreReferences, toRefMeta };
|
|
@@ -17,32 +17,25 @@ function globToRe(g) {
|
|
|
17
17
|
}
|
|
18
18
|
//#endregion
|
|
19
19
|
//#region src/refs/router.ts
|
|
20
|
-
/** Literal (non-wildcard) path segments in a glob — more = more specific. */
|
|
21
|
-
function specificity(glob) {
|
|
22
|
-
return glob.split("/").filter((s) => s && s !== "**" && !s.includes("*")).length;
|
|
23
|
-
}
|
|
24
20
|
/**
|
|
25
|
-
* Score references against a file edit (pure)
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
21
|
+
* Score references against a file edit (pure) — flat scoring, parity with the
|
|
22
|
+
* Python `ref_router._score_ref` (comma-split, single award per category):
|
|
23
|
+
* +10 once if ANY `applies-to` glob matches, +5 once if ANY `trigger-on-edit`
|
|
24
|
+
* fragment (trailing `/` stripped) is a substring of the path, +1 per keyword.
|
|
29
25
|
*/
|
|
30
26
|
function scoreReferences(refs, filePath, content) {
|
|
31
27
|
const scored = [];
|
|
28
|
+
const hay = `${filePath} ${content}`.toLowerCase();
|
|
32
29
|
for (const meta of refs) {
|
|
33
30
|
let score = 0;
|
|
34
|
-
if (meta.appliesTo)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
for (const kw of meta.keywords.split(", ")) {
|
|
43
|
-
const k = kw.trim().toLowerCase();
|
|
44
|
-
if (k && hay.includes(k)) score += 1;
|
|
45
|
-
}
|
|
31
|
+
if (meta.appliesTo?.split(",").some((g) => g.trim() && globToRe(g.trim()).test(filePath))) score += 10;
|
|
32
|
+
if (meta.triggerOnEdit?.split(",").some((f) => {
|
|
33
|
+
const t = f.trim().replace(/\/+$/, "");
|
|
34
|
+
return t && filePath.includes(t);
|
|
35
|
+
})) score += 5;
|
|
36
|
+
if (meta.keywords) for (const kw of meta.keywords.split(",")) {
|
|
37
|
+
const k = kw.trim().toLowerCase();
|
|
38
|
+
if (k && hay.includes(k)) score += 1;
|
|
46
39
|
}
|
|
47
40
|
if (score > 0) scored.push({
|
|
48
41
|
meta,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
|
|
2
|
-
import { t as evaluate } from "./evaluate-
|
|
2
|
+
import { t as evaluate } from "./evaluate-B1n1ti0N.mjs";
|
|
3
3
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
5
|
//#region src/cli/run.ts
|
package/dist/runtime/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as Prompt } from "../types-D56jSgD9.mjs";
|
|
2
2
|
import { t as RefMeta } from "../types-CY5qT2X1.mjs";
|
|
3
|
-
import { t as AgentQuality } from "../session-state-
|
|
3
|
+
import { t as AgentQuality } from "../session-state-Dzq6yrw7.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/runtime/paths.d.ts
|
|
6
6
|
/**
|
|
@@ -73,10 +73,12 @@ interface ToolEvent {
|
|
|
73
73
|
* Map a live tool-use to the activity to record, or null when nothing is
|
|
74
74
|
* tracked. Works across harnesses — tool names are globally distinct:
|
|
75
75
|
* - MCP doc calls (`context7` / `exa`, any separator) → `doc`
|
|
76
|
-
* - `Task` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
|
|
76
|
+
* - `Task`/`Agent` + `subagent_type` (Claude/Cursor) → `agent` (bare agent name)
|
|
77
|
+
* - direct exploration/research (Glob/Grep, explore Bash, web, MCP cache reads)
|
|
78
|
+
* → `agent` credited to the matching REQUIRED_AGENTS phase
|
|
77
79
|
* - a read tool opening a `.md` reference → `ref`
|
|
78
80
|
*/
|
|
79
|
-
declare function activityFor(event: ToolEvent): Activity
|
|
81
|
+
declare function activityFor(event: ToolEvent): Activity[];
|
|
80
82
|
//#endregion
|
|
81
83
|
//#region src/runtime/gate-input.d.ts
|
|
82
84
|
/** A tool-use to gate, plus the session pointers needed for the stateful gates. */
|
|
@@ -162,13 +164,24 @@ declare function normalizeEvent(id: string, payload: Record<string, unknown>): N
|
|
|
162
164
|
*/
|
|
163
165
|
declare function respond(id: string, prompt: Prompt): string;
|
|
164
166
|
//#endregion
|
|
165
|
-
//#region src/runtime/mcp.d.ts
|
|
166
|
-
/**
|
|
167
|
+
//#region src/runtime/mcp-key.d.ts
|
|
168
|
+
/** Cache-key + tool-class helpers for the MCP/WebFetch interception pipeline. */
|
|
169
|
+
/** Default freshness for cached MCP doc results (48h). */
|
|
167
170
|
declare const MCP_TTL_MS = 1728e5;
|
|
171
|
+
/** WebFetch freshness — pages go stale faster than docs (24h, parity with Python). */
|
|
172
|
+
declare const WEBFETCH_TTL_MS = 864e5;
|
|
168
173
|
/** MCP doc tools + WebFetch whose calls are cached / verbosity-capped. */
|
|
169
174
|
declare function isMcpTool(tool: string): boolean;
|
|
170
175
|
/** The query/url that keys the cache. */
|
|
171
176
|
declare function queryOf(input: Record<string, unknown>): string;
|
|
177
|
+
/**
|
|
178
|
+
* The string that keys the cache for `tool`. WebFetch folds `url + "\n" +
|
|
179
|
+
* prompt[:500]` (parity with Python) so distinct prompts on the same URL never
|
|
180
|
+
* collide; every other tool keys on {@link queryOf}.
|
|
181
|
+
*/
|
|
182
|
+
declare function cacheQueryOf(tool: string, input: Record<string, unknown>): string;
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/runtime/mcp.d.ts
|
|
172
185
|
/** A pre-event MCP interception: the native response + any doc source it satisfied. */
|
|
173
186
|
interface McpIntercept {
|
|
174
187
|
stdout: string;
|
|
@@ -180,8 +193,13 @@ interface McpIntercept {
|
|
|
180
193
|
* Harnesses without input-mutation/cache support fall through to null.
|
|
181
194
|
*/
|
|
182
195
|
declare function mcpPreIntercept(id: string, tool: string, input: Record<string, unknown>, dir: string, ttlMs: number, now: number): McpIntercept | null;
|
|
183
|
-
/**
|
|
184
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Post-event: persist the MCP/WebFetch response (extracted to markdown). MCP docs
|
|
198
|
+
* go through {@link mcpCacheWrite} (compact + Jaccard-dedup + `index.json`);
|
|
199
|
+
* WebFetch uses {@link webfetchCacheWrite} (compact, exact key, no index).
|
|
200
|
+
* @param now - Current epoch ms (timestamp source; defaults to `Date.now()`).
|
|
201
|
+
*/
|
|
202
|
+
declare function mcpPostStore(tool: string, input: Record<string, unknown>, response: unknown, dir: string, now?: number): void;
|
|
185
203
|
//#endregion
|
|
186
204
|
//#region src/runtime/inject-context.d.ts
|
|
187
205
|
/**
|
|
@@ -602,18 +620,20 @@ declare function dispatchMemory(event: string, payload: Record<string, unknown>,
|
|
|
602
620
|
//#endregion
|
|
603
621
|
//#region src/runtime/lifecycle/seo/post-tool-use.d.ts
|
|
604
622
|
/**
|
|
605
|
-
* Validate the edited file's SEO completeness. Returns a
|
|
606
|
-
* `
|
|
623
|
+
* Validate the edited file's SEO completeness. Returns a block message (for a
|
|
624
|
+
* top-level `decision: block` response) when HTML-like, opted-in, and missing
|
|
607
625
|
* elements; otherwise `null` (allow).
|
|
608
626
|
* @param payload - The raw PostToolUse payload.
|
|
609
|
-
* @returns The
|
|
627
|
+
* @returns The block reason string, or `null` to allow.
|
|
610
628
|
*/
|
|
611
629
|
declare function seoPostToolUse(payload: Record<string, unknown>): string | null;
|
|
612
630
|
/**
|
|
613
|
-
* SEO PostToolUse as a ready native response: a `
|
|
631
|
+
* SEO PostToolUse as a ready native response: a top-level `decision: block`
|
|
614
632
|
* string when the edited file is missing SEO elements, else `null` (allow).
|
|
633
|
+
* PostToolUse ignores `permissionDecision` (PreToolUse-only), so this must use
|
|
634
|
+
* the `decision`/`reason` keys to actually feed the failure back to Claude.
|
|
615
635
|
* @param payload - The raw PostToolUse payload.
|
|
616
|
-
* @returns The
|
|
636
|
+
* @returns The block response string, or `null` to allow.
|
|
617
637
|
*/
|
|
618
638
|
declare function seoPostToolUseResponse(payload: Record<string, unknown>): string | null;
|
|
619
639
|
//#endregion
|
|
@@ -716,4 +736,4 @@ interface PreContext {
|
|
|
716
736
|
*/
|
|
717
737
|
declare function handlePre(ctx: PreContext): Promise<HandleOutcome>;
|
|
718
738
|
//#endregion
|
|
719
|
-
export { Activity, DEFAULT_WINDOW_MS, DuplicationVerdict, type GateInput, HandleOptions, HandleOutcome, LifecycleInput, MCP_TTL_MS, McpIntercept, NormalizedEvent, PluginScope, PreContext, REQUIRED_AGENTS, SolidProfile, TRIVIAL_BUDGET, ToolEvent, activityFor, aipilotPostToolUse, cartoSessionStart, claudeHome, cleanupSession, countFiles, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dispatchMemory, dryGate, extractSymbols, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writePluginMap, writeTree };
|
|
739
|
+
export { Activity, DEFAULT_WINDOW_MS, DuplicationVerdict, type GateInput, HandleOptions, HandleOutcome, LifecycleInput, MCP_TTL_MS, McpIntercept, NormalizedEvent, PluginScope, PreContext, REQUIRED_AGENTS, SolidProfile, TRIVIAL_BUDGET, ToolEvent, WEBFETCH_TTL_MS, activityFor, aipilotPostToolUse, cacheQueryOf, cartoSessionStart, claudeHome, cleanupSession, countFiles, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dispatchMemory, dryGate, extractSymbols, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writePluginMap, writeTree };
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
|
|
2
|
-
import { $ as sessionStartCore, A as writePluginMap, At as
|
|
2
|
+
import { $ as sessionStartCore, A as writePluginMap, At as fusengineCache, B as trackSessionChanges, C as aipilotPostToolUse, Ct as isoUtc, D as lessonsStateFileFor, Dt as securityStatePath, E as lessonsFileFor, Et as securityStateDir, F as mergeLines, Ft as sessionsDir, G as validateTeammateOutput, H as cleanupSession, I as countFiles, J as detectSolidProfile, K as trackAgentMemory, L as getFileDesc, M as isProject, Mt as sanitizeSessionId, N as writeTree, Nt as saveSessionState, O as cartoSessionStart, Ot as todayUtc, P as loadEnriched, Pt as sessionStatePath, Q as runSessionStartCleanups, R as listChildren, S as dispatchLifecycle, St as activityFor, T as dispatchLessons, Tt as saveSecurityState, U as saveApexState, V as validateRulesLoaded, W as logToolFailure, X as injectRules, Y as solidDetectStart, Z as readRules, _ as postTrackingSideEffects, _t as MCP_TTL_MS, a as TRIVIAL_BUDGET, at as gitContext, b as trackSkillRead, bt as isMcpTool, c as detectDuplication, ct as taskContext, d as lifecycleStdout, dt as defaultStateDir, et as pruneEmptyDirs, f as postEditContext, ft as projectHash, g as securityAdvisory, gt as mcpPreIntercept, h as dispatchMemory, ht as mcpPostStore, i as REQUIRED_AGENTS, it as devContext, j as generateProjectMap, jt as loadSessionState, k as generateEcosystemMap, kt as claudeHome, l as dryGate, lt as respond, m as seoPostToolUseResponse, mt as normalizeEvent, n as handlePre, nt as removeOldFiles, o as gate, ot as projectContext, p as seoPostToolUse, pt as trackFile, q as subagentCacheContext, r as DEFAULT_WINDOW_MS, rt as trimLogFile, s as preCommitGate, st as promptSubmitContext, t as handleHook, tt as purgeTtlTree, u as extractSymbols, ut as recordActivity, v as trackWatchResearch, vt as WEBFETCH_TTL_MS, w as dispatchAipilot, wt as loadSecurityState, x as trackEnrichment, xt as queryOf, y as trackMcpResearch, yt as cacheQueryOf, z as postEditTypescript } from "../handle-VhWQxvyN.mjs";
|
|
3
3
|
//#region src/runtime/storage.ts
|
|
4
4
|
/**
|
|
5
5
|
* The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
|
|
@@ -9,4 +9,4 @@ function harnessStateDir(root) {
|
|
|
9
9
|
return projectLayout(root).stateDir;
|
|
10
10
|
}
|
|
11
11
|
//#endregion
|
|
12
|
-
export { DEFAULT_WINDOW_MS, MCP_TTL_MS, REQUIRED_AGENTS, TRIVIAL_BUDGET, activityFor, aipilotPostToolUse, cartoSessionStart, claudeHome, cleanupSession, countFiles, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dispatchMemory, dryGate, extractSymbols, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writePluginMap, writeTree };
|
|
12
|
+
export { DEFAULT_WINDOW_MS, MCP_TTL_MS, REQUIRED_AGENTS, TRIVIAL_BUDGET, WEBFETCH_TTL_MS, activityFor, aipilotPostToolUse, cacheQueryOf, cartoSessionStart, claudeHome, cleanupSession, countFiles, defaultStateDir, detectDuplication, detectSolidProfile, devContext, dispatchAipilot, dispatchLessons, dispatchLifecycle, dispatchMemory, dryGate, extractSymbols, fusengineCache, gate, generateEcosystemMap, generateProjectMap, getFileDesc, gitContext, handleHook, handlePre, harnessStateDir, injectRules, isMcpTool, isProject, isoUtc, lessonsFileFor, lessonsStateFileFor, lifecycleStdout, listChildren, loadEnriched, loadSecurityState, loadSessionState, logToolFailure, mcpPostStore, mcpPreIntercept, mergeLines, normalizeEvent, postEditContext, postEditTypescript, postTrackingSideEffects, preCommitGate, projectContext, projectHash, promptSubmitContext, pruneEmptyDirs, purgeTtlTree, queryOf, readRules, recordActivity, removeOldFiles, respond, runSessionStartCleanups, sanitizeSessionId, saveApexState, saveSecurityState, saveSessionState, securityAdvisory, securityStateDir, securityStatePath, seoPostToolUse, seoPostToolUseResponse, sessionStartCore, sessionStatePath, sessionsDir, solidDetectStart, subagentCacheContext, taskContext, todayUtc, trackAgentMemory, trackEnrichment, trackFile, trackMcpResearch, trackSessionChanges, trackSkillRead, trackWatchResearch, trimLogFile, validateRulesLoaded, validateTeammateOutput, writePluginMap, writeTree };
|
package/dist/state/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as taskStart, c as ensureStateDir, d as stateFilePath, f as acquireLock, i as taskCreate, l as loadState, n as ApexTaskFile, o as ApexState, r as taskComplete, s as apexStateDir, t as ApexTask, u as saveState } from "../index-
|
|
1
|
+
import { a as taskStart, c as ensureStateDir, d as stateFilePath, f as acquireLock, i as taskCreate, l as loadState, n as ApexTaskFile, o as ApexState, r as taskComplete, s as apexStateDir, t as ApexTask, u as saveState } from "../index-D7GpOmkl.mjs";
|
|
2
2
|
export { ApexState, ApexTask, ApexTaskFile, acquireLock, apexStateDir, ensureStateDir, loadState, saveState, stateFilePath, taskComplete, taskCreate, taskStart };
|
|
@@ -181,4 +181,4 @@ async function saveTrack(file, track) {
|
|
|
181
181
|
writeLastNonce(envelope.nonce);
|
|
182
182
|
}
|
|
183
183
|
//#endregion
|
|
184
|
-
export {
|
|
184
|
+
export { emptyTrack as a, recordDoc as c, trivialCount as d, agentsFresh as i, recordRefRead as l, saveTrack as n, recordAgent as o, verifyTrack as r, recordBrainstormRequired as s, loadTrack as t, recordTrivialEdit as u };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as recordAgent, c as recordRefRead, i as emptyTrack, l as recordTrivialEdit, n as SessionTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as AgentQuality, u as trivialCount } from "../session-state-
|
|
1
|
+
import { a as recordAgent, c as recordRefRead, i as emptyTrack, l as recordTrivialEdit, n as SessionTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as AgentQuality, u as trivialCount } from "../session-state-Dzq6yrw7.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/tracking/store.d.ts
|
|
4
4
|
/**
|
package/dist/tracking/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as emptyTrack, c as recordDoc, d as trivialCount, i as agentsFresh, l as recordRefRead, n as saveTrack, o as recordAgent, s as recordBrainstormRequired, t as loadTrack, u as recordTrivialEdit } from "../store-3WBr37Xz.mjs";
|
|
2
2
|
export { agentsFresh, emptyTrack, loadTrack, recordAgent, recordBrainstormRequired, recordDoc, recordRefRead, recordTrivialEdit, saveTrack, trivialCount };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { a as parseEnvInt } from "./limits-CHn8AIL1.mjs";
|
|
2
|
+
//#region src/config/ttl.ts
|
|
3
|
+
/** Default enforcement-freshness window, in seconds (2 minutes). */
|
|
4
|
+
const DEFAULT_TTL_SEC = 120;
|
|
5
|
+
/** Default env var name carrying the TTL override. */
|
|
6
|
+
const TTL_ENV_KEY = "FUSE_ENFORCE_TTL_SEC";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the enforcement TTL (seconds) from an env map.
|
|
9
|
+
* @param env - environment map (defaults to `process.env`)
|
|
10
|
+
* @param key - env var name (defaults to `FUSE_ENFORCE_TTL_SEC`)
|
|
11
|
+
*/
|
|
12
|
+
function resolveTtlSec(env = process.env, key = TTL_ENV_KEY) {
|
|
13
|
+
return parseEnvInt(env[key], 120);
|
|
14
|
+
}
|
|
15
|
+
/** Human label for a TTL: 120 -> "2min", 240 -> "4min", 90 -> "90s". */
|
|
16
|
+
function ttlLabel(sec) {
|
|
17
|
+
return sec % 60 === 0 ? `${sec / 60}min` : `${sec}s`;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { ttlLabel as i, TTL_ENV_KEY as n, resolveTtlSec as r, DEFAULT_TTL_SEC as t };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-
|
|
4
|
-
import { t as routeReferences } from "./router-
|
|
2
|
+
import { j as countLines } from "./evaluate-B1n1ti0N.mjs";
|
|
3
|
+
import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-BhzDmJ18.mjs";
|
|
4
|
+
import { t as routeReferences } from "./router-BfX0hJg8.mjs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
@@ -65,12 +65,12 @@ function detectProjectType(dir) {
|
|
|
65
65
|
}
|
|
66
66
|
//#endregion
|
|
67
67
|
//#region src/policy/apex.ts
|
|
68
|
-
/** Gate: Context7
|
|
68
|
+
/** Gate: any one documentation source (Context7, Exa, or web) must have been consulted this session. */
|
|
69
69
|
const docConsultedGate = (ctx) => isDocConsulted(ctx.authorizations, ctx.sessionId) ? null : {
|
|
70
70
|
kind: "block",
|
|
71
71
|
title: "APEX: documentation not consulted",
|
|
72
72
|
reason: formatDocDeny(ctx.framework),
|
|
73
|
-
actions: ["
|
|
73
|
+
actions: ["Use ANY ONE of: mcp__context7__query-docs, mcp__exa__web_search_exa, WebSearch, or WebFetch"]
|
|
74
74
|
};
|
|
75
75
|
/** Gate: the routed SOLID references for this edit must have been read. */
|
|
76
76
|
const solidReadGate = (ctx) => {
|
|
@@ -118,7 +118,7 @@ function evaluateApex(ctx, gates = APEX_GATES) {
|
|
|
118
118
|
//#endregion
|
|
119
119
|
//#region src/policy/creation-intent.ts
|
|
120
120
|
const CREATE_RE = /\b(?:create|implement|add|build|new|feature|component|generate|make|develop|scaffold)\b/i;
|
|
121
|
-
const SKIP_RE = /\b(?:fix|bug|debug|update|refactor|rename|move|delete|remove|commit|push|edit|modify|change)\b/i;
|
|
121
|
+
const SKIP_RE = /\b(?:fix|bug|debug|update|refactor|rename|move|delete|remove|commit|push|status|edit|modify|change|adjust|tweak|existing|current)\b/i;
|
|
122
122
|
/**
|
|
123
123
|
* True when a prompt expresses creation intent (a new feature/component) and is
|
|
124
124
|
* not a fix/refactor — the signal that brainstorming should precede creation.
|
|
@@ -129,8 +129,11 @@ function detectCreationIntent(prompt) {
|
|
|
129
129
|
}
|
|
130
130
|
//#endregion
|
|
131
131
|
//#region src/policy/verbosity.ts
|
|
132
|
-
/** Exa
|
|
133
|
-
const
|
|
132
|
+
/** Exa search tool — result count force-capped + token budget capped. */
|
|
133
|
+
const EXA_SEARCH = /exa__web_search|exa_web_search/i;
|
|
134
|
+
/** Exa code-context tool — token budget capped; `numResults` capped only when
|
|
135
|
+
* already present (parity: limit-mcp-verbosity.py never injects it). */
|
|
136
|
+
const EXA_CODE = /exa__get_code_context|exa_get_code_context/i;
|
|
134
137
|
/** Context7 doc tool whose token budget is capped. */
|
|
135
138
|
const CONTEXT7_TOOLS = /context7__query-docs|context7_query-docs|query-docs/i;
|
|
136
139
|
/** Max results an exa MCP call may request. */
|
|
@@ -152,9 +155,12 @@ function capVerbosity(tool, input) {
|
|
|
152
155
|
changed = true;
|
|
153
156
|
}
|
|
154
157
|
};
|
|
155
|
-
if (
|
|
158
|
+
if (EXA_SEARCH.test(tool)) {
|
|
156
159
|
cap("numResults", 3, true);
|
|
157
160
|
cap("tokensNum", MAX_TOKENS, false);
|
|
161
|
+
} else if (EXA_CODE.test(tool)) {
|
|
162
|
+
cap("numResults", 3, false);
|
|
163
|
+
cap("tokensNum", MAX_TOKENS, false);
|
|
158
164
|
} else if (CONTEXT7_TOOLS.test(tool)) cap("tokens", MAX_TOKENS, false);
|
|
159
165
|
return changed ? out : null;
|
|
160
166
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusengine/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
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/cache-C9z9LclL.mjs
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
//#region src/cache/compact.ts
|
|
3
|
-
const HTML_ENTITIES = {
|
|
4
|
-
"&": "&",
|
|
5
|
-
"<": "<",
|
|
6
|
-
">": ">",
|
|
7
|
-
""": "\"",
|
|
8
|
-
"'": "'",
|
|
9
|
-
"'": "'",
|
|
10
|
-
" ": " ",
|
|
11
|
-
"'": "'"
|
|
12
|
-
};
|
|
13
|
-
const BOILERPLATE = [
|
|
14
|
-
/^.*cookie.*(accept|consent|banner).*$/gim,
|
|
15
|
-
/^.*was this (helpful|page helpful|article helpful).*$/gim,
|
|
16
|
-
/^.*©\s*\d{4}.*all rights reserved.*$/gim,
|
|
17
|
-
/^\s*(home|about|contact|privacy|terms)\s*\|\s*.*$/gim,
|
|
18
|
-
/^.*subscribe to (our )?newsletter.*$/gim,
|
|
19
|
-
/^.*follow us on (twitter|facebook|linkedin).*$/gim
|
|
20
|
-
];
|
|
21
|
-
const MAX_BYTES = 5 * 1024;
|
|
22
|
-
function decodeEntities(text) {
|
|
23
|
-
let out = text;
|
|
24
|
-
for (const [ent, ch] of Object.entries(HTML_ENTITIES)) out = out.split(ent).join(ch);
|
|
25
|
-
out = out.replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => String.fromCodePoint(parseInt(h, 16)));
|
|
26
|
-
out = out.replace(/&#(\d+);/g, (_m, d) => String.fromCodePoint(parseInt(d, 10)));
|
|
27
|
-
return out;
|
|
28
|
-
}
|
|
29
|
-
/** Strip HTML entities + boilerplate, normalize blank lines, truncate to ~5KB. */
|
|
30
|
-
function compactMarkdown(content) {
|
|
31
|
-
let text = decodeEntities(content);
|
|
32
|
-
for (const re of BOILERPLATE) text = text.replace(re, "");
|
|
33
|
-
text = text.replace(/\n{3,}/g, "\n\n").trim();
|
|
34
|
-
const enc = new TextEncoder().encode(text);
|
|
35
|
-
if (enc.length > MAX_BYTES) {
|
|
36
|
-
const truncated = new TextDecoder().decode(enc.slice(0, MAX_BYTES));
|
|
37
|
-
text = `${truncated}\n\n[... truncated, ${text.slice(truncated.length).split("\n").length - 1} lines]`;
|
|
38
|
-
}
|
|
39
|
-
return text;
|
|
40
|
-
}
|
|
41
|
-
/** 8-char MD5 of `${toolName}::${query}`. */
|
|
42
|
-
function queryHash(toolName, query) {
|
|
43
|
-
return createHash("md5").update(`${toolName}::${query}`).digest("hex").slice(0, 8);
|
|
44
|
-
}
|
|
45
|
-
/** Bag-of-words Jaccard similarity strictly greater than `threshold`. */
|
|
46
|
-
function jaccardSimilar(a, b, threshold = .8) {
|
|
47
|
-
const ta = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
|
|
48
|
-
const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
|
|
49
|
-
if (ta.size === 0 || tb.size === 0) return false;
|
|
50
|
-
let inter = 0;
|
|
51
|
-
for (const t of ta) if (tb.has(t)) inter++;
|
|
52
|
-
const union = ta.size + tb.size - inter;
|
|
53
|
-
return inter / union > threshold;
|
|
54
|
-
}
|
|
55
|
-
//#endregion
|
|
56
|
-
export { jaccardSimilar as n, queryHash as r, compactMarkdown as t };
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
//#region src/cache/compact.d.ts
|
|
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
|
|
30
|
-
//#region src/cache/store.d.ts
|
|
31
|
-
/** Stable 16-char key from a tool name + query (FNV-1a, non-crypto). */
|
|
32
|
-
declare function mcpCacheKey(tool: string, query: string): string;
|
|
33
|
-
/** Path of a cache entry under `dir`. */
|
|
34
|
-
declare function cachePath(dir: string, tool: string, query: string): string;
|
|
35
|
-
/** Read a cached entry if it exists and is fresh (mtime within `ttlMs`), else null. */
|
|
36
|
-
declare function cacheLookup(dir: string, tool: string, query: string, ttlMs: number, now: number): string | null;
|
|
37
|
-
/** Store a cache entry (creates the dir; no-op on empty content). */
|
|
38
|
-
declare function cacheStore(dir: string, tool: string, query: string, content: string): void;
|
|
39
|
-
//#endregion
|
|
40
|
-
export { extractText as a, summarizeIndex as c, queryHash as d, mcpCacheKey as i, compactMarkdown as l, cachePath as n, IndexSummary as o, cacheStore as r, loadIndex as s, cacheLookup as t, jaccardSimilar as u };
|
package/dist/store-PrNPm6So.mjs
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { dirname, join } from "node:path";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
//#region src/cache/io.ts
|
|
4
|
-
/** Read a JSON array from `path`; [] on missing/corrupt/non-array. */
|
|
5
|
-
function loadIndex(path) {
|
|
6
|
-
try {
|
|
7
|
-
if (!existsSync(path)) return [];
|
|
8
|
-
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
9
|
-
return Array.isArray(data) ? data : [];
|
|
10
|
-
} catch {
|
|
11
|
-
return [];
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
/** Summarize an index of `{ tool?, ts? }` entries. */
|
|
15
|
-
function summarizeIndex(index) {
|
|
16
|
-
const byTool = {};
|
|
17
|
-
const timestamps = [];
|
|
18
|
-
for (const entry of index) {
|
|
19
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
20
|
-
const e = entry;
|
|
21
|
-
if (typeof e.tool === "string") byTool[e.tool] = (byTool[e.tool] ?? 0) + 1;
|
|
22
|
-
if (typeof e.ts === "string") timestamps.push(e.ts);
|
|
23
|
-
}
|
|
24
|
-
return {
|
|
25
|
-
total: index.length,
|
|
26
|
-
byTool,
|
|
27
|
-
oldestTs: timestamps.length ? timestamps.reduce((a, b) => a < b ? a : b) : null,
|
|
28
|
-
newestTs: timestamps.length ? timestamps.reduce((a, b) => a > b ? a : b) : null
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/cache/mcp-response.ts
|
|
33
|
-
const MAX_DEPTH = 5;
|
|
34
|
-
/**
|
|
35
|
-
* Extract usable markdown from an MCP `tool_response`: a string, a list of
|
|
36
|
-
* content blocks (non-text blocks skipped), or any JSON structure (fallback).
|
|
37
|
-
* Recurses up to depth 5 to guard against pathological/cyclic structures.
|
|
38
|
-
*/
|
|
39
|
-
function extractText(resp, depth = 0) {
|
|
40
|
-
if (depth >= MAX_DEPTH) return "";
|
|
41
|
-
if (typeof resp === "string") return resp;
|
|
42
|
-
if (Array.isArray(resp)) {
|
|
43
|
-
const parts = resp.filter((b) => typeof b === "object" && b !== null).filter((b) => b.type === "text").map((b) => b.text ?? "");
|
|
44
|
-
if (parts.length) return parts.join("\n\n");
|
|
45
|
-
const joined = resp.filter((b) => Array.isArray(b) || typeof b === "object" && b !== null).map((b) => extractText(b, depth + 1)).filter(Boolean).join("\n\n");
|
|
46
|
-
if (joined) return joined;
|
|
47
|
-
}
|
|
48
|
-
if (!resp) return "";
|
|
49
|
-
try {
|
|
50
|
-
return JSON.stringify(resp);
|
|
51
|
-
} catch {
|
|
52
|
-
return "";
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
//#endregion
|
|
56
|
-
//#region src/cache/store.ts
|
|
57
|
-
/** Stable 16-char key from a tool name + query (FNV-1a, non-crypto). */
|
|
58
|
-
function mcpCacheKey(tool, query) {
|
|
59
|
-
const s = `${tool}\n${query}`;
|
|
60
|
-
let h = 2166136261;
|
|
61
|
-
for (let i = 0; i < s.length; i++) {
|
|
62
|
-
h ^= s.charCodeAt(i);
|
|
63
|
-
h = Math.imul(h, 16777619);
|
|
64
|
-
}
|
|
65
|
-
return (h >>> 0).toString(16).padStart(8, "0") + (s.length >>> 0).toString(16).padStart(8, "0");
|
|
66
|
-
}
|
|
67
|
-
/** Path of a cache entry under `dir`. */
|
|
68
|
-
function cachePath(dir, tool, query) {
|
|
69
|
-
return join(dir, `${mcpCacheKey(tool, query)}.md`);
|
|
70
|
-
}
|
|
71
|
-
/** Read a cached entry if it exists and is fresh (mtime within `ttlMs`), else null. */
|
|
72
|
-
function cacheLookup(dir, tool, query, ttlMs, now) {
|
|
73
|
-
const path = cachePath(dir, tool, query);
|
|
74
|
-
try {
|
|
75
|
-
if (!existsSync(path) || now - statSync(path).mtimeMs > ttlMs) return null;
|
|
76
|
-
return readFileSync(path, "utf8");
|
|
77
|
-
} catch {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
/** Store a cache entry (creates the dir; no-op on empty content). */
|
|
82
|
-
function cacheStore(dir, tool, query, content) {
|
|
83
|
-
if (!content) return;
|
|
84
|
-
const path = cachePath(dir, tool, query);
|
|
85
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
86
|
-
writeFileSync(path, content);
|
|
87
|
-
}
|
|
88
|
-
//#endregion
|
|
89
|
-
export { extractText as a, mcpCacheKey as i, cachePath as n, loadIndex as o, cacheStore as r, summarizeIndex as s, cacheLookup as t };
|