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
package/lib/notes.js
CHANGED
|
@@ -3,6 +3,7 @@ var path = require("path");
|
|
|
3
3
|
var crypto = require("crypto");
|
|
4
4
|
var config = require("./config");
|
|
5
5
|
var utils = require("./utils");
|
|
6
|
+
var lifecycle = require("./notes-lifecycle");
|
|
6
7
|
|
|
7
8
|
function createNotesManager(opts) {
|
|
8
9
|
var cwd = opts.cwd;
|
|
@@ -19,11 +20,14 @@ function createNotesManager(opts) {
|
|
|
19
20
|
return "n_" + Date.now() + "_" + crypto.randomBytes(3).toString("hex");
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
// Legacy files are normalized on load, never on read. Nothing is rewritten
|
|
24
|
+
// just because a note was looked at; the projected fields reach disk the next
|
|
25
|
+
// time some other write happens.
|
|
22
26
|
function loadFromDisk() {
|
|
23
27
|
try {
|
|
24
28
|
var data = fs.readFileSync(notesFile, "utf8");
|
|
25
29
|
var parsed = JSON.parse(data);
|
|
26
|
-
return parsed.notes || [];
|
|
30
|
+
return lifecycle.normalizeAll(parsed.notes || []).notes;
|
|
27
31
|
} catch (e) {
|
|
28
32
|
return [];
|
|
29
33
|
}
|
|
@@ -56,6 +60,10 @@ function createNotesManager(opts) {
|
|
|
56
60
|
color: data.color || "purple",
|
|
57
61
|
opacity: typeof data.opacity === "number" ? data.opacity : 0.64,
|
|
58
62
|
minimized: false,
|
|
63
|
+
// A new note is always an open attention item.
|
|
64
|
+
state: lifecycle.STATE_OPEN,
|
|
65
|
+
closedAt: null,
|
|
66
|
+
closedBy: null,
|
|
59
67
|
zIndex: notes.length + 1,
|
|
60
68
|
createdAt: now,
|
|
61
69
|
updatedAt: now,
|
|
@@ -74,13 +82,20 @@ function createNotesManager(opts) {
|
|
|
74
82
|
function update(id, changes) {
|
|
75
83
|
for (var i = 0; i < notes.length; i++) {
|
|
76
84
|
if (notes[i].id === id) {
|
|
77
|
-
var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "
|
|
85
|
+
var allowed = ["text", "x", "y", "w", "h", "color", "minimized", "zIndex", "opacity"];
|
|
78
86
|
for (var j = 0; j < allowed.length; j++) {
|
|
79
87
|
var key = allowed[j];
|
|
80
88
|
if (changes[key] !== undefined) {
|
|
81
89
|
notes[i][key] = changes[key];
|
|
82
90
|
}
|
|
83
91
|
}
|
|
92
|
+
// `hidden` is the legacy spelling of the lifecycle. Route it through the
|
|
93
|
+
// transitions so state, hidden, and the close provenance can never
|
|
94
|
+
// disagree, and so an old client cannot half-apply a close.
|
|
95
|
+
if (changes.hidden !== undefined) {
|
|
96
|
+
if (changes.hidden === true) lifecycle.applyClose(notes[i], changes.actor || null, Date.now());
|
|
97
|
+
else lifecycle.applyReopen(notes[i]);
|
|
98
|
+
}
|
|
84
99
|
notes[i].updatedAt = Date.now();
|
|
85
100
|
saveToDisk();
|
|
86
101
|
return notes[i];
|
|
@@ -89,6 +104,44 @@ function createNotesManager(opts) {
|
|
|
89
104
|
return null;
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
// Close a note. This is what completion means: the record stays and only its
|
|
108
|
+
// place on the active canvas changes. Idempotent.
|
|
109
|
+
function close(id, actor) {
|
|
110
|
+
for (var i = 0; i < notes.length; i++) {
|
|
111
|
+
if (notes[i].id !== id) continue;
|
|
112
|
+
if (lifecycle.applyClose(notes[i], actor || null, Date.now())) {
|
|
113
|
+
notes[i].updatedAt = Date.now();
|
|
114
|
+
saveToDisk();
|
|
115
|
+
}
|
|
116
|
+
return notes[i];
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Reopen a closed note. Idempotent.
|
|
122
|
+
function reopen(id) {
|
|
123
|
+
for (var i = 0; i < notes.length; i++) {
|
|
124
|
+
if (notes[i].id !== id) continue;
|
|
125
|
+
if (lifecycle.applyReopen(notes[i])) {
|
|
126
|
+
notes[i].updatedAt = Date.now();
|
|
127
|
+
saveToDisk();
|
|
128
|
+
}
|
|
129
|
+
return notes[i];
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function openList() {
|
|
135
|
+
return lifecycle.openNotes(notes);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function closedList() {
|
|
139
|
+
return lifecycle.closedNotes(notes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Permanent deletion. Deliberately not reachable from any WebSocket message,
|
|
143
|
+
// MCP tool, or UI control: completion closes, it never erases. Retained only
|
|
144
|
+
// for maintenance callers that must truly discard a record.
|
|
92
145
|
function remove(id) {
|
|
93
146
|
for (var i = 0; i < notes.length; i++) {
|
|
94
147
|
if (notes[i].id === id) {
|
|
@@ -123,7 +176,7 @@ function createNotesManager(opts) {
|
|
|
123
176
|
function getActiveNotesText() {
|
|
124
177
|
var active = [];
|
|
125
178
|
for (var i = 0; i < notes.length; i++) {
|
|
126
|
-
if (
|
|
179
|
+
if (lifecycle.isOpen(notes[i]) && notes[i].text) active.push(notes[i]);
|
|
127
180
|
}
|
|
128
181
|
if (active.length === 0) return "";
|
|
129
182
|
var lines = [];
|
|
@@ -137,8 +190,12 @@ function createNotesManager(opts) {
|
|
|
137
190
|
|
|
138
191
|
return {
|
|
139
192
|
list: list,
|
|
193
|
+
openList: openList,
|
|
194
|
+
closedList: closedList,
|
|
140
195
|
create: create,
|
|
141
196
|
update: update,
|
|
197
|
+
close: close,
|
|
198
|
+
reopen: reopen,
|
|
142
199
|
remove: remove,
|
|
143
200
|
bringToFront: bringToFront,
|
|
144
201
|
getActiveNotesText: getActiveNotesText,
|
package/lib/os-users.js
CHANGED
|
@@ -161,7 +161,9 @@ function fsAsUser(op, args, osUserInfo) {
|
|
|
161
161
|
"var fs = require('fs');",
|
|
162
162
|
"var f = " + JSON.stringify(args.file) + ";",
|
|
163
163
|
"var content = " + JSON.stringify(args.content || "") + ";",
|
|
164
|
-
"
|
|
164
|
+
"var flag = " + JSON.stringify(args.flag === "wx" ? "wx" : "w") + ";",
|
|
165
|
+
"var mode = " + JSON.stringify(typeof args.mode === "number" ? args.mode : 0o666) + ";",
|
|
166
|
+
"fs.writeFileSync(f, content, { encoding: 'utf8', flag: flag, mode: mode });",
|
|
165
167
|
"process.stdout.write(JSON.stringify({ ok: true }));",
|
|
166
168
|
].join(" ");
|
|
167
169
|
} else if (op === "mkdir") {
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
// Bounded progressive-disclosure catalog for Capsules visible to a Mate.
|
|
2
|
+
//
|
|
3
|
+
// Skills are gated by Display: a Capsule the human cannot operate by hand is a
|
|
4
|
+
// Capsule no Mate may reach either, so an entry appears here only while its
|
|
5
|
+
// declarative Display floor validates. The gate reads the Capsule's own tree,
|
|
6
|
+
// carried alongside each manifest, and fails closed on a scan error, a missing
|
|
7
|
+
// ID, or a missing or invalid Display.
|
|
8
|
+
|
|
9
|
+
var capsuleFloor = require("./capsule-display-floor");
|
|
2
10
|
|
|
3
11
|
var MAX_NAME_LENGTH = 80;
|
|
4
12
|
var MAX_DESCRIPTION_LENGTH = 240;
|
|
@@ -35,7 +43,7 @@ function discoveryEntry(manifest) {
|
|
|
35
43
|
function buildCapsuleCatalogPrompt(manifests) {
|
|
36
44
|
if (!Array.isArray(manifests) || manifests.length === 0) return "";
|
|
37
45
|
var sorted = manifests.filter(function (manifest) {
|
|
38
|
-
return
|
|
46
|
+
return capsuleFloor.hasUsableFloor(manifest);
|
|
39
47
|
}).slice().sort(function (a, b) {
|
|
40
48
|
var first = String(a.id);
|
|
41
49
|
var second = String(b.id);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Delivers a Capsule turn prompt into this Mate project.
|
|
2
|
+
//
|
|
3
|
+
// A Capsule game against a Mate needs the Mate to actually take its turns:
|
|
4
|
+
// when Logic passes the turn to the mate seat, this module wakes the Mate
|
|
5
|
+
// with a prompt telling it to snapshot the game and act. One session hosts
|
|
6
|
+
// the whole game (found again by session.capsuleGame.toolId), so the user
|
|
7
|
+
// watches a single running conversation of the Mate playing rather than a
|
|
8
|
+
// new session per turn. The prompt is recorded as an internal user message,
|
|
9
|
+
// the same shape the debate engine uses, so the transcript shows the nudge
|
|
10
|
+
// without pretending the human typed it.
|
|
11
|
+
//
|
|
12
|
+
// This module only carries words to the Mate. Every actual game mutation the
|
|
13
|
+
// Mate then makes goes through the same clay_tool_act pipeline as always.
|
|
14
|
+
|
|
15
|
+
function attachCapsuleTurn(ctx) {
|
|
16
|
+
var sm = ctx.sm;
|
|
17
|
+
|
|
18
|
+
function ownsSession(session, principal) {
|
|
19
|
+
if (ctx.isMultiUser()) return !!principal.userId && session.ownerId === principal.userId;
|
|
20
|
+
return !session.ownerId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function findGameSession(principal, toolId) {
|
|
24
|
+
var found = null;
|
|
25
|
+
sm.sessions.forEach(function (session) {
|
|
26
|
+
if (found || !session || session.hidden || session.destroying) return;
|
|
27
|
+
if (!session.capsuleGame || session.capsuleGame.toolId !== toolId) return;
|
|
28
|
+
if (!ownsSession(session, principal)) return;
|
|
29
|
+
found = session;
|
|
30
|
+
});
|
|
31
|
+
return found;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function createGameSession(principal, options) {
|
|
35
|
+
var selection = await ctx.resolveModel(principal);
|
|
36
|
+
if (!selection || selection.status !== "ready" || !selection.vendor || !selection.model) {
|
|
37
|
+
throw new Error(selection && selection.error ? selection.error : "No configured model is available for the Mate.");
|
|
38
|
+
}
|
|
39
|
+
var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
|
|
40
|
+
var session = create.call(sm, {
|
|
41
|
+
ownerId: principal.userId || null,
|
|
42
|
+
vendor: selection.vendor,
|
|
43
|
+
model: selection.model,
|
|
44
|
+
});
|
|
45
|
+
session.title = (options.toolName || options.toolId) + " game";
|
|
46
|
+
session.capsuleGame = { toolId: options.toolId };
|
|
47
|
+
return session;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function deliverCapsuleTurn(principal, options) {
|
|
51
|
+
if (!principal || !options || !options.toolId || !options.text) {
|
|
52
|
+
throw new Error("A bound owner, toolId, and turn text are required.");
|
|
53
|
+
}
|
|
54
|
+
var sdk = ctx.getSdk();
|
|
55
|
+
if (!sdk || typeof sdk.startQuery !== "function") throw new Error("Mate project runtime is unavailable.");
|
|
56
|
+
var session = findGameSession(principal, options.toolId);
|
|
57
|
+
var created = false;
|
|
58
|
+
if (!session) {
|
|
59
|
+
session = await createGameSession(principal, options);
|
|
60
|
+
created = true;
|
|
61
|
+
}
|
|
62
|
+
// Recorded as a dedicated event, not a user message: the transcript shows
|
|
63
|
+
// a short system note (see historyToHomeChat) instead of pretending the
|
|
64
|
+
// human typed the delivery prompt. The full prompt still reaches the
|
|
65
|
+
// model through the query below.
|
|
66
|
+
sm.sendAndRecord(session, {
|
|
67
|
+
type: "capsule_turn",
|
|
68
|
+
text: options.text,
|
|
69
|
+
toolId: options.toolId,
|
|
70
|
+
toolName: options.toolName || options.toolId,
|
|
71
|
+
kind: options.kind || "turn",
|
|
72
|
+
});
|
|
73
|
+
session.isProcessing = true;
|
|
74
|
+
session.lastActivity = Date.now();
|
|
75
|
+
sm.broadcastSessionList();
|
|
76
|
+
try {
|
|
77
|
+
var delivered = typeof sdk.pushMessage === "function" ? sdk.pushMessage(session, options.text) : false;
|
|
78
|
+
if (!delivered) await Promise.resolve(sdk.startQuery(session, options.text, undefined, ctx.getLinuxUserForSession(session)));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
session.isProcessing = false;
|
|
81
|
+
sm.sendAndRecord(session, { type: "error", text: "Capsule turn could not start: " + (error && error.message ? error.message : String(error)) });
|
|
82
|
+
sm.broadcastSessionList();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
// The reference shape the home chat surface opens sessions by.
|
|
86
|
+
return {
|
|
87
|
+
session: session,
|
|
88
|
+
created: created,
|
|
89
|
+
reference: (typeof session.cliSessionId === "string" && session.cliSessionId) || "local:" + session.localId,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { deliverCapsuleTurn: deliverCapsuleTurn };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { attachCapsuleTurn: attachCapsuleTurn };
|
|
@@ -233,7 +233,7 @@ function attachConnection(ctx) {
|
|
|
233
233
|
// so vendor badges showed Claude until the next broadcast).
|
|
234
234
|
sendTo(ws, {
|
|
235
235
|
type: "session_list",
|
|
236
|
-
sessions:
|
|
236
|
+
sessions: sm.mapSessionsForClient(allSessions),
|
|
237
237
|
});
|
|
238
238
|
if (_splitGroups) _splitGroups.sendConnectionState(ws);
|
|
239
239
|
|
|
@@ -350,9 +350,22 @@ function attachConnection(ctx) {
|
|
|
350
350
|
broadcastPresence();
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
// Keep-alive for the project socket, mirroring the `/ws` global handler.
|
|
354
|
+
//
|
|
355
|
+
// A reverse proxy resets its upstream read timeout on data flowing back from
|
|
356
|
+
// the server, so a client ping is only useful if the server answers it. This
|
|
357
|
+
// reads nothing, mutates nothing, and reveals nothing about the connection:
|
|
358
|
+
// it is a fixed reply on a fixed message type.
|
|
359
|
+
function handleKeepaliveMessage(ws, msg) {
|
|
360
|
+
if (!msg || typeof msg !== "object" || msg.type !== "ping") return false;
|
|
361
|
+
sendTo(ws, { type: "pong" });
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
353
365
|
return {
|
|
354
366
|
handleConnection: handleConnection,
|
|
355
367
|
handleDisconnection: handleDisconnection,
|
|
368
|
+
handleKeepaliveMessage: handleKeepaliveMessage,
|
|
356
369
|
};
|
|
357
370
|
}
|
|
358
371
|
|
package/lib/project-http.js
CHANGED
|
@@ -730,7 +730,7 @@ function attachHTTP(ctx) {
|
|
|
730
730
|
}
|
|
731
731
|
|
|
732
732
|
// Git panel status and actions
|
|
733
|
-
var isGitPanelRequest = urlPath === "/api/git/status" ||
|
|
733
|
+
var isGitPanelRequest = urlPath === "/api/git/status" || urlPath === "/api/git/summary" ||
|
|
734
734
|
urlPath === "/api/git/action" || urlPath.indexOf("/api/git/file-diff?") === 0 ||
|
|
735
735
|
urlPath.indexOf("/api/git/session-diff?") === 0;
|
|
736
736
|
if (isGitPanelRequest && usersModule.isMultiUser()) {
|
|
@@ -768,6 +768,20 @@ function attachHTTP(ctx) {
|
|
|
768
768
|
return true;
|
|
769
769
|
}
|
|
770
770
|
|
|
771
|
+
// Compact repository placard data for the always-visible sidebar block.
|
|
772
|
+
// Same permission gate as the panel, but no file payload and no path.
|
|
773
|
+
if (req.method === "GET" && urlPath === "/api/git/summary") {
|
|
774
|
+
try {
|
|
775
|
+
var gitSummary = gitCli.getSummary(cwd, getOsUserInfoForReq(req));
|
|
776
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
777
|
+
res.end(JSON.stringify(gitSummary));
|
|
778
|
+
} catch (e) {
|
|
779
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
780
|
+
res.end(JSON.stringify({ error: e.message || "Unable to read Git summary" }));
|
|
781
|
+
}
|
|
782
|
+
return true;
|
|
783
|
+
}
|
|
784
|
+
|
|
771
785
|
if (req.method === "GET" && urlPath.indexOf("/api/git/file-diff?") === 0) {
|
|
772
786
|
var gitDiffParams = new URLSearchParams(urlPath.slice(urlPath.indexOf("?")));
|
|
773
787
|
var gitDiffPath = gitDiffParams.get("path");
|
package/lib/project-knowledge.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
var fs = require("fs");
|
|
2
2
|
var path = require("path");
|
|
3
|
+
var mateSync = require("./mate-knowledge-sync");
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Attach knowledge file management to a project context.
|
|
@@ -75,6 +76,23 @@ function attachKnowledge(ctx) {
|
|
|
75
76
|
return files;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
// Mirror a Mate-owned knowledge file into the Clay-wide Knowledge backend
|
|
80
|
+
// after the legacy write has already succeeded. The legacy file stays
|
|
81
|
+
// authoritative; this only keeps the new scope from going stale.
|
|
82
|
+
function mirrorMateSource(ws, fileName) {
|
|
83
|
+
if (!isMate) return;
|
|
84
|
+
var user = (ws && ws._clayUser) || null;
|
|
85
|
+
mateSync.syncQuietly({
|
|
86
|
+
mateDir: cwd,
|
|
87
|
+
fileName: fileName,
|
|
88
|
+
actor: {
|
|
89
|
+
type: user ? "user" : "system",
|
|
90
|
+
userId: user ? user.id : null,
|
|
91
|
+
displayName: user ? (user.displayName || user.username) : null,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
78
96
|
function handleKnowledgeMessage(ws, msg) {
|
|
79
97
|
if (msg.type === "knowledge_list") {
|
|
80
98
|
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFiles() });
|
|
@@ -111,6 +129,7 @@ function attachKnowledge(ctx) {
|
|
|
111
129
|
var knowledgeDir = path.join(cwd, "knowledge");
|
|
112
130
|
fs.mkdirSync(knowledgeDir, { recursive: true });
|
|
113
131
|
fs.writeFileSync(path.join(knowledgeDir, safeName), msg.content);
|
|
132
|
+
mirrorMateSource(ws, safeName);
|
|
114
133
|
sendTo(ws, { type: "knowledge_saved", name: safeName });
|
|
115
134
|
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFilesBasic() });
|
|
116
135
|
return true;
|
|
@@ -121,6 +140,9 @@ function attachKnowledge(ctx) {
|
|
|
121
140
|
var safeName = path.basename(msg.name);
|
|
122
141
|
var filePath = path.join(cwd, "knowledge", safeName);
|
|
123
142
|
try { fs.unlinkSync(filePath); } catch (e) {}
|
|
143
|
+
// Reconciling after the unlink appends a tombstone, so a deleted file
|
|
144
|
+
// does not stay live in the new backend.
|
|
145
|
+
mirrorMateSource(ws, safeName);
|
|
124
146
|
sendTo(ws, { type: "knowledge_deleted", name: safeName });
|
|
125
147
|
sendTo(ws, { type: "knowledge_list", files: listKnowledgeFilesBasic() });
|
|
126
148
|
return true;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Stable project knowledge and Git worktree change-set identities.
|
|
2
|
+
//
|
|
3
|
+
// Existing projects freeze their current path-derived scope so every opaque
|
|
4
|
+
// log ref remains valid. New projects receive a path-independent id. A linked
|
|
5
|
+
// worktree stores its generated change-set marker in Git's administrative
|
|
6
|
+
// directory, which survives daemon restarts and branch renames but disappears
|
|
7
|
+
// with the worktree itself; the durable log records retain the id afterward.
|
|
8
|
+
|
|
9
|
+
var crypto = require("crypto");
|
|
10
|
+
var fs = require("fs");
|
|
11
|
+
var path = require("path");
|
|
12
|
+
var { runGitSync } = require("./git-cli");
|
|
13
|
+
var { fsAsUser } = require("./os-users");
|
|
14
|
+
var logsRoot = require("./project-logs-root");
|
|
15
|
+
var utils = require("./utils");
|
|
16
|
+
|
|
17
|
+
var ID_PATTERN = /^[A-Za-z0-9_-]{1,120}$/;
|
|
18
|
+
|
|
19
|
+
function newId(prefix) {
|
|
20
|
+
return prefix + "_" + crypto.randomBytes(16).toString("base64url");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function validId(value, prefix) {
|
|
24
|
+
return typeof value === "string" && ID_PATTERN.test(value) && (!prefix || value.indexOf(prefix + "_") === 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function legacyKnowledgeId(cwd) {
|
|
28
|
+
return utils.encodeCwd(logsRoot.resolveProjectRoot(cwd));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ensureProjectKnowledgeId(project) {
|
|
32
|
+
if (!project || typeof project !== "object") return false;
|
|
33
|
+
if (validId(project.projectKnowledgeId)) return false;
|
|
34
|
+
project.projectKnowledgeId = project.path ? legacyKnowledgeId(project.path) : newId("pk");
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function initializeProjectKnowledge(config) {
|
|
39
|
+
var changed = false;
|
|
40
|
+
var projects = config && Array.isArray(config.projects) ? config.projects : [];
|
|
41
|
+
for (var i = 0; i < projects.length; i++) {
|
|
42
|
+
if (ensureProjectKnowledgeId(projects[i])) changed = true;
|
|
43
|
+
}
|
|
44
|
+
return changed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createProjectKnowledgeId() {
|
|
48
|
+
return newId("pk");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function git(cwd, args, osUserInfo) {
|
|
52
|
+
return runGitSync(cwd, args, null, osUserInfo).trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function commit(cwd, ref, osUserInfo) {
|
|
56
|
+
try { return git(cwd, ["rev-parse", ref || "HEAD"], osUserInfo); }
|
|
57
|
+
catch (e) { return null; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function markerPath(cwd, osUserInfo) {
|
|
61
|
+
try {
|
|
62
|
+
var gitDir = git(cwd, ["rev-parse", "--git-dir"], osUserInfo);
|
|
63
|
+
return path.join(path.resolve(cwd, gitDir), "clay-change-set.json");
|
|
64
|
+
} catch (e) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readMarker(filePath, knowledgeId) {
|
|
70
|
+
if (!filePath) return null;
|
|
71
|
+
try {
|
|
72
|
+
var parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
73
|
+
if (!parsed || parsed.projectKnowledgeId !== knowledgeId || !validId(parsed.changeSetId, "cs")) return null;
|
|
74
|
+
return parsed;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function deterministicFallback(knowledgeId, cwd) {
|
|
81
|
+
var digest = crypto.createHash("sha256").update(knowledgeId + "\u0000" + logsRoot.canonicalPath(cwd)).digest("base64url");
|
|
82
|
+
return "cs_" + digest.substring(0, 22);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function writeMarker(filePath, marker, osUserInfo) {
|
|
86
|
+
var content = JSON.stringify(marker, null, 2) + "\n";
|
|
87
|
+
if (osUserInfo) {
|
|
88
|
+
fsAsUser("write", { file: filePath, content: content, flag: "wx", mode: 0o600 }, osUserInfo);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
fs.writeFileSync(filePath, content, { flag: "wx", mode: 0o600 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveWorktreeContext(cwd, knowledgeId, details, osUserInfo) {
|
|
95
|
+
var input = details || {};
|
|
96
|
+
var filePath = markerPath(cwd, osUserInfo);
|
|
97
|
+
var marker = readMarker(filePath, knowledgeId);
|
|
98
|
+
if (!marker) {
|
|
99
|
+
marker = {
|
|
100
|
+
projectKnowledgeId: knowledgeId,
|
|
101
|
+
changeSetId: newId("cs"),
|
|
102
|
+
baseCommit: input.baseCommit || null,
|
|
103
|
+
createdAt: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
if (filePath) {
|
|
106
|
+
try {
|
|
107
|
+
writeMarker(filePath, marker, osUserInfo);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
marker = readMarker(filePath, knowledgeId) || marker;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!filePath || !readMarker(filePath, knowledgeId)) {
|
|
114
|
+
marker.changeSetId = deterministicFallback(knowledgeId, cwd);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
kind: "worktree",
|
|
118
|
+
changeSetId: marker.changeSetId,
|
|
119
|
+
branch: input.branch || null,
|
|
120
|
+
baseCommit: marker.baseCommit || input.baseCommit || null,
|
|
121
|
+
headCommit: commit(cwd, "HEAD", osUserInfo),
|
|
122
|
+
status: "active",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isMerged(parentCwd, worktreeCwd, osUserInfo) {
|
|
127
|
+
var head = commit(worktreeCwd, "HEAD", osUserInfo);
|
|
128
|
+
if (!head) return false;
|
|
129
|
+
try {
|
|
130
|
+
runGitSync(parentCwd, ["merge-base", "--is-ancestor", head, "HEAD"], null, osUserInfo);
|
|
131
|
+
return true;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function cleanCommit(value) {
|
|
138
|
+
return typeof value === "string" && /^[0-9a-f]{7,64}$/i.test(value) ? value : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function cleanBranch(value) {
|
|
142
|
+
if (typeof value !== "string") return null;
|
|
143
|
+
var cleaned = value.trim().substring(0, 200);
|
|
144
|
+
return cleaned && cleaned.indexOf("\n") === -1 && cleaned.indexOf("\r") === -1 ? cleaned : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeRecordContext(value) {
|
|
148
|
+
if (!value || value.kind !== "worktree" || !validId(value.changeSetId, "cs")) {
|
|
149
|
+
return { kind: "project", changeSetId: null, branch: null, baseCommit: null, headCommit: null, status: "active" };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
kind: "worktree",
|
|
153
|
+
changeSetId: value.changeSetId,
|
|
154
|
+
branch: cleanBranch(value.branch),
|
|
155
|
+
baseCommit: cleanCommit(value.baseCommit),
|
|
156
|
+
headCommit: cleanCommit(value.headCommit),
|
|
157
|
+
status: value.status === "merged" || value.status === "archived" ? value.status : "active",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
ID_PATTERN: ID_PATTERN,
|
|
163
|
+
validId: validId,
|
|
164
|
+
legacyKnowledgeId: legacyKnowledgeId,
|
|
165
|
+
ensureProjectKnowledgeId: ensureProjectKnowledgeId,
|
|
166
|
+
initializeProjectKnowledge: initializeProjectKnowledge,
|
|
167
|
+
createProjectKnowledgeId: createProjectKnowledgeId,
|
|
168
|
+
resolveWorktreeContext: resolveWorktreeContext,
|
|
169
|
+
commit: commit,
|
|
170
|
+
isMerged: isMerged,
|
|
171
|
+
normalizeRecordContext: normalizeRecordContext,
|
|
172
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Deliver new Project Log comments to the session responsible for the
|
|
2
|
+
// canonical entry. A busy Driver gets an isolated hidden reviewer so feedback
|
|
3
|
+
// never waits behind unrelated work in the visible conversation.
|
|
4
|
+
|
|
5
|
+
function attachProjectLogFeedbackDelivery(ctx) {
|
|
6
|
+
var sm = ctx.sm;
|
|
7
|
+
var getSdk = ctx.getSdk;
|
|
8
|
+
var onProcessingChanged = ctx.onProcessingChanged || function () {};
|
|
9
|
+
var getLinuxUserForSession = ctx.getLinuxUserForSession || function () { return null; };
|
|
10
|
+
|
|
11
|
+
function sessionForAuthor(author) {
|
|
12
|
+
if (!author || author.type !== "session" || !author.sessionKey) return null;
|
|
13
|
+
var match = null;
|
|
14
|
+
sm.sessions.forEach(function (session) {
|
|
15
|
+
if (match || !session || session.destroying || session.hidden) return;
|
|
16
|
+
if (session.cliSessionId !== author.sessionKey && "local:" + session.localId !== author.sessionKey) return;
|
|
17
|
+
if ((session.ownerId || null) !== (author.userId || null)) return;
|
|
18
|
+
if (sm.sessions.get(session.localId) !== session) return;
|
|
19
|
+
match = session;
|
|
20
|
+
});
|
|
21
|
+
return match;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findAuthorSession(entry) {
|
|
25
|
+
return sessionForAuthor(entry && entry.updatedBy) || sessionForAuthor(entry && entry.createdBy);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function startImmediateReviewer(source, entry, text, sdk) {
|
|
29
|
+
if (!sm.createSessionRaw || !sm.deleteSessionQuiet) return false;
|
|
30
|
+
var reviewer = sm.createSessionRaw({
|
|
31
|
+
ownerId: source.ownerId || null,
|
|
32
|
+
sessionVisibility: "private",
|
|
33
|
+
vendor: source.vendor || null,
|
|
34
|
+
model: source.model || null,
|
|
35
|
+
effort: source.effort || null,
|
|
36
|
+
permissionMode: source.permissionMode || null,
|
|
37
|
+
});
|
|
38
|
+
reviewer.hidden = true;
|
|
39
|
+
reviewer.title = "Project Log review";
|
|
40
|
+
reviewer.singleTurn = true;
|
|
41
|
+
reviewer.projectLogReview = { ref: entry.ref };
|
|
42
|
+
reviewer.isProcessing = true;
|
|
43
|
+
reviewer.sentToolResults = {};
|
|
44
|
+
reviewer.onQueryComplete = function () {
|
|
45
|
+
setImmediate(function () { sm.deleteSessionQuiet(reviewer.localId); });
|
|
46
|
+
};
|
|
47
|
+
sm.sendAndRecord(reviewer, {
|
|
48
|
+
type: "user_message",
|
|
49
|
+
text: text,
|
|
50
|
+
_internal: true,
|
|
51
|
+
projectLogFeedback: true,
|
|
52
|
+
projectLogRef: entry.ref,
|
|
53
|
+
});
|
|
54
|
+
Promise.resolve(sdk.startQuery(reviewer, text, null, getLinuxUserForSession(reviewer))).catch(function (error) {
|
|
55
|
+
reviewer.isProcessing = false;
|
|
56
|
+
sm.sendAndRecord(reviewer, {
|
|
57
|
+
type: "error",
|
|
58
|
+
text: "Could not review Project Log feedback: " + (error.message || String(error)),
|
|
59
|
+
_internal: true,
|
|
60
|
+
});
|
|
61
|
+
sm.deleteSessionQuiet(reviewer.localId);
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function feedbackPrompt(entry) {
|
|
67
|
+
return "[Project Log feedback]\n" +
|
|
68
|
+
"A user added a comment to " + entry.ref + ". Review the pending feedback now with list_log_feedback, " +
|
|
69
|
+
"then resolve this specific comment through review_log_comment. Apply supported corrections to the canonical " +
|
|
70
|
+
"entry; otherwise clarify or decline with a concise reason. Do not treat this internal notification as user " +
|
|
71
|
+
"chat or answer it only in the conversation.\n" +
|
|
72
|
+
"[End Project Log feedback]";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function deliver(entry) {
|
|
76
|
+
if (!entry || !entry.ref || !entry.pendingFeedbackCount) return false;
|
|
77
|
+
var session = findAuthorSession(entry);
|
|
78
|
+
var sdk = getSdk();
|
|
79
|
+
if (!session || !sdk) return false;
|
|
80
|
+
var text = feedbackPrompt(entry);
|
|
81
|
+
if (session.isProcessing && startImmediateReviewer(session, entry, text, sdk)) return true;
|
|
82
|
+
sm.sendAndRecord(session, {
|
|
83
|
+
type: "user_message",
|
|
84
|
+
text: text,
|
|
85
|
+
_internal: true,
|
|
86
|
+
projectLogFeedback: true,
|
|
87
|
+
projectLogRef: entry.ref,
|
|
88
|
+
});
|
|
89
|
+
session.lastActivity = Date.now();
|
|
90
|
+
if (!session.isProcessing) {
|
|
91
|
+
session.isProcessing = true;
|
|
92
|
+
session.sentToolResults = {};
|
|
93
|
+
onProcessingChanged();
|
|
94
|
+
sm.sendToSession(session, { type: "status", status: "processing" });
|
|
95
|
+
}
|
|
96
|
+
if (!sdk.pushMessage(session, text)) {
|
|
97
|
+
session._queryStartTs = Date.now();
|
|
98
|
+
Promise.resolve(sdk.startQuery(session, text, null, getLinuxUserForSession(session))).catch(function (error) {
|
|
99
|
+
session.isProcessing = false;
|
|
100
|
+
sm.sendAndRecord(session, {
|
|
101
|
+
type: "error",
|
|
102
|
+
text: "Could not deliver Project Log feedback: " + (error.message || String(error)),
|
|
103
|
+
_internal: true,
|
|
104
|
+
});
|
|
105
|
+
onProcessingChanged();
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
sm.broadcastSessionList();
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
deliver: deliver,
|
|
114
|
+
findAuthorSession: findAuthorSession,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { attachProjectLogFeedbackDelivery: attachProjectLogFeedbackDelivery };
|