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
package/LICENSES/README.md
CHANGED
|
@@ -6,6 +6,10 @@ Preexisting MIT-covered portions retain the grants and applicable copyright and
|
|
|
6
6
|
|
|
7
7
|
Third-party components and assets retain their own licenses and notices. In particular, the bundled Source Serif 4 font remains under the [SIL Open Font License 1.1](../lib/public/fonts/source-serif-4/LICENSE.md).
|
|
8
8
|
|
|
9
|
+
The locally generated historical avatar styles retain their DiceBear and
|
|
10
|
+
original artist licenses and attributions, summarized in
|
|
11
|
+
[dicebear-avatars.md](dicebear-avatars.md).
|
|
12
|
+
|
|
9
13
|
The Salt layout, control geometry, text decoration, color map and diagram chrome
|
|
10
14
|
in `lib/salt-*.js` and `lib/assets/salt-colors.json` are adapted from PlantUML
|
|
11
15
|
1.2026.8, copyright 2009–2025 Arnaud Roques, under
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# DiceBear avatar notices
|
|
2
|
+
|
|
3
|
+
Clay locally generates its seven historical avatar choices with DiceBear 9.4.3.
|
|
4
|
+
The generator code is MIT-licensed, copyright Florian Körner. Each design keeps
|
|
5
|
+
its upstream license and attribution:
|
|
6
|
+
|
|
7
|
+
- Thumbs — DiceBear, CC0 1.0.
|
|
8
|
+
- Bottts — Pablo Stanley, free for personal and commercial use.
|
|
9
|
+
- Pixel Art — DiceBear, CC0 1.0.
|
|
10
|
+
- Adventurer — Lisa Wischofsky, CC BY 4.0.
|
|
11
|
+
- Avatar Illustration System (Micah) — Micah Lanier, CC BY 4.0.
|
|
12
|
+
- Fun Emoji Set — Davis Uche, CC BY 4.0.
|
|
13
|
+
- Bootstrap Icons — The Bootstrap Authors, MIT.
|
|
14
|
+
|
|
15
|
+
The complete notices and license links are distributed with their respective
|
|
16
|
+
`@dicebear/*` packages. Upstream: https://www.dicebear.com/
|
|
@@ -32,7 +32,11 @@ function getProjectAccess(config, slug, osUsers) {
|
|
|
32
32
|
var project = findProject(config, parentSlug || slug);
|
|
33
33
|
if (!project) return { error: "Project not found" };
|
|
34
34
|
|
|
35
|
-
var isMateProject = project.slug.indexOf("mate-") === 0;
|
|
35
|
+
var isMateProject = project.isMate === true || project.slug.indexOf("mate-") === 0;
|
|
36
|
+
if (isMateProject) {
|
|
37
|
+
return { slug: slug, isMate: true, visibility: "private", ownerId: project.ownerId || null,
|
|
38
|
+
allowedUsers: [], sessionVisibilityDefault: "private", isWorktree: !!parentSlug };
|
|
39
|
+
}
|
|
36
40
|
var visibility = isMateProject ? "private" : (project.visibility || (osUsers ? "private" : "public"));
|
|
37
41
|
var parentAllowedUsers = uniqueUsers(project.allowedUsers);
|
|
38
42
|
if (!parentSlug) {
|
|
@@ -72,6 +76,7 @@ function setAllowedUsers(config, slug, allowedUsers) {
|
|
|
72
76
|
var parentSlug = parentSlugFor(slug);
|
|
73
77
|
var project = findProject(config, parentSlug || slug);
|
|
74
78
|
if (!project) return { error: "Project not found" };
|
|
79
|
+
if (project.slug.indexOf("mate-") === 0 || project.isMate) return { error: "Clay and Mates cannot be shared." };
|
|
75
80
|
var clean = uniqueUsers(allowedUsers);
|
|
76
81
|
if (!parentSlug) {
|
|
77
82
|
project.allowedUsers = clean;
|
|
@@ -92,6 +97,7 @@ function setSessionVisibilityDefault(config, slug, visibility) {
|
|
|
92
97
|
if (parentSlugFor(slug)) return { error: "Worktrees inherit this setting from their parent project." };
|
|
93
98
|
var project = findProject(config, slug);
|
|
94
99
|
if (!project) return { error: "Project not found" };
|
|
100
|
+
if (project.slug.indexOf("mate-") === 0 || project.isMate) return { error: "Clay and Mates are always private." };
|
|
95
101
|
if (visibility === "shared") project.sessionVisibilityDefault = "shared";
|
|
96
102
|
else delete project.sessionVisibilityDefault;
|
|
97
103
|
return { ok: true, visibility: visibility };
|
package/lib/daemon.js
CHANGED
|
@@ -565,6 +565,7 @@ var relay = createServer({
|
|
|
565
565
|
return result;
|
|
566
566
|
},
|
|
567
567
|
onProjectOwnerChanged: function (slug, ownerId) {
|
|
568
|
+
if (slug.indexOf("mate-") === 0) return {error:"Mate ownership cannot be transferred."};
|
|
568
569
|
console.log("[daemon] onProjectOwnerChanged:", slug, "→", ownerId);
|
|
569
570
|
var oldOwnerId = null;
|
|
570
571
|
var projectIdx = -1;
|
|
@@ -954,6 +955,7 @@ var relay = createServer({
|
|
|
954
955
|
spawnAndRestart();
|
|
955
956
|
},
|
|
956
957
|
onSetProjectVisibility: function (slug, visibility) {
|
|
958
|
+
if (slug.indexOf("mate-") === 0) return {error:"Mates cannot be shared."};
|
|
957
959
|
for (var i = 0; i < config.projects.length; i++) {
|
|
958
960
|
if (config.projects[i].slug === slug) {
|
|
959
961
|
var prevVisibility = config.projects[i].visibility || "public";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
var STYLE_PACKAGES = {
|
|
2
|
+
thumbs: "@dicebear/thumbs",
|
|
3
|
+
bottts: "@dicebear/bottts",
|
|
4
|
+
"pixel-art": "@dicebear/pixel-art",
|
|
5
|
+
adventurer: "@dicebear/adventurer",
|
|
6
|
+
micah: "@dicebear/micah",
|
|
7
|
+
"fun-emoji": "@dicebear/fun-emoji",
|
|
8
|
+
icons: "@dicebear/icons",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
var corePromise = null;
|
|
12
|
+
var stylePromises = Object.create(null);
|
|
13
|
+
var avatarCache = new Map();
|
|
14
|
+
|
|
15
|
+
function normalizeRequest(style, seed, size) {
|
|
16
|
+
if (!Object.prototype.hasOwnProperty.call(STYLE_PACKAGES, style)) {
|
|
17
|
+
throw new Error("Unknown avatar style");
|
|
18
|
+
}
|
|
19
|
+
var normalizedSeed = typeof seed === "string" ? seed.substring(0, 80) : "";
|
|
20
|
+
if (!normalizedSeed) normalizedSeed = "anonymous";
|
|
21
|
+
var normalizedSize = Number(size);
|
|
22
|
+
if (!Number.isInteger(normalizedSize) || normalizedSize < 16 || normalizedSize > 256) normalizedSize = 64;
|
|
23
|
+
return { style: style, seed: normalizedSeed, size: normalizedSize };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function loadCore() {
|
|
27
|
+
if (!corePromise) corePromise = import("@dicebear/core");
|
|
28
|
+
return corePromise;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadStyle(style) {
|
|
32
|
+
if (!stylePromises[style]) stylePromises[style] = import(STYLE_PACKAGES[style]);
|
|
33
|
+
return stylePromises[style];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function renderAvatar(style, seed, size) {
|
|
37
|
+
var request = normalizeRequest(style, seed, size);
|
|
38
|
+
var cacheKey = [request.style, request.seed, request.size].join("|");
|
|
39
|
+
if (avatarCache.has(cacheKey)) return Promise.resolve(avatarCache.get(cacheKey));
|
|
40
|
+
return Promise.all([loadCore(), loadStyle(request.style)]).then(function (modules) {
|
|
41
|
+
var createAvatar = modules[0].createAvatar;
|
|
42
|
+
var styleDefinition = modules[1];
|
|
43
|
+
var svg = createAvatar(styleDefinition, {
|
|
44
|
+
seed: [request.seed],
|
|
45
|
+
size: request.size,
|
|
46
|
+
}).toString();
|
|
47
|
+
if (avatarCache.size >= 512) avatarCache.delete(avatarCache.keys().next().value);
|
|
48
|
+
avatarCache.set(cacheKey, svg);
|
|
49
|
+
return svg;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isSupportedStyle(style) {
|
|
54
|
+
return Object.prototype.hasOwnProperty.call(STYLE_PACKAGES, style);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
isSupportedStyle: isSupportedStyle,
|
|
59
|
+
normalizeRequest: normalizeRequest,
|
|
60
|
+
renderAvatar: renderAvatar,
|
|
61
|
+
styles: Object.keys(STYLE_PACKAGES),
|
|
62
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
var fs = require("fs");
|
|
3
|
+
var path = require("path");
|
|
4
|
+
var Datastore = require("@seald-io/nedb");
|
|
5
|
+
var validation = require("./knowledge-db-validation");
|
|
6
|
+
|
|
7
|
+
var MAX_HISTORY = 50;
|
|
8
|
+
var MAX_RECEIPTS = 500;
|
|
9
|
+
var MAX_PAGE = 100;
|
|
10
|
+
var MAX_FILTERS = 20;
|
|
11
|
+
var MAX_SORTS = 10;
|
|
12
|
+
var MAX_AGGREGATE_BYTES = 8 * 1024 * 1024;
|
|
13
|
+
var IDEMPOTENCY_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
14
|
+
var STORE_REGISTRY = {};
|
|
15
|
+
|
|
16
|
+
function clone(value) { return JSON.parse(JSON.stringify(value)); }
|
|
17
|
+
function error(message, code, currentRevision) { return Object.assign(new Error(message), { code: code || "FAILED", currentRevision: currentRevision }); }
|
|
18
|
+
function actor(input) { return { type: input && input.type === "system" ? "system" : "user", userId: input && input.userId || null, displayName: input && input.displayName || null }; }
|
|
19
|
+
function now() { return Date.now(); }
|
|
20
|
+
function newId(prefix) { return prefix + "_" + crypto.randomUUID(); }
|
|
21
|
+
function mutationId(value) {
|
|
22
|
+
if (typeof value !== "string" || !/^[A-Za-z][A-Za-z0-9-]*_\d{13}_[A-Za-z0-9_-]{1,80}$/.test(value)) throw error("Operation id must include its creation time and a unique suffix.", "INVALID_OPERATION_ID");
|
|
23
|
+
var createdAt = Number(value.match(/_(\d{13})_/)[1]); var age = now() - createdAt;
|
|
24
|
+
if (age > IDEMPOTENCY_WINDOW_MS) throw error("Operation id is outside the retry window.", "OPERATION_EXPIRED");
|
|
25
|
+
if (age < -5 * 60 * 1000) throw error("Operation id creation time is in the future.", "INVALID_OPERATION_ID");
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function canonical(value) {
|
|
29
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
30
|
+
if (!value || typeof value !== "object") return value;
|
|
31
|
+
var out = {}; Object.keys(value).sort().forEach(function (key) { out[key] = canonical(value[key]); }); return out;
|
|
32
|
+
}
|
|
33
|
+
function fingerprint(label, args) { var envelope = Object.assign({}, args); delete envelope.actor; delete envelope.operationId; return crypto.createHash("sha256").update(label + ":" + JSON.stringify(canonical(envelope))).digest("hex"); }
|
|
34
|
+
function ensureRevision(value, current, label) {
|
|
35
|
+
if (!Number.isInteger(value) || value < 1) throw error("A valid " + label + " revision is required.", "STALE", current);
|
|
36
|
+
if (value !== current) throw error("This " + label + " changed since it was opened.", "STALE", current);
|
|
37
|
+
}
|
|
38
|
+
function receipt(database, operationId, expectedFingerprint) {
|
|
39
|
+
var found = (database.receipts || []).filter(function (entry) { return entry.operationId === operationId; })[0];
|
|
40
|
+
if (found && found.fingerprint !== expectedFingerprint) throw error("Operation id was already used with a different payload.", "IDEMPOTENCY_MISMATCH");
|
|
41
|
+
return found ? clone(found.result) : null;
|
|
42
|
+
}
|
|
43
|
+
function remember(database, operationId, operationFingerprint, result) {
|
|
44
|
+
var cutoff = now() - IDEMPOTENCY_WINDOW_MS;
|
|
45
|
+
var active = (database.receipts || []).filter(function (entry) { return entry.at >= cutoff; });
|
|
46
|
+
if (active.length >= MAX_RECEIPTS) throw error("Knowledge database retry capacity is full until an operation receipt expires.", "IDEMPOTENCY_CAPACITY");
|
|
47
|
+
database.receipts = active.concat([{ operationId: operationId, fingerprint: operationFingerprint, at: now(), result: clone(result) }]);
|
|
48
|
+
}
|
|
49
|
+
function publicDatabase(database, includeRecords) {
|
|
50
|
+
var out = { id: database._id, itemType: "db", name: database.name, revision: database.revision, archivedAt: database.archivedAt || null,
|
|
51
|
+
createdAt: database.createdAt, updatedAt: database.updatedAt, createdBy: database.createdBy, updatedBy: database.updatedBy,
|
|
52
|
+
schema: clone(database.schema), views: clone((database.views || []).filter(function (view) { return !view.archived; })), recordCount: database.records.filter(function (record) { return !record.archivedAt; }).length,
|
|
53
|
+
projectionPending: database.projectionPending === true, projectionError: database.projectionError || null };
|
|
54
|
+
if (includeRecords) out.records = clone(database.records);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function publicRecord(record, references) { var out = clone(record); if (references && references.length) out.references = references; return out; }
|
|
58
|
+
function titleValue(database, record) {
|
|
59
|
+
var field = database.schema.filter(function (entry) { return entry.type === "title" && !entry.archived; })[0];
|
|
60
|
+
return field && record.values[field.id] || "Untitled";
|
|
61
|
+
}
|
|
62
|
+
function compareValues(a, b) {
|
|
63
|
+
if (a === b) return 0; if (a === null || a === undefined) return 1; if (b === null || b === undefined) return -1;
|
|
64
|
+
if (typeof a === "boolean") return a === false ? -1 : 1;
|
|
65
|
+
if (typeof a === "number") return a < b ? -1 : 1;
|
|
66
|
+
var left = Array.isArray(a) ? a.join("\u0000") : typeof a === "object" ? (a.id || "") : String(a);
|
|
67
|
+
var right = Array.isArray(b) ? b.join("\u0000") : typeof b === "object" ? (b.id || "") : String(b);
|
|
68
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
69
|
+
}
|
|
70
|
+
function matches(record, filter) {
|
|
71
|
+
var value = record.values[filter.fieldId]; var empty = value === null || value === undefined || value === "" || Array.isArray(value) && value.length === 0;
|
|
72
|
+
if (filter.op === "empty") return empty; if (filter.op === "not-empty") return !empty;
|
|
73
|
+
if (filter.op === "eq") return JSON.stringify(value) === JSON.stringify(filter.value);
|
|
74
|
+
if (filter.op === "neq") return JSON.stringify(value) !== JSON.stringify(filter.value);
|
|
75
|
+
if (filter.op === "contains") return Array.isArray(value) ? value.indexOf(filter.value) !== -1 : String(value || "").toLowerCase().indexOf(String(filter.value || "").toLowerCase()) !== -1;
|
|
76
|
+
if (empty || filter.value === null || filter.value === undefined) return false;
|
|
77
|
+
var comparison = compareValues(value, filter.value);
|
|
78
|
+
return filter.op === "gt" ? comparison > 0 : filter.op === "gte" ? comparison >= 0 : filter.op === "lt" ? comparison < 0 : comparison <= 0;
|
|
79
|
+
}
|
|
80
|
+
function cursor(signature, offset) { return Buffer.from(JSON.stringify({ signature: signature, offset: offset }), "utf8").toString("base64url"); }
|
|
81
|
+
function decodeCursor(value, signature) {
|
|
82
|
+
if (!value) return 0;
|
|
83
|
+
try { var parsed = JSON.parse(Buffer.from(String(value), "base64url").toString("utf8")); if (parsed.signature !== signature || !Number.isInteger(parsed.offset) || parsed.offset < 0) throw new Error(); return parsed.offset; }
|
|
84
|
+
catch (cause) { throw error("Query cursor is invalid for this query.", "INVALID_CURSOR"); }
|
|
85
|
+
}
|
|
86
|
+
function referenceStates(database, record, databases, settings) { var states = []; database.schema.forEach(function (field) { var value = record.values[field.id]; if (field.type !== "reference" || !value) return; if (value.type === "text") { var exists = typeof settings.authorizeTextReference === "function" && settings.authorizeTextReference(value.id) === true; states.push({ fieldId: field.id, type: "text", id: value.id, status: exists ? "available" : "missing" }); return; } var targetDatabase = databases.filter(function (item) { return item._id === value.databaseId; })[0]; var targetRecord = targetDatabase && targetDatabase.records.filter(function (item) { return item.id === value.id; })[0]; var status = !targetDatabase || !targetRecord ? "missing" : targetDatabase.archivedAt || targetRecord.archivedAt ? "archived" : "available"; states.push({ fieldId: field.id, type: "record", id: value.id, databaseId: value.databaseId, status: status, label: targetRecord ? titleValue(targetDatabase, targetRecord) : null }); }); return states; }
|
|
87
|
+
function queryRecords(database, args, databases, settings) {
|
|
88
|
+
var options = args || {}; var view = null;
|
|
89
|
+
if (options.viewId) { view = (database.views || []).filter(function (entry) { return entry.id === options.viewId && !entry.archived; })[0]; if (!view) throw error("Table view not found.", "NOT_FOUND"); }
|
|
90
|
+
if (options.filters && (!Array.isArray(options.filters) || options.filters.length > MAX_FILTERS)) throw error("Query has too many filters.", "LIMIT");
|
|
91
|
+
if (options.sorts && (!Array.isArray(options.sorts) || options.sorts.length > MAX_SORTS)) throw error("Query has too many sorts.", "LIMIT");
|
|
92
|
+
var filters = options.filters ? options.filters.map(function (entry) { return validation.normalizeFilter(entry, database.schema); }) : view ? view.filters : [];
|
|
93
|
+
var sorts = options.sorts ? options.sorts.map(function (entry) { return validation.normalizeSort(entry, database.schema); }) : view ? view.sorts : [];
|
|
94
|
+
var signature = crypto.createHash("sha256").update(JSON.stringify({ databaseId: database._id, revision: database.revision, filters: filters, sorts: sorts, archived: options.archived === true })).digest("hex").slice(0, 20);
|
|
95
|
+
var records = database.records.filter(function (record) { return options.archived === true ? !!record.archivedAt : !record.archivedAt; });
|
|
96
|
+
records = records.filter(function (record) { return filters.every(function (filter) { return matches(record, filter); }); });
|
|
97
|
+
records.sort(function (left, right) {
|
|
98
|
+
for (var i = 0; i < sorts.length; i++) { var order = compareValues(left.values[sorts[i].fieldId], right.values[sorts[i].fieldId]); if (order) return sorts[i].direction === "desc" ? -order : order; }
|
|
99
|
+
return left.id === right.id ? 0 : left.id < right.id ? -1 : 1;
|
|
100
|
+
});
|
|
101
|
+
var requestedLimit = options.limit === undefined ? 50 : options.limit;
|
|
102
|
+
if (!Number.isInteger(requestedLimit) || requestedLimit < 1 || requestedLimit > MAX_PAGE) throw error("Query limit must be an integer between 1 and " + MAX_PAGE + ".", "INVALID");
|
|
103
|
+
var count = requestedLimit; var offset = decodeCursor(options.cursor, signature); var page = records.slice(offset, offset + count);
|
|
104
|
+
var totals = { count: records.length, sums: {} };
|
|
105
|
+
database.schema.forEach(function (field) { if (field.type !== "number" || field.archived) return; var sum = 0; var has = false; records.forEach(function (record) { if (typeof record.values[field.id] === "number") { sum += record.values[field.id]; has = true; } }); if (has) totals.sums[field.id] = sum; });
|
|
106
|
+
return { databaseId: database._id, databaseRevision: database.revision, view: view ? clone(view) : null, records: page.map(function (record) { return publicRecord(record, referenceStates(database, record, databases, settings)); }), nextCursor: offset + count < records.length ? cursor(signature, offset + count) : null, totals: totals };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createKnowledgeDbService(options) {
|
|
110
|
+
var settings = options || {}; var projectDir = path.resolve(settings.projectDir); var projectReal = null; var knowledgeDir = path.join(projectDir, "knowledge"); var dataDir = path.join(knowledgeDir, ".clay-db"); var filename = path.join(dataDir, "databases.db");
|
|
111
|
+
var aggregateLimit = Number.isInteger(settings.maxAggregateBytes) && settings.maxAggregateBytes > 1024 ? Math.min(MAX_AGGREGATE_BYTES, settings.maxAggregateBytes) : MAX_AGGREGATE_BYTES;
|
|
112
|
+
var entry = STORE_REGISTRY[filename];
|
|
113
|
+
if (!entry) { entry = { queues: {}, database: null, loading: null, ready: false }; STORE_REGISTRY[filename] = entry; }
|
|
114
|
+
function safeDirectory(target, create) {
|
|
115
|
+
var stat; try { stat = fs.lstatSync(target); } catch (cause) { if (!create || cause.code !== "ENOENT") throw cause; fs.mkdirSync(target, { recursive: true, mode: 0o700 }); stat = fs.lstatSync(target); }
|
|
116
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw error("Knowledge database path is unsafe.", "UNSAFE_PATH");
|
|
117
|
+
var real = fs.realpathSync(target); if (real !== projectReal && !real.startsWith(projectReal + path.sep)) throw error("Knowledge database path escapes the Mate project.", "UNSAFE_PATH");
|
|
118
|
+
}
|
|
119
|
+
function open() {
|
|
120
|
+
if (entry.loading) return entry.loading;
|
|
121
|
+
entry.loading = Promise.resolve().then(function () { projectReal = fs.realpathSync(projectDir); safeDirectory(knowledgeDir, true); safeDirectory(dataDir, true); var stat; try { stat = fs.lstatSync(filename); } catch (cause) { if (cause.code !== "ENOENT") throw cause; }
|
|
122
|
+
if (stat && (!stat.isFile() || stat.isSymbolicLink())) throw error("Knowledge database file is unsafe.", "UNSAFE_PATH"); entry.database = new Datastore({ filename: filename }); return entry.database.loadDatabaseAsync(); }).then(function () { return entry.database.ensureIndexAsync({ fieldName: "_id", unique: true }); }).then(function () { entry.ready = true; return entry.database; }).catch(function (cause) { entry.database = null; entry.loading = null; entry.ready = false; throw cause; });
|
|
123
|
+
return entry.loading;
|
|
124
|
+
}
|
|
125
|
+
function serialize(key, work) { var previous = entry.queues[key] || Promise.resolve(); var next = previous.catch(function () {}).then(work); entry.queues[key] = next.catch(function () {}); return next; }
|
|
126
|
+
function load(id) { return open().then(function (store) { return store.findOneAsync({ _id: validation.id(id, "database id") }); }).then(function (found) { if (!found) throw error("Knowledge database not found.", "NOT_FOUND"); return found; }); }
|
|
127
|
+
function all() { return open().then(function (store) { return store.findAsync({}); }); }
|
|
128
|
+
function listCached(args) { var items = entry.database ? entry.database.getAllData() : []; return { databases: items.filter(function (item) { return args && args.archived === true ? !!item.archivedAt : !item.archivedAt; }).sort(function (a, b) { return b.updatedAt - a.updatedAt || (a._id === b._id ? 0 : a._id < b._id ? -1 : 1); }).map(function (item) { return publicDatabase(item, false); }) }; }
|
|
129
|
+
function assertAggregate(next) { if (Buffer.byteLength(JSON.stringify(next), "utf8") > aggregateLimit) throw error("Knowledge database reached its storage limit.", "LIMIT"); }
|
|
130
|
+
function replace(current, next) { assertAggregate(next); return entry.database.updateAsync({ _id: current._id, revision: current.revision }, next, {}).then(function (result) { if (result.numAffected !== 1) throw error("This database changed during the operation.", "STALE", current.revision); return next; }); }
|
|
131
|
+
function projectionContent(value) { return JSON.stringify({ schemaVersion: 1, namespace: "knowledge-database-summary", databaseId: value._id, name: value.name, archived: !!value.archivedAt, revision: value.revision, fields: value.schema.filter(function (field) { return !field.archived; }).map(function (field) { return { id: field.id, name: field.name, type: field.type }; }), views: (value.views || []).filter(function (view) { return !view.archived; }).map(function (view) { return { id: view.id, name: view.name, type: "table" }; }), recordCount: value.records.filter(function (record) { return !record.archivedAt; }).length }); }
|
|
132
|
+
function project(value, actorValue) {
|
|
133
|
+
if (typeof settings.projectSummary !== "function") return Promise.resolve({ pending: false });
|
|
134
|
+
return Promise.resolve().then(function () { return settings.projectSummary({ databaseId: value._id, name: value.name, revision: value.revision, archived: !!value.archivedAt, content: projectionContent(value), actor: actorValue }); }).then(function () {
|
|
135
|
+
var clean = clone(value); clean.projectionPending = false; clean.projectionError = null; return entry.database.updateAsync({ _id: value._id, revision: value.revision }, clean, {}).then(function (result) { if (result.numAffected !== 1) return { pending: true, error: "A newer database revision still needs summary projection." }; return { pending: false }; });
|
|
136
|
+
}).catch(function (cause) { var message = cause.message || "Knowledge database summary projection failed."; var failed = clone(value); failed.projectionPending = true; failed.projectionError = message; return entry.database.updateAsync({ _id: value._id, revision: value.revision }, failed, {}).then(function () { return { pending: true, error: message }; }, function () { return { pending: true, error: message }; }); });
|
|
137
|
+
}
|
|
138
|
+
function finish(value, result, actorValue) { return project(value, actorValue).then(function (projection) { return Object.assign({}, result, { projectionPending: projection.pending, projectionError: projection.error || null }); }); }
|
|
139
|
+
function mutate(databaseId, args, label, fn) {
|
|
140
|
+
var operationId = mutationId(args.operationId); var operationFingerprint = fingerprint(label, args); return serialize(databaseId, function () { return load(databaseId).then(function (current) {
|
|
141
|
+
var duplicate = receipt(current, operationId, operationFingerprint); if (duplicate) return Object.assign({}, duplicate, { projectionPending: current.projectionPending === true, projectionError: current.projectionError || null });
|
|
142
|
+
if (current.archivedAt && label !== "database") throw error("Knowledge database is archived.", "ARCHIVED", current.revision);
|
|
143
|
+
var next = clone(current); var result = fn(next, current); next.projectionPending = true; next.projectionError = null; remember(next, operationId, operationFingerprint, result); return replace(current, next).then(function () { return finish(next, result, actor(args.actor)); });
|
|
144
|
+
}); });
|
|
145
|
+
}
|
|
146
|
+
function create(args) {
|
|
147
|
+
var input = args || {}; var operationId = mutationId(input.operationId); var operationFingerprint = fingerprint("create", input); return serialize("create:" + operationId, function () { return all().then(function (items) {
|
|
148
|
+
var duplicate; var duplicateDatabase; items.some(function (item) { duplicate = receipt(item, operationId, operationFingerprint); if (duplicate) duplicateDatabase = item; return !!duplicate; }); if (duplicate) return Object.assign({}, duplicate, { projectionPending: duplicateDatabase.projectionPending === true, projectionError: duplicateDatabase.projectionError || null });
|
|
149
|
+
var at = now(); var who = actor(input.actor); var titleId = newId("fld"); var schema = input.schema ? validation.normalizeSchema(input.schema) : validation.normalizeSchema([{ id: titleId, name: "Name", type: "title", required: true }]);
|
|
150
|
+
var viewId = newId("view"); var item = { _id: newId("db"), name: validation.text(input.name, "Database name", 120, false), revision: 1, archivedAt: null, schema: schema,
|
|
151
|
+
views: [{ id: viewId, name: "Table", type: "table", columns: schema.filter(function (field) { return !field.archived; }).map(function (field) { return field.id; }), filters: [], sorts: [], archived: false }], records: [], recordHistory: {}, databaseHistory: [], receipts: [], createdAt: at, updatedAt: at, createdBy: who, updatedBy: who, projectionPending: true, projectionError: null };
|
|
152
|
+
var result = { database: publicDatabase(item, false) }; remember(item, operationId, operationFingerprint, result); assertAggregate(item); return open().then(function (store) { return store.insertAsync(item); }).then(function () { return finish(item, result, who); });
|
|
153
|
+
}); });
|
|
154
|
+
}
|
|
155
|
+
function list(args) { return all().then(function (items) { return { databases: items.filter(function (item) { return args && args.archived === true ? !!item.archivedAt : !item.archivedAt; }).sort(function (a, b) { return b.updatedAt - a.updatedAt || (a._id === b._id ? 0 : a._id < b._id ? -1 : 1); }).map(function (item) { return publicDatabase(item, false); }) }; }); }
|
|
156
|
+
function read(args) { return load(args.databaseId).then(function (item) { if (item.archivedAt && args.includeArchived !== true) throw error("Knowledge database is archived.", "ARCHIVED", item.revision); return { database: publicDatabase(item, false) }; }); }
|
|
157
|
+
function rename(args) { return mutate(args.databaseId, args, "database", function (next, current) { ensureRevision(args.expectedRevision, current.revision, "database"); next.databaseHistory = next.databaseHistory.concat([{ revision: current.revision, at: now(), actor: actor(args.actor), action: "rename", name: current.name }]).slice(-MAX_HISTORY); next.name = validation.text(args.name, "Database name", 120, false); next.revision++; next.updatedAt = now(); next.updatedBy = actor(args.actor); return { database: publicDatabase(next, false) }; }); }
|
|
158
|
+
function archive(args) { return mutate(args.databaseId, args, "database", function (next, current) { ensureRevision(args.expectedRevision, current.revision, "database"); next.archivedAt = args.recover === true ? null : now(); next.revision++; next.updatedAt = now(); next.updatedBy = actor(args.actor); next.databaseHistory = next.databaseHistory.concat([{ revision: current.revision, at: next.updatedAt, actor: next.updatedBy, action: args.recover === true ? "recover" : "archive" }]).slice(-MAX_HISTORY); return { database: publicDatabase(next, false) }; }); }
|
|
159
|
+
function reconcileViews(views, schema) {
|
|
160
|
+
var active = {}; var titleId = null; schema.forEach(function (field) { if (!field.archived) { active[field.id] = true; if (field.type === "title") titleId = field.id; } });
|
|
161
|
+
return (views || []).map(function (view) { var clean = clone(view); clean.columns = clean.columns.filter(function (fieldId) { return active[fieldId]; }); if (!clean.columns.length && titleId) clean.columns = [titleId]; clean.filters = clean.filters.filter(function (filter) { return active[filter.fieldId]; }); clean.sorts = clean.sorts.filter(function (sort) { return active[sort.fieldId]; }); return clean; });
|
|
162
|
+
}
|
|
163
|
+
function updateSchema(args) { return mutate(args.databaseId, args, "schema", function (next, current) { ensureRevision(args.expectedRevision, current.revision, "database"); var schema = validation.normalizeSchema(args.schema); validation.assertCompatibleSchema(current.schema, schema, current.records); next.databaseHistory = next.databaseHistory.concat([{ revision: current.revision, at: now(), actor: actor(args.actor), action: "schema", schema: current.schema }]).slice(-MAX_HISTORY); next.schema = schema; next.views = reconcileViews(next.views, schema); next.revision++; next.updatedAt = now(); next.updatedBy = actor(args.actor); return { database: publicDatabase(next, false) }; }); }
|
|
164
|
+
function saveView(args) { return mutate(args.databaseId, args, "view", function (next, current) { ensureRevision(args.expectedRevision, current.revision, "database"); var view = validation.normalizeView(args.view, current.schema); var index = next.views.findIndex(function (entry) { return entry.id === view.id; }); if (index === -1 && next.views.length >= validation.MAX_VIEWS) throw error("Database has too many views.", "LIMIT"); if (index === -1) next.views.push(view); else next.views[index] = view; next.revision++; next.updatedAt = now(); next.updatedBy = actor(args.actor); return { database: publicDatabase(next, false), view: clone(view) }; }); }
|
|
165
|
+
function referenceContext(databases, current) { return { authorizeReference: function (reference) { if (reference.type === "text") return typeof settings.authorizeTextReference === "function" && settings.authorizeTextReference(reference.id) === true; var target = databases.filter(function (item) { return item._id === reference.databaseId; })[0]; return !!(target && target.records.some(function (record) { return record.id === reference.id; })); } }; }
|
|
166
|
+
function createRecord(args) { return all().then(function (databases) { return mutate(args.databaseId, args, "record", function (next, current) { ensureRevision(args.expectedDatabaseRevision, current.revision, "database"); if (next.records.length >= validation.MAX_RECORDS) throw error("Database reached the record limit.", "LIMIT"); var at = now(); var who = actor(args.actor); var record = { id: newId("rec"), revision: 1, values: validation.normalizeValues(args.values || {}, current.schema, referenceContext(databases, current), null), archivedAt: null, createdAt: at, updatedAt: at, createdBy: who, updatedBy: who }; next.records.push(record); next.recordHistory[record.id] = []; next.revision++; next.updatedAt = at; next.updatedBy = who; return { databaseRevision: next.revision, record: publicRecord(record) }; }); }); }
|
|
167
|
+
function updateRecord(args) { return all().then(function (databases) { return mutate(args.databaseId, args, "record", function (next, current) { var index = next.records.findIndex(function (record) { return record.id === validation.id(args.recordId, "record id"); }); if (index === -1) throw error("Knowledge database record not found.", "NOT_FOUND"); var before = next.records[index]; ensureRevision(args.expectedRevision, before.revision, "record"); if (before.archivedAt) throw error("Knowledge database record is archived.", "ARCHIVED", before.revision); next.recordHistory[before.id] = (next.recordHistory[before.id] || []).concat([{ revision: before.revision, at: now(), actor: actor(args.actor), action: "update", values: before.values }]).slice(-MAX_HISTORY); var record = clone(before); record.values = validation.normalizeValues(args.values || {}, current.schema, referenceContext(databases, current), record.values); record.revision++; record.updatedAt = now(); record.updatedBy = actor(args.actor); next.records[index] = record; next.revision++; next.updatedAt = record.updatedAt; next.updatedBy = record.updatedBy; return { databaseRevision: next.revision, record: publicRecord(record) }; }); }); }
|
|
168
|
+
function archiveRecord(args) { return mutate(args.databaseId, args, "record", function (next) { var index = next.records.findIndex(function (record) { return record.id === validation.id(args.recordId, "record id"); }); if (index === -1) throw error("Knowledge database record not found.", "NOT_FOUND"); var before = next.records[index]; ensureRevision(args.expectedRevision, before.revision, "record"); next.recordHistory[before.id] = (next.recordHistory[before.id] || []).concat([{ revision: before.revision, at: now(), actor: actor(args.actor), action: args.recover === true ? "recover" : "archive", values: before.values, archivedAt: before.archivedAt }]).slice(-MAX_HISTORY); var record = clone(before); record.archivedAt = args.recover === true ? null : now(); record.revision++; record.updatedAt = now(); record.updatedBy = actor(args.actor); next.records[index] = record; next.revision++; next.updatedAt = record.updatedAt; next.updatedBy = record.updatedBy; return { databaseRevision: next.revision, record: publicRecord(record) }; }); }
|
|
169
|
+
function readRecord(args) { return all().then(function (databases) { var item = databases.filter(function (entry) { return entry._id === validation.id(args.databaseId, "database id"); })[0]; if (!item) throw error("Knowledge database not found.", "NOT_FOUND"); var record = item.records.filter(function (entry) { return entry.id === validation.id(args.recordId, "record id"); })[0]; if (!record) throw error("Knowledge database record not found.", "NOT_FOUND"); return { database: publicDatabase(item, false), record: publicRecord(record, referenceStates(item, record, databases, settings)), title: titleValue(item, record), history: clone(item.recordHistory[record.id] || []) }; }); }
|
|
170
|
+
function query(args) { return all().then(function (databases) { var item = databases.filter(function (entry) { return entry._id === validation.id(args.databaseId, "database id"); })[0]; if (!item) throw error("Knowledge database not found.", "NOT_FOUND"); if (item.archivedAt) throw error("Knowledge database is archived.", "ARCHIVED", item.revision); return queryRecords(item, args, databases, settings); }); }
|
|
171
|
+
function retryProjection(args) { return serialize(args.databaseId, function () { return load(args.databaseId).then(function (item) { return finish(item, { database: publicDatabase(item, false) }, actor(args.actor)); }); }); }
|
|
172
|
+
return { filename: filename, hasStore: function () { try { return fs.lstatSync(filename).isFile(); } catch (cause) { return false; } }, isReady: function () { return entry.ready === true; }, initialize: open, listCached: listCached, create: create, list: list, read: read, rename: rename, archive: archive, recover: function (args) { return archive(Object.assign({}, args, { recover: true })); }, updateSchema: updateSchema, saveView: saveView, createRecord: createRecord, updateRecord: updateRecord, archiveRecord: archiveRecord, recoverRecord: function (args) { return archiveRecord(Object.assign({}, args, { recover: true })); }, readRecord: readRecord, query: query, retryProjection: retryProjection };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { createKnowledgeDbService: createKnowledgeDbService, MAX_HISTORY: MAX_HISTORY, MAX_PAGE: MAX_PAGE, MAX_AGGREGATE_BYTES: MAX_AGGREGATE_BYTES, IDEMPOTENCY_WINDOW_MS: IDEMPOTENCY_WINDOW_MS };
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
var markdown = require("./knowledge-markdown");
|
|
3
|
+
|
|
4
|
+
var MAX_INDEX_BYTES = 64 * 1024 * 1024;
|
|
5
|
+
var MAX_RESULT_LIMIT = 100;
|
|
6
|
+
|
|
7
|
+
function identity(file) {
|
|
8
|
+
return file.common ? "shared:" + encodeURIComponent(file.ownMateId || "unknown") + ":" + encodeURIComponent(file.id || file.name) : "local:" + encodeURIComponent(file.id || file.name);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createKnowledgeDocumentIndex(options) {
|
|
12
|
+
var cache = {};
|
|
13
|
+
|
|
14
|
+
function reconcile() {
|
|
15
|
+
var listed = options.list(); var documents = []; var next = {}; var errors = []; var bytes = 0;
|
|
16
|
+
for (var i = 0; i < listed.length; i++) {
|
|
17
|
+
var file = listed[i]; var key = identity(file); var known = cache[key]; var content; var token = file.revision || null;
|
|
18
|
+
if (file.size > options.maxDocumentBytes) { errors.push({ name: file.name, code: "TOO_LARGE", error: "Document exceeds the safe read limit." }); continue; }
|
|
19
|
+
if (known && token && known.token === token) {
|
|
20
|
+
if (bytes + known.bytes > MAX_INDEX_BYTES) { errors.push({ name: file.name, code: "INDEX_LIMIT", error: "Workspace Knowledge exceeds the safe index limit." }); continue; }
|
|
21
|
+
next[key] = known; documents.push(known.document); bytes += known.bytes; continue;
|
|
22
|
+
}
|
|
23
|
+
try { content = options.read(file); } catch (error) { errors.push({ name: file.name, code: error.code || "READ_FAILED", error: error.message }); continue; }
|
|
24
|
+
var size = Buffer.byteLength(content, "utf8");
|
|
25
|
+
if (size > options.maxDocumentBytes) { errors.push({ name: file.name, code: "TOO_LARGE", error: "Document exceeds the safe read limit." }); continue; }
|
|
26
|
+
if (bytes + size > MAX_INDEX_BYTES) { errors.push({ name: file.name, code: "INDEX_LIMIT", error: "Workspace Knowledge exceeds the safe index limit." }); continue; }
|
|
27
|
+
token = token || crypto.createHash("sha256").update(content, "utf8").digest("hex");
|
|
28
|
+
var parsed = file.itemType === "db" ? { links: [], headings: [], tags: [], properties: {}, propertyErrors: [] } : markdown.parse(content);
|
|
29
|
+
var document = Object.assign({}, file, { key: key, content: content, parsed: parsed });
|
|
30
|
+
next[key] = { token: token, bytes: size, document: document }; documents.push(document); bytes += size;
|
|
31
|
+
}
|
|
32
|
+
cache = next;
|
|
33
|
+
return { documents: documents, errors: errors, complete: errors.length === 0, bytes: bytes };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function search(query, offset, limit) {
|
|
37
|
+
var indexed = reconcile(); var needle = String(query || "").trim().toLowerCase().slice(0, 200); var matches = [];
|
|
38
|
+
if (needle) for (var i = 0; i < indexed.documents.length; i++) {
|
|
39
|
+
var file = indexed.documents[i]; var at = file.content.toLowerCase().indexOf(needle);
|
|
40
|
+
if (file.name.toLowerCase().indexOf(needle) !== -1 || at !== -1) matches.push(Object.assign({}, file, { content: undefined, parsed: undefined, snippet: markdown.snippet(file.content, at === -1 ? 0 : at, needle) }));
|
|
41
|
+
}
|
|
42
|
+
var start = Math.max(0, Number(offset) || 0); var count = Math.max(1, Math.min(MAX_RESULT_LIMIT, Number(limit) || 50));
|
|
43
|
+
return { files: matches.slice(start, start + count), total: matches.length, offset: start, limit: count, complete: indexed.complete, errors: indexed.errors };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { reconcile: reconcile, search: search, identity: identity };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { createKnowledgeDocumentIndex: createKnowledgeDocumentIndex, identity: identity, MAX_INDEX_BYTES: MAX_INDEX_BYTES, MAX_RESULT_LIMIT: MAX_RESULT_LIMIT };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
var fs = require("fs");
|
|
3
|
+
var path = require("path");
|
|
4
|
+
|
|
5
|
+
var MAX_DRAFT_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
function safePart(value) { return crypto.createHash("sha256").update(String(value || "unknown")).digest("hex").slice(0, 24); }
|
|
8
|
+
function createKnowledgeDraftStore(baseDir, scopeKey) {
|
|
9
|
+
var root = path.join(baseDir, "knowledge-document-drafts", safePart(scopeKey));
|
|
10
|
+
function safeDirectory(directory, create) {
|
|
11
|
+
var stat;
|
|
12
|
+
try { stat = fs.lstatSync(directory); } catch (error) { if (!create || error.code !== "ENOENT") throw error; fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); stat = fs.lstatSync(directory); }
|
|
13
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw Object.assign(new Error("Knowledge draft path is not a safe directory."), { code: "UNSAFE_PATH" });
|
|
14
|
+
}
|
|
15
|
+
function userRoot(userId) { return path.join(root, safePart(userId)); }
|
|
16
|
+
function target(userId, documentId) { return path.join(userRoot(userId), safePart(documentId) + ".json"); }
|
|
17
|
+
function read(userId, documentId) {
|
|
18
|
+
try { safeDirectory(root, false); safeDirectory(userRoot(userId), false); var file = target(userId, documentId); var stat = fs.lstatSync(file);
|
|
19
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_DRAFT_BYTES + 4096) throw Object.assign(new Error("Knowledge draft is not a safe file."), { code: "UNSAFE_PATH" });
|
|
20
|
+
var value = JSON.parse(fs.readFileSync(file, "utf8")); return value && value.version === 1 ? value : null; }
|
|
21
|
+
catch (error) { if (error.code === "ENOENT" || error instanceof SyntaxError) return null; throw error; }
|
|
22
|
+
}
|
|
23
|
+
function save(userId, documentId, draft) {
|
|
24
|
+
var content = draft && draft.content;
|
|
25
|
+
if (typeof content !== "string" || Buffer.byteLength(content, "utf8") > MAX_DRAFT_BYTES) throw new Error("Knowledge draft is invalid or too large.");
|
|
26
|
+
var current = read(userId, documentId); var clientVersion = Math.max(0, Number(draft.clientVersion) || 0);
|
|
27
|
+
if (current && Number(current.clientVersion) > clientVersion) return current;
|
|
28
|
+
var value = { version: 1, documentId: String(documentId), name: String(draft.name || "").slice(0, 500), content: content,
|
|
29
|
+
baseRevision: draft.baseRevision || null, cursor: draft.cursor && typeof draft.cursor === "object" ? { line: Number(draft.cursor.line) || 0, ch: Number(draft.cursor.ch) || 0 } : null,
|
|
30
|
+
clientVersion: clientVersion, newDocument: draft.newDocument === true, savedAt: Date.now() };
|
|
31
|
+
safeDirectory(root, true); safeDirectory(userRoot(userId), true); var file = target(userId, documentId);
|
|
32
|
+
var temporary = file + ".tmp-" + process.pid + "-" + crypto.randomBytes(5).toString("hex");
|
|
33
|
+
try { fs.writeFileSync(temporary, JSON.stringify(value), { flag: "wx", mode: 0o600 }); fs.renameSync(temporary, file); }
|
|
34
|
+
finally { try { fs.unlinkSync(temporary); } catch (error) {} }
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
function remove(userId, documentId, throughVersion) {
|
|
38
|
+
var current = read(userId, documentId); if (!current) return false;
|
|
39
|
+
if (throughVersion != null && Number(current.clientVersion) > Number(throughVersion)) return false;
|
|
40
|
+
try { fs.unlinkSync(target(userId, documentId)); return true; } catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
41
|
+
}
|
|
42
|
+
function list(userId) {
|
|
43
|
+
var folder = userRoot(userId); var names; try { safeDirectory(root, false); safeDirectory(folder, false); } catch (error) { if (error.code === "ENOENT") return []; throw error; }
|
|
44
|
+
try { names = fs.readdirSync(folder); } catch (error) { if (error.code === "ENOENT") return []; throw error; }
|
|
45
|
+
var values = [];
|
|
46
|
+
for (var i = 0; i < names.length; i++) {
|
|
47
|
+
if (!/^[a-f0-9]{24}\.json$/.test(names[i])) continue;
|
|
48
|
+
try { var file = path.join(folder, names[i]); var stat = fs.lstatSync(file); if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_DRAFT_BYTES + 4096) continue;
|
|
49
|
+
var value = JSON.parse(fs.readFileSync(file, "utf8")); if (value && value.version === 1) values.push(value); } catch (error) {}
|
|
50
|
+
}
|
|
51
|
+
return values.sort(function (a, b) { return b.savedAt - a.savedAt; });
|
|
52
|
+
}
|
|
53
|
+
return { read: read, save: save, remove: remove, list: list };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { createKnowledgeDraftStore: createKnowledgeDraftStore };
|