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
@@ -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/
package/bin/cli.js CHANGED
@@ -37,6 +37,7 @@ var { generateAuthToken } = require("../lib/server");
37
37
  var { enableMultiUser, disableMultiUser, hasAdmin, isMultiUser, getSetupCode } = require("../lib/users");
38
38
  var clayStudioCert = require("../lib/clay-studio-cert");
39
39
  var cliWordmark = require("../lib/cli-wordmark");
40
+ var buildStartupProjects = require("../lib/startup-projects").buildStartupProjects;
40
41
 
41
42
  function openUrl(url) {
42
43
  try {
@@ -446,9 +447,6 @@ async function restartDaemonFromConfig() {
446
447
  var newConfig = Object.assign({}, lastConfig, {
447
448
  pid: null,
448
449
  port: targetPort,
449
- projects: (lastConfig.projects || []).filter(function (p) {
450
- return fs.existsSync(p.path);
451
- })
452
450
  });
453
451
 
454
452
  ensureConfigDir();
@@ -1445,56 +1443,15 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
1445
1443
  return;
1446
1444
  }
1447
1445
 
1448
- var allProjects = [];
1449
- var usedSlugs = [];
1450
-
1451
- // Load previous config to preserve per-project settings (visibility, allowedUsers)
1452
1446
  var prevConfig = loadConfig();
1453
- var prevProjectMap = {};
1454
- if (prevConfig && prevConfig.projects) {
1455
- for (var pi = 0; pi < prevConfig.projects.length; pi++) {
1456
- prevProjectMap[prevConfig.projects[pi].path] = prevConfig.projects[pi];
1457
- }
1458
- }
1459
-
1460
- // Only include cwd if explicitly requested
1447
+ var recentProjects = extraProjects || [];
1461
1448
  if (addCwd) {
1462
- var slug = generateSlug(cwd, []);
1463
- var cwdEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1464
- // Restore title/icon from .clayrc if available
1465
- var cwdRc = loadClayrc();
1466
- var cwdRecent = cwdRc.recentProjects || [];
1467
- for (var cr = 0; cr < cwdRecent.length; cr++) {
1468
- if (cwdRecent[cr].path === cwd) {
1469
- if (cwdRecent[cr].title) cwdEntry.title = cwdRecent[cr].title;
1470
- if (cwdRecent[cr].icon) cwdEntry.icon = cwdRecent[cr].icon;
1471
- break;
1472
- }
1473
- }
1474
- // Restore project-level settings from previous config
1475
- if (prevProjectMap[cwd]) {
1476
- cwdEntry = Object.assign({}, prevProjectMap[cwd], cwdEntry);
1477
- }
1478
- allProjects.push(cwdEntry);
1479
- usedSlugs.push(slug);
1480
- }
1481
-
1482
- // Add restored projects (from ~/.clayrc)
1483
- if (extraProjects && extraProjects.length > 0) {
1484
- for (var ep = 0; ep < extraProjects.length; ep++) {
1485
- var rp = extraProjects[ep];
1486
- if (rp.path === cwd) continue; // skip if same as cwd
1487
- if (!fs.existsSync(rp.path)) continue; // skip missing directories
1488
- var rpSlug = generateSlug(rp.path, usedSlugs);
1489
- usedSlugs.push(rpSlug);
1490
- var rpEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
1491
- // Restore project-level settings from previous config
1492
- if (prevProjectMap[rp.path]) {
1493
- rpEntry = Object.assign({}, prevProjectMap[rp.path], rpEntry);
1494
- }
1495
- allProjects.push(rpEntry);
1496
- }
1449
+ var cwdRecent = (loadClayrc().recentProjects || []).filter(function (p) { return p.path === cwd; });
1450
+ recentProjects = cwdRecent.concat(recentProjects);
1497
1451
  }
1452
+ var allProjects = buildStartupProjects(prevConfig, recentProjects, {
1453
+ cwd: cwd, addCwd: addCwd, existsSync: fs.existsSync, generateSlug: generateSlug,
1454
+ });
1498
1455
 
1499
1456
  var config = Object.assign({}, prevConfig || {}, {
1500
1457
  pid: null,
@@ -1629,49 +1586,10 @@ async function devMode(mode, keepAwake, existingPinHash, wantOsUsers) {
1629
1586
  return;
1630
1587
  }
1631
1588
 
1632
- var slug = generateSlug(cwd, []);
1633
- var cwdDevEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1634
-
1635
- // Load previous config to preserve per-project settings (visibility, allowedUsers)
1636
1589
  var prevDevConfig = loadConfig();
1637
- var prevDevProjectMap = {};
1638
- if (prevDevConfig && prevDevConfig.projects) {
1639
- for (var pdi = 0; pdi < prevDevConfig.projects.length; pdi++) {
1640
- prevDevProjectMap[prevDevConfig.projects[pdi].path] = prevDevConfig.projects[pdi];
1641
- }
1642
- }
1643
-
1644
- // Restore previous projects
1645
- var rc = loadClayrc();
1646
- var restorable = (rc.recentProjects || []).filter(function (p) {
1647
- return p.path !== cwd && fs.existsSync(p.path);
1590
+ var allProjects = buildStartupProjects(prevDevConfig, loadClayrc().recentProjects || [], {
1591
+ cwd: cwd, addCwd: true, existsSync: fs.existsSync, generateSlug: generateSlug,
1648
1592
  });
1649
- // Restore title/icon for cwd from .clayrc
1650
- var rcAll = rc.recentProjects || [];
1651
- for (var ci = 0; ci < rcAll.length; ci++) {
1652
- if (rcAll[ci].path === cwd) {
1653
- if (rcAll[ci].title) cwdDevEntry.title = rcAll[ci].title;
1654
- if (rcAll[ci].icon) cwdDevEntry.icon = rcAll[ci].icon;
1655
- break;
1656
- }
1657
- }
1658
- // Restore access settings for cwd from previous config
1659
- if (prevDevProjectMap[cwd]) {
1660
- cwdDevEntry = Object.assign({}, prevDevProjectMap[cwd], cwdDevEntry);
1661
- }
1662
- var allProjects = [cwdDevEntry];
1663
- var usedSlugs = [slug];
1664
- for (var ri = 0; ri < restorable.length; ri++) {
1665
- var rp = restorable[ri];
1666
- var rpSlug = generateSlug(rp.path, usedSlugs);
1667
- usedSlugs.push(rpSlug);
1668
- var rpDevEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
1669
- // Restore project-level settings from previous config
1670
- if (prevDevProjectMap[rp.path]) {
1671
- rpDevEntry = Object.assign({}, prevDevProjectMap[rp.path], rpDevEntry);
1672
- }
1673
- allProjects.push(rpDevEntry);
1674
- }
1675
1593
 
1676
1594
  var config = Object.assign({}, prevDevConfig || {}, {
1677
1595
  pid: null,
@@ -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 };
@@ -0,0 +1,11 @@
1
+ // Persist before publishing a setting to live daemon state.
2
+ function saveDaemonSetting(config, key, value, saveConfig) {
3
+ var overrides = {}; overrides[key] = value;
4
+ var next = Object.assign({}, config, overrides);
5
+ if (value === undefined) delete next[key];
6
+ saveConfig(next);
7
+ if (value === undefined) delete config[key];
8
+ Object.assign(config, next);
9
+ return next;
10
+ }
11
+ module.exports = { saveDaemonSetting: saveDaemonSetting };
package/lib/daemon.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ var saveDaemonSetting = require("./daemon-settings").saveDaemonSetting;
2
3
 
3
4
  // --- Node version check ---
4
5
  var nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
@@ -72,9 +73,7 @@ if (projectLogContext.initializeProjectKnowledge(config)) {
72
73
  }
73
74
 
74
75
  // --- OS users mode: check required system dependencies ---
75
- // Supplementary-group inheritance (issue #367): default on so sessions behave
76
- // like a normal login; operators can disable via config / the settings toggle.
77
- osUsersMod.setInheritGroups(config.inheritGroups !== false);
76
+ // User-switched processes inherit the account groups, like a normal login.
78
77
  if (config.osUsers) {
79
78
  var checkExec = require("child_process").execFileSync;
80
79
  var missing = [];
@@ -86,11 +85,7 @@ if (config.osUsers) {
86
85
  console.error("[daemon] Install with: sudo apt install " + missing.map(function (m) { return m.split(" ")[0]; }).join(" "));
87
86
  process.exit(78); // EX_CONFIG
88
87
  }
89
- if (config.inheritGroups !== false) {
90
- console.log("[daemon] Sessions inherit each user's supplementary groups (docker, etc.); disable in Settings > User Groups.");
91
- } else {
92
- console.log("[daemon] Sessions run with primary group only (supplementary groups dropped); enable in Settings > User Groups.");
93
- }
88
+ console.log("[daemon] Sessions inherit each user's supplementary groups when setpriv is available.");
94
89
  }
95
90
 
96
91
  // --- TLS ---
@@ -565,6 +560,7 @@ var relay = createServer({
565
560
  return result;
566
561
  },
567
562
  onProjectOwnerChanged: function (slug, ownerId) {
563
+ if (slug.indexOf("mate-") === 0) return {error:"Mate ownership cannot be transferred."};
568
564
  console.log("[daemon] onProjectOwnerChanged:", slug, "→", ownerId);
569
565
  var oldOwnerId = null;
570
566
  var projectIdx = -1;
@@ -636,12 +632,7 @@ var relay = createServer({
636
632
  return { envrc: config.sharedEnv || "" };
637
633
  },
638
634
  onSetSharedEnv: function (envrc) {
639
- if (envrc) {
640
- config.sharedEnv = envrc;
641
- } else {
642
- delete config.sharedEnv;
643
- }
644
- saveConfig(config);
635
+ saveDaemonSetting(config, "sharedEnv", envrc || undefined, saveConfig);
645
636
  return { ok: true };
646
637
  },
647
638
  onGetServerDefaultEffort: function () {
@@ -811,7 +802,6 @@ var relay = createServer({
811
802
  keepAwake: !!config.keepAwake,
812
803
  autoContinueOnRateLimit: !!config.autoContinueOnRateLimit,
813
804
  osUsers: !!config.osUsers,
814
- inheritGroups: config.inheritGroups !== false,
815
805
  chatLayout: config.chatLayout || "channel",
816
806
  matesEnabled: config.matesEnabled === true,
817
807
  capsulesEnabled: config.capsulesEnabled === true,
@@ -826,25 +816,18 @@ var relay = createServer({
826
816
  onSetImageRetention: function (days) {
827
817
  var d = parseInt(days, 10);
828
818
  if (isNaN(d) || d < 0) d = 7;
829
- config.imageRetentionDays = d;
830
- saveConfig(config);
819
+ saveDaemonSetting(config, "imageRetentionDays", d, saveConfig);
831
820
  console.log("[daemon] Image retention:", d === 0 ? "forever" : d + " days");
832
821
  return { ok: true, days: d };
833
822
  },
834
823
  onSetUpdateChannel: function (channel) {
835
- config.updateChannel = channel === "beta" ? "beta" : "stable";
836
- saveConfig(config);
824
+ saveDaemonSetting(config, "updateChannel", channel === "beta" ? "beta" : "stable", saveConfig);
837
825
  console.log("[daemon] Update channel:", config.updateChannel, "(web)");
838
826
  return { ok: true, updateChannel: config.updateChannel };
839
827
  },
840
828
  onSetPin: function (pin) {
841
- if (pin) {
842
- config.pinHash = generateAuthToken(pin);
843
- } else {
844
- config.pinHash = null;
845
- }
829
+ saveDaemonSetting(config, "pinHash", pin ? generateAuthToken(pin) : null, saveConfig);
846
830
  relay.setAuthToken(config.pinHash);
847
- saveConfig(config);
848
831
  console.log("[daemon] PIN", pin ? "set" : "removed", "(web)");
849
832
  return { ok: true, pinEnabled: !!config.pinHash };
850
833
  },
@@ -920,8 +903,7 @@ var relay = createServer({
920
903
  },
921
904
  onSetKeepAwake: function (value) {
922
905
  var want = !!value;
923
- config.keepAwake = want;
924
- saveConfig(config);
906
+ saveDaemonSetting(config, "keepAwake", want, saveConfig);
925
907
  if (want && !caffeinateProc && process.platform === "darwin") {
926
908
  try {
927
909
  var { spawn: spawnCaff } = require("child_process");
@@ -935,16 +917,6 @@ var relay = createServer({
935
917
  console.log("[daemon] Keep awake:", want, "(web)");
936
918
  return { ok: true, keepAwake: want };
937
919
  },
938
- onSetInheritGroups: function (value) {
939
- var want = !!value;
940
- config.inheritGroups = want;
941
- saveConfig(config);
942
- // Apply live so subsequent session/terminal spawns pick it up without a
943
- // daemon restart; already-running sessions keep their current groups.
944
- osUsersMod.setInheritGroups(want);
945
- console.log("[daemon] Inherit supplementary groups:", want, "(web)");
946
- return { ok: true, inheritGroups: want };
947
- },
948
920
  onShutdown: function () {
949
921
  console.log("[daemon] Shutdown requested via web UI");
950
922
  gracefulShutdown();
@@ -954,6 +926,7 @@ var relay = createServer({
954
926
  spawnAndRestart();
955
927
  },
956
928
  onSetProjectVisibility: function (slug, visibility) {
929
+ if (slug.indexOf("mate-") === 0) return {error:"Mates cannot be shared."};
957
930
  for (var i = 0; i < config.projects.length; i++) {
958
931
  if (config.projects[i].slug === slug) {
959
932
  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 };