okf-catalog 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/CHANGELOG.md +24 -0
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +87 -0
- package/dist/bundle/cat-file.js +94 -0
- package/dist/bundle/cat-file.js.map +1 -0
- package/dist/bundle/contract.js +60 -0
- package/dist/bundle/contract.js.map +1 -0
- package/dist/bundle/frontmatter.js +106 -0
- package/dist/bundle/frontmatter.js.map +1 -0
- package/dist/bundle/git-tree.js +109 -0
- package/dist/bundle/git-tree.js.map +1 -0
- package/dist/bundle/index-file.js +90 -0
- package/dist/bundle/index-file.js.map +1 -0
- package/dist/bundle/links.js +57 -0
- package/dist/bundle/links.js.map +1 -0
- package/dist/bundle/load.js +363 -0
- package/dist/bundle/load.js.map +1 -0
- package/dist/bundle/manifest.js +95 -0
- package/dist/bundle/manifest.js.map +1 -0
- package/dist/bundle/markdown.js +210 -0
- package/dist/bundle/markdown.js.map +1 -0
- package/dist/bundle/model.js +7 -0
- package/dist/bundle/model.js.map +1 -0
- package/dist/bundle/page.js +374 -0
- package/dist/bundle/page.js.map +1 -0
- package/dist/bundle/paths.js +36 -0
- package/dist/bundle/paths.js.map +1 -0
- package/dist/bundle/reserved.js +56 -0
- package/dist/bundle/reserved.js.map +1 -0
- package/dist/bundle/timestamp.js +49 -0
- package/dist/bundle/timestamp.js.map +1 -0
- package/dist/catalog/model.js +32 -0
- package/dist/catalog/model.js.map +1 -0
- package/dist/catalog/nearest.js +56 -0
- package/dist/catalog/nearest.js.map +1 -0
- package/dist/catalog/outputs.js +438 -0
- package/dist/catalog/outputs.js.map +1 -0
- package/dist/catalog/provenance.js +37 -0
- package/dist/catalog/provenance.js.map +1 -0
- package/dist/catalog/runtime.js +2 -0
- package/dist/catalog/runtime.js.map +1 -0
- package/dist/catalog/text.js +140 -0
- package/dist/catalog/text.js.map +1 -0
- package/dist/cli.js +81 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/check.js +155 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/pack.js +182 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/serve.js +452 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/config/company-config.js +272 -0
- package/dist/config/company-config.js.map +1 -0
- package/dist/derive/derived-document.js +42 -0
- package/dist/derive/derived-document.js.map +1 -0
- package/dist/engine/qmd-render.js +54 -0
- package/dist/engine/qmd-render.js.map +1 -0
- package/dist/engine/qmd.js +241 -0
- package/dist/engine/qmd.js.map +1 -0
- package/dist/fs/cache-dir.js +140 -0
- package/dist/fs/cache-dir.js.map +1 -0
- package/dist/fs/company-lock.js +162 -0
- package/dist/fs/company-lock.js.map +1 -0
- package/dist/fs/walk.js +168 -0
- package/dist/fs/walk.js.map +1 -0
- package/dist/log.js +65 -0
- package/dist/log.js.map +1 -0
- package/dist/mcp/server.js +30 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.js +45 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/tools.js +249 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/report/report.js +43 -0
- package/dist/report/report.js.map +1 -0
- package/dist/search/engine.js +2 -0
- package/dist/search/engine.js.map +1 -0
- package/dist/search/query.js +91 -0
- package/dist/search/query.js.map +1 -0
- package/dist/search/search.js +224 -0
- package/dist/search/search.js.map +1 -0
- package/dist/search/snippet.js +95 -0
- package/dist/search/snippet.js.map +1 -0
- package/dist/serve/poller.js +119 -0
- package/dist/serve/poller.js.map +1 -0
- package/dist/serve/runtime.js +390 -0
- package/dist/serve/runtime.js.map +1 -0
- package/dist/source/git-runner.js +232 -0
- package/dist/source/git-runner.js.map +1 -0
- package/dist/source/git.js +457 -0
- package/dist/source/git.js.map +1 -0
- package/dist/source/local.js +20 -0
- package/dist/source/local.js.map +1 -0
- package/dist/source/source.js +2 -0
- package/dist/source/source.js.map +1 -0
- package/npm-shrinkwrap.json +5482 -0
- package/package.json +80 -0
- package/plugin/claude-code/.claude-plugin/plugin.json +15 -0
- package/plugin/claude-code/.mcp.json +10 -0
- package/plugin/claude-code/skills/okf-catalog/SKILL.md +15 -0
- package/recipes/publish/README.md +32 -0
- package/recipes/publish/checkers.lock +435 -0
- package/recipes/publish/checkers.txt +5 -0
- package/recipes/publish/pack.sh +60 -0
- package/recipes/publish/publish.yml +88 -0
- package/recipes/publish/push.sh +63 -0
package/dist/fs/walk.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readdirSync, readSync, realpathSync, } from "node:fs";
|
|
2
|
+
import { join, sep } from "node:path";
|
|
3
|
+
import { byCodeUnit } from "../bundle/paths.js";
|
|
4
|
+
const errorCode = (error) => error.code ?? error.name ?? "error";
|
|
5
|
+
/**
|
|
6
|
+
* Reads a bundle folder into memory. Regular files only: a symbolic link is refused where it stands, a pipe,
|
|
7
|
+
* socket or device is refused as a special file, an oversize file is refused from its size without being read,
|
|
8
|
+
* and an entry that cannot be listed, inspected or read is refused with the system's error code while the rest
|
|
9
|
+
* of the bundle is still reported. Dot-leading entries are returned as hidden paths and never opened, so a
|
|
10
|
+
* checkout's `.git` never enters the walk. Files are opened without following links and without blocking, and
|
|
11
|
+
* read through the descriptor that was checked. Every directory entry counts toward the file cap, so a folder of
|
|
12
|
+
* links or hidden files cannot grow without bound. The root is resolved once; a folder whose real path leaves it
|
|
13
|
+
* is refused. Residual: a folder replaced by a link between its check and its listing is listed once, because
|
|
14
|
+
* Node cannot list a directory through a descriptor; whoever can swap folders during the walk already owns the
|
|
15
|
+
* bundle.
|
|
16
|
+
*/
|
|
17
|
+
export function walkBundle(root, caps) {
|
|
18
|
+
let realRoot;
|
|
19
|
+
try {
|
|
20
|
+
realRoot = realpathSync(root);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error(`the bundle folder ${root} does not exist or cannot be read`);
|
|
24
|
+
}
|
|
25
|
+
const result = { files: [], hidden: [], hiddenFolders: [], refusals: [] };
|
|
26
|
+
const refuse = (path, rule, detail) => {
|
|
27
|
+
result.refusals.push({ path, rule, detail });
|
|
28
|
+
};
|
|
29
|
+
let total = 0;
|
|
30
|
+
let entries = 0;
|
|
31
|
+
const pending = [""];
|
|
32
|
+
while (pending.length > 0) {
|
|
33
|
+
const relDir = pending.pop();
|
|
34
|
+
const absDir = relDir === "" ? realRoot : join(realRoot, relDir);
|
|
35
|
+
if (relDir !== "") {
|
|
36
|
+
// Checked when it was listed as an entry; checked again here, right before it is read, to narrow the
|
|
37
|
+
// window in which a folder swapped for a link could be followed.
|
|
38
|
+
let real;
|
|
39
|
+
try {
|
|
40
|
+
real = realpathSync(absDir);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
refuse(relDir, "unreadable", `${errorCode(error)}: the folder could not be resolved`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (real !== absDir) {
|
|
47
|
+
refuse(relDir, "path-escape", "the folder changed under the walk");
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let listed;
|
|
52
|
+
try {
|
|
53
|
+
listed = readdirSync(absDir, { withFileTypes: true });
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (relDir === "")
|
|
57
|
+
throw new Error(`the bundle folder ${root} cannot be listed (${errorCode(error)})`);
|
|
58
|
+
refuse(relDir, "unreadable", `${errorCode(error)}: the folder could not be listed`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
listed.sort((a, b) => byCodeUnit(a.name, b.name));
|
|
62
|
+
for (const entry of listed) {
|
|
63
|
+
entries += 1;
|
|
64
|
+
if (entries > caps.files) {
|
|
65
|
+
result.fatal = {
|
|
66
|
+
path: "",
|
|
67
|
+
rule: "too-many-files",
|
|
68
|
+
detail: `more than ${caps.files} entries (files, folders, links and hidden entries all count)`,
|
|
69
|
+
};
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
const rel = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
|
|
73
|
+
if (entry.name.startsWith(".")) {
|
|
74
|
+
result.hidden.push(rel);
|
|
75
|
+
if (entry.isDirectory())
|
|
76
|
+
result.hiddenFolders.push(rel);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const abs = join(absDir, entry.name);
|
|
80
|
+
let stat;
|
|
81
|
+
try {
|
|
82
|
+
stat = lstatSync(abs);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
refuse(rel, "unreadable", `${errorCode(error)}: the entry could not be inspected`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (stat.isSymbolicLink()) {
|
|
89
|
+
refuse(rel, "symlink", "symbolic links are not followed");
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (stat.isDirectory()) {
|
|
93
|
+
let real;
|
|
94
|
+
try {
|
|
95
|
+
real = realpathSync(abs);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
refuse(rel, "unreadable", `${errorCode(error)}: the folder could not be resolved`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (real !== realRoot && !real.startsWith(realRoot + sep)) {
|
|
102
|
+
refuse(rel, "path-escape", "the folder's real path leaves the bundle");
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
pending.push(rel);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (!stat.isFile()) {
|
|
109
|
+
refuse(rel, "special-file", "not a regular file (a pipe, socket or device)");
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (stat.size > caps.fileBytes) {
|
|
113
|
+
refuse(rel, "oversize", `${stat.size} bytes exceeds the cap of ${caps.fileBytes}`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let bytes;
|
|
117
|
+
try {
|
|
118
|
+
bytes = readRegularFile(abs, caps.fileBytes);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
refuse(rel, "unreadable", `${errorCode(error)}: the file could not be read`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (bytes === undefined) {
|
|
125
|
+
refuse(rel, "special-file", "the entry changed under the walk and is not a regular file");
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (bytes.length > caps.fileBytes) {
|
|
129
|
+
refuse(rel, "oversize", `more than ${caps.fileBytes} bytes`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
result.files.push({ path: rel, bytes });
|
|
133
|
+
total += bytes.length;
|
|
134
|
+
if (total > caps.treeBytes) {
|
|
135
|
+
result.fatal = {
|
|
136
|
+
path: "",
|
|
137
|
+
rule: "tree-too-large",
|
|
138
|
+
detail: `more than ${caps.treeBytes} bytes`,
|
|
139
|
+
};
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
result.files.sort((a, b) => byCodeUnit(a.path, b.path));
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
/** Opens without following a link and without blocking, checks the descriptor, and reads at most `cap + 1` bytes. */
|
|
148
|
+
function readRegularFile(abs, cap) {
|
|
149
|
+
const fd = openSync(abs, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
150
|
+
try {
|
|
151
|
+
const stat = fstatSync(fd);
|
|
152
|
+
if (!stat.isFile())
|
|
153
|
+
return undefined;
|
|
154
|
+
const buffer = Buffer.alloc(Math.min(stat.size, cap + 1));
|
|
155
|
+
let offset = 0;
|
|
156
|
+
while (offset < buffer.length) {
|
|
157
|
+
const n = readSync(fd, buffer, offset, buffer.length - offset, offset);
|
|
158
|
+
if (n === 0)
|
|
159
|
+
break;
|
|
160
|
+
offset += n;
|
|
161
|
+
}
|
|
162
|
+
return buffer.subarray(0, offset);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
closeSync(fd);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=walk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"walk.js","sourceRoot":"","sources":["../../src/fs/walk.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n closeSync,\n constants,\n type Dirent,\n fstatSync,\n lstatSync,\n openSync,\n readdirSync,\n readSync,\n realpathSync,\n} from \"node:fs\";\nimport { join, sep } from \"node:path\";\nimport type { BundleFile, Caps, Refusal } from \"../bundle/model.js\";\nimport { byCodeUnit } from \"../bundle/paths.js\";\n\nexport interface WalkResult {\n files: BundleFile[];\n /** Dot-leading entries, as paths, never descended into or read. */\n hidden: string[];\n /** The hidden entries that are folders; a manifest entry beneath one counts as present. */\n hiddenFolders: string[];\n refusals: Refusal[];\n fatal?: Refusal;\n}\n\nconst errorCode = (error: unknown): string =>\n (error as NodeJS.ErrnoException).code ?? (error as Error).name ?? \"error\";\n\n/**\n * Reads a bundle folder into memory. Regular files only: a symbolic link is refused where it stands, a pipe,\n * socket or device is refused as a special file, an oversize file is refused from its size without being read,\n * and an entry that cannot be listed, inspected or read is refused with the system's error code while the rest\n * of the bundle is still reported. Dot-leading entries are returned as hidden paths and never opened, so a\n * checkout's `.git` never enters the walk. Files are opened without following links and without blocking, and\n * read through the descriptor that was checked. Every directory entry counts toward the file cap, so a folder of\n * links or hidden files cannot grow without bound. The root is resolved once; a folder whose real path leaves it\n * is refused. Residual: a folder replaced by a link between its check and its listing is listed once, because\n * Node cannot list a directory through a descriptor; whoever can swap folders during the walk already owns the\n * bundle.\n */\nexport function walkBundle(root: string, caps: Caps): WalkResult {\n let realRoot: string;\n try {\n realRoot = realpathSync(root);\n } catch {\n throw new Error(`the bundle folder ${root} does not exist or cannot be read`);\n }\n const result: WalkResult = { files: [], hidden: [], hiddenFolders: [], refusals: [] };\n const refuse = (path: string, rule: Refusal[\"rule\"], detail: string): void => {\n result.refusals.push({ path, rule, detail });\n };\n let total = 0;\n let entries = 0;\n const pending: string[] = [\"\"];\n while (pending.length > 0) {\n const relDir = pending.pop() as string;\n const absDir = relDir === \"\" ? realRoot : join(realRoot, relDir);\n if (relDir !== \"\") {\n // Checked when it was listed as an entry; checked again here, right before it is read, to narrow the\n // window in which a folder swapped for a link could be followed.\n let real: string;\n try {\n real = realpathSync(absDir);\n } catch (error) {\n refuse(relDir, \"unreadable\", `${errorCode(error)}: the folder could not be resolved`);\n continue;\n }\n if (real !== absDir) {\n refuse(relDir, \"path-escape\", \"the folder changed under the walk\");\n continue;\n }\n }\n let listed: Dirent[];\n try {\n listed = readdirSync(absDir, { withFileTypes: true });\n } catch (error) {\n if (relDir === \"\")\n throw new Error(`the bundle folder ${root} cannot be listed (${errorCode(error)})`);\n refuse(relDir, \"unreadable\", `${errorCode(error)}: the folder could not be listed`);\n continue;\n }\n listed.sort((a: Dirent, b: Dirent) => byCodeUnit(a.name, b.name));\n for (const entry of listed) {\n entries += 1;\n if (entries > caps.files) {\n result.fatal = {\n path: \"\",\n rule: \"too-many-files\",\n detail: `more than ${caps.files} entries (files, folders, links and hidden entries all count)`,\n };\n return result;\n }\n const rel = relDir === \"\" ? entry.name : `${relDir}/${entry.name}`;\n if (entry.name.startsWith(\".\")) {\n result.hidden.push(rel);\n if (entry.isDirectory()) result.hiddenFolders.push(rel);\n continue;\n }\n const abs = join(absDir, entry.name);\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(abs);\n } catch (error) {\n refuse(rel, \"unreadable\", `${errorCode(error)}: the entry could not be inspected`);\n continue;\n }\n if (stat.isSymbolicLink()) {\n refuse(rel, \"symlink\", \"symbolic links are not followed\");\n continue;\n }\n if (stat.isDirectory()) {\n let real: string;\n try {\n real = realpathSync(abs);\n } catch (error) {\n refuse(rel, \"unreadable\", `${errorCode(error)}: the folder could not be resolved`);\n continue;\n }\n if (real !== realRoot && !real.startsWith(realRoot + sep)) {\n refuse(rel, \"path-escape\", \"the folder's real path leaves the bundle\");\n continue;\n }\n pending.push(rel);\n continue;\n }\n if (!stat.isFile()) {\n refuse(rel, \"special-file\", \"not a regular file (a pipe, socket or device)\");\n continue;\n }\n if (stat.size > caps.fileBytes) {\n refuse(rel, \"oversize\", `${stat.size} bytes exceeds the cap of ${caps.fileBytes}`);\n continue;\n }\n let bytes: Uint8Array | undefined;\n try {\n bytes = readRegularFile(abs, caps.fileBytes);\n } catch (error) {\n refuse(rel, \"unreadable\", `${errorCode(error)}: the file could not be read`);\n continue;\n }\n if (bytes === undefined) {\n refuse(rel, \"special-file\", \"the entry changed under the walk and is not a regular file\");\n continue;\n }\n if (bytes.length > caps.fileBytes) {\n refuse(rel, \"oversize\", `more than ${caps.fileBytes} bytes`);\n continue;\n }\n result.files.push({ path: rel, bytes });\n total += bytes.length;\n if (total > caps.treeBytes) {\n result.fatal = {\n path: \"\",\n rule: \"tree-too-large\",\n detail: `more than ${caps.treeBytes} bytes`,\n };\n return result;\n }\n }\n }\n result.files.sort((a, b) => byCodeUnit(a.path, b.path));\n return result;\n}\n\n/** Opens without following a link and without blocking, checks the descriptor, and reads at most `cap + 1` bytes. */\nfunction readRegularFile(abs: string, cap: number): Uint8Array | undefined {\n const fd = openSync(abs, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);\n try {\n const stat = fstatSync(fd);\n if (!stat.isFile()) return undefined;\n const buffer = Buffer.alloc(Math.min(stat.size, cap + 1));\n let offset = 0;\n while (offset < buffer.length) {\n const n = readSync(fd, buffer, offset, buffer.length - offset, offset);\n if (n === 0) break;\n offset += n;\n }\n return buffer.subarray(0, offset);\n } finally {\n closeSync(fd);\n }\n}\n"]}
|
package/dist/log.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { escapeControls } from "./catalog/text.js";
|
|
2
|
+
const ORDER = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
3
|
+
const COUNTS = [
|
|
4
|
+
"documents",
|
|
5
|
+
"notIndexed",
|
|
6
|
+
"collisions",
|
|
7
|
+
"encodedFolders",
|
|
8
|
+
"admitted",
|
|
9
|
+
"refused",
|
|
10
|
+
"degraded",
|
|
11
|
+
"ms",
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* The closed allowlist of fields per event. Nothing outside it is written, so no question, path argument, title,
|
|
15
|
+
* body or snippet can reach the log through any event.
|
|
16
|
+
*/
|
|
17
|
+
const ALLOWED = {
|
|
18
|
+
"serve.start": ["company", "source", "lock", "dev", "node", "configRule", "note"],
|
|
19
|
+
"serve.refusing": ["problem", "detail"],
|
|
20
|
+
"serve.shutdown": ["reason"],
|
|
21
|
+
"engine.reset": ["detail"],
|
|
22
|
+
"load.done": COUNTS,
|
|
23
|
+
"load.fatal": ["rule", "path", "detail"],
|
|
24
|
+
"refresh.done": COUNTS,
|
|
25
|
+
"refresh.fatal": ["rule", "path", "detail"],
|
|
26
|
+
"refresh.failed": ["error", "detail"],
|
|
27
|
+
"load.served-previous": ["commit", "refused"],
|
|
28
|
+
"poller.tick": ["outcome", "ms", "error", "detail"],
|
|
29
|
+
"source.recloned": ["reason"],
|
|
30
|
+
"tool.call": ["tool", "ms", "hits", "engineQueries", "rowsFetched", "error"],
|
|
31
|
+
"transport.error": ["error"],
|
|
32
|
+
console: ["method", "text"],
|
|
33
|
+
};
|
|
34
|
+
/** The most characters a free-text field keeps; one error message cannot flood the log. */
|
|
35
|
+
export const LOG_VALUE_CAP = 2_000;
|
|
36
|
+
const clipValue = (value) => value.length <= LOG_VALUE_CAP ? value : `${value.slice(0, LOG_VALUE_CAP)}…`;
|
|
37
|
+
/** JSON lines on a stream, one object per line: `time`, `level`, `event`, then the event's allowed fields, control characters escaped. */
|
|
38
|
+
export function createLog(stream, level, clock = () => new Date()) {
|
|
39
|
+
const threshold = ORDER[level];
|
|
40
|
+
const write = (at, event, fields = {}) => {
|
|
41
|
+
if (ORDER[at] > threshold)
|
|
42
|
+
return;
|
|
43
|
+
const record = { time: clock().toISOString(), level: at, event };
|
|
44
|
+
const allowed = ALLOWED[event] ?? [];
|
|
45
|
+
for (const key of allowed) {
|
|
46
|
+
const value = fields[key];
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
record[key] =
|
|
50
|
+
typeof value === "string"
|
|
51
|
+
? escapeControls(clipValue(value))
|
|
52
|
+
: Array.isArray(value)
|
|
53
|
+
? value.map((v) => escapeControls(clipValue(String(v))))
|
|
54
|
+
: value;
|
|
55
|
+
}
|
|
56
|
+
stream.write(`${JSON.stringify(record)}\n`);
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
error: (event, fields) => write("error", event, fields),
|
|
60
|
+
warn: (event, fields) => write("warn", event, fields),
|
|
61
|
+
info: (event, fields) => write("info", event, fields),
|
|
62
|
+
debug: (event, fields) => write("debug", event, fields),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=log.js.map
|
package/dist/log.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"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","sourcesContent":["import { escapeControls } from \"./catalog/text.js\";\n\nexport type Level = \"error\" | \"warn\" | \"info\" | \"debug\";\nconst ORDER: Record<Level, number> = { error: 0, warn: 1, info: 2, debug: 3 };\n\nconst COUNTS = [\n \"documents\",\n \"notIndexed\",\n \"collisions\",\n \"encodedFolders\",\n \"admitted\",\n \"refused\",\n \"degraded\",\n \"ms\",\n] as const;\n\n/**\n * The closed allowlist of fields per event. Nothing outside it is written, so no question, path argument, title,\n * body or snippet can reach the log through any event.\n */\nconst ALLOWED: Record<string, readonly string[]> = {\n \"serve.start\": [\"company\", \"source\", \"lock\", \"dev\", \"node\", \"configRule\", \"note\"],\n \"serve.refusing\": [\"problem\", \"detail\"],\n \"serve.shutdown\": [\"reason\"],\n \"engine.reset\": [\"detail\"],\n \"load.done\": COUNTS,\n \"load.fatal\": [\"rule\", \"path\", \"detail\"],\n \"refresh.done\": COUNTS,\n \"refresh.fatal\": [\"rule\", \"path\", \"detail\"],\n \"refresh.failed\": [\"error\", \"detail\"],\n \"load.served-previous\": [\"commit\", \"refused\"],\n \"poller.tick\": [\"outcome\", \"ms\", \"error\", \"detail\"],\n \"source.recloned\": [\"reason\"],\n \"tool.call\": [\"tool\", \"ms\", \"hits\", \"engineQueries\", \"rowsFetched\", \"error\"],\n \"transport.error\": [\"error\"],\n console: [\"method\", \"text\"],\n};\n\nexport type Fields = Record<string, string | number | boolean | readonly string[] | undefined>;\n\n/** The most characters a free-text field keeps; one error message cannot flood the log. */\nexport const LOG_VALUE_CAP = 2_000;\nconst clipValue = (value: string): string =>\n value.length <= LOG_VALUE_CAP ? value : `${value.slice(0, LOG_VALUE_CAP)}…`;\n\nexport interface Log {\n error(event: string, fields?: Fields): void;\n warn(event: string, fields?: Fields): void;\n info(event: string, fields?: Fields): void;\n debug(event: string, fields?: Fields): void;\n}\n\n/** JSON lines on a stream, one object per line: `time`, `level`, `event`, then the event's allowed fields, control characters escaped. */\nexport function createLog(\n stream: { write(text: string): void },\n level: Level,\n clock: () => Date = () => new Date(),\n): Log {\n const threshold = ORDER[level];\n const write = (at: Level, event: string, fields: Fields = {}): void => {\n if (ORDER[at] > threshold) return;\n const record: Record<string, unknown> = { time: clock().toISOString(), level: at, event };\n const allowed = ALLOWED[event] ?? [];\n for (const key of allowed) {\n const value = fields[key];\n if (value === undefined) continue;\n record[key] =\n typeof value === \"string\"\n ? escapeControls(clipValue(value))\n : Array.isArray(value)\n ? value.map((v) => escapeControls(clipValue(String(v))))\n : value;\n }\n stream.write(`${JSON.stringify(record)}\\n`);\n };\n return {\n error: (event, fields) => write(\"error\", event, fields),\n warn: (event, fields) => write(\"warn\", event, fields),\n info: (event, fields) => write(\"info\", event, fields),\n debug: (event, fields) => write(\"debug\", event, fields),\n };\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import { DATA_SENTENCE } from "../catalog/text.js";
|
|
3
|
+
import { registerTools } from "./tools.js";
|
|
4
|
+
/** What the client is told at `initialize`: how to use the catalog, and the rules the tool results repeat. */
|
|
5
|
+
export const INSTRUCTIONS = [
|
|
6
|
+
"This server serves one company's knowledge bundle (Open Knowledge Format).",
|
|
7
|
+
"Start with `catalog` to see what exists, search with keywords through `search`, then read a page whole with `get_page`.",
|
|
8
|
+
"Cite every answer with the page's path, its trust tier, its verifier and its recheck date as the result states them; say so when a page is overdue or deprecated, and follow a replacement.",
|
|
9
|
+
"When no page answers, say that there is none. Never claim a page says what it does not.",
|
|
10
|
+
DATA_SENTENCE,
|
|
11
|
+
].join(" ");
|
|
12
|
+
/** Builds one server per connection. The runtime lives outside, so a probe connection costs nothing. */
|
|
13
|
+
export function createServerFactory(runtime, options, clock, log, version = "0.0.0") {
|
|
14
|
+
return () => {
|
|
15
|
+
const server = new McpServer({ name: "okf-catalog", version }, { instructions: INSTRUCTIONS });
|
|
16
|
+
registerTools(server, runtime, options, clock, log);
|
|
17
|
+
// The first load starts when a client sends `notifications/initialized` (the 2025-era handshake). A
|
|
18
|
+
// current-protocol client adopts the discovery result and sends neither `initialize` nor the notification;
|
|
19
|
+
// over stdio its first request that is not a probe starts the load (see `stdio.ts`), and any tool call does
|
|
20
|
+
// through `lease()`. A probe connection never gets this far.
|
|
21
|
+
const inner = server.server;
|
|
22
|
+
const previous = inner.oninitialized;
|
|
23
|
+
inner.oninitialized = () => {
|
|
24
|
+
previous?.();
|
|
25
|
+
runtime.start?.();
|
|
26
|
+
};
|
|
27
|
+
return server;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,8GAA8G;AAC9G,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,4EAA4E;IAC5E,yHAAyH;IACzH,6LAA6L;IAC7L,yFAAyF;IACzF,aAAa;CACd,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEZ,wGAAwG;AACxG,MAAM,UAAU,mBAAmB,CACjC,OAAgB,EAChB,OAAoB,EACpB,KAAiB,EACjB,GAAS,EACT,OAAO,GAAG,OAAO;IAEjB,OAAO,GAAG,EAAE;QACV,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC,CAAC;QAC/F,aAAa,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;QACpD,oGAAoG;QACpG,2GAA2G;QAC3G,4GAA4G;QAC5G,6DAA6D;QAC7D,MAAM,KAAK,GAAG,MAAM,CAAC,MAAwC,CAAC;QAC9D,MAAM,QAAQ,GAAG,KAAK,CAAC,aAAa,CAAC;QACrC,KAAK,CAAC,aAAa,GAAG,GAAG,EAAE;YACzB,QAAQ,EAAE,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QACpB,CAAC,CAAC;QACF,OAAO,MAAM,CAAC;IAChB,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import { McpServer } from \"@modelcontextprotocol/server\";\nimport type { Runtime, ToolOptions } from \"../catalog/runtime.js\";\nimport { DATA_SENTENCE } from \"../catalog/text.js\";\nimport type { Log } from \"../log.js\";\nimport { registerTools } from \"./tools.js\";\n\n/** What the client is told at `initialize`: how to use the catalog, and the rules the tool results repeat. */\nexport const INSTRUCTIONS = [\n \"This server serves one company's knowledge bundle (Open Knowledge Format).\",\n \"Start with `catalog` to see what exists, search with keywords through `search`, then read a page whole with `get_page`.\",\n \"Cite every answer with the page's path, its trust tier, its verifier and its recheck date as the result states them; say so when a page is overdue or deprecated, and follow a replacement.\",\n \"When no page answers, say that there is none. Never claim a page says what it does not.\",\n DATA_SENTENCE,\n].join(\" \");\n\n/** Builds one server per connection. The runtime lives outside, so a probe connection costs nothing. */\nexport function createServerFactory(\n runtime: Runtime,\n options: ToolOptions,\n clock: () => Date,\n log?: Log,\n version = \"0.0.0\",\n): () => McpServer {\n return () => {\n const server = new McpServer({ name: \"okf-catalog\", version }, { instructions: INSTRUCTIONS });\n registerTools(server, runtime, options, clock, log);\n // The first load starts when a client sends `notifications/initialized` (the 2025-era handshake). A\n // current-protocol client adopts the discovery result and sends neither `initialize` nor the notification;\n // over stdio its first request that is not a probe starts the load (see `stdio.ts`), and any tool call does\n // through `lease()`. A probe connection never gets this far.\n const inner = server.server as { oninitialized?: () => void };\n const previous = inner.oninitialized;\n inner.oninitialized = () => {\n previous?.();\n runtime.start?.();\n };\n return server;\n };\n}\n"]}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { StdioServerTransport, serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
2
|
+
/** Requests that must cost the server nothing: the probe, the handshake and liveness. Everything else is work. */
|
|
3
|
+
const PROBES = new Set(["server/discover", "initialize", "ping"]);
|
|
4
|
+
/**
|
|
5
|
+
* Calls `onWork` the first time a message arrives that means the client wants the bundle: the 2025-era
|
|
6
|
+
* `notifications/initialized`, or any request that is not a probe (a current-protocol client adopts the
|
|
7
|
+
* discovery result and sends neither `initialize` nor the notification, so its `tools/list` is the first sign).
|
|
8
|
+
* The SDK assigns the transport's `onmessage` when it connects; an accessor wraps whatever it assigns.
|
|
9
|
+
*/
|
|
10
|
+
function watchForWork(transport, onWork) {
|
|
11
|
+
let inner;
|
|
12
|
+
let started = false;
|
|
13
|
+
Object.defineProperty(transport, "onmessage", {
|
|
14
|
+
configurable: true,
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: () => inner,
|
|
17
|
+
set: (listener) => {
|
|
18
|
+
inner =
|
|
19
|
+
listener === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: (message, extra) => {
|
|
22
|
+
if (!started) {
|
|
23
|
+
const m = message;
|
|
24
|
+
const isWork = typeof m.method === "string" &&
|
|
25
|
+
(m.method === "notifications/initialized" ||
|
|
26
|
+
(m.id !== undefined && !PROBES.has(m.method)));
|
|
27
|
+
if (isWork) {
|
|
28
|
+
started = true;
|
|
29
|
+
onWork();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
listener(message, extra);
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** Serves the factory over the given streams. The transport writes to `stdout` and nothing else does. */
|
|
38
|
+
export function serveOverStdio(factory, streams, onerror, onWork) {
|
|
39
|
+
const transport = new StdioServerTransport(streams.stdin, streams.stdout);
|
|
40
|
+
if (onWork !== undefined)
|
|
41
|
+
watchForWork(transport, onWork);
|
|
42
|
+
const handle = serveStdio(factory, { transport, onerror });
|
|
43
|
+
return { close: () => handle.close() };
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/mcp/stdio.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAMtF,kHAAkH;AAClH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,iBAAiB,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;AAIlE;;;;;GAKG;AACH,SAAS,YAAY,CAAC,SAA+B,EAAE,MAAkB;IACvE,IAAI,KAA2B,CAAC;IAChC,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,WAAW,EAAE;QAC5C,YAAY,EAAE,IAAI;QAClB,UAAU,EAAE,IAAI;QAChB,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK;QAChB,GAAG,EAAE,CAAC,QAA8B,EAAE,EAAE;YACtC,KAAK;gBACH,QAAQ,KAAK,SAAS;oBACpB,CAAC,CAAC,SAAS;oBACX,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;wBACjB,IAAI,CAAC,OAAO,EAAE,CAAC;4BACb,MAAM,CAAC,GAAG,OAA6C,CAAC;4BACxD,MAAM,MAAM,GACV,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ;gCAC5B,CAAC,CAAC,CAAC,MAAM,KAAK,2BAA2B;oCACvC,CAAC,CAAC,CAAC,EAAE,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;4BACnD,IAAI,MAAM,EAAE,CAAC;gCACX,OAAO,GAAG,IAAI,CAAC;gCACf,MAAM,EAAE,CAAC;4BACX,CAAC;wBACH,CAAC;wBACD,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;oBAC3B,CAAC,CAAC;QACV,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,cAAc,CAC5B,OAAwB,EACxB,OAA8C,EAC9C,OAA+B,EAC/B,MAAmB;IAEnB,MAAM,SAAS,GAAG,IAAI,oBAAoB,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1E,IAAI,MAAM,KAAK,SAAS;QAAE,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAC1D,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3D,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;AACzC,CAAC","sourcesContent":["import type { Readable, Writable } from \"node:stream\";\nimport type { McpServer } from \"@modelcontextprotocol/server\";\nimport { StdioServerTransport, serveStdio } from \"@modelcontextprotocol/server/stdio\";\n\nexport interface StdioHandle {\n close(): Promise<void>;\n}\n\n/** Requests that must cost the server nothing: the probe, the handshake and liveness. Everything else is work. */\nconst PROBES = new Set([\"server/discover\", \"initialize\", \"ping\"]);\n\ntype Listener = (message: unknown, extra?: unknown) => void;\n\n/**\n * Calls `onWork` the first time a message arrives that means the client wants the bundle: the 2025-era\n * `notifications/initialized`, or any request that is not a probe (a current-protocol client adopts the\n * discovery result and sends neither `initialize` nor the notification, so its `tools/list` is the first sign).\n * The SDK assigns the transport's `onmessage` when it connects; an accessor wraps whatever it assigns.\n */\nfunction watchForWork(transport: StdioServerTransport, onWork: () => void): void {\n let inner: Listener | undefined;\n let started = false;\n Object.defineProperty(transport, \"onmessage\", {\n configurable: true,\n enumerable: true,\n get: () => inner,\n set: (listener: Listener | undefined) => {\n inner =\n listener === undefined\n ? undefined\n : (message, extra) => {\n if (!started) {\n const m = message as { method?: unknown; id?: unknown };\n const isWork =\n typeof m.method === \"string\" &&\n (m.method === \"notifications/initialized\" ||\n (m.id !== undefined && !PROBES.has(m.method)));\n if (isWork) {\n started = true;\n onWork();\n }\n }\n listener(message, extra);\n };\n },\n });\n}\n\n/** Serves the factory over the given streams. The transport writes to `stdout` and nothing else does. */\nexport function serveOverStdio(\n factory: () => McpServer,\n streams: { stdin: Readable; stdout: Writable },\n onerror: (error: Error) => void,\n onWork?: () => void,\n): StdioHandle {\n const transport = new StdioServerTransport(streams.stdin, streams.stdout);\n if (onWork !== undefined) watchForWork(transport, onWork);\n const handle = serveStdio(factory, { transport, onerror });\n return { close: () => handle.close() };\n}\n"]}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
import { byCodeUnit } from "../bundle/paths.js";
|
|
3
|
+
import { listTypes } from "../catalog/model.js";
|
|
4
|
+
import { nearestPaths } from "../catalog/nearest.js";
|
|
5
|
+
import { CatalogOutputSchema, PageOutputSchema, projectCatalog, projectPage, projectReserved, projectSearch, projectStatus, refusingText, SearchOutputSchema, StatusOutputSchema, statusSummary, } from "../catalog/outputs.js";
|
|
6
|
+
import { DATA_SENTENCE, safe } from "../catalog/text.js";
|
|
7
|
+
import { search } from "../search/search.js";
|
|
8
|
+
const FOLDER_LIST_CAP = 50;
|
|
9
|
+
const fail = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
10
|
+
const ok = (text, structured) => ({
|
|
11
|
+
content: [{ type: "text", text }],
|
|
12
|
+
structuredContent: structured,
|
|
13
|
+
});
|
|
14
|
+
/** A blank optional string means absent. */
|
|
15
|
+
const blank = (value) => value === undefined || value.trim().length === 0 ? undefined : value.trim();
|
|
16
|
+
/** A folder as the search filter normalises it: no leading or trailing slashes; the empty string is the root. */
|
|
17
|
+
const normaliseFolder = (value) => value.trim().replace(/^\/+|\/+$/g, "");
|
|
18
|
+
/** A page path as the model may write it: one leading `/` or `./` stripped (OKF bundle-absolute links). */
|
|
19
|
+
const normalisePath = (value) => value.trim().replace(/^(\.\/|\/)/, "");
|
|
20
|
+
function folderList(catalog) {
|
|
21
|
+
const names = [...catalog.folders.keys()]
|
|
22
|
+
.sort(byCodeUnit)
|
|
23
|
+
.map((f) => (f === "" ? "(root)" : safe(f)));
|
|
24
|
+
const shown = names.slice(0, FOLDER_LIST_CAP).join(", ");
|
|
25
|
+
return names.length > FOLDER_LIST_CAP ? `${shown} … (${names.length} folders)` : shown;
|
|
26
|
+
}
|
|
27
|
+
function refused(generation) {
|
|
28
|
+
const fatal = generation.report.fatal;
|
|
29
|
+
if (fatal === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
return fail(`the bundle was refused and nothing is served: ${safe(fatal.rule)}${fatal.path ? ` (${safe(fatal.path)})` : ""}: ${safe(fatal.detail)}`);
|
|
32
|
+
}
|
|
33
|
+
function reservedAt(catalog, path) {
|
|
34
|
+
const slash = path.lastIndexOf("/");
|
|
35
|
+
const folder = slash === -1 ? "" : path.slice(0, slash);
|
|
36
|
+
const name = slash === -1 ? path : path.slice(slash + 1);
|
|
37
|
+
const entry = catalog.folders.get(folder);
|
|
38
|
+
if (entry === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (name === "index.md" && entry.index !== undefined)
|
|
41
|
+
return { file: entry.index, source: entry.indexSource };
|
|
42
|
+
if (name === "log.md" && entry.log !== undefined)
|
|
43
|
+
return { file: entry.log, source: "file" };
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
function servedPaths(catalog) {
|
|
47
|
+
const paths = [...catalog.pages.keys()];
|
|
48
|
+
for (const [folder, entry] of catalog.folders) {
|
|
49
|
+
const prefix = folder === "" ? "" : `${folder}/`;
|
|
50
|
+
if (entry.index !== undefined)
|
|
51
|
+
paths.push(`${prefix}index.md`);
|
|
52
|
+
if (entry.log !== undefined)
|
|
53
|
+
paths.push(`${prefix}log.md`);
|
|
54
|
+
}
|
|
55
|
+
return paths;
|
|
56
|
+
}
|
|
57
|
+
function pageText(output) {
|
|
58
|
+
const tail = output.truncated && output.nextOffset !== undefined
|
|
59
|
+
? `\n[truncated at the result budget; continue with offset ${output.nextOffset}]`
|
|
60
|
+
: "";
|
|
61
|
+
return `${output.citation}\n${output.notice}\n${output.body}${tail}`;
|
|
62
|
+
}
|
|
63
|
+
const describeType = (text) => `${text} ${DATA_SENTENCE}`;
|
|
64
|
+
/** The one sentence every tool answers with while the server cannot serve: the fix, never a path the model has no business with. */
|
|
65
|
+
const refusingSentence = (refusing) => `the server is refusing every request until its configuration is fixed: ${refusingText(refusing)}`;
|
|
66
|
+
/**
|
|
67
|
+
* The four tools. Every handler runs under a lease on the current generation, answers in both channels, and
|
|
68
|
+
* turns anything it cannot repair into a fixed sentence; the detail goes to the log, never to the model.
|
|
69
|
+
*/
|
|
70
|
+
export function registerTools(server, runtime, options, clock, log) {
|
|
71
|
+
const guarded = (tool, fn) => async (args) => {
|
|
72
|
+
const started = performance.now();
|
|
73
|
+
const refusing = runtime.status().refusing;
|
|
74
|
+
if (refusing !== undefined)
|
|
75
|
+
return fail(refusingSentence(refusing));
|
|
76
|
+
try {
|
|
77
|
+
const { logFields, ...result } = await runtime.lease(async (generation, engine) => fn(args, generation, engine));
|
|
78
|
+
const hits = result.structuredContent?.hits?.length;
|
|
79
|
+
log?.info("tool.call", {
|
|
80
|
+
tool,
|
|
81
|
+
ms: Math.round(performance.now() - started),
|
|
82
|
+
...(hits === undefined ? {} : { hits }),
|
|
83
|
+
...(logFields ?? {}),
|
|
84
|
+
...(result.isError === true ? { error: "answered-with-error" } : {}),
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
log?.error("tool.call", {
|
|
90
|
+
tool,
|
|
91
|
+
ms: Math.round(performance.now() - started),
|
|
92
|
+
error: error.message,
|
|
93
|
+
});
|
|
94
|
+
// The call that started the first load is the one that sees it fail: answer with the fix, as later calls do.
|
|
95
|
+
const refusingNow = runtime.status().refusing;
|
|
96
|
+
if (refusingNow !== undefined)
|
|
97
|
+
return fail(refusingSentence(refusingNow));
|
|
98
|
+
return fail("the server hit a defect answering this call; its log has the detail");
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
server.registerTool("search", {
|
|
102
|
+
title: "Search the knowledge bundle",
|
|
103
|
+
description: describeType("Finds pages by keywords. Write one concept per word; common words are dropped, and when no page holds every word the match is relaxed and the result says so. Each hit carries its path, type, status, trust tier, recheck date, source count, resource and a quoted snippet."),
|
|
104
|
+
inputSchema: z.object({
|
|
105
|
+
question: z
|
|
106
|
+
.string()
|
|
107
|
+
.min(1)
|
|
108
|
+
.max(200)
|
|
109
|
+
.describe("Keywords, or a short question; one concept per word."),
|
|
110
|
+
type: z
|
|
111
|
+
.string()
|
|
112
|
+
.max(200)
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("Only pages of this type (case does not matter)."),
|
|
115
|
+
topic: z
|
|
116
|
+
.string()
|
|
117
|
+
.max(1024)
|
|
118
|
+
.optional()
|
|
119
|
+
.describe("Only pages under this folder of the bundle."),
|
|
120
|
+
include_stale: z
|
|
121
|
+
.boolean()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("Include pages past their recheck date; they are flagged as overdue."),
|
|
124
|
+
limit: z.number().int().min(1).max(25).optional().describe("How many hits, 1 to 25."),
|
|
125
|
+
}),
|
|
126
|
+
outputSchema: SearchOutputSchema,
|
|
127
|
+
annotations: { readOnlyHint: true },
|
|
128
|
+
}, guarded("search", async (args, generation, engine) => {
|
|
129
|
+
const stop = refused(generation);
|
|
130
|
+
if (stop !== undefined)
|
|
131
|
+
return stop;
|
|
132
|
+
const wantedType = blank(args.type);
|
|
133
|
+
let type;
|
|
134
|
+
if (wantedType !== undefined) {
|
|
135
|
+
const types = listTypes(generation.catalog);
|
|
136
|
+
type = types.find((t) => t.toLowerCase() === wantedType.toLowerCase());
|
|
137
|
+
if (type === undefined) {
|
|
138
|
+
return fail(`no page has the type ${JSON.stringify(safe(wantedType))}; the types in use are: ${types.map(safe).join(", ") || "(none)"}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const wantedTopic = blank(args.topic);
|
|
142
|
+
let topic;
|
|
143
|
+
if (wantedTopic !== undefined) {
|
|
144
|
+
topic = normaliseFolder(wantedTopic);
|
|
145
|
+
if (topic.length > 0 && !generation.catalog.folders.has(topic)) {
|
|
146
|
+
return fail(`there is no folder ${JSON.stringify(safe(topic))}; the folders are: ${folderList(generation.catalog)}`);
|
|
147
|
+
}
|
|
148
|
+
if (topic.length === 0)
|
|
149
|
+
topic = undefined;
|
|
150
|
+
}
|
|
151
|
+
const now = clock();
|
|
152
|
+
const response = await search(generation.catalog, engine, {
|
|
153
|
+
question: args.question,
|
|
154
|
+
...(type === undefined ? {} : { type }),
|
|
155
|
+
...(topic === undefined ? {} : { topic }),
|
|
156
|
+
includeStale: args.include_stale ?? false,
|
|
157
|
+
limit: args.limit ?? options.limitDefault,
|
|
158
|
+
}, now);
|
|
159
|
+
if (response.reason === "no-content-terms") {
|
|
160
|
+
return fail("every word of the question is a common word the index ignores; ask with keywords, the distinctive words a page would use");
|
|
161
|
+
}
|
|
162
|
+
const output = projectSearch(response, generation.catalog, now, { dev: options.dev });
|
|
163
|
+
return {
|
|
164
|
+
...ok([output.summary, ...output.hits.map((h) => h.citation)].join("\n"), output),
|
|
165
|
+
logFields: { engineQueries: response.engineQueries, rowsFetched: response.rowsFetched },
|
|
166
|
+
};
|
|
167
|
+
}));
|
|
168
|
+
server.registerTool("get_page", {
|
|
169
|
+
title: "Read a page",
|
|
170
|
+
description: describeType("Returns one page whole, with its provenance header first: path, type, status, trust tier, verifier, recheck date and deprecation. Reserved files (index.md, log.md) are served too. A long page is cut at the result budget and says where to continue."),
|
|
171
|
+
inputSchema: z.object({
|
|
172
|
+
path: z
|
|
173
|
+
.string()
|
|
174
|
+
.min(1)
|
|
175
|
+
.max(1024)
|
|
176
|
+
.describe("The page's path in the bundle, as a search result or a catalog lists it."),
|
|
177
|
+
offset: z
|
|
178
|
+
.number()
|
|
179
|
+
.int()
|
|
180
|
+
.min(0)
|
|
181
|
+
.optional()
|
|
182
|
+
.describe("Continue a long page from this character offset."),
|
|
183
|
+
}),
|
|
184
|
+
outputSchema: PageOutputSchema,
|
|
185
|
+
annotations: { readOnlyHint: true },
|
|
186
|
+
}, guarded("get_page", (args, generation) => {
|
|
187
|
+
const stop = refused(generation);
|
|
188
|
+
if (stop !== undefined)
|
|
189
|
+
return stop;
|
|
190
|
+
const path = normalisePath(args.path);
|
|
191
|
+
const offset = args.offset ?? 0;
|
|
192
|
+
const page = generation.catalog.pages.get(path);
|
|
193
|
+
if (page !== undefined) {
|
|
194
|
+
const output = projectPage(page, clock(), offset, options.resultBudget);
|
|
195
|
+
return ok(pageText(output), output);
|
|
196
|
+
}
|
|
197
|
+
const reserved = reservedAt(generation.catalog, path);
|
|
198
|
+
if (reserved !== undefined) {
|
|
199
|
+
const output = projectReserved(reserved.file, reserved.source, offset, options.resultBudget);
|
|
200
|
+
return ok(pageText(output), output);
|
|
201
|
+
}
|
|
202
|
+
const nearest = nearestPaths(servedPaths(generation.catalog), path);
|
|
203
|
+
return fail(`no page at ${JSON.stringify(safe(path))}; the nearest served paths are: ${nearest.map(safe).join(", ") || "(none)"}`);
|
|
204
|
+
}));
|
|
205
|
+
server.registerTool("catalog", {
|
|
206
|
+
title: "List a folder of the bundle",
|
|
207
|
+
description: describeType("Returns a folder's index: its pages with their titles and descriptions, and the folder's index text as the company wrote it or as the server generated it. Start here, at the root, to see what exists."),
|
|
208
|
+
inputSchema: z.object({
|
|
209
|
+
folder: z
|
|
210
|
+
.string()
|
|
211
|
+
.max(1024)
|
|
212
|
+
.optional()
|
|
213
|
+
.describe("A folder of the bundle; omit for the root."),
|
|
214
|
+
offset: z
|
|
215
|
+
.number()
|
|
216
|
+
.int()
|
|
217
|
+
.min(0)
|
|
218
|
+
.optional()
|
|
219
|
+
.describe("Continue a long index from this character offset."),
|
|
220
|
+
}),
|
|
221
|
+
outputSchema: CatalogOutputSchema,
|
|
222
|
+
annotations: { readOnlyHint: true },
|
|
223
|
+
}, guarded("catalog", (args, generation) => {
|
|
224
|
+
const stop = refused(generation);
|
|
225
|
+
if (stop !== undefined)
|
|
226
|
+
return stop;
|
|
227
|
+
const folder = normaliseFolder(args.folder ?? "");
|
|
228
|
+
const output = projectCatalog(generation.catalog, folder, args.offset ?? 0, options.resultBudget);
|
|
229
|
+
if (output === undefined) {
|
|
230
|
+
return fail(`there is no folder ${JSON.stringify(safe(folder))}; the folders are: ${folderList(generation.catalog)}`);
|
|
231
|
+
}
|
|
232
|
+
const tail = output.truncated && output.nextOffset !== undefined
|
|
233
|
+
? `\n[truncated at the result budget; continue with offset ${output.nextOffset}]`
|
|
234
|
+
: "";
|
|
235
|
+
const head = `catalog of ${folder === "" ? "the bundle root" : safe(folder)} (${output.source} index, ${output.entries.length} pages)`;
|
|
236
|
+
return ok(`${head}\n${output.notice}\n${output.text}${tail}`, output);
|
|
237
|
+
}));
|
|
238
|
+
server.registerTool("status", {
|
|
239
|
+
title: "Report the bundle's state",
|
|
240
|
+
description: describeType("Reports what was loaded: counts of pages admitted, refused and degraded, the lists the report carries, the integrity mode, the engine's counts and the lock. Nothing in it is page text."),
|
|
241
|
+
inputSchema: z.object({}),
|
|
242
|
+
outputSchema: StatusOutputSchema,
|
|
243
|
+
annotations: { readOnlyHint: true },
|
|
244
|
+
}, guarded("status", (_args, generation) => {
|
|
245
|
+
const output = projectStatus(generation, runtime.status(), options, clock());
|
|
246
|
+
return ok(statusSummary(output), output);
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=tools.js.map
|