clay-server 3.8.1 → 4.0.0-beta.10
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/background-task-timing.js +105 -0
- 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 +23 -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/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +224 -0
- package/lib/mate-creation-mcp-server.js +55 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -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 +17 -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 +23 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-mcp-server.js +290 -0
- package/lib/project-logs-query.js +167 -0
- package/lib/project-logs-root.js +67 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +357 -0
- package/lib/project-logs-snapshot.js +202 -0
- package/lib/project-logs-store.js +498 -0
- package/lib/project-logs-versioning.js +128 -0
- package/lib/project-logs.js +347 -0
- package/lib/project-mate-creation-proposal.js +120 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +36 -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 +414 -0
- package/lib/project-worker-proposal.js +25 -25
- package/lib/project-workspace-query.js +144 -0
- package/lib/project.js +433 -102
- package/lib/public/app.js +109 -48
- 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/input.css +79 -25
- 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/project-logs.css +234 -0
- package/lib/public/css/scheduler.css +1 -1
- 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 +96 -74
- package/lib/public/modules/app-connection.js +100 -5
- 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 +124 -38
- package/lib/public/modules/app-notifications.js +8 -119
- package/lib/public/modules/app-panels.js +43 -2
- 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/background-tasks-ui.js +139 -16
- 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 +165 -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-sidebar.js +0 -8
- 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-logs-ambient.js +238 -0
- package/lib/public/modules/project-logs-render.js +359 -0
- package/lib/public/modules/project-logs.js +328 -0
- 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/scheduler.js +17 -3
- 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 +12 -73
- package/lib/public/modules/sidebar-presence.js +37 -0
- package/lib/public/modules/sidebar-sessions.js +25 -1
- 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 +106 -53
- 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-palette.js +76 -4
- 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 +14 -2
- package/lib/runtime-env.js +65 -0
- package/lib/sdk-bridge.js +120 -32
- package/lib/sdk-message-processor.js +34 -13
- 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 +166 -59
- package/lib/session-pair-mcp-server.js +6 -6
- package/lib/sessions.js +22 -4
- 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 +107 -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 +141 -46
- package/lib/yoke/adapters/codex.js +79 -11
- 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-background-tasks.js +8 -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 +4 -3
- 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,126 @@
|
|
|
1
|
+
var z;
|
|
2
|
+
try { z = require("zod"); } catch (e) { z = null; }
|
|
3
|
+
var toolUiSpec = require("./tool-ui-spec");
|
|
4
|
+
|
|
5
|
+
function textResult(value) {
|
|
6
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function errorResult(error) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: "Error: " + (error && error.message ? error.message : String(error)) }],
|
|
12
|
+
isError: true,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function run(handler) {
|
|
17
|
+
return Promise.resolve().then(handler).then(textResult).catch(errorResult);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function schemas() {
|
|
21
|
+
if (!z) return { list: {}, tool: {}, act: {}, set: {}, install: {}, update: {}, uninstall: {} };
|
|
22
|
+
return {
|
|
23
|
+
list: {},
|
|
24
|
+
tool: { toolId: z.string().min(1).describe("Installed capsule ID.") },
|
|
25
|
+
act: {
|
|
26
|
+
toolId: z.string().min(1),
|
|
27
|
+
actionId: z.string().min(1),
|
|
28
|
+
args: z.record(z.string(), z.any()).optional().describe("Arguments for the tool action."),
|
|
29
|
+
},
|
|
30
|
+
set: {
|
|
31
|
+
toolId: z.string().min(1),
|
|
32
|
+
controlId: z.string().min(1),
|
|
33
|
+
value: z.any().describe("New value for the named control."),
|
|
34
|
+
},
|
|
35
|
+
install: {
|
|
36
|
+
manifest: z.record(z.string(), z.any()),
|
|
37
|
+
uiTree: z.record(z.string(), z.any()),
|
|
38
|
+
logicSource: z.string().min(1),
|
|
39
|
+
},
|
|
40
|
+
update: {
|
|
41
|
+
toolId: z.string().min(1),
|
|
42
|
+
baseRevision: z.string().min(1),
|
|
43
|
+
manifest: z.record(z.string(), z.any()),
|
|
44
|
+
uiTree: z.record(z.string(), z.any()),
|
|
45
|
+
logicSource: z.string().min(1),
|
|
46
|
+
},
|
|
47
|
+
uninstall: { toolId: z.string().min(1).describe("Installed worker capsule ID to remove.") },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var INSTALL_DESCRIPTION = [
|
|
52
|
+
"Install a new user-owned worker capsule. Existing Capsule IDs are rejected; read source and use clay_tool_update instead. This requires user approval.",
|
|
53
|
+
"manifest: {id: lowercase-slug, name, description?: concise single-line purpose, useWhen?: concise single-line trigger, lucideIcon?, runtime:'worker', permissions?:['llm'], initialAction?, skills?: markdown detailed procedural recipe}.",
|
|
54
|
+
"New Capsules should provide description and useWhen so Mates can discover when the Capsule is useful; keep the full operating procedure in skills.",
|
|
55
|
+
toolUiSpec.authoringDescription(),
|
|
56
|
+
"logicSource must define: var tool = { initialState: {...}, actions: { name: function (state, args, api) { ... return newState; } } }. Async actions may call api.setState(nextState) to publish intermediate busy/progress UI state before returning the complete final state. An uncaught action error restores the pre-action UI state; catch errors and return an explicit final state to show inline error feedback. Actions may use api.storage.list/get/put/delete/query, api.llm.complete({system?,prompt,model?}) when manifest permissions includes llm, and api.callerId. model is only fast|standard|deep. No DOM, imports, network globals, or vendor model names.",
|
|
57
|
+
].join(" ");
|
|
58
|
+
|
|
59
|
+
function getToolDefs(handlers) {
|
|
60
|
+
var shape = schemas();
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
name: "clay_tool_list",
|
|
64
|
+
description: "List installed Capsules with concise discovery metadata, runtimes, and their full detailed usage recipes.",
|
|
65
|
+
inputSchema: shape.list,
|
|
66
|
+
handler: function () { return run(function () { return handlers.list(); }); },
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "clay_tool_snapshot",
|
|
70
|
+
description: "Read a capsule's current state, controls, and declarative UI.",
|
|
71
|
+
inputSchema: shape.tool,
|
|
72
|
+
handler: function (args) { return run(function () { return handlers.snapshot(args.toolId); }); },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "clay_tool_act",
|
|
76
|
+
description: "Run a named tool action through the same action path used by the human UI.",
|
|
77
|
+
inputSchema: shape.act,
|
|
78
|
+
handler: function (args) {
|
|
79
|
+
return run(function () { return handlers.act(args.toolId, args.actionId, args.args || {}); });
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "clay_tool_set",
|
|
84
|
+
description: "Set an interactive control by ID through the control's normal bound action.",
|
|
85
|
+
inputSchema: shape.set,
|
|
86
|
+
handler: function (args) { return run(function () { return handlers.set(args.toolId, args.controlId, args.value); }); },
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "clay_tool_source",
|
|
90
|
+
description: "Read the exact authored manifest, declarative UI, logic source, and revision for an installed worker Capsule. The user must first enable Allow Mate editing for that Capsule.",
|
|
91
|
+
inputSchema: shape.tool,
|
|
92
|
+
handler: function (args) { return run(function () { return handlers.source(args.toolId); }); },
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "clay_tool_install",
|
|
96
|
+
description: INSTALL_DESCRIPTION,
|
|
97
|
+
inputSchema: shape.install,
|
|
98
|
+
handler: function (args) {
|
|
99
|
+
return run(function () { return handlers.install({ manifest: args.manifest, uiTree: args.uiTree, logicSource: args.logicSource }); });
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "clay_tool_update",
|
|
104
|
+
description: "Replace the complete authored source of an existing worker Capsule after validating its current revision. This requires user approval and preserves Capsule storage.",
|
|
105
|
+
inputSchema: shape.update,
|
|
106
|
+
handler: function (args) {
|
|
107
|
+
return run(function () {
|
|
108
|
+
return handlers.update(args.toolId, {
|
|
109
|
+
baseRevision: args.baseRevision,
|
|
110
|
+
manifest: args.manifest,
|
|
111
|
+
uiTree: args.uiTree,
|
|
112
|
+
logicSource: args.logicSource,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: "clay_tool_uninstall",
|
|
119
|
+
description: "Remove a user-owned capsule folder and its data. This requires user approval.",
|
|
120
|
+
inputSchema: shape.uninstall,
|
|
121
|
+
handler: function (args) { return run(function () { return handlers.uninstall(args.toolId); }); },
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { INSTALL_DESCRIPTION: INSTALL_DESCRIPTION, getToolDefs: getToolDefs };
|
package/lib/tool-llm.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
var MODEL_ALIASES = ["fast", "standard", "deep"];
|
|
2
|
+
|
|
3
|
+
function validateArgs(args) {
|
|
4
|
+
args = args || {};
|
|
5
|
+
if (typeof args.prompt !== "string" || !args.prompt.trim()) throw new Error("LLM prompt is required.");
|
|
6
|
+
if (args.system !== undefined && typeof args.system !== "string") throw new Error("LLM system prompt must be a string.");
|
|
7
|
+
var alias = args.model || "fast";
|
|
8
|
+
if (MODEL_ALIASES.indexOf(alias) === -1) {
|
|
9
|
+
throw new Error("LLM model must be a capability alias: fast, standard, or deep. Vendor model names are not allowed.");
|
|
10
|
+
}
|
|
11
|
+
return { system: args.system || "", prompt: args.prompt, model: alias };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function complete(opts) {
|
|
15
|
+
var args = validateArgs(opts.args);
|
|
16
|
+
var adapters = opts.adapters || {};
|
|
17
|
+
var selection = opts.selection || {};
|
|
18
|
+
var vendor = selection.vendor;
|
|
19
|
+
var model = selection.model;
|
|
20
|
+
if (!vendor || !model || typeof model !== "string") {
|
|
21
|
+
throw new Error(selection.error || "No configured model is available. Configure a provider and retry.");
|
|
22
|
+
}
|
|
23
|
+
var adapter = adapters[vendor];
|
|
24
|
+
if (!adapter) throw new Error("The configured " + vendor + " model provider is unavailable.");
|
|
25
|
+
var yoke = require("./yoke");
|
|
26
|
+
var result = await yoke.completeOnce(adapter, {
|
|
27
|
+
cwd: opts.cwd,
|
|
28
|
+
model: model,
|
|
29
|
+
systemPrompt: args.system || undefined,
|
|
30
|
+
prompt: args.prompt,
|
|
31
|
+
timeoutMs: opts.timeoutMs || 60000,
|
|
32
|
+
query: {
|
|
33
|
+
cwd: opts.cwd,
|
|
34
|
+
linuxUser: opts.linuxUser || undefined,
|
|
35
|
+
skipProjectInstructions: true,
|
|
36
|
+
skipSkills: true,
|
|
37
|
+
canUseTool: function () { return Promise.resolve({ behavior: "deny", message: "Capsule LLM calls cannot use tools." }); },
|
|
38
|
+
adapterOptions: { CLAUDE: { linuxUser: opts.linuxUser || undefined, settingSources: ["user"] } },
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
return result.text;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
MODEL_ALIASES: MODEL_ALIASES,
|
|
46
|
+
validateArgs: validateArgs,
|
|
47
|
+
complete: complete,
|
|
48
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var Datastore = require("@seald-io/nedb");
|
|
4
|
+
var toolsRegistry = require("./tools-registry");
|
|
5
|
+
|
|
6
|
+
var MAX_DOCUMENT_BYTES = 64 * 1024;
|
|
7
|
+
var datastoresByFile = {};
|
|
8
|
+
|
|
9
|
+
function createToolStorage(ctx, toolId) {
|
|
10
|
+
toolsRegistry.validateToolId(toolId);
|
|
11
|
+
var directory = path.join(toolsRegistry.resolveToolsRoot(ctx), toolId);
|
|
12
|
+
var filename = path.join(directory, "data.db");
|
|
13
|
+
|
|
14
|
+
function getDatastore() {
|
|
15
|
+
if (!datastoresByFile[filename]) {
|
|
16
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
17
|
+
datastoresByFile[filename] = new Datastore({ filename: filename, autoload: true });
|
|
18
|
+
}
|
|
19
|
+
return datastoresByFile[filename];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function ready() {
|
|
23
|
+
var datastore = getDatastore();
|
|
24
|
+
await datastore.autoloadPromise;
|
|
25
|
+
return datastore;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateDocument(doc) {
|
|
29
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) throw new Error("Storage document must be an object.");
|
|
30
|
+
var bytes = Buffer.byteLength(JSON.stringify(doc), "utf8");
|
|
31
|
+
if (bytes > MAX_DOCUMENT_BYTES) throw new Error("Storage document exceeds the 64KB limit.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function list() {
|
|
35
|
+
var datastore = await ready();
|
|
36
|
+
return datastore.findAsync({});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function get(id) {
|
|
40
|
+
if (typeof id !== "string" || !id) throw new Error("Storage document ID is required.");
|
|
41
|
+
var datastore = await ready();
|
|
42
|
+
return datastore.findOneAsync({ _id: id });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function put(doc) {
|
|
46
|
+
validateDocument(doc);
|
|
47
|
+
var datastore = await ready();
|
|
48
|
+
if (!doc._id) return datastore.insertAsync(doc);
|
|
49
|
+
var changes = {};
|
|
50
|
+
var keys = Object.keys(doc);
|
|
51
|
+
for (var i = 0; i < keys.length; i++) {
|
|
52
|
+
if (keys[i] !== "_id") changes[keys[i]] = doc[keys[i]];
|
|
53
|
+
}
|
|
54
|
+
var result = await datastore.updateAsync(
|
|
55
|
+
{ _id: doc._id },
|
|
56
|
+
{ $set: changes },
|
|
57
|
+
{ upsert: true, returnUpdatedDocs: true }
|
|
58
|
+
);
|
|
59
|
+
return result.affectedDocuments;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function remove(id) {
|
|
63
|
+
if (typeof id !== "string" || !id) throw new Error("Storage document ID is required.");
|
|
64
|
+
var datastore = await ready();
|
|
65
|
+
return datastore.removeAsync({ _id: id }, {});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function query(selector) {
|
|
69
|
+
if (!selector || typeof selector !== "object" || Array.isArray(selector)) throw new Error("Storage query must be an object.");
|
|
70
|
+
var datastore = await ready();
|
|
71
|
+
return datastore.findAsync(selector);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { list: list, get: get, put: put, delete: remove, query: query };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { MAX_DOCUMENT_BYTES: MAX_DOCUMENT_BYTES, createToolStorage: createToolStorage };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
var CONDITION_OPS = ["all", "any", "not", "equals", "notEquals", "in", "gt", "gte", "lt", "lte"];
|
|
2
|
+
var MAX_CONDITIONS = 40;
|
|
3
|
+
|
|
4
|
+
function isPlainObject(value) {
|
|
5
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
6
|
+
var prototype = Object.getPrototypeOf(value);
|
|
7
|
+
return prototype === Object.prototype || prototype === null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isSafePath(value) {
|
|
11
|
+
if (typeof value !== "string" || !/^(?:\$(?:state|item)\.)?[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/.test(value)) return false;
|
|
12
|
+
var parts = value.replace(/^\$(?:state|item)\./, "").split(".");
|
|
13
|
+
return !parts.some(function (part) { return part === "__proto__" || part === "constructor" || part === "prototype"; });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function safeScalar(value) {
|
|
17
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateDynamic(value, rule, location, validateRule) {
|
|
21
|
+
if (!isPlainObject(value) || !Object.prototype.hasOwnProperty.call(value, "$bind")) return false;
|
|
22
|
+
var dynamicAllowed = rule === "text" || rule === "bindingBoolean" || rule === "number" || rule === "positiveNumber" || rule === "options" || rule === "columns" || rule === "scalar" || !!(rule && rule.integer) || Array.isArray(rule);
|
|
23
|
+
if (!dynamicAllowed) throw new Error(location + " does not allow a dynamic value.");
|
|
24
|
+
var keys = Object.keys(value);
|
|
25
|
+
if (keys.some(function (key) { return key !== "$bind" && key !== "fallback" && key !== "$enum"; }) || !isSafePath(value.$bind)) throw new Error(location + " dynamic value requires a safe $bind and optional fallback.");
|
|
26
|
+
if (Array.isArray(rule)) {
|
|
27
|
+
if (!Array.isArray(value.$enum) || !value.$enum.length || value.$enum.length > rule.length || value.$enum.some(function (entry) { return rule.indexOf(entry) === -1; })) throw new Error(location + ".$enum must contain only allowed enum values.");
|
|
28
|
+
} else if (value.$enum !== undefined) throw new Error(location + ".$enum is only valid for enum properties.");
|
|
29
|
+
if (value.fallback !== undefined) validateRule(value.fallback, rule, location + ".fallback");
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateCondition(condition, location, context) {
|
|
34
|
+
context = context || { count: 0 };
|
|
35
|
+
context.count++;
|
|
36
|
+
if (context.count > MAX_CONDITIONS) throw new Error(location + " exceeds " + MAX_CONDITIONS + " condition terms.");
|
|
37
|
+
if (typeof condition === "string") {
|
|
38
|
+
if (!isSafePath(condition)) throw new Error(location + " must be a safe state path or condition object.");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (!isPlainObject(condition)) throw new Error(location + " must be a safe state path or condition object.");
|
|
42
|
+
var keys = Object.keys(condition);
|
|
43
|
+
if (keys.length !== 1 || CONDITION_OPS.indexOf(keys[0]) === -1) throw new Error(location + " must contain exactly one allowed condition operator.");
|
|
44
|
+
var op = keys[0];
|
|
45
|
+
var value = condition[op];
|
|
46
|
+
if (op === "all" || op === "any") {
|
|
47
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 12) throw new Error(location + "." + op + " must contain 1 to 12 conditions.");
|
|
48
|
+
for (var i = 0; i < value.length; i++) validateCondition(value[i], location + "." + op + "[" + i + "]", context);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (op === "not") {
|
|
52
|
+
validateCondition(value, location + ".not", context);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!isPlainObject(value) || !isSafePath(value.path)) throw new Error(location + "." + op + " requires a safe path.");
|
|
56
|
+
var allowed = op === "in" ? ["path", "values"] : ["path", "value"];
|
|
57
|
+
if (Object.keys(value).some(function (key) { return allowed.indexOf(key) === -1; })) throw new Error(location + "." + op + " contains an unknown property.");
|
|
58
|
+
if (op === "in") {
|
|
59
|
+
if (!Array.isArray(value.values) || value.values.length < 1 || value.values.length > 50 || value.values.some(function (entry) { return !safeScalar(entry); })) throw new Error(location + ".in values must be 1 to 50 safe scalars.");
|
|
60
|
+
} else if ((op === "gt" || op === "gte" || op === "lt" || op === "lte") && (typeof value.value !== "number" || !Number.isFinite(value.value))) {
|
|
61
|
+
throw new Error(location + "." + op + " value must be finite numeric data.");
|
|
62
|
+
} else if (op !== "in" && !safeScalar(value.value)) {
|
|
63
|
+
throw new Error(location + "." + op + " value must be scalar.");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateValidation(value, location) {
|
|
68
|
+
if (!isPlainObject(value)) throw new Error(location + " must be a validation object.");
|
|
69
|
+
var allowed = ["minLength", "maxLength", "min", "max", "step", "pattern", "message"];
|
|
70
|
+
var keys = Object.keys(value);
|
|
71
|
+
for (var i = 0; i < keys.length; i++) if (allowed.indexOf(keys[i]) === -1) throw new Error(location + " contains unknown validation rule '" + keys[i] + "'.");
|
|
72
|
+
if (value.minLength !== undefined && (!Number.isInteger(value.minLength) || value.minLength < 0 || value.minLength > 10000)) throw new Error(location + ".minLength is out of bounds.");
|
|
73
|
+
if (value.maxLength !== undefined && (!Number.isInteger(value.maxLength) || value.maxLength < 1 || value.maxLength > 10000)) throw new Error(location + ".maxLength is out of bounds.");
|
|
74
|
+
if (value.minLength !== undefined && value.maxLength !== undefined && value.minLength > value.maxLength) throw new Error(location + " minLength exceeds maxLength.");
|
|
75
|
+
var numeric = ["min", "max", "step"];
|
|
76
|
+
for (var ni = 0; ni < numeric.length; ni++) if (value[numeric[ni]] !== undefined && (typeof value[numeric[ni]] !== "number" || !Number.isFinite(value[numeric[ni]]))) throw new Error(location + "." + numeric[ni] + " must be finite numeric data.");
|
|
77
|
+
if (value.step !== undefined && value.step <= 0) throw new Error(location + ".step must be positive.");
|
|
78
|
+
if (value.pattern !== undefined && ["email", "url", "integer", "decimal"].indexOf(value.pattern) === -1) throw new Error(location + ".pattern is not allowed.");
|
|
79
|
+
if (value.message !== undefined && (typeof value.message !== "string" || value.message.length > 300)) throw new Error(location + ".message must be short text.");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { validateDynamic: validateDynamic, validateCondition: validateCondition, validateValidation: validateValidation };
|
|
@@ -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
|
+
};
|