clay-server 5.0.2-beta.1 → 5.1.0-beta.2
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/LICENSES/README.md +4 -0
- package/LICENSES/dicebear-avatars.md +16 -0
- package/lib/daemon-project-access.js +7 -1
- package/lib/daemon.js +2 -0
- package/lib/dicebear-avatar.js +62 -0
- package/lib/knowledge-db-service.js +175 -0
- package/lib/knowledge-db-validation.js +0 -0
- package/lib/knowledge-document-index.js +49 -0
- package/lib/knowledge-draft-store.js +56 -0
- package/lib/knowledge-file-store.js +199 -0
- package/lib/knowledge-markdown.js +128 -0
- package/lib/mate-knowledge-mcp-server.js +29 -6
- package/lib/mate-knowledge-service.js +23 -2
- package/lib/mate-knowledge-sync.js +36 -21
- package/lib/mate-workspace-color.js +4 -0
- package/lib/mates.js +18 -0
- package/lib/project-connection.js +36 -2
- package/lib/project-debate-proposal.js +6 -0
- package/lib/project-debate-workspace.js +43 -0
- package/lib/project-debate.js +1 -0
- package/lib/project-file-path.js +2 -3
- package/lib/project-knowledge.js +315 -136
- package/lib/project-mate-creation-proposal.js +1 -0
- package/lib/project-request-access.js +13 -1
- package/lib/project-sessions.js +39 -2
- package/lib/project-user-knowledge.js +60 -0
- package/lib/project.js +59 -18
- package/lib/public/app.js +73 -34
- package/lib/public/clay-studio-wordmark-bold.svg +20 -0
- package/lib/public/css/clay-workspace.css +105 -0
- package/lib/public/css/debates-workbench.css +47 -0
- package/lib/public/css/filebrowser.css +10 -0
- package/lib/public/css/home-debate-live.css +26 -0
- package/lib/public/css/home-hub.css +3 -1
- package/lib/public/css/home-mate-model-picker.css +15 -131
- package/lib/public/css/home-mate-settings.css +168 -16
- package/lib/public/css/icon-strip.css +249 -32
- package/lib/public/css/mate-conversation.css +176 -0
- package/lib/public/css/mate-knowledge-workbench.css +118 -0
- package/lib/public/css/mate-workspace-color.css +16 -0
- package/lib/public/css/mates.css +28 -28
- package/lib/public/css/messages.css +1 -114
- package/lib/public/css/mobile-nav.css +16 -16
- package/lib/public/css/overlays.css +8 -0
- package/lib/public/css/project-logs.css +7 -0
- package/lib/public/css/project-mate-creation.css +105 -0
- package/lib/public/css/right-workbench.css +3 -3
- package/lib/public/css/session-folders.css +80 -21
- package/lib/public/css/sidebar.css +183 -107
- package/lib/public/css/title-bar.css +151 -0
- package/lib/public/css/worker-delegation.css +47 -0
- package/lib/public/css/worker-proposal.css +72 -150
- package/lib/public/css/you.css +74 -0
- package/lib/public/index.html +33 -101
- package/lib/public/modules/account-island.js +230 -0
- package/lib/public/modules/app-cursors.js +19 -43
- package/lib/public/modules/app-debate-ui.js +4 -3
- package/lib/public/modules/app-header.js +106 -0
- package/lib/public/modules/app-message-router.js +12 -3
- package/lib/public/modules/app-messages.js +34 -9
- package/lib/public/modules/app-notifications.js +4 -7
- package/lib/public/modules/app-projects.js +5 -38
- package/lib/public/modules/app-rendering.js +12 -13
- package/lib/public/modules/avatar.js +25 -9
- package/lib/public/modules/chat-render-runtime.js +16 -4
- package/lib/public/modules/command-palette.js +2 -15
- package/lib/public/modules/cursor-sharing-preference.js +3 -3
- package/lib/public/modules/debate-controls.js +299 -0
- package/lib/public/modules/debate.js +2 -0
- package/lib/public/modules/debates-workbench.js +339 -0
- package/lib/public/modules/default-ai.js +81 -65
- package/lib/public/modules/filebrowser.js +28 -8
- package/lib/public/modules/home-debate-controls.js +18 -289
- package/lib/public/modules/home-mate-avatar-editor.js +228 -0
- package/lib/public/modules/home-mate-color-picker.js +66 -0
- package/lib/public/modules/home-mate-creation.js +1 -1
- package/lib/public/modules/home-mate-instructions.js +90 -0
- package/lib/public/modules/home-mate-model-picker.js +80 -271
- package/lib/public/modules/home-mate-profile-editor.js +72 -0
- package/lib/public/modules/home-mate-settings.js +118 -125
- package/lib/public/modules/home-shell.js +3 -0
- package/lib/public/modules/home-surface-boot.js +1 -9
- package/lib/public/modules/html-preview.js +19 -0
- package/lib/public/modules/icon-strip-tooltip.js +95 -0
- package/lib/public/modules/knowledge-db-edit-state.js +35 -0
- package/lib/public/modules/knowledge-db-workbench.js +234 -0
- package/lib/public/modules/knowledge-history-state.js +55 -0
- package/lib/public/modules/knowledge-markdown-editor.js +57 -0
- package/lib/public/modules/knowledge-preview-links.js +70 -0
- package/lib/public/modules/knowledge-workbench-shell.js +21 -0
- package/lib/public/modules/mate-knowledge-workbench-state.js +174 -0
- package/lib/public/modules/mate-knowledge-workbench.js +500 -0
- package/lib/public/modules/mate-knowledge.js +11 -3
- package/lib/public/modules/mate-management.js +54 -10
- package/lib/public/modules/mate-sidebar.js +11 -1
- package/lib/public/modules/mate-workspace-color.js +33 -0
- package/lib/public/modules/mention.js +5 -13
- package/lib/public/modules/profile.js +51 -18
- package/lib/public/modules/project-chat-identity.js +79 -0
- package/lib/public/modules/project-debate-planning-workspace.js +250 -0
- package/lib/public/modules/project-debate-workspace.js +185 -0
- package/lib/public/modules/project-header-identity.js +127 -0
- package/lib/public/modules/project-logs-render.js +9 -4
- package/lib/public/modules/project-mate-creation-workspace.js +414 -0
- package/lib/public/modules/project-mate-navigation.js +238 -0
- package/lib/public/modules/project-session-navigation.js +29 -0
- package/lib/public/modules/project-switcher.js +6 -8
- package/lib/public/modules/projects-hub-view.js +5 -6
- package/lib/public/modules/projects-hub.js +2 -6
- package/lib/public/modules/qrcode.js +27 -8
- package/lib/public/modules/search-clay-chat.js +8 -11
- package/lib/public/modules/session-activity-tooltip.js +12 -0
- package/lib/public/modules/session-create-form.js +86 -14
- package/lib/public/modules/session-create.js +50 -7
- package/lib/public/modules/session-list-visibility.js +41 -0
- package/lib/public/modules/session-presence-ui.js +4 -1
- package/lib/public/modules/session-title-marquee.js +51 -0
- package/lib/public/modules/sidebar-mates.js +3 -50
- package/lib/public/modules/sidebar-mobile.js +63 -60
- package/lib/public/modules/sidebar-projects.js +84 -97
- package/lib/public/modules/sidebar-session-folders-ui.js +2 -2
- package/lib/public/modules/sidebar-session-hierarchy.js +149 -70
- package/lib/public/modules/sidebar-sessions.js +49 -50
- package/lib/public/modules/sidebar.js +11 -13
- package/lib/public/modules/thinking-lifecycle.js +7 -6
- package/lib/public/modules/tool-palette-order.js +4 -3
- package/lib/public/modules/tool-palette.js +13 -3
- package/lib/public/modules/tools.js +22 -22
- package/lib/public/modules/worker-proposal.js +42 -8
- package/lib/public/modules/worker-runtime-picker.js +115 -0
- package/lib/public/modules/you-activity.js +121 -0
- package/lib/public/modules/you.js +251 -0
- package/lib/public/style.css +16 -8
- package/lib/public/vendor/codemirror/LICENSE +21 -0
- package/lib/public/vendor/codemirror/codemirror.css +344 -0
- package/lib/public/vendor/codemirror/codemirror.js +9884 -0
- package/lib/public/vendor/codemirror/continuelist.js +101 -0
- package/lib/public/vendor/codemirror/markdown.js +886 -0
- package/lib/public/vendor/codemirror/show-hint.css +37 -0
- package/lib/public/vendor/codemirror/show-hint.js +523 -0
- package/lib/sdk-bridge.js +15 -3
- package/lib/server-admin.js +11 -0
- package/lib/server-home-chat-events.js +1 -1
- package/lib/server-home-chat.js +25 -3
- package/lib/server-home-debate-planning.js +4 -1
- package/lib/server-home-debates.js +2 -0
- package/lib/server-home-mate-creation.js +4 -1
- package/lib/server-mate-instructions.js +44 -0
- package/lib/server-mates.js +12 -3
- package/lib/server-palette.js +1 -1
- package/lib/server-settings.js +65 -1
- package/lib/server-user-knowledge.js +119 -0
- package/lib/server.js +21 -1
- package/lib/session-visibility.js +7 -2
- package/lib/sessions.js +32 -6
- package/lib/user-knowledge-service.js +167 -0
- package/lib/users-permissions.js +7 -0
- package/lib/users-project-mate-filter-preferences.js +39 -0
- package/lib/users.js +6 -0
- package/lib/workspace-query-access.js +4 -1
- package/lib/ws-schema.js +50 -0
- package/package.json +10 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
var fs = require("fs");
|
|
3
|
+
var path = require("path");
|
|
4
|
+
|
|
5
|
+
var MAX_CONTENT_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
var RESERVED = { "session-digests.jsonl": true, "sticky-notes.md": true, "memory-summary.md": true };
|
|
7
|
+
|
|
8
|
+
function revision(content) { return crypto.createHash("sha256").update(content, "utf8").digest("hex"); }
|
|
9
|
+
function cleanName(value) {
|
|
10
|
+
if (typeof value !== "string") return null;
|
|
11
|
+
var name = value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
12
|
+
if (!name || name.length > 500 || name[0] === "/" || /[\x00-\x1f\x7f]/.test(name)) return null;
|
|
13
|
+
var parts = name.split("/");
|
|
14
|
+
if (parts.length > 20 || parts.some(function (part) { return !part || part === "." || part === ".." || part.length > 120; })) return null;
|
|
15
|
+
if (!/\.(md|jsonl)$/i.test(name) || RESERVED[name]) return null;
|
|
16
|
+
return parts.join("/");
|
|
17
|
+
}
|
|
18
|
+
function validName(value) { return cleanName(value) !== null; }
|
|
19
|
+
function itemType(name) { return /\.jsonl$/i.test(name) ? "db" : "text"; }
|
|
20
|
+
function atomicJson(target, value) {
|
|
21
|
+
var temporary = target + ".tmp-" + process.pid + "-" + crypto.randomBytes(6).toString("hex");
|
|
22
|
+
try { fs.writeFileSync(temporary, JSON.stringify(value, null, 2), { flag: "wx", mode: 0o600 }); fs.renameSync(temporary, target); }
|
|
23
|
+
finally { try { fs.unlinkSync(temporary); } catch (error) {} }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createKnowledgeFileStore(cwd, options) {
|
|
27
|
+
var directory = path.join(cwd, "knowledge");
|
|
28
|
+
var settings = options || {};
|
|
29
|
+
var metaRoot = settings.metaDir || path.join(cwd, ".clay", "knowledge-documents");
|
|
30
|
+
var registryPath = path.join(metaRoot, "documents.json");
|
|
31
|
+
var historyRoot = path.join(metaRoot, "history");
|
|
32
|
+
var fileCache = {};
|
|
33
|
+
function safeDirectory(target, create) {
|
|
34
|
+
var stat;
|
|
35
|
+
try { stat = fs.lstatSync(target); }
|
|
36
|
+
catch (error) { if (!create || error.code !== "ENOENT") throw error; fs.mkdirSync(target, { recursive: true, mode: 0o700 }); stat = fs.lstatSync(target); }
|
|
37
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw Object.assign(new Error("Knowledge metadata path is not a safe directory."), { code: "UNSAFE_PATH" });
|
|
38
|
+
}
|
|
39
|
+
function ensureMetadata(createHistory) {
|
|
40
|
+
safeDirectory(metaRoot, true);
|
|
41
|
+
var registryStat;
|
|
42
|
+
try { registryStat = fs.lstatSync(registryPath); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
43
|
+
if (registryStat && (!registryStat.isFile() || registryStat.isSymbolicLink())) throw Object.assign(new Error("Knowledge metadata registry is not a safe file."), { code: "UNSAFE_PATH" });
|
|
44
|
+
if (createHistory) safeDirectory(historyRoot, true);
|
|
45
|
+
else { try { safeDirectory(historyRoot, false); } catch (error) { if (error.code !== "ENOENT") throw error; } }
|
|
46
|
+
}
|
|
47
|
+
function ensureDirectory(create) {
|
|
48
|
+
var stat;
|
|
49
|
+
try { stat = fs.lstatSync(directory); }
|
|
50
|
+
catch (error) { if (!create || error.code !== "ENOENT") throw error; fs.mkdirSync(directory, { recursive: true }); stat = fs.lstatSync(directory); }
|
|
51
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Knowledge directory is not a safe directory.");
|
|
52
|
+
}
|
|
53
|
+
function targetPath(name, createParents) {
|
|
54
|
+
var clean = cleanName(name); if (!clean) throw new Error("Invalid knowledge file name."); ensureDirectory(!!createParents);
|
|
55
|
+
var parts = clean.split("/"); var cursor = directory;
|
|
56
|
+
for (var i = 0; i < parts.length - 1; i++) {
|
|
57
|
+
cursor = path.join(cursor, parts[i]); var stat;
|
|
58
|
+
try { stat = fs.lstatSync(cursor); }
|
|
59
|
+
catch (error) { if (!createParents || error.code !== "ENOENT") throw error; fs.mkdirSync(cursor, { mode: 0o700 }); stat = fs.lstatSync(cursor); }
|
|
60
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Knowledge path contains an unsafe directory.");
|
|
61
|
+
}
|
|
62
|
+
return path.join(directory, clean);
|
|
63
|
+
}
|
|
64
|
+
function registry() {
|
|
65
|
+
ensureMetadata(false);
|
|
66
|
+
try { var parsed = JSON.parse(fs.readFileSync(registryPath, "utf8")); return parsed && parsed.version === 1 && parsed.documents ? parsed : { version: 1, documents: {} }; }
|
|
67
|
+
catch (error) { if (error.code === "ENOENT" || error instanceof SyntaxError) return { version: 1, documents: {} }; throw error; }
|
|
68
|
+
}
|
|
69
|
+
function writeRegistry(value) { ensureMetadata(false); atomicJson(registryPath, value); }
|
|
70
|
+
function existing(name) {
|
|
71
|
+
var clean = cleanName(name); if (!clean) throw new Error("Invalid knowledge file name."); var target; var stat;
|
|
72
|
+
try { target = targetPath(clean, false); } catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
73
|
+
try { stat = fs.lstatSync(target); } catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
74
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Knowledge file is not a safe regular file.");
|
|
75
|
+
if (stat.size > MAX_CONTENT_BYTES) throw Object.assign(new Error("Knowledge document exceeds the safe read limit."), { code: "TOO_LARGE" });
|
|
76
|
+
var cached = fileCache[clean]; var content;
|
|
77
|
+
if (cached && cached.size === stat.size && cached.mtime === stat.mtimeMs && cached.ctime === stat.ctimeMs && cached.ino === stat.ino) content = cached.content;
|
|
78
|
+
else { content = fs.readFileSync(target, "utf8"); fileCache[clean] = { content: content, revision: revision(content), size: stat.size, mtime: stat.mtimeMs, ctime: stat.ctimeMs, ino: stat.ino }; }
|
|
79
|
+
cached = fileCache[clean]; return { name: clean, content: content, revision: cached.revision, size: stat.size, mtime: stat.mtimeMs, itemType: itemType(clean) };
|
|
80
|
+
}
|
|
81
|
+
function walk(relative, out) {
|
|
82
|
+
var base = relative ? path.join(directory, relative) : directory; var names = fs.readdirSync(base);
|
|
83
|
+
for (var i = 0; i < names.length; i++) {
|
|
84
|
+
var rel = relative ? relative + "/" + names[i] : names[i]; var stat;
|
|
85
|
+
try { stat = fs.lstatSync(path.join(directory, rel)); } catch (error) { continue; }
|
|
86
|
+
if (stat.isSymbolicLink()) continue;
|
|
87
|
+
if (stat.isDirectory()) { walk(rel, out); continue; }
|
|
88
|
+
if (!stat.isFile() || !validName(rel)) continue;
|
|
89
|
+
var item; try { item = existing(rel); } catch (error) {
|
|
90
|
+
if (error.code === "TOO_LARGE") item = { name: rel, content: null, revision: null, size: stat.size, mtime: stat.mtimeMs, itemType: itemType(rel), readError: error.message };
|
|
91
|
+
else continue;
|
|
92
|
+
}
|
|
93
|
+
if (item) out.push(item);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function reconcileIdentity(items) {
|
|
97
|
+
var data = registry(); var changed = false; var used = {}; var currentNames = {};
|
|
98
|
+
items.forEach(function (item) { currentNames[item.name] = true; });
|
|
99
|
+
Object.keys(data.documents).forEach(function (id) { if (!data.documents[id].deleted) used[data.documents[id].name] = id; });
|
|
100
|
+
for (var i = 0; i < items.length; i++) {
|
|
101
|
+
var id = used[items[i].name];
|
|
102
|
+
if (!id) {
|
|
103
|
+
var candidates = items[i].revision ? Object.keys(data.documents).filter(function (key) { var doc = data.documents[key]; return !currentNames[doc.name] && doc.lastRevision === items[i].revision; }) : [];
|
|
104
|
+
id = candidates.length === 1 ? candidates[0] : crypto.randomUUID(); changed = true;
|
|
105
|
+
}
|
|
106
|
+
if (!data.documents[id] || data.documents[id].name !== items[i].name || data.documents[id].lastRevision !== items[i].revision || data.documents[id].deleted) changed = true;
|
|
107
|
+
data.documents[id] = Object.assign({}, data.documents[id] || {}, { id: id, name: items[i].name,
|
|
108
|
+
lastRevision: items[i].revision || data.documents[id] && data.documents[id].lastRevision || null, deleted: false });
|
|
109
|
+
items[i].id = id; used[items[i].name] = id;
|
|
110
|
+
}
|
|
111
|
+
Object.keys(data.documents).forEach(function (id) {
|
|
112
|
+
var found = items.some(function (item) { return item.id === id; });
|
|
113
|
+
if (!found && !data.documents[id].deleted) { data.documents[id].deleted = true; changed = true; }
|
|
114
|
+
});
|
|
115
|
+
if (changed) writeRegistry(data); return data;
|
|
116
|
+
}
|
|
117
|
+
function list() {
|
|
118
|
+
try { ensureDirectory(false); } catch (error) { if (error.code === "ENOENT") return []; throw error; }
|
|
119
|
+
var items = []; walk("", items); reconcileIdentity(items);
|
|
120
|
+
return items.map(function (item) { return { id: item.id, name: item.name, path: item.name, folder: path.posix.dirname(item.name) === "." ? "" : path.posix.dirname(item.name),
|
|
121
|
+
itemType: item.itemType, size: item.size, mtime: item.mtime, revision: item.revision, writable: item.itemType === "text" }; })
|
|
122
|
+
.sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
123
|
+
}
|
|
124
|
+
function read(name) {
|
|
125
|
+
ensureDirectory(false); var item = existing(name); if (!item) throw Object.assign(new Error("Knowledge file not found."), { code: "ENOENT" });
|
|
126
|
+
var data = registry(); var id = Object.keys(data.documents).filter(function (key) { return !data.documents[key].deleted && data.documents[key].name === item.name; })[0];
|
|
127
|
+
if (!id) { list(); data = registry(); id = Object.keys(data.documents).filter(function (key) { return !data.documents[key].deleted && data.documents[key].name === item.name; })[0]; }
|
|
128
|
+
return Object.assign(item, { id: id || null });
|
|
129
|
+
}
|
|
130
|
+
function assertExpected(current, expected) {
|
|
131
|
+
if (!current && expected) throw Object.assign(new Error("This document was removed since it was opened."), { code: "STALE", currentRevision: null });
|
|
132
|
+
if (current && (!expected || expected !== current.revision)) throw Object.assign(new Error("This document changed since it was opened."), { code: "STALE", currentRevision: current.revision });
|
|
133
|
+
}
|
|
134
|
+
function snapshot(item, reason) {
|
|
135
|
+
if (!item) return;
|
|
136
|
+
var listed = list(); var identity = listed.filter(function (entry) { return entry.name === item.name; })[0]; if (!identity) return; ensureMetadata(true);
|
|
137
|
+
var folder = path.join(historyRoot, identity.id); safeDirectory(folder, true);
|
|
138
|
+
var target = path.join(folder, Date.now() + "-" + item.revision + ".md");
|
|
139
|
+
fs.writeFileSync(target, item.content, { flag: "wx", mode: 0o600 });
|
|
140
|
+
}
|
|
141
|
+
function save(name, content, expectedRevision) {
|
|
142
|
+
var clean = cleanName(name); if (!clean || itemType(clean) !== "text") throw new Error("Only valid Markdown documents can be edited.");
|
|
143
|
+
if (typeof content !== "string") throw new Error("Knowledge content is required.");
|
|
144
|
+
if (Buffer.byteLength(content, "utf8") > MAX_CONTENT_BYTES) throw new Error("Knowledge document is too large.");
|
|
145
|
+
ensureDirectory(true); var current = existing(clean); assertExpected(current, expectedRevision); if (current) snapshot(current, "before-save");
|
|
146
|
+
var target = targetPath(clean, true); var temporary = path.join(path.dirname(target), ".clay-knowledge-" + process.pid + "-" + crypto.randomBytes(8).toString("hex"));
|
|
147
|
+
try { fs.writeFileSync(temporary, content, { flag: "wx", mode: 0o600 }); fs.renameSync(temporary, target); }
|
|
148
|
+
finally { try { fs.unlinkSync(temporary); } catch (error) {} }
|
|
149
|
+
delete fileCache[clean];
|
|
150
|
+
return read(clean);
|
|
151
|
+
}
|
|
152
|
+
function rename(from, to, expectedRevision) {
|
|
153
|
+
var cleanFrom = cleanName(from); var cleanTo = cleanName(to);
|
|
154
|
+
if (!cleanTo || itemType(cleanTo) !== "text") throw new Error("Only valid Markdown documents can be renamed or moved.");
|
|
155
|
+
if (!cleanFrom || itemType(cleanFrom) !== "text") throw Object.assign(new Error("This Knowledge source is read only."), { code: "READ_ONLY" });
|
|
156
|
+
ensureDirectory(false); var current = existing(cleanFrom); if (!current) throw Object.assign(new Error("Knowledge file not found."), { code: "ENOENT" });
|
|
157
|
+
assertExpected(current, expectedRevision); if (existing(cleanTo)) throw Object.assign(new Error("A document with that path already exists."), { code: "EXISTS" }); snapshot(current, "before-move");
|
|
158
|
+
fs.renameSync(targetPath(cleanFrom, false), targetPath(cleanTo, true));
|
|
159
|
+
delete fileCache[cleanFrom];
|
|
160
|
+
var data = registry(); Object.keys(data.documents).forEach(function (id) { if (data.documents[id].name === cleanFrom && !data.documents[id].deleted) data.documents[id].name = cleanTo; }); writeRegistry(data);
|
|
161
|
+
return read(cleanTo);
|
|
162
|
+
}
|
|
163
|
+
function remove(name, expectedRevision) {
|
|
164
|
+
var clean = cleanName(name); if (!clean || itemType(clean) !== "text") throw Object.assign(new Error("This Knowledge source is read only."), { code: "READ_ONLY" });
|
|
165
|
+
ensureDirectory(false); var current = existing(clean); if (!current) throw Object.assign(new Error("Knowledge file not found."), { code: "ENOENT" });
|
|
166
|
+
assertExpected(current, expectedRevision); snapshot(current, "before-delete"); fs.unlinkSync(targetPath(clean, false)); delete fileCache[clean]; list(); return current;
|
|
167
|
+
}
|
|
168
|
+
function history(id) {
|
|
169
|
+
if (!/^[a-f0-9-]{20,50}$/i.test(String(id || ""))) throw new Error("Invalid document identity."); ensureMetadata(false); var folder = path.join(historyRoot, id); var names;
|
|
170
|
+
try { safeDirectory(folder, false); } catch (error) { if (error.code === "ENOENT") return []; throw error; }
|
|
171
|
+
try { names = fs.readdirSync(folder); } catch (error) { if (error.code === "ENOENT") return []; throw error; }
|
|
172
|
+
return names.filter(function (name) { return /^\d+-[a-f0-9]{64}\.md$/.test(name); }).sort().reverse().slice(0, 50).map(function (name) {
|
|
173
|
+
var target = path.join(folder, name); var stat = fs.lstatSync(target);
|
|
174
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_CONTENT_BYTES) throw Object.assign(new Error("Knowledge history contains an unsafe version."), { code: "UNSAFE_PATH" });
|
|
175
|
+
return { version: name, size: stat.size, createdAt: Number(name.split("-")[0]) };
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function readHistory(id, versionName) {
|
|
179
|
+
var versions = history(id); if (!versions.some(function (entry) { return entry.version === versionName; })) throw Object.assign(new Error("Knowledge history version not found."), { code: "ENOENT" });
|
|
180
|
+
return { version: versionName, content: fs.readFileSync(path.join(historyRoot, id, versionName), "utf8") };
|
|
181
|
+
}
|
|
182
|
+
function restore(id, versionName, name, expectedRevision) {
|
|
183
|
+
if (!/^[a-f0-9-]{20,50}$/i.test(String(id || "")) || !/^\d+-[a-f0-9]{64}\.md$/.test(String(versionName || ""))) throw new Error("Invalid history version.");
|
|
184
|
+
var selected = readHistory(id, versionName);
|
|
185
|
+
var data = registry(); var record = data.documents[id]; if (!record) throw Object.assign(new Error("Knowledge document identity not found."), { code: "ENOENT" });
|
|
186
|
+
var current = existing(name); var restored = save(name, selected.content, current ? expectedRevision || null : null);
|
|
187
|
+
data = registry(); Object.keys(data.documents).forEach(function (key) { if (key !== id && data.documents[key].name === restored.name && !data.documents[key].deleted) delete data.documents[key]; });
|
|
188
|
+
data.documents[id] = Object.assign({}, record, { id: id, name: restored.name, lastRevision: restored.revision, deleted: false }); writeRegistry(data);
|
|
189
|
+
return Object.assign(restored, { id: id });
|
|
190
|
+
}
|
|
191
|
+
function deleted() {
|
|
192
|
+
var data = registry(); return Object.keys(data.documents).filter(function (id) { return data.documents[id].deleted; }).map(function (id) {
|
|
193
|
+
return { id: id, name: data.documents[id].name, revision: data.documents[id].lastRevision || null, deleted: true, historyCount: history(id).length };
|
|
194
|
+
}).filter(function (item) { return item.historyCount > 0; }).sort(function (a, b) { return a.name.localeCompare(b.name); });
|
|
195
|
+
}
|
|
196
|
+
return { list: list, read: read, save: save, rename: rename, remove: remove, history: history, readHistory: readHistory, restore: restore, deleted: deleted };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = { createKnowledgeFileStore: createKnowledgeFileStore, revision: revision, validName: validName, cleanName: cleanName, itemType: itemType, MAX_CONTENT_BYTES: MAX_CONTENT_BYTES };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
var path = require("path");
|
|
2
|
+
|
|
3
|
+
function slug(value) {
|
|
4
|
+
return String(value || "").trim().toLowerCase().replace(/<[^>]*>/g, "").replace(/[^\p{L}\p{N}\s-]/gu, "").replace(/\s+/g, "-").replace(/-+/g, "-");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function frontmatter(content) {
|
|
8
|
+
var text = String(content || "");
|
|
9
|
+
if (text.indexOf("---\n") !== 0) return { properties: {}, tags: [], end: 0, errors: [] };
|
|
10
|
+
var close = text.indexOf("\n---\n", 4);
|
|
11
|
+
if (close === -1 || close > 8192) return { properties: {}, tags: [], end: 0, errors: ["Frontmatter is missing its closing --- line."] };
|
|
12
|
+
var properties = {}; var errors = []; var lines = text.slice(4, close).split("\n");
|
|
13
|
+
for (var i = 0; i < lines.length; i++) {
|
|
14
|
+
if (!lines[i].trim() || /^\s*#/.test(lines[i])) continue;
|
|
15
|
+
var match = /^([A-Za-z][A-Za-z0-9_-]{0,63}):\s*(.*)$/.exec(lines[i]);
|
|
16
|
+
if (!match) { errors.push("Invalid property on line " + (i + 2) + "."); continue; }
|
|
17
|
+
var value = match[2].trim();
|
|
18
|
+
if (value.length > 500) errors.push("Property " + match[1] + " is too long.");
|
|
19
|
+
else properties[match[1]] = value;
|
|
20
|
+
}
|
|
21
|
+
var tags = [];
|
|
22
|
+
if (properties.tags) {
|
|
23
|
+
var raw = properties.tags.replace(/^\[/, "").replace(/\]$/, "").split(",");
|
|
24
|
+
for (var t = 0; t < raw.length; t++) {
|
|
25
|
+
var tag = raw[t].trim().replace(/^#/, "");
|
|
26
|
+
if (/^[\p{L}\p{N}_/-]{1,64}$/u.test(tag) && tags.indexOf(tag) === -1) tags.push(tag);
|
|
27
|
+
else if (tag) errors.push("Invalid tag: " + tag);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { properties: properties, tags: tags, end: close + 5, errors: errors };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ignoredRanges(text) {
|
|
34
|
+
var ranges = []; var lines = text.split("\n"); var offset = 0; var fence = null;
|
|
35
|
+
for (var i = 0; i < lines.length; i++) {
|
|
36
|
+
var line = lines[i]; var marker = /^( {0,3})(`{3,}|~{3,})(.*)$/.exec(line);
|
|
37
|
+
if (marker && (!fence || marker[2][0] === fence.marker && marker[2].length >= fence.length && /^\s*$/.test(marker[3]))) {
|
|
38
|
+
if (!fence) fence = { marker: marker[2][0], length: marker[2].length, start: offset };
|
|
39
|
+
else { ranges.push([fence.start, offset + line.length]); fence = null; }
|
|
40
|
+
}
|
|
41
|
+
if (!fence) {
|
|
42
|
+
var cursor = 0;
|
|
43
|
+
while (cursor < line.length) {
|
|
44
|
+
var opener = /`+/.exec(line.slice(cursor)); if (!opener) break;
|
|
45
|
+
var start = cursor + opener.index; var run = opener[0]; var close = line.indexOf(run, start + run.length);
|
|
46
|
+
if (close === -1) { cursor = start + run.length; continue; }
|
|
47
|
+
ranges.push([offset + start, offset + close + run.length]); cursor = close + run.length;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
offset += line.length + 1;
|
|
51
|
+
}
|
|
52
|
+
if (fence) ranges.push([fence.start, text.length]);
|
|
53
|
+
return ranges;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function escaped(text, start) {
|
|
57
|
+
var slashes = 0;
|
|
58
|
+
for (var i = start - 1; i >= 0 && text[i] === "\\"; i--) slashes++;
|
|
59
|
+
return slashes % 2 === 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function inside(ranges, start) {
|
|
63
|
+
for (var i = 0; i < ranges.length; i++) if (start >= ranges[i][0] && start < ranges[i][1]) return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parse(content) {
|
|
68
|
+
var text = String(content || ""); var ignored = ignoredRanges(text); var links = []; var headings = []; var match;
|
|
69
|
+
var headingPattern = /^(#{1,6})\s+(.+?)\s*#*\s*$/gm;
|
|
70
|
+
while ((match = headingPattern.exec(text))) {
|
|
71
|
+
if (!inside(ignored, match.index)) headings.push({ depth: match[1].length, text: match[2].trim(), slug: slug(match[2]), offset: match.index });
|
|
72
|
+
}
|
|
73
|
+
var wiki = /(!)?\[\[([^\]\n]+)\]\]/g;
|
|
74
|
+
while ((match = wiki.exec(text))) {
|
|
75
|
+
if (inside(ignored, match.index) || escaped(text, match.index)) continue;
|
|
76
|
+
var parts = match[2].split("|"); var target = parts[0].trim(); var alias = parts.length > 1 ? parts.slice(1).join("|").trim() : "";
|
|
77
|
+
var hash = target.indexOf("#");
|
|
78
|
+
var wikiTarget = hash === -1 ? target : target.slice(0, hash);
|
|
79
|
+
links.push({ kind: match[1] ? "embed" : "wiki", target: wikiTarget, heading: hash === -1 ? "" : target.slice(hash + 1), alias: alias,
|
|
80
|
+
start: match.index, end: match.index + match[0].length, raw: match[0], external: /^[a-z][a-z0-9+.-]*:/i.test(wikiTarget) || wikiTarget.indexOf("//") === 0 });
|
|
81
|
+
}
|
|
82
|
+
var markdown = /(!)?\[([^\]\n]*)\]\(([^)\n]+)\)/g;
|
|
83
|
+
while ((match = markdown.exec(text))) {
|
|
84
|
+
if (inside(ignored, match.index) || escaped(text, match.index)) continue;
|
|
85
|
+
var destination = match[3].trim().replace(/^<|>$/g, ""); var hashAt = destination.indexOf("#");
|
|
86
|
+
links.push({ kind: match[1] ? "image" : "markdown", target: hashAt === -1 ? destination : destination.slice(0, hashAt), heading: hashAt === -1 ? "" : destination.slice(hashAt + 1), alias: match[2],
|
|
87
|
+
start: match.index, end: match.index + match[0].length, raw: match[0], external: /^[a-z][a-z0-9+.-]*:/i.test(destination) || destination.indexOf("//") === 0 });
|
|
88
|
+
}
|
|
89
|
+
links.sort(function (a, b) { return a.start - b.start; });
|
|
90
|
+
var meta = frontmatter(text); var tags = meta.tags.slice(); var tagPattern = /(^|\s)#([\p{L}\p{N}_/-]{1,64})/gu;
|
|
91
|
+
while ((match = tagPattern.exec(text))) if (!inside(ignored, match.index) && tags.indexOf(match[2]) === -1) tags.push(match[2]);
|
|
92
|
+
return { links: links, headings: headings, properties: meta.properties, tags: tags, propertyErrors: meta.errors };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeTarget(sourceName, target) {
|
|
96
|
+
var raw = String(target || "").trim();
|
|
97
|
+
if (!raw || /^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.indexOf("//") === 0) return null;
|
|
98
|
+
try { raw = decodeURIComponent(raw); } catch (error) {}
|
|
99
|
+
raw = raw.replace(/\\/g, "/");
|
|
100
|
+
if (!/\.md$/i.test(raw)) raw += ".md";
|
|
101
|
+
return path.posix.normalize(path.posix.join(path.posix.dirname(sourceName), raw)).replace(/^\.\//, "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveLink(sourceName, link, documents, sourceId) {
|
|
105
|
+
if (link.external) return null;
|
|
106
|
+
if (!link.target && link.heading) {
|
|
107
|
+
var self = documents.filter(function (item) { return sourceId ? item.id === sourceId : item.name === sourceName; });
|
|
108
|
+
return self.length === 1 ? self[0] : null;
|
|
109
|
+
}
|
|
110
|
+
var target = normalizeTarget(sourceName, link.target);
|
|
111
|
+
if (!target) return null;
|
|
112
|
+
var exact = documents.filter(function (item) { return item.name.toLowerCase() === target.toLowerCase(); });
|
|
113
|
+
if (exact.length === 1) return exact[0];
|
|
114
|
+
if (link.target.indexOf("/") === -1 && link.target.indexOf("\\") === -1) {
|
|
115
|
+
var base = path.posix.basename(target).toLowerCase();
|
|
116
|
+
var named = documents.filter(function (item) { return path.posix.basename(item.name).toLowerCase() === base; });
|
|
117
|
+
if (named.length === 1) return named[0];
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function snippet(content, offset, needle) {
|
|
123
|
+
var raw = String(content || ""); var at = typeof offset === "number" ? offset : raw.toLowerCase().indexOf(String(needle || "").toLowerCase());
|
|
124
|
+
if (at < 0) at = 0; var start = Math.max(0, at - 70); var end = Math.min(raw.length, at + 150); var text = raw.slice(start, end).replace(/\s+/g, " ").trim();
|
|
125
|
+
return (start ? "…" : "") + text + (end < raw.length ? "…" : "");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { parse: parse, slug: slug, normalizeTarget: normalizeTarget, resolveLink: resolveLink, snippet: snippet };
|
|
@@ -46,14 +46,37 @@ function handler(bound, method) {
|
|
|
46
46
|
if (!bound || typeof bound[method] !== "function") {
|
|
47
47
|
return errorResult(new Error("Knowledge tools require an exact session-bound Mate."));
|
|
48
48
|
}
|
|
49
|
-
try {
|
|
50
|
-
return textResult(bound[method](args || {}));
|
|
51
|
-
} catch (e) {
|
|
52
|
-
return errorResult(e);
|
|
53
|
-
}
|
|
49
|
+
try { return Promise.resolve(bound[method](args || {})).then(textResult, errorResult); } catch (e) { return errorResult(e); }
|
|
54
50
|
};
|
|
55
51
|
}
|
|
56
52
|
|
|
53
|
+
var ID = { type: "string", description: "Stable opaque id returned by a Knowledge database tool." };
|
|
54
|
+
var REVISION = { type: "number", description: "Revision returned by the most recent read or mutation; stale revisions are rejected." };
|
|
55
|
+
var OPERATION = { type: "string", description: "Stable retry id formatted label_<current epoch milliseconds>_<unique suffix>. Reuse the exact id and payload after an uncertain response; receipts are retained for the documented 24-hour retry window." };
|
|
56
|
+
var FIELD = { type: "object", description: "Field with stable id, name, type, required, and type-specific options or reference target." };
|
|
57
|
+
var FILTER = { type: "object", description: "Validated filter: fieldId, op, and value. Supported ops are eq, neq, contains, gt, gte, lt, lte, empty, not-empty." };
|
|
58
|
+
var SORT = { type: "object", description: "Validated sort with fieldId and asc or desc direction." };
|
|
59
|
+
|
|
60
|
+
function databaseTools(bound) {
|
|
61
|
+
return [
|
|
62
|
+
{ name: "list_databases", description: "List canonical Knowledge databases, including archived databases when requested, so the Mate can discover and recover them without relying on summary projection.", inputSchema: buildShape({ archived: { type: "boolean" } }, []), handler: handler(bound, "listDatabases") },
|
|
63
|
+
{ name: "create_database", description: "Create a first-class database in this Mate's Knowledge. Defaults to one required title field and a Table view.", inputSchema: buildShape({ name: { type: "string" }, schema: { type: "array", items: FIELD }, operationId: OPERATION }, ["name", "operationId"]), handler: handler(bound, "createDatabase") },
|
|
64
|
+
{ name: "read_database", description: "Read one Knowledge database's current schema, views, revision, and record count.", inputSchema: buildShape({ databaseId: ID, includeArchived: { type: "boolean" } }, ["databaseId"]), handler: handler(bound, "readDatabase") },
|
|
65
|
+
{ name: "rename_database", description: "Rename a Knowledge database with optimistic revision checking.", inputSchema: buildShape({ databaseId: ID, name: { type: "string" }, expectedRevision: REVISION, operationId: OPERATION }, ["databaseId", "name", "expectedRevision", "operationId"]), handler: handler(bound, "renameDatabase") },
|
|
66
|
+
{ name: "archive_database", description: "Recoverably archive a Knowledge database.", inputSchema: buildShape({ databaseId: ID, expectedRevision: REVISION, operationId: OPERATION }, ["databaseId", "expectedRevision", "operationId"]), handler: handler(bound, "archiveDatabase") },
|
|
67
|
+
{ name: "recover_database", description: "Recover an archived Knowledge database.", inputSchema: buildShape({ databaseId: ID, expectedRevision: REVISION, operationId: OPERATION }, ["databaseId", "expectedRevision", "operationId"]), handler: handler(bound, "recoverDatabase") },
|
|
68
|
+
{ name: "update_database_schema", description: "Replace the schema using stable field ids. Exactly one active title field is required; populated fields cannot be removed or change type and should be archived instead.", inputSchema: buildShape({ databaseId: ID, schema: { type: "array", items: FIELD }, expectedRevision: REVISION, operationId: OPERATION }, ["databaseId", "schema", "expectedRevision", "operationId"]), handler: handler(bound, "updateDatabaseSchema") },
|
|
69
|
+
{ name: "save_table_view", description: "Create or revise a persisted Table view with selected columns, validated filters, and deterministic sorts.", inputSchema: buildShape({ databaseId: ID, view: { type: "object", description: "View with stable id, name, columns, filters, and sorts." }, expectedRevision: REVISION, operationId: OPERATION }, ["databaseId", "view", "expectedRevision", "operationId"]), handler: handler(bound, "saveTableView") },
|
|
70
|
+
{ name: "query_database", description: "Query a bounded deterministic page of records using a saved view or explicit validated filters and sorts. Returns count and numeric sums.", inputSchema: buildShape({ databaseId: ID, viewId: ID, filters: { type: "array", items: FILTER }, sorts: { type: "array", items: SORT }, cursor: { type: "string" }, limit: { type: "number" }, archived: { type: "boolean" } }, ["databaseId"]), handler: handler(bound, "queryDatabase") },
|
|
71
|
+
{ name: "create_database_record", description: "Create a typed record. Values are keyed by stable field id; unknown fields and unsafe keys are rejected.", inputSchema: buildShape({ databaseId: ID, values: { type: "object" }, expectedDatabaseRevision: REVISION, operationId: OPERATION }, ["databaseId", "values", "expectedDatabaseRevision", "operationId"]), handler: handler(bound, "createDatabaseRecord") },
|
|
72
|
+
{ name: "read_database_record", description: "Read one record with its values, revision, attribution, archive state, and bounded history.", inputSchema: buildShape({ databaseId: ID, recordId: ID }, ["databaseId", "recordId"]), handler: handler(bound, "readDatabaseRecord") },
|
|
73
|
+
{ name: "update_database_record", description: "Update selected typed values using the record revision. Edits to other fields made since a prior save are not overwritten.", inputSchema: buildShape({ databaseId: ID, recordId: ID, values: { type: "object" }, expectedRevision: REVISION, operationId: OPERATION }, ["databaseId", "recordId", "values", "expectedRevision", "operationId"]), handler: handler(bound, "updateDatabaseRecord") },
|
|
74
|
+
{ name: "archive_database_record", description: "Recoverably archive one record using its current revision.", inputSchema: buildShape({ databaseId: ID, recordId: ID, expectedRevision: REVISION, operationId: OPERATION }, ["databaseId", "recordId", "expectedRevision", "operationId"]), handler: handler(bound, "archiveDatabaseRecord") },
|
|
75
|
+
{ name: "recover_database_record", description: "Recover one archived record using its current revision.", inputSchema: buildShape({ databaseId: ID, recordId: ID, expectedRevision: REVISION, operationId: OPERATION }, ["databaseId", "recordId", "expectedRevision", "operationId"]), handler: handler(bound, "recoverDatabaseRecord") },
|
|
76
|
+
{ name: "retry_database_sync", description: "Retry the shared append-only Knowledge summary projection after a canonical database mutation reported projectionPending.", inputSchema: buildShape({ databaseId: ID }, ["databaseId"]), handler: handler(bound, "retryDatabaseSync") },
|
|
77
|
+
].map(function (tool) { tool.description = MATE_CONTRACT + " " + tool.description; return tool; });
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
function mateTools(bound) {
|
|
58
81
|
return [
|
|
59
82
|
{
|
|
@@ -87,7 +110,7 @@ function mateTools(bound) {
|
|
|
87
110
|
}, ["ref"]),
|
|
88
111
|
handler: handler(bound, "readKnowledge"),
|
|
89
112
|
},
|
|
90
|
-
];
|
|
113
|
+
].concat(databaseTools(bound));
|
|
91
114
|
}
|
|
92
115
|
|
|
93
116
|
function clayTools(bound) {
|
|
@@ -393,7 +393,9 @@ function attachMateKnowledgeService(ctx) {
|
|
|
393
393
|
|
|
394
394
|
var def = mate.builtinKey ? builtinMates.getBuiltinByKey(mate.builtinKey) : null;
|
|
395
395
|
var isClay = !!(mate.builtinKey === "clay" && def && def.hostAgent === true);
|
|
396
|
-
return { own: own, isClay: isClay, ownerId: source.projectOwnerId || null
|
|
396
|
+
return { own: own, isClay: isClay, ownerId: source.projectOwnerId || null,
|
|
397
|
+
databaseService: typeof project.getKnowledgeDbService === "function" ? project.getKnowledgeDbService() : null,
|
|
398
|
+
actor: { type: "user", userId: source.session && source.session.ownerId || source.projectOwnerId || null, displayName: null } };
|
|
397
399
|
}
|
|
398
400
|
|
|
399
401
|
// A tool handler captured earlier stays valid only while its exact source
|
|
@@ -408,7 +410,7 @@ function attachMateKnowledgeService(ctx) {
|
|
|
408
410
|
function revalidate() {
|
|
409
411
|
var current = resolve(source);
|
|
410
412
|
if (!current) throw new Error("This Knowledge binding is no longer valid for the current session.");
|
|
411
|
-
if (current.isClay !== initial.isClay || current.own.scopeId !== initial.own.scopeId) {
|
|
413
|
+
if (current.isClay !== initial.isClay || current.own.scopeId !== initial.own.scopeId || current.databaseService !== initial.databaseService) {
|
|
412
414
|
throw new Error("This Knowledge binding is no longer valid for the current session.");
|
|
413
415
|
}
|
|
414
416
|
return current;
|
|
@@ -423,6 +425,10 @@ function attachMateKnowledgeService(ctx) {
|
|
|
423
425
|
function ownScope() {
|
|
424
426
|
return [revalidate().own];
|
|
425
427
|
}
|
|
428
|
+
function db(method, args) {
|
|
429
|
+
var current = revalidate(); if (!current.databaseService || typeof current.databaseService[method] !== "function") throw new Error("Knowledge databases are unavailable for this Mate.");
|
|
430
|
+
return current.databaseService[method](Object.assign({}, args || {}, { actor: current.actor }));
|
|
431
|
+
}
|
|
426
432
|
return {
|
|
427
433
|
isClay: false,
|
|
428
434
|
mateId: initial.own.mateId,
|
|
@@ -430,6 +436,21 @@ function attachMateKnowledgeService(ctx) {
|
|
|
430
436
|
listKnowledge: function (args) { return listFor(ownScope(), args || {}, false); },
|
|
431
437
|
searchKnowledge: function (args) { return searchFor(ownScope(), args || {}, false); },
|
|
432
438
|
readKnowledge: function (args) { return readFor(ownScope(), args || {}, false); },
|
|
439
|
+
listDatabases: function (args) { return db("list", args); },
|
|
440
|
+
createDatabase: function (args) { return db("create", args); },
|
|
441
|
+
readDatabase: function (args) { return db("read", args); },
|
|
442
|
+
renameDatabase: function (args) { return db("rename", args); },
|
|
443
|
+
archiveDatabase: function (args) { return db("archive", args); },
|
|
444
|
+
recoverDatabase: function (args) { return db("recover", args); },
|
|
445
|
+
updateDatabaseSchema: function (args) { return db("updateSchema", args); },
|
|
446
|
+
saveTableView: function (args) { return db("saveView", args); },
|
|
447
|
+
queryDatabase: function (args) { return db("query", args); },
|
|
448
|
+
createDatabaseRecord: function (args) { return db("createRecord", args); },
|
|
449
|
+
readDatabaseRecord: function (args) { return db("readRecord", args); },
|
|
450
|
+
updateDatabaseRecord: function (args) { return db("updateRecord", args); },
|
|
451
|
+
archiveDatabaseRecord: function (args) { return db("archiveRecord", args); },
|
|
452
|
+
recoverDatabaseRecord: function (args) { return db("recoverRecord", args); },
|
|
453
|
+
retryDatabaseSync: function (args) { return db("retryProjection", args); },
|
|
433
454
|
};
|
|
434
455
|
}
|
|
435
456
|
|
|
@@ -23,7 +23,7 @@ var config = require("./config");
|
|
|
23
23
|
var knowledgeImport = require("./knowledge-import");
|
|
24
24
|
|
|
25
25
|
var SOURCE_NAMESPACE = "mate-knowledge";
|
|
26
|
-
var SOURCE_VERSION =
|
|
26
|
+
var SOURCE_VERSION = 3;
|
|
27
27
|
var MAX_SOURCE_BYTES = 64 * 1024 * 1024;
|
|
28
28
|
|
|
29
29
|
// Identity and template artifacts. Never mirrored, at any version.
|
|
@@ -84,6 +84,13 @@ function sourceKindFor(name) {
|
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function safeSourceName(value) {
|
|
88
|
+
var name = String(value || "").replace(/\\/g, "/").replace(/^\.\//, ""); var parts = name.split("/");
|
|
89
|
+
if (!name || name[0] === "/" || name.length > 500 || parts.length > 20) return null;
|
|
90
|
+
if (parts.some(function (part) { return !part || part === "." || part === ".." || part.length > 120; })) return null;
|
|
91
|
+
return parts.join("/");
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
function isJournal(name) {
|
|
88
95
|
return !!JOURNAL_KINDS[name] || /\.jsonl$/.test(name);
|
|
89
96
|
}
|
|
@@ -133,9 +140,8 @@ function isMateSourceEntry(entry) {
|
|
|
133
140
|
if (typeof source.sourceVersion !== "number" || source.sourceVersion > SOURCE_VERSION) return false;
|
|
134
141
|
var name = source.fileName;
|
|
135
142
|
if (!name || typeof name !== "string") return false;
|
|
136
|
-
// The
|
|
137
|
-
|
|
138
|
-
if (path.basename(name) !== name) return false;
|
|
143
|
+
// The source name and relative path must agree, so neither field alone is trusted.
|
|
144
|
+
if (safeSourceName(name) !== name) return false;
|
|
139
145
|
return source.relPath === relativeSource(name);
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -180,6 +186,18 @@ function readStat(filePath) {
|
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
function safeKnowledgeFileStat(root, name) {
|
|
190
|
+
var parts = name.split("/"); var cursor = root;
|
|
191
|
+
for (var i = 0; i < parts.length; i++) {
|
|
192
|
+
cursor = path.join(cursor, parts[i]); var stat;
|
|
193
|
+
try { stat = fs.lstatSync(cursor); } catch (error) { if (error.code === "ENOENT") return null; throw error; }
|
|
194
|
+
if (stat.isSymbolicLink()) throw new Error("Knowledge source contains a symbolic link.");
|
|
195
|
+
if (i < parts.length - 1 && !stat.isDirectory()) throw new Error("Knowledge source path is not a directory.");
|
|
196
|
+
if (i === parts.length - 1 && !stat.isFile()) return null;
|
|
197
|
+
}
|
|
198
|
+
return fs.statSync(cursor);
|
|
199
|
+
}
|
|
200
|
+
|
|
183
201
|
// --- File-addressed sources -------------------------------------------
|
|
184
202
|
|
|
185
203
|
function syncFile(importer, scope, name, kind, filePath, stat, actor, summary) {
|
|
@@ -361,8 +379,8 @@ function syncMateSource(opts) {
|
|
|
361
379
|
var scope = options.scope || scopeForMateDir(options.mateDir);
|
|
362
380
|
if (!scope) return summary;
|
|
363
381
|
if (options.mateName) scope.mateName = options.mateName;
|
|
364
|
-
var
|
|
365
|
-
if (!name) return summary;
|
|
382
|
+
var rawName = String(options.fileName || ""); var name = safeSourceName(rawName);
|
|
383
|
+
if (!name) { if (rawName) summary.unsupported++; return summary; }
|
|
366
384
|
if (IDENTITY_FILES.indexOf(name) !== -1) { summary.identitySkipped++; return summary; }
|
|
367
385
|
if (EXCLUDED_FILES.indexOf(name) !== -1) { summary.excluded++; return summary; }
|
|
368
386
|
var kind = sourceKindFor(name);
|
|
@@ -370,8 +388,8 @@ function syncMateSource(opts) {
|
|
|
370
388
|
|
|
371
389
|
var actor = normalizeActor(options.actor);
|
|
372
390
|
var importer = options.importer || importerFor(scope, options.baseDir);
|
|
373
|
-
var
|
|
374
|
-
var stat =
|
|
391
|
+
var knowledgeRoot = path.join(scope.mateDir, "knowledge"); var filePath = path.join(knowledgeRoot, name);
|
|
392
|
+
var stat = safeKnowledgeFileStat(knowledgeRoot, name);
|
|
375
393
|
|
|
376
394
|
if (isJournal(name)) syncJournal(importer, scope, name, kind, filePath, stat, actor, summary);
|
|
377
395
|
else syncFile(importer, scope, name, kind, filePath, stat, actor, summary);
|
|
@@ -382,19 +400,11 @@ function syncMateSource(opts) {
|
|
|
382
400
|
return summary;
|
|
383
401
|
}
|
|
384
402
|
|
|
385
|
-
// Alias that reads better at a call site reacting to a removal or a rewrite.
|
|
386
403
|
function reconcileSource(opts) {
|
|
387
404
|
return syncMateSource(opts);
|
|
388
405
|
}
|
|
389
406
|
|
|
390
407
|
// Reconcile every durable source in one Mate directory, in both directions.
|
|
391
|
-
//
|
|
392
|
-
// Enumerating the directory alone is not enough. A knowledge file or a whole
|
|
393
|
-
// journal can be deleted while the daemon is stopped, or by direct filesystem
|
|
394
|
-
// tooling, and nothing would ever call syncMateSource for it again. So the
|
|
395
|
-
// scope's own live records are also consulted: any source this module
|
|
396
|
-
// previously imported that is no longer on disk is reconciled, which tombstones
|
|
397
|
-
// a missing file record or every line of a missing journal.
|
|
398
408
|
function reconcileMate(opts) {
|
|
399
409
|
var options = opts || {};
|
|
400
410
|
var summary = options.summary || newSummary();
|
|
@@ -404,11 +414,17 @@ function reconcileMate(opts) {
|
|
|
404
414
|
var knowledgeDir = path.join(scope.mateDir, "knowledge");
|
|
405
415
|
var present = {};
|
|
406
416
|
var entries = [];
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
417
|
+
function discover(relative) {
|
|
418
|
+
var folder = relative ? path.join(knowledgeDir, relative) : knowledgeDir; var children;
|
|
419
|
+
try { children = fs.readdirSync(folder, { withFileTypes: true }); } catch (error) { return; }
|
|
420
|
+
for (var d = 0; d < children.length; d++) {
|
|
421
|
+
var name = relative ? relative + "/" + children[d].name : children[d].name; var stat;
|
|
422
|
+
try { stat = fs.lstatSync(path.join(knowledgeDir, name)); } catch (error) { continue; }
|
|
423
|
+
if (stat.isSymbolicLink()) continue;
|
|
424
|
+
if (stat.isDirectory()) discover(name); else if (stat.isFile()) entries.push({ name: name });
|
|
425
|
+
}
|
|
411
426
|
}
|
|
427
|
+
discover("");
|
|
412
428
|
var importer = options.importer || importerFor(scope, options.baseDir);
|
|
413
429
|
|
|
414
430
|
function reconcileOne(fileName) {
|
|
@@ -423,7 +439,6 @@ function reconcileMate(opts) {
|
|
|
423
439
|
}
|
|
424
440
|
|
|
425
441
|
for (var i = 0; i < entries.length; i++) {
|
|
426
|
-
if (!entries[i].isFile()) continue;
|
|
427
442
|
// Recorded as present whatever its kind, so an excluded or unsupported file
|
|
428
443
|
// that still exists is never mistaken for a deleted source.
|
|
429
444
|
present[entries[i].name] = true;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Only curated pigments may affect a Mate's workspace surface.
|
|
2
|
+
var COLORS = ['mint', 'rose', 'plum', 'coral', 'peach', 'gold', 'sky', 'indigo', 'stone'];
|
|
3
|
+
function valid(value) { return typeof value === 'string' && COLORS.indexOf(value) !== -1; }
|
|
4
|
+
module.exports = { colors: COLORS, valid: valid };
|
package/lib/mates.js
CHANGED
|
@@ -2,6 +2,7 @@ var fs = require("fs");
|
|
|
2
2
|
var path = require("path");
|
|
3
3
|
var crypto = require("crypto");
|
|
4
4
|
var config = require("./config");
|
|
5
|
+
var workspaceColor = require("./mate-workspace-color");
|
|
5
6
|
|
|
6
7
|
var crisisSafety = require("./crisis-safety");
|
|
7
8
|
var matesPrompts = require("./mates-prompts");
|
|
@@ -192,9 +193,17 @@ function getMate(ctx, id) {
|
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
function updateMate(ctx, id, updates) {
|
|
196
|
+
updates = Object.assign({}, updates);
|
|
197
|
+
delete updates.id;
|
|
198
|
+
delete updates.createdBy;
|
|
199
|
+
delete updates.builtinKey;
|
|
195
200
|
var data = loadMates(ctx);
|
|
196
201
|
for (var i = 0; i < data.mates.length; i++) {
|
|
197
202
|
if (data.mates[i].id === id) {
|
|
203
|
+
if (Object.prototype.hasOwnProperty.call(updates, "workspaceColor")) {
|
|
204
|
+
if (data.mates[i].primary || data.mates[i].builtinKey === "clay") delete updates.workspaceColor;
|
|
205
|
+
else if (!workspaceColor.valid(updates.workspaceColor)) return { error: "Choose a supported workspace color." };
|
|
206
|
+
}
|
|
198
207
|
// Primary mates: protect name and core identity fields
|
|
199
208
|
if (data.mates[i].primary) {
|
|
200
209
|
delete updates.name;
|
|
@@ -206,6 +215,15 @@ function updateMate(ctx, id, updates) {
|
|
|
206
215
|
delete updates.profile.displayName;
|
|
207
216
|
}
|
|
208
217
|
}
|
|
218
|
+
// Locked built-in artwork is server-managed. Preserve it even when a
|
|
219
|
+
// stale or crafted client submits a replacement profile object.
|
|
220
|
+
if (data.mates[i].profile && data.mates[i].profile.avatarLocked && updates.profile) {
|
|
221
|
+
updates.profile.avatarStyle = data.mates[i].profile.avatarStyle;
|
|
222
|
+
updates.profile.avatarSeed = data.mates[i].profile.avatarSeed;
|
|
223
|
+
updates.profile.avatarColor = data.mates[i].profile.avatarColor;
|
|
224
|
+
updates.profile.avatarCustom = data.mates[i].profile.avatarCustom;
|
|
225
|
+
updates.profile.avatarLocked = true;
|
|
226
|
+
}
|
|
209
227
|
var keys = Object.keys(updates);
|
|
210
228
|
for (var j = 0; j < keys.length; j++) {
|
|
211
229
|
data.mates[i][keys[j]] = updates[keys[j]];
|