clay-server 4.0.0-beta.8 → 4.0.0
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/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -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/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +15 -1
- package/lib/project-file-history.js +223 -0
- package/lib/project-file-watch.js +77 -49
- package/lib/project-filesystem.js +31 -220
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-request-access.js +66 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +185 -6
- package/lib/public/app.js +63 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +84 -3
- package/lib/public/css/messages.css +40 -1
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +97 -10
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +36 -3
- package/lib/public/index.html +43 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +124 -6
- package/lib/public/modules/app-home-hub.js +12 -8
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +92 -11
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +3 -2
- package/lib/public/modules/app-projects.js +56 -18
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-surface-boot.js +7 -3
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-activation.js +45 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-pair-ui.js +15 -4
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/split-worker-runtime.js +24 -0
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/thinking-lifecycle.js +175 -0
- package/lib/public/modules/thinking-summary.js +28 -0
- package/lib/public/modules/thinking-view.js +75 -0
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +31 -123
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -20
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +135 -46
- package/lib/sdk-message-processor.js +36 -19
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +137 -22
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +41 -0
- package/lib/sessions.js +57 -10
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +6 -6
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex.js +24 -14
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
var crypto = require("crypto");
|
|
2
|
+
|
|
3
|
+
var PROVENANCE_VERSION = 1;
|
|
4
|
+
var MAX_ORIGIN_LENGTH = 128;
|
|
5
|
+
|
|
6
|
+
function safeOriginId(value) {
|
|
7
|
+
if (typeof value !== "string") return null;
|
|
8
|
+
var trimmed = value.trim();
|
|
9
|
+
if (!trimmed || trimmed.length > MAX_ORIGIN_LENGTH) return null;
|
|
10
|
+
return trimmed;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function ensureOrigin(session) {
|
|
14
|
+
if (!session) return null;
|
|
15
|
+
var existing = safeOriginId(session.sessionOriginId);
|
|
16
|
+
if (existing) {
|
|
17
|
+
session.sessionOriginId = existing;
|
|
18
|
+
return existing;
|
|
19
|
+
}
|
|
20
|
+
session.sessionOriginId = crypto.randomUUID();
|
|
21
|
+
return session.sessionOriginId;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isWorker(session) {
|
|
25
|
+
return !!(session && session.sessionProvenance && session.sessionProvenance.kind === "worker");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function nextWorkerGeneration(parentOriginId, sessions) {
|
|
29
|
+
var next = 1;
|
|
30
|
+
sessions.forEach(function (session) {
|
|
31
|
+
var provenance = session && session.sessionProvenance;
|
|
32
|
+
if (!provenance || provenance.kind !== "worker" || provenance.parentSessionOriginId !== parentOriginId) return;
|
|
33
|
+
var generation = Number.isInteger(provenance.generation) && provenance.generation > 0 ? provenance.generation : 0;
|
|
34
|
+
if (generation >= next) next = generation + 1;
|
|
35
|
+
});
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function markWorker(driver, worker, sessions) {
|
|
40
|
+
if (!driver || !worker || driver === worker) throw new Error("Split Worker provenance requires distinct sessions");
|
|
41
|
+
if ((driver.ownerId || null) !== (worker.ownerId || null)) throw new Error("Split Worker provenance requires the same owner");
|
|
42
|
+
if (isWorker(driver)) throw new Error("A Split Worker cannot become a Driver");
|
|
43
|
+
var parentOriginId = ensureOrigin(driver);
|
|
44
|
+
ensureOrigin(worker);
|
|
45
|
+
worker.sessionProvenance = {
|
|
46
|
+
version: PROVENANCE_VERSION,
|
|
47
|
+
kind: "worker",
|
|
48
|
+
parentSessionOriginId: parentOriginId,
|
|
49
|
+
generation: nextWorkerGeneration(parentOriginId, sessions),
|
|
50
|
+
createdVia: "split-worker",
|
|
51
|
+
createdAt: Date.now(),
|
|
52
|
+
};
|
|
53
|
+
return worker.sessionProvenance;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function restore(meta, session) {
|
|
57
|
+
var storedOriginId = safeOriginId(meta && meta.sessionOriginId);
|
|
58
|
+
ensureOrigin(session);
|
|
59
|
+
var stored = meta && meta.sessionProvenance;
|
|
60
|
+
if (!stored || stored.kind !== "worker") return storedOriginId !== session.sessionOriginId;
|
|
61
|
+
var parentOriginId = safeOriginId(stored.parentSessionOriginId);
|
|
62
|
+
// A corrupt Worker record remains a Worker. It is projected as orphaned
|
|
63
|
+
// instead of silently gaining top-level Driver authority.
|
|
64
|
+
session.sessionProvenance = {
|
|
65
|
+
version: PROVENANCE_VERSION,
|
|
66
|
+
kind: "worker",
|
|
67
|
+
parentSessionOriginId: parentOriginId,
|
|
68
|
+
generation: Number.isInteger(stored.generation) && stored.generation > 0 ? stored.generation : null,
|
|
69
|
+
createdVia: stored.createdVia === "split-worker" ? "split-worker" : null,
|
|
70
|
+
createdAt: typeof stored.createdAt === "number" && isFinite(stored.createdAt) ? stored.createdAt : null,
|
|
71
|
+
};
|
|
72
|
+
return storedOriginId !== session.sessionOriginId || JSON.stringify(stored) !== JSON.stringify(session.sessionProvenance);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function metadata(session) {
|
|
76
|
+
var result = { sessionOriginId: ensureOrigin(session) };
|
|
77
|
+
if (isWorker(session)) result.sessionProvenance = Object.assign({}, session.sessionProvenance);
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hierarchyFor(sessions) {
|
|
82
|
+
var originMap = {};
|
|
83
|
+
var duplicateOrigins = {};
|
|
84
|
+
for (var i = 0; i < sessions.length; i++) {
|
|
85
|
+
var originId = safeOriginId(sessions[i] && sessions[i].sessionOriginId);
|
|
86
|
+
if (!originId) continue;
|
|
87
|
+
if (originMap[originId]) duplicateOrigins[originId] = true;
|
|
88
|
+
else originMap[originId] = sessions[i];
|
|
89
|
+
}
|
|
90
|
+
var result = {};
|
|
91
|
+
for (var j = 0; j < sessions.length; j++) {
|
|
92
|
+
var session = sessions[j];
|
|
93
|
+
if (!isWorker(session)) {
|
|
94
|
+
result[session.localId] = { role: "driver", parentSessionId: null, parentAvailable: false, generation: null };
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
var provenance = session.sessionProvenance;
|
|
98
|
+
var parentOriginId = safeOriginId(provenance.parentSessionOriginId);
|
|
99
|
+
var parent = duplicateOrigins[parentOriginId] ? null : originMap[parentOriginId];
|
|
100
|
+
var sameOwner = parent && (parent.ownerId || null) === (session.ownerId || null);
|
|
101
|
+
var validParent = sameOwner && !isWorker(parent) ? parent : null;
|
|
102
|
+
result[session.localId] = {
|
|
103
|
+
role: "worker",
|
|
104
|
+
parentSessionId: validParent ? validParent.localId : null,
|
|
105
|
+
parentAvailable: !!validParent,
|
|
106
|
+
generation: Number.isInteger(provenance.generation) && provenance.generation > 0 ? provenance.generation : null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
ensureOrigin: ensureOrigin,
|
|
114
|
+
hierarchyFor: hierarchyFor,
|
|
115
|
+
isWorker: isWorker,
|
|
116
|
+
markWorker: markWorker,
|
|
117
|
+
metadata: metadata,
|
|
118
|
+
restore: restore,
|
|
119
|
+
};
|
|
@@ -27,7 +27,7 @@ function getToolDefs(handlers) {
|
|
|
27
27
|
return [
|
|
28
28
|
{
|
|
29
29
|
name: "spawn_sessions",
|
|
30
|
-
description: "Create sibling work sessions
|
|
30
|
+
description: "Create ordinary sibling work sessions for background parallel work. This does not create a visible Driver/Split Worker pair and must not be used when the user asks for a Worker, paired pane, or split collaborator; use send_to_partner for that. Set forkFromCurrent to copy this session's full conversation context into every child. Children share the project's working directory, so prefer parallel analysis or other non-conflicting work over concurrent edits. Results can be polled with check_spawned_sessions; spawned children cannot spawn further sessions.",
|
|
31
31
|
inputSchema: buildShape({
|
|
32
32
|
sessions: { type: "string", description: "JSON array of objects: [{\"title\":\"Task title\",\"prompt\":\"Task instructions\"}]" },
|
|
33
33
|
vendor: { type: "string", description: "Optional vendor id. Defaults to the parent session vendor, then the project default." },
|
|
@@ -280,7 +280,10 @@ function attachSplitGroups(ctx) {
|
|
|
280
280
|
});
|
|
281
281
|
|
|
282
282
|
function sendState(ws) {
|
|
283
|
-
if (!ws
|
|
283
|
+
if (!ws) return;
|
|
284
|
+
// Pane clients need the same owner-scoped pair roles as the parent shell.
|
|
285
|
+
// The Worker composer lock resolves its role from this projection; hiding
|
|
286
|
+
// the projection from panes leaves a configured Worker looking editable.
|
|
284
287
|
ctx.sendTo(ws, { type: "split_groups", groups: store.listFor(ws) });
|
|
285
288
|
}
|
|
286
289
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Session title policy shared by query startup and completed-turn processing.
|
|
2
|
+
// Initial titles are immediate, provisional UI labels. Model-generated titles
|
|
3
|
+
// replace them after enough conversation exists to describe the actual topic.
|
|
4
|
+
|
|
5
|
+
var DEFAULT_GENERATION_TURN = 2;
|
|
6
|
+
var HOME_GENERATION_TURN = 1;
|
|
7
|
+
var MAX_PROVISIONAL_CHARS = 60;
|
|
8
|
+
var LEGACY_SEARCH_TITLE = "This conversation began in Clay Studio's global search.";
|
|
9
|
+
|
|
10
|
+
function cleanText(value) {
|
|
11
|
+
if (typeof value !== "string") return "";
|
|
12
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function firstRecordedUserMessage(session) {
|
|
16
|
+
var history = session && Array.isArray(session.history) ? session.history : [];
|
|
17
|
+
for (var i = 0; i < history.length; i++) {
|
|
18
|
+
if (history[i] && history[i].type === "user_message") {
|
|
19
|
+
var text = cleanText(history[i].text);
|
|
20
|
+
if (text) return text;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function selectInitialTitle(session, transportText) {
|
|
27
|
+
var existing = cleanText(session && session.title);
|
|
28
|
+
if (existing) return { title: existing, provisional: false };
|
|
29
|
+
|
|
30
|
+
// Prefer the visible message recorded in history. Query transports may wrap
|
|
31
|
+
// it in identical provider instructions, as global Ask Clay does.
|
|
32
|
+
var source = firstRecordedUserMessage(session) || cleanText(transportText);
|
|
33
|
+
if (source.length < 10) return { title: null, provisional: false };
|
|
34
|
+
return {
|
|
35
|
+
title: source.length > MAX_PROVISIONAL_CHARS ? source.substring(0, MAX_PROVISIONAL_CHARS) : source,
|
|
36
|
+
provisional: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function generationTurn(isMate) {
|
|
41
|
+
return isMate === true ? HOME_GENERATION_TURN : DEFAULT_GENERATION_TURN;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function repairLegacySearchTitle(session) {
|
|
45
|
+
var title = cleanText(session && session.title);
|
|
46
|
+
if (!session || title.indexOf(LEGACY_SEARCH_TITLE) !== 0) return false;
|
|
47
|
+
var source = firstRecordedUserMessage(session);
|
|
48
|
+
if (!source) return false;
|
|
49
|
+
session.title = source.length > MAX_PROVISIONAL_CHARS ? source.substring(0, MAX_PROVISIONAL_CHARS) : source;
|
|
50
|
+
session.titleAutoGenerated = false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
DEFAULT_GENERATION_TURN: DEFAULT_GENERATION_TURN,
|
|
56
|
+
HOME_GENERATION_TURN: HOME_GENERATION_TURN,
|
|
57
|
+
selectInitialTitle: selectInitialTitle,
|
|
58
|
+
generationTurn: generationTurn,
|
|
59
|
+
repairLegacySearchTitle: repairLegacySearchTitle,
|
|
60
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function normalize(value) {
|
|
2
|
+
return value === "shared" ? "shared" : "private";
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function restore(meta) {
|
|
6
|
+
// Older releases persisted the shared default without recording intent.
|
|
7
|
+
return meta.sessionVisibilityExplicit === true ? normalize(meta.sessionVisibility) : "private";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function handleChange(ctx, ws, msg) {
|
|
11
|
+
if (typeof msg.sessionId !== "number" || (msg.visibility !== "shared" && msg.visibility !== "private")) return;
|
|
12
|
+
var session = ctx.sm.sessions.get(msg.sessionId);
|
|
13
|
+
if (!session) return;
|
|
14
|
+
if (ctx.usersModule.isMultiUser()) {
|
|
15
|
+
var user = ws._clayUser;
|
|
16
|
+
var project = ctx.getProjectAccess ? ctx.getProjectAccess() : { visibility: "public" };
|
|
17
|
+
if (!user || !session.ownerId || session.ownerId !== user.id ||
|
|
18
|
+
!ctx.usersModule.canAccessSession(user.id, session, project)) return;
|
|
19
|
+
} else if (session.ownerId) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
ctx.sm.setSessionVisibility(msg.sessionId, msg.visibility);
|
|
23
|
+
revokeViewers(ctx, session);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function revokeViewers(ctx, session) {
|
|
27
|
+
if (ctx.usersModule.isMultiUser() && session.sessionVisibility === "private") {
|
|
28
|
+
var project = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
|
|
29
|
+
ctx.clients.forEach(function(client) {
|
|
30
|
+
if (client._clayActiveSession !== session.localId) return;
|
|
31
|
+
var userId = client._clayUser && client._clayUser.id;
|
|
32
|
+
if (userId && ctx.usersModule.canAccessSession(userId, session, project)) return;
|
|
33
|
+
// Clear the server-side selection before reconnecting so no further
|
|
34
|
+
// session events or input can cross the revoked access boundary.
|
|
35
|
+
client._clayActiveSession = null;
|
|
36
|
+
if (client.readyState === 1) client.close(1008, "Session access changed");
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { normalize: normalize, restore: restore, handleChange: handleChange, revokeViewers: revokeViewers };
|
package/lib/sessions.js
CHANGED
|
@@ -6,6 +6,8 @@ var users = require("./users");
|
|
|
6
6
|
var { CODEX_DEFAULTS } = require("./codex-defaults");
|
|
7
7
|
var hygiene = require("./session-hygiene");
|
|
8
8
|
var backgroundTaskTiming = require("./background-task-timing");
|
|
9
|
+
var sessionProvenance = require("./session-provenance");
|
|
10
|
+
var sessionVisibility = require("./session-visibility");
|
|
9
11
|
|
|
10
12
|
function createSessionManager(opts) {
|
|
11
13
|
var cwd = opts.cwd;
|
|
@@ -165,7 +167,8 @@ function createSessionManager(opts) {
|
|
|
165
167
|
if (session.dangerouslySkipPermissions) metaObj.dangerouslySkipPermissions = true;
|
|
166
168
|
if (session.permissionMode) metaObj.permissionMode = session.permissionMode;
|
|
167
169
|
if (session.permissionModeBeforeFullAccess) metaObj.permissionModeBeforeFullAccess = session.permissionModeBeforeFullAccess;
|
|
168
|
-
|
|
170
|
+
metaObj.sessionVisibility = sessionVisibility.normalize(session.sessionVisibility);
|
|
171
|
+
if (session.sessionVisibilityExplicit) metaObj.sessionVisibilityExplicit = true;
|
|
169
172
|
if (session.bookmarked) metaObj.bookmarked = true;
|
|
170
173
|
if (typeof session.favoriteOrder === "number") metaObj.favoriteOrder = session.favoriteOrder;
|
|
171
174
|
if (session.lastRewindUuid) metaObj.lastRewindUuid = session.lastRewindUuid;
|
|
@@ -180,6 +183,9 @@ function createSessionManager(opts) {
|
|
|
180
183
|
if (session.homeDebateInitialTopic) metaObj.homeDebateInitialTopic = session.homeDebateInitialTopic;
|
|
181
184
|
if (session.homeClayEntryRequestId) metaObj.homeClayEntryRequestId = session.homeClayEntryRequestId;
|
|
182
185
|
if (session.homeClayEntryMode) metaObj.homeClayEntryMode = session.homeClayEntryMode;
|
|
186
|
+
var provenanceMeta = sessionProvenance.metadata(session);
|
|
187
|
+
metaObj.sessionOriginId = provenanceMeta.sessionOriginId;
|
|
188
|
+
if (provenanceMeta.sessionProvenance) metaObj.sessionProvenance = provenanceMeta.sessionProvenance;
|
|
183
189
|
var meta = JSON.stringify(metaObj);
|
|
184
190
|
var lines = [meta];
|
|
185
191
|
for (var i = 0; i < session.history.length; i++) {
|
|
@@ -300,6 +306,8 @@ function createSessionManager(opts) {
|
|
|
300
306
|
if (m.homeClayEntryRequestId) session.homeClayEntryRequestId = m.homeClayEntryRequestId;
|
|
301
307
|
if (m.homeClayEntryMode) session.homeClayEntryMode = m.homeClayEntryMode;
|
|
302
308
|
if (m.ownerId) session.ownerId = m.ownerId;
|
|
309
|
+
session.sessionOriginId = m.sessionOriginId || null;
|
|
310
|
+
session._provenanceMigrationNeeded = sessionProvenance.restore(m, session);
|
|
303
311
|
// Born-TUI session: PTY is gone after restart, but the cliSessionId
|
|
304
312
|
// is still resumable via `claude --resume <id>`. We mark the mode
|
|
305
313
|
// here so it shows up in the sidebar with the right icon; the
|
|
@@ -315,11 +323,26 @@ function createSessionManager(opts) {
|
|
|
315
323
|
session.terminalId = null;
|
|
316
324
|
session.runtimeMode = null;
|
|
317
325
|
session.runtimeTerminalId = null;
|
|
318
|
-
session.sessionVisibility = m
|
|
326
|
+
session.sessionVisibility = sessionVisibility.restore(m);
|
|
327
|
+
session.sessionVisibilityExplicit = m.sessionVisibilityExplicit === true;
|
|
319
328
|
session.bookmarked = !!m.bookmarked;
|
|
320
329
|
session.favoriteOrder = typeof m.favoriteOrder === "number" ? m.favoriteOrder : null;
|
|
321
330
|
sessions.set(localId, session);
|
|
322
331
|
}
|
|
332
|
+
|
|
333
|
+
// Versionless legacy records are safe top-level Drivers. Assigning an
|
|
334
|
+
// opaque origin ID is the only migration: no title, slug or active split
|
|
335
|
+
// state is used to invent historical parentage.
|
|
336
|
+
sessions.forEach(function (session) {
|
|
337
|
+
if (!session.cliSessionId || !session._provenanceMigrationNeeded) return;
|
|
338
|
+
var activityTime = session.lastActivity;
|
|
339
|
+
saveSessionFile(session);
|
|
340
|
+
try {
|
|
341
|
+
var preservedTime = new Date(activityTime);
|
|
342
|
+
fs.utimesSync(sessionFilePath(session.cliSessionId), preservedTime, preservedTime);
|
|
343
|
+
} catch (e) {}
|
|
344
|
+
delete session._provenanceMigrationNeeded;
|
|
345
|
+
});
|
|
323
346
|
}
|
|
324
347
|
|
|
325
348
|
// Adopt orphaned CLI sessions from ~/.claude/projects/<encoded-cwd>/ as
|
|
@@ -427,7 +450,7 @@ function createSessionManager(opts) {
|
|
|
427
450
|
terminalId: null,
|
|
428
451
|
runtimeMode: null,
|
|
429
452
|
runtimeTerminalId: null,
|
|
430
|
-
sessionVisibility: "
|
|
453
|
+
sessionVisibility: "private",
|
|
431
454
|
bookmarked: false,
|
|
432
455
|
favoriteOrder: null,
|
|
433
456
|
};
|
|
@@ -516,7 +539,7 @@ function createSessionManager(opts) {
|
|
|
516
539
|
loop: loop,
|
|
517
540
|
spawn: s.spawn || null,
|
|
518
541
|
ownerId: s.ownerId || null,
|
|
519
|
-
sessionVisibility: s.sessionVisibility
|
|
542
|
+
sessionVisibility: sessionVisibility.normalize(s.sessionVisibility),
|
|
520
543
|
bookmarked: !!s.bookmarked,
|
|
521
544
|
favoriteOrder: typeof s.favoriteOrder === "number" ? s.favoriteOrder : null,
|
|
522
545
|
unread: unreadMap[s.localId] || 0,
|
|
@@ -532,6 +555,19 @@ function createSessionManager(opts) {
|
|
|
532
555
|
};
|
|
533
556
|
}
|
|
534
557
|
|
|
558
|
+
function mapSessionsForClient(source, clientActiveId, wsUnread) {
|
|
559
|
+
var hierarchy = sessionProvenance.hierarchyFor(source);
|
|
560
|
+
return source.map(function (session) {
|
|
561
|
+
var mapped = mapSessionForClient(session, clientActiveId, wsUnread);
|
|
562
|
+
var node = hierarchy[session.localId];
|
|
563
|
+
mapped.sessionRole = node.role;
|
|
564
|
+
mapped.parentSessionId = node.parentSessionId;
|
|
565
|
+
mapped.parentAvailable = node.parentAvailable;
|
|
566
|
+
mapped.workerGeneration = node.generation;
|
|
567
|
+
return mapped;
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
535
571
|
function getVisibleSessions() {
|
|
536
572
|
var multiUser = users.isMultiUser();
|
|
537
573
|
return [...sessions.values()].filter(function (s) {
|
|
@@ -554,14 +590,14 @@ function createSessionManager(opts) {
|
|
|
554
590
|
if (ws.readyState === 1) {
|
|
555
591
|
ws.send(JSON.stringify({
|
|
556
592
|
type: "session_list",
|
|
557
|
-
sessions: filtered
|
|
593
|
+
sessions: mapSessionsForClient(filtered, clientActiveId, wsUnread),
|
|
558
594
|
}));
|
|
559
595
|
}
|
|
560
596
|
});
|
|
561
597
|
} else {
|
|
562
598
|
send({
|
|
563
599
|
type: "session_list",
|
|
564
|
-
sessions: allVisible
|
|
600
|
+
sessions: mapSessionsForClient(allVisible),
|
|
565
601
|
});
|
|
566
602
|
}
|
|
567
603
|
}
|
|
@@ -587,7 +623,8 @@ function createSessionManager(opts) {
|
|
|
587
623
|
history: [],
|
|
588
624
|
messageUUIDs: [],
|
|
589
625
|
ownerId: (sessionOpts && sessionOpts.ownerId) || null,
|
|
590
|
-
sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility)
|
|
626
|
+
sessionVisibility: sessionVisibility.normalize(sessionOpts && sessionOpts.sessionVisibility),
|
|
627
|
+
sessionVisibilityExplicit: !!(sessionOpts && sessionOpts.sessionVisibility === "shared"),
|
|
591
628
|
bookmarked: false,
|
|
592
629
|
favoriteOrder: null,
|
|
593
630
|
vendor: (sessionOpts && sessionOpts.vendor) || null,
|
|
@@ -598,7 +635,9 @@ function createSessionManager(opts) {
|
|
|
598
635
|
permissionMode: (sessionOpts && sessionOpts.permissionMode) || null,
|
|
599
636
|
permissionModeBeforeFullAccess: null,
|
|
600
637
|
terminalId: null,
|
|
638
|
+
sessionOriginId: null,
|
|
601
639
|
};
|
|
640
|
+
sessionProvenance.ensureOrigin(session);
|
|
602
641
|
sessions.set(localId, session);
|
|
603
642
|
switchSession(localId, targetWs);
|
|
604
643
|
return session;
|
|
@@ -626,7 +665,8 @@ function createSessionManager(opts) {
|
|
|
626
665
|
history: [],
|
|
627
666
|
messageUUIDs: [],
|
|
628
667
|
ownerId: (sessionOpts && sessionOpts.ownerId) || null,
|
|
629
|
-
sessionVisibility: (sessionOpts && sessionOpts.sessionVisibility)
|
|
668
|
+
sessionVisibility: sessionVisibility.normalize(sessionOpts && sessionOpts.sessionVisibility),
|
|
669
|
+
sessionVisibilityExplicit: !!(sessionOpts && sessionOpts.sessionVisibility === "shared"),
|
|
630
670
|
bookmarked: false,
|
|
631
671
|
favoriteOrder: null,
|
|
632
672
|
vendor: (sessionOpts && sessionOpts.vendor) || null,
|
|
@@ -637,7 +677,9 @@ function createSessionManager(opts) {
|
|
|
637
677
|
permissionMode: (sessionOpts && sessionOpts.permissionMode) || null,
|
|
638
678
|
permissionModeBeforeFullAccess: null,
|
|
639
679
|
terminalId: null,
|
|
680
|
+
sessionOriginId: null,
|
|
640
681
|
};
|
|
682
|
+
sessionProvenance.ensureOrigin(session);
|
|
641
683
|
sessions.set(localId, session);
|
|
642
684
|
return session;
|
|
643
685
|
}
|
|
@@ -879,7 +921,8 @@ function createSessionManager(opts) {
|
|
|
879
921
|
// Send to active clients without recording to history/disk (ephemeral data)
|
|
880
922
|
if (sendEach) {
|
|
881
923
|
var data = JSON.stringify(obj);
|
|
882
|
-
sendEach(function (ws) {
|
|
924
|
+
sendEach(function (ws, filterFn) {
|
|
925
|
+
if (filterFn && !filterFn(session)) return;
|
|
883
926
|
if (session.homeDebatePlanning === true) return;
|
|
884
927
|
if (ws._clayActiveSession === session.localId && ws.readyState === 1) {
|
|
885
928
|
ws.send(data);
|
|
@@ -910,7 +953,8 @@ function createSessionManager(opts) {
|
|
|
910
953
|
// Multi-user: send to clients whose active session matches this one
|
|
911
954
|
var data = JSON.stringify(outgoingObj);
|
|
912
955
|
var ioData = null;
|
|
913
|
-
sendEach(function (ws) {
|
|
956
|
+
sendEach(function (ws, filterFn) {
|
|
957
|
+
if (filterFn && !filterFn(session)) return;
|
|
914
958
|
if (session.homeDebatePlanning === true) return;
|
|
915
959
|
if (ws._clayActiveSession === session.localId) {
|
|
916
960
|
if (ws.readyState === 1) ws.send(data);
|
|
@@ -1248,11 +1292,14 @@ function createSessionManager(opts) {
|
|
|
1248
1292
|
searchSessionContent: searchSessionContent,
|
|
1249
1293
|
setResolveLoopInfo: setResolveLoopInfo,
|
|
1250
1294
|
mapSessionForClient: mapSessionForClient,
|
|
1295
|
+
mapSessionsForClient: mapSessionsForClient,
|
|
1251
1296
|
migrateSessionTitles: migrateSessionTitles,
|
|
1252
1297
|
setSessionVisibility: function (localId, visibility) {
|
|
1253
1298
|
var session = sessions.get(localId);
|
|
1254
1299
|
if (!session) return { error: "Session not found" };
|
|
1300
|
+
if (visibility !== "shared" && visibility !== "private") return { error: "Invalid session visibility" };
|
|
1255
1301
|
session.sessionVisibility = visibility;
|
|
1302
|
+
session.sessionVisibilityExplicit = true;
|
|
1256
1303
|
saveSessionFile(session);
|
|
1257
1304
|
broadcastSessionList();
|
|
1258
1305
|
return { ok: true };
|
package/lib/tools-registry.js
CHANGED
|
@@ -3,9 +3,10 @@ var path = require("path");
|
|
|
3
3
|
var config = require("./config");
|
|
4
4
|
var toolUiSpec = require("./tool-ui-spec");
|
|
5
5
|
var toolCapsuleSource = require("./tool-capsule-source");
|
|
6
|
+
var capsuleFloor = require("./capsule-display-floor");
|
|
6
7
|
|
|
7
8
|
var CAPSULES_ROOT = path.join(__dirname, "capsules");
|
|
8
|
-
var SEED_MARKER = ".capsules-
|
|
9
|
+
var SEED_MARKER = ".capsules-v10";
|
|
9
10
|
var REMOVED_BUILTIN_IDS = ["board", "translator", "scratchpad"];
|
|
10
11
|
var UI_NODE_TYPES = toolUiSpec.UI_NODE_TYPES;
|
|
11
12
|
var DISCOVERY_FIELD_MAX_LENGTH = 240;
|
|
@@ -98,6 +99,11 @@ function validateManifest(manifest, opts) {
|
|
|
98
99
|
if (manifest.skills !== undefined && typeof manifest.skills !== "string") {
|
|
99
100
|
throw new Error("Tool skills must be markdown text.");
|
|
100
101
|
}
|
|
102
|
+
// The floor gate always validates the Display itself, so a manifest may
|
|
103
|
+
// never carry a claim about it.
|
|
104
|
+
if (manifest.displayFloor !== undefined) {
|
|
105
|
+
throw new Error("Tool displayFloor cannot be authored; Clay validates the Display itself.");
|
|
106
|
+
}
|
|
101
107
|
validateDiscoveryField(manifest, "description", "description");
|
|
102
108
|
validateDiscoveryField(manifest, "useWhen", "useWhen");
|
|
103
109
|
if (manifest.permissions !== undefined) {
|
|
@@ -129,33 +135,90 @@ function toolDirectory(ctx, id) {
|
|
|
129
135
|
return path.join(resolveToolsRoot(ctx), id);
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
function hasAnySeedMarker(root) {
|
|
139
|
+
try {
|
|
140
|
+
return fs.readdirSync(root).some(function (name) { return /^\.capsules-v\d+$/.test(name); });
|
|
141
|
+
} catch (e) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
132
146
|
function seedBuiltInCapsules(ctx) {
|
|
133
147
|
var root = resolveToolsRoot(ctx);
|
|
134
148
|
var marker = path.join(root, SEED_MARKER);
|
|
135
149
|
if (fs.existsSync(marker)) return;
|
|
150
|
+
// The removed-built-in cleanup is a one-time migration. Any earlier marker
|
|
151
|
+
// means it already ran, so a later marker bump must not delete a Capsule the
|
|
152
|
+
// user has since authored under one of those IDs.
|
|
153
|
+
var migrated = hasAnySeedMarker(root);
|
|
136
154
|
fs.mkdirSync(root, { recursive: true });
|
|
137
|
-
for (var ri = 0; ri < REMOVED_BUILTIN_IDS.length; ri++) {
|
|
155
|
+
for (var ri = 0; !migrated && ri < REMOVED_BUILTIN_IDS.length; ri++) {
|
|
138
156
|
fs.rmSync(path.join(root, REMOVED_BUILTIN_IDS[ri]), { recursive: true, force: true });
|
|
139
157
|
}
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
158
|
+
// lib/capsules/ holds the shipped Capsules, currently the Pig validation
|
|
159
|
+
// Capsule. It may still be absent from an older checkout or tarball, and
|
|
160
|
+
// seeding also owns the removed-built-in migration above and the marker
|
|
161
|
+
// below, so a missing root means "nothing to copy" rather than a failure.
|
|
162
|
+
// Anything other than a missing root is still a real error.
|
|
145
163
|
var entries = [];
|
|
146
164
|
try {
|
|
147
165
|
entries = fs.readdirSync(CAPSULES_ROOT, { withFileTypes: true });
|
|
148
166
|
} catch (e) {
|
|
149
167
|
if (e.code !== "ENOENT") throw e;
|
|
150
168
|
}
|
|
169
|
+
// A previously seeded server-runtime Capsule that is no longer shipped is
|
|
170
|
+
// always a stale copy: the server runtime is reserved for lib/capsules/, so
|
|
171
|
+
// no user-authored Capsule can ever match this test. Remove it, or it would
|
|
172
|
+
// linger as a dead entry with no Logic behind it.
|
|
173
|
+
var shippedIds = Object.create(null);
|
|
174
|
+
for (var si = 0; si < entries.length; si++) {
|
|
175
|
+
if (entries[si].isDirectory()) shippedIds[entries[si].name] = true;
|
|
176
|
+
}
|
|
177
|
+
var rootEntries = [];
|
|
178
|
+
try {
|
|
179
|
+
rootEntries = fs.readdirSync(root, { withFileTypes: true });
|
|
180
|
+
} catch (e) {
|
|
181
|
+
if (e.code !== "ENOENT") throw e;
|
|
182
|
+
}
|
|
183
|
+
for (var re = 0; re < rootEntries.length; re++) {
|
|
184
|
+
if (!rootEntries[re].isDirectory() || shippedIds[rootEntries[re].name]) continue;
|
|
185
|
+
try {
|
|
186
|
+
var staleManifest = JSON.parse(fs.readFileSync(path.join(root, rootEntries[re].name, "manifest.json"), "utf8"));
|
|
187
|
+
if (staleManifest && staleManifest.runtime === "server") {
|
|
188
|
+
fs.rmSync(path.join(root, rootEntries[re].name), { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
} catch (e) { /* not a capsule directory; leave it alone */ }
|
|
191
|
+
}
|
|
192
|
+
|
|
151
193
|
var seeded = [];
|
|
194
|
+
var SOURCE_FILES = ["manifest.json", "ui.json", "logic.js", "display.js", "skills.md"];
|
|
152
195
|
for (var i = 0; i < entries.length; i++) {
|
|
153
196
|
if (!entries[i].isDirectory()) continue;
|
|
154
197
|
var destination = path.join(root, entries[i].name);
|
|
155
198
|
if (!fs.existsSync(destination)) {
|
|
156
199
|
fs.cpSync(path.join(CAPSULES_ROOT, entries[i].name), destination, { recursive: true });
|
|
157
200
|
seeded.push(entries[i].name);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
var shippedManifestPath = path.join(CAPSULES_ROOT, entries[i].name, "manifest.json");
|
|
204
|
+
var shippedRuntime = "worker";
|
|
205
|
+
try {
|
|
206
|
+
shippedRuntime = JSON.parse(fs.readFileSync(shippedManifestPath, "utf8")).runtime || "worker";
|
|
207
|
+
} catch (e) { /* fall through to the conservative worker path */ }
|
|
208
|
+
if (shippedRuntime === "server") {
|
|
209
|
+
// Server-runtime Capsules are Clay-owned: their source is not editable
|
|
210
|
+
// by users or Mates, so a marker bump syncs the shipped source files
|
|
211
|
+
// exactly, dropping ones the shipped set no longer carries. The
|
|
212
|
+
// Capsule's datastore and anything else in the directory is preserved.
|
|
213
|
+
for (var sf = 0; sf < SOURCE_FILES.length; sf++) {
|
|
214
|
+
var shippedFile = path.join(CAPSULES_ROOT, entries[i].name, SOURCE_FILES[sf]);
|
|
215
|
+
var localFile = path.join(destination, SOURCE_FILES[sf]);
|
|
216
|
+
if (fs.existsSync(shippedFile)) fs.copyFileSync(shippedFile, localFile);
|
|
217
|
+
else fs.rmSync(localFile, { force: true });
|
|
218
|
+
}
|
|
158
219
|
} else {
|
|
220
|
+
// Worker Capsules may have been edited by the user, so only files the
|
|
221
|
+
// local copy is missing entirely are filled in.
|
|
159
222
|
var sourceFiles = fs.readdirSync(path.join(CAPSULES_ROOT, entries[i].name));
|
|
160
223
|
for (var fi = 0; fi < sourceFiles.length; fi++) {
|
|
161
224
|
var sourceFile = path.join(CAPSULES_ROOT, entries[i].name, sourceFiles[fi]);
|
|
@@ -185,6 +248,11 @@ function getTool(ctx, id) {
|
|
|
185
248
|
manifest: Object.assign({}, manifest, { runtime: runtime }),
|
|
186
249
|
logicSource: runtime === "worker" ? fs.readFileSync(logicPath, "utf8") : null,
|
|
187
250
|
uiTree: uiTree,
|
|
251
|
+
// Rich Display is opted into by shipping a display.js next to the floor,
|
|
252
|
+
// the same way logic.js opts into the worker runtime. Only the flag
|
|
253
|
+
// travels; the source is served solely by the isolated frame origin, so
|
|
254
|
+
// it can never leak into a manifest, a prompt, or a Mate surface.
|
|
255
|
+
hasRichDisplay: fs.existsSync(path.join(directory, "display.js")),
|
|
188
256
|
metadata: toolCapsuleSource.sanitizedMetadata(resolveToolsRoot(ctx), id),
|
|
189
257
|
};
|
|
190
258
|
}
|
|
@@ -199,8 +267,14 @@ function listTools(ctx) {
|
|
|
199
267
|
if (entries[i].name.charAt(0) === ".") continue;
|
|
200
268
|
try {
|
|
201
269
|
var tool = getTool(ctx, entries[i].name);
|
|
202
|
-
if (tool)
|
|
203
|
-
|
|
270
|
+
if (tool) {
|
|
271
|
+
// The flat manifest carries its own Display alongside it, so a
|
|
272
|
+
// Mate-facing gate can validate the real tree without a second read
|
|
273
|
+
// and without trusting a claim. Non-enumerable, so the tree never
|
|
274
|
+
// leaks into a projection, a prompt, or a serialized message.
|
|
275
|
+
Object.defineProperty(tool.manifest, "uiTree", { value: tool.uiTree, enumerable: false, configurable: true });
|
|
276
|
+
manifests.push(tool.manifest);
|
|
277
|
+
} else manifests.push({ id: entries[i].name, error: "Capsule requires manifest.json and ui.json." });
|
|
204
278
|
} catch (e) {
|
|
205
279
|
manifests.push({ id: entries[i].name, error: e.message });
|
|
206
280
|
}
|
|
@@ -216,6 +290,7 @@ function installTool(ctx, input) {
|
|
|
216
290
|
throw new Error("Tool logic source is required.");
|
|
217
291
|
}
|
|
218
292
|
validateUiTreeForManifest(input.uiTree, input.manifest);
|
|
293
|
+
capsuleFloor.assertFloor(input.uiTree, input.manifest);
|
|
219
294
|
var manifest = Object.assign({}, input.manifest, { name: input.manifest.name.trim(), runtime: "worker" });
|
|
220
295
|
var directory = toolDirectory(ctx, manifest.id);
|
|
221
296
|
if (fs.existsSync(directory)) throw new Error("A Capsule with this ID already exists. Read its source and use the update tool instead.");
|
|
@@ -253,7 +328,10 @@ function updateTool(ctx, id, input) {
|
|
|
253
328
|
if (!tool) throw new Error("Tool not found.");
|
|
254
329
|
toolCapsuleSource.updateSource(toolDirectory(ctx, id), input, {
|
|
255
330
|
validateManifest: validateManifest,
|
|
256
|
-
validateUiNode: function (uiTree) {
|
|
331
|
+
validateUiNode: function (uiTree) {
|
|
332
|
+
validateUiTreeForManifest(uiTree, input.manifest);
|
|
333
|
+
return capsuleFloor.assertFloor(uiTree, input.manifest);
|
|
334
|
+
},
|
|
257
335
|
});
|
|
258
336
|
return getTool(ctx, id);
|
|
259
337
|
}
|
|
@@ -268,6 +346,7 @@ function removeTool(ctx, id) {
|
|
|
268
346
|
|
|
269
347
|
module.exports = {
|
|
270
348
|
CAPSULES_ROOT: CAPSULES_ROOT,
|
|
349
|
+
SEED_MARKER: SEED_MARKER,
|
|
271
350
|
UI_NODE_TYPES: UI_NODE_TYPES,
|
|
272
351
|
resolveToolsRoot: resolveToolsRoot,
|
|
273
352
|
validateToolId: validateToolId,
|