clay-server 2.47.0-beta.2 → 2.47.0-beta.4
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/claude-hook-installer.js +0 -1
- package/lib/codex-defaults.js +12 -2
- package/lib/project-connection.js +2 -1
- package/lib/project-notifications.js +3 -1
- package/lib/project-session-spawn.js +366 -0
- package/lib/project-sessions.js +17 -9
- package/lib/project.js +47 -29
- package/lib/public/app.js +2 -2
- package/lib/public/css/mates.css +0 -196
- package/lib/public/modules/app-messages.js +3 -23
- package/lib/public/modules/app-notifications.js +12 -4
- package/lib/public/modules/app-panels.js +6 -3
- package/lib/public/modules/app-rate-limit.js +41 -13
- package/lib/public/modules/app-rendering.js +2 -0
- package/lib/public/modules/input.js +4 -5
- package/lib/public/modules/mate-sidebar.js +0 -3
- package/lib/public/modules/tui-grab.js +12 -3
- package/lib/sdk-bridge.js +55 -36
- package/lib/sdk-message-processor.js +4 -6
- package/lib/session-spawn-mcp-server.js +56 -0
- package/lib/sessions.js +3 -0
- package/lib/ws-schema.js +0 -14
- package/lib/yoke/adapters/claude.js +25 -0
- package/lib/yoke/adapters/codex.js +34 -21
- package/lib/yoke/adapters/kiro.js +33 -19
- package/lib/yoke/index.js +18 -6
- package/lib/yoke/vendor-registry.js +55 -0
- package/package.json +3 -3
- package/lib/mate-datastore.js +0 -359
- package/lib/project-mate-datastore.js +0 -232
- package/lib/public/modules/mate-datastore-ui.js +0 -280
|
@@ -43,7 +43,6 @@ var CLAY_MANAGED_ALLOW = [
|
|
|
43
43
|
"mcp__clay-browser__browser_watch_tab",
|
|
44
44
|
"mcp__clay-browser__browser_unwatch_tab",
|
|
45
45
|
"mcp__clay-debate__propose_debate",
|
|
46
|
-
"mcp__clay-datastore__*",
|
|
47
46
|
"mcp__clay-history__*",
|
|
48
47
|
// Email: read-side only. Send / reply / mark_read still prompt.
|
|
49
48
|
"mcp__clay-email__clay_read_email",
|
package/lib/codex-defaults.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
var CODEX_DEFAULTS = {
|
|
2
|
-
approval: "on-
|
|
2
|
+
approval: "on-request",
|
|
3
3
|
sandbox: "danger-full-access",
|
|
4
4
|
webSearch: "live",
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
+
var CODEX_APPROVAL_POLICIES = ["untrusted", "on-request", "granular", "never"];
|
|
8
|
+
|
|
9
|
+
function normalizeCodexApproval(value) {
|
|
10
|
+
if (value === "on-failure") return CODEX_DEFAULTS.approval;
|
|
11
|
+
if (CODEX_APPROVAL_POLICIES.indexOf(value) === -1) return CODEX_DEFAULTS.approval;
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
function getCodexConfig(sm) {
|
|
8
16
|
return {
|
|
9
|
-
approval: (sm && sm.codexApproval)
|
|
17
|
+
approval: normalizeCodexApproval(sm && sm.codexApproval),
|
|
10
18
|
sandbox: (sm && sm.codexSandbox) || CODEX_DEFAULTS.sandbox,
|
|
11
19
|
webSearch: (sm && sm.codexWebSearch) || CODEX_DEFAULTS.webSearch,
|
|
12
20
|
};
|
|
@@ -14,5 +22,7 @@ function getCodexConfig(sm) {
|
|
|
14
22
|
|
|
15
23
|
module.exports = {
|
|
16
24
|
CODEX_DEFAULTS: CODEX_DEFAULTS,
|
|
25
|
+
CODEX_APPROVAL_POLICIES: CODEX_APPROVAL_POLICIES,
|
|
26
|
+
normalizeCodexApproval: normalizeCodexApproval,
|
|
17
27
|
getCodexConfig: getCodexConfig,
|
|
18
28
|
};
|
|
@@ -4,6 +4,7 @@ var usersModule = require("./users");
|
|
|
4
4
|
var userPresence = require("./user-presence");
|
|
5
5
|
var emailAccounts = require("./email-accounts");
|
|
6
6
|
var { getCodexConfig } = require("./codex-defaults");
|
|
7
|
+
var yoke = require("./yoke");
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Attach connection/disconnection handlers to a project context.
|
|
@@ -132,7 +133,7 @@ function attachConnection(ctx) {
|
|
|
132
133
|
var restoredActive = restoredState.active;
|
|
133
134
|
var initialVendor = (restoredActive && restoredActive.vendor) || sm.defaultVendor || "claude";
|
|
134
135
|
var initialModels = (sm.modelsByVendor && sm.modelsByVendor[initialVendor]) || sm.availableModels || [];
|
|
135
|
-
sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked });
|
|
136
|
+
sendTo(ws, { type: "info", cwd: cwd, slug: slug, project: title || project, version: currentVersion, debug: !!debug, dangerouslySkipPermissions: dangerouslySkipPermissions, osUsers: osUsers, lanHost: lanHost, projectCount: _filteredProjects.length, projects: _filteredProjects, projectOwnerId: projectOwnerId, ownerLocked: ownerLocked, vendors: yoke.VENDOR_REGISTRY });
|
|
136
137
|
// Update notifications are pushed on a scheduled interval (see
|
|
137
138
|
// scheduleUpdateBroadcast). We no longer push on connect to avoid
|
|
138
139
|
// re-triggering the banner on every page refresh.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
var fs = require("fs");
|
|
6
6
|
var path = require("path");
|
|
7
7
|
var config = require("./config");
|
|
8
|
+
var yoke = require("./yoke");
|
|
8
9
|
|
|
9
10
|
var NOTIF_FILE = path.join(config.CONFIG_DIR, "notifications.json");
|
|
10
11
|
var REMINDER_INTERVAL = 60 * 60 * 1000; // 1 hour
|
|
@@ -21,7 +22,8 @@ function generateId() {
|
|
|
21
22
|
var formatters = {
|
|
22
23
|
auth_required: function (data) {
|
|
23
24
|
var vendor = data.vendor || "claude";
|
|
24
|
-
var
|
|
25
|
+
var vendorInfo = yoke.getVendorInfo(vendor);
|
|
26
|
+
var title = data.title || (((vendorInfo && vendorInfo.displayName) || "Claude Code") + " is not logged in");
|
|
25
27
|
return {
|
|
26
28
|
type: "auth_required",
|
|
27
29
|
title: title,
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
var yoke = require("./yoke");
|
|
2
|
+
var sessionSpawnMcp = require("./session-spawn-mcp-server");
|
|
3
|
+
var cliSessions = require("./cli-sessions");
|
|
4
|
+
var os = require("os");
|
|
5
|
+
var osUsers = require("./os-users");
|
|
6
|
+
|
|
7
|
+
var MAX_SESSIONS_PER_CALL = 10;
|
|
8
|
+
var MAX_CHILDREN_PER_PARENT = 20;
|
|
9
|
+
var SPAWN_CONCURRENCY = 3;
|
|
10
|
+
|
|
11
|
+
function parseBatch(raw) {
|
|
12
|
+
var parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(raw);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
throw new Error("sessions must be a valid JSON array");
|
|
17
|
+
}
|
|
18
|
+
if (!Array.isArray(parsed)) {
|
|
19
|
+
throw new Error("sessions must be a valid JSON array");
|
|
20
|
+
}
|
|
21
|
+
if (parsed.length < 1 || parsed.length > MAX_SESSIONS_PER_CALL) {
|
|
22
|
+
throw new Error("sessions must contain between 1 and 10 entries");
|
|
23
|
+
}
|
|
24
|
+
var result = [];
|
|
25
|
+
for (var i = 0; i < parsed.length; i++) {
|
|
26
|
+
var entry = parsed[i];
|
|
27
|
+
if (!entry || typeof entry.prompt !== "string" || !entry.prompt.trim()) {
|
|
28
|
+
throw new Error("session " + (i + 1) + " must include a non-empty prompt");
|
|
29
|
+
}
|
|
30
|
+
var title = typeof entry.title === "string" ? entry.title.trim() : "";
|
|
31
|
+
result.push({
|
|
32
|
+
title: title || "Spawned task " + (i + 1),
|
|
33
|
+
prompt: entry.prompt.trim(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getSessionsArray(sessions) {
|
|
40
|
+
if (!sessions) return [];
|
|
41
|
+
if (typeof sessions.values === "function") return Array.from(sessions.values());
|
|
42
|
+
return sessions;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assertSpawnAllowed(parent, sessions, requestedCount) {
|
|
46
|
+
if (!parent) throw new Error("no active parent session");
|
|
47
|
+
if (parent.spawn) throw new Error("spawned sessions cannot spawn further sessions");
|
|
48
|
+
var all = getSessionsArray(sessions);
|
|
49
|
+
var childCount = 0;
|
|
50
|
+
for (var i = 0; i < all.length; i++) {
|
|
51
|
+
if (all[i].spawn && all[i].spawn.parentId === parent.localId) childCount++;
|
|
52
|
+
}
|
|
53
|
+
if (childCount + requestedCount > MAX_CHILDREN_PER_PARENT) {
|
|
54
|
+
throw new Error("a parent session cannot have more than 20 children");
|
|
55
|
+
}
|
|
56
|
+
return childCount;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateVendor(vendor, adapters, linuxUser, getVendorInfo) {
|
|
60
|
+
if (!vendor || !adapters || !adapters[vendor]) {
|
|
61
|
+
throw new Error("vendor is not available: " + (vendor || "unknown"));
|
|
62
|
+
}
|
|
63
|
+
var lookup = getVendorInfo || yoke.getVendorInfo;
|
|
64
|
+
var info = lookup(vendor);
|
|
65
|
+
if (linuxUser && info && info.osUserIsolation === false) {
|
|
66
|
+
throw new Error((info.displayName || vendor) + " is not available for OS-isolated users");
|
|
67
|
+
}
|
|
68
|
+
return vendor;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createSpawnQueue(concurrency) {
|
|
72
|
+
var limit = concurrency || SPAWN_CONCURRENCY;
|
|
73
|
+
var pending = [];
|
|
74
|
+
var running = [];
|
|
75
|
+
|
|
76
|
+
function startTask(task) {
|
|
77
|
+
task.state = "running";
|
|
78
|
+
running.push(task);
|
|
79
|
+
var finished = false;
|
|
80
|
+
function complete(err) {
|
|
81
|
+
if (finished) return;
|
|
82
|
+
finished = true;
|
|
83
|
+
var index = running.indexOf(task);
|
|
84
|
+
if (index !== -1) running.splice(index, 1);
|
|
85
|
+
task.state = err ? "error" : "done";
|
|
86
|
+
task.error = err || null;
|
|
87
|
+
pump();
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
task.start(complete);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
complete(e);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pump() {
|
|
97
|
+
while (running.length < limit && pending.length > 0) {
|
|
98
|
+
var task = pending.shift();
|
|
99
|
+
startTask(task);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function add(tasks) {
|
|
104
|
+
for (var i = 0; i < tasks.length; i++) {
|
|
105
|
+
tasks[i].state = "queued";
|
|
106
|
+
pending.push(tasks[i]);
|
|
107
|
+
}
|
|
108
|
+
pump();
|
|
109
|
+
var queued = 0;
|
|
110
|
+
var active = 0;
|
|
111
|
+
for (var j = 0; j < tasks.length; j++) {
|
|
112
|
+
if (tasks[j].state === "queued") queued++;
|
|
113
|
+
if (tasks[j].state === "running") active++;
|
|
114
|
+
}
|
|
115
|
+
return { queued: queued, running: active };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
add: add,
|
|
120
|
+
get pendingCount() { return pending.length; },
|
|
121
|
+
get runningCount() { return running.length; },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hasSessionError(session) {
|
|
126
|
+
var history = session.history || [];
|
|
127
|
+
var recent = history.slice(-3);
|
|
128
|
+
for (var i = 0; i < recent.length; i++) {
|
|
129
|
+
if (recent[i].type === "error" || (recent[i].type === "done" && recent[i].code === 1)) return true;
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toolResult(value) {
|
|
135
|
+
return Promise.resolve({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toolError(err) {
|
|
139
|
+
return Promise.resolve({
|
|
140
|
+
content: [{ type: "text", text: "Error: " + (err.message || err) }],
|
|
141
|
+
isError: true,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function rebuildMessageUUIDs(history) {
|
|
146
|
+
var messageUUIDs = [];
|
|
147
|
+
for (var i = 0; i < history.length; i++) {
|
|
148
|
+
if (history[i].type === "message_uuid") {
|
|
149
|
+
messageUUIDs.push({
|
|
150
|
+
uuid: history[i].uuid,
|
|
151
|
+
type: history[i].messageType,
|
|
152
|
+
historyIndex: i,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return messageUUIDs;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function attachSessionSpawn(ctx) {
|
|
160
|
+
var sm = ctx.sm;
|
|
161
|
+
var queue = createSpawnQueue(SPAWN_CONCURRENCY);
|
|
162
|
+
var adapters = ctx.adapters || {};
|
|
163
|
+
var readCliSessionHistory = ctx.readCliSessionHistory || cliSessions.readCliSessionHistory;
|
|
164
|
+
|
|
165
|
+
function resolveSessionHome(session) {
|
|
166
|
+
var linuxUser = ctx.getLinuxUserForSession(session);
|
|
167
|
+
if (linuxUser) {
|
|
168
|
+
try {
|
|
169
|
+
var info = osUsers.resolveOsUserInfo(linuxUser);
|
|
170
|
+
if (info && info.home) return info.home;
|
|
171
|
+
} catch (e) {}
|
|
172
|
+
}
|
|
173
|
+
return os.homedir();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function validateFork(parent, vendor, explicitVendor) {
|
|
177
|
+
if (!parent.cliSessionId) {
|
|
178
|
+
throw new Error("forkFromCurrent requires the calling session to have at least one completed turn");
|
|
179
|
+
}
|
|
180
|
+
var capabilities = sm.capabilitiesByVendor && sm.capabilitiesByVendor[vendor];
|
|
181
|
+
if (!capabilities || capabilities.fork !== true) {
|
|
182
|
+
throw new Error("forkFromCurrent is not supported by vendor: " + vendor);
|
|
183
|
+
}
|
|
184
|
+
var parentVendor = parent.vendor || sm.defaultVendor;
|
|
185
|
+
if (explicitVendor && explicitVendor !== parentVendor) {
|
|
186
|
+
throw new Error("forkFromCurrent children must use the parent's vendor");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function applyForkToTask(task, forkResult, history) {
|
|
191
|
+
task.session.cliSessionId = forkResult.sessionId;
|
|
192
|
+
task.session.history = history.slice();
|
|
193
|
+
task.session.messageUUIDs = rebuildMessageUUIDs(task.session.history);
|
|
194
|
+
sm.saveSessionFile(task.session);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function spawnOne(parent, spec, index, batchId, vendor, linuxUser) {
|
|
198
|
+
var create = typeof sm.createSessionRaw === "function" ? sm.createSessionRaw : sm.createSession;
|
|
199
|
+
var session = create.call(sm, {
|
|
200
|
+
ownerId: parent.ownerId || null,
|
|
201
|
+
sessionVisibility: parent.sessionVisibility || "shared",
|
|
202
|
+
vendor: vendor,
|
|
203
|
+
});
|
|
204
|
+
session.spawn = { parentId: parent.localId, index: index, batchId: batchId };
|
|
205
|
+
session.title = spec.title;
|
|
206
|
+
sm.saveSessionFile(session);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
session: session,
|
|
210
|
+
state: "created",
|
|
211
|
+
start: function (done) {
|
|
212
|
+
function finish(err) {
|
|
213
|
+
delete session.onQueryComplete;
|
|
214
|
+
delete session.singleTurn;
|
|
215
|
+
done(err);
|
|
216
|
+
}
|
|
217
|
+
var userMessage = { type: "user_message", text: spec.prompt };
|
|
218
|
+
session.history.push(userMessage);
|
|
219
|
+
sm.appendToSessionFile(session, userMessage);
|
|
220
|
+
session.isProcessing = true;
|
|
221
|
+
session.lastActivity = Date.now();
|
|
222
|
+
session.sentToolResults = {};
|
|
223
|
+
session.singleTurn = true;
|
|
224
|
+
session.onQueryComplete = function () {
|
|
225
|
+
finish(hasSessionError(session) ? new Error("child session failed") : null);
|
|
226
|
+
};
|
|
227
|
+
var sdk = ctx.getSdk();
|
|
228
|
+
if (!sdk || typeof sdk.startQuery !== "function") {
|
|
229
|
+
session.isProcessing = false;
|
|
230
|
+
finish(new Error("SDK bridge is not ready"));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
Promise.resolve(sdk.startQuery(session, spec.prompt, undefined, linuxUser)).then(function () {
|
|
234
|
+
if (!session.isProcessing && !session.queryInstance) {
|
|
235
|
+
finish(new Error("child session failed to start"));
|
|
236
|
+
}
|
|
237
|
+
}).catch(function (err) {
|
|
238
|
+
session.isProcessing = false;
|
|
239
|
+
session.history.push({ type: "error", text: err.message || String(err) });
|
|
240
|
+
finish(err);
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function spawn(args, caller) {
|
|
247
|
+
try {
|
|
248
|
+
// The caller is bound per query in getLocalMcpServers (project.js), so
|
|
249
|
+
// a child session resolving itself here is what makes the depth guard
|
|
250
|
+
// sound. Never fall back to sm.getActiveSession(): that is the
|
|
251
|
+
// project-global "last viewed" session and can belong to someone else.
|
|
252
|
+
var parent = caller;
|
|
253
|
+
if (!parent) throw new Error("spawn_sessions requires a session-bound tool server");
|
|
254
|
+
var specs = parseBatch(args.sessions);
|
|
255
|
+
assertSpawnAllowed(parent, sm.sessions, specs.length);
|
|
256
|
+
var explicitVendor = args.vendor && args.vendor.trim();
|
|
257
|
+
var vendor = explicitVendor || parent.vendor || sm.defaultVendor;
|
|
258
|
+
var linuxUser = ctx.getLinuxUserForSession(parent);
|
|
259
|
+
validateVendor(vendor, adapters, linuxUser);
|
|
260
|
+
var forkFromCurrent = args.forkFromCurrent === true;
|
|
261
|
+
if (forkFromCurrent) validateFork(parent, vendor, explicitVendor);
|
|
262
|
+
var batchId = "sp_" + Date.now().toString(36);
|
|
263
|
+
var tasks = [];
|
|
264
|
+
var spawned = [];
|
|
265
|
+
var failed = null;
|
|
266
|
+
var sdk = ctx.getSdk();
|
|
267
|
+
var lastMessage = parent.messageUUIDs && parent.messageUUIDs.length > 0
|
|
268
|
+
? parent.messageUUIDs[parent.messageUUIDs.length - 1]
|
|
269
|
+
: null;
|
|
270
|
+
// Claude treats an omitted upToMessageId as a full-session fork;
|
|
271
|
+
// Codex forks the whole thread and ignores the UUID option.
|
|
272
|
+
var lastUuid = lastMessage ? lastMessage.uuid : undefined;
|
|
273
|
+
for (var i = 0; i < specs.length; i++) {
|
|
274
|
+
var forkResult = null;
|
|
275
|
+
var forkHistory = null;
|
|
276
|
+
if (forkFromCurrent) {
|
|
277
|
+
try {
|
|
278
|
+
if (!sdk || typeof sdk.forkSession !== "function") throw new Error("SDK bridge is not ready");
|
|
279
|
+
forkResult = await sdk.forkSession(parent, lastUuid);
|
|
280
|
+
if (!forkResult || !forkResult.sessionId) throw new Error("Fork returned no session id");
|
|
281
|
+
if (forkResult.useLocalHistory) {
|
|
282
|
+
forkHistory = parent.history.slice();
|
|
283
|
+
} else {
|
|
284
|
+
forkHistory = await readCliSessionHistory(
|
|
285
|
+
resolveSessionHome(parent), ctx.cwd, forkResult.sessionId
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
} catch (e) {
|
|
289
|
+
failed = { index: i, error: e.message || String(e) };
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
var task = spawnOne(parent, specs[i], i, batchId, vendor, linuxUser);
|
|
294
|
+
if (forkResult) applyForkToTask(task, forkResult, forkHistory || []);
|
|
295
|
+
tasks.push(task);
|
|
296
|
+
spawned.push({ localId: task.session.localId, title: task.session.title });
|
|
297
|
+
}
|
|
298
|
+
var counts = queue.add(tasks);
|
|
299
|
+
if (tasks.length > 0) sm.broadcastSessionList();
|
|
300
|
+
var result = { spawned: spawned, queued: counts.queued, running: counts.running };
|
|
301
|
+
if (failed) result.failed = failed;
|
|
302
|
+
return toolResult(result);
|
|
303
|
+
} catch (e) {
|
|
304
|
+
return toolError(e);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function check(args, caller) {
|
|
309
|
+
try {
|
|
310
|
+
var parent = caller;
|
|
311
|
+
if (!parent) throw new Error("check_spawned_sessions requires a session-bound tool server");
|
|
312
|
+
var parentOnly = args.parentOnly !== false;
|
|
313
|
+
var all = getSessionsArray(sm.sessions);
|
|
314
|
+
var statuses = [];
|
|
315
|
+
for (var i = 0; i < all.length; i++) {
|
|
316
|
+
var session = all[i];
|
|
317
|
+
if (!session.spawn) continue;
|
|
318
|
+
if (parentOnly && session.spawn.parentId !== parent.localId) continue;
|
|
319
|
+
statuses.push({
|
|
320
|
+
localId: session.localId,
|
|
321
|
+
title: session.title || "New Session",
|
|
322
|
+
status: session.isProcessing ? "running" : (hasSessionError(session) ? "error" : "done"),
|
|
323
|
+
turnCount: session.turnCount || 0,
|
|
324
|
+
lastActivity: session.lastActivity || session.createdAt || 0,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return toolResult(statuses);
|
|
328
|
+
} catch (e) {
|
|
329
|
+
return toolError(e);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// boundSession: the session whose query this tool server instance is
|
|
334
|
+
// mounted into. Omitted for the static instance kept in the project's
|
|
335
|
+
// mcpServers map, which exists only so tool descriptors can be listed;
|
|
336
|
+
// its handlers fail closed if anything ever routes a call to them.
|
|
337
|
+
function createMcpServer(adapter, boundSession) {
|
|
338
|
+
if (ctx.isMate || !adapter || typeof adapter.createToolServer !== "function") return null;
|
|
339
|
+
return adapter.createToolServer({
|
|
340
|
+
name: "clay-sessions",
|
|
341
|
+
version: "1.0.0",
|
|
342
|
+
tools: sessionSpawnMcp.getToolDefs({
|
|
343
|
+
spawn: function (args) { return spawn(args, boundSession || null); },
|
|
344
|
+
check: function (args) { return check(args, boundSession || null); },
|
|
345
|
+
}),
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
createMcpServer: createMcpServer,
|
|
351
|
+
spawnOne: spawnOne,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
module.exports = {
|
|
356
|
+
MAX_SESSIONS_PER_CALL: MAX_SESSIONS_PER_CALL,
|
|
357
|
+
MAX_CHILDREN_PER_PARENT: MAX_CHILDREN_PER_PARENT,
|
|
358
|
+
SPAWN_CONCURRENCY: SPAWN_CONCURRENCY,
|
|
359
|
+
parseBatch: parseBatch,
|
|
360
|
+
assertSpawnAllowed: assertSpawnAllowed,
|
|
361
|
+
validateVendor: validateVendor,
|
|
362
|
+
createSpawnQueue: createSpawnQueue,
|
|
363
|
+
hasSessionError: hasSessionError,
|
|
364
|
+
rebuildMessageUUIDs: rebuildMessageUUIDs,
|
|
365
|
+
attachSessionSpawn: attachSessionSpawn,
|
|
366
|
+
};
|
package/lib/project-sessions.js
CHANGED
|
@@ -3,6 +3,12 @@ var path = require("path");
|
|
|
3
3
|
var crypto = require("crypto");
|
|
4
4
|
var { execFileSync } = require("child_process");
|
|
5
5
|
var { CODEX_DEFAULTS, getCodexConfig } = require("./codex-defaults");
|
|
6
|
+
var yoke = require("./yoke");
|
|
7
|
+
|
|
8
|
+
function vendorSupportsTui(vendor) {
|
|
9
|
+
var info = yoke.getVendorInfo(vendor);
|
|
10
|
+
return !!(info && info.sessionModes.indexOf("tui") !== -1);
|
|
11
|
+
}
|
|
6
12
|
|
|
7
13
|
// Format a user's answer to an ask_user_questions card as a plain user
|
|
8
14
|
// message so the MCP path can feed it back to the agent on the next turn.
|
|
@@ -274,6 +280,8 @@ function attachSessions(ctx) {
|
|
|
274
280
|
var sid = session.cliSessionId;
|
|
275
281
|
var localId = session.localId;
|
|
276
282
|
var resumeSkip = session.dangerouslySkipPermissions ? " --dangerously-skip-permissions" : "";
|
|
283
|
+
// Command construction is Claude-specific. Generalize this before any
|
|
284
|
+
// other vendor declares "tui" in the YOKE registry.
|
|
277
285
|
var cmd = "claude --resume " + sid + resumeSkip + "; exit\n";
|
|
278
286
|
var term = tm.create(80, 24, getOsUserInfoForWs(ws), ws, {
|
|
279
287
|
initialInput: cmd,
|
|
@@ -346,7 +354,7 @@ function attachSessions(ctx) {
|
|
|
346
354
|
// born-TUI session shows the same read-only + Resume view as a fresh click.
|
|
347
355
|
function resolveSessionForView(session, ws) {
|
|
348
356
|
if (!session) return;
|
|
349
|
-
if (session.vendor && session.vendor
|
|
357
|
+
if (session.vendor && !vendorSupportsTui(session.vendor)) { session.tuiSuspended = false; return; }
|
|
350
358
|
var pref = getClaudeOpenModeForWs(ws);
|
|
351
359
|
// A LIVE runtime always wins over the viewer's claudeOpenMode pref:
|
|
352
360
|
// another user (or this user in another tab) may be in the session right
|
|
@@ -424,13 +432,13 @@ function attachSessions(ctx) {
|
|
|
424
432
|
if (ws._clayUser && usersModule.isMultiUser()) sessionOpts.ownerId = ws._clayUser.id;
|
|
425
433
|
if (msg.sessionVisibility) sessionOpts.sessionVisibility = msg.sessionVisibility;
|
|
426
434
|
if (msg.vendor) sessionOpts.vendor = msg.vendor;
|
|
427
|
-
// Mode resolution:
|
|
428
|
-
//
|
|
435
|
+
// Mode resolution: vendors without a TUI session mode are always GUI.
|
|
436
|
+
// TUI-capable sessions honor the explicit msg.mode if provided, otherwise
|
|
429
437
|
// fall back to the user's claudeOpenMode preference. This is what
|
|
430
438
|
// makes the sidebar's "Claude" icon button create the right kind of
|
|
431
439
|
// session without the client needing to know the preference.
|
|
432
440
|
var requestedMode;
|
|
433
|
-
if (msg.vendor
|
|
441
|
+
if (msg.vendor && !vendorSupportsTui(msg.vendor)) {
|
|
434
442
|
requestedMode = "gui";
|
|
435
443
|
} else if (msg.mode === "tui" || msg.mode === "gui") {
|
|
436
444
|
requestedMode = msg.mode;
|
|
@@ -625,7 +633,7 @@ function attachSessions(ctx) {
|
|
|
625
633
|
// for every viewer; only cold sessions follow the clicker's
|
|
626
634
|
// claudeOpenMode pref. Nothing is spawned here.
|
|
627
635
|
var xmTarget = sm.sessions.get(msg.id);
|
|
628
|
-
if (xmTarget && (xmTarget.vendor
|
|
636
|
+
if (xmTarget && (!xmTarget.vendor || vendorSupportsTui(xmTarget.vendor))) {
|
|
629
637
|
// Single source of truth: live runtime wins (tui stays tui, gui stays
|
|
630
638
|
// gui); only cold sessions follow the viewer's pref. No PTY is spawned
|
|
631
639
|
// on switch - born-TUI resumes lazily via the Resume bar
|
|
@@ -678,7 +686,7 @@ function attachSessions(ctx) {
|
|
|
678
686
|
if (msg.type === "resume_tui_session") {
|
|
679
687
|
if (msg.id && sm.sessions.has(msg.id)) {
|
|
680
688
|
var rtTarget = sm.sessions.get(msg.id);
|
|
681
|
-
var rtOk = rtTarget && (rtTarget.vendor
|
|
689
|
+
var rtOk = rtTarget && (!rtTarget.vendor || vendorSupportsTui(rtTarget.vendor)) &&
|
|
682
690
|
rtTarget.cliSessionId && tm;
|
|
683
691
|
if (rtOk) {
|
|
684
692
|
if (usersModule.isMultiUser() && ws._clayUser &&
|
|
@@ -706,7 +714,7 @@ function attachSessions(ctx) {
|
|
|
706
714
|
if (msg.type === "suspend_tui_session") {
|
|
707
715
|
if (msg.id && sm.sessions.has(msg.id)) {
|
|
708
716
|
var stTarget = sm.sessions.get(msg.id);
|
|
709
|
-
var stOk = stTarget && (stTarget.vendor
|
|
717
|
+
var stOk = stTarget && (!stTarget.vendor || vendorSupportsTui(stTarget.vendor));
|
|
710
718
|
if (stOk && (!usersModule.isMultiUser() || !ws._clayUser ||
|
|
711
719
|
usersModule.canAccessSession(ws._clayUser.id, stTarget, { visibility: "public" }))) {
|
|
712
720
|
if (tm) {
|
|
@@ -737,7 +745,7 @@ function attachSessions(ctx) {
|
|
|
737
745
|
var tprId = msg.id;
|
|
738
746
|
var tprSess = (tprId && sm.sessions.has(tprId)) ? sm.sessions.get(tprId) : null;
|
|
739
747
|
if (!tprSess || !tprSess.cliSessionId || tprSess.mode !== "tui") return true;
|
|
740
|
-
if (tprSess.vendor && tprSess.vendor
|
|
748
|
+
if (tprSess.vendor && !vendorSupportsTui(tprSess.vendor)) return true;
|
|
741
749
|
if (usersModule.isMultiUser() && ws._clayUser
|
|
742
750
|
&& !usersModule.canAccessSession(ws._clayUser.id, tprSess, { visibility: "public" })) {
|
|
743
751
|
return true;
|
|
@@ -1084,7 +1092,7 @@ function attachSessions(ctx) {
|
|
|
1084
1092
|
|
|
1085
1093
|
// Codex-specific settings (stored on sessionManager, passed to adapter via adapterOptions)
|
|
1086
1094
|
if (msg.type === "set_codex_approval") {
|
|
1087
|
-
sm.codexApproval = msg.approval
|
|
1095
|
+
sm.codexApproval = getCodexConfig({ codexApproval: msg.approval }).approval;
|
|
1088
1096
|
send(Object.assign({ type: "codex_config" }, getCodexConfig(sm)));
|
|
1089
1097
|
return true;
|
|
1090
1098
|
}
|