@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,44 @@
|
|
|
1
|
+
//#region src/refs/types.d.ts
|
|
2
|
+
/** Parsed frontmatter metadata from a reference .md file. */
|
|
3
|
+
interface RefMeta {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
keywords: string;
|
|
7
|
+
priority: string;
|
|
8
|
+
related: string;
|
|
9
|
+
appliesTo: string;
|
|
10
|
+
triggerOnEdit: string;
|
|
11
|
+
level: string;
|
|
12
|
+
filePath: string;
|
|
13
|
+
}
|
|
14
|
+
/** A reference with its computed routing score. */
|
|
15
|
+
interface ScoredRef {
|
|
16
|
+
meta: RefMeta;
|
|
17
|
+
score: number;
|
|
18
|
+
}
|
|
19
|
+
/** Routing result with categorized references. */
|
|
20
|
+
interface RouteResult {
|
|
21
|
+
required: ScoredRef[];
|
|
22
|
+
optional: ScoredRef[];
|
|
23
|
+
skillPath: string;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/refs/frontmatter.d.ts
|
|
27
|
+
/** Extract frontmatter key/value pairs from markdown content (quotes stripped). */
|
|
28
|
+
declare function parseFrontmatter(content: string): Record<string, string>;
|
|
29
|
+
/** Convert a simple glob (`**`, `*`) to an anchored RegExp. */
|
|
30
|
+
declare function globToRe(g: string): RegExp;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/refs/router.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* Score references against a file edit (pure):
|
|
35
|
+
* +10 per `applies-to` glob match, +5 per `trigger-on-edit` fragment, +1 per keyword.
|
|
36
|
+
*/
|
|
37
|
+
declare function scoreReferences(refs: RefMeta[], filePath: string, content: string): ScoredRef[];
|
|
38
|
+
/**
|
|
39
|
+
* Route references for a file edit: top-2 required + next-2 optional, ensuring a
|
|
40
|
+
* `principle` and a `template` appear in the top 4. Returns null when nothing scores.
|
|
41
|
+
*/
|
|
42
|
+
declare function routeReferences(refs: RefMeta[], filePath: string, content: string, skillPath?: string): RouteResult | null;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { RefMeta, RouteResult, ScoredRef, globToRe, parseFrontmatter, routeReferences, scoreReferences };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//#region src/refs/frontmatter.ts
|
|
2
|
+
/** Extract frontmatter key/value pairs from markdown content (quotes stripped). */
|
|
3
|
+
function parseFrontmatter(content) {
|
|
4
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
5
|
+
if (!m?.[1]) return {};
|
|
6
|
+
const result = {};
|
|
7
|
+
for (const line of m[1].split("\n")) {
|
|
8
|
+
const idx = line.indexOf(":");
|
|
9
|
+
if (idx > 0) result[line.slice(0, idx).trim()] = line.slice(idx + 1).trim().replace(/^["']|["']$/g, "");
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
/** Convert a simple glob (`**`, `*`) to an anchored RegExp. */
|
|
14
|
+
function globToRe(g) {
|
|
15
|
+
const escaped = g.trim().replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\0/g, ".*");
|
|
16
|
+
return new RegExp(`^${escaped}$`);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/refs/router.ts
|
|
20
|
+
/**
|
|
21
|
+
* Score references against a file edit (pure):
|
|
22
|
+
* +10 per `applies-to` glob match, +5 per `trigger-on-edit` fragment, +1 per keyword.
|
|
23
|
+
*/
|
|
24
|
+
function scoreReferences(refs, filePath, content) {
|
|
25
|
+
const scored = [];
|
|
26
|
+
for (const meta of refs) {
|
|
27
|
+
let score = 0;
|
|
28
|
+
if (meta.appliesTo) {
|
|
29
|
+
for (const g of meta.appliesTo.split(", ")) if (globToRe(g).test(filePath)) score += 10;
|
|
30
|
+
}
|
|
31
|
+
if (meta.triggerOnEdit) {
|
|
32
|
+
for (const frag of meta.triggerOnEdit.split(", ")) if (filePath.includes(frag.trim())) score += 5;
|
|
33
|
+
}
|
|
34
|
+
if (meta.keywords) for (const kw of meta.keywords.split(", ")) {
|
|
35
|
+
const k = kw.trim();
|
|
36
|
+
if (k && (filePath.includes(k) || content.includes(k))) score += 1;
|
|
37
|
+
}
|
|
38
|
+
if (score > 0) scored.push({
|
|
39
|
+
meta,
|
|
40
|
+
score
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return scored;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Route references for a file edit: top-2 required + next-2 optional, ensuring a
|
|
47
|
+
* `principle` and a `template` appear in the top 4. Returns null when nothing scores.
|
|
48
|
+
*/
|
|
49
|
+
function routeReferences(refs, filePath, content, skillPath = "") {
|
|
50
|
+
const scored = scoreReferences(refs, filePath, content);
|
|
51
|
+
if (!scored.length) return null;
|
|
52
|
+
scored.sort((a, b) => b.score - a.score);
|
|
53
|
+
const hoist = (level) => {
|
|
54
|
+
if (scored.slice(0, 4).some((r) => r.meta.level === level)) return;
|
|
55
|
+
const found = scored.find((r) => r.meta.level === level);
|
|
56
|
+
if (found) {
|
|
57
|
+
scored.splice(scored.indexOf(found), 1);
|
|
58
|
+
scored.splice(3, 0, found);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
hoist("principle");
|
|
62
|
+
hoist("template");
|
|
63
|
+
return {
|
|
64
|
+
required: scored.slice(0, 2),
|
|
65
|
+
optional: scored.slice(2, 4),
|
|
66
|
+
skillPath
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { globToRe, parseFrontmatter, routeReferences, scoreReferences };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { t as AuthEntry } from "../doc-helpers-CG1nuf-c.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/state/lock.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Acquire a directory-based lock (atomic `mkdir`, EEXIST = already held) with a timeout.
|
|
6
|
+
* @returns a release function, or null if the lock could not be acquired in time.
|
|
7
|
+
*/
|
|
8
|
+
declare function acquireLock(lockDir: string, timeoutMs?: number): Promise<(() => Promise<void>) | null>;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/state/apex-state.d.ts
|
|
11
|
+
declare const DEFAULT_STATE: {
|
|
12
|
+
$schema: string;
|
|
13
|
+
description: string;
|
|
14
|
+
target: Record<string, string>;
|
|
15
|
+
authorizations: Record<string, AuthEntry & {
|
|
16
|
+
doc_consulted?: string;
|
|
17
|
+
}>;
|
|
18
|
+
};
|
|
19
|
+
/** APEX state shape. */
|
|
20
|
+
type ApexState = typeof DEFAULT_STATE;
|
|
21
|
+
/** APEX state directory under a home dir. */
|
|
22
|
+
declare function apexStateDir(home?: string): string;
|
|
23
|
+
/** Daily state file path: `<home>/.claude/logs/00-apex/<YYYY-MM-DD>-state.json`. */
|
|
24
|
+
declare function stateFilePath(home?: string, today?: string): string;
|
|
25
|
+
/** Ensure the state directory exists and return its path. */
|
|
26
|
+
declare function ensureStateDir(home?: string): Promise<string>;
|
|
27
|
+
/** Load APEX state, or a fresh default. */
|
|
28
|
+
declare function loadState(path: string): Promise<ApexState>;
|
|
29
|
+
/** Save APEX state. */
|
|
30
|
+
declare function saveState(path: string, state: ApexState): Promise<void>;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/state/task-helpers.d.ts
|
|
33
|
+
/** A task entry in task.json. */
|
|
34
|
+
interface ApexTask {
|
|
35
|
+
subject: string;
|
|
36
|
+
description: string;
|
|
37
|
+
status: string;
|
|
38
|
+
phase: string;
|
|
39
|
+
started_at?: string;
|
|
40
|
+
completed_at?: string;
|
|
41
|
+
created_at?: string;
|
|
42
|
+
doc_consulted: Record<string, unknown>;
|
|
43
|
+
files_modified: string[];
|
|
44
|
+
blockedBy?: string[];
|
|
45
|
+
}
|
|
46
|
+
/** Structure of `.claude/apex/task.json`. */
|
|
47
|
+
interface ApexTaskFile {
|
|
48
|
+
current_task: string;
|
|
49
|
+
created_at: string;
|
|
50
|
+
tasks: Record<string, ApexTask>;
|
|
51
|
+
}
|
|
52
|
+
/** Add a new pending task. */
|
|
53
|
+
declare function taskCreate(file: string, id: string, subject: string, desc: string): Promise<void>;
|
|
54
|
+
/** Mark a task in_progress (creating a stub if absent). */
|
|
55
|
+
declare function taskStart(file: string, id: string, subject?: string, desc?: string, blocked?: string): Promise<void>;
|
|
56
|
+
/** Mark a task completed. */
|
|
57
|
+
declare function taskComplete(file: string, id: string): Promise<void>;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { ApexState, ApexTask, ApexTaskFile, acquireLock, apexStateDir, ensureStateDir, loadState, saveState, stateFilePath, taskComplete, taskCreate, taskStart };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { n as readJsonFile, r as writeJsonFile, t as ensureDir } from "../json-io-RH82El2J.mjs";
|
|
2
|
+
import { mkdir, rmdir } from "node:fs/promises";
|
|
3
|
+
//#region src/state/lock.ts
|
|
4
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5
|
+
/**
|
|
6
|
+
* Acquire a directory-based lock (atomic `mkdir`, EEXIST = already held) with a timeout.
|
|
7
|
+
* @returns a release function, or null if the lock could not be acquired in time.
|
|
8
|
+
*/
|
|
9
|
+
async function acquireLock(lockDir, timeoutMs = 5e3) {
|
|
10
|
+
const start = Date.now();
|
|
11
|
+
while (Date.now() - start < timeoutMs) try {
|
|
12
|
+
await mkdir(lockDir, { recursive: false });
|
|
13
|
+
return async () => {
|
|
14
|
+
try {
|
|
15
|
+
await rmdir(lockDir);
|
|
16
|
+
} catch {}
|
|
17
|
+
};
|
|
18
|
+
} catch {
|
|
19
|
+
await sleep(100);
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/state/apex-state.ts
|
|
25
|
+
const DEFAULT_STATE = {
|
|
26
|
+
$schema: "apex-state-v1",
|
|
27
|
+
description: "APEX/SOLID state - sessions[] + 2min expiry",
|
|
28
|
+
target: {},
|
|
29
|
+
authorizations: {}
|
|
30
|
+
};
|
|
31
|
+
/** APEX state directory under a home dir. */
|
|
32
|
+
function apexStateDir(home = process.env.HOME ?? "") {
|
|
33
|
+
return `${home}/.claude/logs/00-apex`;
|
|
34
|
+
}
|
|
35
|
+
/** Daily state file path: `<home>/.claude/logs/00-apex/<YYYY-MM-DD>-state.json`. */
|
|
36
|
+
function stateFilePath(home = process.env.HOME ?? "", today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)) {
|
|
37
|
+
return `${apexStateDir(home)}/${today}-state.json`;
|
|
38
|
+
}
|
|
39
|
+
/** Ensure the state directory exists and return its path. */
|
|
40
|
+
async function ensureStateDir(home) {
|
|
41
|
+
const dir = apexStateDir(home);
|
|
42
|
+
await ensureDir(dir);
|
|
43
|
+
return dir;
|
|
44
|
+
}
|
|
45
|
+
/** Load APEX state, or a fresh default. */
|
|
46
|
+
async function loadState(path) {
|
|
47
|
+
return await readJsonFile(path) ?? { ...DEFAULT_STATE };
|
|
48
|
+
}
|
|
49
|
+
/** Save APEX state. */
|
|
50
|
+
async function saveState(path, state) {
|
|
51
|
+
await writeJsonFile(path, state);
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/state/task-helpers.ts
|
|
55
|
+
/** Add a new pending task. */
|
|
56
|
+
async function taskCreate(file, id, subject, desc) {
|
|
57
|
+
const data = await readJsonFile(file);
|
|
58
|
+
if (!data) return;
|
|
59
|
+
data.tasks[id] = {
|
|
60
|
+
subject,
|
|
61
|
+
description: desc,
|
|
62
|
+
status: "pending",
|
|
63
|
+
phase: "pending",
|
|
64
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65
|
+
doc_consulted: {},
|
|
66
|
+
files_modified: [],
|
|
67
|
+
blockedBy: []
|
|
68
|
+
};
|
|
69
|
+
await writeJsonFile(file, data);
|
|
70
|
+
}
|
|
71
|
+
/** Mark a task in_progress (creating a stub if absent). */
|
|
72
|
+
async function taskStart(file, id, subject, desc, blocked) {
|
|
73
|
+
const data = await readJsonFile(file);
|
|
74
|
+
if (!data) return;
|
|
75
|
+
const task = data.tasks[id] ?? {
|
|
76
|
+
subject: "",
|
|
77
|
+
description: "",
|
|
78
|
+
status: "in_progress",
|
|
79
|
+
phase: "analyze",
|
|
80
|
+
doc_consulted: {},
|
|
81
|
+
files_modified: []
|
|
82
|
+
};
|
|
83
|
+
data.tasks[id] = task;
|
|
84
|
+
data.current_task = id;
|
|
85
|
+
task.status = "in_progress";
|
|
86
|
+
task.phase = "analyze";
|
|
87
|
+
task.started_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
88
|
+
if (subject) task.subject = subject;
|
|
89
|
+
if (desc) task.description = desc;
|
|
90
|
+
if (blocked) task.blockedBy = blocked.split(",");
|
|
91
|
+
await writeJsonFile(file, data);
|
|
92
|
+
}
|
|
93
|
+
/** Mark a task completed. */
|
|
94
|
+
async function taskComplete(file, id) {
|
|
95
|
+
const data = await readJsonFile(file);
|
|
96
|
+
const task = data?.tasks[id];
|
|
97
|
+
if (!data || !task) return;
|
|
98
|
+
task.status = "completed";
|
|
99
|
+
task.phase = "completed";
|
|
100
|
+
task.completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
101
|
+
await writeJsonFile(file, data);
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { acquireLock, apexStateDir, ensureStateDir, loadState, saveState, stateFilePath, taskComplete, taskCreate, taskStart };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//#region src/statusline/constants.d.ts
|
|
2
|
+
/** Time interval constants (ms). */
|
|
3
|
+
declare const TIME_INTERVALS: {
|
|
4
|
+
readonly MINUTE_MS: 60_000;
|
|
5
|
+
readonly HOUR_MS: 3_600_000;
|
|
6
|
+
};
|
|
7
|
+
/** Percentage thresholds for progressive coloring. */
|
|
8
|
+
declare const COLOR_THRESHOLDS: {
|
|
9
|
+
readonly WARNING: 70;
|
|
10
|
+
readonly CRITICAL: 90;
|
|
11
|
+
};
|
|
12
|
+
/** Fill/empty glyphs per progress-bar style. */
|
|
13
|
+
declare const PROGRESS_CHARS: {
|
|
14
|
+
readonly blocks: {
|
|
15
|
+
readonly fill: "█";
|
|
16
|
+
readonly empty: "░";
|
|
17
|
+
};
|
|
18
|
+
readonly bars: {
|
|
19
|
+
readonly fill: "▰";
|
|
20
|
+
readonly empty: "▱";
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/** Defaults for {@link generateProgressBar}. */
|
|
24
|
+
declare const PROGRESS_BAR_DEFAULTS: {
|
|
25
|
+
readonly STYLE: "blocks";
|
|
26
|
+
readonly LENGTH: 10;
|
|
27
|
+
};
|
|
28
|
+
/** 9 gradient blocks from empty to full. */
|
|
29
|
+
declare const GRADIENT_BLOCKS: readonly [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"];
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/statusline/colors.d.ts
|
|
32
|
+
/** A text-decorating function. */
|
|
33
|
+
type ColorFn = (text: string) => string;
|
|
34
|
+
/** Forced ANSI palette (every entry is a colorizer, plus reset + support flag). */
|
|
35
|
+
interface Palette {
|
|
36
|
+
blue: ColorFn;
|
|
37
|
+
cyan: ColorFn;
|
|
38
|
+
green: ColorFn;
|
|
39
|
+
yellow: ColorFn;
|
|
40
|
+
red: ColorFn;
|
|
41
|
+
magenta: ColorFn;
|
|
42
|
+
white: ColorFn;
|
|
43
|
+
gray: ColorFn;
|
|
44
|
+
purple: ColorFn;
|
|
45
|
+
orange: ColorFn;
|
|
46
|
+
brightRed: ColorFn;
|
|
47
|
+
brightYellow: ColorFn;
|
|
48
|
+
brightGreen: ColorFn;
|
|
49
|
+
bold: ColorFn;
|
|
50
|
+
dim: ColorFn;
|
|
51
|
+
reset: string;
|
|
52
|
+
isSupported: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Forced ANSI color helpers (ignore TTY detection). */
|
|
55
|
+
declare const colors: Palette;
|
|
56
|
+
/** Color `text` by threshold: green < WARNING <= yellow < CRITICAL <= red. */
|
|
57
|
+
declare function progressiveColor(value: number, text: string): string;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/statusline/formatters.d.ts
|
|
60
|
+
/** Format a path: truncated (default), full, relative (~), or basename. */
|
|
61
|
+
declare function formatPath(path: string, style?: "truncated" | "full" | "relative" | "basename"): string;
|
|
62
|
+
/** Basename of a path. */
|
|
63
|
+
declare function formatBasename(path: string): string;
|
|
64
|
+
/** Humanize a remaining duration in ms (e.g. "2h - 5m"). */
|
|
65
|
+
declare function formatTimeLeft(ms: number): string;
|
|
66
|
+
/** Format a token count as "12K" (or "12.3K" with decimals). */
|
|
67
|
+
declare function formatTokens(tokens: number, showDecimals?: boolean): string;
|
|
68
|
+
/** Format a cost as "$1.23". */
|
|
69
|
+
declare function formatCost(cost: number, decimals?: number): string;
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/statusline/progress-bar.d.ts
|
|
72
|
+
/** Options for {@link generateProgressBar}. */
|
|
73
|
+
interface ProgressBarOptions {
|
|
74
|
+
style?: keyof typeof PROGRESS_CHARS;
|
|
75
|
+
length?: number;
|
|
76
|
+
useProgressiveColor?: boolean;
|
|
77
|
+
showPercentage?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** Render a fill/empty progress bar. */
|
|
80
|
+
declare function generateProgressBar(percentage: number, options?: ProgressBarOptions): string;
|
|
81
|
+
/** Render a fine-grained gradient bar (sub-block resolution). */
|
|
82
|
+
declare function generateGradientBar(percentage: number, length?: number): string;
|
|
83
|
+
//#endregion
|
|
84
|
+
export { COLOR_THRESHOLDS, ColorFn, GRADIENT_BLOCKS, PROGRESS_BAR_DEFAULTS, PROGRESS_CHARS, Palette, ProgressBarOptions, TIME_INTERVALS, colors, formatBasename, formatCost, formatPath, formatTimeLeft, formatTokens, generateGradientBar, generateProgressBar, progressiveColor };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
//#region src/statusline/constants.ts
|
|
3
|
+
/** Time interval constants (ms). */
|
|
4
|
+
const TIME_INTERVALS = {
|
|
5
|
+
MINUTE_MS: 6e4,
|
|
6
|
+
HOUR_MS: 36e5
|
|
7
|
+
};
|
|
8
|
+
/** Percentage thresholds for progressive coloring. */
|
|
9
|
+
const COLOR_THRESHOLDS = {
|
|
10
|
+
WARNING: 70,
|
|
11
|
+
CRITICAL: 90
|
|
12
|
+
};
|
|
13
|
+
/** Fill/empty glyphs per progress-bar style. */
|
|
14
|
+
const PROGRESS_CHARS = {
|
|
15
|
+
blocks: {
|
|
16
|
+
fill: "█",
|
|
17
|
+
empty: "░"
|
|
18
|
+
},
|
|
19
|
+
bars: {
|
|
20
|
+
fill: "▰",
|
|
21
|
+
empty: "▱"
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
/** Defaults for {@link generateProgressBar}. */
|
|
25
|
+
const PROGRESS_BAR_DEFAULTS = {
|
|
26
|
+
STYLE: "blocks",
|
|
27
|
+
LENGTH: 10
|
|
28
|
+
};
|
|
29
|
+
/** 9 gradient blocks from empty to full. */
|
|
30
|
+
const GRADIENT_BLOCKS = [
|
|
31
|
+
" ",
|
|
32
|
+
"▏",
|
|
33
|
+
"▎",
|
|
34
|
+
"▍",
|
|
35
|
+
"▌",
|
|
36
|
+
"▋",
|
|
37
|
+
"▊",
|
|
38
|
+
"▉",
|
|
39
|
+
"█"
|
|
40
|
+
];
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/statusline/colors.ts
|
|
43
|
+
const ansi = (code) => (text) => `\x1b[${code}m${text}\x1b[0m`;
|
|
44
|
+
const ansi256 = (code) => (text) => `\x1b[38;5;${code}m${text}\x1b[0m`;
|
|
45
|
+
/** Forced ANSI color helpers (ignore TTY detection). */
|
|
46
|
+
const colors = {
|
|
47
|
+
blue: ansi("0;34"),
|
|
48
|
+
cyan: ansi("0;36"),
|
|
49
|
+
green: ansi("0;32"),
|
|
50
|
+
yellow: ansi("0;33"),
|
|
51
|
+
red: ansi("0;31"),
|
|
52
|
+
magenta: ansi("0;35"),
|
|
53
|
+
white: ansi("0;37"),
|
|
54
|
+
gray: ansi256(240),
|
|
55
|
+
purple: ansi256(135),
|
|
56
|
+
orange: ansi256(208),
|
|
57
|
+
brightRed: ansi("1;91"),
|
|
58
|
+
brightYellow: ansi("1;93"),
|
|
59
|
+
brightGreen: ansi("1;92"),
|
|
60
|
+
bold: ansi("1"),
|
|
61
|
+
dim: ansi("2"),
|
|
62
|
+
reset: "\x1B[0m",
|
|
63
|
+
isSupported: true
|
|
64
|
+
};
|
|
65
|
+
/** Color `text` by threshold: green < WARNING <= yellow < CRITICAL <= red. */
|
|
66
|
+
function progressiveColor(value, text) {
|
|
67
|
+
if (value >= COLOR_THRESHOLDS.CRITICAL) return colors.brightRed(text);
|
|
68
|
+
if (value >= COLOR_THRESHOLDS.WARNING) return colors.brightYellow(text);
|
|
69
|
+
return colors.green(text);
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/statusline/formatters.ts
|
|
73
|
+
/** Format a path: truncated (default), full, relative (~), or basename. */
|
|
74
|
+
function formatPath(path, style = "truncated") {
|
|
75
|
+
const home = process.env.HOME || "";
|
|
76
|
+
const isUnderHome = home !== "" && path.startsWith(home);
|
|
77
|
+
const withTilde = isUnderHome ? path.replace(home, "~") : path;
|
|
78
|
+
const parts = withTilde.split("/").filter(Boolean);
|
|
79
|
+
const name = parts[parts.length - 1] || withTilde;
|
|
80
|
+
switch (style) {
|
|
81
|
+
case "full": return path;
|
|
82
|
+
case "basename": return name;
|
|
83
|
+
case "relative": return withTilde;
|
|
84
|
+
default:
|
|
85
|
+
if (isUnderHome && parts.length > 2) return `~/../${name}`;
|
|
86
|
+
if (isUnderHome) return withTilde;
|
|
87
|
+
if (parts.length > 3) return `/../${name}`;
|
|
88
|
+
return withTilde;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Basename of a path. */
|
|
92
|
+
function formatBasename(path) {
|
|
93
|
+
return basename(path);
|
|
94
|
+
}
|
|
95
|
+
/** Humanize a remaining duration in ms (e.g. "2h - 5m"). */
|
|
96
|
+
function formatTimeLeft(ms) {
|
|
97
|
+
if (ms <= 0) return "0m";
|
|
98
|
+
const dayMs = TIME_INTERVALS.HOUR_MS * 24;
|
|
99
|
+
const days = Math.floor(ms / dayMs);
|
|
100
|
+
const hours = Math.floor(ms % dayMs / TIME_INTERVALS.HOUR_MS);
|
|
101
|
+
const minutes = Math.floor(ms % TIME_INTERVALS.HOUR_MS / TIME_INTERVALS.MINUTE_MS);
|
|
102
|
+
if (days > 0 && hours > 0) return `${days}d - ${hours}h`;
|
|
103
|
+
if (days > 0) return `${days}d`;
|
|
104
|
+
if (hours > 0 && minutes > 0) return `${hours}h - ${minutes}m`;
|
|
105
|
+
if (hours > 0) return `${hours}h`;
|
|
106
|
+
return `${minutes}m`;
|
|
107
|
+
}
|
|
108
|
+
/** Format a token count as "12K" (or "12.3K" with decimals). */
|
|
109
|
+
function formatTokens(tokens, showDecimals = false) {
|
|
110
|
+
const k = tokens / 1e3;
|
|
111
|
+
return showDecimals ? `${k.toFixed(1)}K` : `${Math.round(k)}K`;
|
|
112
|
+
}
|
|
113
|
+
/** Format a cost as "$1.23". */
|
|
114
|
+
function formatCost(cost, decimals = 2) {
|
|
115
|
+
return `$${cost.toFixed(decimals)}`;
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/statusline/progress-bar.ts
|
|
119
|
+
const clampPct = (p) => Math.max(0, Math.min(100, Number.isNaN(p) ? 0 : p));
|
|
120
|
+
/** Render a fill/empty progress bar. */
|
|
121
|
+
function generateProgressBar(percentage, options = {}) {
|
|
122
|
+
const { style = PROGRESS_BAR_DEFAULTS.STYLE, length = PROGRESS_BAR_DEFAULTS.LENGTH, useProgressiveColor = false, showPercentage = false } = options;
|
|
123
|
+
const pct = clampPct(percentage);
|
|
124
|
+
const filled = Math.round(pct / 100 * length);
|
|
125
|
+
const empty = Math.max(0, length - filled);
|
|
126
|
+
const chars = PROGRESS_CHARS[style];
|
|
127
|
+
let bar = chars.fill.repeat(filled) + chars.empty.repeat(empty);
|
|
128
|
+
if (useProgressiveColor) bar = progressiveColor(pct, bar);
|
|
129
|
+
if (showPercentage) {
|
|
130
|
+
const pctText = `${Math.round(pct)}%`;
|
|
131
|
+
bar += ` ${useProgressiveColor ? progressiveColor(pct, pctText) : pctText}`;
|
|
132
|
+
}
|
|
133
|
+
return bar;
|
|
134
|
+
}
|
|
135
|
+
/** Render a fine-grained gradient bar (sub-block resolution). */
|
|
136
|
+
function generateGradientBar(percentage, length = 10) {
|
|
137
|
+
const pct = clampPct(percentage);
|
|
138
|
+
const exact = pct / 100 * length;
|
|
139
|
+
const full = Math.floor(exact);
|
|
140
|
+
const remainder = exact - full;
|
|
141
|
+
let bar = GRADIENT_BLOCKS[8].repeat(full);
|
|
142
|
+
if (full < length && remainder > 0) bar += GRADIENT_BLOCKS[Math.floor(remainder * 8)] ?? "";
|
|
143
|
+
const remaining = length - bar.length;
|
|
144
|
+
if (remaining > 0) bar += " ".repeat(remaining);
|
|
145
|
+
return progressiveColor(pct, bar);
|
|
146
|
+
}
|
|
147
|
+
//#endregion
|
|
148
|
+
export { COLOR_THRESHOLDS, GRADIENT_BLOCKS, PROGRESS_BAR_DEFAULTS, PROGRESS_CHARS, TIME_INTERVALS, colors, formatBasename, formatCost, formatPath, formatTimeLeft, formatTokens, generateGradientBar, generateProgressBar, progressiveColor };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/prompt/types.d.ts
|
|
2
|
+
/** How a policy outcome should surface in a harness. */
|
|
3
|
+
type PromptKind = "ask" | "block" | "inform";
|
|
4
|
+
/**
|
|
5
|
+
* Portable, harness-agnostic representation of a policy outcome as a prompt.
|
|
6
|
+
* Adapters render this into their harness's native shape (a Claude
|
|
7
|
+
* `permissionDecision`, a Cursor block, a CLI exit + message, ...).
|
|
8
|
+
*/
|
|
9
|
+
interface Prompt {
|
|
10
|
+
kind: PromptKind;
|
|
11
|
+
/** Short title, e.g. "SOLID file-size limit". */
|
|
12
|
+
title: string;
|
|
13
|
+
/** Why this fired. */
|
|
14
|
+
reason: string;
|
|
15
|
+
/** Concrete next actions to proceed. */
|
|
16
|
+
actions?: string[];
|
|
17
|
+
}
|
|
18
|
+
/** Render a {@link Prompt} as a consistent, agent-readable memo block. */
|
|
19
|
+
declare function formatPrompt(p: Prompt): string;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { PromptKind as n, formatPrompt as r, Prompt as t };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/prompt/types.ts
|
|
2
|
+
/** Render a {@link Prompt} as a consistent, agent-readable memo block. */
|
|
3
|
+
function formatPrompt(p) {
|
|
4
|
+
const lines = [`[${p.kind === "block" ? "BLOCKED" : p.kind === "ask" ? "CONFIRM" : "NOTE"}] ${p.title}`, p.reason];
|
|
5
|
+
if (p.actions?.length) {
|
|
6
|
+
lines.push("Next:");
|
|
7
|
+
p.actions.forEach((a, i) => lines.push(` ${i + 1}. ${a}`));
|
|
8
|
+
}
|
|
9
|
+
return lines.join("\n");
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { formatPrompt as t };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/util/compact-json.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Compact JSON serializer for cache/state files.
|
|
4
|
+
* Top-level keys are indented one per line; nested objects/arrays render inline.
|
|
5
|
+
* Behaviour preserved from the fusengine hooks.
|
|
6
|
+
*/
|
|
7
|
+
declare function compactJson(data: unknown): string;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/util/project-root.d.ts
|
|
10
|
+
/** True for a source-code file outside generated/vendored directories. */
|
|
11
|
+
declare function isCodeFile(p: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the project root, preferring the repo boundary (`.git`) over a
|
|
14
|
+
* nested `package.json` — avoids false roots in monorepos. Null if none found.
|
|
15
|
+
*/
|
|
16
|
+
declare function projectRootOrNull(dir: string): string | null;
|
|
17
|
+
/** Like {@link projectRootOrNull} but falls back to `process.cwd()`. */
|
|
18
|
+
declare function projectRoot(dir: string): string;
|
|
19
|
+
//#endregion
|
|
20
|
+
export { compactJson, isCodeFile, projectRoot, projectRootOrNull };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fusengine/harness",
|
|
3
|
+
"version": "0.1.0",
|
|
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, and thin per-harness adapters. Bun-native, with a built dist for Node + bundlers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "src/index.ts",
|
|
7
|
+
"types": "dist/index.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": { "bun": "./src/index.ts", "types": "./dist/index.d.mts", "import": "./dist/index.mjs" },
|
|
10
|
+
"./config": { "bun": "./src/config/index.ts", "types": "./dist/config/index.d.mts", "import": "./dist/config/index.mjs" },
|
|
11
|
+
"./util": { "bun": "./src/util/index.ts", "types": "./dist/util/index.d.mts", "import": "./dist/util/index.mjs" },
|
|
12
|
+
"./detect": { "bun": "./src/detect/index.ts", "types": "./dist/detect/index.d.mts", "import": "./dist/detect/index.mjs" },
|
|
13
|
+
"./policy": { "bun": "./src/policy/index.ts", "types": "./dist/policy/index.d.mts", "import": "./dist/policy/index.mjs" },
|
|
14
|
+
"./prompt": { "bun": "./src/prompt/index.ts", "types": "./dist/prompt/index.d.mts", "import": "./dist/prompt/index.mjs" },
|
|
15
|
+
"./memory": { "bun": "./src/memory/index.ts", "types": "./dist/memory/index.d.mts", "import": "./dist/memory/index.mjs" },
|
|
16
|
+
"./cache": { "bun": "./src/cache/index.ts", "types": "./dist/cache/index.d.mts", "import": "./dist/cache/index.mjs" },
|
|
17
|
+
"./freshness": { "bun": "./src/freshness/index.ts", "types": "./dist/freshness/index.d.mts", "import": "./dist/freshness/index.mjs" },
|
|
18
|
+
"./refs": { "bun": "./src/refs/index.ts", "types": "./dist/refs/index.d.mts", "import": "./dist/refs/index.mjs" },
|
|
19
|
+
"./state": { "bun": "./src/state/index.ts", "types": "./dist/state/index.d.mts", "import": "./dist/state/index.mjs" },
|
|
20
|
+
"./statusline": { "bun": "./src/statusline/index.ts", "types": "./dist/statusline/index.d.mts", "import": "./dist/statusline/index.mjs" },
|
|
21
|
+
"./adapters/claude": { "bun": "./src/adapters/claude/index.ts", "types": "./dist/adapters/claude/index.d.mts", "import": "./dist/adapters/claude/index.mjs" }
|
|
22
|
+
},
|
|
23
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"build": "tsdown src/index.ts src/config/index.ts src/util/index.ts src/detect/index.ts src/policy/index.ts src/prompt/index.ts src/memory/index.ts src/cache/index.ts src/freshness/index.ts src/refs/index.ts src/state/index.ts src/statusline/index.ts src/adapters/claude/index.ts --dts --format esm --clean --out-dir dist",
|
|
30
|
+
"prepublishOnly": "bun test && tsc --noEmit && bun run build"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@vercel/detect-agent": "*"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@vercel/detect-agent": { "optional": true }
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bun": "latest",
|
|
43
|
+
"tsdown": "^0.22.3",
|
|
44
|
+
"typescript": "^6.0.3"
|
|
45
|
+
}
|
|
46
|
+
}
|