blun-king-cli 9.1.563 → 9.1.565
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/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
- package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
- package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
- package/agent-spine-plugin/.mcp.json +8 -8
- package/agent-spine-plugin/CONTRIBUTING.md +52 -0
- package/agent-spine-plugin/LICENSE +186 -186
- package/agent-spine-plugin/README.md +394 -0
- package/agent-spine-plugin/SECURITY.md +47 -0
- package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
- package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
- package/agent-spine-plugin/bin/agentspine.js +7 -7
- package/agent-spine-plugin/blun.plugin.json +33 -33
- package/agent-spine-plugin/hooks/codex.json +47 -47
- package/agent-spine-plugin/hooks/hooks.json +106 -106
- package/agent-spine-plugin/hooks/version.json +5 -5
- package/agent-spine-plugin/package.json +69 -69
- package/agent-spine-plugin/scripts/check-hosts.js +199 -199
- package/agent-spine-plugin/skill/SKILL.md +76 -76
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
- package/agent-spine-plugin/src/cli.js +1488 -1442
- package/agent-spine-plugin/src/hook.js +886 -812
- package/agent-spine-plugin/src/index.js +93 -93
- package/agent-spine-plugin/src/lib/acceptance.js +333 -333
- package/agent-spine-plugin/src/lib/attention.js +755 -755
- package/agent-spine-plugin/src/lib/audit.js +351 -342
- package/agent-spine-plugin/src/lib/authentication.js +515 -515
- package/agent-spine-plugin/src/lib/briefing.js +317 -317
- package/agent-spine-plugin/src/lib/catalog.js +167 -167
- package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
- package/agent-spine-plugin/src/lib/context.js +154 -154
- package/agent-spine-plugin/src/lib/continuity.js +338 -338
- package/agent-spine-plugin/src/lib/coordination.js +577 -577
- package/agent-spine-plugin/src/lib/documents.js +217 -217
- package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
- package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
- package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
- package/agent-spine-plugin/src/lib/graph.js +337 -337
- package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
- package/agent-spine-plugin/src/lib/https-transport.js +392 -392
- package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
- package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
- package/agent-spine-plugin/src/lib/learning.js +6923 -6466
- package/agent-spine-plugin/src/lib/object-transport.js +206 -206
- package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
- package/agent-spine-plugin/src/lib/paths.js +109 -109
- package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
- package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
- package/agent-spine-plugin/src/lib/preflight.js +702 -702
- package/agent-spine-plugin/src/lib/runtime.js +13 -13
- package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
- package/agent-spine-plugin/src/lib/sharing.js +969 -969
- package/agent-spine-plugin/src/lib/source-roots.js +529 -482
- package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
- package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
- package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
- package/agent-spine-plugin/src/mcp.js +597 -572
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +202 -195
- package/bin/active-steer-priority-policy.cjs +24 -0
- package/bin/launcher-runtime.js +8 -1
- package/bin/mnemo-tool-agent-policy.cjs +22 -0
- package/bin/thinking-activity-status-policy.cjs +132 -0
- package/bin/thinking-only-guard.cjs +75 -0
- package/bin/tool-call-loop-policy.cjs +53 -0
- package/bin/turn-thinking-policy.cjs +25 -1
- package/blun.mjs +554 -64
- package/package.json +4 -1
- package/standard-skills/translate-native/LICENSE +21 -21
- package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
- package/standard-skills/translate-native/references/native-orthography.md +79 -79
- package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
- package/standard-skills/translate-native/references/structured-content.md +72 -72
- package/standard-skills/translate-native/references/translationese-review.md +77 -77
- package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
- package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
- package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
- package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
- package/standard-skills/translate-native/scripts/language_quality.py +377 -377
- package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
- package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
- package/standard-tools/language-guard/blun_language_guard.py +697 -697
- package/standard-tools/language-guard/check_diacritics.py +353 -353
- package/standard-tools/language-guard/guard_service_client.py +264 -264
- package/standard-tools/language-guard/language_quality.py +377 -377
- package/standard-tools/language-guard/translation_guard.py +916 -916
|
@@ -1,217 +1,217 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstat, opendir, readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
-
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
-
|
|
5
|
-
const SKIP_DIRS = new Set([
|
|
6
|
-
".git", ".hg", ".svn", ".next", ".nuxt", ".turbo", ".venv",
|
|
7
|
-
"build", "coverage", "dist", "node_modules", "target", "vendor"
|
|
8
|
-
]);
|
|
9
|
-
|
|
10
|
-
const RULE_NAMES = new Set([
|
|
11
|
-
"AGENTS.md", "AGENTS.override.md", "CLAUDE.md", "CLAUDE.local.md",
|
|
12
|
-
"GEMINI.md", "CONSTITUTION.md", "RULES.md"
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
const MEMORY_NAMES = new Set(["MEMORY.md"]);
|
|
16
|
-
const SOUL_RE = /(^|[_-])(soul|persona|voice)([_-]|\.)/i;
|
|
17
|
-
const MEMORY_PATH_RE = /(^|\/)(memory|memories)(\/|$)/i;
|
|
18
|
-
const DOCUMENT_CONTENT = new WeakMap();
|
|
19
|
-
|
|
20
|
-
function sha256(buffer) {
|
|
21
|
-
return createHash("sha256").update(buffer).digest("hex");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function classify(relativePath) {
|
|
25
|
-
const name = basename(relativePath);
|
|
26
|
-
if (RULE_NAMES.has(name)) return { layer: "constitution", protected: true };
|
|
27
|
-
if (MEMORY_NAMES.has(name)) return { layer: "memory-index", protected: true };
|
|
28
|
-
if (SOUL_RE.test(name) || name === "SOUL.md") return { layer: "soul", protected: true };
|
|
29
|
-
if (MEMORY_PATH_RE.test(relativePath)) return { layer: "memory-fact", protected: true };
|
|
30
|
-
return { layer: "reference", protected: false };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function authorityFor(layer) {
|
|
34
|
-
if (layer === "constitution") return "host-instruction-candidate";
|
|
35
|
-
return "context-only";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function hostsFor(name) {
|
|
39
|
-
if (name === "AGENTS.md" || name === "AGENTS.override.md") return ["codex"];
|
|
40
|
-
if (name === "CLAUDE.md" || name === "CLAUDE.local.md") return ["claude"];
|
|
41
|
-
if (name === "GEMINI.md") return ["gemini"];
|
|
42
|
-
return ["generic"];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function documentSnapshotContent(document) {
|
|
46
|
-
const value = DOCUMENT_CONTENT.get(document);
|
|
47
|
-
return value ? Buffer.from(value) : null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function attachDocumentSnapshot(document, buffer) {
|
|
51
|
-
if (buffer) DOCUMENT_CONTENT.set(document, Buffer.from(buffer));
|
|
52
|
-
return document;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function markdownOutsideCode(text) {
|
|
56
|
-
let fence = null;
|
|
57
|
-
return String(text).split(/\r?\n/).map((line) => {
|
|
58
|
-
const marker = line.match(/^\s{0,3}(`{3,}|~{3,})/)?.[1] || null;
|
|
59
|
-
if (!fence && marker) { fence = marker[0]; return ""; }
|
|
60
|
-
if (fence && marker?.[0] === fence) { fence = null; return ""; }
|
|
61
|
-
if (fence) return "";
|
|
62
|
-
return line.replace(/`+[^`\n]*`+/g, "");
|
|
63
|
-
}).join("\n");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function indexExplicitDocuments(entries) {
|
|
67
|
-
const documents = [];
|
|
68
|
-
for (const entry of entries) {
|
|
69
|
-
let path;
|
|
70
|
-
let metadata;
|
|
71
|
-
let buffer;
|
|
72
|
-
if (entry.snapshot) {
|
|
73
|
-
path = entry.snapshot.path;
|
|
74
|
-
metadata = entry.snapshot.metadata;
|
|
75
|
-
buffer = Buffer.from(entry.snapshot.buffer);
|
|
76
|
-
if (buffer.byteLength !== metadata.bytes || sha256(buffer) !== metadata.sha256) {
|
|
77
|
-
throw new Error(`source snapshot integrity failed: ${entry.id}`);
|
|
78
|
-
}
|
|
79
|
-
} else {
|
|
80
|
-
const supplied = await lstat(entry.path);
|
|
81
|
-
if (supplied.isSymbolicLink() || !supplied.isFile()) throw new Error(`source is not a regular non-symlink file: ${entry.id}`);
|
|
82
|
-
path = await realpath(entry.path);
|
|
83
|
-
metadata = await lstat(path);
|
|
84
|
-
if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(`source is not a regular non-symlink file: ${entry.id}`);
|
|
85
|
-
buffer = await readFile(path);
|
|
86
|
-
}
|
|
87
|
-
if (buffer.byteLength > (entry.maxBytes || 4 * 1024 * 1024)) throw new Error(`source exceeds its byte limit: ${entry.id}`);
|
|
88
|
-
const classification = classify(entry.id);
|
|
89
|
-
const document = {
|
|
90
|
-
path,
|
|
91
|
-
relativePath: entry.id,
|
|
92
|
-
name: basename(path),
|
|
93
|
-
...classification,
|
|
94
|
-
layer: entry.layer || classification.layer,
|
|
95
|
-
protected: entry.protected ?? classification.protected,
|
|
96
|
-
authority: authorityFor(entry.layer || classification.layer),
|
|
97
|
-
classificationSource: "host-native-source-binding",
|
|
98
|
-
hosts: [entry.host],
|
|
99
|
-
bytes: buffer.byteLength,
|
|
100
|
-
modifiedAt: entry.snapshot ? metadata.modifiedAt : metadata.mtime.toISOString(),
|
|
101
|
-
sha256: entry.snapshot ? metadata.sha256 : sha256(buffer),
|
|
102
|
-
links: [],
|
|
103
|
-
sourceScope: entry.scope,
|
|
104
|
-
sourceBinding: entry.binding,
|
|
105
|
-
precedence: entry.precedence,
|
|
106
|
-
relevance: entry.relevance || null
|
|
107
|
-
};
|
|
108
|
-
attachDocumentSnapshot(document, entry.snapshot ? buffer : null);
|
|
109
|
-
documents.push(document);
|
|
110
|
-
}
|
|
111
|
-
return documents;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function extractMarkdownLinks(text, filePath, root) {
|
|
115
|
-
const links = new Set();
|
|
116
|
-
text = markdownOutsideCode(text);
|
|
117
|
-
const patterns = [
|
|
118
|
-
/\[[^\]]*\]\(([^)]+\.md)(?:#[^)]+)?\)/gi,
|
|
119
|
-
/(?:^|\s)@([^\s`'\"]+\.md)(?:\s|$)/gim
|
|
120
|
-
];
|
|
121
|
-
for (const pattern of patterns) {
|
|
122
|
-
for (const match of text.matchAll(pattern)) {
|
|
123
|
-
const raw = match[1].trim().replace(/^<|>$/g, "");
|
|
124
|
-
if (/^[a-z]+:/i.test(raw)) continue;
|
|
125
|
-
let decoded = raw;
|
|
126
|
-
try { decoded = decodeURIComponent(raw); } catch { /* Keep malformed URLs visible instead of failing the scan. */ }
|
|
127
|
-
const target = resolve(dirname(filePath), decoded);
|
|
128
|
-
const rel = relative(root, target).replaceAll("\\", "/");
|
|
129
|
-
if (!rel.startsWith("../") && rel !== "..") links.add(rel);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return [...links].sort();
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function isInside(parent, child) {
|
|
136
|
-
const value = relative(parent, child);
|
|
137
|
-
return value === "" || (!isAbsolute(value) && !value.startsWith(`..${sep}`) && value !== "..");
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function walk(directory, found, excludedRoots) {
|
|
141
|
-
const entries = [];
|
|
142
|
-
for await (const entry of await opendir(directory)) entries.push(entry);
|
|
143
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
144
|
-
for (const entry of entries) {
|
|
145
|
-
if (entry.isSymbolicLink()) continue;
|
|
146
|
-
const fullPath = join(directory, entry.name);
|
|
147
|
-
if (entry.isDirectory()) {
|
|
148
|
-
if (!SKIP_DIRS.has(entry.name) && !excludedRoots.some((root) => isInside(root, fullPath))) {
|
|
149
|
-
await walk(fullPath, found, excludedRoots);
|
|
150
|
-
}
|
|
151
|
-
} else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") {
|
|
152
|
-
found.push(fullPath);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function discoverDocuments(root, { excludeRoots = [] } = {}) {
|
|
158
|
-
const files = [];
|
|
159
|
-
// Canonicalize existing exclusion roots just like the scanned root. On macOS
|
|
160
|
-
// temporary directories commonly cross /var -> /private/var, while Windows
|
|
161
|
-
// may expose the same directory through differently cased/normalized paths.
|
|
162
|
-
// Comparing only resolved spellings would therefore walk the private state
|
|
163
|
-
// directory even though it is the configured exclusion boundary.
|
|
164
|
-
const excludedRoots = (await Promise.all(excludeRoots.map(async (path) => {
|
|
165
|
-
try { return await realpath(resolve(path)); } catch (error) {
|
|
166
|
-
if (error.code === "ENOENT") return resolve(path);
|
|
167
|
-
throw error;
|
|
168
|
-
}
|
|
169
|
-
}))).filter((path) => isInside(root, path));
|
|
170
|
-
await walk(root, files, excludedRoots);
|
|
171
|
-
const documents = new Array(files.length);
|
|
172
|
-
let cursor = 0;
|
|
173
|
-
async function worker() {
|
|
174
|
-
while (cursor < files.length) {
|
|
175
|
-
const index = cursor++;
|
|
176
|
-
const path = files[index];
|
|
177
|
-
const [buffer, metadata] = await Promise.all([readFile(path), stat(path)]);
|
|
178
|
-
const relativePath = relative(root, path).replaceAll("\\", "/");
|
|
179
|
-
const classification = classify(relativePath);
|
|
180
|
-
documents[index] = {
|
|
181
|
-
path,
|
|
182
|
-
relativePath,
|
|
183
|
-
name: basename(path),
|
|
184
|
-
...classification,
|
|
185
|
-
authority: authorityFor(classification.layer),
|
|
186
|
-
classificationSource: "filename-and-path-hint",
|
|
187
|
-
hosts: hostsFor(basename(path)),
|
|
188
|
-
bytes: buffer.byteLength,
|
|
189
|
-
modifiedAt: metadata.mtime.toISOString(),
|
|
190
|
-
sha256: sha256(buffer),
|
|
191
|
-
links: extractMarkdownLinks(buffer.toString("utf8"), path, root)
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
const concurrency = Math.max(1, Math.min(16, files.length));
|
|
196
|
-
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
197
|
-
|
|
198
|
-
const byPath = new Map(documents.map((document) => [document.relativePath, document]));
|
|
199
|
-
const queue = documents.filter((document) => document.protected);
|
|
200
|
-
const visited = new Set(queue.map((document) => document.relativePath));
|
|
201
|
-
while (queue.length) {
|
|
202
|
-
const current = queue.shift();
|
|
203
|
-
for (const linkedPath of current.links) {
|
|
204
|
-
const linked = byPath.get(linkedPath);
|
|
205
|
-
if (linked && !visited.has(linkedPath)) {
|
|
206
|
-
linked.protected = true;
|
|
207
|
-
visited.add(linkedPath);
|
|
208
|
-
queue.push(linked);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return documents;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function isRuleDocument(name) {
|
|
216
|
-
return RULE_NAMES.has(name);
|
|
217
|
-
}
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, opendir, readFile, realpath, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
const SKIP_DIRS = new Set([
|
|
6
|
+
".git", ".hg", ".svn", ".next", ".nuxt", ".turbo", ".venv",
|
|
7
|
+
"build", "coverage", "dist", "node_modules", "target", "vendor"
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const RULE_NAMES = new Set([
|
|
11
|
+
"AGENTS.md", "AGENTS.override.md", "CLAUDE.md", "CLAUDE.local.md",
|
|
12
|
+
"GEMINI.md", "CONSTITUTION.md", "RULES.md"
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const MEMORY_NAMES = new Set(["MEMORY.md"]);
|
|
16
|
+
const SOUL_RE = /(^|[_-])(soul|persona|voice)([_-]|\.)/i;
|
|
17
|
+
const MEMORY_PATH_RE = /(^|\/)(memory|memories)(\/|$)/i;
|
|
18
|
+
const DOCUMENT_CONTENT = new WeakMap();
|
|
19
|
+
|
|
20
|
+
function sha256(buffer) {
|
|
21
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function classify(relativePath) {
|
|
25
|
+
const name = basename(relativePath);
|
|
26
|
+
if (RULE_NAMES.has(name)) return { layer: "constitution", protected: true };
|
|
27
|
+
if (MEMORY_NAMES.has(name)) return { layer: "memory-index", protected: true };
|
|
28
|
+
if (SOUL_RE.test(name) || name === "SOUL.md") return { layer: "soul", protected: true };
|
|
29
|
+
if (MEMORY_PATH_RE.test(relativePath)) return { layer: "memory-fact", protected: true };
|
|
30
|
+
return { layer: "reference", protected: false };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function authorityFor(layer) {
|
|
34
|
+
if (layer === "constitution") return "host-instruction-candidate";
|
|
35
|
+
return "context-only";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hostsFor(name) {
|
|
39
|
+
if (name === "AGENTS.md" || name === "AGENTS.override.md") return ["codex"];
|
|
40
|
+
if (name === "CLAUDE.md" || name === "CLAUDE.local.md") return ["claude"];
|
|
41
|
+
if (name === "GEMINI.md") return ["gemini"];
|
|
42
|
+
return ["generic"];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function documentSnapshotContent(document) {
|
|
46
|
+
const value = DOCUMENT_CONTENT.get(document);
|
|
47
|
+
return value ? Buffer.from(value) : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function attachDocumentSnapshot(document, buffer) {
|
|
51
|
+
if (buffer) DOCUMENT_CONTENT.set(document, Buffer.from(buffer));
|
|
52
|
+
return document;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function markdownOutsideCode(text) {
|
|
56
|
+
let fence = null;
|
|
57
|
+
return String(text).split(/\r?\n/).map((line) => {
|
|
58
|
+
const marker = line.match(/^\s{0,3}(`{3,}|~{3,})/)?.[1] || null;
|
|
59
|
+
if (!fence && marker) { fence = marker[0]; return ""; }
|
|
60
|
+
if (fence && marker?.[0] === fence) { fence = null; return ""; }
|
|
61
|
+
if (fence) return "";
|
|
62
|
+
return line.replace(/`+[^`\n]*`+/g, "");
|
|
63
|
+
}).join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function indexExplicitDocuments(entries) {
|
|
67
|
+
const documents = [];
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
let path;
|
|
70
|
+
let metadata;
|
|
71
|
+
let buffer;
|
|
72
|
+
if (entry.snapshot) {
|
|
73
|
+
path = entry.snapshot.path;
|
|
74
|
+
metadata = entry.snapshot.metadata;
|
|
75
|
+
buffer = Buffer.from(entry.snapshot.buffer);
|
|
76
|
+
if (buffer.byteLength !== metadata.bytes || sha256(buffer) !== metadata.sha256) {
|
|
77
|
+
throw new Error(`source snapshot integrity failed: ${entry.id}`);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
const supplied = await lstat(entry.path);
|
|
81
|
+
if (supplied.isSymbolicLink() || !supplied.isFile()) throw new Error(`source is not a regular non-symlink file: ${entry.id}`);
|
|
82
|
+
path = await realpath(entry.path);
|
|
83
|
+
metadata = await lstat(path);
|
|
84
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(`source is not a regular non-symlink file: ${entry.id}`);
|
|
85
|
+
buffer = await readFile(path);
|
|
86
|
+
}
|
|
87
|
+
if (buffer.byteLength > (entry.maxBytes || 4 * 1024 * 1024)) throw new Error(`source exceeds its byte limit: ${entry.id}`);
|
|
88
|
+
const classification = classify(entry.id);
|
|
89
|
+
const document = {
|
|
90
|
+
path,
|
|
91
|
+
relativePath: entry.id,
|
|
92
|
+
name: basename(path),
|
|
93
|
+
...classification,
|
|
94
|
+
layer: entry.layer || classification.layer,
|
|
95
|
+
protected: entry.protected ?? classification.protected,
|
|
96
|
+
authority: authorityFor(entry.layer || classification.layer),
|
|
97
|
+
classificationSource: "host-native-source-binding",
|
|
98
|
+
hosts: [entry.host],
|
|
99
|
+
bytes: buffer.byteLength,
|
|
100
|
+
modifiedAt: entry.snapshot ? metadata.modifiedAt : metadata.mtime.toISOString(),
|
|
101
|
+
sha256: entry.snapshot ? metadata.sha256 : sha256(buffer),
|
|
102
|
+
links: [],
|
|
103
|
+
sourceScope: entry.scope,
|
|
104
|
+
sourceBinding: entry.binding,
|
|
105
|
+
precedence: entry.precedence,
|
|
106
|
+
relevance: entry.relevance || null
|
|
107
|
+
};
|
|
108
|
+
attachDocumentSnapshot(document, entry.snapshot ? buffer : null);
|
|
109
|
+
documents.push(document);
|
|
110
|
+
}
|
|
111
|
+
return documents;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function extractMarkdownLinks(text, filePath, root) {
|
|
115
|
+
const links = new Set();
|
|
116
|
+
text = markdownOutsideCode(text);
|
|
117
|
+
const patterns = [
|
|
118
|
+
/\[[^\]]*\]\(([^)]+\.md)(?:#[^)]+)?\)/gi,
|
|
119
|
+
/(?:^|\s)@([^\s`'\"]+\.md)(?:\s|$)/gim
|
|
120
|
+
];
|
|
121
|
+
for (const pattern of patterns) {
|
|
122
|
+
for (const match of text.matchAll(pattern)) {
|
|
123
|
+
const raw = match[1].trim().replace(/^<|>$/g, "");
|
|
124
|
+
if (/^[a-z]+:/i.test(raw)) continue;
|
|
125
|
+
let decoded = raw;
|
|
126
|
+
try { decoded = decodeURIComponent(raw); } catch { /* Keep malformed URLs visible instead of failing the scan. */ }
|
|
127
|
+
const target = resolve(dirname(filePath), decoded);
|
|
128
|
+
const rel = relative(root, target).replaceAll("\\", "/");
|
|
129
|
+
if (!rel.startsWith("../") && rel !== "..") links.add(rel);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return [...links].sort();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isInside(parent, child) {
|
|
136
|
+
const value = relative(parent, child);
|
|
137
|
+
return value === "" || (!isAbsolute(value) && !value.startsWith(`..${sep}`) && value !== "..");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function walk(directory, found, excludedRoots) {
|
|
141
|
+
const entries = [];
|
|
142
|
+
for await (const entry of await opendir(directory)) entries.push(entry);
|
|
143
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
if (entry.isSymbolicLink()) continue;
|
|
146
|
+
const fullPath = join(directory, entry.name);
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
if (!SKIP_DIRS.has(entry.name) && !excludedRoots.some((root) => isInside(root, fullPath))) {
|
|
149
|
+
await walk(fullPath, found, excludedRoots);
|
|
150
|
+
}
|
|
151
|
+
} else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") {
|
|
152
|
+
found.push(fullPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function discoverDocuments(root, { excludeRoots = [] } = {}) {
|
|
158
|
+
const files = [];
|
|
159
|
+
// Canonicalize existing exclusion roots just like the scanned root. On macOS
|
|
160
|
+
// temporary directories commonly cross /var -> /private/var, while Windows
|
|
161
|
+
// may expose the same directory through differently cased/normalized paths.
|
|
162
|
+
// Comparing only resolved spellings would therefore walk the private state
|
|
163
|
+
// directory even though it is the configured exclusion boundary.
|
|
164
|
+
const excludedRoots = (await Promise.all(excludeRoots.map(async (path) => {
|
|
165
|
+
try { return await realpath(resolve(path)); } catch (error) {
|
|
166
|
+
if (error.code === "ENOENT") return resolve(path);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
}))).filter((path) => isInside(root, path));
|
|
170
|
+
await walk(root, files, excludedRoots);
|
|
171
|
+
const documents = new Array(files.length);
|
|
172
|
+
let cursor = 0;
|
|
173
|
+
async function worker() {
|
|
174
|
+
while (cursor < files.length) {
|
|
175
|
+
const index = cursor++;
|
|
176
|
+
const path = files[index];
|
|
177
|
+
const [buffer, metadata] = await Promise.all([readFile(path), stat(path)]);
|
|
178
|
+
const relativePath = relative(root, path).replaceAll("\\", "/");
|
|
179
|
+
const classification = classify(relativePath);
|
|
180
|
+
documents[index] = {
|
|
181
|
+
path,
|
|
182
|
+
relativePath,
|
|
183
|
+
name: basename(path),
|
|
184
|
+
...classification,
|
|
185
|
+
authority: authorityFor(classification.layer),
|
|
186
|
+
classificationSource: "filename-and-path-hint",
|
|
187
|
+
hosts: hostsFor(basename(path)),
|
|
188
|
+
bytes: buffer.byteLength,
|
|
189
|
+
modifiedAt: metadata.mtime.toISOString(),
|
|
190
|
+
sha256: sha256(buffer),
|
|
191
|
+
links: extractMarkdownLinks(buffer.toString("utf8"), path, root)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const concurrency = Math.max(1, Math.min(16, files.length));
|
|
196
|
+
await Promise.all(Array.from({ length: concurrency }, () => worker()));
|
|
197
|
+
|
|
198
|
+
const byPath = new Map(documents.map((document) => [document.relativePath, document]));
|
|
199
|
+
const queue = documents.filter((document) => document.protected);
|
|
200
|
+
const visited = new Set(queue.map((document) => document.relativePath));
|
|
201
|
+
while (queue.length) {
|
|
202
|
+
const current = queue.shift();
|
|
203
|
+
for (const linkedPath of current.links) {
|
|
204
|
+
const linked = byPath.get(linkedPath);
|
|
205
|
+
if (linked && !visited.has(linkedPath)) {
|
|
206
|
+
linked.protected = true;
|
|
207
|
+
visited.add(linkedPath);
|
|
208
|
+
queue.push(linked);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return documents;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function isRuleDocument(name) {
|
|
216
|
+
return RULE_NAMES.has(name);
|
|
217
|
+
}
|