clay-server 4.0.0-beta.2 → 4.0.0-beta.21
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/background-task-timing.js +105 -0
- 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 +14 -1
- 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-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-vendor-login.js +20 -3
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +178 -6
- package/lib/public/app.js +58 -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 +163 -28
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +52 -0
- 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 +34 -1
- package/lib/public/index.html +45 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +109 -4
- package/lib/public/modules/app-home-hub.js +19 -7
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +84 -7
- 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 +46 -4
- package/lib/public/modules/app-projects.js +13 -8
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/background-tasks-ui.js +139 -16
- 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-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-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-view.js +5 -2
- 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/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 +21 -1
- 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 -12
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +132 -46
- package/lib/sdk-message-processor.js +60 -14
- 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 +124 -18
- 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 +39 -0
- package/lib/sessions.js +61 -11
- 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 +2 -2
- 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 +81 -4
- package/lib/yoke/adapters/codex.js +63 -15
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
// Project Logs message handling and session-bound MCP wiring.
|
|
2
|
+
//
|
|
3
|
+
// Every capability here is derived server-side. The WebSocket path binds from
|
|
4
|
+
// ws._clayUser plus this project's own slug, and the MCP path binds from the
|
|
5
|
+
// exact live session object. Neither accepts identity or project scope from
|
|
6
|
+
// the message, so a client cannot widen its own reach by asking.
|
|
7
|
+
//
|
|
8
|
+
// Authorship is split deliberately. Canonical entries are created and revised
|
|
9
|
+
// only by this project's agent sessions through `clay-logs`; a connected human
|
|
10
|
+
// reads the ledger, appends attributed comments, and lets the project owner
|
|
11
|
+
// delete an entry. That keeps the log an honest record rather than a wiki.
|
|
12
|
+
|
|
13
|
+
var logsMcp = require("./project-logs-mcp-server");
|
|
14
|
+
|
|
15
|
+
var LIST_LIMIT = 50;
|
|
16
|
+
var MAX_NOTICE_TITLE_CHARS = 160;
|
|
17
|
+
var MAX_NOTICE_SUMMARY_CHARS = 240;
|
|
18
|
+
|
|
19
|
+
// Canonical writes that move the ledger forward. A plain comment, a
|
|
20
|
+
// clarification, and a decline are participation, not revisions, so they never
|
|
21
|
+
// notify.
|
|
22
|
+
var NOTIFYING_OPS = ["create", "update", "link", "incorporate", "revert", "delete"];
|
|
23
|
+
|
|
24
|
+
function clip(value, max) {
|
|
25
|
+
if (typeof value !== "string") return "";
|
|
26
|
+
return value.length > max ? value.substring(0, max) : value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Bounded ledger metadata only. No body, no comment text, no author identity
|
|
30
|
+
// beyond the vendor that wrote it.
|
|
31
|
+
function updateNotice(entry, op) {
|
|
32
|
+
return {
|
|
33
|
+
type: "project_log_updated",
|
|
34
|
+
ref: entry.ref,
|
|
35
|
+
revision: entry.revisions,
|
|
36
|
+
op: op,
|
|
37
|
+
category: entry.category,
|
|
38
|
+
priority: entry.priority,
|
|
39
|
+
title: clip(entry.title, MAX_NOTICE_TITLE_CHARS),
|
|
40
|
+
summary: clip(entry.summary, MAX_NOTICE_SUMMARY_CHARS),
|
|
41
|
+
at: entry.updatedAt,
|
|
42
|
+
vendor: (entry.updatedBy && entry.updatedBy.vendor) || null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
var SYSTEM_PROMPT_LABEL = "--- Project Logs (durable project record; manage via clay-logs tools) ---";
|
|
47
|
+
|
|
48
|
+
function parseJsonArray(value) {
|
|
49
|
+
if (Array.isArray(value)) return value;
|
|
50
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
51
|
+
var parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(value);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
throw new Error("Expected a JSON array.");
|
|
56
|
+
}
|
|
57
|
+
if (!Array.isArray(parsed)) throw new Error("Expected a JSON array.");
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The tool surface accepts JSON-encoded arrays because MCP shapes are flat
|
|
62
|
+
// scalars. The service and store only ever see real arrays.
|
|
63
|
+
function coerceToolArgs(args) {
|
|
64
|
+
var out = Object.assign({}, args || {});
|
|
65
|
+
if (out.tags !== undefined) out.tags = parseJsonArray(out.tags);
|
|
66
|
+
if (out.links !== undefined) out.links = parseJsonArray(out.links);
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var HANDLED_TYPES = [
|
|
71
|
+
"project_logs_list",
|
|
72
|
+
"project_log_read",
|
|
73
|
+
"project_log_comment",
|
|
74
|
+
"project_log_delete",
|
|
75
|
+
// Retired human mutations. Still claimed so an older client gets an
|
|
76
|
+
// explicit refusal rather than silence.
|
|
77
|
+
"project_log_create",
|
|
78
|
+
"project_log_update",
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
var RETIRED_TYPES = ["project_log_create", "project_log_update"];
|
|
82
|
+
|
|
83
|
+
function attachProjectLogs(ctx) {
|
|
84
|
+
var service = ctx.service;
|
|
85
|
+
var sm = ctx.sm;
|
|
86
|
+
var slug = ctx.projectSlug;
|
|
87
|
+
var isMate = ctx.isMate === true;
|
|
88
|
+
var mateId = ctx.mateId || null;
|
|
89
|
+
var sendTo = ctx.sendTo;
|
|
90
|
+
var getClients = ctx.getClients || function () { return []; };
|
|
91
|
+
var getProjectOwnerId = ctx.getProjectOwnerId || function () { return null; };
|
|
92
|
+
var onFeedback = ctx.onFeedback || function () {};
|
|
93
|
+
|
|
94
|
+
// Every socket in this project's client set passed the project access check
|
|
95
|
+
// at WebSocket upgrade, so a broadcast here is already scoped to authorized
|
|
96
|
+
// human users of this project. Nothing is read from any payload. Pane
|
|
97
|
+
// connections are excluded, matching how sticky notes broadcast.
|
|
98
|
+
function broadcastUpdate(entry, op) {
|
|
99
|
+
if (isMate || !entry) return;
|
|
100
|
+
if (NOTIFYING_OPS.indexOf(op) === -1) return;
|
|
101
|
+
var notice = updateNotice(entry, op);
|
|
102
|
+
var clients = getClients();
|
|
103
|
+
for (var client of clients) {
|
|
104
|
+
if (!client || client.readyState !== 1 || client._clayPane) continue;
|
|
105
|
+
sendTo(client, notice);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function broadcastCommentReview(entry, request) {
|
|
110
|
+
if (isMate || !entry || !request || !request.commentId) return;
|
|
111
|
+
var notice = {
|
|
112
|
+
type: "project_log_comment_reviewed",
|
|
113
|
+
ref: entry.ref,
|
|
114
|
+
commentId: request.commentId,
|
|
115
|
+
action: request.action,
|
|
116
|
+
};
|
|
117
|
+
var clients = getClients();
|
|
118
|
+
for (var client of clients) {
|
|
119
|
+
if (!client || client.readyState !== 1 || client._clayPane) continue;
|
|
120
|
+
sendTo(client, notice);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Wrap a canonical write so a successful one notifies. A throw never reaches
|
|
125
|
+
// here, so a failed or no-op write is silent.
|
|
126
|
+
function notifying(op, run) {
|
|
127
|
+
return function (args) {
|
|
128
|
+
var result = run(args);
|
|
129
|
+
broadcastUpdate(result, op);
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- Bindings --------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
function sessionBinding(session) {
|
|
137
|
+
if (!service) return null;
|
|
138
|
+
if (isMate) {
|
|
139
|
+
if (!mateId) return null;
|
|
140
|
+
if (session && (!sm || !sm.sessions || sm.sessions.get(session.localId) !== session)) return null;
|
|
141
|
+
return service.bindMate({
|
|
142
|
+
projectSlug: slug,
|
|
143
|
+
projectOwnerId: getProjectOwnerId(),
|
|
144
|
+
isMate: true,
|
|
145
|
+
mateId: mateId,
|
|
146
|
+
session: session || null,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (!session) return null;
|
|
150
|
+
return service.bindProjectSession({ projectSlug: slug, session: session });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function userBinding(ws) {
|
|
154
|
+
if (!service || isMate) return null;
|
|
155
|
+
return service.bindUser({ projectSlug: slug, user: (ws && ws._clayUser) || null });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- WebSocket surface -----------------------------------------------
|
|
159
|
+
|
|
160
|
+
function fail(ws, requestId, message) {
|
|
161
|
+
sendTo(ws, { type: "project_logs_error", requestId: requestId || null, message: message });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Search already returns the ledger row shape, so a query needs no per-hit
|
|
165
|
+
// read and neither path dumps a record body into the list.
|
|
166
|
+
// Returns both the rows and this project's live category vocabulary, so the
|
|
167
|
+
// filter is populated solely from what this project actually uses.
|
|
168
|
+
function listEntries(bound, query, category, contextMode) {
|
|
169
|
+
var options = { kind: category, limit: LIST_LIMIT, contextMode: contextMode };
|
|
170
|
+
if (query) {
|
|
171
|
+
options.query = query;
|
|
172
|
+
var hits = bound.searchLogs(options);
|
|
173
|
+
return {
|
|
174
|
+
entries: hits.results,
|
|
175
|
+
categories: hits.categories || [],
|
|
176
|
+
contextMode: hits.contextMode,
|
|
177
|
+
currentChangeSetId: hits.currentChangeSetId,
|
|
178
|
+
isWorktree: hits.isWorktree,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
var listed = bound.listLogs(options);
|
|
182
|
+
return {
|
|
183
|
+
entries: listed.entries,
|
|
184
|
+
categories: listed.categories || [],
|
|
185
|
+
contextMode: listed.contextMode,
|
|
186
|
+
currentChangeSetId: listed.currentChangeSetId,
|
|
187
|
+
isWorktree: listed.isWorktree,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function handleLogsMessage(ws, msg) {
|
|
192
|
+
if (!msg || typeof msg.type !== "string") return false;
|
|
193
|
+
if (HANDLED_TYPES.indexOf(msg.type) === -1) return false;
|
|
194
|
+
|
|
195
|
+
var requestId = typeof msg.requestId === "string" ? msg.requestId : null;
|
|
196
|
+
if (isMate) {
|
|
197
|
+
fail(ws, requestId, "Project Logs are available in projects, not in Mate conversations.");
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Canonical authorship belongs to the project's agent sessions. A human
|
|
202
|
+
// asking to create or revise is refused before any binding is attempted,
|
|
203
|
+
// so no privilege level can reach the canonical writers.
|
|
204
|
+
if (RETIRED_TYPES.indexOf(msg.type) !== -1) {
|
|
205
|
+
fail(ws, requestId, "Project Logs are written by this project's agent sessions. Add a comment instead.");
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
var bound = userBinding(ws);
|
|
210
|
+
if (!bound) {
|
|
211
|
+
fail(ws, requestId, "Project Logs are not available for this project.");
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
if (msg.type === "project_logs_list") {
|
|
217
|
+
var query = typeof msg.query === "string" ? msg.query.trim() : "";
|
|
218
|
+
// A view preference, not an authorization input: the store validates it.
|
|
219
|
+
var category = typeof msg.category === "string" && msg.category ? msg.category : null;
|
|
220
|
+
var contextMode = typeof msg.contextMode === "string" ? msg.contextMode : null;
|
|
221
|
+
var listing = listEntries(bound, query, category, contextMode);
|
|
222
|
+
sendTo(ws, {
|
|
223
|
+
type: "project_logs_state",
|
|
224
|
+
requestId: requestId,
|
|
225
|
+
entries: listing.entries,
|
|
226
|
+
categories: listing.categories,
|
|
227
|
+
contextMode: listing.contextMode,
|
|
228
|
+
currentChangeSetId: listing.currentChangeSetId,
|
|
229
|
+
isWorktree: listing.isWorktree === true,
|
|
230
|
+
canDelete: bound.canDelete === true,
|
|
231
|
+
});
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (msg.type === "project_log_read") {
|
|
236
|
+
sendTo(ws, { type: "project_log_entry", requestId: requestId, entry: bound.readLog({ ref: msg.ref }), canDelete: bound.canDelete === true });
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (msg.type === "project_log_delete") {
|
|
241
|
+
var removed = bound.removeLog({ ref: msg.ref });
|
|
242
|
+
sendTo(ws, { type: "project_log_deleted", requestId: requestId, ref: removed.ref });
|
|
243
|
+
broadcastUpdate(removed, "delete");
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (msg.type === "project_log_comment") {
|
|
248
|
+
var commented = bound.commentLog({ ref: msg.ref, body: msg.body });
|
|
249
|
+
var reviewQueued = false;
|
|
250
|
+
try { reviewQueued = onFeedback(commented) === true; } catch (deliveryError) {
|
|
251
|
+
console.error("[project-logs] Failed to deliver comment:", deliveryError.message || deliveryError);
|
|
252
|
+
}
|
|
253
|
+
sendTo(ws, { type: "project_log_commented", requestId: requestId, entry: commented, reviewQueued: reviewQueued });
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
} catch (e) {
|
|
257
|
+
fail(ws, requestId, (e && e.message) || "Project Logs could not complete the request.");
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// --- MCP surface ------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
function toolHandlers(bound) {
|
|
267
|
+
if (!bound) return null;
|
|
268
|
+
return {
|
|
269
|
+
listLogs: function (args) { return bound.listLogs(args || {}); },
|
|
270
|
+
searchLogs: function (args) { return bound.searchLogs(args || {}); },
|
|
271
|
+
readLog: function (args) { return bound.readLog(args || {}); },
|
|
272
|
+
logHistory: function (args) { return bound.logHistory(args || {}); },
|
|
273
|
+
readLogRevision: function (args) { return bound.readLogRevision(args || {}); },
|
|
274
|
+
listLogFeedback: function (args) { return bound.listLogFeedback(args || {}); },
|
|
275
|
+
reviewLogComment: function (args) {
|
|
276
|
+
var request = coerceToolArgs(args);
|
|
277
|
+
var result = bound.reviewLogComment(request);
|
|
278
|
+
broadcastCommentReview(result, request);
|
|
279
|
+
// Only an incorporation is a canonical revision; clarify and decline
|
|
280
|
+
// resolve a comment and change nothing.
|
|
281
|
+
if (request && request.action === "incorporate") broadcastUpdate(result, "incorporate");
|
|
282
|
+
return result;
|
|
283
|
+
},
|
|
284
|
+
revertLog: notifying("revert", function (args) { return bound.revertLog(args || {}); }),
|
|
285
|
+
createLog: notifying("create", function (args) { return bound.createLog(coerceToolArgs(args)); }),
|
|
286
|
+
updateLog: notifying("update", function (args) { return bound.updateLog(coerceToolArgs(args)); }),
|
|
287
|
+
linkLog: notifying("link", function (args) { return bound.linkLog(coerceToolArgs(args)); }),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// A Mate project advertises tools only when the registry confirms
|
|
292
|
+
// authoritative builtin Clay, and then only the read-only cross-project set.
|
|
293
|
+
function includeGlobal() {
|
|
294
|
+
if (!isMate) return false;
|
|
295
|
+
var projectBound = sessionBinding(null);
|
|
296
|
+
return !!(projectBound && projectBound.isClay === true);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function getToolDefs(session) {
|
|
300
|
+
if (!service) return [];
|
|
301
|
+
if (isMate && !includeGlobal()) return [];
|
|
302
|
+
var bound = session ? sessionBinding(session) : null;
|
|
303
|
+
if (session && !bound) return [];
|
|
304
|
+
return logsMcp.getToolDefs(toolHandlers(bound), isMate);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function createMcpServer(adapter, session) {
|
|
308
|
+
if (!service) return null;
|
|
309
|
+
if (isMate && !includeGlobal()) return null;
|
|
310
|
+
if (!isMate && session && !sessionBinding(session)) {
|
|
311
|
+
// A session that cannot bind still gets the fail-closed descriptor
|
|
312
|
+
// rather than a silently missing server.
|
|
313
|
+
return logsMcp.createMcpServer(adapter, null, false);
|
|
314
|
+
}
|
|
315
|
+
var bound = session ? sessionBinding(session) : null;
|
|
316
|
+
return logsMcp.createMcpServer(adapter, toolHandlers(bound), isMate);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function getDynamicToolDefs(session) {
|
|
320
|
+
return getToolDefs(session).map(function (tool) {
|
|
321
|
+
tool.permissionName = "mcp__clay-logs__" + tool.name;
|
|
322
|
+
return tool;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function getBridgeTools(session, normalizeSchema) {
|
|
327
|
+
if (!session) return [];
|
|
328
|
+
var defs = getToolDefs(session);
|
|
329
|
+
var tools = [];
|
|
330
|
+
for (var i = 0; i < defs.length; i++) {
|
|
331
|
+
tools.push({
|
|
332
|
+
server: "clay-logs",
|
|
333
|
+
name: defs[i].name,
|
|
334
|
+
description: defs[i].description || defs[i].name,
|
|
335
|
+
inputSchema: normalizeSchema(defs[i].inputSchema),
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return tools;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function callBridgeTool(session, toolName, args) {
|
|
342
|
+
if (!session) return Promise.reject(new Error("Project Logs require a valid session."));
|
|
343
|
+
var defs = getToolDefs(session);
|
|
344
|
+
for (var i = 0; i < defs.length; i++) {
|
|
345
|
+
if (defs[i].name === toolName) return Promise.resolve(defs[i].handler(args || {}));
|
|
346
|
+
}
|
|
347
|
+
return Promise.reject(new Error("Project Logs tool not found: " + toolName));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// A count only. The comment bodies stay in the tool, so a pending queue is
|
|
351
|
+
// discoverable on the next turn without dumping the discussion into context
|
|
352
|
+
// and without starting a turn of its own.
|
|
353
|
+
function pendingFeedbackSignal(session) {
|
|
354
|
+
if (!session) return "";
|
|
355
|
+
var bound = sessionBinding(session);
|
|
356
|
+
if (!bound || typeof bound.listLogFeedback !== "function") return "";
|
|
357
|
+
var total = 0;
|
|
358
|
+
try {
|
|
359
|
+
total = bound.listLogFeedback({ limit: 1 }).total || 0;
|
|
360
|
+
} catch (e) {
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
if (!total) return "";
|
|
364
|
+
return "\n" + total + (total === 1 ? " comment awaits" : " comments await") +
|
|
365
|
+
" your review. Call list_log_feedback to see them.";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function getSystemPrompt(session) {
|
|
369
|
+
if (!service) return "";
|
|
370
|
+
if (isMate) {
|
|
371
|
+
if (!includeGlobal() || (session && !sessionBinding(session))) return "";
|
|
372
|
+
return SYSTEM_PROMPT_LABEL + "\n" + logsMcp.CLAY_READ_CONTRACT;
|
|
373
|
+
}
|
|
374
|
+
if (session && !sessionBinding(session)) return "";
|
|
375
|
+
return SYSTEM_PROMPT_LABEL + "\n" + logsMcp.LOGS_CONTRACT +
|
|
376
|
+
"\n" + logsMcp.LEARNING_CONTRACT +
|
|
377
|
+
"\n" + logsMcp.ATTENTION_CONTRACT +
|
|
378
|
+
"\n" + logsMcp.REVIEW_CONTRACT + pendingFeedbackSignal(session);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
handleLogsMessage: handleLogsMessage,
|
|
383
|
+
createMcpServer: createMcpServer,
|
|
384
|
+
getToolDefs: getToolDefs,
|
|
385
|
+
getDynamicToolDefs: getDynamicToolDefs,
|
|
386
|
+
getBridgeTools: getBridgeTools,
|
|
387
|
+
callBridgeTool: callBridgeTool,
|
|
388
|
+
getSystemPrompt: getSystemPrompt,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
module.exports = {
|
|
393
|
+
SYSTEM_PROMPT_LABEL: SYSTEM_PROMPT_LABEL,
|
|
394
|
+
NOTIFYING_OPS: NOTIFYING_OPS,
|
|
395
|
+
updateNotice: updateNotice,
|
|
396
|
+
HANDLED_TYPES: HANDLED_TYPES,
|
|
397
|
+
RETIRED_TYPES: RETIRED_TYPES,
|
|
398
|
+
attachProjectLogs: attachProjectLogs,
|
|
399
|
+
coerceToolArgs: coerceToolArgs,
|
|
400
|
+
};
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
var fs = require("fs");
|
|
2
2
|
var path = require("path");
|
|
3
3
|
var crypto = require("crypto");
|
|
4
|
+
var mateSync = require("./mate-knowledge-sync");
|
|
5
|
+
|
|
6
|
+
// Mirror a Mate-owned journal into the Clay-wide Knowledge backend after the
|
|
7
|
+
// legacy append has already succeeded. Reconciling the whole journal keeps the
|
|
8
|
+
// new scope current whether the write added, rewrote, or removed lines.
|
|
9
|
+
function mirrorMateJournal(mateDir, fileName, job) {
|
|
10
|
+
mateSync.syncQuietly({
|
|
11
|
+
mateDir: mateDir,
|
|
12
|
+
fileName: fileName,
|
|
13
|
+
actor: { type: "agent", vendor: (job && job.vendor) || null, sessionKey: (job && job.sessionKey) || null },
|
|
14
|
+
});
|
|
15
|
+
}
|
|
4
16
|
|
|
5
17
|
function attachMateInteraction(ctx) {
|
|
6
18
|
var cwd = ctx.cwd;
|
|
@@ -278,6 +290,7 @@ function attachMateInteraction(ctx) {
|
|
|
278
290
|
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
279
291
|
var digestFile = path.join(knowledgeDir, "session-digests.jsonl");
|
|
280
292
|
fs.appendFileSync(digestFile, JSON.stringify(digestObj) + "\n");
|
|
293
|
+
mirrorMateJournal(mateDir, "session-digests.jsonl", job);
|
|
281
294
|
} catch (e) {
|
|
282
295
|
console.error("[digest-worker] Write failed for " + job.mateId + ":", e.message);
|
|
283
296
|
}
|
|
@@ -302,6 +315,7 @@ function attachMateInteraction(ctx) {
|
|
|
302
315
|
}));
|
|
303
316
|
}
|
|
304
317
|
fs.appendFileSync(obsFile, obsLines.join("\n") + "\n");
|
|
318
|
+
mirrorMateJournal(mateDir, "user-observations.jsonl", job);
|
|
305
319
|
} catch (e) {
|
|
306
320
|
console.error("[digest-worker] Observations write failed for " + job.mateId + ":", e.message);
|
|
307
321
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Session-bound `clay-knowledge` wiring for Mate projects.
|
|
2
|
+
//
|
|
3
|
+
// Mate Knowledge is available only inside a Mate project. A non-Mate Project
|
|
4
|
+
// Driver session gets nothing here and continues to use `clay-logs`. Which
|
|
5
|
+
// tool set a Mate receives is decided by the server Mate registry through the
|
|
6
|
+
// service binder, never by a project flag or a tool argument.
|
|
7
|
+
|
|
8
|
+
var knowledgeMcp = require("./mate-knowledge-mcp-server");
|
|
9
|
+
|
|
10
|
+
var SYSTEM_PROMPT_LABEL = "--- Your Knowledge (durable personal and expertise context; manage via clay-knowledge tools) ---";
|
|
11
|
+
var CLAY_PROMPT_LABEL = "--- Mate Knowledge coordination (via clay-knowledge tools) ---";
|
|
12
|
+
|
|
13
|
+
function attachProjectMateKnowledge(ctx) {
|
|
14
|
+
var service = ctx.service;
|
|
15
|
+
var sm = ctx.sm;
|
|
16
|
+
var slug = ctx.projectSlug;
|
|
17
|
+
var isMate = ctx.isMate === true;
|
|
18
|
+
var mateId = ctx.mateId || null;
|
|
19
|
+
var getProjectOwnerId = ctx.getProjectOwnerId || function () { return null; };
|
|
20
|
+
|
|
21
|
+
function binding(session) {
|
|
22
|
+
if (!service || !isMate || !mateId) return null;
|
|
23
|
+
if (session && (!sm || !sm.sessions || sm.sessions.get(session.localId) !== session)) return null;
|
|
24
|
+
return service.bind({
|
|
25
|
+
projectSlug: slug,
|
|
26
|
+
projectOwnerId: getProjectOwnerId(),
|
|
27
|
+
isMate: true,
|
|
28
|
+
mateId: mateId,
|
|
29
|
+
session: session || null,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Decided once from the project-level binding so a static descriptor and a
|
|
34
|
+
// session-bound server always advertise the same tool set.
|
|
35
|
+
function includeClay() {
|
|
36
|
+
var projectBound = binding(null);
|
|
37
|
+
return !!(projectBound && projectBound.isClay === true);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getToolDefs(session) {
|
|
41
|
+
if (!service || !isMate) return [];
|
|
42
|
+
var bound = session ? binding(session) : null;
|
|
43
|
+
if (session && !bound) return [];
|
|
44
|
+
return knowledgeMcp.getToolDefs(bound, includeClay());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createMcpServer(adapter, session) {
|
|
48
|
+
if (!service || !isMate) return null;
|
|
49
|
+
if (!binding(null)) return null;
|
|
50
|
+
var bound = session ? binding(session) : null;
|
|
51
|
+
// A session that cannot bind still gets the fail-closed descriptor rather
|
|
52
|
+
// than a silently missing server.
|
|
53
|
+
return knowledgeMcp.createMcpServer(adapter, bound, includeClay());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getDynamicToolDefs(session) {
|
|
57
|
+
return getToolDefs(session).map(function (tool) {
|
|
58
|
+
tool.permissionName = "mcp__clay-knowledge__" + tool.name;
|
|
59
|
+
return tool;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getBridgeTools(session, normalizeSchema) {
|
|
64
|
+
if (!session) return [];
|
|
65
|
+
var defs = getToolDefs(session);
|
|
66
|
+
var tools = [];
|
|
67
|
+
for (var i = 0; i < defs.length; i++) {
|
|
68
|
+
tools.push({
|
|
69
|
+
server: "clay-knowledge",
|
|
70
|
+
name: defs[i].name,
|
|
71
|
+
description: defs[i].description || defs[i].name,
|
|
72
|
+
inputSchema: normalizeSchema(defs[i].inputSchema),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return tools;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function callBridgeTool(session, toolName, args) {
|
|
79
|
+
if (!session) return Promise.reject(new Error("Knowledge tools require a valid Mate session."));
|
|
80
|
+
var defs = getToolDefs(session);
|
|
81
|
+
for (var i = 0; i < defs.length; i++) {
|
|
82
|
+
if (defs[i].name === toolName) return Promise.resolve(defs[i].handler(args || {}));
|
|
83
|
+
}
|
|
84
|
+
return Promise.reject(new Error("Knowledge tool not found: " + toolName));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getSystemPrompt(session) {
|
|
88
|
+
if (!service || !isMate) return "";
|
|
89
|
+
var bound = session ? binding(session) : binding(null);
|
|
90
|
+
if (!bound) return "";
|
|
91
|
+
return bound.isClay
|
|
92
|
+
? CLAY_PROMPT_LABEL + "\n" + knowledgeMcp.CLAY_CONTRACT
|
|
93
|
+
: SYSTEM_PROMPT_LABEL + "\n" + knowledgeMcp.MATE_CONTRACT;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
createMcpServer: createMcpServer,
|
|
98
|
+
getToolDefs: getToolDefs,
|
|
99
|
+
getDynamicToolDefs: getDynamicToolDefs,
|
|
100
|
+
getBridgeTools: getBridgeTools,
|
|
101
|
+
callBridgeTool: callBridgeTool,
|
|
102
|
+
getSystemPrompt: getSystemPrompt,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
SYSTEM_PROMPT_LABEL: SYSTEM_PROMPT_LABEL,
|
|
108
|
+
CLAY_PROMPT_LABEL: CLAY_PROMPT_LABEL,
|
|
109
|
+
attachProjectMateKnowledge: attachProjectMateKnowledge,
|
|
110
|
+
};
|
package/lib/project-memory.js
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
var fs = require("fs");
|
|
2
2
|
var path = require("path");
|
|
3
|
+
var mateSync = require("./mate-knowledge-sync");
|
|
4
|
+
|
|
5
|
+
// Mirror a Mate-owned durable memory source into the Clay-wide Knowledge
|
|
6
|
+
// backend after the legacy write has already succeeded. Reconciling the source
|
|
7
|
+
// covers writes, rewrites, and removals with one call.
|
|
8
|
+
function mirrorMateSource(mateDir, fileName, actor) {
|
|
9
|
+
mateSync.syncQuietly({
|
|
10
|
+
mateDir: mateDir,
|
|
11
|
+
fileName: fileName,
|
|
12
|
+
actor: actor || { type: "agent" },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
3
15
|
|
|
4
16
|
function attachMemory(ctx) {
|
|
5
17
|
var cwd = ctx.cwd;
|
|
@@ -467,6 +479,7 @@ function attachMemory(ctx) {
|
|
|
467
479
|
}
|
|
468
480
|
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
469
481
|
fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
|
|
482
|
+
mirrorMateSource(path.dirname(knowledgeDir), "memory-summary.md");
|
|
470
483
|
console.log("[memory-summary] Updated memory-summary.md for mate " + mateId);
|
|
471
484
|
} catch (e) {
|
|
472
485
|
console.error("[memory-summary] Failed to write memory-summary.md for mate " + mateId + ":", e.message);
|
|
@@ -573,6 +586,7 @@ function attachMemory(ctx) {
|
|
|
573
586
|
}
|
|
574
587
|
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
575
588
|
fs.writeFileSync(summaryFile, cleaned + "\n", "utf8");
|
|
589
|
+
mirrorMateSource(path.dirname(knowledgeDir), "memory-summary.md");
|
|
576
590
|
console.log("[memory-summary] Generated initial memory-summary.md for mate " + mateId + " from " + digestsText.length + " digests");
|
|
577
591
|
} catch (e) {
|
|
578
592
|
console.error("[memory-summary] Failed to write initial memory-summary.md for mate " + mateId + ":", e.message);
|
|
@@ -666,6 +680,14 @@ function attachMemory(ctx) {
|
|
|
666
680
|
} else {
|
|
667
681
|
fs.writeFileSync(digestFile, lines.join("\n") + "\n");
|
|
668
682
|
}
|
|
683
|
+
// Reconciling the journal after the rewrite tombstones the removed
|
|
684
|
+
// line, so a deleted memory does not stay active in the new backend.
|
|
685
|
+
var deleteUser = (ws && ws._clayUser) || null;
|
|
686
|
+
mirrorMateSource(cwd, "session-digests.jsonl", {
|
|
687
|
+
type: deleteUser ? "user" : "system",
|
|
688
|
+
userId: deleteUser ? deleteUser.id : null,
|
|
689
|
+
displayName: deleteUser ? (deleteUser.displayName || deleteUser.username) : null,
|
|
690
|
+
});
|
|
669
691
|
}
|
|
670
692
|
} catch (e) {}
|
|
671
693
|
sendTo(ws, { type: "memory_deleted", index: msg.index });
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Idempotent acknowledgement support for ordinary project chat messages.
|
|
2
|
+
|
|
3
|
+
var CLIENT_MESSAGE_ID_RE = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
4
|
+
|
|
5
|
+
function normalizeClientMessageId(value) {
|
|
6
|
+
if (typeof value !== "string" || !CLIENT_MESSAGE_ID_RE.test(value)) return null;
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function ownerKey(ws) {
|
|
11
|
+
return ws && ws._clayUser && ws._clayUser.id ? String(ws._clayUser.id) : "_default";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function deliveryKey(ownerId, clientMessageId) {
|
|
15
|
+
return ownerId + ":" + clientMessageId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureRecordedMessages(session) {
|
|
19
|
+
if (session._recordedClientMessages) return session._recordedClientMessages;
|
|
20
|
+
var recorded = new Map();
|
|
21
|
+
var history = Array.isArray(session.history) ? session.history : [];
|
|
22
|
+
for (var i = 0; i < history.length; i++) {
|
|
23
|
+
var item = history[i];
|
|
24
|
+
var clientMessageId = normalizeClientMessageId(item && item.clientMessageId);
|
|
25
|
+
if (!clientMessageId) continue;
|
|
26
|
+
recorded.set(deliveryKey(item.from ? String(item.from) : "_default", clientMessageId), item);
|
|
27
|
+
}
|
|
28
|
+
session._recordedClientMessages = recorded;
|
|
29
|
+
return recorded;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createProjectMessageDelivery(sendTo) {
|
|
33
|
+
function inspect(ws, session, msg) {
|
|
34
|
+
var clientMessageId = normalizeClientMessageId(msg && msg.clientMessageId);
|
|
35
|
+
if (!clientMessageId) return { clientMessageId: null, ownerId: ownerKey(ws), duplicate: false };
|
|
36
|
+
var ownerId = ownerKey(ws);
|
|
37
|
+
var recordedMessage = ensureRecordedMessages(session).get(deliveryKey(ownerId, clientMessageId)) || null;
|
|
38
|
+
return {
|
|
39
|
+
clientMessageId: clientMessageId,
|
|
40
|
+
ownerId: ownerId,
|
|
41
|
+
duplicate: !!recordedMessage,
|
|
42
|
+
recordedMessage: recordedMessage,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function markRecorded(session, receipt, message) {
|
|
47
|
+
if (!receipt || !receipt.clientMessageId) return;
|
|
48
|
+
ensureRecordedMessages(session).set(deliveryKey(receipt.ownerId, receipt.clientMessageId), message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function acknowledge(ws, receipt, message) {
|
|
52
|
+
if (!receipt || !receipt.clientMessageId) return;
|
|
53
|
+
sendTo(ws, {
|
|
54
|
+
type: "message_ack",
|
|
55
|
+
clientMessageId: receipt.clientMessageId,
|
|
56
|
+
message: message || receipt.recordedMessage || null,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
inspect: inspect,
|
|
62
|
+
markRecorded: markRecorded,
|
|
63
|
+
acknowledge: acknowledge,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
createProjectMessageDelivery: createProjectMessageDelivery,
|
|
69
|
+
normalizeClientMessageId: normalizeClientMessageId,
|
|
70
|
+
};
|