clay-server 3.8.1 → 4.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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-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-worker-proposal.js +25 -25
- package/lib/project-workspace-query.js +144 -0
- package/lib/project.js +329 -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 +78 -20
- package/lib/public/modules/app-notifications.js +4 -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/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/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/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/user-settings.js +16 -23
- 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 +276 -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 +86 -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 +37 -6
- package/lib/yoke/adapters/kiro.js +2 -1
- package/lib/yoke/claude-user-input.js +38 -0
- 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 +2 -1
- 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
|
@@ -6,14 +6,14 @@ function getToolDefs(handlers) {
|
|
|
6
6
|
return [
|
|
7
7
|
{
|
|
8
8
|
name: "send_to_partner",
|
|
9
|
-
description: "Delegate one concrete task to the Worker. If this session has no pair yet, Clay creates a real Worker session and opens it visibly in the right pane before starting the task. A completed turn does not end the Worker session: reuse the same Worker for follow-up implementation and corrections instead of taking over its work. Detached completions are pushed back automatically. A delegated turn cannot delegate back, so keep orchestration one hop deep.",
|
|
9
|
+
description: "Delegate one concrete task to the visible Split Worker. If this session has no pair yet, Clay creates a real Split Worker session and opens it visibly in the right pane before starting the task. A completed turn does not end the Split Worker session: reuse the same Split Worker for follow-up implementation and corrections instead of taking over its work. Detached completions are pushed back automatically. A delegated turn cannot delegate back, so keep orchestration one hop deep.",
|
|
10
10
|
inputSchema: buildShape({
|
|
11
11
|
message: { type: "string", description: "The complete task or question for the partner." },
|
|
12
12
|
wait: { type: "boolean", description: "Wait for the partner's turn to finish. Defaults to true." },
|
|
13
13
|
timeoutSeconds: { type: "number", description: "Maximum wait in seconds, from 1 to 900. Defaults to 300." },
|
|
14
|
-
workerVendor: { type: "string", description: "Optional Worker vendor to use only when creating a new pair. Defaults to a suitable installed vendor." },
|
|
15
|
-
workerModel: { type: "string", description: "Optional Worker model to use only when creating a new pair." },
|
|
16
|
-
workerEffort: { type: "string", description: "Optional Worker reasoning effort to use only when creating a new pair." },
|
|
14
|
+
workerVendor: { type: "string", description: "Optional Split Worker vendor to use only when creating a new pair. Defaults to a suitable installed vendor." },
|
|
15
|
+
workerModel: { type: "string", description: "Optional Split Worker model to use only when creating a new pair." },
|
|
16
|
+
workerEffort: { type: "string", description: "Optional Split Worker reasoning effort to use only when creating a new pair." },
|
|
17
17
|
}, ["message"]),
|
|
18
18
|
handler: function (args) { return handlers.send(args || {}); },
|
|
19
19
|
},
|
|
@@ -27,13 +27,13 @@ function getToolDefs(handlers) {
|
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
name: "interrupt_partner",
|
|
30
|
-
description: "Interrupt the Worker's current task. Use this when the task is going in the wrong direction, needs to be reprioritized, or must stop before the next instruction. The Worker returns any partial result to you for review.",
|
|
30
|
+
description: "Interrupt the Split Worker's current task. Use this when the task is going in the wrong direction, needs to be reprioritized, or must stop before the next instruction. The Split Worker returns any partial result to you for review.",
|
|
31
31
|
inputSchema: buildShape({}),
|
|
32
32
|
handler: function (args) { return handlers.interrupt(args || {}); },
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
name: "close_partner",
|
|
36
|
-
description: "Close the visible Worker pane and dissolve the Driver/Worker pair while preserving both session histories. If the Worker is still running, Clay interrupts it before closing the pair.",
|
|
36
|
+
description: "Close the visible Split Worker pane and dissolve the Driver/Split Worker pair while preserving both session histories. If the Split Worker is still running, Clay interrupts it before closing the pair.",
|
|
37
37
|
inputSchema: buildShape({}),
|
|
38
38
|
handler: function (args) { return handlers.close(args || {}); },
|
|
39
39
|
},
|
package/lib/sessions.js
CHANGED
|
@@ -170,9 +170,15 @@ function createSessionManager(opts) {
|
|
|
170
170
|
if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
|
|
171
171
|
if (session.loop) metaObj.loop = session.loop;
|
|
172
172
|
if (session.spawn) metaObj.spawn = session.spawn;
|
|
173
|
+
if (session.assignment) metaObj.assignment = session.assignment;
|
|
173
174
|
if (session.handoff) metaObj.handoff = session.handoff;
|
|
174
175
|
if (session.debateState) metaObj.debateState = session.debateState;
|
|
175
176
|
if (session.debateSetupMode) metaObj.debateSetupMode = true;
|
|
177
|
+
if (session.homeDebatePlanning) metaObj.homeDebatePlanning = true;
|
|
178
|
+
if (session.homeDebatePhase) metaObj.homeDebatePhase = session.homeDebatePhase;
|
|
179
|
+
if (session.homeDebateInitialTopic) metaObj.homeDebateInitialTopic = session.homeDebateInitialTopic;
|
|
180
|
+
if (session.homeClayEntryRequestId) metaObj.homeClayEntryRequestId = session.homeClayEntryRequestId;
|
|
181
|
+
if (session.homeClayEntryMode) metaObj.homeClayEntryMode = session.homeClayEntryMode;
|
|
176
182
|
var meta = JSON.stringify(metaObj);
|
|
177
183
|
var lines = [meta];
|
|
178
184
|
for (var i = 0; i < session.history.length; i++) {
|
|
@@ -283,9 +289,15 @@ function createSessionManager(opts) {
|
|
|
283
289
|
session.effort = m.effort || null;
|
|
284
290
|
if (m.loop) session.loop = m.loop;
|
|
285
291
|
if (m.spawn) session.spawn = m.spawn;
|
|
292
|
+
if (m.assignment) session.assignment = m.assignment;
|
|
286
293
|
if (m.handoff) session.handoff = m.handoff;
|
|
287
294
|
if (m.debateState) session.debateState = m.debateState;
|
|
288
295
|
if (m.debateSetupMode) session.debateSetupMode = true;
|
|
296
|
+
if (m.homeDebatePlanning) session.homeDebatePlanning = true;
|
|
297
|
+
if (m.homeDebatePhase) session.homeDebatePhase = m.homeDebatePhase;
|
|
298
|
+
if (m.homeDebateInitialTopic) session.homeDebateInitialTopic = m.homeDebateInitialTopic;
|
|
299
|
+
if (m.homeClayEntryRequestId) session.homeClayEntryRequestId = m.homeClayEntryRequestId;
|
|
300
|
+
if (m.homeClayEntryMode) session.homeClayEntryMode = m.homeClayEntryMode;
|
|
289
301
|
if (m.ownerId) session.ownerId = m.ownerId;
|
|
290
302
|
// Born-TUI session: PTY is gone after restart, but the cliSessionId
|
|
291
303
|
// is still resumable via `claude --resume <id>`. We mark the mode
|
|
@@ -865,6 +877,7 @@ function createSessionManager(opts) {
|
|
|
865
877
|
if (sendEach) {
|
|
866
878
|
var data = JSON.stringify(obj);
|
|
867
879
|
sendEach(function (ws) {
|
|
880
|
+
if (session.homeDebatePlanning === true) return;
|
|
868
881
|
if (ws._clayActiveSession === session.localId && ws.readyState === 1) {
|
|
869
882
|
ws.send(data);
|
|
870
883
|
}
|
|
@@ -881,8 +894,8 @@ function createSessionManager(opts) {
|
|
|
881
894
|
if (!outgoingObj._ts) outgoingObj._ts = obj._ts;
|
|
882
895
|
session.history.push(obj);
|
|
883
896
|
appendToSessionFile(session, obj);
|
|
884
|
-
// Per-session out-of-band subscribers
|
|
885
|
-
//
|
|
897
|
+
// Per-session out-of-band subscribers mirror mate session events into
|
|
898
|
+
// a parallel UI without joining the project's
|
|
886
899
|
// ws clients set). Subscribers receive the same obj that goes to ws
|
|
887
900
|
// clients; they are responsible for any transform + dispatch.
|
|
888
901
|
if (session._subscribers && session._subscribers.size > 0) {
|
|
@@ -895,6 +908,7 @@ function createSessionManager(opts) {
|
|
|
895
908
|
var data = JSON.stringify(outgoingObj);
|
|
896
909
|
var ioData = null;
|
|
897
910
|
sendEach(function (ws) {
|
|
911
|
+
if (session.homeDebatePlanning === true) return;
|
|
898
912
|
if (ws._clayActiveSession === session.localId) {
|
|
899
913
|
if (ws.readyState === 1) ws.send(data);
|
|
900
914
|
} else if (session.isProcessing && !session._ioThrottle) {
|
|
@@ -993,11 +1007,12 @@ function createSessionManager(opts) {
|
|
|
993
1007
|
activeSessionId = mostRecent.localId;
|
|
994
1008
|
}
|
|
995
1009
|
|
|
996
|
-
function searchSessions(query) {
|
|
1010
|
+
function searchSessions(query, canInclude) {
|
|
997
1011
|
if (!query) return [];
|
|
998
1012
|
var q = query.toLowerCase();
|
|
999
1013
|
var results = [];
|
|
1000
1014
|
sessions.forEach(function (session) {
|
|
1015
|
+
if (typeof canInclude === "function" && !canInclude(session)) return;
|
|
1001
1016
|
var titleMatch = (session.title || "New Session").toLowerCase().indexOf(q) !== -1;
|
|
1002
1017
|
var contentMatch = false;
|
|
1003
1018
|
for (var i = 0; i < session.history.length; i++) {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var crypto = require("crypto");
|
|
4
|
+
|
|
5
|
+
var METADATA_FILE = ".capsule-metadata.json";
|
|
6
|
+
|
|
7
|
+
function metadataPath(root) {
|
|
8
|
+
return path.join(root, METADATA_FILE);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readMetadata(root) {
|
|
12
|
+
try {
|
|
13
|
+
var parsed = JSON.parse(fs.readFileSync(metadataPath(root), "utf8"));
|
|
14
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
15
|
+
} catch (error) {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeFileAtomic(file, value) {
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
var temporary = file + ".tmp-" + process.pid + "-" + crypto.randomBytes(6).toString("hex");
|
|
23
|
+
try {
|
|
24
|
+
fs.writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600 });
|
|
25
|
+
fs.renameSync(temporary, file);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
try { fs.rmSync(temporary, { force: true }); } catch (cleanupError) {}
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sanitizedMetadata(root, toolId) {
|
|
33
|
+
var record = readMetadata(root)[toolId];
|
|
34
|
+
return { mateEditingAllowed: !!(record && record.mateEditingAllowed === true) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setMateEditingAllowed(root, toolId, allowed) {
|
|
38
|
+
var all = readMetadata(root);
|
|
39
|
+
all[toolId] = { mateEditingAllowed: allowed === true };
|
|
40
|
+
writeFileAtomic(metadataPath(root), JSON.stringify(all, null, 2) + "\n");
|
|
41
|
+
return sanitizedMetadata(root, toolId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function removeMetadata(root, toolId) {
|
|
45
|
+
var all = readMetadata(root);
|
|
46
|
+
if (!Object.prototype.hasOwnProperty.call(all, toolId)) return;
|
|
47
|
+
delete all[toolId];
|
|
48
|
+
writeFileAtomic(metadataPath(root), JSON.stringify(all, null, 2) + "\n");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function revisionForParts(manifestSource, uiSource, logicSource) {
|
|
52
|
+
var hash = crypto.createHash("sha256");
|
|
53
|
+
var parts = [manifestSource, uiSource, logicSource === null ? "" : logicSource];
|
|
54
|
+
for (var i = 0; i < parts.length; i++) {
|
|
55
|
+
var bytes = Buffer.from(parts[i], "utf8");
|
|
56
|
+
hash.update(String(bytes.length));
|
|
57
|
+
hash.update(":");
|
|
58
|
+
hash.update(bytes);
|
|
59
|
+
}
|
|
60
|
+
return hash.digest("hex");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readSource(directory) {
|
|
64
|
+
var manifestSource = fs.readFileSync(path.join(directory, "manifest.json"), "utf8");
|
|
65
|
+
var uiSource = fs.readFileSync(path.join(directory, "ui.json"), "utf8");
|
|
66
|
+
var manifest = JSON.parse(manifestSource);
|
|
67
|
+
var uiTree = JSON.parse(uiSource);
|
|
68
|
+
var runtime = manifest.runtime || "worker";
|
|
69
|
+
var logicSource = runtime === "worker" ? fs.readFileSync(path.join(directory, "logic.js"), "utf8") : null;
|
|
70
|
+
return {
|
|
71
|
+
manifest: manifest,
|
|
72
|
+
uiTree: uiTree,
|
|
73
|
+
logicSource: logicSource,
|
|
74
|
+
logicAvailable: runtime === "worker",
|
|
75
|
+
revision: revisionForParts(manifestSource, uiSource, logicSource),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function replaceSourceFiles(directory, sources) {
|
|
80
|
+
var names = ["manifest.json", "ui.json", "logic.js"];
|
|
81
|
+
var transaction = crypto.randomBytes(6).toString("hex");
|
|
82
|
+
var staged = [];
|
|
83
|
+
var backups = [];
|
|
84
|
+
try {
|
|
85
|
+
for (var i = 0; i < names.length; i++) {
|
|
86
|
+
var target = path.join(directory, names[i]);
|
|
87
|
+
var stage = target + ".next-" + transaction;
|
|
88
|
+
staged.push({ target: target, stage: stage });
|
|
89
|
+
fs.writeFileSync(stage, sources[names[i]], "utf8");
|
|
90
|
+
}
|
|
91
|
+
for (var j = 0; j < staged.length; j++) {
|
|
92
|
+
var backup = staged[j].target + ".previous-" + transaction;
|
|
93
|
+
fs.renameSync(staged[j].target, backup);
|
|
94
|
+
backups.push({ target: staged[j].target, backup: backup });
|
|
95
|
+
fs.renameSync(staged[j].stage, staged[j].target);
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
for (var ri = backups.length - 1; ri >= 0; ri--) {
|
|
99
|
+
try {
|
|
100
|
+
fs.rmSync(backups[ri].target, { force: true });
|
|
101
|
+
fs.renameSync(backups[ri].backup, backups[ri].target);
|
|
102
|
+
} catch (rollbackError) {}
|
|
103
|
+
}
|
|
104
|
+
for (var si = 0; si < staged.length; si++) {
|
|
105
|
+
try { fs.rmSync(staged[si].stage, { force: true }); } catch (cleanupError) {}
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
for (var k = 0; k < backups.length; k++) {
|
|
110
|
+
try { fs.rmSync(backups[k].backup, { force: true }); } catch (cleanupError) {}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function updateSource(directory, input, validators) {
|
|
115
|
+
var current = readSource(directory);
|
|
116
|
+
if (!current.logicAvailable) throw new Error("Server-managed Capsules cannot be edited.");
|
|
117
|
+
if (!input || typeof input !== "object") throw new Error("Complete Capsule source is required.");
|
|
118
|
+
if (typeof input.baseRevision !== "string" || input.baseRevision !== current.revision) {
|
|
119
|
+
throw new Error("Capsule source changed. Read the latest source and retry your update.");
|
|
120
|
+
}
|
|
121
|
+
validators.validateManifest(input.manifest, { allowServerRuntime: false });
|
|
122
|
+
validators.validateUiNode(input.uiTree);
|
|
123
|
+
if (input.manifest.id !== current.manifest.id) throw new Error("Capsule updates cannot change the Capsule ID.");
|
|
124
|
+
if ((input.manifest.runtime || "worker") !== "worker") throw new Error("Capsule updates cannot change the runtime.");
|
|
125
|
+
if (typeof input.logicSource !== "string" || !input.logicSource.trim()) throw new Error("Tool logic source is required.");
|
|
126
|
+
var manifest = Object.assign({}, input.manifest, { name: input.manifest.name.trim(), runtime: "worker" });
|
|
127
|
+
replaceSourceFiles(directory, {
|
|
128
|
+
"manifest.json": JSON.stringify(manifest, null, 2) + "\n",
|
|
129
|
+
"ui.json": JSON.stringify(input.uiTree, null, 2) + "\n",
|
|
130
|
+
"logic.js": input.logicSource,
|
|
131
|
+
});
|
|
132
|
+
return readSource(directory);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
METADATA_FILE: METADATA_FILE,
|
|
137
|
+
sanitizedMetadata: sanitizedMetadata,
|
|
138
|
+
setMateEditingAllowed: setMateEditingAllowed,
|
|
139
|
+
removeMetadata: removeMetadata,
|
|
140
|
+
readSource: readSource,
|
|
141
|
+
updateSource: updateSource,
|
|
142
|
+
};
|
|
@@ -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 };
|