clay-server 4.1.0-beta.6 → 4.1.0-beta.7
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/LICENSE +661 -21
- package/LICENSES/MIT-legacy.txt +21 -0
- package/LICENSES/README.md +7 -0
- package/README.md +2 -2
- package/bin/cli.js +0 -2
- package/lib/daemon.js +0 -2
- package/lib/default-ai-runtime.js +117 -0
- package/lib/git-cli.js +1 -0
- package/lib/issue-work-session.js +10 -0
- package/lib/issues-comments.js +40 -0
- package/lib/issues-commit-verification.js +30 -0
- package/lib/issues-mcp-server.js +27 -0
- package/lib/issues-schema.js +119 -0
- package/lib/issues-service.js +339 -0
- package/lib/issues-store.js +210 -0
- package/lib/model-entry.js +15 -0
- package/lib/project-capsule-turn.js +1 -0
- package/lib/project-connection.js +2 -0
- package/lib/project-delegated-session.js +1 -0
- package/lib/project-filesystem.js +10 -10
- package/lib/project-issue-launch.js +154 -0
- package/lib/project-issues.js +58 -0
- package/lib/project-logs-mcp-server.js +8 -12
- package/lib/project-message-delivery.js +4 -2
- package/lib/project-models.js +71 -3
- package/lib/project-pending-message-queue.js +264 -0
- package/lib/project-scheduled-tasks.js +2 -4
- package/lib/project-sessions.js +82 -31
- package/lib/project-tool-llm-runtime.js +48 -0
- package/lib/project-user-message.js +178 -42
- package/lib/project-workspace-query.js +25 -0
- package/lib/project.js +102 -18
- package/lib/public/app.js +38 -4
- package/lib/public/css/default-ai.css +33 -0
- package/lib/public/css/input.css +89 -5
- package/lib/public/css/issues.css +101 -0
- package/lib/public/css/messages.css +19 -0
- package/lib/public/css/pending-message-queue.css +187 -0
- package/lib/public/css/scheduled-tasks.css +3 -4
- package/lib/public/index.html +25 -17
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-cursors.js +29 -12
- package/lib/public/modules/app-message-router.js +8 -0
- package/lib/public/modules/app-messages.js +12 -3
- package/lib/public/modules/clay-file-links.js +111 -0
- package/lib/public/modules/clay-issue-links.js +131 -0
- package/lib/public/modules/clay-log-links.js +2 -0
- package/lib/public/modules/cursor-sharing-preference.js +38 -0
- package/lib/public/modules/default-ai.js +377 -0
- package/lib/public/modules/filebrowser.js +43 -2
- package/lib/public/modules/input.js +7 -17
- package/lib/public/modules/issues-context.js +117 -0
- package/lib/public/modules/issues-render.js +29 -0
- package/lib/public/modules/issues.js +270 -0
- package/lib/public/modules/markdown.js +4 -0
- package/lib/public/modules/message-delivery.js +5 -3
- package/lib/public/modules/pending-message-image-editor.js +143 -0
- package/lib/public/modules/pending-message-queue-model.js +22 -0
- package/lib/public/modules/pending-message-queue.js +449 -0
- package/lib/public/modules/scheduled-task-engine.js +0 -50
- package/lib/public/modules/scheduled-tasks.js +7 -16
- package/lib/public/modules/sidebar-mobile.js +3 -0
- package/lib/public/modules/sticky-note-markdown.js +1 -0
- package/lib/public/modules/terminal.js +2 -0
- package/lib/public/modules/tool-palette-order.js +1 -0
- package/lib/public/style.css +2 -0
- package/lib/sdk-bridge.js +34 -11
- package/lib/server-cursor-sharing.js +65 -0
- package/lib/server-default-ai.js +265 -0
- package/lib/server-home-chat.js +29 -9
- package/lib/server-home-clay-entry.js +4 -1
- package/lib/server-home-debate-planning.js +32 -14
- package/lib/server-home-mate-creation.js +30 -11
- package/lib/server-home-models.js +14 -6
- package/lib/server-issue-links.js +18 -0
- package/lib/server.js +41 -0
- package/lib/session-notes-mcp-server.js +1 -1
- package/lib/session-search.js +4 -1
- package/lib/tool-llm.js +1 -0
- package/lib/users-default-ai-preferences.js +80 -0
- package/lib/users-preferences.js +35 -0
- package/lib/users.js +10 -0
- package/lib/workspace-query-projection.js +29 -0
- package/lib/workspace-query-service.js +19 -29
- package/lib/ws-schema.js +36 -1
- package/lib/yoke/adapters/claude-worker.js +4 -2
- package/lib/yoke/adapters/claude.js +5 -1
- package/lib/yoke/package.json +1 -1
- package/package.json +5 -6
- package/bin/claude-relay.js +0 -6
|
@@ -43,11 +43,11 @@ function attachFilesystem(ctx) {
|
|
|
43
43
|
// Unsubscription must remain available after permission revocation.
|
|
44
44
|
if (/^fs_/.test(msg.type) && msg.type !== "fs_unwatch") {
|
|
45
45
|
if (!access.canUseFiles(ws)) {
|
|
46
|
-
sendTo(ws, { type: msg.type + "_result", error: "File browser access is not permitted" });
|
|
46
|
+
sendTo(ws, { type: msg.type + "_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, error: "File browser access is not permitted" });
|
|
47
47
|
return true;
|
|
48
48
|
}
|
|
49
49
|
try { access.osIdentity(ws); } catch (e) {
|
|
50
|
-
sendTo(ws, { type: msg.type + "_result", error: e.message });
|
|
50
|
+
sendTo(ws, { type: msg.type + "_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, error: e.message });
|
|
51
51
|
return true;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -152,7 +152,7 @@ function attachFilesystem(ctx) {
|
|
|
152
152
|
fsFile = safeAbsPath(msg.path);
|
|
153
153
|
}
|
|
154
154
|
if (!fsFile) {
|
|
155
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, error: "Access denied" });
|
|
155
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, error: "Access denied" });
|
|
156
156
|
return true;
|
|
157
157
|
}
|
|
158
158
|
try {
|
|
@@ -162,34 +162,34 @@ function attachFilesystem(ctx) {
|
|
|
162
162
|
// Run stat and read as target OS user
|
|
163
163
|
var statResult = fsAsUser("stat", { file: fsFile }, fsReadUserInfo);
|
|
164
164
|
if (statResult.size > FS_MAX_SIZE) {
|
|
165
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, binary: true, size: statResult.size, error: "File too large (" + (statResult.size / 1024 / 1024).toFixed(1) + " MB)" });
|
|
165
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, binary: true, size: statResult.size, error: "File too large (" + (statResult.size / 1024 / 1024).toFixed(1) + " MB)" });
|
|
166
166
|
return true;
|
|
167
167
|
}
|
|
168
168
|
if (BINARY_EXTS.has(ext)) {
|
|
169
|
-
var result = { type: "fs_read_result", path: msg.path, binary: true, size: statResult.size };
|
|
169
|
+
var result = { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, binary: true, size: statResult.size };
|
|
170
170
|
if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
|
|
171
171
|
sendTo(ws, result);
|
|
172
172
|
return true;
|
|
173
173
|
}
|
|
174
174
|
var readResult = fsAsUser("read", { file: fsFile, readContent: true }, fsReadUserInfo);
|
|
175
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, content: readResult.content, size: statResult.size });
|
|
175
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, content: readResult.content, size: statResult.size });
|
|
176
176
|
} else {
|
|
177
177
|
var stat = fs.statSync(fsFile);
|
|
178
178
|
if (stat.size > FS_MAX_SIZE) {
|
|
179
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, binary: true, size: stat.size, error: "File too large (" + (stat.size / 1024 / 1024).toFixed(1) + " MB)" });
|
|
179
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, binary: true, size: stat.size, error: "File too large (" + (stat.size / 1024 / 1024).toFixed(1) + " MB)" });
|
|
180
180
|
return true;
|
|
181
181
|
}
|
|
182
182
|
if (BINARY_EXTS.has(ext)) {
|
|
183
|
-
var result = { type: "fs_read_result", path: msg.path, binary: true, size: stat.size };
|
|
183
|
+
var result = { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, binary: true, size: stat.size };
|
|
184
184
|
if (IMAGE_EXTS.has(ext)) result.imageUrl = "api/file?path=" + encodeURIComponent(msg.path);
|
|
185
185
|
sendTo(ws, result);
|
|
186
186
|
return true;
|
|
187
187
|
}
|
|
188
188
|
var content = fs.readFileSync(fsFile, "utf8");
|
|
189
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, content: content, size: stat.size });
|
|
189
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, content: content, size: stat.size });
|
|
190
190
|
}
|
|
191
191
|
} catch (e) {
|
|
192
|
-
sendTo(ws, { type: "fs_read_result", path: msg.path, error: e.message });
|
|
192
|
+
sendTo(ws, { type: "fs_read_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, error: e.message });
|
|
193
193
|
}
|
|
194
194
|
return true;
|
|
195
195
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Creates one visible Driver session for an exact Issue revision.
|
|
2
|
+
|
|
3
|
+
var yoke = require("./yoke");
|
|
4
|
+
var provenance = require("./session-provenance");
|
|
5
|
+
|
|
6
|
+
function attachIssueLaunch(ctx) {
|
|
7
|
+
var requests = new Map();
|
|
8
|
+
var operations = new Map();
|
|
9
|
+
|
|
10
|
+
function osUsersEnabled() {
|
|
11
|
+
return typeof ctx.osUsers === "function" ? ctx.osUsers() === true : ctx.osUsers === true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function requestFingerprint(args) {
|
|
15
|
+
return JSON.stringify({ ref: args.ref, expectedRevision: args.expectedRevision });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function trimRequests() {
|
|
19
|
+
if (requests.size <= 200) return;
|
|
20
|
+
for (var pair of requests) {
|
|
21
|
+
if (pair[1].settled) { requests.delete(pair[0]); return; }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function record(session, event) {
|
|
26
|
+
if (ctx.sm && typeof ctx.sm.sendAndRecord === "function") ctx.sm.sendAndRecord(session, event);
|
|
27
|
+
else {
|
|
28
|
+
session.history = session.history || [];
|
|
29
|
+
session.history.push(event);
|
|
30
|
+
if (ctx.sm && typeof ctx.sm.appendToSessionFile === "function") ctx.sm.appendToSessionFile(session, event);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sendStatus(session, status) {
|
|
35
|
+
if (ctx.sm && typeof ctx.sm.sendToSession === "function") ctx.sm.sendToSession(session, { type: "status", status: status });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function lastLaunchError(session, fromIndex) {
|
|
39
|
+
var history = session.history || [];
|
|
40
|
+
for (var i = history.length - 1; i >= fromIndex; i--) {
|
|
41
|
+
if (history[i] && history[i].type === "error" && history[i].text) return String(history[i].text);
|
|
42
|
+
}
|
|
43
|
+
return "The Driver session could not start its agent runtime.";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function failLaunch(session, fromIndex, error) {
|
|
47
|
+
var message = error && error.message ? error.message : lastLaunchError(session, fromIndex);
|
|
48
|
+
if (lastLaunchError(session, fromIndex) === "The Driver session could not start its agent runtime.") {
|
|
49
|
+
record(session, { type: "error", text: "Could not start issue work: " + message, issueRef: session.issueOrigin && session.issueOrigin.ref });
|
|
50
|
+
}
|
|
51
|
+
session.isProcessing = false;
|
|
52
|
+
if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
|
|
53
|
+
sendStatus(session, "idle");
|
|
54
|
+
var launchError = new Error(message || "The Driver session could not start its agent runtime.");
|
|
55
|
+
launchError.sessionId = session.localId;
|
|
56
|
+
return launchError;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function run(ws, actor, actorId, args, bound) {
|
|
60
|
+
var entry = bound.readIssue({ ref: args.ref });
|
|
61
|
+
if (entry.revision !== args.expectedRevision) throw new Error("Issue revision conflict. Reload before starting work.");
|
|
62
|
+
if (entry.status === "resolved" || entry.status === "closed") throw new Error("Reopen this issue before starting work.");
|
|
63
|
+
if ((entry.linkedWorkSessions || []).length >= 40) throw new Error("This issue has reached its work-session limit.");
|
|
64
|
+
|
|
65
|
+
var expectedLinuxUser = typeof ctx.getLinuxUserForSession === "function" ? ctx.getLinuxUserForSession({ ownerId: actorId }) : null;
|
|
66
|
+
if (osUsersEnabled() && !expectedLinuxUser) throw new Error("An OS identity is required to start work.");
|
|
67
|
+
var runtime = await ctx.resolveDefaultAi(ws);
|
|
68
|
+
if (ws.readyState !== 1 || ws._clayUser !== actor || actorId !== (ws._clayUser && ws._clayUser.id || null)) {
|
|
69
|
+
throw new Error("The requesting connection changed.");
|
|
70
|
+
}
|
|
71
|
+
if (typeof ctx.canStartWork === "function" && ctx.canStartWork(ws, actor) !== true) throw new Error("Issue work is no longer allowed from this connection.");
|
|
72
|
+
entry = bound.readIssue({ ref: args.ref });
|
|
73
|
+
if (entry.revision !== args.expectedRevision) throw new Error("Issue revision conflict. Reload before starting work.");
|
|
74
|
+
if (entry.status === "resolved" || entry.status === "closed") throw new Error("Reopen this issue before starting work.");
|
|
75
|
+
if (!runtime || !runtime.ready || !ctx.sm || (ctx.sm.installedVendors || []).indexOf(runtime.vendor) < 0) {
|
|
76
|
+
throw new Error(runtime && runtime.error || "Default AI is unavailable.");
|
|
77
|
+
}
|
|
78
|
+
if (yoke.sessionTools.capability(runtime.vendor).kind === "none") throw new Error("The selected provider does not support Issues tools.");
|
|
79
|
+
var liveLinuxUser = typeof ctx.getLinuxUserForSession === "function" ? ctx.getLinuxUserForSession({ ownerId: actorId }) : null;
|
|
80
|
+
if (osUsersEnabled() && liveLinuxUser !== expectedLinuxUser) throw new Error("The OS identity changed while issue work was being prepared.");
|
|
81
|
+
|
|
82
|
+
var session = ctx.sm.createSessionRaw({ vendor: runtime.vendor, model: runtime.model, effort: runtime.effort || null,
|
|
83
|
+
mode: "gui", ownerId: actor && actor.id || null, sessionVisibility: "private" });
|
|
84
|
+
provenance.ensureOrigin(session);
|
|
85
|
+
if (session.hidden === true || session.delegated === true || provenance.isWorker(session)) {
|
|
86
|
+
ctx.sm.deleteSession(session.localId);
|
|
87
|
+
throw new Error("A visible Driver session is required to start issue work.");
|
|
88
|
+
}
|
|
89
|
+
var linuxUser = typeof ctx.ensureProjectAccessForSession === "function"
|
|
90
|
+
? ctx.ensureProjectAccessForSession(session) : expectedLinuxUser;
|
|
91
|
+
if (osUsersEnabled() && (!linuxUser || linuxUser !== expectedLinuxUser)) {
|
|
92
|
+
ctx.sm.deleteSession(session.localId);
|
|
93
|
+
throw new Error("The OS identity changed before issue work could start.");
|
|
94
|
+
}
|
|
95
|
+
session.issueOrigin = { ref: entry.ref, revision: args.expectedRevision, requestId: args.requestId, sessionOriginId: session.sessionOriginId };
|
|
96
|
+
try { bound.linkWorkSession(args, session); }
|
|
97
|
+
catch (error) { ctx.sm.deleteSession(session.localId); throw error; }
|
|
98
|
+
|
|
99
|
+
session.title = "Issue · " + entry.title;
|
|
100
|
+
session.titleManuallySet = true;
|
|
101
|
+
ctx.sm.saveSessionFile(session);
|
|
102
|
+
var prompt = "Work on project issue " + entry.ref + ". Read it with read_issue, inspect the repository, implement the requested outcome and verify it. Update this same issue as work progresses. Do not commit, publish, or create a PR unless explicitly authorized. If implementation is verified but no commit is authorized, keep it in_progress and record that state. Resolve only with verified commit evidence. Session completion never resolves the issue automatically.\n\nRequested issue:\n" + JSON.stringify({ title: entry.title, summary: entry.summary, body: entry.body });
|
|
103
|
+
ctx.sm.switchSession(session.localId, ws);
|
|
104
|
+
ws._clayActiveSession = session.localId;
|
|
105
|
+
record(session, { type: "user_message", text: prompt, issueRef: entry.ref, issueRevision: args.expectedRevision });
|
|
106
|
+
session.isProcessing = true;
|
|
107
|
+
sendStatus(session, "processing");
|
|
108
|
+
if (typeof ctx.onProcessingChanged === "function") ctx.onProcessingChanged();
|
|
109
|
+
ctx.sm.broadcastSessionList();
|
|
110
|
+
var historyStart = (session.history || []).length;
|
|
111
|
+
var sdk = ctx.getSdk && ctx.getSdk();
|
|
112
|
+
if (!sdk || typeof sdk.startQuery !== "function") throw failLaunch(session, historyStart, new Error("The SDK bridge is not ready."));
|
|
113
|
+
try { await sdk.startQuery(session, prompt, undefined, linuxUser); }
|
|
114
|
+
catch (error) { throw failLaunch(session, historyStart, error); }
|
|
115
|
+
if (ctx.sm.sessions.get(session.localId) !== session || !session.queryInstance) {
|
|
116
|
+
throw failLaunch(session, historyStart, null);
|
|
117
|
+
}
|
|
118
|
+
return { sessionId: session.localId, sessionOriginId: session.sessionOriginId, ref: entry.ref };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return function startWork(ws, msg, bound) {
|
|
122
|
+
var args = Object.assign({}, msg && msg.args || {}, { requestId: msg && msg.requestId });
|
|
123
|
+
var actor = ws && ws._clayUser || null;
|
|
124
|
+
var actorId = actor && actor.id || null;
|
|
125
|
+
if (!msg || typeof msg.requestId !== "string" || !msg.requestId || msg.requestId.length > 200) {
|
|
126
|
+
return Promise.reject(new Error("A work request identifier is required."));
|
|
127
|
+
}
|
|
128
|
+
if (!bound || typeof args.ref !== "string" || !Number.isInteger(args.expectedRevision)) {
|
|
129
|
+
return Promise.reject(new Error("An exact issue reference and expectedRevision are required."));
|
|
130
|
+
}
|
|
131
|
+
var fingerprint = requestFingerprint(args);
|
|
132
|
+
var requestKey = JSON.stringify([bound.scopeId, actorId, msg.requestId]);
|
|
133
|
+
var prior = requests.get(requestKey);
|
|
134
|
+
if (prior) {
|
|
135
|
+
if (prior.ws !== ws || prior.fingerprint !== fingerprint) return Promise.reject(new Error("Work request identifier was already used with different arguments."));
|
|
136
|
+
return prior.task;
|
|
137
|
+
}
|
|
138
|
+
var operationKey = JSON.stringify([bound.scopeId, actorId, args.ref, args.expectedRevision]);
|
|
139
|
+
var operation = operations.get(operationKey);
|
|
140
|
+
var task = operation ? operation.task : run(ws, actor, actorId, args, bound);
|
|
141
|
+
if (!operation) {
|
|
142
|
+
operations.set(operationKey, { task: task });
|
|
143
|
+
task.then(function () { if (operations.get(operationKey).task === task) operations.delete(operationKey); },
|
|
144
|
+
function () { if (operations.get(operationKey).task === task) operations.delete(operationKey); });
|
|
145
|
+
}
|
|
146
|
+
var request = { ws: ws, fingerprint: fingerprint, task: task, settled: false };
|
|
147
|
+
requests.set(requestKey, request);
|
|
148
|
+
task.then(function () { request.settled = true; trimRequests(); }, function () { request.settled = true; trimRequests(); });
|
|
149
|
+
trimRequests();
|
|
150
|
+
return task;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = { attachIssueLaunch: attachIssueLaunch };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
var mcp = require("./issues-mcp-server");
|
|
2
|
+
var resolveIssueWorkSession = require("./issue-work-session").resolveIssueWorkSession;
|
|
3
|
+
function attachProjectIssues(ctx) {
|
|
4
|
+
function user(ws) {
|
|
5
|
+
return !ctx.isMate && ctx.service && ctx.service.bindUser({ projectSlug: ctx.projectSlug, userId: ws._clayUser && ws._clayUser.id });
|
|
6
|
+
}
|
|
7
|
+
function notify(entry, type) {
|
|
8
|
+
for (var ws of ctx.getClients()) {
|
|
9
|
+
if (!ws || ws.readyState !== 1 || ws._clayPane) continue;
|
|
10
|
+
try { var bound = user(ws); if (bound) { if (entry.deleted) bound.readIssueTombstone({ ref: entry.ref }); else bound.readIssue({ ref: entry.ref }); ctx.sendTo(ws, Object.assign({ type: type || "issue_updated", projectSlug: ctx.projectSlug, ref: entry.ref, status: entry.status, title: entry.title, revision: entry.revision }, type === "issue_deleted" ? { deleted: true } : {})); } } catch (error) { /* Access may have changed. */ }
|
|
11
|
+
}
|
|
12
|
+
return entry;
|
|
13
|
+
}
|
|
14
|
+
function sessionBound(session) {
|
|
15
|
+
var bound = !ctx.isMate && ctx.service && session && ctx.service.bindProjectSession({ projectSlug: ctx.projectSlug, session: session });
|
|
16
|
+
if (!bound) return null;
|
|
17
|
+
["createIssue", "updateIssue", "reviewIssueComment"].forEach(function (method) {
|
|
18
|
+
var original = bound[method];
|
|
19
|
+
bound[method] = async function (args) { var result = await original(args); if (method === "reviewIssueComment") return notify(result, "issue_commented"); return notify(result); };
|
|
20
|
+
});
|
|
21
|
+
return bound;
|
|
22
|
+
}
|
|
23
|
+
function defs(session) { return ctx.isMate || !ctx.service || (session && !sessionBound(session)) ? [] : mcp.getToolDefs(sessionBound(session)); }
|
|
24
|
+
function handleMessage(ws, msg) {
|
|
25
|
+
var methods = { issues_list: "listIssues", issue_read: "readIssue", issue_create: "createIssue", issue_update: "updateIssue", issue_history: "issueHistory", issue_revision: "readIssueRevision", issue_comment: "commentIssue", issue_delete: "removeIssue" };
|
|
26
|
+
if (!methods[msg.type] && msg.type !== "issue_start_work" && msg.type !== "issue_open_work") return false;
|
|
27
|
+
Promise.resolve().then(async function () {
|
|
28
|
+
var bound = user(ws);
|
|
29
|
+
if (!bound) throw new Error("Issues are unavailable for this project.");
|
|
30
|
+
var result;
|
|
31
|
+
if (msg.type === "issue_open_work") {
|
|
32
|
+
var session = resolveIssueWorkSession(bound, msg.args || {});
|
|
33
|
+
ctx.sm.switchSession(session.localId, ws); ws._clayActiveSession = session.localId;
|
|
34
|
+
result = { sessionId: session.localId };
|
|
35
|
+
} else if (msg.type === "issue_start_work") { result = await ctx.startWork(ws, msg, bound); notify(bound.readIssue({ ref: msg.args.ref })); }
|
|
36
|
+
else {
|
|
37
|
+
result = await bound[methods[msg.type]](msg.args || {});
|
|
38
|
+
if (msg.type === "issue_create" || msg.type === "issue_update") notify(result);
|
|
39
|
+
else if (msg.type === "issue_comment") notify(result, "issue_commented");
|
|
40
|
+
else if (msg.type === "issue_delete") notify(result, "issue_deleted");
|
|
41
|
+
}
|
|
42
|
+
ctx.sendTo(ws, { type: "issues_result", requestId: msg.requestId, action: msg.type, result: result, canDelete: bound.canDelete === true });
|
|
43
|
+
}).catch(function (error) {
|
|
44
|
+
var result = error && error.sessionId ? { sessionId: error.sessionId } : undefined;
|
|
45
|
+
ctx.sendTo(ws, { type: "issues_result", requestId: msg.requestId, action: msg.type, error: error.message, result: result });
|
|
46
|
+
});
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
handleMessage: handleMessage, notify: notify,
|
|
51
|
+
getDynamicToolDefs: function (session) { return defs(session).map(function (tool) { tool.permissionName = "mcp__clay-issues__" + tool.name; return tool; }); },
|
|
52
|
+
createMcpServer: function (adapter, session) { return ctx.isMate || !ctx.service || !adapter.createToolServer ? null : adapter.createToolServer({ name: "clay-issues", version: "1.0.0", tools: defs(session) }); },
|
|
53
|
+
getBridgeTools: function (session, normalize) { return defs(session).map(function (tool) { return { server: "clay-issues", name: tool.name, description: tool.description, inputSchema: normalize(tool.inputSchema) }; }); },
|
|
54
|
+
callBridgeTool: function (session, name, args) { var tool = defs(session).find(function (item) { return item.name === name; }); if (!tool || !session) return Promise.reject(new Error("Issue tool unavailable.")); return tool.handler(args); },
|
|
55
|
+
getSystemPrompt: function (session) { return sessionBound(session) ? "--- Project Issues ---\n" + mcp.CONTRACT : ""; },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
module.exports = { attachProjectIssues: attachProjectIssues };
|
|
@@ -52,21 +52,17 @@ var LEARNING_CONTRACT =
|
|
|
52
52
|
// history or loses the history when the board is cleared.
|
|
53
53
|
//
|
|
54
54
|
// This is guidance for the Driver, not an automatic mirror: notes are also
|
|
55
|
-
// written by people and by Mates that hold no Logs authority, so
|
|
56
|
-
// makes creating a note mutate
|
|
55
|
+
// written by people and by Mates that hold no Issues or Logs authority, so
|
|
56
|
+
// nothing here makes creating a note mutate either canonical record.
|
|
57
57
|
var ATTENTION_CONTRACT =
|
|
58
58
|
"Sticky Notes and Project Logs are two different layers and must not be confused. " +
|
|
59
59
|
"A Sticky Note is the transient attention layer: an unresolved, actionable commitment or defect that stays on the active board only while it still needs action, and is closed once it no longer does. Closing is reversible and never deletes the note. " +
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"If the
|
|
65
|
-
"
|
|
66
|
-
"When the defect is fixed, revise that same entry with the remediation, how it was verified, and the outcome as a new canonical revision, and only then close the Sticky Note. Close it, never delete it: the note leaves the active board and the entry stays permanent. " +
|
|
67
|
-
"If you find and fully fix a defect inside the current task, do not open a Sticky Note for it at all, and write a log entry only when the discovery itself has durable value for the project. " +
|
|
68
|
-
"This pairing applies to concrete unresolved defects, not to everything on the board. Never mirror speculation, general cleanup ideas, transient blockers, proposals, or ordinary notes into the ledger. " +
|
|
69
|
-
"Notes written by people or by other sessions are not yours to mirror; judge only what you discovered yourself.";
|
|
60
|
+
"Project Issues are the primary record for concrete defects. For a Project Driver session with Issues authority, search, reuse, or create the Issue first, recording observable evidence, affected component, impact, and next action there. Then write only a concise Sticky Note title, one-line actionable cue, and the actual opaque issue: reference; never duplicate the full defect details in the note. " +
|
|
61
|
+
"If you lack Issues authority, route the defect through an eligible Project Driver. Never invent an issue reference, drop the alert, mirror storage automatically, or expand privileges. " +
|
|
62
|
+
"Project Logs remain required for concise work continuity, decisions, and results. Reference the issue in the Log instead of duplicating its full defect report. Logs are durable and versioned; the issue keeps the defect lifecycle and the note remains the transient alert. " +
|
|
63
|
+
"If the Issue already exists, revise that Issue instead of creating a second one. When the defect is fixed, update the Issue with remediation and verification while respecting its real commit-evidence and status rules; only then close a Sticky Note created by this same session. Close it, never delete it. " +
|
|
64
|
+
"If you find and fully fix a defect inside the current task, do not open a Sticky Note for it at all, and keep the required concise Log only when the work instruction or result belongs in the project record. " +
|
|
65
|
+
"This Issue-plus-note flow applies to concrete unresolved defects, not to ordinary notes or proposals. Notes written by people or by other sessions are not yours to mirror; judge authority and evidence rather than mutating records automatically.";
|
|
70
66
|
|
|
71
67
|
var REVIEW_CONTRACT =
|
|
72
68
|
"People cannot edit the ledger, so a comment is a proposal or a piece of evidence and never an automatic change. Judge each one against the project itself. " +
|
|
@@ -29,7 +29,7 @@ function ensureRecordedMessages(session) {
|
|
|
29
29
|
return recorded;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function createProjectMessageDelivery(sendTo) {
|
|
32
|
+
function createProjectMessageDelivery(sendTo, projectSlug) {
|
|
33
33
|
function inspect(ws, session, msg) {
|
|
34
34
|
var clientMessageId = normalizeClientMessageId(msg && msg.clientMessageId);
|
|
35
35
|
if (!clientMessageId) return { clientMessageId: null, ownerId: ownerKey(ws), duplicate: false };
|
|
@@ -48,11 +48,13 @@ function createProjectMessageDelivery(sendTo) {
|
|
|
48
48
|
ensureRecordedMessages(session).set(deliveryKey(receipt.ownerId, receipt.clientMessageId), message);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function acknowledge(ws, receipt, message) {
|
|
51
|
+
function acknowledge(ws, receipt, message, session) {
|
|
52
52
|
if (!receipt || !receipt.clientMessageId) return;
|
|
53
53
|
sendTo(ws, {
|
|
54
54
|
type: "message_ack",
|
|
55
55
|
clientMessageId: receipt.clientMessageId,
|
|
56
|
+
projectSlug: projectSlug || null,
|
|
57
|
+
sessionId: session ? session.localId : null,
|
|
56
58
|
message: message || receipt.recordedMessage || null,
|
|
57
59
|
});
|
|
58
60
|
}
|
package/lib/project-models.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
var yoke = require("./yoke");
|
|
2
|
+
var defaultAiRuntime = require("./default-ai-runtime");
|
|
2
3
|
|
|
3
4
|
function modelEntryValue(entry) {
|
|
4
5
|
if (!entry) return "";
|
|
@@ -12,6 +13,17 @@ function modelEntryMatches(entry, value) {
|
|
|
12
13
|
return entry.value === value || entry.id === value || entry.resolvedModel === value;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
function modelsWithEffortMetadata(models, vendorInfo) {
|
|
17
|
+
var fallback = vendorInfo && Array.isArray(vendorInfo.effortLevels) ? vendorInfo.effortLevels : [];
|
|
18
|
+
return (models || []).map(function (model) {
|
|
19
|
+
if (typeof model === "string") {
|
|
20
|
+
return { value: model, displayName: model, supportedEffortLevels: fallback.slice() };
|
|
21
|
+
}
|
|
22
|
+
if (!model || Array.isArray(model.supportedEffortLevels)) return model;
|
|
23
|
+
return Object.assign({}, model, { supportedEffortLevels: fallback.slice() });
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
function normalizeCatalogModels(models) {
|
|
16
28
|
var normalized = [];
|
|
17
29
|
models = models || [];
|
|
@@ -64,8 +76,12 @@ function attachModels(ctx) {
|
|
|
64
76
|
var serverPort = ctx.serverPort;
|
|
65
77
|
var serverTls = ctx.serverTls;
|
|
66
78
|
var serverAuthToken = ctx.serverAuthToken;
|
|
79
|
+
var resolveDefaultAi = ctx.resolveDefaultAi;
|
|
67
80
|
|
|
68
|
-
function getVendorAvailability() {
|
|
81
|
+
function getVendorAvailability(ws) {
|
|
82
|
+
if (sm.installedVendors === undefined && sdk && typeof sdk.refreshInstalledVendors === "function") {
|
|
83
|
+
sdk.refreshInstalledVendors(getLinuxUserForWs(ws));
|
|
84
|
+
}
|
|
69
85
|
var available = (sm.availableVendors || []).slice();
|
|
70
86
|
var installed = (sm.installedVendors || []).slice();
|
|
71
87
|
var ids = available.slice();
|
|
@@ -160,8 +176,8 @@ function attachModels(ctx) {
|
|
|
160
176
|
console.error("[project-models] model loading failed for " + vendor + ":", e.message || e);
|
|
161
177
|
}
|
|
162
178
|
|
|
163
|
-
var vendorModels = (sm.modelsByVendor && sm.modelsByVendor[vendor]) || [];
|
|
164
179
|
var vendorInfo = yoke.getVendorInfo(vendor);
|
|
180
|
+
var vendorModels = modelsWithEffortMetadata((sm.modelsByVendor && sm.modelsByVendor[vendor]) || [], vendorInfo);
|
|
165
181
|
var displayName = vendorInfo ? vendorInfo.displayName : vendor;
|
|
166
182
|
var status = vendorModels.length > 0 ? "ready" : (loadError ? "error" : "empty");
|
|
167
183
|
var defaultModel = "";
|
|
@@ -191,11 +207,62 @@ function attachModels(ctx) {
|
|
|
191
207
|
installedVendors: sm.installedVendors || [],
|
|
192
208
|
status: status,
|
|
193
209
|
error: errorText,
|
|
210
|
+
effortLevels: vendorInfo && Array.isArray(vendorInfo.effortLevels) ? vendorInfo.effortLevels.slice() : [],
|
|
194
211
|
};
|
|
195
212
|
}
|
|
196
213
|
|
|
197
214
|
async function resolveConfiguredModel(ws, alias) {
|
|
198
|
-
|
|
215
|
+
if (typeof resolveDefaultAi === "function") {
|
|
216
|
+
var defaultSelection = await resolveDefaultAi(ws);
|
|
217
|
+
var normalizedAlias = alias === "fast" || alias === "deep" ? alias : "standard";
|
|
218
|
+
if (!defaultSelection || !defaultSelection.ready) {
|
|
219
|
+
return {
|
|
220
|
+
status: "error",
|
|
221
|
+
vendor: defaultSelection && defaultSelection.vendor || "",
|
|
222
|
+
vendorName: defaultSelection && defaultSelection.vendor || "",
|
|
223
|
+
model: "",
|
|
224
|
+
modelName: "",
|
|
225
|
+
alias: normalizedAlias,
|
|
226
|
+
effort: "",
|
|
227
|
+
error: defaultSelection && defaultSelection.error || "Default AI is unavailable.",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
var selectedModel = defaultSelection.model;
|
|
231
|
+
var selectedName = defaultSelection.model;
|
|
232
|
+
if (normalizedAlias !== "standard") {
|
|
233
|
+
var selectedCatalog = await getVendorCatalog(ws, defaultSelection.vendor);
|
|
234
|
+
var aliasEntry = selectCatalogModel(selectedCatalog.models || [], defaultSelection.model, normalizedAlias);
|
|
235
|
+
if (!aliasEntry || selectedCatalog.status !== "ready") {
|
|
236
|
+
return {
|
|
237
|
+
status: "error",
|
|
238
|
+
vendor: defaultSelection.vendor,
|
|
239
|
+
vendorName: defaultSelection.vendor,
|
|
240
|
+
model: "",
|
|
241
|
+
modelName: "",
|
|
242
|
+
alias: normalizedAlias,
|
|
243
|
+
effort: defaultSelection.effort || "",
|
|
244
|
+
error: selectedCatalog.error || "The selected Default AI provider has no available model.",
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
selectedModel = aliasEntry.value;
|
|
248
|
+
selectedName = aliasEntry.displayName || selectedModel;
|
|
249
|
+
var aliasEffort = defaultAiRuntime.resolveEffort({ effort: defaultSelection.effort || "" }, aliasEntry.source, selectedCatalog);
|
|
250
|
+
if (!aliasEffort.ok) aliasEffort = defaultAiRuntime.resolveEffort(null, aliasEntry.source, selectedCatalog);
|
|
251
|
+
defaultSelection = Object.assign({}, defaultSelection, { effort: aliasEffort.effort || "" });
|
|
252
|
+
}
|
|
253
|
+
var selectedInfo = yoke.getVendorInfo(defaultSelection.vendor);
|
|
254
|
+
return {
|
|
255
|
+
status: "ready",
|
|
256
|
+
vendor: defaultSelection.vendor,
|
|
257
|
+
vendorName: selectedInfo && selectedInfo.displayName ? selectedInfo.displayName : defaultSelection.vendor,
|
|
258
|
+
alias: normalizedAlias,
|
|
259
|
+
model: selectedModel,
|
|
260
|
+
modelName: selectedName,
|
|
261
|
+
effort: defaultSelection.effort || "",
|
|
262
|
+
error: "",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
var availability = getVendorAvailability(ws);
|
|
199
266
|
var candidates = [];
|
|
200
267
|
var preferredVendor = sm.defaultVendor || "";
|
|
201
268
|
if (preferredVendor) candidates.push(preferredVendor);
|
|
@@ -341,5 +408,6 @@ module.exports = {
|
|
|
341
408
|
modelEntryMatches: modelEntryMatches,
|
|
342
409
|
modelEntryValue: modelEntryValue,
|
|
343
410
|
normalizeCatalogModels: normalizeCatalogModels,
|
|
411
|
+
modelsWithEffortMetadata: modelsWithEffortMetadata,
|
|
344
412
|
selectCatalogModel: selectCatalogModel,
|
|
345
413
|
};
|