@fusengine/harness 0.1.0
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/LICENSE +21 -0
- package/README.md +68 -0
- package/dist/adapters/claude/index.d.mts +36 -0
- package/dist/adapters/claude/index.mjs +66 -0
- package/dist/cache/index.d.mts +30 -0
- package/dist/cache/index.mjs +110 -0
- package/dist/compact-json-DK2nX-MK.mjs +21 -0
- package/dist/config/index.d.mts +37 -0
- package/dist/config/index.mjs +20 -0
- package/dist/detect/index.d.mts +31 -0
- package/dist/detect/index.mjs +82 -0
- package/dist/doc-helpers-CG1nuf-c.d.mts +30 -0
- package/dist/evaluate-CsYyUucy.mjs +152 -0
- package/dist/freshness/index.d.mts +14 -0
- package/dist/freshness/index.mjs +64 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.mjs +17 -0
- package/dist/json-io-RH82El2J.mjs +40 -0
- package/dist/limits-CHn8AIL1.mjs +31 -0
- package/dist/memory/index.d.mts +30 -0
- package/dist/memory/index.mjs +94 -0
- package/dist/policy/index.d.mts +80 -0
- package/dist/policy/index.mjs +34 -0
- package/dist/project-root-C4ks_q1G.mjs +32 -0
- package/dist/prompt/index.d.mts +2 -0
- package/dist/prompt/index.mjs +2 -0
- package/dist/refs/index.d.mts +44 -0
- package/dist/refs/index.mjs +70 -0
- package/dist/state/index.d.mts +59 -0
- package/dist/state/index.mjs +104 -0
- package/dist/statusline/index.d.mts +84 -0
- package/dist/statusline/index.mjs +148 -0
- package/dist/types-D56jSgD9.d.mts +21 -0
- package/dist/types-ernB1Dy3.mjs +12 -0
- package/dist/util/index.d.mts +20 -0
- package/dist/util/index.mjs +3 -0
- package/package.json +46 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { i as splitTarget, r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
|
|
2
|
+
import { t as isCodeFile } from "./project-root-C4ks_q1G.mjs";
|
|
3
|
+
//#region src/policy/detect-framework.ts
|
|
4
|
+
/**
|
|
5
|
+
* Detect the framework from a file path extension + content patterns.
|
|
6
|
+
* Aligned with the fusengine require-solid-read detection (distinct from
|
|
7
|
+
* {@link detectProjectType}, which scans config files on disk).
|
|
8
|
+
*/
|
|
9
|
+
function detectFramework(filePath, content) {
|
|
10
|
+
if (/\.(tsx?|jsx?|vue|svelte)$/.test(filePath) || /from ['"]react|useState|className=/.test(content)) {
|
|
11
|
+
if (/(page|layout|loading|error|route)\.(ts|tsx)$/.test(filePath) || /use client|use server/.test(content)) return "nextjs";
|
|
12
|
+
return "react";
|
|
13
|
+
}
|
|
14
|
+
if (/\.swift$/.test(filePath)) return "swift";
|
|
15
|
+
if (/\.php$/.test(filePath)) return "laravel";
|
|
16
|
+
if (/\.java$/.test(filePath)) return "java";
|
|
17
|
+
if (/\.go$/.test(filePath)) return "go";
|
|
18
|
+
if (/\.rb$/.test(filePath)) return "ruby";
|
|
19
|
+
if (/\.rs$/.test(filePath)) return "rust";
|
|
20
|
+
if (/\.css$/.test(filePath) || /@tailwind|@apply/.test(content)) return "tailwind";
|
|
21
|
+
return "generic";
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/policy/file-size.ts
|
|
25
|
+
/** Count lines in file content (empty string = 0). */
|
|
26
|
+
function countLines(content) {
|
|
27
|
+
return content === "" ? 0 : content.split("\n").length;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Evaluate a file's line count against the SOLID limit.
|
|
31
|
+
* @param lines - the file's line count
|
|
32
|
+
* @param max - the limit (defaults to `resolveMaxLines()`)
|
|
33
|
+
*/
|
|
34
|
+
function evaluateFileSize(lines, max = resolveMaxLines()) {
|
|
35
|
+
if (lines <= max) return {
|
|
36
|
+
ok: true,
|
|
37
|
+
lines,
|
|
38
|
+
max,
|
|
39
|
+
message: null
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
lines,
|
|
44
|
+
max,
|
|
45
|
+
message: `File has ${lines} lines (max: ${max}). Split into modules under ${splitTarget(max)} lines (Single Responsibility).`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/policy/patterns.ts
|
|
50
|
+
/**
|
|
51
|
+
* Guard pattern data, ported verbatim from the fusengine git/install guards.
|
|
52
|
+
* Note (faithful): `git push.*--force` also matches `--force-with-lease` —
|
|
53
|
+
* preserved from the source guard.
|
|
54
|
+
*/
|
|
55
|
+
/** Destructive git operations to block outright. */
|
|
56
|
+
const GIT_BLOCKED = [
|
|
57
|
+
/git push.*--force/,
|
|
58
|
+
/git push.*-f/,
|
|
59
|
+
/git reset.*--hard/,
|
|
60
|
+
/git clean.*-fd/,
|
|
61
|
+
/git branch.*-D/,
|
|
62
|
+
/git rebase.*--force/
|
|
63
|
+
];
|
|
64
|
+
/** Git operations that warrant a confirmation prompt. */
|
|
65
|
+
const GIT_ASK = [
|
|
66
|
+
/git push/,
|
|
67
|
+
/git checkout/,
|
|
68
|
+
/git reset/,
|
|
69
|
+
/git rebase/,
|
|
70
|
+
/git merge/,
|
|
71
|
+
/git stash/,
|
|
72
|
+
/git clean/,
|
|
73
|
+
/git rm/,
|
|
74
|
+
/git mv/,
|
|
75
|
+
/git restore/,
|
|
76
|
+
/git revert/,
|
|
77
|
+
/git cherry-pick/
|
|
78
|
+
];
|
|
79
|
+
/** System-level package installs (need confirmation). */
|
|
80
|
+
const SYSTEM_INSTALL = [
|
|
81
|
+
/brew install/,
|
|
82
|
+
/brew upgrade/,
|
|
83
|
+
/brew cask/,
|
|
84
|
+
/apt install/,
|
|
85
|
+
/apt-get install/
|
|
86
|
+
];
|
|
87
|
+
/** Project-level package installs. */
|
|
88
|
+
const PROJECT_INSTALL = [
|
|
89
|
+
/npm install/,
|
|
90
|
+
/npm i /,
|
|
91
|
+
/yarn add/,
|
|
92
|
+
/pnpm add/,
|
|
93
|
+
/pip install/,
|
|
94
|
+
/pip3 install/,
|
|
95
|
+
/composer require/,
|
|
96
|
+
/bun add/,
|
|
97
|
+
/bun install/,
|
|
98
|
+
/cargo install/,
|
|
99
|
+
/go install/,
|
|
100
|
+
/gem install/,
|
|
101
|
+
/pipx install/
|
|
102
|
+
];
|
|
103
|
+
/** True when `cmd` matches any pattern in `patterns`. */
|
|
104
|
+
function matchPatterns(cmd, patterns) {
|
|
105
|
+
return patterns.some((re) => re.test(cmd));
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/policy/evaluate.ts
|
|
109
|
+
/**
|
|
110
|
+
* Evaluate a single tool-use against the bundled policies, returning a pure
|
|
111
|
+
* decision plus a portable {@link Prompt}. Adapters translate the prompt into
|
|
112
|
+
* their harness's native response (Claude `permissionDecision`, etc.).
|
|
113
|
+
*/
|
|
114
|
+
function evaluate(ctx) {
|
|
115
|
+
if (ctx.command && matchPatterns(ctx.command, GIT_BLOCKED)) {
|
|
116
|
+
const reason = `Destructive git command: ${ctx.command}`;
|
|
117
|
+
return {
|
|
118
|
+
decision: "deny",
|
|
119
|
+
message: reason,
|
|
120
|
+
prompt: {
|
|
121
|
+
kind: "block",
|
|
122
|
+
title: "Destructive git command",
|
|
123
|
+
reason,
|
|
124
|
+
actions: ["Use a non-destructive alternative (e.g. --force-with-lease; avoid --hard / -D)"]
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (ctx.filePath && isCodeFile(ctx.filePath) && ctx.content !== void 0) {
|
|
129
|
+
const verdict = evaluateFileSize(countLines(ctx.content), ctx.maxLines);
|
|
130
|
+
if (!verdict.ok) return {
|
|
131
|
+
decision: "deny",
|
|
132
|
+
message: verdict.message,
|
|
133
|
+
prompt: {
|
|
134
|
+
kind: "block",
|
|
135
|
+
title: "SOLID file-size limit",
|
|
136
|
+
reason: verdict.message ?? "",
|
|
137
|
+
actions: [`Split into modules under ${verdict.max} lines (Single Responsibility)`, "Then re-run the write"]
|
|
138
|
+
},
|
|
139
|
+
meta: {
|
|
140
|
+
framework: detectFramework(ctx.filePath, ctx.content),
|
|
141
|
+
lines: verdict.lines,
|
|
142
|
+
max: verdict.max
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
decision: "allow",
|
|
148
|
+
message: null
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { SYSTEM_INSTALL as a, evaluateFileSize as c, PROJECT_INSTALL as i, detectFramework as l, GIT_ASK as n, matchPatterns as o, GIT_BLOCKED as r, countLines as s, evaluate as t };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { a as isDocConsulted, i as formatDocSatisfactionStatus, n as DocSatisfactionStatus, o as resolveSessions, r as formatDocDeny, t as AuthEntry } from "../doc-helpers-CG1nuf-c.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/freshness/trivial-edit-counter.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Increment a session's trivial-edit counter, evicting timestamps older than
|
|
6
|
+
* `windowMs`. Decoupled + injectable `now` for testability.
|
|
7
|
+
* @param filePath - session state file path
|
|
8
|
+
* @param windowMs - sliding window in ms
|
|
9
|
+
* @param now - current epoch ms (defaults to `Date.now()`)
|
|
10
|
+
* @returns number of trivial edits within the window (including this one)
|
|
11
|
+
*/
|
|
12
|
+
declare function incrementTrivialEditCounter(filePath: string, windowMs: number, now?: number): Promise<number>;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { AuthEntry, DocSatisfactionStatus, formatDocDeny, formatDocSatisfactionStatus, incrementTrivialEditCounter, isDocConsulted, resolveSessions };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { n as readJsonFile, r as writeJsonFile, t as ensureDir } from "../json-io-RH82El2J.mjs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
//#region src/freshness/doc-helpers.ts
|
|
4
|
+
/** Resolve the sessions array from an auth entry (legacy fallback). */
|
|
5
|
+
function resolveSessions(auth) {
|
|
6
|
+
if (!auth) return [];
|
|
7
|
+
return auth.sessions ?? (auth.session ? [auth.session] : []);
|
|
8
|
+
}
|
|
9
|
+
function sessionAuthsFor(authorizations, sessionId) {
|
|
10
|
+
if (!authorizations) return [];
|
|
11
|
+
return Object.values(authorizations).filter((a) => a.doc_sessions?.includes(sessionId));
|
|
12
|
+
}
|
|
13
|
+
function evaluateDoc(auths) {
|
|
14
|
+
const sources = auths.flatMap((a) => a.sources ?? [a.source ?? ""]);
|
|
15
|
+
const readPaths = auths.flatMap((a) => a.read_paths ?? []);
|
|
16
|
+
const liveC7 = sources.some((s) => /context7/.test(s));
|
|
17
|
+
const liveExa = sources.some((s) => /exa/.test(s));
|
|
18
|
+
const cacheC7 = readPaths.some((p) => /\/context\/mcp\/context7-/.test(p));
|
|
19
|
+
const cacheExa = readPaths.some((p) => /\/context\/mcp\/(exa-search|exa-code-context)-/.test(p));
|
|
20
|
+
return {
|
|
21
|
+
context7: liveC7 || cacheC7,
|
|
22
|
+
exa: liveExa || cacheExa,
|
|
23
|
+
viaCache: !liveC7 && cacheC7 || !liveExa && cacheExa
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** True when BOTH Context7 and Exa were satisfied for the session. */
|
|
27
|
+
function isDocConsulted(authorizations, sessionId) {
|
|
28
|
+
const s = evaluateDoc(sessionAuthsFor(authorizations, sessionId));
|
|
29
|
+
return s.context7 && s.exa;
|
|
30
|
+
}
|
|
31
|
+
/** Report how each doc source was satisfied for a session. */
|
|
32
|
+
function formatDocSatisfactionStatus(authorizations, sessionId) {
|
|
33
|
+
return evaluateDoc(sessionAuthsFor(authorizations, sessionId));
|
|
34
|
+
}
|
|
35
|
+
/** Deny message when online documentation has not been consulted. */
|
|
36
|
+
function formatDocDeny(framework) {
|
|
37
|
+
return [
|
|
38
|
+
`APEX: Online documentation not consulted for ${framework}!`,
|
|
39
|
+
"Use BOTH: 1) mcp__context7__query-docs AND 2) mcp__exa__web_search_exa.",
|
|
40
|
+
"This check is once per session — after consulting both, Write/Edit will be allowed."
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/freshness/trivial-edit-counter.ts
|
|
45
|
+
/**
|
|
46
|
+
* Increment a session's trivial-edit counter, evicting timestamps older than
|
|
47
|
+
* `windowMs`. Decoupled + injectable `now` for testability.
|
|
48
|
+
* @param filePath - session state file path
|
|
49
|
+
* @param windowMs - sliding window in ms
|
|
50
|
+
* @param now - current epoch ms (defaults to `Date.now()`)
|
|
51
|
+
* @returns number of trivial edits within the window (including this one)
|
|
52
|
+
*/
|
|
53
|
+
async function incrementTrivialEditCounter(filePath, windowMs, now = Date.now()) {
|
|
54
|
+
await ensureDir(dirname(filePath));
|
|
55
|
+
const state = await readJsonFile(filePath) ?? {};
|
|
56
|
+
const cutoff = now - windowMs;
|
|
57
|
+
const edits = (state.trivial_edits ?? []).filter((ts) => ts > cutoff);
|
|
58
|
+
edits.push(now);
|
|
59
|
+
state.trivial_edits = edits;
|
|
60
|
+
await writeJsonFile(filePath, state);
|
|
61
|
+
return edits.length;
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { formatDocDeny, formatDocSatisfactionStatus, incrementTrivialEditCounter, isDocConsulted, resolveSessions };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { n as PromptKind, r as formatPrompt, t as Prompt } from "./types-D56jSgD9.mjs";
|
|
2
|
+
import { IndexSummary, compactMarkdown, extractText, jaccardSimilar, loadIndex, queryHash, summarizeIndex } from "./cache/index.mjs";
|
|
3
|
+
import { DEFAULT_MAX_LINES, DEFAULT_TTL_SEC, MAX_LINES_ENV_KEY, TTL_ENV_KEY, parseEnvInt, resolveMaxLines, resolveTtlSec, splitTarget, ttlLabel } from "./config/index.mjs";
|
|
4
|
+
import { HarnessId, HarnessInfo, HarnessMode, HarnessVia, detectHarness, detectMode, modeFor } from "./detect/index.mjs";
|
|
5
|
+
import { a as isDocConsulted, i as formatDocSatisfactionStatus, n as DocSatisfactionStatus, o as resolveSessions, r as formatDocDeny, t as AuthEntry } from "./doc-helpers-CG1nuf-c.mjs";
|
|
6
|
+
import { incrementTrivialEditCounter } from "./freshness/index.mjs";
|
|
7
|
+
import { compactJson, isCodeFile, projectRoot, projectRootOrNull } from "./util/index.mjs";
|
|
8
|
+
import { DEV_KEYWORDS, FileSizeVerdict, GIT_ASK, GIT_BLOCKED, PROJECT_INSTALL, PolicyContext, PolicyResult, ProjectType, SYSTEM_INSTALL, countLines, detectFramework, detectProjectType, evaluate, evaluateFileSize, isApexCommand, matchPatterns } from "./policy/index.mjs";
|
|
9
|
+
import { ReminderState, addRoot, ensureMemoryGitignore, nowStamp, readRoots, readState, registryFile, setStateField, stateFileFor, throttleMs } from "./memory/index.mjs";
|
|
10
|
+
import { RefMeta, RouteResult, ScoredRef, globToRe, parseFrontmatter, routeReferences, scoreReferences } from "./refs/index.mjs";
|
|
11
|
+
import { ApexState, ApexTask, ApexTaskFile, acquireLock, apexStateDir, ensureStateDir, loadState, saveState, stateFilePath, taskComplete, taskCreate, taskStart } from "./state/index.mjs";
|
|
12
|
+
import { COLOR_THRESHOLDS, ColorFn, GRADIENT_BLOCKS, PROGRESS_BAR_DEFAULTS, PROGRESS_CHARS, Palette, ProgressBarOptions, TIME_INTERVALS, colors, formatBasename, formatCost, formatPath, formatTimeLeft, formatTokens, generateGradientBar, generateProgressBar, progressiveColor } from "./statusline/index.mjs";
|
|
13
|
+
export { ApexState, ApexTask, ApexTaskFile, AuthEntry, COLOR_THRESHOLDS, ColorFn, DEFAULT_MAX_LINES, DEFAULT_TTL_SEC, DEV_KEYWORDS, DocSatisfactionStatus, FileSizeVerdict, GIT_ASK, GIT_BLOCKED, GRADIENT_BLOCKS, HarnessId, HarnessInfo, HarnessMode, HarnessVia, IndexSummary, MAX_LINES_ENV_KEY, PROGRESS_BAR_DEFAULTS, PROGRESS_CHARS, PROJECT_INSTALL, Palette, PolicyContext, PolicyResult, ProgressBarOptions, ProjectType, Prompt, PromptKind, RefMeta, ReminderState, RouteResult, SYSTEM_INSTALL, ScoredRef, TIME_INTERVALS, TTL_ENV_KEY, acquireLock, addRoot, apexStateDir, colors, compactJson, compactMarkdown, countLines, detectFramework, detectHarness, detectMode, detectProjectType, ensureMemoryGitignore, ensureStateDir, evaluate, evaluateFileSize, extractText, formatBasename, formatCost, formatDocDeny, formatDocSatisfactionStatus, formatPath, formatPrompt, formatTimeLeft, formatTokens, generateGradientBar, generateProgressBar, globToRe, incrementTrivialEditCounter, isApexCommand, isCodeFile, isDocConsulted, jaccardSimilar, loadIndex, loadState, matchPatterns, modeFor, nowStamp, parseEnvInt, parseFrontmatter, progressiveColor, projectRoot, projectRootOrNull, queryHash, readRoots, readState, registryFile, resolveMaxLines, resolveSessions, resolveTtlSec, routeReferences, saveState, scoreReferences, setStateField, splitTarget, stateFileFor, stateFilePath, summarizeIndex, taskComplete, taskCreate, taskStart, throttleMs, ttlLabel };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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 { DEFAULT_TTL_SEC, TTL_ENV_KEY, resolveTtlSec, ttlLabel } from "./config/index.mjs";
|
|
3
|
+
import { t as compactJson } from "./compact-json-DK2nX-MK.mjs";
|
|
4
|
+
import { n as projectRoot, r as projectRootOrNull, t as isCodeFile } from "./project-root-C4ks_q1G.mjs";
|
|
5
|
+
import "./util/index.mjs";
|
|
6
|
+
import { detectHarness, detectMode, modeFor } from "./detect/index.mjs";
|
|
7
|
+
import { a as SYSTEM_INSTALL, c as evaluateFileSize, i as PROJECT_INSTALL, l as detectFramework, n as GIT_ASK, o as matchPatterns, r as GIT_BLOCKED, s as countLines, t as evaluate } from "./evaluate-CsYyUucy.mjs";
|
|
8
|
+
import { DEV_KEYWORDS, detectProjectType, isApexCommand } from "./policy/index.mjs";
|
|
9
|
+
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
10
|
+
import "./prompt/index.mjs";
|
|
11
|
+
import { addRoot, ensureMemoryGitignore, nowStamp, readRoots, readState, registryFile, setStateField, stateFileFor, throttleMs } from "./memory/index.mjs";
|
|
12
|
+
import { compactMarkdown, extractText, jaccardSimilar, loadIndex, queryHash, summarizeIndex } from "./cache/index.mjs";
|
|
13
|
+
import { formatDocDeny, formatDocSatisfactionStatus, incrementTrivialEditCounter, isDocConsulted, resolveSessions } from "./freshness/index.mjs";
|
|
14
|
+
import { globToRe, parseFrontmatter, routeReferences, scoreReferences } from "./refs/index.mjs";
|
|
15
|
+
import { acquireLock, apexStateDir, ensureStateDir, loadState, saveState, stateFilePath, taskComplete, taskCreate, taskStart } from "./state/index.mjs";
|
|
16
|
+
import { COLOR_THRESHOLDS, GRADIENT_BLOCKS, PROGRESS_BAR_DEFAULTS, PROGRESS_CHARS, TIME_INTERVALS, colors, formatBasename, formatCost, formatPath, formatTimeLeft, formatTokens, generateGradientBar, generateProgressBar, progressiveColor } from "./statusline/index.mjs";
|
|
17
|
+
export { COLOR_THRESHOLDS, DEFAULT_MAX_LINES, DEFAULT_TTL_SEC, DEV_KEYWORDS, GIT_ASK, GIT_BLOCKED, GRADIENT_BLOCKS, MAX_LINES_ENV_KEY, PROGRESS_BAR_DEFAULTS, PROGRESS_CHARS, PROJECT_INSTALL, SYSTEM_INSTALL, TIME_INTERVALS, TTL_ENV_KEY, acquireLock, addRoot, apexStateDir, colors, compactJson, compactMarkdown, countLines, detectFramework, detectHarness, detectMode, detectProjectType, ensureMemoryGitignore, ensureStateDir, evaluate, evaluateFileSize, extractText, formatBasename, formatCost, formatDocDeny, formatDocSatisfactionStatus, formatPath, formatPrompt, formatTimeLeft, formatTokens, generateGradientBar, generateProgressBar, globToRe, incrementTrivialEditCounter, isApexCommand, isCodeFile, isDocConsulted, jaccardSimilar, loadIndex, loadState, matchPatterns, modeFor, nowStamp, parseEnvInt, parseFrontmatter, progressiveColor, projectRoot, projectRootOrNull, queryHash, readRoots, readState, registryFile, resolveMaxLines, resolveSessions, resolveTtlSec, routeReferences, saveState, scoreReferences, setStateField, splitTarget, stateFileFor, stateFilePath, summarizeIndex, taskComplete, taskCreate, taskStart, throttleMs, ttlLabel };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { t as compactJson } from "./compact-json-DK2nX-MK.mjs";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { mkdir } from "node:fs/promises";
|
|
6
|
+
//#region src/util/json-io.ts
|
|
7
|
+
/** Atomically write `data` to `path` (temp + rename, 0o600). Cross-FS safe on macOS/Linux. */
|
|
8
|
+
function atomicWrite(path, data) {
|
|
9
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
10
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
11
|
+
try {
|
|
12
|
+
writeFileSync(tmp, data, { encoding: "utf8" });
|
|
13
|
+
chmodSync(tmp, 384);
|
|
14
|
+
renameSync(tmp, path);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
try {
|
|
17
|
+
unlinkSync(tmp);
|
|
18
|
+
} catch {}
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Ensure a directory exists. */
|
|
23
|
+
async function ensureDir(dir) {
|
|
24
|
+
await mkdir(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
/** Read & JSON-parse a file; null on missing/corrupt. */
|
|
27
|
+
async function readJsonFile(path) {
|
|
28
|
+
try {
|
|
29
|
+
if (!existsSync(path)) return null;
|
|
30
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Atomically write JSON (compact form when `compact`). */
|
|
36
|
+
async function writeJsonFile(path, data, compact = false) {
|
|
37
|
+
atomicWrite(path, compact ? compactJson(data) : JSON.stringify(data, null, 2));
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { readJsonFile as n, writeJsonFile as r, ensureDir as t };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
//#region src/config/env.ts
|
|
2
|
+
/**
|
|
3
|
+
* Robust integer-from-env parser.
|
|
4
|
+
* undefined / empty / whitespace / NaN / float / <= 0 all fall back to `fallback`.
|
|
5
|
+
* `Number("")` is 0, so the empty guard is required before `Number()`.
|
|
6
|
+
*/
|
|
7
|
+
function parseEnvInt(raw, fallback) {
|
|
8
|
+
if (!raw?.trim()) return fallback;
|
|
9
|
+
const n = Number(raw.trim());
|
|
10
|
+
return Number.isInteger(n) && n > 0 ? n : fallback;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/config/limits.ts
|
|
14
|
+
/** Default SOLID max lines per file. */
|
|
15
|
+
const DEFAULT_MAX_LINES = 100;
|
|
16
|
+
/** Default env var name carrying the max-lines override. */
|
|
17
|
+
const MAX_LINES_ENV_KEY = "FUSE_SOLID_MAX_LINES";
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the SOLID max-lines limit from an env map.
|
|
20
|
+
* @param env - environment map (defaults to `process.env`)
|
|
21
|
+
* @param key - env var name (defaults to `FUSE_SOLID_MAX_LINES`)
|
|
22
|
+
*/
|
|
23
|
+
function resolveMaxLines(env = process.env, key = MAX_LINES_ENV_KEY) {
|
|
24
|
+
return parseEnvInt(env[key], 100);
|
|
25
|
+
}
|
|
26
|
+
/** Advisory module-split headroom = `maxLines - 10` (never below 1). */
|
|
27
|
+
function splitTarget(maxLines) {
|
|
28
|
+
return Math.max(maxLines - 10, 1);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { parseEnvInt as a, splitTarget as i, MAX_LINES_ENV_KEY as n, resolveMaxLines as r, DEFAULT_MAX_LINES as t };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/memory/state.d.ts
|
|
2
|
+
/** Per-project reminder throttle state, stored at `<root>/MEMORY/state.json`. */
|
|
3
|
+
interface ReminderState {
|
|
4
|
+
lastRemindedAt: number;
|
|
5
|
+
lastCodeEditAt: number;
|
|
6
|
+
}
|
|
7
|
+
/** Absolute path of the per-project throttle state file. */
|
|
8
|
+
declare function stateFileFor(root: string): string;
|
|
9
|
+
/** Read both throttle timestamps; missing/corrupt fields default to 0. */
|
|
10
|
+
declare function readState(file: string): ReminderState;
|
|
11
|
+
/** Persist one field without clobbering the other (read-modify-write). */
|
|
12
|
+
declare function setStateField(file: string, key: keyof ReminderState, value: number): void;
|
|
13
|
+
/** Local wall-clock timestamp for a lesson bullet: `YYYY-MM-DD HH:MM`. */
|
|
14
|
+
declare function nowStamp(): string;
|
|
15
|
+
/** Throttle window (ms) from `FUSE_LESSONS_THROTTLE_MIN` (default 5 min). */
|
|
16
|
+
declare function throttleMs(env?: Record<string, string | undefined>): number;
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/memory/registry.d.ts
|
|
19
|
+
/** Absolute path of the global roots registry, or null when home is unusable. */
|
|
20
|
+
declare function registryFile(home?: string | undefined): string | null;
|
|
21
|
+
/** Read the registered project roots (deduplicated string entries only). */
|
|
22
|
+
declare function readRoots(home?: string): string[];
|
|
23
|
+
/** Register a project root once (deduplicated, read-modify-write, non-throwing). */
|
|
24
|
+
declare function addRoot(root: string, home?: string): void;
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/memory/gitignore.d.ts
|
|
27
|
+
/** Ensure `<memoryDir>/.gitignore` ignores the machine-local `state.json`. */
|
|
28
|
+
declare function ensureMemoryGitignore(memoryDir: string): void;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ReminderState, addRoot, ensureMemoryGitignore, nowStamp, readRoots, readState, registryFile, setStateField, stateFileFor, throttleMs };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
//#region src/memory/gitignore.ts
|
|
4
|
+
/** Ensure `<memoryDir>/.gitignore` ignores the machine-local `state.json`. */
|
|
5
|
+
function ensureMemoryGitignore(memoryDir) {
|
|
6
|
+
const file = `${memoryDir}/.gitignore`;
|
|
7
|
+
try {
|
|
8
|
+
const existing = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
9
|
+
if (/^state\.json$/m.test(existing)) return;
|
|
10
|
+
writeFileSync(file, existing.trim() ? `${existing.trimEnd()}\nstate.json\n` : "state.json\n");
|
|
11
|
+
} catch {}
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/memory/state.ts
|
|
15
|
+
/** Absolute path of the per-project throttle state file. */
|
|
16
|
+
function stateFileFor(root) {
|
|
17
|
+
return `${root}/MEMORY/state.json`;
|
|
18
|
+
}
|
|
19
|
+
function num(v) {
|
|
20
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
21
|
+
}
|
|
22
|
+
/** Read both throttle timestamps; missing/corrupt fields default to 0. */
|
|
23
|
+
function readState(file) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
26
|
+
return {
|
|
27
|
+
lastRemindedAt: num(parsed?.lastRemindedAt),
|
|
28
|
+
lastCodeEditAt: num(parsed?.lastCodeEditAt)
|
|
29
|
+
};
|
|
30
|
+
} catch {
|
|
31
|
+
return {
|
|
32
|
+
lastRemindedAt: 0,
|
|
33
|
+
lastCodeEditAt: 0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Persist one field without clobbering the other (read-modify-write). */
|
|
38
|
+
function setStateField(file, key, value) {
|
|
39
|
+
const next = {
|
|
40
|
+
...readState(file),
|
|
41
|
+
[key]: value
|
|
42
|
+
};
|
|
43
|
+
const dir = dirname(file);
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
ensureMemoryGitignore(dir);
|
|
46
|
+
writeFileSync(file, JSON.stringify(next));
|
|
47
|
+
}
|
|
48
|
+
/** Local wall-clock timestamp for a lesson bullet: `YYYY-MM-DD HH:MM`. */
|
|
49
|
+
function nowStamp() {
|
|
50
|
+
const d = /* @__PURE__ */ new Date();
|
|
51
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
52
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
53
|
+
}
|
|
54
|
+
/** Throttle window (ms) from `FUSE_LESSONS_THROTTLE_MIN` (default 5 min). */
|
|
55
|
+
function throttleMs(env = process.env) {
|
|
56
|
+
const raw = env.FUSE_LESSONS_THROTTLE_MIN?.trim();
|
|
57
|
+
const min = raw ? Number(raw) : 5;
|
|
58
|
+
return (Number.isFinite(min) ? Math.max(0, min) : 5) * 6e4;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/memory/registry.ts
|
|
62
|
+
/** Registry path relative to the home dir. */
|
|
63
|
+
const SUBPATH = ".claude/fusengine-cache/lessons/roots.json";
|
|
64
|
+
/** Absolute path of the global roots registry, or null when home is unusable. */
|
|
65
|
+
function registryFile(home = process.env.HOME) {
|
|
66
|
+
const h = home?.trim();
|
|
67
|
+
if (!h || !h.startsWith("/")) return null;
|
|
68
|
+
return `${h}/${SUBPATH}`;
|
|
69
|
+
}
|
|
70
|
+
/** Read the registered project roots (deduplicated string entries only). */
|
|
71
|
+
function readRoots(home) {
|
|
72
|
+
const file = registryFile(home);
|
|
73
|
+
if (!file) return [];
|
|
74
|
+
try {
|
|
75
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
76
|
+
return Array.isArray(parsed) ? parsed.filter((e) => typeof e === "string") : [];
|
|
77
|
+
} catch {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Register a project root once (deduplicated, read-modify-write, non-throwing). */
|
|
82
|
+
function addRoot(root, home) {
|
|
83
|
+
const file = registryFile(home);
|
|
84
|
+
if (!file) return;
|
|
85
|
+
try {
|
|
86
|
+
const roots = new Set(readRoots(home));
|
|
87
|
+
if (roots.has(root)) return;
|
|
88
|
+
roots.add(root);
|
|
89
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
90
|
+
writeFileSync(file, JSON.stringify([...roots]));
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { addRoot, ensureMemoryGitignore, nowStamp, readRoots, readState, registryFile, setStateField, stateFileFor, throttleMs };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { t as Prompt } from "../types-D56jSgD9.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/policy/detect-project.d.ts
|
|
4
|
+
/** Project types detected from filesystem indicators. */
|
|
5
|
+
type ProjectType = "nextjs" | "nuxt" | "angular" | "svelte" | "vue" | "react" | "tailwind" | "laravel" | "rails" | "django" | "python" | "go" | "rust" | "swift" | "java" | "scala" | "elixir" | "ruby" | "generic";
|
|
6
|
+
/** Keywords that signal a development task (APEX trigger). */
|
|
7
|
+
declare const DEV_KEYWORDS: RegExp;
|
|
8
|
+
/** True when the prompt invokes the /apex command. */
|
|
9
|
+
declare function isApexCommand(prompt: string): boolean;
|
|
10
|
+
/** Detect the project type by scanning config files in `dir`. */
|
|
11
|
+
declare function detectProjectType(dir: string): ProjectType;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/policy/detect-framework.d.ts
|
|
14
|
+
/**
|
|
15
|
+
* Detect the framework from a file path extension + content patterns.
|
|
16
|
+
* Aligned with the fusengine require-solid-read detection (distinct from
|
|
17
|
+
* {@link detectProjectType}, which scans config files on disk).
|
|
18
|
+
*/
|
|
19
|
+
declare function detectFramework(filePath: string, content: string): string;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/policy/file-size.d.ts
|
|
22
|
+
/** Verdict from {@link evaluateFileSize}. */
|
|
23
|
+
interface FileSizeVerdict {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
lines: number;
|
|
26
|
+
max: number;
|
|
27
|
+
message: string | null;
|
|
28
|
+
}
|
|
29
|
+
/** Count lines in file content (empty string = 0). */
|
|
30
|
+
declare function countLines(content: string): number;
|
|
31
|
+
/**
|
|
32
|
+
* Evaluate a file's line count against the SOLID limit.
|
|
33
|
+
* @param lines - the file's line count
|
|
34
|
+
* @param max - the limit (defaults to `resolveMaxLines()`)
|
|
35
|
+
*/
|
|
36
|
+
declare function evaluateFileSize(lines: number, max?: number): FileSizeVerdict;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/policy/patterns.d.ts
|
|
39
|
+
/**
|
|
40
|
+
* Guard pattern data, ported verbatim from the fusengine git/install guards.
|
|
41
|
+
* Note (faithful): `git push.*--force` also matches `--force-with-lease` —
|
|
42
|
+
* preserved from the source guard.
|
|
43
|
+
*/
|
|
44
|
+
/** Destructive git operations to block outright. */
|
|
45
|
+
declare const GIT_BLOCKED: ReadonlyArray<RegExp>;
|
|
46
|
+
/** Git operations that warrant a confirmation prompt. */
|
|
47
|
+
declare const GIT_ASK: ReadonlyArray<RegExp>;
|
|
48
|
+
/** System-level package installs (need confirmation). */
|
|
49
|
+
declare const SYSTEM_INSTALL: ReadonlyArray<RegExp>;
|
|
50
|
+
/** Project-level package installs. */
|
|
51
|
+
declare const PROJECT_INSTALL: ReadonlyArray<RegExp>;
|
|
52
|
+
/** True when `cmd` matches any pattern in `patterns`. */
|
|
53
|
+
declare function matchPatterns(cmd: string, patterns: ReadonlyArray<RegExp>): boolean;
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/policy/evaluate.d.ts
|
|
56
|
+
/** Harness-agnostic input to {@link evaluate}. */
|
|
57
|
+
interface PolicyContext {
|
|
58
|
+
/** Tool name (e.g. "Write", "Edit", "Bash"). */
|
|
59
|
+
tool: string;
|
|
60
|
+
filePath?: string;
|
|
61
|
+
content?: string;
|
|
62
|
+
command?: string;
|
|
63
|
+
/** Optional override for the SOLID max-lines limit. */
|
|
64
|
+
maxLines?: number;
|
|
65
|
+
}
|
|
66
|
+
/** Harness-agnostic policy decision (+ a portable prompt for adapters to render). */
|
|
67
|
+
interface PolicyResult {
|
|
68
|
+
decision: "allow" | "deny" | "warn";
|
|
69
|
+
message: string | null;
|
|
70
|
+
prompt?: Prompt;
|
|
71
|
+
meta?: Record<string, unknown>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Evaluate a single tool-use against the bundled policies, returning a pure
|
|
75
|
+
* decision plus a portable {@link Prompt}. Adapters translate the prompt into
|
|
76
|
+
* their harness's native response (Claude `permissionDecision`, etc.).
|
|
77
|
+
*/
|
|
78
|
+
declare function evaluate(ctx: PolicyContext): PolicyResult;
|
|
79
|
+
//#endregion
|
|
80
|
+
export { DEV_KEYWORDS, FileSizeVerdict, GIT_ASK, GIT_BLOCKED, PROJECT_INSTALL, PolicyContext, PolicyResult, ProjectType, SYSTEM_INSTALL, countLines, detectFramework, detectProjectType, evaluate, evaluateFileSize, isApexCommand, matchPatterns };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { a as SYSTEM_INSTALL, c as evaluateFileSize, i as PROJECT_INSTALL, l as detectFramework, n as GIT_ASK, o as matchPatterns, r as GIT_BLOCKED, s as countLines, t as evaluate } from "../evaluate-CsYyUucy.mjs";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
//#region src/policy/detect-project.ts
|
|
5
|
+
/** Keywords that signal a development task (APEX trigger). */
|
|
6
|
+
const DEV_KEYWORDS = /\b(implement|create|build|fix|add|refactor|develop|feature|bug|update|modify|change|write|code)\b/i;
|
|
7
|
+
/** True when the prompt invokes the /apex command. */
|
|
8
|
+
function isApexCommand(prompt) {
|
|
9
|
+
return /(?:^|\s)\/apex|\/fuse-ai-pilot:apex/i.test(prompt);
|
|
10
|
+
}
|
|
11
|
+
/** Detect the project type by scanning config files in `dir`. */
|
|
12
|
+
function detectProjectType(dir) {
|
|
13
|
+
const has = (f) => existsSync(join(dir, f));
|
|
14
|
+
if (has("next.config.js") || has("next.config.ts") || has("next.config.mjs")) return "nextjs";
|
|
15
|
+
if (has("nuxt.config.ts") || has("nuxt.config.js")) return "nuxt";
|
|
16
|
+
if (has("angular.json")) return "angular";
|
|
17
|
+
if (has("svelte.config.js") || has("svelte.config.ts")) return "svelte";
|
|
18
|
+
if (has("vite.config.ts") && has("src/App.vue")) return "vue";
|
|
19
|
+
if (has("vite.config.ts") || has("vite.config.js")) return "react";
|
|
20
|
+
if (has("tailwind.config.js") || has("tailwind.config.ts")) return "tailwind";
|
|
21
|
+
if (has("composer.json") && has("artisan")) return "laravel";
|
|
22
|
+
if (has("Gemfile") && has("config/routes.rb")) return "rails";
|
|
23
|
+
if (has("requirements.txt") || has("pyproject.toml") || has("setup.py")) return has("manage.py") ? "django" : "python";
|
|
24
|
+
if (has("go.mod")) return "go";
|
|
25
|
+
if (has("Cargo.toml")) return "rust";
|
|
26
|
+
if (has("Package.swift")) return "swift";
|
|
27
|
+
if (has("pom.xml") || has("build.gradle") || has("build.gradle.kts")) return "java";
|
|
28
|
+
if (has("build.sbt")) return "scala";
|
|
29
|
+
if (has("mix.exs")) return "elixir";
|
|
30
|
+
if (has("Gemfile")) return "ruby";
|
|
31
|
+
return "generic";
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { DEV_KEYWORDS, GIT_ASK, GIT_BLOCKED, PROJECT_INSTALL, SYSTEM_INSTALL, countLines, detectFramework, detectProjectType, evaluate, evaluateFileSize, isApexCommand, matchPatterns };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
//#region src/util/project-root.ts
|
|
4
|
+
/** Source-code file extensions worth tracking as "code was written". */
|
|
5
|
+
const CODE_EXT = /\.(ts|tsx|js|jsx|py|php|swift|go|rs|rb|java|vue|svelte|astro|css|kt|dart|cpp|c)$/;
|
|
6
|
+
/** Generated/vendored directories that never count as code. */
|
|
7
|
+
const SKIP_DIRS = /(node_modules|vendor|dist|build|\.next|DerivedData|\.git)/;
|
|
8
|
+
/** True for a source-code file outside generated/vendored directories. */
|
|
9
|
+
function isCodeFile(p) {
|
|
10
|
+
return CODE_EXT.test(p) && !SKIP_DIRS.test(p);
|
|
11
|
+
}
|
|
12
|
+
function walkUpFor(dir, marker) {
|
|
13
|
+
let current = resolve(dir);
|
|
14
|
+
while (current !== "/") {
|
|
15
|
+
if (existsSync(`${current}/${marker}`)) return current;
|
|
16
|
+
current = dirname(current);
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the project root, preferring the repo boundary (`.git`) over a
|
|
22
|
+
* nested `package.json` — avoids false roots in monorepos. Null if none found.
|
|
23
|
+
*/
|
|
24
|
+
function projectRootOrNull(dir) {
|
|
25
|
+
return walkUpFor(dir, ".git") ?? walkUpFor(dir, "package.json");
|
|
26
|
+
}
|
|
27
|
+
/** Like {@link projectRootOrNull} but falls back to `process.cwd()`. */
|
|
28
|
+
function projectRoot(dir) {
|
|
29
|
+
return projectRootOrNull(dir) ?? process.cwd();
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { projectRoot as n, projectRootOrNull as r, isCodeFile as t };
|