faf-mcp 2.1.1 → 2.1.3
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/CHANGELOG.md +30 -1
- package/CLAUDE.md +3 -3
- package/README.md +8 -7
- package/dist/src/cli.js +2 -15
- package/dist/src/cli.js.map +1 -1
- package/dist/src/faf-core/commands/bi-sync.js +3 -2
- package/dist/src/faf-core/commands/bi-sync.js.map +1 -1
- package/dist/src/faf-core/compiler/faf-compiler.js +65 -6
- package/dist/src/faf-core/compiler/faf-compiler.js.map +1 -1
- package/dist/src/faf-core/inject.d.ts +19 -0
- package/dist/src/faf-core/inject.js +57 -0
- package/dist/src/faf-core/inject.js.map +1 -0
- package/dist/src/faf-core/parsers/agents-parser.js +3 -2
- package/dist/src/faf-core/parsers/agents-parser.js.map +1 -1
- package/dist/src/faf-core/parsers/cursorrules-parser.js +6 -2
- package/dist/src/faf-core/parsers/cursorrules-parser.js.map +1 -1
- package/dist/src/faf-core/parsers/gemini-parser.js +3 -2
- package/dist/src/faf-core/parsers/gemini-parser.js.map +1 -1
- package/dist/src/handlers/championship-tools.js +11 -11
- package/dist/src/handlers/championship-tools.js.map +1 -1
- package/dist/src/handlers/cloud-handler.js +1 -1
- package/dist/src/handlers/cloud-handler.js.map +1 -1
- package/dist/src/handlers/fileHandler.js +31 -22
- package/dist/src/handlers/fileHandler.js.map +1 -1
- package/dist/src/handlers/tools.js +105 -127
- package/dist/src/handlers/tools.js.map +1 -1
- package/dist/src/server.d.ts +2 -4
- package/dist/src/server.js +10 -93
- package/dist/src/server.js.map +1 -1
- package/dist/src/utils/safe-path.d.ts +66 -0
- package/dist/src/utils/safe-path.js +203 -0
- package/dist/src/utils/safe-path.js.map +1 -0
- package/package.json +2 -5
- package/project.faf +3 -3
- package/scripts/check-stylesheet-drift.mjs +1 -1
package/dist/src/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* safe-path.ts — confinement for caller-supplied `path` arguments.
|
|
3
|
+
*
|
|
4
|
+
* Several MCP tools (`refresh_faf`, `faf_score`, `faf_get_orchestration_policy`,
|
|
5
|
+
* `refresh_blend`, …) accept a `path` argument and read a `.faf` context file
|
|
6
|
+
* from it. Historically that path flowed straight through `path.resolve()` into
|
|
7
|
+
* `fs.readFileSync()` with no confinement, so an absolute path or `../` traversal
|
|
8
|
+
* could read ANY file the server uid can read (CWE-22 / CWE-73 / CWE-200) and
|
|
9
|
+
* have its contents echoed back — e.g. `refresh_faf({path:"~/.ssh/id_rsa"})`.
|
|
10
|
+
*
|
|
11
|
+
* This module is the single chokepoint that closes that. Two layers:
|
|
12
|
+
*
|
|
13
|
+
* 1. Context-file allow-list (ALWAYS ON) — this server's only job is reading
|
|
14
|
+
* `.faf` / `.fafm` context files. When a caller path resolves to a *file*,
|
|
15
|
+
* it must be one of those. That alone blocks the entire secret-disclosure
|
|
16
|
+
* surface — `/etc/passwd`, `~/.ssh/id_rsa`, `~/.aws/credentials`, `.env`,
|
|
17
|
+
* etc. are none of them `.faf` files — regardless of directory. A `.faf`
|
|
18
|
+
* is a public project-context format, so reading one anywhere discloses no
|
|
19
|
+
* secrets (a planted one only echoes what the attacker already wrote).
|
|
20
|
+
*
|
|
21
|
+
* 2. Root confinement (OPT-IN) — when `FAF_ALLOWED_ROOTS` is set (OS-path
|
|
22
|
+
* delimited), the resolved path must additionally stay within one of those
|
|
23
|
+
* roots. Off by default so legitimate `.faf` files outside $HOME (CI temp
|
|
24
|
+
* fixtures, /opt, /srv, monorepos) keep working; operators who want a hard
|
|
25
|
+
* directory boundary opt in.
|
|
26
|
+
*
|
|
27
|
+
* Layer 1 is the security boundary; layer 2 is defense-in-depth for locked-down
|
|
28
|
+
* deployments. Either way, `..` traversal and absolute paths can never reach a
|
|
29
|
+
* non-context file.
|
|
30
|
+
*/
|
|
31
|
+
export declare class PathConfinementError extends Error {
|
|
32
|
+
constructor(message: string);
|
|
33
|
+
}
|
|
34
|
+
/** True when `p`'s basename is a `.faf` / `.fafm` context file. */
|
|
35
|
+
export declare function isFafContextFile(p: string): boolean;
|
|
36
|
+
/** Opt-in allowed roots from `FAF_ALLOWED_ROOTS` (OS-delimited). Empty when
|
|
37
|
+
* unset → root confinement is not enforced (the `.faf`-only rule still is). */
|
|
38
|
+
export declare function allowedRoots(): string[];
|
|
39
|
+
/**
|
|
40
|
+
* Roots for the general-purpose file tools (`faf_read` / `faf_write`). Unlike
|
|
41
|
+
* the `.faf` tools, these legitimately handle any file *type* — but they must
|
|
42
|
+
* still be confined to the project. Default root = the process cwd; override /
|
|
43
|
+
* extend with `FAF_ALLOWED_ROOTS`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function fileOpRoots(): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Confine a general-purpose file read/write path: any file type, but it must
|
|
48
|
+
* stay within fileOpRoots(). Closes absolute-path escapes (`~/.ssh/id_rsa`),
|
|
49
|
+
* `..` traversal, and arbitrary writes outside the project. Throws
|
|
50
|
+
* PathConfinementError on violation. Returns the safe (symlink-canonical) path.
|
|
51
|
+
*/
|
|
52
|
+
export declare function confineFileOp(input: unknown): string;
|
|
53
|
+
export interface ConfineOptions {
|
|
54
|
+
/** Override allowed roots (resolved internally). Defaults to allowedRoots()
|
|
55
|
+
* (the opt-in `FAF_ALLOWED_ROOTS`). Empty = root confinement not enforced. */
|
|
56
|
+
roots?: string[];
|
|
57
|
+
/** When the resolved path is an existing file, require it be a `.faf`/`.fafm`
|
|
58
|
+
* context file. Use for sinks that read the path verbatim. Default true. */
|
|
59
|
+
requireFafFile?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve and confine a caller-supplied path. Returns the safe absolute path,
|
|
63
|
+
* or throws PathConfinementError. Never reaches the filesystem read itself —
|
|
64
|
+
* callers do that with the returned value.
|
|
65
|
+
*/
|
|
66
|
+
export declare function confinePath(input: unknown, opts?: ConfineOptions): string;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* safe-path.ts — confinement for caller-supplied `path` arguments.
|
|
4
|
+
*
|
|
5
|
+
* Several MCP tools (`refresh_faf`, `faf_score`, `faf_get_orchestration_policy`,
|
|
6
|
+
* `refresh_blend`, …) accept a `path` argument and read a `.faf` context file
|
|
7
|
+
* from it. Historically that path flowed straight through `path.resolve()` into
|
|
8
|
+
* `fs.readFileSync()` with no confinement, so an absolute path or `../` traversal
|
|
9
|
+
* could read ANY file the server uid can read (CWE-22 / CWE-73 / CWE-200) and
|
|
10
|
+
* have its contents echoed back — e.g. `refresh_faf({path:"~/.ssh/id_rsa"})`.
|
|
11
|
+
*
|
|
12
|
+
* This module is the single chokepoint that closes that. Two layers:
|
|
13
|
+
*
|
|
14
|
+
* 1. Context-file allow-list (ALWAYS ON) — this server's only job is reading
|
|
15
|
+
* `.faf` / `.fafm` context files. When a caller path resolves to a *file*,
|
|
16
|
+
* it must be one of those. That alone blocks the entire secret-disclosure
|
|
17
|
+
* surface — `/etc/passwd`, `~/.ssh/id_rsa`, `~/.aws/credentials`, `.env`,
|
|
18
|
+
* etc. are none of them `.faf` files — regardless of directory. A `.faf`
|
|
19
|
+
* is a public project-context format, so reading one anywhere discloses no
|
|
20
|
+
* secrets (a planted one only echoes what the attacker already wrote).
|
|
21
|
+
*
|
|
22
|
+
* 2. Root confinement (OPT-IN) — when `FAF_ALLOWED_ROOTS` is set (OS-path
|
|
23
|
+
* delimited), the resolved path must additionally stay within one of those
|
|
24
|
+
* roots. Off by default so legitimate `.faf` files outside $HOME (CI temp
|
|
25
|
+
* fixtures, /opt, /srv, monorepos) keep working; operators who want a hard
|
|
26
|
+
* directory boundary opt in.
|
|
27
|
+
*
|
|
28
|
+
* Layer 1 is the security boundary; layer 2 is defense-in-depth for locked-down
|
|
29
|
+
* deployments. Either way, `..` traversal and absolute paths can never reach a
|
|
30
|
+
* non-context file.
|
|
31
|
+
*/
|
|
32
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
33
|
+
if (k2 === undefined) k2 = k;
|
|
34
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
35
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
36
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
37
|
+
}
|
|
38
|
+
Object.defineProperty(o, k2, desc);
|
|
39
|
+
}) : (function(o, m, k, k2) {
|
|
40
|
+
if (k2 === undefined) k2 = k;
|
|
41
|
+
o[k2] = m[k];
|
|
42
|
+
}));
|
|
43
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
44
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
45
|
+
}) : function(o, v) {
|
|
46
|
+
o["default"] = v;
|
|
47
|
+
});
|
|
48
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
49
|
+
var ownKeys = function(o) {
|
|
50
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
51
|
+
var ar = [];
|
|
52
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
53
|
+
return ar;
|
|
54
|
+
};
|
|
55
|
+
return ownKeys(o);
|
|
56
|
+
};
|
|
57
|
+
return function (mod) {
|
|
58
|
+
if (mod && mod.__esModule) return mod;
|
|
59
|
+
var result = {};
|
|
60
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
61
|
+
__setModuleDefault(result, mod);
|
|
62
|
+
return result;
|
|
63
|
+
};
|
|
64
|
+
})();
|
|
65
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
66
|
+
exports.PathConfinementError = void 0;
|
|
67
|
+
exports.isFafContextFile = isFafContextFile;
|
|
68
|
+
exports.allowedRoots = allowedRoots;
|
|
69
|
+
exports.fileOpRoots = fileOpRoots;
|
|
70
|
+
exports.confineFileOp = confineFileOp;
|
|
71
|
+
exports.confinePath = confinePath;
|
|
72
|
+
const path = __importStar(require("path"));
|
|
73
|
+
const os = __importStar(require("os"));
|
|
74
|
+
const fs = __importStar(require("fs"));
|
|
75
|
+
class PathConfinementError extends Error {
|
|
76
|
+
constructor(message) {
|
|
77
|
+
super(message);
|
|
78
|
+
this.name = 'PathConfinementError';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.PathConfinementError = PathConfinementError;
|
|
82
|
+
/** True when `p`'s basename is a `.faf` / `.fafm` context file. */
|
|
83
|
+
function isFafContextFile(p) {
|
|
84
|
+
const base = path.basename(p).toLowerCase();
|
|
85
|
+
return base === '.faf' || base.endsWith('.faf') || base.endsWith('.fafm');
|
|
86
|
+
}
|
|
87
|
+
/** Expand a leading `~` / `~/` for the CURRENT user only. `~otheruser` is left
|
|
88
|
+
* literal (it will then fail the root check rather than reaching another home). */
|
|
89
|
+
function expandTilde(p) {
|
|
90
|
+
if (p === '~')
|
|
91
|
+
return os.homedir();
|
|
92
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) {
|
|
93
|
+
return path.join(os.homedir(), p.slice(2));
|
|
94
|
+
}
|
|
95
|
+
return p;
|
|
96
|
+
}
|
|
97
|
+
/** Opt-in allowed roots from `FAF_ALLOWED_ROOTS` (OS-delimited). Empty when
|
|
98
|
+
* unset → root confinement is not enforced (the `.faf`-only rule still is). */
|
|
99
|
+
function allowedRoots() {
|
|
100
|
+
const env = process.env.FAF_ALLOWED_ROOTS;
|
|
101
|
+
if (env && env.trim()) {
|
|
102
|
+
return env
|
|
103
|
+
.split(path.delimiter)
|
|
104
|
+
.map((r) => r.trim())
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.map((r) => path.resolve(expandTilde(r)));
|
|
107
|
+
}
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
function withinRoots(resolved, roots) {
|
|
111
|
+
return roots.some((root) => resolved === root || resolved.startsWith(root + path.sep));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Symlink-canonical absolute path, tolerant of a not-yet-existing target
|
|
115
|
+
* (a `faf_write` to a new file). Resolves the nearest EXISTING ancestor through
|
|
116
|
+
* symlinks, then re-appends the missing tail — so a new file under /tmp matches
|
|
117
|
+
* a /private/tmp root on macOS instead of slipping past the confinement check.
|
|
118
|
+
*/
|
|
119
|
+
function canonicalize(input) {
|
|
120
|
+
let cur = path.resolve(input);
|
|
121
|
+
const tail = [];
|
|
122
|
+
for (;;) {
|
|
123
|
+
try {
|
|
124
|
+
const real = fs.realpathSync(cur);
|
|
125
|
+
return tail.length ? path.join(real, ...tail.reverse()) : real;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
const parent = path.dirname(cur);
|
|
129
|
+
if (parent === cur)
|
|
130
|
+
return path.resolve(input); // hit filesystem root
|
|
131
|
+
tail.push(path.basename(cur));
|
|
132
|
+
cur = parent;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Roots for the general-purpose file tools (`faf_read` / `faf_write`). Unlike
|
|
138
|
+
* the `.faf` tools, these legitimately handle any file *type* — but they must
|
|
139
|
+
* still be confined to the project. Default root = the process cwd; override /
|
|
140
|
+
* extend with `FAF_ALLOWED_ROOTS`.
|
|
141
|
+
*/
|
|
142
|
+
function fileOpRoots() {
|
|
143
|
+
const opt = allowedRoots();
|
|
144
|
+
if (opt.length)
|
|
145
|
+
return opt;
|
|
146
|
+
// Default: the project (cwd) plus the OS temp dir(s) — legitimate scratch
|
|
147
|
+
// space for tools. Still blocks the high-value targets — $HOME secrets
|
|
148
|
+
// (~/.ssh, ~/.aws), /etc, and anything reached via ../ traversal.
|
|
149
|
+
const roots = [path.resolve(process.cwd()), os.tmpdir()];
|
|
150
|
+
// On macOS the canonical system temp (/tmp → /private/tmp) differs from
|
|
151
|
+
// os.tmpdir() (/var/folders/...); include it (roots are canonicalized later).
|
|
152
|
+
if (process.platform !== 'win32')
|
|
153
|
+
roots.push('/tmp');
|
|
154
|
+
return roots;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Confine a general-purpose file read/write path: any file type, but it must
|
|
158
|
+
* stay within fileOpRoots(). Closes absolute-path escapes (`~/.ssh/id_rsa`),
|
|
159
|
+
* `..` traversal, and arbitrary writes outside the project. Throws
|
|
160
|
+
* PathConfinementError on violation. Returns the safe (symlink-canonical) path.
|
|
161
|
+
*/
|
|
162
|
+
function confineFileOp(input) {
|
|
163
|
+
return confinePath(input, { requireFafFile: false, roots: fileOpRoots() });
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Resolve and confine a caller-supplied path. Returns the safe absolute path,
|
|
167
|
+
* or throws PathConfinementError. Never reaches the filesystem read itself —
|
|
168
|
+
* callers do that with the returned value.
|
|
169
|
+
*/
|
|
170
|
+
function confinePath(input, opts = {}) {
|
|
171
|
+
if (typeof input !== 'string' || input.length === 0) {
|
|
172
|
+
throw new PathConfinementError('path must be a non-empty string');
|
|
173
|
+
}
|
|
174
|
+
if (input.includes('\0')) {
|
|
175
|
+
throw new PathConfinementError('path contains a null byte');
|
|
176
|
+
}
|
|
177
|
+
// Canonicalize THROUGH symlinks when the target exists. This closes the
|
|
178
|
+
// symlink bypass: a file named `project.faf` that is actually a symlink to
|
|
179
|
+
// `/etc/passwd` would pass a lexical name check but read the secret. We check
|
|
180
|
+
// the real target's name instead. Missing paths stay lexical (nothing to read
|
|
181
|
+
// yet — a directory walk or a downstream ENOENT handles them).
|
|
182
|
+
const resolved = canonicalize(expandTilde(input));
|
|
183
|
+
let isFile = false;
|
|
184
|
+
try {
|
|
185
|
+
isFile = fs.statSync(resolved).isFile();
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
isFile = false;
|
|
189
|
+
}
|
|
190
|
+
const roots = (opts.roots ?? allowedRoots()).map(canonicalize);
|
|
191
|
+
// Layer 2 (opt-in): enforce root confinement only when roots are configured.
|
|
192
|
+
if (roots.length > 0 && !withinRoots(resolved, roots)) {
|
|
193
|
+
throw new PathConfinementError(`path escapes FAF_ALLOWED_ROOTS: "${input}".`);
|
|
194
|
+
}
|
|
195
|
+
// Layer 1 (always on): a resolved *file* must be a `.faf`/`.fafm` context file.
|
|
196
|
+
const requireFaf = opts.requireFafFile ?? true;
|
|
197
|
+
if (requireFaf && isFile && !isFafContextFile(resolved)) {
|
|
198
|
+
throw new PathConfinementError(`refusing to read a non-context file via \`path\`: "${input}". ` +
|
|
199
|
+
`Only .faf / .fafm files (or a directory containing one) are allowed.`);
|
|
200
|
+
}
|
|
201
|
+
return resolved;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=safe-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-path.js","sourceRoot":"","sources":["../../../src/utils/safe-path.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "faf-mcp",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.3",
|
|
4
4
|
"mcpName": "io.github.Wolfe-Jam/faf-mcp",
|
|
5
5
|
"description": "Persistent Project Context for Cursor, IDEs and VS Code. IANA-registered .faf format, 25 MCP tools, 309 tests.",
|
|
6
6
|
"icon": "./assets/icons/faf-icon-256.png",
|
|
@@ -102,14 +102,11 @@
|
|
|
102
102
|
},
|
|
103
103
|
"dependencies": {
|
|
104
104
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
105
|
-
"cors": "^2.8.5",
|
|
106
|
-
"express": "^4.21.0",
|
|
107
105
|
"faf-cli": "^6.7.1",
|
|
106
|
+
"faf-scoring-kernel": "^2.0.3",
|
|
108
107
|
"yaml": "^2.4.1"
|
|
109
108
|
},
|
|
110
109
|
"devDependencies": {
|
|
111
|
-
"@types/cors": "^2.8.19",
|
|
112
|
-
"@types/express": "^5.0.3",
|
|
113
110
|
"@types/jsonwebtoken": "^9.0.5",
|
|
114
111
|
"@types/node": "^20.11.0",
|
|
115
112
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
package/project.faf
CHANGED
|
@@ -11,11 +11,11 @@ stack:
|
|
|
11
11
|
ui_library: slotignored
|
|
12
12
|
state_management: slotignored
|
|
13
13
|
backend: MCP SDK (TS)
|
|
14
|
-
api_type: MCP (stdio
|
|
14
|
+
api_type: MCP (stdio + Streamable HTTP)
|
|
15
15
|
runtime: Node.js >=18
|
|
16
16
|
database: slotignored
|
|
17
17
|
connection: slotignored
|
|
18
|
-
hosting:
|
|
18
|
+
hosting: npm + Cloudflare edge
|
|
19
19
|
build: TypeScript (tsc)
|
|
20
20
|
cicd: GitHub Actions
|
|
21
21
|
monorepo_tool: slotignored
|
|
@@ -29,7 +29,7 @@ human_context:
|
|
|
29
29
|
who: Developers using Claude, Cursor, Windsurf, VS Code, Cline, and any MCP-compatible IDE
|
|
30
30
|
what: Universal MCP server providing .faf context tools — 25 core + 36 advanced, interop with AGENTS.md, .cursorrules, GEMINI.md
|
|
31
31
|
why: Eliminate the 20-minute AI context tax — give AI instant project understanding in 30 seconds
|
|
32
|
-
where:
|
|
32
|
+
where: npm · MCP Registry · Cloudflare edge (ide.faf.one/mcp/v1) · any MCP-compatible IDE — people get it how they wish
|
|
33
33
|
when: Production/Stable — v2.0.0 WJTTC certified (309/309 tests, 9 suites)
|
|
34
34
|
how: npx faf-mcp or npm install -g faf-mcp, then add to your MCP config
|
|
35
35
|
monorepo:
|
|
@@ -21,7 +21,7 @@ import { dirname, join } from 'node:path';
|
|
|
21
21
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
22
|
|
|
23
23
|
const SOURCE = 'docs/style-source.html';
|
|
24
|
-
const SURFACES = ['docs/index.html'
|
|
24
|
+
const SURFACES = ['docs/index.html'];
|
|
25
25
|
|
|
26
26
|
const OPEN = '/* === faf-mcp:stylesheet canonical';
|
|
27
27
|
const CLOSE = '/* === /faf-mcp:stylesheet canonical === */';
|