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,259 @@
|
|
|
1
|
+
// Vocabulary and validation for Project Logs.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from project-logs-store.js so the store stays focused on records
|
|
4
|
+
// and stays inside the module size limit.
|
|
5
|
+
//
|
|
6
|
+
// Two independent axes, deliberately:
|
|
7
|
+
// category - what kind of record this is, a project-local vocabulary
|
|
8
|
+
// priority - how much it matters, a stable global enum
|
|
9
|
+
// Conflating them is what made "an urgent decision" unrepresentable before.
|
|
10
|
+
//
|
|
11
|
+
// Category is NOT a global enum. Every project evolves its own vocabulary: an
|
|
12
|
+
// agent reuses an established category when one fits and coins a new one when
|
|
13
|
+
// the project needs a durable distinction it does not yet have. What is fixed
|
|
14
|
+
// is only the shape of a category label, not the set of labels. The seeds
|
|
15
|
+
// below are guidance for a project with no history yet, never a whitelist.
|
|
16
|
+
|
|
17
|
+
// Suggested starting points, surfaced in tool guidance only.
|
|
18
|
+
var SEED_CATEGORIES = [
|
|
19
|
+
"decision",
|
|
20
|
+
"security",
|
|
21
|
+
"idea",
|
|
22
|
+
"operations",
|
|
23
|
+
"incident",
|
|
24
|
+
"investigation",
|
|
25
|
+
"progress",
|
|
26
|
+
"reference",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
var MAX_CATEGORY_CHARS = 32;
|
|
30
|
+
// Letters and digits in any script, so a project can keep its vocabulary in the
|
|
31
|
+
// language it actually works in. Normalization runs server-side only, so this
|
|
32
|
+
// depends on Node's Unicode property escapes and never on a browser baseline.
|
|
33
|
+
var CATEGORY_PATTERN = /^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+)*$/u;
|
|
34
|
+
var CATEGORY_STRIP = /[^\p{L}\p{N}-]/gu;
|
|
35
|
+
|
|
36
|
+
// Length is counted in code points, not UTF-16 units, so a CJK or Hangul label
|
|
37
|
+
// is bounded the same way a Latin one is.
|
|
38
|
+
function codePointLength(value) {
|
|
39
|
+
return Array.from(value).length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Deterministic normalization to a lowercase, hyphen-separated label. Anything
|
|
43
|
+
// that cannot be reduced to a safe label is rejected rather than silently
|
|
44
|
+
// coerced, so a caller is told when its input was unusable instead of getting a
|
|
45
|
+
// surprising slug.
|
|
46
|
+
//
|
|
47
|
+
// Path and identity shapes are refused outright: a category is dry metadata and
|
|
48
|
+
// must never be able to carry a slug, a path segment, or a user id.
|
|
49
|
+
function normalizeCategory(value) {
|
|
50
|
+
if (typeof value !== "string") throw new Error("A log category must be a string.");
|
|
51
|
+
var raw = value.normalize("NFC").trim();
|
|
52
|
+
if (!raw) throw new Error("A log category is required.");
|
|
53
|
+
if (codePointLength(raw) > MAX_CATEGORY_CHARS * 2) {
|
|
54
|
+
throw new Error("A log category must be " + MAX_CATEGORY_CHARS + " characters or fewer.");
|
|
55
|
+
}
|
|
56
|
+
if (/[\/\\]/.test(raw) || raw.indexOf("..") !== -1) {
|
|
57
|
+
throw new Error("A log category cannot contain path characters.");
|
|
58
|
+
}
|
|
59
|
+
var slug = raw
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[_\s]+/g, "-")
|
|
62
|
+
.replace(CATEGORY_STRIP, "")
|
|
63
|
+
.replace(/-+/g, "-")
|
|
64
|
+
.replace(/^-|-$/g, "");
|
|
65
|
+
if (!slug) throw new Error("A log category must contain letters or digits: " + value);
|
|
66
|
+
if (codePointLength(slug) > MAX_CATEGORY_CHARS) {
|
|
67
|
+
throw new Error("A log category must be " + MAX_CATEGORY_CHARS + " characters or fewer.");
|
|
68
|
+
}
|
|
69
|
+
if (!CATEGORY_PATTERN.test(slug)) throw new Error("Invalid log category: " + value);
|
|
70
|
+
return slug;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Non-throwing form for reading stored records, where a corrupt on-disk value
|
|
74
|
+
// should make that record unusable rather than break the whole projection.
|
|
75
|
+
// Filters deliberately do NOT use this: a caller that supplies a malformed
|
|
76
|
+
// category is told so.
|
|
77
|
+
function safeCategory(value) {
|
|
78
|
+
try {
|
|
79
|
+
return normalizeCategory(value);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var PRIORITIES = ["normal", "important", "urgent"];
|
|
86
|
+
var DEFAULT_PRIORITY = "normal";
|
|
87
|
+
|
|
88
|
+
var MAX_SUMMARY_CHARS = 400;
|
|
89
|
+
|
|
90
|
+
function isPriority(value) {
|
|
91
|
+
return PRIORITIES.indexOf(value) !== -1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizePriority(value) {
|
|
95
|
+
return isPriority(value) ? value : DEFAULT_PRIORITY;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A summary for an entry written before summaries existed. Deterministic and
|
|
99
|
+
// derived from stored content only, so the same record always yields the same
|
|
100
|
+
// text and nothing is invented.
|
|
101
|
+
function fallbackSummary(entry) {
|
|
102
|
+
if (!entry) return "";
|
|
103
|
+
if (typeof entry.summary === "string" && entry.summary.trim()) {
|
|
104
|
+
return entry.summary.trim().substring(0, MAX_SUMMARY_CHARS);
|
|
105
|
+
}
|
|
106
|
+
// Prefer the first line of prose. A leading Markdown heading usually just
|
|
107
|
+
// restates the title, so it is only used when there is nothing else.
|
|
108
|
+
var body = typeof entry.body === "string" ? entry.body : "";
|
|
109
|
+
var lines = body.split("\n");
|
|
110
|
+
var heading = "";
|
|
111
|
+
var firstLine = "";
|
|
112
|
+
for (var i = 0; i < lines.length; i++) {
|
|
113
|
+
var raw = lines[i].trim();
|
|
114
|
+
if (!raw) continue;
|
|
115
|
+
var isHeading = raw.charAt(0) === "#";
|
|
116
|
+
var line = raw.replace(/^#+\s*/, "").replace(/^[-*>]\s*/, "").trim();
|
|
117
|
+
if (!line) continue;
|
|
118
|
+
if (isHeading) {
|
|
119
|
+
if (!heading) heading = line;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
firstLine = line;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
if (!firstLine) firstLine = heading;
|
|
126
|
+
if (!firstLine) return "";
|
|
127
|
+
return firstLine.length > MAX_SUMMARY_CHARS
|
|
128
|
+
? firstLine.substring(0, MAX_SUMMARY_CHARS)
|
|
129
|
+
: firstLine;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Validation for a new agent-written record. Category, title, and summary are
|
|
133
|
+
// all required: a log without a summary cannot be understood from a list, and
|
|
134
|
+
// that is the whole point of the surface.
|
|
135
|
+
function validateNewRecord(input) {
|
|
136
|
+
var data = input || {};
|
|
137
|
+
var category = normalizeCategory(data.category || data.kind);
|
|
138
|
+
if (typeof data.title !== "string" || !data.title.trim()) throw new Error("A log title is required.");
|
|
139
|
+
if (typeof data.summary !== "string" || !data.summary.trim()) {
|
|
140
|
+
throw new Error("A one or two sentence summary is required so the entry is understandable from a list.");
|
|
141
|
+
}
|
|
142
|
+
// A mistyped priority is refused rather than silently downgraded to normal:
|
|
143
|
+
// an agent that meant "urgent" must not get "normal" without being told.
|
|
144
|
+
if (data.priority !== undefined && data.priority !== null && !isPriority(data.priority)) {
|
|
145
|
+
throw new Error("Unknown log priority: " + data.priority);
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
category: category,
|
|
149
|
+
priority: normalizePriority(data.priority),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Validation for a revision. The category may move to another writable value
|
|
154
|
+
// or stay on its legacy one; nothing is required, but nothing may be emptied.
|
|
155
|
+
function validateRevision(input) {
|
|
156
|
+
var data = input || {};
|
|
157
|
+
var out = {};
|
|
158
|
+
if (data.category !== undefined || data.kind !== undefined) {
|
|
159
|
+
out.category = normalizeCategory(data.category || data.kind);
|
|
160
|
+
}
|
|
161
|
+
if (data.priority !== undefined) {
|
|
162
|
+
if (!isPriority(data.priority)) throw new Error("Unknown log priority: " + data.priority);
|
|
163
|
+
out.priority = data.priority;
|
|
164
|
+
}
|
|
165
|
+
if (data.title !== undefined) {
|
|
166
|
+
if (typeof data.title !== "string" || !data.title.trim()) throw new Error("A log title cannot be emptied.");
|
|
167
|
+
out.title = data.title;
|
|
168
|
+
}
|
|
169
|
+
if (data.summary !== undefined) {
|
|
170
|
+
if (typeof data.summary !== "string" || !data.summary.trim()) throw new Error("A log summary cannot be emptied.");
|
|
171
|
+
out.summary = data.summary;
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
// --- Normalization -------------------------------------------------------
|
|
178
|
+
//
|
|
179
|
+
// Moved here from project-logs-store.js so the store stays inside the module
|
|
180
|
+
// size limit. These are pure text and shape helpers with no record semantics.
|
|
181
|
+
|
|
182
|
+
var MAX_TAGS = 12;
|
|
183
|
+
var MAX_TAG_CHARS = 40;
|
|
184
|
+
var MAX_LINKS = 20;
|
|
185
|
+
var MAX_LINK_CHARS = 200;
|
|
186
|
+
|
|
187
|
+
function cleanText(value, max) {
|
|
188
|
+
if (typeof value !== "string") return "";
|
|
189
|
+
var text = value.replace(/\u0000/g, "").trim();
|
|
190
|
+
return text.length > max ? text.substring(0, max) : text;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function cleanLine(value, max) {
|
|
194
|
+
return cleanText(value, max).replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeAuthor(author) {
|
|
198
|
+
var source = author || {};
|
|
199
|
+
var type = source.type === "user" ? "user" : "session";
|
|
200
|
+
return {
|
|
201
|
+
type: type,
|
|
202
|
+
userId: source.userId ? cleanLine(String(source.userId), 120) : null,
|
|
203
|
+
displayName: cleanLine(source.displayName || "", 120) || null,
|
|
204
|
+
sessionKey: source.sessionKey ? cleanLine(String(source.sessionKey), 200) : null,
|
|
205
|
+
vendor: cleanLine(source.vendor || "", 40) || null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeTags(tags) {
|
|
210
|
+
if (!Array.isArray(tags)) return [];
|
|
211
|
+
var out = [];
|
|
212
|
+
for (var i = 0; i < tags.length && out.length < MAX_TAGS; i++) {
|
|
213
|
+
var tag = cleanLine(String(tags[i] == null ? "" : tags[i]), MAX_TAG_CHARS).toLowerCase();
|
|
214
|
+
if (tag && out.indexOf(tag) === -1) out.push(tag);
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeLinks(links) {
|
|
220
|
+
if (!Array.isArray(links)) return [];
|
|
221
|
+
var out = [];
|
|
222
|
+
for (var i = 0; i < links.length && out.length < MAX_LINKS; i++) {
|
|
223
|
+
var raw = links[i];
|
|
224
|
+
var value = typeof raw === "string" ? { ref: raw } : (raw || {});
|
|
225
|
+
var ref = cleanLine(String(value.ref || value.sessionRef || ""), MAX_LINK_CHARS);
|
|
226
|
+
if (!ref) continue;
|
|
227
|
+
var label = cleanLine(value.label || "", MAX_LINK_CHARS) || null;
|
|
228
|
+
var duplicate = false;
|
|
229
|
+
for (var j = 0; j < out.length; j++) {
|
|
230
|
+
if (out[j].ref === ref) { duplicate = true; break; }
|
|
231
|
+
}
|
|
232
|
+
if (!duplicate) out.push({ ref: ref, label: label });
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
SEED_CATEGORIES: SEED_CATEGORIES,
|
|
239
|
+
MAX_CATEGORY_CHARS: MAX_CATEGORY_CHARS,
|
|
240
|
+
CATEGORY_PATTERN: CATEGORY_PATTERN,
|
|
241
|
+
codePointLength: codePointLength,
|
|
242
|
+
normalizeCategory: normalizeCategory,
|
|
243
|
+
safeCategory: safeCategory,
|
|
244
|
+
PRIORITIES: PRIORITIES,
|
|
245
|
+
DEFAULT_PRIORITY: DEFAULT_PRIORITY,
|
|
246
|
+
MAX_SUMMARY_CHARS: MAX_SUMMARY_CHARS,
|
|
247
|
+
isPriority: isPriority,
|
|
248
|
+
normalizePriority: normalizePriority,
|
|
249
|
+
fallbackSummary: fallbackSummary,
|
|
250
|
+
MAX_TAGS: MAX_TAGS,
|
|
251
|
+
MAX_LINKS: MAX_LINKS,
|
|
252
|
+
cleanText: cleanText,
|
|
253
|
+
cleanLine: cleanLine,
|
|
254
|
+
normalizeAuthor: normalizeAuthor,
|
|
255
|
+
normalizeTags: normalizeTags,
|
|
256
|
+
normalizeLinks: normalizeLinks,
|
|
257
|
+
validateNewRecord: validateNewRecord,
|
|
258
|
+
validateRevision: validateRevision,
|
|
259
|
+
};
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// Authorization and binding layer for Project Logs.
|
|
2
|
+
//
|
|
3
|
+
// Modeled on workspace-query-service.js: a caller never states who it is.
|
|
4
|
+
// Every binder re-derives the principal from authoritative server state and
|
|
5
|
+
// returns null when anything fails to line up, so an unbound or stale caller
|
|
6
|
+
// gets no capability rather than a degraded one.
|
|
7
|
+
//
|
|
8
|
+
// Product rules enforced here:
|
|
9
|
+
// - Project Logs are project records. Mate projects have none.
|
|
10
|
+
// - Ordinary Mates cannot reach project Logs at all.
|
|
11
|
+
// - Authoritative builtin Clay may read Logs, and only for projects the
|
|
12
|
+
// owning user is authorized to see. It may never write.
|
|
13
|
+
// - A project session is bound to exactly its own project's Logs.
|
|
14
|
+
// - Canonical authorship belongs to Project Driver sessions alone. A human,
|
|
15
|
+
// however privileged, may read and comment but may never create or revise
|
|
16
|
+
// a canonical entry: the log is the agent's record of the work.
|
|
17
|
+
// - Members of a shared project read each other's entries and comments with
|
|
18
|
+
// attribution.
|
|
19
|
+
// - A worktree resolves to its parent project for both authorization and
|
|
20
|
+
// storage, so worktree work never forks project knowledge.
|
|
21
|
+
|
|
22
|
+
var builtinMates = require("./builtin-mates");
|
|
23
|
+
var logsStore = require("./project-logs-store");
|
|
24
|
+
|
|
25
|
+
function readOnly() {
|
|
26
|
+
throw new Error("Project Logs are read-only for builtin Clay.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Canonical entries are written by the project's own agent sessions. A human
|
|
30
|
+
// participates by commenting, which is a separate, append-only surface.
|
|
31
|
+
function agentOnly() {
|
|
32
|
+
throw new Error("Project Logs are written by this project's agent sessions. Add a comment instead.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Reviewing feedback and moving the record between revisions are the Driver's
|
|
36
|
+
// judgement calls. A human cannot review their own comment into the record,
|
|
37
|
+
// and Clay cannot review another project's.
|
|
38
|
+
function driverOnly() {
|
|
39
|
+
throw new Error("Only this project's agent sessions can review feedback or change revisions.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Bounded discovery of comments still waiting on the Driver. The store owns the
|
|
43
|
+
// one-pass scan over every live entry, so nothing here imposes a ceiling that
|
|
44
|
+
// could hide a pending comment on an older record.
|
|
45
|
+
function attachProjectLogsService(ctx) {
|
|
46
|
+
var context = ctx || {};
|
|
47
|
+
var getProjects = context.getProjects;
|
|
48
|
+
var isMultiUser = context.isMultiUser;
|
|
49
|
+
var resolveMate = context.resolveMate;
|
|
50
|
+
var canAccessProject = context.canAccessProject;
|
|
51
|
+
var findUserById = context.findUserById || function () { return null; };
|
|
52
|
+
var storeCache = new Map();
|
|
53
|
+
|
|
54
|
+
var openStore = context.openStore || function (cwd) {
|
|
55
|
+
if (!storeCache.has(cwd)) {
|
|
56
|
+
storeCache.set(cwd, logsStore.createProjectLogsStore({ cwd: cwd, baseDir: context.baseDir }));
|
|
57
|
+
}
|
|
58
|
+
return storeCache.get(cwd);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function projectFor(slug) {
|
|
62
|
+
if (!slug || typeof slug !== "string") return null;
|
|
63
|
+
var projects = getProjects();
|
|
64
|
+
return (projects && projects.get(slug)) || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A worktree inherits its parent's visibility, so authorization is decided
|
|
68
|
+
// against the parent whenever the parent is registered.
|
|
69
|
+
function effectiveStatus(status) {
|
|
70
|
+
if (!status || status.isWorktree !== true || !status.parentSlug) return status;
|
|
71
|
+
var parent = projectFor(status.parentSlug);
|
|
72
|
+
if (!parent) return status;
|
|
73
|
+
var parentStatus = parent.getStatus();
|
|
74
|
+
return parentStatus || status;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ownsProject(principal, status) {
|
|
78
|
+
if (!status) return false;
|
|
79
|
+
if (isMultiUser()) return !!principal.userId && status.projectOwnerId === principal.userId;
|
|
80
|
+
return !status.projectOwnerId || (!!principal.singleUserOwnerId && status.projectOwnerId === principal.singleUserOwnerId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function authorizedProject(principal, status) {
|
|
84
|
+
if (!status) return false;
|
|
85
|
+
if (status.isMate === true) return false;
|
|
86
|
+
var governing = effectiveStatus(status);
|
|
87
|
+
if (ownsProject(principal, governing)) return true;
|
|
88
|
+
if (isMultiUser() && principal.userId && typeof canAccessProject === "function") {
|
|
89
|
+
return canAccessProject(principal.userId, governing) === true;
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Open the store for the governing project. When a worktree's parent is
|
|
95
|
+
// registered, its path is authoritative, so worktree Logs are shared without
|
|
96
|
+
// depending on a Git subprocess. The store's own root resolution remains the
|
|
97
|
+
// fallback for unregistered parents and for subdirectories of a checkout.
|
|
98
|
+
function storeForStatus(status) {
|
|
99
|
+
var governing = effectiveStatus(status);
|
|
100
|
+
if (!governing || !governing.path) throw new Error("Project Logs are unavailable for this project.");
|
|
101
|
+
return openStore(governing.path);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function statusFor(principal, slug) {
|
|
105
|
+
var project = projectFor(slug);
|
|
106
|
+
if (!project) throw new Error("Project not found.");
|
|
107
|
+
var status = project.getStatus();
|
|
108
|
+
if (!authorizedProject(principal, status)) throw new Error("Project Logs are not available for this project.");
|
|
109
|
+
return status;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function displayNameFor(userId) {
|
|
113
|
+
if (!userId) return null;
|
|
114
|
+
var user = findUserById(userId);
|
|
115
|
+
if (!user) return null;
|
|
116
|
+
return user.displayName || user.username || null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sessionAuthor(session) {
|
|
120
|
+
return {
|
|
121
|
+
type: "session",
|
|
122
|
+
userId: session.ownerId || null,
|
|
123
|
+
displayName: displayNameFor(session.ownerId),
|
|
124
|
+
sessionKey: session.cliSessionId || "local:" + session.localId,
|
|
125
|
+
vendor: session.vendor || null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function userAuthor(user) {
|
|
130
|
+
return {
|
|
131
|
+
type: "user",
|
|
132
|
+
userId: (user && user.id) || null,
|
|
133
|
+
displayName: (user && (user.displayName || user.username)) || null,
|
|
134
|
+
sessionKey: null,
|
|
135
|
+
vendor: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// --- Binders ---------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
// Exact session object identity, not a caller-supplied session id. A session
|
|
142
|
+
// handed in from a stale or forged reference is rejected even when its
|
|
143
|
+
// localId matches a live session.
|
|
144
|
+
function resolveProjectSession(source) {
|
|
145
|
+
if (!source || !source.projectSlug || !source.session) return null;
|
|
146
|
+
var project = projectFor(source.projectSlug);
|
|
147
|
+
if (!project) return null;
|
|
148
|
+
var status = project.getStatus();
|
|
149
|
+
if (!status || status.isMate === true) return null;
|
|
150
|
+
var manager = project.getSessionManager ? project.getSessionManager() : project.sm;
|
|
151
|
+
if (!manager || !manager.sessions || manager.sessions.get(source.session.localId) !== source.session) return null;
|
|
152
|
+
|
|
153
|
+
var principal;
|
|
154
|
+
if (isMultiUser()) {
|
|
155
|
+
// Fail closed: an unattributed session in multi-user mode gets nothing.
|
|
156
|
+
if (!source.session.ownerId) return null;
|
|
157
|
+
principal = { userId: source.session.ownerId, singleUserOwnerId: null };
|
|
158
|
+
} else {
|
|
159
|
+
if (source.session.ownerId && status.projectOwnerId && source.session.ownerId !== status.projectOwnerId) return null;
|
|
160
|
+
principal = { userId: null, singleUserOwnerId: source.session.ownerId || status.projectOwnerId || null };
|
|
161
|
+
}
|
|
162
|
+
if (!authorizedProject(principal, status)) return null;
|
|
163
|
+
|
|
164
|
+
principal.kind = "session";
|
|
165
|
+
principal.projectSlug = source.projectSlug;
|
|
166
|
+
principal.isClay = false;
|
|
167
|
+
return { principal: principal, status: status, author: sessionAuthor(source.session) };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// A tool handler captured earlier stays valid only while its exact source
|
|
171
|
+
// still resolves. Session identity and project authorization are both
|
|
172
|
+
// re-derived on every call, so a session dropped from the manager, or
|
|
173
|
+
// replaced by a different object with the same id, loses the capability
|
|
174
|
+
// immediately rather than continuing to work through a captured handler.
|
|
175
|
+
function bindProjectSession(source) {
|
|
176
|
+
var initial = resolveProjectSession(source);
|
|
177
|
+
if (!initial) return null;
|
|
178
|
+
function revalidate() {
|
|
179
|
+
var current = resolveProjectSession(source);
|
|
180
|
+
if (!current || current.principal.projectSlug !== initial.principal.projectSlug) {
|
|
181
|
+
throw new Error("This Project Logs binding is no longer valid for the current session.");
|
|
182
|
+
}
|
|
183
|
+
return current;
|
|
184
|
+
}
|
|
185
|
+
return createProjectBound(initial, revalidate);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function bindUser(source) {
|
|
189
|
+
if (!source || !source.projectSlug) return null;
|
|
190
|
+
var project = projectFor(source.projectSlug);
|
|
191
|
+
if (!project) return null;
|
|
192
|
+
var status = project.getStatus();
|
|
193
|
+
if (!status || status.isMate === true) return null;
|
|
194
|
+
var user = source.user || null;
|
|
195
|
+
|
|
196
|
+
var principal;
|
|
197
|
+
if (isMultiUser()) {
|
|
198
|
+
if (!user || !user.id) return null;
|
|
199
|
+
principal = { userId: user.id, singleUserOwnerId: null };
|
|
200
|
+
} else {
|
|
201
|
+
principal = { userId: null, singleUserOwnerId: (user && user.id) || status.projectOwnerId || null };
|
|
202
|
+
}
|
|
203
|
+
if (!authorizedProject(principal, status)) return null;
|
|
204
|
+
|
|
205
|
+
principal.kind = "user";
|
|
206
|
+
principal.projectSlug = source.projectSlug;
|
|
207
|
+
principal.isClay = false;
|
|
208
|
+
var envelope = { principal: principal, status: status, author: userAuthor(user) };
|
|
209
|
+
return createProjectBound(envelope, function () { return envelope; });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Ordinary Mates get null. Only the authoritative builtin Clay host agent,
|
|
213
|
+
// confirmed against the server Mate registry rather than a project flag,
|
|
214
|
+
// receives a read-only cross-project binding.
|
|
215
|
+
function resolveMateSource(source) {
|
|
216
|
+
if (!source || source.isMate !== true || !source.mateId || typeof resolveMate !== "function") return null;
|
|
217
|
+
var sourceProject = projectFor(source.projectSlug);
|
|
218
|
+
if (!sourceProject) return null;
|
|
219
|
+
var sourceStatus = sourceProject.getStatus();
|
|
220
|
+
if (!sourceStatus || sourceStatus.isMate !== true) return null;
|
|
221
|
+
if (sourceStatus.mateId !== source.mateId || sourceStatus.projectOwnerId !== source.projectOwnerId) return null;
|
|
222
|
+
if (source.session) {
|
|
223
|
+
var manager = sourceProject.getSessionManager ? sourceProject.getSessionManager() : sourceProject.sm;
|
|
224
|
+
if (!manager || !manager.sessions || manager.sessions.get(source.session.localId) !== source.session) return null;
|
|
225
|
+
}
|
|
226
|
+
var mate = resolveMate(source.projectOwnerId || null, source.mateId);
|
|
227
|
+
if (!mate || mate.id !== source.mateId) return null;
|
|
228
|
+
if (isMultiUser() && (!source.projectOwnerId || mate.createdBy !== source.projectOwnerId)) return null;
|
|
229
|
+
|
|
230
|
+
var def = mate.builtinKey ? builtinMates.getBuiltinByKey(mate.builtinKey) : null;
|
|
231
|
+
var isClay = !!(mate.builtinKey === "clay" && def && def.hostAgent === true);
|
|
232
|
+
if (!isClay) return null;
|
|
233
|
+
if (isMultiUser() && !source.projectOwnerId) return null;
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
kind: "clay",
|
|
237
|
+
userId: isMultiUser() ? source.projectOwnerId : null,
|
|
238
|
+
singleUserOwnerId: isMultiUser() ? null : (source.projectOwnerId || null),
|
|
239
|
+
projectSlug: null,
|
|
240
|
+
mateId: mate.id,
|
|
241
|
+
isClay: true,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Clay keeps its authoritative registry and user authorization checks on
|
|
246
|
+
// every call, so a Mate that is deleted, demoted out of builtin Clay, or
|
|
247
|
+
// reassigned to another user loses the read view immediately.
|
|
248
|
+
function bindMate(source) {
|
|
249
|
+
var initial = resolveMateSource(source);
|
|
250
|
+
if (!initial) return null;
|
|
251
|
+
function revalidate() {
|
|
252
|
+
var current = resolveMateSource(source);
|
|
253
|
+
if (!current || current.mateId !== initial.mateId || current.userId !== initial.userId) {
|
|
254
|
+
throw new Error("This Project Logs binding is no longer valid for the current session.");
|
|
255
|
+
}
|
|
256
|
+
return current;
|
|
257
|
+
}
|
|
258
|
+
return createClayBound(revalidate);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --- Bound capability surfaces ---------------------------------------
|
|
262
|
+
|
|
263
|
+
function createProjectBound(initial, revalidate) {
|
|
264
|
+
var store = storeForStatus(initial.status);
|
|
265
|
+
|
|
266
|
+
// Identity, authorization, and the target store are all re-derived per
|
|
267
|
+
// call. revalidate() re-checks exact session identity where there is one;
|
|
268
|
+
// the project authorization check then catches a visibility change.
|
|
269
|
+
function live() {
|
|
270
|
+
var current = revalidate();
|
|
271
|
+
var project = projectFor(current.principal.projectSlug);
|
|
272
|
+
if (!project) throw new Error("Project not found.");
|
|
273
|
+
var currentStatus = project.getStatus();
|
|
274
|
+
if (!authorizedProject(current.principal, currentStatus)) throw new Error("Project Logs are not available for this project.");
|
|
275
|
+
return storeForStatus(currentStatus);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function author() {
|
|
279
|
+
return revalidate().author;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// A session may write canonically; a user may only comment. The capability
|
|
283
|
+
// is decided by the binder that produced this envelope, never by a flag on
|
|
284
|
+
// the request.
|
|
285
|
+
var canonical = initial.principal.kind === "session";
|
|
286
|
+
return {
|
|
287
|
+
isClay: false,
|
|
288
|
+
canWrite: canonical,
|
|
289
|
+
canComment: true,
|
|
290
|
+
projectSlug: initial.principal.projectSlug,
|
|
291
|
+
scopeId: store.scopeId,
|
|
292
|
+
root: store.root,
|
|
293
|
+
author: initial.author,
|
|
294
|
+
listLogs: function (args) { return live().list(args || {}); },
|
|
295
|
+
searchLogs: function (args) { return live().search(args || {}); },
|
|
296
|
+
readLog: function (args) {
|
|
297
|
+
var entry = live().read(args && args.ref, false);
|
|
298
|
+
if (!entry) throw new Error("Log entry not found.");
|
|
299
|
+
return entry;
|
|
300
|
+
},
|
|
301
|
+
logHistory: function (args) { return live().history(args && args.ref, args || {}); },
|
|
302
|
+
commentLog: function (args) { return live().comment(args && args.ref, args || {}, author()); },
|
|
303
|
+
readLogRevision: function (args) { return live().readRevision(args && args.ref, args && args.revision); },
|
|
304
|
+
listLogFeedback: canonical ? function (args) { return live().feedback(args || {}); } : driverOnly,
|
|
305
|
+
reviewLogComment: canonical ? function (args) { return live().review(args && args.ref, args || {}, author()); } : driverOnly,
|
|
306
|
+
revertLog: canonical
|
|
307
|
+
? function (args) { return live().revert(args && args.ref, args && args.revision, args && args.reason, author()); }
|
|
308
|
+
: driverOnly,
|
|
309
|
+
createLog: canonical ? function (args) { return live().create(args || {}, author()); } : agentOnly,
|
|
310
|
+
updateLog: canonical ? function (args) { return live().update(args && args.ref, args || {}, author()); } : agentOnly,
|
|
311
|
+
linkLog: canonical ? function (args) { return live().link(args && args.ref, args && args.links, author()); } : agentOnly,
|
|
312
|
+
removeLog: canonical ? function (args) { return live().remove(args && args.ref, author()); } : agentOnly,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function createClayBound(revalidate) {
|
|
317
|
+
function target(args) {
|
|
318
|
+
var principal = revalidate();
|
|
319
|
+
var slug = args && args.projectSlug;
|
|
320
|
+
if (!slug) throw new Error("An exact project slug is required.");
|
|
321
|
+
return storeForStatus(statusFor(principal, slug));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
isClay: true,
|
|
326
|
+
canWrite: false,
|
|
327
|
+
canComment: false,
|
|
328
|
+
projectSlug: null,
|
|
329
|
+
author: null,
|
|
330
|
+
listLogs: function (args) { return target(args).list(args || {}); },
|
|
331
|
+
searchLogs: function (args) { return target(args).search(args || {}); },
|
|
332
|
+
readLog: function (args) {
|
|
333
|
+
var entry = target(args).read(args && args.ref, false);
|
|
334
|
+
if (!entry) throw new Error("Log entry not found.");
|
|
335
|
+
return entry;
|
|
336
|
+
},
|
|
337
|
+
logHistory: function (args) { return target(args).history(args && args.ref, args || {}); },
|
|
338
|
+
commentLog: readOnly,
|
|
339
|
+
readLogRevision: function (args) { return target(args).readRevision(args && args.ref, args && args.revision); },
|
|
340
|
+
listLogFeedback: readOnly,
|
|
341
|
+
reviewLogComment: readOnly,
|
|
342
|
+
revertLog: readOnly,
|
|
343
|
+
createLog: readOnly,
|
|
344
|
+
updateLog: readOnly,
|
|
345
|
+
linkLog: readOnly,
|
|
346
|
+
removeLog: readOnly,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
bindProjectSession: bindProjectSession,
|
|
352
|
+
bindUser: bindUser,
|
|
353
|
+
bindMate: bindMate,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
module.exports = { attachProjectLogsService: attachProjectLogsService };
|