clay-server 5.1.0-beta.1 → 6.0.0-beta.1

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.
Files changed (222) hide show
  1. package/LICENSES/README.md +4 -0
  2. package/LICENSES/dicebear-avatars.md +16 -0
  3. package/bin/cli.js +9 -91
  4. package/lib/daemon-project-access.js +7 -1
  5. package/lib/daemon-settings.js +11 -0
  6. package/lib/daemon.js +10 -37
  7. package/lib/dicebear-avatar.js +62 -0
  8. package/lib/knowledge-db-service.js +175 -0
  9. package/lib/knowledge-db-validation.js +0 -0
  10. package/lib/knowledge-document-index.js +49 -0
  11. package/lib/knowledge-draft-store.js +56 -0
  12. package/lib/knowledge-file-store.js +199 -0
  13. package/lib/knowledge-markdown.js +128 -0
  14. package/lib/linear-store.js +35 -8
  15. package/lib/mate-knowledge-mcp-server.js +29 -6
  16. package/lib/mate-knowledge-service.js +23 -2
  17. package/lib/mate-knowledge-sync.js +36 -21
  18. package/lib/mate-workspace-color.js +4 -0
  19. package/lib/mates.js +18 -0
  20. package/lib/os-users.js +5 -16
  21. package/lib/project-connection.js +36 -9
  22. package/lib/project-debate-proposal.js +6 -0
  23. package/lib/project-debate-workspace.js +43 -0
  24. package/lib/project-debate.js +1 -0
  25. package/lib/project-filesystem.js +21 -39
  26. package/lib/project-knowledge.js +315 -136
  27. package/lib/project-mate-creation-proposal.js +1 -0
  28. package/lib/project-mate-interaction.js +2 -2
  29. package/lib/project-server-settings.js +31 -0
  30. package/lib/project-sessions.js +92 -182
  31. package/lib/project-user-knowledge.js +60 -0
  32. package/lib/project.js +59 -18
  33. package/lib/public/app.js +79 -71
  34. package/lib/public/clay-studio-wordmark-bold.svg +20 -0
  35. package/lib/public/css/clay-workspace.css +105 -0
  36. package/lib/public/css/debates-workbench.css +47 -0
  37. package/lib/public/css/home-debate-live.css +26 -0
  38. package/lib/public/css/home-hub.css +3 -1
  39. package/lib/public/css/home-mate-model-picker.css +15 -131
  40. package/lib/public/css/home-mate-settings.css +168 -16
  41. package/lib/public/css/icon-strip.css +249 -32
  42. package/lib/public/css/input.css +3 -1
  43. package/lib/public/css/mate-conversation.css +176 -0
  44. package/lib/public/css/mate-knowledge-workbench.css +118 -0
  45. package/lib/public/css/mate-workspace-color.css +16 -0
  46. package/lib/public/css/mates.css +28 -28
  47. package/lib/public/css/messages.css +1 -114
  48. package/lib/public/css/mobile-nav.css +16 -16
  49. package/lib/public/css/overlays.css +8 -0
  50. package/lib/public/css/project-logs.css +7 -0
  51. package/lib/public/css/project-mate-creation.css +105 -0
  52. package/lib/public/css/project-settings-modal.css +56 -0
  53. package/lib/public/css/right-workbench.css +3 -3
  54. package/lib/public/css/server-settings-system.css +125 -0
  55. package/lib/public/css/session-folders.css +80 -21
  56. package/lib/public/css/session-github.css +25 -16
  57. package/lib/public/css/settings-ui.css +247 -22
  58. package/lib/public/css/shared-browser.css +2 -0
  59. package/lib/public/css/sidebar.css +183 -107
  60. package/lib/public/css/title-bar.css +156 -1
  61. package/lib/public/css/worker-delegation.css +47 -0
  62. package/lib/public/css/worker-proposal.css +72 -150
  63. package/lib/public/css/you.css +74 -0
  64. package/lib/public/index.html +203 -547
  65. package/lib/public/modules/account-island.js +232 -0
  66. package/lib/public/modules/admin-settings-email.js +197 -0
  67. package/lib/public/modules/admin-settings-invitations.js +183 -0
  68. package/lib/public/modules/admin-settings-projects.js +198 -0
  69. package/lib/public/modules/admin-settings-ui.js +100 -0
  70. package/lib/public/modules/admin-settings-users.js +331 -0
  71. package/lib/public/modules/admin.js +19 -949
  72. package/lib/public/modules/app-cursors.js +19 -43
  73. package/lib/public/modules/app-debate-ui.js +4 -3
  74. package/lib/public/modules/app-dm.js +0 -2
  75. package/lib/public/modules/app-header.js +106 -0
  76. package/lib/public/modules/app-message-router.js +12 -3
  77. package/lib/public/modules/app-messages.js +47 -61
  78. package/lib/public/modules/app-notifications.js +4 -7
  79. package/lib/public/modules/app-projects.js +5 -38
  80. package/lib/public/modules/app-rate-limit.js +70 -12
  81. package/lib/public/modules/app-rendering.js +12 -13
  82. package/lib/public/modules/avatar.js +25 -9
  83. package/lib/public/modules/chat-render-runtime.js +16 -4
  84. package/lib/public/modules/command-palette.js +2 -15
  85. package/lib/public/modules/context-sources.js +27 -18
  86. package/lib/public/modules/cursor-sharing-preference.js +3 -3
  87. package/lib/public/modules/debate-controls.js +299 -0
  88. package/lib/public/modules/debate.js +2 -0
  89. package/lib/public/modules/debates-workbench.js +339 -0
  90. package/lib/public/modules/default-ai.js +88 -65
  91. package/lib/public/modules/header-tui-font.js +10 -20
  92. package/lib/public/modules/home-debate-controls.js +18 -289
  93. package/lib/public/modules/home-mate-avatar-editor.js +228 -0
  94. package/lib/public/modules/home-mate-color-picker.js +66 -0
  95. package/lib/public/modules/home-mate-creation.js +1 -1
  96. package/lib/public/modules/home-mate-instructions.js +90 -0
  97. package/lib/public/modules/home-mate-model-picker.js +80 -271
  98. package/lib/public/modules/home-mate-profile-editor.js +72 -0
  99. package/lib/public/modules/home-mate-settings.js +118 -125
  100. package/lib/public/modules/home-shell.js +3 -0
  101. package/lib/public/modules/home-surface-boot.js +1 -9
  102. package/lib/public/modules/icon-strip-tooltip.js +95 -0
  103. package/lib/public/modules/knowledge-db-edit-state.js +35 -0
  104. package/lib/public/modules/knowledge-db-workbench.js +234 -0
  105. package/lib/public/modules/knowledge-history-state.js +55 -0
  106. package/lib/public/modules/knowledge-markdown-editor.js +57 -0
  107. package/lib/public/modules/knowledge-preview-links.js +70 -0
  108. package/lib/public/modules/knowledge-workbench-shell.js +21 -0
  109. package/lib/public/modules/linear-settings.js +3 -2
  110. package/lib/public/modules/markdown.js +5 -6
  111. package/lib/public/modules/mate-knowledge-workbench-state.js +174 -0
  112. package/lib/public/modules/mate-knowledge-workbench.js +500 -0
  113. package/lib/public/modules/mate-knowledge.js +11 -3
  114. package/lib/public/modules/mate-management.js +54 -10
  115. package/lib/public/modules/mate-sidebar.js +11 -1
  116. package/lib/public/modules/mate-workspace-color.js +33 -0
  117. package/lib/public/modules/mention.js +5 -13
  118. package/lib/public/modules/notification-settings.js +81 -0
  119. package/lib/public/modules/notifications.js +44 -28
  120. package/lib/public/modules/personal-settings-dialog.js +51 -0
  121. package/lib/public/modules/profile.js +66 -24
  122. package/lib/public/modules/project-chat-identity.js +79 -0
  123. package/lib/public/modules/project-debate-planning-workspace.js +250 -0
  124. package/lib/public/modules/project-debate-workspace.js +185 -0
  125. package/lib/public/modules/project-header-identity.js +127 -0
  126. package/lib/public/modules/project-logs-render.js +9 -4
  127. package/lib/public/modules/project-mate-creation-workspace.js +414 -0
  128. package/lib/public/modules/project-mate-navigation.js +238 -0
  129. package/lib/public/modules/project-session-navigation.js +29 -0
  130. package/lib/public/modules/project-settings-environment.js +106 -0
  131. package/lib/public/modules/project-settings.js +72 -628
  132. package/lib/public/modules/project-switcher.js +6 -8
  133. package/lib/public/modules/projects-hub-view.js +5 -6
  134. package/lib/public/modules/projects-hub.js +2 -6
  135. package/lib/public/modules/qrcode.js +27 -8
  136. package/lib/public/modules/search-clay-chat.js +8 -11
  137. package/lib/public/modules/server-settings-controls.js +316 -0
  138. package/lib/public/modules/server-settings-environment.js +194 -0
  139. package/lib/public/modules/server-settings-navigation.js +39 -0
  140. package/lib/public/modules/server-settings-state.js +54 -0
  141. package/lib/public/modules/server-settings.js +116 -788
  142. package/lib/public/modules/server-storage-settings.js +84 -0
  143. package/lib/public/modules/session-activity-tooltip.js +12 -0
  144. package/lib/public/modules/session-create-form.js +86 -14
  145. package/lib/public/modules/session-create.js +50 -7
  146. package/lib/public/modules/session-github.js +2 -1
  147. package/lib/public/modules/session-list-visibility.js +41 -0
  148. package/lib/public/modules/session-presence-ui.js +3 -0
  149. package/lib/public/modules/session-title-marquee.js +51 -0
  150. package/lib/public/modules/settings-env-values.js +34 -0
  151. package/lib/public/modules/settings-select.js +121 -0
  152. package/lib/public/modules/shared-browser.js +10 -7
  153. package/lib/public/modules/sidebar-mates.js +3 -50
  154. package/lib/public/modules/sidebar-mobile.js +63 -60
  155. package/lib/public/modules/sidebar-projects.js +84 -97
  156. package/lib/public/modules/sidebar-session-folders-ui.js +2 -2
  157. package/lib/public/modules/sidebar-session-hierarchy.js +149 -70
  158. package/lib/public/modules/sidebar-sessions.js +49 -50
  159. package/lib/public/modules/sidebar.js +11 -13
  160. package/lib/public/modules/stt-picker.js +14 -5
  161. package/lib/public/modules/terminal-prefs.js +22 -0
  162. package/lib/public/modules/theme.js +88 -52
  163. package/lib/public/modules/thinking-lifecycle.js +7 -6
  164. package/lib/public/modules/tool-palette-order.js +4 -3
  165. package/lib/public/modules/tool-palette.js +13 -3
  166. package/lib/public/modules/tools.js +22 -22
  167. package/lib/public/modules/user-settings-account.js +318 -0
  168. package/lib/public/modules/user-settings-appearance.js +55 -0
  169. package/lib/public/modules/user-settings-editors.js +24 -0
  170. package/lib/public/modules/user-settings-integrations.js +104 -0
  171. package/lib/public/modules/user-settings-layout.js +80 -0
  172. package/lib/public/modules/user-settings-preferences.js +48 -0
  173. package/lib/public/modules/user-settings-profile.js +53 -0
  174. package/lib/public/modules/user-settings-themes.js +38 -0
  175. package/lib/public/modules/user-settings-voice.js +75 -0
  176. package/lib/public/modules/user-settings.js +53 -480
  177. package/lib/public/modules/worker-proposal.js +42 -8
  178. package/lib/public/modules/worker-runtime-picker.js +115 -0
  179. package/lib/public/modules/you-activity.js +121 -0
  180. package/lib/public/modules/you.js +251 -0
  181. package/lib/public/style.css +20 -8
  182. package/lib/public/vendor/codemirror/LICENSE +21 -0
  183. package/lib/public/vendor/codemirror/codemirror.css +344 -0
  184. package/lib/public/vendor/codemirror/codemirror.js +9884 -0
  185. package/lib/public/vendor/codemirror/continuelist.js +101 -0
  186. package/lib/public/vendor/codemirror/markdown.js +886 -0
  187. package/lib/public/vendor/codemirror/show-hint.css +37 -0
  188. package/lib/public/vendor/codemirror/show-hint.js +523 -0
  189. package/lib/sdk-bridge.js +15 -3
  190. package/lib/sdk-message-processor.js +16 -4
  191. package/lib/sdk-quota-snapshot.js +37 -0
  192. package/lib/server-admin.js +11 -0
  193. package/lib/server-appearance-settings.js +71 -0
  194. package/lib/server-home-chat-events.js +1 -1
  195. package/lib/server-home-chat.js +25 -3
  196. package/lib/server-home-debate-planning.js +4 -1
  197. package/lib/server-home-debates.js +2 -0
  198. package/lib/server-home-mate-creation.js +4 -1
  199. package/lib/server-linear.js +14 -8
  200. package/lib/server-mate-instructions.js +44 -0
  201. package/lib/server-mates.js +12 -3
  202. package/lib/server-notification-settings.js +67 -0
  203. package/lib/server-palette.js +1 -1
  204. package/lib/server-settings.js +69 -1
  205. package/lib/server-storage.js +56 -0
  206. package/lib/server-user-knowledge.js +119 -0
  207. package/lib/server.js +31 -3
  208. package/lib/session-visibility.js +7 -2
  209. package/lib/sessions.js +34 -9
  210. package/lib/startup-projects.js +40 -0
  211. package/lib/storage-usage.js +127 -0
  212. package/lib/user-knowledge-service.js +167 -0
  213. package/lib/users-permissions.js +7 -0
  214. package/lib/users-preferences.js +0 -74
  215. package/lib/users-project-mate-filter-preferences.js +39 -0
  216. package/lib/users.js +6 -4
  217. package/lib/workspace-query-access.js +4 -1
  218. package/lib/ws-schema.js +52 -4
  219. package/lib/yoke/adapters/codex.js +8 -45
  220. package/lib/yoke/codex-model-catalog.js +40 -0
  221. package/lib/yoke/codex-rate-limits.js +26 -0
  222. package/package.json +12 -3
@@ -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 };
@@ -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 };
@@ -19,34 +19,61 @@ function attachLinearStore(root) {
19
19
  catch (error) { if (error.code !== 'EEXIST') throw error; }
20
20
  return fs.readFileSync(file);
21
21
  }
22
- function key(owner) {
22
+ function record(owner, id) {
23
23
  var data = read(owner);
24
+ return !id || id === "default" ? data : Object.hasOwn(data.connections || {}, id) ? data.connections[id] : {};
25
+ }
26
+ function key(owner, id) {
27
+ var data = record(owner, id);
24
28
  if (!data.secret) return null;
25
29
  var parts = data.secret.split('.').map(function (part) { return Buffer.from(part, 'base64'); });
26
30
  var cipher = crypto.createDecipheriv('aes-256-gcm', masterKey(), parts[0]);
27
- cipher.setAAD(Buffer.from(owner)); cipher.setAuthTag(parts[1]);
31
+ cipher.setAAD(Buffer.from(id && id !== 'default' ? owner + ':' + id : owner)); cipher.setAuthTag(parts[1]);
28
32
  return Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8');
29
33
  }
30
- function view(owner) {
31
- var data = read(owner);
34
+ function view(owner, id) {
35
+ var data = record(owner, id);
32
36
  return { connected: !!data.secret, workspace: data.workspace || null, revision: data.revision || null };
33
37
  }
34
- function save(owner, secret, workspace) {
38
+ function save(owner, secret, workspace, id, name) {
39
+ if (id && (!/^[a-zA-Z0-9_-]{1,64}$/.test(id) || ['__proto__', 'constructor', 'prototype'].indexOf(id) >= 0)) throw new Error("Invalid connection.");
40
+ var previous = read(owner);
35
41
  var file = filename(owner), data = { revision: crypto.randomBytes(16).toString('hex') };
36
42
  if (secret !== null) {
37
43
  if (typeof secret !== 'string' || !secret.trim() || secret.length > 4096 || /[\r\n]/.test(secret)) throw new Error('Enter a valid Linear API key.');
38
44
  var iv = crypto.randomBytes(12), cipher = crypto.createCipheriv('aes-256-gcm', masterKey(), iv);
39
- cipher.setAAD(Buffer.from(owner));
45
+ cipher.setAAD(Buffer.from(id && id !== 'default' ? owner + ':' + id : owner));
40
46
  var bytes = Buffer.concat([cipher.update(secret.trim(), 'utf8'), cipher.final()]);
41
47
  data.secret = [iv, cipher.getAuthTag(), bytes].map(function (part) { return part.toString('base64'); }).join('.');
42
48
  data.workspace = workspace;
43
49
  }
50
+ if (name !== undefined) {
51
+ if (typeof name !== 'string' || !name.trim() || name.trim().length > 80) throw new Error('Enter a connection name of up to 80 characters.');
52
+ data.name = name.trim();
53
+ } else data.name = record(owner, id).name;
54
+ if (id && id !== 'default') {
55
+ previous.connections = previous.connections || {};
56
+ if (secret === null) delete previous.connections[id];
57
+ else {
58
+ if (!previous.connections[id] && Object.keys(previous.connections).length >= 30) throw new Error('Connection limit reached.');
59
+ previous.connections[id] = data;
60
+ }
61
+ data = previous;
62
+ } else data.connections = previous.connections;
44
63
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
45
64
  var temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
46
65
  try { fs.writeFileSync(temp, JSON.stringify(data), { mode: 0o600 }); fs.renameSync(temp, file); }
47
66
  finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
48
- return view(owner);
67
+ return view(owner, id);
68
+ }
69
+ function list(owner) {
70
+ var data = read(owner), entries = [];
71
+ if (data.secret) entries.push(Object.assign({ id: 'default', name: data.name || 'Existing connection', legacy: true }, view(owner)));
72
+ Object.keys(data.connections || {}).forEach(function (id) {
73
+ entries.push(Object.assign({ id: id, name: data.connections[id].name || 'Linear', legacy: false }, view(owner, id)));
74
+ });
75
+ return entries;
49
76
  }
50
- return { key: key, view: view, save: save };
77
+ return { key: key, view: view, save: save, list: list };
51
78
  }
52
79
  module.exports = { attachLinearStore: attachLinearStore };
@@ -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