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.
- package/LICENSES/README.md +4 -0
- package/LICENSES/dicebear-avatars.md +16 -0
- package/bin/cli.js +9 -91
- package/lib/daemon-project-access.js +7 -1
- package/lib/daemon-settings.js +11 -0
- package/lib/daemon.js +10 -37
- package/lib/dicebear-avatar.js +62 -0
- package/lib/knowledge-db-service.js +175 -0
- package/lib/knowledge-db-validation.js +0 -0
- package/lib/knowledge-document-index.js +49 -0
- package/lib/knowledge-draft-store.js +56 -0
- package/lib/knowledge-file-store.js +199 -0
- package/lib/knowledge-markdown.js +128 -0
- package/lib/linear-store.js +35 -8
- package/lib/mate-knowledge-mcp-server.js +29 -6
- package/lib/mate-knowledge-service.js +23 -2
- package/lib/mate-knowledge-sync.js +36 -21
- package/lib/mate-workspace-color.js +4 -0
- package/lib/mates.js +18 -0
- package/lib/os-users.js +5 -16
- package/lib/project-connection.js +36 -9
- package/lib/project-debate-proposal.js +6 -0
- package/lib/project-debate-workspace.js +43 -0
- package/lib/project-debate.js +1 -0
- package/lib/project-filesystem.js +21 -39
- package/lib/project-knowledge.js +315 -136
- package/lib/project-mate-creation-proposal.js +1 -0
- package/lib/project-mate-interaction.js +2 -2
- package/lib/project-server-settings.js +31 -0
- package/lib/project-sessions.js +92 -182
- package/lib/project-user-knowledge.js +60 -0
- package/lib/project.js +59 -18
- package/lib/public/app.js +79 -71
- package/lib/public/clay-studio-wordmark-bold.svg +20 -0
- package/lib/public/css/clay-workspace.css +105 -0
- package/lib/public/css/debates-workbench.css +47 -0
- package/lib/public/css/home-debate-live.css +26 -0
- package/lib/public/css/home-hub.css +3 -1
- package/lib/public/css/home-mate-model-picker.css +15 -131
- package/lib/public/css/home-mate-settings.css +168 -16
- package/lib/public/css/icon-strip.css +249 -32
- package/lib/public/css/input.css +3 -1
- package/lib/public/css/mate-conversation.css +176 -0
- package/lib/public/css/mate-knowledge-workbench.css +118 -0
- package/lib/public/css/mate-workspace-color.css +16 -0
- package/lib/public/css/mates.css +28 -28
- package/lib/public/css/messages.css +1 -114
- package/lib/public/css/mobile-nav.css +16 -16
- package/lib/public/css/overlays.css +8 -0
- package/lib/public/css/project-logs.css +7 -0
- package/lib/public/css/project-mate-creation.css +105 -0
- package/lib/public/css/project-settings-modal.css +56 -0
- package/lib/public/css/right-workbench.css +3 -3
- package/lib/public/css/server-settings-system.css +125 -0
- package/lib/public/css/session-folders.css +80 -21
- package/lib/public/css/session-github.css +25 -16
- package/lib/public/css/settings-ui.css +247 -22
- package/lib/public/css/shared-browser.css +2 -0
- package/lib/public/css/sidebar.css +183 -107
- package/lib/public/css/title-bar.css +156 -1
- package/lib/public/css/worker-delegation.css +47 -0
- package/lib/public/css/worker-proposal.css +72 -150
- package/lib/public/css/you.css +74 -0
- package/lib/public/index.html +203 -547
- package/lib/public/modules/account-island.js +232 -0
- package/lib/public/modules/admin-settings-email.js +197 -0
- package/lib/public/modules/admin-settings-invitations.js +183 -0
- package/lib/public/modules/admin-settings-projects.js +198 -0
- package/lib/public/modules/admin-settings-ui.js +100 -0
- package/lib/public/modules/admin-settings-users.js +331 -0
- package/lib/public/modules/admin.js +19 -949
- package/lib/public/modules/app-cursors.js +19 -43
- package/lib/public/modules/app-debate-ui.js +4 -3
- package/lib/public/modules/app-dm.js +0 -2
- package/lib/public/modules/app-header.js +106 -0
- package/lib/public/modules/app-message-router.js +12 -3
- package/lib/public/modules/app-messages.js +47 -61
- package/lib/public/modules/app-notifications.js +4 -7
- package/lib/public/modules/app-projects.js +5 -38
- package/lib/public/modules/app-rate-limit.js +70 -12
- package/lib/public/modules/app-rendering.js +12 -13
- package/lib/public/modules/avatar.js +25 -9
- package/lib/public/modules/chat-render-runtime.js +16 -4
- package/lib/public/modules/command-palette.js +2 -15
- package/lib/public/modules/context-sources.js +27 -18
- package/lib/public/modules/cursor-sharing-preference.js +3 -3
- package/lib/public/modules/debate-controls.js +299 -0
- package/lib/public/modules/debate.js +2 -0
- package/lib/public/modules/debates-workbench.js +339 -0
- package/lib/public/modules/default-ai.js +88 -65
- package/lib/public/modules/header-tui-font.js +10 -20
- package/lib/public/modules/home-debate-controls.js +18 -289
- package/lib/public/modules/home-mate-avatar-editor.js +228 -0
- package/lib/public/modules/home-mate-color-picker.js +66 -0
- package/lib/public/modules/home-mate-creation.js +1 -1
- package/lib/public/modules/home-mate-instructions.js +90 -0
- package/lib/public/modules/home-mate-model-picker.js +80 -271
- package/lib/public/modules/home-mate-profile-editor.js +72 -0
- package/lib/public/modules/home-mate-settings.js +118 -125
- package/lib/public/modules/home-shell.js +3 -0
- package/lib/public/modules/home-surface-boot.js +1 -9
- package/lib/public/modules/icon-strip-tooltip.js +95 -0
- package/lib/public/modules/knowledge-db-edit-state.js +35 -0
- package/lib/public/modules/knowledge-db-workbench.js +234 -0
- package/lib/public/modules/knowledge-history-state.js +55 -0
- package/lib/public/modules/knowledge-markdown-editor.js +57 -0
- package/lib/public/modules/knowledge-preview-links.js +70 -0
- package/lib/public/modules/knowledge-workbench-shell.js +21 -0
- package/lib/public/modules/linear-settings.js +3 -2
- package/lib/public/modules/markdown.js +5 -6
- package/lib/public/modules/mate-knowledge-workbench-state.js +174 -0
- package/lib/public/modules/mate-knowledge-workbench.js +500 -0
- package/lib/public/modules/mate-knowledge.js +11 -3
- package/lib/public/modules/mate-management.js +54 -10
- package/lib/public/modules/mate-sidebar.js +11 -1
- package/lib/public/modules/mate-workspace-color.js +33 -0
- package/lib/public/modules/mention.js +5 -13
- package/lib/public/modules/notification-settings.js +81 -0
- package/lib/public/modules/notifications.js +44 -28
- package/lib/public/modules/personal-settings-dialog.js +51 -0
- package/lib/public/modules/profile.js +66 -24
- package/lib/public/modules/project-chat-identity.js +79 -0
- package/lib/public/modules/project-debate-planning-workspace.js +250 -0
- package/lib/public/modules/project-debate-workspace.js +185 -0
- package/lib/public/modules/project-header-identity.js +127 -0
- package/lib/public/modules/project-logs-render.js +9 -4
- package/lib/public/modules/project-mate-creation-workspace.js +414 -0
- package/lib/public/modules/project-mate-navigation.js +238 -0
- package/lib/public/modules/project-session-navigation.js +29 -0
- package/lib/public/modules/project-settings-environment.js +106 -0
- package/lib/public/modules/project-settings.js +72 -628
- package/lib/public/modules/project-switcher.js +6 -8
- package/lib/public/modules/projects-hub-view.js +5 -6
- package/lib/public/modules/projects-hub.js +2 -6
- package/lib/public/modules/qrcode.js +27 -8
- package/lib/public/modules/search-clay-chat.js +8 -11
- package/lib/public/modules/server-settings-controls.js +316 -0
- package/lib/public/modules/server-settings-environment.js +194 -0
- package/lib/public/modules/server-settings-navigation.js +39 -0
- package/lib/public/modules/server-settings-state.js +54 -0
- package/lib/public/modules/server-settings.js +116 -788
- package/lib/public/modules/server-storage-settings.js +84 -0
- package/lib/public/modules/session-activity-tooltip.js +12 -0
- package/lib/public/modules/session-create-form.js +86 -14
- package/lib/public/modules/session-create.js +50 -7
- package/lib/public/modules/session-github.js +2 -1
- package/lib/public/modules/session-list-visibility.js +41 -0
- package/lib/public/modules/session-presence-ui.js +3 -0
- package/lib/public/modules/session-title-marquee.js +51 -0
- package/lib/public/modules/settings-env-values.js +34 -0
- package/lib/public/modules/settings-select.js +121 -0
- package/lib/public/modules/shared-browser.js +10 -7
- package/lib/public/modules/sidebar-mates.js +3 -50
- package/lib/public/modules/sidebar-mobile.js +63 -60
- package/lib/public/modules/sidebar-projects.js +84 -97
- package/lib/public/modules/sidebar-session-folders-ui.js +2 -2
- package/lib/public/modules/sidebar-session-hierarchy.js +149 -70
- package/lib/public/modules/sidebar-sessions.js +49 -50
- package/lib/public/modules/sidebar.js +11 -13
- package/lib/public/modules/stt-picker.js +14 -5
- package/lib/public/modules/terminal-prefs.js +22 -0
- package/lib/public/modules/theme.js +88 -52
- package/lib/public/modules/thinking-lifecycle.js +7 -6
- package/lib/public/modules/tool-palette-order.js +4 -3
- package/lib/public/modules/tool-palette.js +13 -3
- package/lib/public/modules/tools.js +22 -22
- package/lib/public/modules/user-settings-account.js +318 -0
- package/lib/public/modules/user-settings-appearance.js +55 -0
- package/lib/public/modules/user-settings-editors.js +24 -0
- package/lib/public/modules/user-settings-integrations.js +104 -0
- package/lib/public/modules/user-settings-layout.js +80 -0
- package/lib/public/modules/user-settings-preferences.js +48 -0
- package/lib/public/modules/user-settings-profile.js +53 -0
- package/lib/public/modules/user-settings-themes.js +38 -0
- package/lib/public/modules/user-settings-voice.js +75 -0
- package/lib/public/modules/user-settings.js +53 -480
- package/lib/public/modules/worker-proposal.js +42 -8
- package/lib/public/modules/worker-runtime-picker.js +115 -0
- package/lib/public/modules/you-activity.js +121 -0
- package/lib/public/modules/you.js +251 -0
- package/lib/public/style.css +20 -8
- package/lib/public/vendor/codemirror/LICENSE +21 -0
- package/lib/public/vendor/codemirror/codemirror.css +344 -0
- package/lib/public/vendor/codemirror/codemirror.js +9884 -0
- package/lib/public/vendor/codemirror/continuelist.js +101 -0
- package/lib/public/vendor/codemirror/markdown.js +886 -0
- package/lib/public/vendor/codemirror/show-hint.css +37 -0
- package/lib/public/vendor/codemirror/show-hint.js +523 -0
- package/lib/sdk-bridge.js +15 -3
- package/lib/sdk-message-processor.js +16 -4
- package/lib/sdk-quota-snapshot.js +37 -0
- package/lib/server-admin.js +11 -0
- package/lib/server-appearance-settings.js +71 -0
- package/lib/server-home-chat-events.js +1 -1
- package/lib/server-home-chat.js +25 -3
- package/lib/server-home-debate-planning.js +4 -1
- package/lib/server-home-debates.js +2 -0
- package/lib/server-home-mate-creation.js +4 -1
- package/lib/server-linear.js +14 -8
- package/lib/server-mate-instructions.js +44 -0
- package/lib/server-mates.js +12 -3
- package/lib/server-notification-settings.js +67 -0
- package/lib/server-palette.js +1 -1
- package/lib/server-settings.js +69 -1
- package/lib/server-storage.js +56 -0
- package/lib/server-user-knowledge.js +119 -0
- package/lib/server.js +31 -3
- package/lib/session-visibility.js +7 -2
- package/lib/sessions.js +34 -9
- package/lib/startup-projects.js +40 -0
- package/lib/storage-usage.js +127 -0
- package/lib/user-knowledge-service.js +167 -0
- package/lib/users-permissions.js +7 -0
- package/lib/users-preferences.js +0 -74
- package/lib/users-project-mate-filter-preferences.js +39 -0
- package/lib/users.js +6 -4
- package/lib/workspace-query-access.js +4 -1
- package/lib/ws-schema.js +52 -4
- package/lib/yoke/adapters/codex.js +8 -45
- package/lib/yoke/codex-model-catalog.js +40 -0
- package/lib/yoke/codex-rate-limits.js +26 -0
- package/package.json +12 -3
package/LICENSES/README.md
CHANGED
|
@@ -6,6 +6,10 @@ Preexisting MIT-covered portions retain the grants and applicable copyright and
|
|
|
6
6
|
|
|
7
7
|
Third-party components and assets retain their own licenses and notices. In particular, the bundled Source Serif 4 font remains under the [SIL Open Font License 1.1](../lib/public/fonts/source-serif-4/LICENSE.md).
|
|
8
8
|
|
|
9
|
+
The locally generated historical avatar styles retain their DiceBear and
|
|
10
|
+
original artist licenses and attributions, summarized in
|
|
11
|
+
[dicebear-avatars.md](dicebear-avatars.md).
|
|
12
|
+
|
|
9
13
|
The Salt layout, control geometry, text decoration, color map and diagram chrome
|
|
10
14
|
in `lib/salt-*.js` and `lib/assets/salt-colors.json` are adapted from PlantUML
|
|
11
15
|
1.2026.8, copyright 2009–2025 Arnaud Roques, under
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# DiceBear avatar notices
|
|
2
|
+
|
|
3
|
+
Clay locally generates its seven historical avatar choices with DiceBear 9.4.3.
|
|
4
|
+
The generator code is MIT-licensed, copyright Florian Körner. Each design keeps
|
|
5
|
+
its upstream license and attribution:
|
|
6
|
+
|
|
7
|
+
- Thumbs — DiceBear, CC0 1.0.
|
|
8
|
+
- Bottts — Pablo Stanley, free for personal and commercial use.
|
|
9
|
+
- Pixel Art — DiceBear, CC0 1.0.
|
|
10
|
+
- Adventurer — Lisa Wischofsky, CC BY 4.0.
|
|
11
|
+
- Avatar Illustration System (Micah) — Micah Lanier, CC BY 4.0.
|
|
12
|
+
- Fun Emoji Set — Davis Uche, CC BY 4.0.
|
|
13
|
+
- Bootstrap Icons — The Bootstrap Authors, MIT.
|
|
14
|
+
|
|
15
|
+
The complete notices and license links are distributed with their respective
|
|
16
|
+
`@dicebear/*` packages. Upstream: https://www.dicebear.com/
|
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
|
|
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
|
|
1463
|
-
|
|
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
|
|
1638
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 };
|