@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.
Files changed (52) hide show
  1. package/README.md +7 -4
  2. package/dist/adapters/claude/index.d.mts +2 -2
  3. package/dist/adapters/claude/index.mjs +2 -2
  4. package/dist/adapters/cline/index.mjs +1 -1
  5. package/dist/adapters/codex/index.d.mts +1 -1
  6. package/dist/adapters/codex/index.mjs +1 -1
  7. package/dist/adapters/cursor/index.mjs +1 -1
  8. package/dist/adapters/gemini/index.mjs +1 -1
  9. package/dist/cache/index.d.mts +2 -2
  10. package/dist/cache/index.mjs +3 -3
  11. package/dist/cache-la_KkjCS.mjs +1 -0
  12. package/dist/{claude-ZgTC5yDS.mjs → claude-BnFrSfMo.mjs} +17 -3
  13. package/dist/cli/bin.mjs +70 -6
  14. package/dist/cli/index.mjs +1 -1
  15. package/dist/config/index.d.mts +2 -2
  16. package/dist/config/index.mjs +3 -2
  17. package/dist/{doc-helpers-Dd_x1-tZ.mjs → doc-helpers-BhzDmJ18.mjs} +8 -6
  18. package/dist/{doc-helpers-CG1nuf-c.d.mts → doc-helpers-D14nkD5D.d.mts} +5 -3
  19. package/dist/{dotenv-TLNMiSjP.mjs → dotenv-DGyLln7U.mjs} +1 -20
  20. package/dist/{evaluate-za4vT-p7.mjs → evaluate-B1n1ti0N.mjs} +89 -49
  21. package/dist/freshness/index.d.mts +1 -1
  22. package/dist/freshness/index.mjs +1 -1
  23. package/dist/{handle-t-lQuQc7.mjs → handle-VhWQxvyN.mjs} +612 -163
  24. package/dist/index-BX-xcvhY.d.mts +75 -0
  25. package/dist/{index-DLUfyYmU.d.mts → index-COBvvc3L.d.mts} +3 -1
  26. package/dist/{index-FrWgmkbP.d.mts → index-CwOdFBOr.d.mts} +11 -2
  27. package/dist/{index-CPoF_hLP.d.mts → index-D7GpOmkl.d.mts} +1 -1
  28. package/dist/{index-QzK2dv0V.d.mts → index-DTrjSNmI.d.mts} +38 -13
  29. package/dist/{index-DL8MxjuP.d.mts → index-mmRF3KNp.d.mts} +4 -4
  30. package/dist/index.d.mts +7 -7
  31. package/dist/index.mjs +9 -9
  32. package/dist/{loader-CyAoJv2W.mjs → loader-Bn-DbZmt.mjs} +1 -1
  33. package/dist/mcp-store-CDUVqtJ0.mjs +238 -0
  34. package/dist/policy/index.d.mts +2 -2
  35. package/dist/policy/index.mjs +3 -3
  36. package/dist/refs/index.d.mts +1 -1
  37. package/dist/refs/index.mjs +2 -2
  38. package/dist/{router-D8cVrI-s.mjs → router-BfX0hJg8.mjs} +13 -20
  39. package/dist/{run-DOARDruz.mjs → run-D-Ydrw3D.mjs} +1 -1
  40. package/dist/runtime/index.d.mts +33 -13
  41. package/dist/runtime/index.mjs +2 -2
  42. package/dist/{session-state-DxNkIBZ2.d.mts → session-state-Dzq6yrw7.d.mts} +1 -1
  43. package/dist/state/index.d.mts +1 -1
  44. package/dist/{store-7j02oGjt.mjs → store-3WBr37Xz.mjs} +1 -1
  45. package/dist/tracking/index.d.mts +1 -1
  46. package/dist/tracking/index.mjs +1 -1
  47. package/dist/ttl-BG55s6HZ.mjs +20 -0
  48. package/dist/{validate-osn-huLZ.mjs → validate-B3wkqEoN.mjs} +15 -9
  49. package/package.json +1 -1
  50. package/dist/cache-C9z9LclL.mjs +0 -56
  51. package/dist/index-DPkCX_AR.d.mts +0 -40
  52. package/dist/store-PrNPm6So.mjs +0 -89
@@ -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-osn-huLZ.mjs";
2
- import { A as evaluateFileSize, C as securityGuard, D as SYSTEM_INSTALL, E as PROJECT_INSTALL, O as matchPatterns, S as CRITICAL_PATTERNS, T as GIT_BLOCKED, _ as CODE_REDIRECT, a as registerGuard, b as protectedPathGuard, 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 detectFramework, k as countLines, 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 bashWriteGuard, w as GIT_ASK, x as ASK_PATTERNS, y as PROTECTED_FRAGMENTS } from "../evaluate-za4vT-p7.mjs";
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 };
@@ -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-DL8MxjuP.mjs";
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 };
@@ -1,4 +1,4 @@
1
- import { i as parseFrontmatter, n as scoreReferences, r as globToRe, t as routeReferences } from "../router-D8cVrI-s.mjs";
2
- import { n as toRefMeta, t as loadRefs } from "../loader-CyAoJv2W.mjs";
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
- * +10 per `applies-to` glob match, weighted by glob SPECIFICITY (+5 per literal path
27
- * segment) so a more specific skill wins (an app-router glob beats a bare extension
28
- * glob beats a plain TS glob). +5 per `trigger-on-edit` fragment, +1 per keyword.
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
- for (const g of meta.appliesTo.split(", ")) if (globToRe(g).test(filePath)) score += 10 + specificity(g) * 5;
36
- }
37
- if (meta.triggerOnEdit) {
38
- for (const frag of meta.triggerOnEdit.split(", ")) if (filePath.includes(frag.trim())) score += 5;
39
- }
40
- if (meta.keywords) {
41
- const hay = `${filePath} ${content}`.toLowerCase();
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-za4vT-p7.mjs";
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
@@ -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-DxNkIBZ2.mjs";
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 | null;
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
- /** Default freshness for cached MCP/WebFetch results (48h). */
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
- /** Post-event: store the MCP/WebFetch response (extracted to markdown) in the cache. */
184
- declare function mcpPostStore(tool: string, input: Record<string, unknown>, response: unknown, dir: string): void;
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 deny message (for a
606
- * `permissionDecision: deny` response) when HTML-like, opted-in, and missing
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 deny reason string, or `null` to allow.
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 `permissionDecision: deny`
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 deny response string, or `null` to allow.
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 };
@@ -1,5 +1,5 @@
1
1
  import { r as projectLayout } from "../layout-C0jaaCQC.mjs";
2
- import { $ as sessionStartCore, A as writePluginMap, At as sanitizeSessionId, B as trackSessionChanges, C as aipilotPostToolUse, Ct as saveSecurityState, D as lessonsStateFileFor, Dt as claudeHome, E as lessonsFileFor, Et as todayUtc, F as mergeLines, G as validateTeammateOutput, H as cleanupSession, I as countFiles, J as detectSolidProfile, K as trackAgentMemory, L as getFileDesc, M as isProject, Mt as sessionStatePath, N as writeTree, Nt as sessionsDir, O as cartoSessionStart, Ot as fusengineCache, P as loadEnriched, Q as runSessionStartCleanups, R as listChildren, S as dispatchLifecycle, St as loadSecurityState, T as dispatchLessons, Tt as securityStatePath, U as saveApexState, V as validateRulesLoaded, W as logToolFailure, X as injectRules, Y as solidDetectStart, Z as readRules, _ as postTrackingSideEffects, _t as mcpPostStore, a as TRIVIAL_BUDGET, at as gitContext, b as trackSkillRead, bt as activityFor, 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 isMcpTool, h as dispatchMemory, ht as MCP_TTL_MS, i as REQUIRED_AGENTS, it as devContext, j as generateProjectMap, jt as saveSessionState, k as generateEcosystemMap, kt as loadSessionState, 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 mcpPreIntercept, w as dispatchAipilot, wt as securityStateDir, x as trackEnrichment, xt as isoUtc, y as trackMcpResearch, yt as queryOf, z as postEditTypescript } from "../handle-t-lQuQc7.mjs";
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 };
@@ -1,4 +1,4 @@
1
- import { t as AuthEntry } from "./doc-helpers-CG1nuf-c.mjs";
1
+ import { t as AuthEntry } from "./doc-helpers-D14nkD5D.mjs";
2
2
 
3
3
  //#region src/tracking/session-state.d.ts
4
4
  /** Quality of a recorded agent call (the freshness gate ignores insufficient ones). */
@@ -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-CPoF_hLP.mjs";
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 { recordAgent as a, recordRefRead as c, emptyTrack as i, recordTrivialEdit as l, saveTrack as n, recordBrainstormRequired as o, agentsFresh as r, recordDoc as s, loadTrack as t, trivialCount as u };
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-DxNkIBZ2.mjs";
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
  /**
@@ -1,2 +1,2 @@
1
- import { a as recordAgent, c as recordRefRead, i as emptyTrack, l as recordTrivialEdit, n as saveTrack, o as recordBrainstormRequired, r as agentsFresh, s as recordDoc, t as loadTrack, u as trivialCount } from "../store-7j02oGjt.mjs";
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 { k as countLines } from "./evaluate-za4vT-p7.mjs";
3
- import { r as isDocConsulted, t as formatDocDeny } from "./doc-helpers-Dd_x1-tZ.mjs";
4
- import { t as routeReferences } from "./router-D8cVrI-s.mjs";
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 + Exa must have been consulted this session. */
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: ["Call mcp__context7__query-docs", "Call mcp__exa__web_search_exa"]
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 MCP tools whose result count + token budget are capped. */
133
- const EXA_TOOLS = /exa__web_search|exa__get_code_context|exa_web_search|exa_get_code_context/i;
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 (EXA_TOOLS.test(tool)) {
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.39",
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",
@@ -1,56 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- //#region src/cache/compact.ts
3
- const HTML_ENTITIES = {
4
- "&amp;": "&",
5
- "&lt;": "<",
6
- "&gt;": ">",
7
- "&quot;": "\"",
8
- "&#x27;": "'",
9
- "&#39;": "'",
10
- "&nbsp;": " ",
11
- "&apos;": "'"
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 };
@@ -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 };