@fusengine/harness 0.1.42 → 0.1.44
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 +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/{claude-CeRYMOaG.mjs → claude-DLh0fHWM.mjs} +6 -2
- package/dist/cli/bin.mjs +233 -11
- package/dist/cli/index.mjs +1 -1
- package/dist/config/index.d.mts +1 -1
- package/dist/config/index.mjs +1 -2
- package/dist/{doc-helpers-D14nkD5D.d.mts → doc-helpers-BNfYWvYv.d.mts} +9 -1
- package/dist/{doc-helpers-BhzDmJ18.mjs → doc-helpers-CWZegVdR.mjs} +14 -5
- package/dist/{dotenv-DGyLln7U.mjs → dotenv-B9nM4cuQ.mjs} +26 -1
- package/dist/evaluate-d7Pp8XJH.mjs +784 -0
- package/dist/freshness/index.d.mts +1 -1
- package/dist/freshness/index.mjs +1 -1
- package/dist/{handle-BTHcKWQ5.mjs → handle-CtMMVoxT.mjs} +789 -357
- package/dist/home-state-mKZxP4oZ.mjs +52 -0
- package/dist/{index-D7GpOmkl.d.mts → index-BA-SqNR7.d.mts} +1 -1
- package/dist/{index-DXQfL1u8.d.mts → index-BXPySPxE.d.mts} +7 -1
- package/dist/{index-CwOdFBOr.d.mts → index-BxjzFraL.d.mts} +3 -1
- package/dist/{index-DN4cZDbU.d.mts → index-CVhw7eA0.d.mts} +111 -23
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +7 -8
- package/dist/{loader-Bn-DbZmt.mjs → loader-AGz4nK7d.mjs} +1 -1
- package/dist/policy/index.d.mts +2 -2
- package/dist/policy/index.mjs +3 -3
- package/dist/refs/index.mjs +2 -2
- package/dist/{router-BfX0hJg8.mjs → router-PKVNBHge.mjs} +11 -1
- package/dist/{run-jgivVDv6.mjs → run-DLXtA5DH.mjs} +1 -1
- package/dist/runtime/index.d.mts +71 -14
- package/dist/runtime/index.mjs +3 -3
- package/dist/{session-state-Dzq6yrw7.d.mts → session-state-D4F_Dub6.d.mts} +1 -1
- package/dist/state/index.d.mts +1 -1
- package/dist/{store-CdWOQ9zD.mjs → store-CNjFenWe.mjs} +3 -50
- package/dist/tracking/index.d.mts +1 -1
- package/dist/tracking/index.mjs +1 -1
- package/dist/{validate-xi-zc-22.mjs → validate-Ca7NSp-r.mjs} +495 -83
- package/package.json +1 -1
- package/dist/evaluate-CeivW6G0.mjs +0 -477
- package/dist/ttl-BG55s6HZ.mjs +0 -20
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as fileSizeGuard, c as
|
|
2
|
-
export { ClaudeHookInput, blockResponse, contextResponse, denyResponse, fileSizeGuard, guard, readClaudeInput, toClaudeResponse };
|
|
1
|
+
import { a as fileSizeGuard, c as systemMessage, i as denyResponse, l as toClaudeResponse, n as blockResponse, o as guard, r as contextResponse, s as readClaudeInput, t as ClaudeHookInput } from "../../index-BxjzFraL.mjs";
|
|
2
|
+
export { ClaudeHookInput, blockResponse, contextResponse, denyResponse, fileSizeGuard, guard, readClaudeInput, systemMessage, toClaudeResponse };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as guard, i as fileSizeGuard, n as contextResponse, o as readClaudeInput, r as denyResponse, s as
|
|
2
|
-
export { blockResponse, contextResponse, denyResponse, fileSizeGuard, guard, readClaudeInput, toClaudeResponse };
|
|
1
|
+
import { a as guard, c as toClaudeResponse, i as fileSizeGuard, n as contextResponse, o as readClaudeInput, r as denyResponse, s as systemMessage, t as blockResponse } from "../../claude-DLh0fHWM.mjs";
|
|
2
|
+
export { blockResponse, contextResponse, denyResponse, fileSizeGuard, guard, readClaudeInput, systemMessage, toClaudeResponse };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as denyResponse, o as guard, r as contextResponse, s as readClaudeInput, t as ClaudeHookInput } from "../../index-
|
|
1
|
+
import { i as denyResponse, o as guard, r as contextResponse, s as readClaudeInput, t as ClaudeHookInput } from "../../index-BxjzFraL.mjs";
|
|
2
2
|
export { type ClaudeHookInput as CodexHookInput, contextResponse, denyResponse, guard, readClaudeInput as readCodexInput };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as guard, n as contextResponse, o as readClaudeInput, r as denyResponse } from "../../claude-
|
|
1
|
+
import { a as guard, n as contextResponse, o as readClaudeInput, r as denyResponse } from "../../claude-DLh0fHWM.mjs";
|
|
2
2
|
export { contextResponse, denyResponse, guard, readClaudeInput as readCodexInput };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as evaluate } from "./evaluate-
|
|
1
|
+
import { t as evaluate } from "./evaluate-d7Pp8XJH.mjs";
|
|
2
2
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
@@ -116,6 +116,10 @@ function contextResponse(event, text) {
|
|
|
116
116
|
additionalContext: text
|
|
117
117
|
} });
|
|
118
118
|
}
|
|
119
|
+
/** A raw `systemMessage` notice — shown to the user without blocking the tool. Mirrors the shared Python `hook_output.allow_pass`/`post_pass` convention. */
|
|
120
|
+
function systemMessage(text) {
|
|
121
|
+
return JSON.stringify({ systemMessage: text });
|
|
122
|
+
}
|
|
119
123
|
/**
|
|
120
124
|
* Render a portable {@link Prompt} as a Claude Code hook response:
|
|
121
125
|
* `block` → `permissionDecision: deny`, `ask` → `permissionDecision: ask`
|
|
@@ -148,4 +152,4 @@ function guard(input) {
|
|
|
148
152
|
/** @deprecated use {@link guard}. Kept for back-compat. */
|
|
149
153
|
const fileSizeGuard = guard;
|
|
150
154
|
//#endregion
|
|
151
|
-
export { guard as a,
|
|
155
|
+
export { guard as a, toClaudeResponse as c, readText as d, sleep as f, fileSizeGuard as i, collectFiles as l, writeText as m, contextResponse as n, readClaudeInput as o, spawnCapture as p, denyResponse as r, systemMessage as s, blockResponse as t, pathExists as u };
|
package/dist/cli/bin.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { r as resolveTtlSec } from "../
|
|
3
|
-
import { r as loadDotenv } from "../dotenv-DGyLln7U.mjs";
|
|
2
|
+
import { r as loadDotenv, s as resolveTtlSec } from "../dotenv-B9nM4cuQ.mjs";
|
|
4
3
|
import { t as detectHarness } from "../harness-C8Nxxyn_.mjs";
|
|
5
|
-
import {
|
|
4
|
+
import { t as claudeHome } from "../home-state-mKZxP4oZ.mjs";
|
|
5
|
+
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-DLXtA5DH.mjs";
|
|
6
6
|
import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import { Ot as todayUtc, t as handleHook } from "../handle-BTHcKWQ5.mjs";
|
|
7
|
+
import { Mt as todayUtc, t as handleHook } from "../handle-CtMMVoxT.mjs";
|
|
9
8
|
import { delimiter, dirname, join } from "node:path";
|
|
10
9
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
10
|
import { homedir } from "node:os";
|
|
@@ -78,6 +77,222 @@ async function scanChangelog(now = Date.now(), home = homedir()) {
|
|
|
78
77
|
};
|
|
79
78
|
}
|
|
80
79
|
//#endregion
|
|
80
|
+
//#region src/runtime/lifecycle/security/scan-patterns.ts
|
|
81
|
+
/** OWASP scan patterns keyed by detected language (ported 1:1 from Python). */
|
|
82
|
+
const PATTERNS = {
|
|
83
|
+
javascript: [
|
|
84
|
+
{
|
|
85
|
+
severity: "HIGH",
|
|
86
|
+
category: "XSS",
|
|
87
|
+
regex: /innerHTML\s*=/,
|
|
88
|
+
glob: "*.js"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
severity: "HIGH",
|
|
92
|
+
category: "XSS",
|
|
93
|
+
regex: /dangerouslySetInnerHTML/,
|
|
94
|
+
glob: "*.js"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
severity: "CRITICAL",
|
|
98
|
+
category: "CODE_EXEC",
|
|
99
|
+
regex: /eval\(/,
|
|
100
|
+
glob: "*.js"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
severity: "CRITICAL",
|
|
104
|
+
category: "CODE_EXEC",
|
|
105
|
+
regex: /new Function\(/,
|
|
106
|
+
glob: "*.js"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
severity: "CRITICAL",
|
|
110
|
+
category: "CMD_INJECTION",
|
|
111
|
+
regex: /child_process/,
|
|
112
|
+
glob: "*.js"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
severity: "HIGH",
|
|
116
|
+
category: "CMD_INJECTION",
|
|
117
|
+
regex: /shell:\s*true/,
|
|
118
|
+
glob: "*.js"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
severity: "MEDIUM",
|
|
122
|
+
category: "WEAK_CRYPTO",
|
|
123
|
+
regex: /Math\.random\(\)/,
|
|
124
|
+
glob: "*.js"
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
severity: "CRITICAL",
|
|
128
|
+
category: "SECRETS",
|
|
129
|
+
regex: /AKIA[0-9A-Z]{16}/,
|
|
130
|
+
glob: "*.js"
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
php: [
|
|
134
|
+
{
|
|
135
|
+
severity: "CRITICAL",
|
|
136
|
+
category: "RCE",
|
|
137
|
+
regex: /shell_exec|system\(|passthru\(/,
|
|
138
|
+
glob: "*.php"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
severity: "CRITICAL",
|
|
142
|
+
category: "CODE_EXEC",
|
|
143
|
+
regex: /eval\(|assert\(/,
|
|
144
|
+
glob: "*.php"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
severity: "HIGH",
|
|
148
|
+
category: "SQL_INJECTION",
|
|
149
|
+
regex: /mysql_query\(/,
|
|
150
|
+
glob: "*.php"
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
python: [
|
|
154
|
+
{
|
|
155
|
+
severity: "CRITICAL",
|
|
156
|
+
category: "CODE_EXEC",
|
|
157
|
+
regex: /eval\(|exec\(/,
|
|
158
|
+
glob: "*.py"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
severity: "CRITICAL",
|
|
162
|
+
category: "CMD_INJECTION",
|
|
163
|
+
regex: /os\.system\(|subprocess.*shell=True/,
|
|
164
|
+
glob: "*.py"
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
severity: "HIGH",
|
|
168
|
+
category: "DESERIALIZATION",
|
|
169
|
+
regex: /pickle\.loads\(/,
|
|
170
|
+
glob: "*.py"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
severity: "HIGH",
|
|
174
|
+
category: "TLS",
|
|
175
|
+
regex: /verify=False|ssl\.CERT_NONE/,
|
|
176
|
+
glob: "*.py"
|
|
177
|
+
}
|
|
178
|
+
],
|
|
179
|
+
swift: [
|
|
180
|
+
{
|
|
181
|
+
severity: "HIGH",
|
|
182
|
+
category: "INSECURE_STORAGE",
|
|
183
|
+
regex: /UserDefaults.*password|token|secret/,
|
|
184
|
+
glob: "*.swift"
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
severity: "MEDIUM",
|
|
188
|
+
category: "INSECURE_HTTP",
|
|
189
|
+
regex: /"http:\/\//,
|
|
190
|
+
glob: "*.swift"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
severity: "HIGH",
|
|
194
|
+
category: "WEAK_KEYCHAIN",
|
|
195
|
+
regex: /kSecAttrAccessibleAlways/,
|
|
196
|
+
glob: "*.swift"
|
|
197
|
+
}
|
|
198
|
+
]
|
|
199
|
+
};
|
|
200
|
+
/** Return the scan patterns for a language ("" / go / rust / unknown → none). */
|
|
201
|
+
function getPatterns(lang) {
|
|
202
|
+
return PATTERNS[lang] ?? [];
|
|
203
|
+
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/runtime/lifecycle/security/scan.ts
|
|
206
|
+
/**
|
|
207
|
+
* @module security/scan
|
|
208
|
+
* OWASP vulnerability scanner. Ports the Python `security-scan.py`: detect the
|
|
209
|
+
* project language, walk source files (excluding vendored/build dirs), match the
|
|
210
|
+
* per-language {@link getPatterns} rules, and aggregate a JSON report. Invoked by
|
|
211
|
+
* the `harness scan` CLI command (skill `/fuse-security:scan`), NOT a hook.
|
|
212
|
+
* @packageDocumentation
|
|
213
|
+
*/
|
|
214
|
+
const EXCLUDE_RE = /(^|\/)(node_modules|vendor|\.git|dist|build)(\/|$)/;
|
|
215
|
+
const LANG_FILES = [
|
|
216
|
+
["package.json", "javascript"],
|
|
217
|
+
["composer.json", "php"],
|
|
218
|
+
["requirements.txt", "python"],
|
|
219
|
+
["pyproject.toml", "python"],
|
|
220
|
+
["Package.swift", "swift"],
|
|
221
|
+
["go.mod", "go"],
|
|
222
|
+
["Cargo.toml", "rust"]
|
|
223
|
+
];
|
|
224
|
+
/** Detect the project language from its config files (first match wins). */
|
|
225
|
+
function detectLanguage(dir) {
|
|
226
|
+
for (const [file, lang] of LANG_FILES) if (existsSync(join(dir, file))) return lang;
|
|
227
|
+
return "unknown";
|
|
228
|
+
}
|
|
229
|
+
/** Recursively collect files whose name ends with `ext`, skipping vendored dirs. */
|
|
230
|
+
function collectFiles(dir, ext, out = []) {
|
|
231
|
+
let entries;
|
|
232
|
+
try {
|
|
233
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
234
|
+
} catch {
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
for (const e of entries) {
|
|
238
|
+
const full = join(dir, e.name);
|
|
239
|
+
if (EXCLUDE_RE.test(full)) continue;
|
|
240
|
+
if (e.isDirectory()) collectFiles(full, ext, out);
|
|
241
|
+
else if (e.name.endsWith(ext)) out.push(full);
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
/** Apply one pattern to every matching file and collect line-level findings. */
|
|
246
|
+
function scanPattern(dir, p) {
|
|
247
|
+
const ext = p.glob.replace(/^\*/, "");
|
|
248
|
+
const findings = [];
|
|
249
|
+
for (const file of collectFiles(dir, ext)) {
|
|
250
|
+
let text;
|
|
251
|
+
try {
|
|
252
|
+
text = readFileSync(file, "utf8");
|
|
253
|
+
} catch {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
text.split("\n").forEach((line, i) => {
|
|
257
|
+
if (p.regex.test(line)) findings.push({
|
|
258
|
+
severity: p.severity,
|
|
259
|
+
category: p.category,
|
|
260
|
+
pattern: p.regex.source,
|
|
261
|
+
file,
|
|
262
|
+
line: i + 1
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return findings;
|
|
267
|
+
}
|
|
268
|
+
/** Run the OWASP scan over `dir` and return the aggregated {@link ScanReport}. */
|
|
269
|
+
function runSecurityScan(dir) {
|
|
270
|
+
const language = detectLanguage(dir);
|
|
271
|
+
const counts = {
|
|
272
|
+
critical: 0,
|
|
273
|
+
high: 0,
|
|
274
|
+
medium: 0,
|
|
275
|
+
low: 0
|
|
276
|
+
};
|
|
277
|
+
const findings = [];
|
|
278
|
+
for (const p of getPatterns(language)) {
|
|
279
|
+
const hits = scanPattern(dir, p);
|
|
280
|
+
findings.push(...hits);
|
|
281
|
+
const key = p.severity.toLowerCase();
|
|
282
|
+
counts[key] = (counts[key] ?? 0) + hits.length;
|
|
283
|
+
}
|
|
284
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
285
|
+
return {
|
|
286
|
+
language,
|
|
287
|
+
directory: dir,
|
|
288
|
+
summary: {
|
|
289
|
+
...counts,
|
|
290
|
+
total
|
|
291
|
+
},
|
|
292
|
+
findings
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
//#endregion
|
|
81
296
|
//#region src/refs/discover.ts
|
|
82
297
|
/** Immediate subdirectory names of `p`, or `[]` when it is missing/inaccessible. */
|
|
83
298
|
function subdirs(p) {
|
|
@@ -178,7 +393,7 @@ function runningVersion(moduleUrl) {
|
|
|
178
393
|
path: found?.path ?? "unknown"
|
|
179
394
|
};
|
|
180
395
|
}
|
|
181
|
-
/** One-line `pkg vX.Y.Z` banner (
|
|
396
|
+
/** One-line `pkg vX.Y.Z` banner (stderr, only on explicit `--version`/`doctor` commands — never on `hook`, to avoid spamming automated invocations). */
|
|
182
397
|
function versionBanner(moduleUrl) {
|
|
183
398
|
return `${PKG} v${runningVersion(moduleUrl).version}`;
|
|
184
399
|
}
|
|
@@ -244,12 +459,14 @@ async function readStdin() {
|
|
|
244
459
|
}
|
|
245
460
|
}
|
|
246
461
|
const cmd = process.argv[2];
|
|
247
|
-
process.stderr.write(versionBanner(import.meta.url) + "\n");
|
|
248
462
|
if (cmd === "--version" || cmd === "-v") {
|
|
463
|
+
process.stderr.write(versionBanner(import.meta.url) + "\n");
|
|
249
464
|
process.stdout.write(runningVersion(import.meta.url).version + "\n");
|
|
250
465
|
process.exit(0);
|
|
251
|
-
} else if (cmd === "doctor")
|
|
252
|
-
|
|
466
|
+
} else if (cmd === "doctor") {
|
|
467
|
+
process.stderr.write(versionBanner(import.meta.url) + "\n");
|
|
468
|
+
process.exit(await runDoctor(import.meta.url));
|
|
469
|
+
} else if (cmd === "hook") {
|
|
253
470
|
const id = process.argv[3] ?? detectHarness().id;
|
|
254
471
|
loadDotenv(id);
|
|
255
472
|
const scopeArg = process.argv[4];
|
|
@@ -262,7 +479,8 @@ else if (cmd === "hook") {
|
|
|
262
479
|
"aipilot",
|
|
263
480
|
"lessons",
|
|
264
481
|
"seo",
|
|
265
|
-
"memory"
|
|
482
|
+
"memory",
|
|
483
|
+
"tailwindcss"
|
|
266
484
|
])).has(scopeArg) ? scopeArg : "core";
|
|
267
485
|
const marketplaces = (process.env.FUSE_HARNESS_MARKETPLACES ?? "fusengine-plugins").split(",").map((s) => s.trim()).filter(Boolean);
|
|
268
486
|
const refsDir = process.env.FUSE_HARNESS_REFS || discoverRefs(homedir(), process.cwd(), marketplaces) || void 0;
|
|
@@ -295,7 +513,11 @@ else if (cmd === "hook") {
|
|
|
295
513
|
}) + "\n");
|
|
296
514
|
process.exit(1);
|
|
297
515
|
}
|
|
298
|
-
else {
|
|
516
|
+
else if (cmd === "scan") {
|
|
517
|
+
const dir = process.argv[3] ?? process.cwd();
|
|
518
|
+
process.stdout.write(JSON.stringify(runSecurityScan(dir), null, 2) + "\n");
|
|
519
|
+
process.exit(0);
|
|
520
|
+
} else {
|
|
299
521
|
const files = stagedFiles();
|
|
300
522
|
if (files.length === 0) process.exit(0);
|
|
301
523
|
const violations = checkStaged(files, stagedContent);
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-
|
|
1
|
+
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-DLXtA5DH.mjs";
|
|
2
2
|
export { checkStaged, stagedContent, stagedFiles };
|
package/dist/config/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as parseEnvInt, a as ProjectLayout, c as projectLayout, d as resolveMaxLines, f as splitTarget, g as ttlLabel, h as resolveTtlSec, i as parseEnvFile, l as DEFAULT_MAX_LINES, m as TTL_ENV_KEY, n as envCandidates, o as STATE_GITIGNORE, p as DEFAULT_TTL_SEC, r as loadDotenv, s as STATE_ROOT, t as HOME_DIR, u as MAX_LINES_ENV_KEY } from "../index-
|
|
1
|
+
import { _ as parseEnvInt, a as ProjectLayout, c as projectLayout, d as resolveMaxLines, f as splitTarget, g as ttlLabel, h as resolveTtlSec, i as parseEnvFile, l as DEFAULT_MAX_LINES, m as TTL_ENV_KEY, n as envCandidates, o as STATE_GITIGNORE, p as DEFAULT_TTL_SEC, r as loadDotenv, s as STATE_ROOT, t as HOME_DIR, u as MAX_LINES_ENV_KEY } from "../index-BXPySPxE.mjs";
|
|
2
2
|
export { DEFAULT_MAX_LINES, DEFAULT_TTL_SEC, HOME_DIR, MAX_LINES_ENV_KEY, ProjectLayout, STATE_GITIGNORE, STATE_ROOT, TTL_ENV_KEY, envCandidates, loadDotenv, parseEnvFile, parseEnvInt, projectLayout, resolveMaxLines, resolveTtlSec, splitTarget, ttlLabel };
|
package/dist/config/index.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { a as parseEnvInt, i as splitTarget, n as MAX_LINES_ENV_KEY, r as resolveMaxLines, t as DEFAULT_MAX_LINES } from "../limits-CHn8AIL1.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { a as DEFAULT_TTL_SEC, c as ttlLabel, i as parseEnvFile, n as envCandidates, o as TTL_ENV_KEY, r as loadDotenv, s as resolveTtlSec, t as HOME_DIR } from "../dotenv-B9nM4cuQ.mjs";
|
|
3
3
|
import { n as STATE_ROOT, r as projectLayout, t as STATE_GITIGNORE } from "../layout-C0jaaCQC.mjs";
|
|
4
|
-
import { i as parseEnvFile, n as envCandidates, r as loadDotenv, t as HOME_DIR } from "../dotenv-DGyLln7U.mjs";
|
|
5
4
|
import "../config-la_KkjCS.mjs";
|
|
6
5
|
export { DEFAULT_MAX_LINES, DEFAULT_TTL_SEC, HOME_DIR, MAX_LINES_ENV_KEY, STATE_GITIGNORE, STATE_ROOT, TTL_ENV_KEY, envCandidates, loadDotenv, parseEnvFile, parseEnvInt, projectLayout, resolveMaxLines, resolveTtlSec, splitTarget, ttlLabel };
|
|
@@ -22,7 +22,15 @@ interface DocSatisfactionStatus {
|
|
|
22
22
|
web: boolean;
|
|
23
23
|
viaCache: boolean;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* True when documentation research is satisfied for the session. Primary path
|
|
27
|
+
* is parity with the Python `mcp_research_done`
|
|
28
|
+
* (`_shared/scripts/check_skill_common.py`): `"context7:" in content and
|
|
29
|
+
* "exa:" in content`, a strict AND (the Python source has no web-alone path).
|
|
30
|
+
* `web` (WebSearch/WebFetch/fuse-browser) is a deliberate TS-side addition —
|
|
31
|
+
* it alone satisfies the gate, since fuse-browser is this harness's preferred
|
|
32
|
+
* doc-research fast-path (see CLAUDE.md) and Context7/Exa can be unavailable.
|
|
33
|
+
*/
|
|
26
34
|
declare function isDocConsulted(authorizations: Record<string, AuthEntry> | undefined, sessionId: string): boolean;
|
|
27
35
|
/** Report how each doc source was satisfied for a session. */
|
|
28
36
|
declare function formatDocSatisfactionStatus(authorizations: Record<string, AuthEntry> | undefined, sessionId: string): DocSatisfactionStatus;
|
|
@@ -23,10 +23,18 @@ function evaluateDoc(auths) {
|
|
|
23
23
|
viaCache: !liveC7 && cacheC7 || !liveExa && cacheExa
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* True when documentation research is satisfied for the session. Primary path
|
|
28
|
+
* is parity with the Python `mcp_research_done`
|
|
29
|
+
* (`_shared/scripts/check_skill_common.py`): `"context7:" in content and
|
|
30
|
+
* "exa:" in content`, a strict AND (the Python source has no web-alone path).
|
|
31
|
+
* `web` (WebSearch/WebFetch/fuse-browser) is a deliberate TS-side addition —
|
|
32
|
+
* it alone satisfies the gate, since fuse-browser is this harness's preferred
|
|
33
|
+
* doc-research fast-path (see CLAUDE.md) and Context7/Exa can be unavailable.
|
|
34
|
+
*/
|
|
27
35
|
function isDocConsulted(authorizations, sessionId) {
|
|
28
36
|
const s = evaluateDoc(sessionAuthsFor(authorizations, sessionId));
|
|
29
|
-
return s.context7
|
|
37
|
+
return s.context7 && s.exa || s.web;
|
|
30
38
|
}
|
|
31
39
|
/** Report how each doc source was satisfied for a session. */
|
|
32
40
|
function formatDocSatisfactionStatus(authorizations, sessionId) {
|
|
@@ -35,9 +43,10 @@ function formatDocSatisfactionStatus(authorizations, sessionId) {
|
|
|
35
43
|
/** Deny message when online documentation has not been consulted. */
|
|
36
44
|
function formatDocDeny(framework) {
|
|
37
45
|
return [
|
|
38
|
-
`
|
|
39
|
-
"
|
|
40
|
-
"
|
|
46
|
+
`BLOCKED: No MCP research done for ${framework}. Use BOTH:`,
|
|
47
|
+
"1) mcp__context7__query-docs AND 2) mcp__exa__web_search_exa",
|
|
48
|
+
"— OR a web fallback alone: mcp__fuse-browser__browser_fetch / browser_crawl / browser_serp_batch, WebSearch, or WebFetch.",
|
|
49
|
+
"This check is once per session — after consulting the required source(s), Write/Edit will be allowed."
|
|
41
50
|
].join("\n");
|
|
42
51
|
}
|
|
43
52
|
//#endregion
|
|
@@ -1,6 +1,31 @@
|
|
|
1
|
+
import { a as parseEnvInt } from "./limits-CHn8AIL1.mjs";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
5
|
+
//#region src/config/ttl.ts
|
|
6
|
+
/**
|
|
7
|
+
* Default enforcement-freshness window, in seconds (2 minutes). Matches the
|
|
8
|
+
* plugin's original `FUSE_ENFORCE_TTL_SEC` default. This is the constant that
|
|
9
|
+
* actually reaches production (via `resolveTtlSec` -> `bin.ts`); `gate.ts`'s
|
|
10
|
+
* `DEFAULT_WINDOW_MS` is only a fallback for direct programmatic callers that
|
|
11
|
+
* omit `windowMs` (e.g. tests) and never applies on the real CLI path.
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_TTL_SEC = 120;
|
|
14
|
+
/** Default env var name carrying the TTL override. */
|
|
15
|
+
const TTL_ENV_KEY = "FUSE_ENFORCE_TTL_SEC";
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the enforcement TTL (seconds) from an env map.
|
|
18
|
+
* @param env - environment map (defaults to `process.env`)
|
|
19
|
+
* @param key - env var name (defaults to `FUSE_ENFORCE_TTL_SEC`)
|
|
20
|
+
*/
|
|
21
|
+
function resolveTtlSec(env = process.env, key = TTL_ENV_KEY) {
|
|
22
|
+
return parseEnvInt(env[key], 120);
|
|
23
|
+
}
|
|
24
|
+
/** Human label for a TTL: 120 -> "2min", 240 -> "4min", 90 -> "90s". */
|
|
25
|
+
function ttlLabel(sec) {
|
|
26
|
+
return sec % 60 === 0 ? `${sec / 60}min` : `${sec}s`;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
4
29
|
//#region src/config/dotenv.ts
|
|
5
30
|
/**
|
|
6
31
|
* Native .env loader. Ports the claude-plugins `services/env-file.ts`
|
|
@@ -52,4 +77,4 @@ function loadDotenv(id, env = process.env, home = homedir(), cwd = process.cwd()
|
|
|
52
77
|
for (const path of envCandidates(id, home, cwd)) for (const [k, v] of Object.entries(parseEnvFile(path))) if (env[k] === void 0) env[k] = v;
|
|
53
78
|
}
|
|
54
79
|
//#endregion
|
|
55
|
-
export { parseEnvFile as i, envCandidates as n, loadDotenv as r, HOME_DIR as t };
|
|
80
|
+
export { DEFAULT_TTL_SEC as a, ttlLabel as c, parseEnvFile as i, envCandidates as n, TTL_ENV_KEY as o, loadDotenv as r, resolveTtlSec as s, HOME_DIR as t };
|