clay-server 3.8.1 → 4.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ask-user-mcp-server.js +5 -119
- package/lib/builtin-mates.js +6 -6
- package/lib/claude-hook-installer.js +15 -0
- package/lib/clay-history-mcp-server.js +62 -297
- package/lib/daemon.js +1 -1
- package/lib/debate-model-selection.js +119 -0
- package/lib/home-debate-tool-policy.js +164 -0
- package/lib/home-response-notifications.js +26 -0
- package/lib/mate-creation-mcp-server.js +55 -0
- package/lib/mate-ready-creation.js +95 -0
- package/lib/mates.js +2 -0
- package/lib/notes.js +2 -1
- package/lib/project-ask-user.js +64 -0
- package/lib/project-capsule-catalog.js +101 -0
- package/lib/project-connection.js +4 -0
- package/lib/project-debate-proposal.js +133 -17
- package/lib/project-debate.js +214 -110
- package/lib/project-delegated-follow-up.js +84 -0
- package/lib/project-delegated-session.js +53 -0
- package/lib/project-filesystem.js +7 -9
- package/lib/project-knowledge.js +1 -0
- package/lib/project-mate-creation-proposal.js +120 -0
- package/lib/project-memory.js +14 -7
- package/lib/project-models.js +157 -32
- package/lib/project-session-notes.js +2 -2
- package/lib/project-session-pair.js +8 -8
- package/lib/project-sessions.js +66 -51
- package/lib/project-vendor-login.js +397 -0
- package/lib/project-worker-proposal.js +25 -25
- package/lib/project-workspace-query.js +144 -0
- package/lib/project.js +349 -102
- package/lib/public/app.js +85 -42
- package/lib/public/css/avatar-imprints.css +0 -1
- package/lib/public/css/base.css +4 -0
- package/lib/public/css/capsule-ui.css +389 -0
- package/lib/public/css/command-palette.css +306 -6
- package/lib/public/css/home-capsule-library.css +243 -0
- package/lib/public/css/home-capsule-source.css +192 -0
- package/lib/public/css/home-debate-live.css +146 -0
- package/lib/public/css/home-debate-planning.css +125 -0
- package/lib/public/css/home-debates-archive.css +281 -0
- package/lib/public/css/home-hub.css +896 -405
- package/lib/public/css/home-mate-creation.css +34 -0
- package/lib/public/css/home-mate-model-picker.css +131 -0
- package/lib/public/css/home-mate-settings.css +344 -0
- package/lib/public/css/home-session-actions.css +265 -0
- package/lib/public/css/home-sidebar.css +667 -0
- package/lib/public/css/icon-strip.css +1 -2
- package/lib/public/css/mates.css +7 -34
- package/lib/public/css/pane.css +1 -1
- package/lib/public/css/playbook.css +0 -80
- package/lib/public/css/sticky-notes.css +53 -117
- package/lib/public/css/tui-attention.css +0 -44
- package/lib/public/css/workspace-assignment.css +45 -0
- package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
- package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
- package/lib/public/index.html +93 -74
- package/lib/public/modules/app-connection.js +10 -1
- package/lib/public/modules/app-dm.js +43 -160
- package/lib/public/modules/app-home-hub.js +328 -585
- package/lib/public/modules/app-message-cards.js +242 -0
- package/lib/public/modules/app-message-router.js +84 -0
- package/lib/public/modules/app-messages.js +102 -37
- package/lib/public/modules/app-notifications.js +8 -119
- package/lib/public/modules/app-projects.js +9 -1
- package/lib/public/modules/app-rendering.js +21 -602
- package/lib/public/modules/avatar-imprint.js +11 -8
- package/lib/public/modules/chat-bubble-renderer.js +152 -0
- package/lib/public/modules/chat-render-runtime.js +198 -0
- package/lib/public/modules/command-palette.js +225 -436
- package/lib/public/modules/dm-render.js +82 -0
- package/lib/public/modules/home-capsule-creation-intent.js +31 -0
- package/lib/public/modules/home-capsule-library.js +146 -0
- package/lib/public/modules/home-capsule-source.js +383 -0
- package/lib/public/modules/home-chat-identity.js +35 -0
- package/lib/public/modules/home-chat-scroll.js +77 -0
- package/lib/public/modules/home-chat-stream-state.js +34 -0
- package/lib/public/modules/home-composer-focus.js +25 -0
- package/lib/public/modules/home-conversations-sheet.js +179 -0
- package/lib/public/modules/home-debate-activity.js +48 -0
- package/lib/public/modules/home-debate-controls.js +295 -0
- package/lib/public/modules/home-debate-launch.js +41 -0
- package/lib/public/modules/home-debate-live.js +284 -0
- package/lib/public/modules/home-debate-models.js +73 -0
- package/lib/public/modules/home-debate-planning.js +335 -0
- package/lib/public/modules/home-debates-archive.js +279 -0
- package/lib/public/modules/home-dock-resize.js +74 -0
- package/lib/public/modules/home-dock.js +446 -0
- package/lib/public/modules/home-mate-chat.js +496 -0
- package/lib/public/modules/home-mate-creation.js +109 -0
- package/lib/public/modules/home-mate-model-picker.js +281 -0
- package/lib/public/modules/home-mate-selection.js +38 -0
- package/lib/public/modules/home-mate-settings-menu.js +131 -0
- package/lib/public/modules/home-mate-settings.js +383 -0
- package/lib/public/modules/home-session-actions.js +373 -0
- package/lib/public/modules/home-session-model-confirmation.js +13 -0
- package/lib/public/modules/home-shell.js +9 -0
- package/lib/public/modules/home-sidebar-chat-list.js +63 -0
- package/lib/public/modules/home-sidebar.js +158 -0
- package/lib/public/modules/home-sub-surface.js +19 -0
- package/lib/public/modules/home-surface-boot.js +68 -0
- package/lib/public/modules/home-surface.js +141 -0
- package/lib/public/modules/home-tools.js +271 -0
- package/lib/public/modules/markdown.js +50 -2
- package/lib/public/modules/mate-management.js +86 -0
- package/lib/public/modules/mate-wizard.js +0 -7
- package/lib/public/modules/mention.js +17 -19
- package/lib/public/modules/pane-bridge.js +9 -0
- package/lib/public/modules/profile.js +4 -5
- package/lib/public/modules/project-settings.js +34 -28
- package/lib/public/modules/project-switcher.js +11 -5
- package/lib/public/modules/project-workspace-assignment.js +23 -0
- package/lib/public/modules/search-clay-chat.js +402 -0
- package/lib/public/modules/server-settings.js +14 -12
- package/lib/public/modules/session-actions.js +1 -1
- package/lib/public/modules/sidebar-mates.js +56 -552
- package/lib/public/modules/sidebar-mobile.js +7 -64
- package/lib/public/modules/sidebar-presence.js +37 -0
- package/lib/public/modules/split-pair-ui.js +13 -13
- package/lib/public/modules/split-view.js +7 -0
- package/lib/public/modules/sticky-notes.js +7 -6
- package/lib/public/modules/tool-input-composition.js +48 -0
- package/lib/public/modules/tool-llm-status.js +167 -0
- package/lib/public/modules/tool-renderer-advanced.js +285 -0
- package/lib/public/modules/tool-renderer-chart.js +158 -0
- package/lib/public/modules/tool-renderer-semantics.js +35 -0
- package/lib/public/modules/tool-renderer.js +464 -0
- package/lib/public/modules/tool-runtime.js +309 -0
- package/lib/public/modules/tool-ui-evaluator.js +97 -0
- package/lib/public/modules/tool-ui-tree.js +21 -0
- package/lib/public/modules/tui-attention.js +10 -0
- package/lib/public/modules/user-settings.js +16 -23
- package/lib/public/modules/vendor-login.js +287 -0
- package/lib/public/modules/worker-proposal.js +2 -2
- package/lib/public/modules/workspace-assignment-card.js +149 -0
- package/lib/public/modules/workspace-assignment-routing.js +4 -0
- package/lib/public/style.css +13 -2
- package/lib/runtime-env.js +65 -0
- package/lib/sdk-bridge.js +120 -32
- package/lib/sdk-message-processor.js +10 -6
- package/lib/server-home-capsule-creation.js +36 -0
- package/lib/server-home-chat-events.js +287 -0
- package/lib/server-home-chat.js +518 -0
- package/lib/server-home-clay-entry.js +65 -0
- package/lib/server-home-clay-session-links.js +26 -0
- package/lib/server-home-debate-planning.js +214 -0
- package/lib/server-home-debates.js +137 -0
- package/lib/server-home-mate-creation.js +261 -0
- package/lib/server-home-models.js +241 -0
- package/lib/server-home-preferences.js +61 -0
- package/lib/server-mates.js +37 -0
- package/lib/server-tools.js +377 -0
- package/lib/server.js +111 -59
- package/lib/session-pair-mcp-server.js +6 -6
- package/lib/sessions.js +18 -3
- package/lib/tool-capsule-source.js +142 -0
- package/lib/tool-control-mcp-server.js +126 -0
- package/lib/tool-llm.js +48 -0
- package/lib/tool-storage.js +77 -0
- package/lib/tool-ui-spec-advanced.js +82 -0
- package/lib/tool-ui-spec.js +261 -0
- package/lib/tools-registry.js +286 -0
- package/lib/users-home-dock-preferences.js +77 -0
- package/lib/users-home-surface-preferences.js +99 -0
- package/lib/users-preferences.js +4 -7
- package/lib/users.js +12 -0
- package/lib/whats-new-content.js +3 -3
- package/lib/workspace-assignment-service.js +420 -0
- package/lib/workspace-query-mcp-server.js +154 -0
- package/lib/workspace-query-service.js +492 -0
- package/lib/ws-schema.js +93 -12
- package/lib/yoke/adapters/acp.js +2 -1
- package/lib/yoke/adapters/antigravity.js +2 -1
- package/lib/yoke/adapters/claude-worker.js +11 -4
- package/lib/yoke/adapters/claude.js +73 -43
- package/lib/yoke/adapters/codex.js +40 -10
- package/lib/yoke/adapters/kiro.js +2 -1
- package/lib/yoke/claude-user-input.js +38 -0
- package/lib/yoke/codex-app-server.js +20 -2
- package/lib/yoke/codex-user-input.js +64 -0
- package/lib/yoke/complete-once.js +164 -0
- package/lib/yoke/index.js +5 -1
- package/lib/yoke/interface.js +19 -0
- package/lib/yoke/mcp-bridge-server.js +2 -6
- package/lib/yoke/user-input.js +254 -0
- package/package.json +3 -2
- package/lib/public/css/home-chat.css +0 -380
- package/lib/public/modules/home-chat.js +0 -438
- package/lib/server-clay-home.js +0 -245
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// Canonical safe declarative Capsule UI vocabulary and validation.
|
|
2
|
+
|
|
3
|
+
var advanced = require("./tool-ui-spec-advanced");
|
|
4
|
+
|
|
5
|
+
var UI_NODE_TYPES = [
|
|
6
|
+
"stack", "row", "text", "heading", "list", "table", "card", "section", "callout", "icon",
|
|
7
|
+
"input", "textarea", "select", "model-select", "checkbox", "button", "badge", "divider", "empty-state",
|
|
8
|
+
"form", "tabs", "tab", "dialog", "menu", "menu-item", "pagination", "chart", "switch", "case",
|
|
9
|
+
];
|
|
10
|
+
var UI_ICONS = [
|
|
11
|
+
"arrow-right", "book-open", "check", "circle-alert", "circle-check", "clock-3", "file-text",
|
|
12
|
+
"history", "info", "languages", "lightbulb", "message-square", "notebook-pen", "plus", "sparkles",
|
|
13
|
+
"trash-2", "triangle-alert", "wand-sparkles", "x", "chevron-left", "chevron-right", "chevron-down",
|
|
14
|
+
"menu", "more-horizontal", "bar-chart-3", "line-chart", "pie-chart",
|
|
15
|
+
];
|
|
16
|
+
var MAX_DEPTH = 14;
|
|
17
|
+
var MAX_NODES = 300;
|
|
18
|
+
var MAX_CHILDREN = 80;
|
|
19
|
+
|
|
20
|
+
var COMMON = {
|
|
21
|
+
tone: ["neutral", "accent", "info", "success", "warning", "danger"],
|
|
22
|
+
size: ["xs", "sm", "md", "lg"],
|
|
23
|
+
emphasis: ["subtle", "regular", "strong"],
|
|
24
|
+
};
|
|
25
|
+
var COMMON_TYPES = {
|
|
26
|
+
tone: ["text", "heading", "card", "callout", "icon", "button", "badge", "empty-state"],
|
|
27
|
+
size: ["text", "heading", "icon", "button", "badge", "input", "textarea", "select"],
|
|
28
|
+
emphasis: ["text", "heading", "button", "badge"],
|
|
29
|
+
};
|
|
30
|
+
var PROPS = {
|
|
31
|
+
stack: { gap: ["none", "xs", "sm", "md", "lg", "xl"], variant: ["plain", "inset"] },
|
|
32
|
+
row: {
|
|
33
|
+
gap: ["none", "xs", "sm", "md", "lg"], wrap: "boolean",
|
|
34
|
+
align: ["start", "center", "end", "baseline", "stretch"],
|
|
35
|
+
justify: ["start", "center", "end", "between"],
|
|
36
|
+
},
|
|
37
|
+
text: { text: "text", role: ["body", "muted", "caption", "strong", "output"] },
|
|
38
|
+
heading: { text: "text", level: { integer: [1, 6] }, role: ["display", "section"] },
|
|
39
|
+
list: { gap: ["none", "xs", "sm", "md", "lg"], variant: ["plain", "cards", "divided"], filter: "text", filterKey: "path", sortKey: "path", sortDirection: ["asc", "desc"], page: "number", pageSize: { integer: [1, 100] } },
|
|
40
|
+
table: { columns: "columns", filter: "text", filterKey: "path", sortKey: "path", sortDirection: ["asc", "desc"], page: "number", pageSize: { integer: [1, 100] } },
|
|
41
|
+
card: { variant: ["plain", "outlined", "raised", "callout", "output"], padding: ["none", "sm", "md", "lg"] },
|
|
42
|
+
section: { label: "text", accessibleLabel: "text", variant: ["plain", "inset"], gap: ["xs", "sm", "md", "lg"] },
|
|
43
|
+
callout: { title: "text", text: "text", icon: "icon", variant: ["soft", "outlined"] },
|
|
44
|
+
icon: { icon: "icon", label: "text", decorative: "boolean" },
|
|
45
|
+
input: { label: "text", hint: "text", placeholder: "text", error: "text", required: "bindingBoolean", disabled: "bindingBoolean", inputType: ["text", "search", "email", "url", "number", "range", "date", "time", "datetime-local", "tel"], validation: "validation" },
|
|
46
|
+
textarea: { label: "text", hint: "text", placeholder: "text", error: "text", required: "bindingBoolean", disabled: "bindingBoolean", rows: { integer: [2, 16] }, validation: "validation" },
|
|
47
|
+
select: { label: "text", hint: "text", error: "text", required: "bindingBoolean", disabled: "bindingBoolean", options: "options" },
|
|
48
|
+
"model-select": { label: "text", hint: "text", required: "boolean", disabled: "bindingBoolean" },
|
|
49
|
+
checkbox: { label: "text", hint: "text", error: "text", disabled: "bindingBoolean" },
|
|
50
|
+
button: {
|
|
51
|
+
label: "text", accessibleLabel: "text", variant: ["primary", "secondary", "ghost", "danger"],
|
|
52
|
+
icon: "icon", iconPosition: ["start", "end"], disabled: "bindingBoolean", args: "args",
|
|
53
|
+
},
|
|
54
|
+
badge: { text: "text", icon: "icon" },
|
|
55
|
+
divider: {},
|
|
56
|
+
"empty-state": { title: "text", text: "text", icon: "icon" },
|
|
57
|
+
form: { label: "text", submitLabel: "text" },
|
|
58
|
+
tabs: { label: "text", options: "options" },
|
|
59
|
+
tab: { label: "text", value: "scalar", disabled: "bindingBoolean" },
|
|
60
|
+
dialog: { label: "text", description: "text", open: "bindingBoolean", closeAction: "action" },
|
|
61
|
+
menu: { label: "text", triggerLabel: "text", options: "options" },
|
|
62
|
+
"menu-item": { label: "text", disabled: "bindingBoolean", args: "args" },
|
|
63
|
+
pagination: { label: "text", total: "number", pageSize: { integer: [1, 100] }, pageSizes: "numberOptions", pageAction: "action", pageSizeAction: "action" },
|
|
64
|
+
chart: { label: "text", kind: ["bar", "line", "donut", "progress", "metric"], categoryKey: "path", valueKey: "path", max: "positiveNumber", maxItems: { integer: [1, 50] } },
|
|
65
|
+
switch: {},
|
|
66
|
+
case: { value: "scalar", default: "boolean" },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function isPlainObject(value) {
|
|
70
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
71
|
+
var prototype = Object.getPrototypeOf(value);
|
|
72
|
+
return prototype === Object.prototype || prototype === null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateText(value, location) {
|
|
76
|
+
if (typeof value !== "string" || value.length > 2000) throw new Error(location + " must be text up to 2000 characters.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isSafePath(value, template) {
|
|
80
|
+
var pattern = template ? /^\$(?:state|item)(?:\.[A-Za-z0-9_-]+)+$/ : /^(?:\$(?:state|item)\.)?[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
|
|
81
|
+
if (typeof value !== "string" || !pattern.test(value)) return false;
|
|
82
|
+
var parts = value.replace(/^\$(?:state|item)\./, "").split(".");
|
|
83
|
+
return !parts.some(function (part) { return part === "__proto__" || part === "constructor" || part === "prototype"; });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateTemplateValue(value, location, depth) {
|
|
87
|
+
if (depth > 6) throw new Error(location + " is nested too deeply.");
|
|
88
|
+
if (typeof value === "string" && value.charAt(0) === "$" && !isSafePath(value, true)) throw new Error(location + " contains an unsafe state path.");
|
|
89
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return;
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
if (value.length > 50) throw new Error(location + " has too many values.");
|
|
92
|
+
for (var i = 0; i < value.length; i++) validateTemplateValue(value[i], location + "[" + i + "]", depth + 1);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!isPlainObject(value)) throw new Error(location + " must contain only safe JSON values.");
|
|
96
|
+
var keys = Object.keys(value);
|
|
97
|
+
if (keys.length > 50) throw new Error(location + " has too many fields.");
|
|
98
|
+
for (var ki = 0; ki < keys.length; ki++) {
|
|
99
|
+
if (keys[ki] === "__proto__" || keys[ki] === "constructor" || keys[ki] === "prototype") throw new Error(location + " contains an unsafe field.");
|
|
100
|
+
validateTemplateValue(value[keys[ki]], location + "." + keys[ki], depth + 1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function validateOptions(value, location) {
|
|
105
|
+
if (!Array.isArray(value) || value.length > 100) throw new Error(location + " must be an array of up to 100 options.");
|
|
106
|
+
for (var i = 0; i < value.length; i++) {
|
|
107
|
+
var option = value[i];
|
|
108
|
+
if (typeof option === "string" || typeof option === "number") continue;
|
|
109
|
+
if (!isPlainObject(option) || Object.keys(option).some(function (key) { return key !== "value" && key !== "label" && key !== "disabled"; })) {
|
|
110
|
+
throw new Error(location + " options must be scalar values or {value,label,disabled?} objects.");
|
|
111
|
+
}
|
|
112
|
+
if ((typeof option.value !== "string" && typeof option.value !== "number") || typeof option.label !== "string") {
|
|
113
|
+
throw new Error(location + " option value/label types are invalid.");
|
|
114
|
+
}
|
|
115
|
+
if (option.disabled !== undefined && typeof option.disabled !== "boolean") throw new Error(location + " option disabled must be boolean.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateColumns(value, location) {
|
|
120
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 20) throw new Error(location + " must contain 1 to 20 columns.");
|
|
121
|
+
for (var i = 0; i < value.length; i++) {
|
|
122
|
+
var column = value[i];
|
|
123
|
+
if (!isPlainObject(column) || !isSafePath(column.key, false) || (column.label !== undefined && typeof column.label !== "string")) {
|
|
124
|
+
throw new Error(location + " columns require a string key and optional label.");
|
|
125
|
+
}
|
|
126
|
+
if (Object.keys(column).some(function (key) { return key !== "key" && key !== "label"; })) throw new Error(location + " contains an unknown column property.");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function validateRule(value, rule, location) {
|
|
131
|
+
if (advanced.validateDynamic(value, rule, location, validateRule)) return;
|
|
132
|
+
if (Array.isArray(rule)) {
|
|
133
|
+
if (rule.indexOf(value) === -1) throw new Error(location + " must be one of: " + rule.join(", ") + ".");
|
|
134
|
+
} else if (rule === "text") validateText(value, location);
|
|
135
|
+
else if (rule === "boolean" && typeof value !== "boolean") throw new Error(location + " must be a boolean.");
|
|
136
|
+
else if (rule === "bindingBoolean" && typeof value !== "boolean" && !isSafePath(value, true)) throw new Error(location + " must be a boolean or state binding.");
|
|
137
|
+
else if (rule === "icon" && (typeof value !== "string" || UI_ICONS.indexOf(value) === -1)) throw new Error(location + " must be an allowed Lucide icon.");
|
|
138
|
+
else if (rule === "options") validateOptions(value, location);
|
|
139
|
+
else if (rule === "columns") validateColumns(value, location);
|
|
140
|
+
else if (rule === "numberOptions") {
|
|
141
|
+
if (!Array.isArray(value) || !value.length || value.length > 10 || value.some(function (entry) { return !Number.isInteger(entry) || entry < 1 || entry > 100; })) throw new Error(location + " must contain 1 to 10 page sizes from 1 to 100.");
|
|
142
|
+
}
|
|
143
|
+
else if (rule === "args") validateTemplateValue(value, location, 0);
|
|
144
|
+
else if (rule === "validation") advanced.validateValidation(value, location);
|
|
145
|
+
else if (rule === "scalar") {
|
|
146
|
+
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") throw new Error(location + " must be a scalar value.");
|
|
147
|
+
}
|
|
148
|
+
else if (rule === "number" && (typeof value !== "number" || !Number.isFinite(value))) throw new Error(location + " must be finite numeric data.");
|
|
149
|
+
else if (rule === "positiveNumber" && (typeof value !== "number" || !Number.isFinite(value) || value <= 0)) throw new Error(location + " must be positive finite numeric data.");
|
|
150
|
+
else if (rule === "action" && (typeof value !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(value))) throw new Error(location + " must be a safe action name.");
|
|
151
|
+
else if (rule === "path" && !isSafePath(value, false)) throw new Error(location + " must be a safe state path.");
|
|
152
|
+
else if (rule && rule.integer && (!Number.isInteger(value) || value < rule.integer[0] || value > rule.integer[1])) throw new Error(location + " must be an integer from " + rule.integer[0] + " to " + rule.integer[1] + ".");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function validateProps(node, location) {
|
|
156
|
+
var props = node.props || {};
|
|
157
|
+
if (!isPlainObject(props)) throw new Error("UI node props at " + location + " must be an object.");
|
|
158
|
+
var rules = Object.assign({}, PROPS[node.type]);
|
|
159
|
+
var commonKeys = Object.keys(COMMON_TYPES);
|
|
160
|
+
for (var ci = 0; ci < commonKeys.length; ci++) {
|
|
161
|
+
if (COMMON_TYPES[commonKeys[ci]].indexOf(node.type) !== -1) rules[commonKeys[ci]] = COMMON[commonKeys[ci]];
|
|
162
|
+
}
|
|
163
|
+
var keys = Object.keys(props);
|
|
164
|
+
for (var i = 0; i < keys.length; i++) {
|
|
165
|
+
if (!Object.prototype.hasOwnProperty.call(rules, keys[i])) throw new Error("Unknown UI property '" + keys[i] + "' at " + location + ".");
|
|
166
|
+
validateRule(props[keys[i]], rules[keys[i]], location + ".props." + keys[i]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function validateUiNode(node, location, context, depth) {
|
|
171
|
+
location = location || "root";
|
|
172
|
+
context = context || { count: 0, ids: Object.create(null) };
|
|
173
|
+
depth = depth || 0;
|
|
174
|
+
if (!isPlainObject(node)) throw new Error("UI node at " + location + " must be an object.");
|
|
175
|
+
context.count++;
|
|
176
|
+
if (context.count > MAX_NODES) throw new Error("UI tree exceeds " + MAX_NODES + " nodes.");
|
|
177
|
+
if (depth > MAX_DEPTH) throw new Error("UI tree exceeds depth " + MAX_DEPTH + ".");
|
|
178
|
+
var nodeKeys = Object.keys(node);
|
|
179
|
+
for (var nk = 0; nk < nodeKeys.length; nk++) {
|
|
180
|
+
if (["type", "id", "props", "children", "bind", "action", "when", "else"].indexOf(nodeKeys[nk]) === -1) throw new Error("Unknown UI node field '" + nodeKeys[nk] + "' at " + location + ".");
|
|
181
|
+
}
|
|
182
|
+
if (UI_NODE_TYPES.indexOf(node.type) === -1) throw new Error("Unknown UI node type '" + node.type + "' at " + location + ".");
|
|
183
|
+
if (node.id !== undefined) {
|
|
184
|
+
if (typeof node.id !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(node.id)) throw new Error("UI node ID at " + location + " must be a safe identifier.");
|
|
185
|
+
if (context.ids[node.id]) throw new Error("Duplicate UI node ID '" + node.id + "'.");
|
|
186
|
+
context.ids[node.id] = true;
|
|
187
|
+
}
|
|
188
|
+
if (node.bind !== undefined && !isSafePath(node.bind, false)) throw new Error("UI node bind at " + location + " must be a safe dot path.");
|
|
189
|
+
if (node.when !== undefined) advanced.validateCondition(node.when, "UI node when at " + location);
|
|
190
|
+
if (node.action !== undefined && (typeof node.action !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(node.action))) throw new Error("UI node action at " + location + " must be a safe action name.");
|
|
191
|
+
validateProps(node, location);
|
|
192
|
+
var props = node.props || {};
|
|
193
|
+
if (["input", "textarea", "select", "model-select", "checkbox"].indexOf(node.type) !== -1 && (!node.bind || !node.action)) throw new Error("Interactive " + node.type + " at " + location + " requires bind and action.");
|
|
194
|
+
if (["input", "textarea", "select", "model-select", "checkbox"].indexOf(node.type) !== -1 && !props.label && !node.id) throw new Error("Interactive " + node.type + " at " + location + " requires a label or ID.");
|
|
195
|
+
if (node.type === "button" && !node.action) throw new Error("Button at " + location + " requires an action.");
|
|
196
|
+
if (node.type === "button" && !props.label && !props.accessibleLabel) throw new Error("Button at " + location + " requires a label or accessibleLabel.");
|
|
197
|
+
if (node.type === "menu-item" && (!node.action || !props.label)) throw new Error("Menu item at " + location + " requires an action and label.");
|
|
198
|
+
if (node.type === "form" && !node.action) throw new Error("Form at " + location + " requires an action.");
|
|
199
|
+
if (node.type === "tabs" && (!node.bind || !node.action || (!(props.options) && !(node.children || []).length))) throw new Error("Tabs at " + location + " requires bind, action, and tabs or options.");
|
|
200
|
+
if (node.type === "tab" && props.value === undefined) throw new Error("Tab at " + location + " requires a value.");
|
|
201
|
+
if (node.type === "pagination" && (!node.bind || !props.pageAction || props.total === undefined)) throw new Error("Pagination at " + location + " requires bind, total, and pageAction.");
|
|
202
|
+
if (node.type === "chart") {
|
|
203
|
+
if (!node.bind || !props.label || !props.kind || !props.valueKey) throw new Error("Chart at " + location + " requires bind, label, kind, and valueKey.");
|
|
204
|
+
if (["bar", "line", "donut"].indexOf(props.kind) !== -1 && !props.categoryKey) throw new Error("Chart kind " + props.kind + " at " + location + " requires categoryKey.");
|
|
205
|
+
if (props.kind === "progress" && props.max === undefined) throw new Error("Progress chart at " + location + " requires a positive max.");
|
|
206
|
+
}
|
|
207
|
+
if (node.type === "case" && props.value !== undefined && props.value !== null && typeof props.value === "object") throw new Error("Case at " + location + " requires a static scalar value.");
|
|
208
|
+
if (node.type === "switch" && !node.bind) throw new Error("Switch at " + location + " requires a bind path.");
|
|
209
|
+
if (node.type === "dialog" && (!props.label || props.open === undefined || !props.closeAction)) throw new Error("Dialog at " + location + " requires label, open, and closeAction.");
|
|
210
|
+
if (node.type === "menu" && (!props.label || !props.triggerLabel || (!props.options && !(node.children || []).length) || (props.options && !node.action))) throw new Error("Menu at " + location + " requires label, triggerLabel, and menu items or options with an action.");
|
|
211
|
+
if (node.type === "icon" && !props.icon) throw new Error("Icon at " + location + " requires an icon property.");
|
|
212
|
+
if (node.type === "icon" && props.decorative !== true && !props.label) throw new Error("Non-decorative icon at " + location + " requires a label.");
|
|
213
|
+
if (node.children !== undefined && !Array.isArray(node.children)) throw new Error("UI node children at " + location + " must be an array.");
|
|
214
|
+
var children = node.children || [];
|
|
215
|
+
if (node.type === "tabs" && children.some(function (child) { return child.type !== "tab"; })) throw new Error("Tabs at " + location + " may contain only tab nodes.");
|
|
216
|
+
if (node.type === "menu" && children.some(function (child) { return child.type !== "menu-item"; })) throw new Error("Menu at " + location + " may contain only menu-item nodes.");
|
|
217
|
+
if (node.type === "switch" && children.some(function (child) { return child.type !== "case"; })) throw new Error("Switch at " + location + " may contain only case nodes.");
|
|
218
|
+
if (node.type === "switch") {
|
|
219
|
+
var defaultCases = children.filter(function (child) { return child.props && child.props.default === true; }).length;
|
|
220
|
+
if (defaultCases > 1) throw new Error("Switch at " + location + " has multiple default cases.");
|
|
221
|
+
}
|
|
222
|
+
if (children.length > MAX_CHILDREN) throw new Error("UI node at " + location + " has too many children.");
|
|
223
|
+
for (var i = 0; i < children.length; i++) validateUiNode(children[i], location + ".children[" + i + "]", context, depth + 1);
|
|
224
|
+
if (node.else !== undefined) validateUiNode(node.else, location + ".else", context, depth + 1);
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function hasNodeType(node, type) {
|
|
229
|
+
if (!node || typeof node !== "object") return false;
|
|
230
|
+
if (node.type === type) return true;
|
|
231
|
+
var children = Array.isArray(node.children) ? node.children : [];
|
|
232
|
+
for (var i = 0; i < children.length; i++) {
|
|
233
|
+
if (hasNodeType(children[i], type)) return true;
|
|
234
|
+
}
|
|
235
|
+
if (node.else && hasNodeType(node.else, type)) return true;
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function validateUiTreeForManifest(uiTree, manifest) {
|
|
240
|
+
validateUiNode(uiTree);
|
|
241
|
+
if (!hasNodeType(uiTree, "model-select")) return true;
|
|
242
|
+
var runtime = manifest && manifest.runtime ? manifest.runtime : "worker";
|
|
243
|
+
if (runtime !== "worker") throw new Error("model-select is available only to worker Capsules.");
|
|
244
|
+
var permissions = manifest && Array.isArray(manifest.permissions) ? manifest.permissions : [];
|
|
245
|
+
if (permissions.indexOf("llm") === -1) throw new Error("model-select requires the Capsule manifest llm permission.");
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function authoringDescription() {
|
|
250
|
+
return "uiTree uses safe JSON nodes {type,id?,props?,children?,bind?,action?,when?,else?}. Types: " + UI_NODE_TYPES.join(", ") + ". Presentation is semantic only: tone neutral/accent/info/success/warning/danger, bounded size/emphasis/layout/card/button/text/list roles, and allowed Lucide icons. when conditionally renders a node and accepts a bounded AST: all/any/not/equals/notEquals/in/gt/gte/lt/lte; else is its validated fallback, and switch contains static case children. Dynamic safe props and options use {$bind:\"state.path\",fallback?:value}; dynamic enum props also require an allowed $enum subset, and no expression executes. Bounded integer props may bind dynamically and are clamped by the host. Forms provide bounded field validation and bound inline errors; inputs support text/search/email/url/number/range/date/time/datetime-local/tel. Tabs and menus accept static children or bounded dynamic options; dialogs use host accessibility and keyboard behavior. Lists/tables accept bounded filter/sort/page props, including capped dynamic columns; pagination emits named page and page-size actions. Charts are fixed host-rendered bar/line/donut/progress/metric views with at most 50 points; donut uses at most 12 segments, progress requires a declared positive max, and every chart includes textual data. model-select is only for worker Capsules with llm permission and exposes only fast/standard/deep capability aliases; vendor model IDs are rejected. Arbitrary class, style, HTML, JavaScript, authored SVG, event attributes, vendor model IDs, and unknown props are rejected. Runtime options and collections are defensively capped.";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
UI_NODE_TYPES: UI_NODE_TYPES,
|
|
255
|
+
UI_ICONS: UI_ICONS,
|
|
256
|
+
MAX_DEPTH: MAX_DEPTH,
|
|
257
|
+
MAX_NODES: MAX_NODES,
|
|
258
|
+
validateUiNode: validateUiNode,
|
|
259
|
+
validateUiTreeForManifest: validateUiTreeForManifest,
|
|
260
|
+
authoringDescription: authoringDescription,
|
|
261
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var config = require("./config");
|
|
4
|
+
var toolUiSpec = require("./tool-ui-spec");
|
|
5
|
+
var toolCapsuleSource = require("./tool-capsule-source");
|
|
6
|
+
|
|
7
|
+
var CAPSULES_ROOT = path.join(__dirname, "capsules");
|
|
8
|
+
var SEED_MARKER = ".capsules-v7";
|
|
9
|
+
var REMOVED_BUILTIN_IDS = ["board", "translator", "scratchpad"];
|
|
10
|
+
var UI_NODE_TYPES = toolUiSpec.UI_NODE_TYPES;
|
|
11
|
+
var DISCOVERY_FIELD_MAX_LENGTH = 240;
|
|
12
|
+
|
|
13
|
+
function resolveToolsRoot(ctx) {
|
|
14
|
+
if (ctx && ctx.linuxUser) return path.join("/home", ctx.linuxUser, ".clay", "tools");
|
|
15
|
+
if (ctx && ctx.multiUser && ctx.userId) return path.join(config.CONFIG_DIR, "tools", ctx.userId);
|
|
16
|
+
return path.join(config.CONFIG_DIR, "tools");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validateToolId(id) {
|
|
20
|
+
if (typeof id !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) {
|
|
21
|
+
throw new Error("Tool ID must be a lowercase slug.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isTrustedServerRuntime(manifest) {
|
|
26
|
+
if (!manifest || manifest.runtime !== "server") return false;
|
|
27
|
+
var sourceManifest = path.join(CAPSULES_ROOT, manifest.id || "", "manifest.json");
|
|
28
|
+
try {
|
|
29
|
+
var shipped = JSON.parse(fs.readFileSync(sourceManifest, "utf8"));
|
|
30
|
+
return shipped.id === manifest.id && shipped.runtime === "server";
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function filesMatch(first, second) {
|
|
37
|
+
try {
|
|
38
|
+
return fs.readFileSync(first).equals(fs.readFileSync(second));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function manifestWithoutHydratedMetadata(manifest) {
|
|
45
|
+
var result = Object.assign({}, manifest);
|
|
46
|
+
delete result.modelAlias;
|
|
47
|
+
delete result.description;
|
|
48
|
+
delete result.useWhen;
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hydrateShippedManifestMetadata(directory, manifest) {
|
|
53
|
+
if (!manifest) return manifest;
|
|
54
|
+
var shippedDirectory = path.join(CAPSULES_ROOT, manifest.id || "");
|
|
55
|
+
var shippedManifestPath = path.join(shippedDirectory, "manifest.json");
|
|
56
|
+
try {
|
|
57
|
+
var shipped = JSON.parse(fs.readFileSync(shippedManifestPath, "utf8"));
|
|
58
|
+
if (shipped.id !== manifest.id) return manifest;
|
|
59
|
+
if (JSON.stringify(manifestWithoutHydratedMetadata(manifest)) !== JSON.stringify(manifestWithoutHydratedMetadata(shipped))) return manifest;
|
|
60
|
+
if (!filesMatch(path.join(directory, "ui.json"), path.join(shippedDirectory, "ui.json"))) return manifest;
|
|
61
|
+
if ((manifest.runtime || "worker") === "worker" && !filesMatch(path.join(directory, "logic.js"), path.join(shippedDirectory, "logic.js"))) return manifest;
|
|
62
|
+
var hydrated = Object.assign({}, manifest);
|
|
63
|
+
var fields = ["modelAlias", "description", "useWhen"];
|
|
64
|
+
for (var i = 0; i < fields.length; i++) {
|
|
65
|
+
if (hydrated[fields[i]] === undefined && shipped[fields[i]] !== undefined) hydrated[fields[i]] = shipped[fields[i]];
|
|
66
|
+
}
|
|
67
|
+
return hydrated;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return manifest;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateDiscoveryField(manifest, field, label) {
|
|
74
|
+
if (manifest[field] === undefined) return;
|
|
75
|
+
if (typeof manifest[field] !== "string" || !manifest[field].trim()) {
|
|
76
|
+
throw new Error("Tool " + label + " must be non-empty text.");
|
|
77
|
+
}
|
|
78
|
+
if (manifest[field] !== manifest[field].trim() || /[\u0000-\u001f\u007f]/.test(manifest[field])) {
|
|
79
|
+
throw new Error("Tool " + label + " must be a single trimmed line.");
|
|
80
|
+
}
|
|
81
|
+
if (manifest[field].length > DISCOVERY_FIELD_MAX_LENGTH) {
|
|
82
|
+
throw new Error("Tool " + label + " must be " + DISCOVERY_FIELD_MAX_LENGTH + " characters or fewer.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validateManifest(manifest, opts) {
|
|
87
|
+
opts = opts || {};
|
|
88
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
89
|
+
throw new Error("Tool manifest is required.");
|
|
90
|
+
}
|
|
91
|
+
validateToolId(manifest.id);
|
|
92
|
+
if (typeof manifest.name !== "string" || !manifest.name.trim()) {
|
|
93
|
+
throw new Error("Tool name is required.");
|
|
94
|
+
}
|
|
95
|
+
if (manifest.lucideIcon !== undefined && (typeof manifest.lucideIcon !== "string" || !/^[a-z0-9-]+$/.test(manifest.lucideIcon))) {
|
|
96
|
+
throw new Error("Tool icon must be a lowercase Lucide icon name.");
|
|
97
|
+
}
|
|
98
|
+
if (manifest.skills !== undefined && typeof manifest.skills !== "string") {
|
|
99
|
+
throw new Error("Tool skills must be markdown text.");
|
|
100
|
+
}
|
|
101
|
+
validateDiscoveryField(manifest, "description", "description");
|
|
102
|
+
validateDiscoveryField(manifest, "useWhen", "useWhen");
|
|
103
|
+
if (manifest.permissions !== undefined) {
|
|
104
|
+
if (!Array.isArray(manifest.permissions)) throw new Error("Tool permissions must be an array.");
|
|
105
|
+
for (var pi = 0; pi < manifest.permissions.length; pi++) {
|
|
106
|
+
if (manifest.permissions[pi] !== "llm") throw new Error("Unknown tool permission '" + manifest.permissions[pi] + "'.");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (manifest.modelAlias !== undefined && ["fast", "standard", "deep"].indexOf(manifest.modelAlias) === -1) {
|
|
110
|
+
throw new Error("Tool modelAlias must be fast, standard, or deep.");
|
|
111
|
+
}
|
|
112
|
+
var runtime = manifest.runtime || "worker";
|
|
113
|
+
if (runtime !== "worker" && runtime !== "server") {
|
|
114
|
+
throw new Error("Tool runtime must be 'worker' or 'server'.");
|
|
115
|
+
}
|
|
116
|
+
if (runtime === "server" && !opts.allowServerRuntime) {
|
|
117
|
+
throw new Error("Server-runtime capsules cannot be installed over WebSocket.");
|
|
118
|
+
}
|
|
119
|
+
if (runtime === "server" && !isTrustedServerRuntime(manifest)) {
|
|
120
|
+
throw new Error("Server runtime is reserved for shipped built-in capsules.");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
var validateUiNode = toolUiSpec.validateUiNode;
|
|
125
|
+
var validateUiTreeForManifest = toolUiSpec.validateUiTreeForManifest;
|
|
126
|
+
|
|
127
|
+
function toolDirectory(ctx, id) {
|
|
128
|
+
validateToolId(id);
|
|
129
|
+
return path.join(resolveToolsRoot(ctx), id);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function seedBuiltInCapsules(ctx) {
|
|
133
|
+
var root = resolveToolsRoot(ctx);
|
|
134
|
+
var marker = path.join(root, SEED_MARKER);
|
|
135
|
+
if (fs.existsSync(marker)) return;
|
|
136
|
+
fs.mkdirSync(root, { recursive: true });
|
|
137
|
+
for (var ri = 0; ri < REMOVED_BUILTIN_IDS.length; ri++) {
|
|
138
|
+
fs.rmSync(path.join(root, REMOVED_BUILTIN_IDS[ri]), { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
// Clay ships no built-in Capsules any more, so lib/capsules/ is absent from a
|
|
141
|
+
// fresh checkout and from the npm tarball (neither git nor npm carries an
|
|
142
|
+
// empty directory). Seeding still owns the removed-built-in migration above
|
|
143
|
+
// and the marker below, so an absent root means "nothing to copy" rather than
|
|
144
|
+
// a failure. Anything other than a missing root is still a real error.
|
|
145
|
+
var entries = [];
|
|
146
|
+
try {
|
|
147
|
+
entries = fs.readdirSync(CAPSULES_ROOT, { withFileTypes: true });
|
|
148
|
+
} catch (e) {
|
|
149
|
+
if (e.code !== "ENOENT") throw e;
|
|
150
|
+
}
|
|
151
|
+
var seeded = [];
|
|
152
|
+
for (var i = 0; i < entries.length; i++) {
|
|
153
|
+
if (!entries[i].isDirectory()) continue;
|
|
154
|
+
var destination = path.join(root, entries[i].name);
|
|
155
|
+
if (!fs.existsSync(destination)) {
|
|
156
|
+
fs.cpSync(path.join(CAPSULES_ROOT, entries[i].name), destination, { recursive: true });
|
|
157
|
+
seeded.push(entries[i].name);
|
|
158
|
+
} else {
|
|
159
|
+
var sourceFiles = fs.readdirSync(path.join(CAPSULES_ROOT, entries[i].name));
|
|
160
|
+
for (var fi = 0; fi < sourceFiles.length; fi++) {
|
|
161
|
+
var sourceFile = path.join(CAPSULES_ROOT, entries[i].name, sourceFiles[fi]);
|
|
162
|
+
var destinationFile = path.join(destination, sourceFiles[fi]);
|
|
163
|
+
if (!fs.existsSync(destinationFile)) fs.copyFileSync(sourceFile, destinationFile);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
fs.writeFileSync(marker, seeded.join("\n") + "\n", "utf8");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getTool(ctx, id) {
|
|
171
|
+
var directory = toolDirectory(ctx, id);
|
|
172
|
+
var manifestPath = path.join(directory, "manifest.json");
|
|
173
|
+
var logicPath = path.join(directory, "logic.js");
|
|
174
|
+
var uiPath = path.join(directory, "ui.json");
|
|
175
|
+
if (!fs.existsSync(manifestPath) || !fs.existsSync(uiPath)) return null;
|
|
176
|
+
var manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
177
|
+
var uiTree = JSON.parse(fs.readFileSync(uiPath, "utf8"));
|
|
178
|
+
validateManifest(manifest, { allowServerRuntime: true });
|
|
179
|
+
if (manifest.id !== id) throw new Error("Capsule folder name must match manifest ID.");
|
|
180
|
+
validateUiTreeForManifest(uiTree, manifest);
|
|
181
|
+
var runtime = manifest.runtime || "worker";
|
|
182
|
+
if (runtime === "worker" && !fs.existsSync(logicPath)) throw new Error("Worker capsule logic.js is required.");
|
|
183
|
+
manifest = hydrateShippedManifestMetadata(directory, manifest);
|
|
184
|
+
return {
|
|
185
|
+
manifest: Object.assign({}, manifest, { runtime: runtime }),
|
|
186
|
+
logicSource: runtime === "worker" ? fs.readFileSync(logicPath, "utf8") : null,
|
|
187
|
+
uiTree: uiTree,
|
|
188
|
+
metadata: toolCapsuleSource.sanitizedMetadata(resolveToolsRoot(ctx), id),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function listTools(ctx) {
|
|
193
|
+
seedBuiltInCapsules(ctx);
|
|
194
|
+
var root = resolveToolsRoot(ctx);
|
|
195
|
+
var entries = fs.readdirSync(root, { withFileTypes: true });
|
|
196
|
+
var manifests = [];
|
|
197
|
+
for (var i = 0; i < entries.length; i++) {
|
|
198
|
+
if (!entries[i].isDirectory()) continue;
|
|
199
|
+
if (entries[i].name.charAt(0) === ".") continue;
|
|
200
|
+
try {
|
|
201
|
+
var tool = getTool(ctx, entries[i].name);
|
|
202
|
+
if (tool) manifests.push(tool.manifest);
|
|
203
|
+
else manifests.push({ id: entries[i].name, error: "Capsule requires manifest.json and ui.json." });
|
|
204
|
+
} catch (e) {
|
|
205
|
+
manifests.push({ id: entries[i].name, error: e.message });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
manifests.sort(function (a, b) { return (a.name || a.id).localeCompare(b.name || b.id); });
|
|
209
|
+
return manifests;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function installTool(ctx, input) {
|
|
213
|
+
if (!input || typeof input !== "object") throw new Error("Tool installation payload is required.");
|
|
214
|
+
validateManifest(input.manifest, { allowServerRuntime: false });
|
|
215
|
+
if (typeof input.logicSource !== "string" || !input.logicSource.trim()) {
|
|
216
|
+
throw new Error("Tool logic source is required.");
|
|
217
|
+
}
|
|
218
|
+
validateUiTreeForManifest(input.uiTree, input.manifest);
|
|
219
|
+
var manifest = Object.assign({}, input.manifest, { name: input.manifest.name.trim(), runtime: "worker" });
|
|
220
|
+
var directory = toolDirectory(ctx, manifest.id);
|
|
221
|
+
if (fs.existsSync(directory)) throw new Error("A Capsule with this ID already exists. Read its source and use the update tool instead.");
|
|
222
|
+
fs.mkdirSync(resolveToolsRoot(ctx), { recursive: true });
|
|
223
|
+
fs.mkdirSync(directory);
|
|
224
|
+
try {
|
|
225
|
+
fs.writeFileSync(path.join(directory, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
226
|
+
fs.writeFileSync(path.join(directory, "logic.js"), input.logicSource, "utf8");
|
|
227
|
+
fs.writeFileSync(path.join(directory, "ui.json"), JSON.stringify(input.uiTree, null, 2) + "\n", "utf8");
|
|
228
|
+
} catch (error) {
|
|
229
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
return getTool(ctx, manifest.id);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function getToolSource(ctx, id) {
|
|
236
|
+
var tool = getTool(ctx, id);
|
|
237
|
+
if (!tool) throw new Error("Tool not found.");
|
|
238
|
+
return toolCapsuleSource.readSource(toolDirectory(ctx, id));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getToolMetadata(ctx, id) {
|
|
242
|
+
if (!getTool(ctx, id)) throw new Error("Tool not found.");
|
|
243
|
+
return toolCapsuleSource.sanitizedMetadata(resolveToolsRoot(ctx), id);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function setMateEditingAllowed(ctx, id, allowed) {
|
|
247
|
+
if (!getTool(ctx, id)) throw new Error("Tool not found.");
|
|
248
|
+
return toolCapsuleSource.setMateEditingAllowed(resolveToolsRoot(ctx), id, allowed);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function updateTool(ctx, id, input) {
|
|
252
|
+
var tool = getTool(ctx, id);
|
|
253
|
+
if (!tool) throw new Error("Tool not found.");
|
|
254
|
+
toolCapsuleSource.updateSource(toolDirectory(ctx, id), input, {
|
|
255
|
+
validateManifest: validateManifest,
|
|
256
|
+
validateUiNode: function (uiTree) { return validateUiTreeForManifest(uiTree, input.manifest); },
|
|
257
|
+
});
|
|
258
|
+
return getTool(ctx, id);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function removeTool(ctx, id) {
|
|
262
|
+
var directory = toolDirectory(ctx, id);
|
|
263
|
+
if (!fs.existsSync(directory)) return false;
|
|
264
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
265
|
+
toolCapsuleSource.removeMetadata(resolveToolsRoot(ctx), id);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
CAPSULES_ROOT: CAPSULES_ROOT,
|
|
271
|
+
UI_NODE_TYPES: UI_NODE_TYPES,
|
|
272
|
+
resolveToolsRoot: resolveToolsRoot,
|
|
273
|
+
validateToolId: validateToolId,
|
|
274
|
+
validateManifest: validateManifest,
|
|
275
|
+
validateUiNode: validateUiNode,
|
|
276
|
+
validateUiTreeForManifest: validateUiTreeForManifest,
|
|
277
|
+
seedBuiltInCapsules: seedBuiltInCapsules,
|
|
278
|
+
listTools: listTools,
|
|
279
|
+
getTool: getTool,
|
|
280
|
+
installTool: installTool,
|
|
281
|
+
getToolSource: getToolSource,
|
|
282
|
+
getToolMetadata: getToolMetadata,
|
|
283
|
+
setMateEditingAllowed: setMateEditingAllowed,
|
|
284
|
+
updateTool: updateTool,
|
|
285
|
+
removeTool: removeTool,
|
|
286
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Per-user persistence for the home tool dock.
|
|
2
|
+
|
|
3
|
+
function attachHomeDockPreferences(deps) {
|
|
4
|
+
var loadUsers = deps.loadUsers;
|
|
5
|
+
var saveUsers = deps.saveUsers;
|
|
6
|
+
|
|
7
|
+
function defaults() {
|
|
8
|
+
return { dockOpen: false, dockFocus: false, dockWidth: null, activeToolId: null };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalize(value) {
|
|
12
|
+
var source = value && typeof value === "object" ? value : {};
|
|
13
|
+
var width = typeof source.dockWidth === "number" && Number.isFinite(source.dockWidth)
|
|
14
|
+
? Math.max(420, Math.min(1600, Math.round(source.dockWidth)))
|
|
15
|
+
: null;
|
|
16
|
+
var toolId = typeof source.activeToolId === "string" && /^[a-z0-9][a-z0-9_-]{0,63}$/.test(source.activeToolId)
|
|
17
|
+
? source.activeToolId
|
|
18
|
+
: null;
|
|
19
|
+
return {
|
|
20
|
+
dockOpen: source.dockOpen === true,
|
|
21
|
+
dockFocus: source.dockOpen === true && source.dockFocus === true,
|
|
22
|
+
dockWidth: width,
|
|
23
|
+
activeToolId: toolId,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadSingleUserPreference() {
|
|
28
|
+
try {
|
|
29
|
+
var config = require("./config");
|
|
30
|
+
var current = config.loadConfig() || {};
|
|
31
|
+
return normalize(current.homeDockPreference);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return defaults();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveSingleUserPreference(preference) {
|
|
38
|
+
try {
|
|
39
|
+
var config = require("./config");
|
|
40
|
+
var current = config.loadConfig() || {};
|
|
41
|
+
config.saveConfig(Object.assign({}, current, { homeDockPreference: preference }));
|
|
42
|
+
return { ok: true, preference: preference };
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return { error: "Unable to save home dock preference" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getHomeDockPreference(userId) {
|
|
49
|
+
var data = loadUsers();
|
|
50
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
51
|
+
if (data.users[i].id === userId) return normalize(data.users[i].homeDockPreference);
|
|
52
|
+
}
|
|
53
|
+
if (userId === "default") return loadSingleUserPreference();
|
|
54
|
+
return defaults();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function setHomeDockPreference(userId, patch) {
|
|
58
|
+
var current = getHomeDockPreference(userId);
|
|
59
|
+
var next = normalize(Object.assign({}, current, patch || {}));
|
|
60
|
+
var data = loadUsers();
|
|
61
|
+
for (var i = 0; i < data.users.length; i++) {
|
|
62
|
+
if (data.users[i].id !== userId) continue;
|
|
63
|
+
data.users[i].homeDockPreference = next;
|
|
64
|
+
saveUsers(data);
|
|
65
|
+
return { ok: true, preference: next };
|
|
66
|
+
}
|
|
67
|
+
if (userId === "default") return saveSingleUserPreference(next);
|
|
68
|
+
return { error: "User not found" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
getHomeDockPreference: getHomeDockPreference,
|
|
73
|
+
setHomeDockPreference: setHomeDockPreference,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { attachHomeDockPreferences: attachHomeDockPreferences };
|