clay-server 3.8.1 → 4.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ask-user-mcp-server.js +5 -119
- package/lib/background-task-timing.js +105 -0
- package/lib/builtin-mates.js +6 -6
- package/lib/claude-hook-installer.js +15 -0
- package/lib/clay-history-mcp-server.js +62 -297
- package/lib/daemon.js +23 -1
- package/lib/debate-model-selection.js +119 -0
- package/lib/home-debate-tool-policy.js +164 -0
- package/lib/home-response-notifications.js +26 -0
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +224 -0
- package/lib/mate-creation-mcp-server.js +55 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/mate-ready-creation.js +95 -0
- package/lib/mates.js +2 -0
- package/lib/notes.js +2 -1
- package/lib/project-ask-user.js +64 -0
- package/lib/project-capsule-catalog.js +101 -0
- package/lib/project-connection.js +17 -0
- package/lib/project-debate-proposal.js +133 -17
- package/lib/project-debate.js +214 -110
- package/lib/project-delegated-follow-up.js +84 -0
- package/lib/project-delegated-session.js +53 -0
- package/lib/project-filesystem.js +7 -9
- package/lib/project-knowledge.js +23 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-mcp-server.js +290 -0
- package/lib/project-logs-query.js +167 -0
- package/lib/project-logs-root.js +67 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +357 -0
- package/lib/project-logs-snapshot.js +202 -0
- package/lib/project-logs-store.js +498 -0
- package/lib/project-logs-versioning.js +128 -0
- package/lib/project-logs.js +347 -0
- package/lib/project-mate-creation-proposal.js +120 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +36 -7
- package/lib/project-models.js +157 -32
- package/lib/project-session-notes.js +2 -2
- package/lib/project-session-pair.js +8 -8
- package/lib/project-sessions.js +66 -51
- package/lib/project-vendor-login.js +414 -0
- package/lib/project-worker-proposal.js +25 -25
- package/lib/project-workspace-query.js +144 -0
- package/lib/project.js +433 -102
- package/lib/public/app.js +109 -48
- package/lib/public/css/avatar-imprints.css +0 -1
- package/lib/public/css/base.css +4 -0
- package/lib/public/css/capsule-ui.css +389 -0
- package/lib/public/css/command-palette.css +306 -6
- package/lib/public/css/home-capsule-library.css +243 -0
- package/lib/public/css/home-capsule-source.css +192 -0
- package/lib/public/css/home-debate-live.css +146 -0
- package/lib/public/css/home-debate-planning.css +125 -0
- package/lib/public/css/home-debates-archive.css +281 -0
- package/lib/public/css/home-hub.css +896 -405
- package/lib/public/css/home-mate-creation.css +34 -0
- package/lib/public/css/home-mate-model-picker.css +131 -0
- package/lib/public/css/home-mate-settings.css +344 -0
- package/lib/public/css/home-session-actions.css +265 -0
- package/lib/public/css/home-sidebar.css +667 -0
- package/lib/public/css/icon-strip.css +1 -2
- package/lib/public/css/input.css +79 -25
- package/lib/public/css/mates.css +7 -34
- package/lib/public/css/pane.css +1 -1
- package/lib/public/css/playbook.css +0 -80
- package/lib/public/css/project-logs.css +234 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/sticky-notes.css +53 -117
- package/lib/public/css/tui-attention.css +0 -44
- package/lib/public/css/workspace-assignment.css +45 -0
- package/lib/public/fonts/source-serif-4/LICENSE.md +93 -0
- package/lib/public/fonts/source-serif-4/SourceSerif4Caption-Semibold.ttf.woff2 +0 -0
- package/lib/public/index.html +96 -74
- package/lib/public/modules/app-connection.js +100 -5
- package/lib/public/modules/app-dm.js +43 -160
- package/lib/public/modules/app-home-hub.js +328 -585
- package/lib/public/modules/app-message-cards.js +242 -0
- package/lib/public/modules/app-message-router.js +84 -0
- package/lib/public/modules/app-messages.js +124 -38
- package/lib/public/modules/app-notifications.js +8 -119
- package/lib/public/modules/app-panels.js +43 -2
- package/lib/public/modules/app-projects.js +9 -1
- package/lib/public/modules/app-rendering.js +21 -602
- package/lib/public/modules/avatar-imprint.js +11 -8
- package/lib/public/modules/background-tasks-ui.js +139 -16
- package/lib/public/modules/chat-bubble-renderer.js +152 -0
- package/lib/public/modules/chat-render-runtime.js +198 -0
- package/lib/public/modules/command-palette.js +225 -436
- package/lib/public/modules/dm-render.js +82 -0
- package/lib/public/modules/home-capsule-creation-intent.js +31 -0
- package/lib/public/modules/home-capsule-library.js +146 -0
- package/lib/public/modules/home-capsule-source.js +383 -0
- package/lib/public/modules/home-chat-identity.js +35 -0
- package/lib/public/modules/home-chat-scroll.js +77 -0
- package/lib/public/modules/home-chat-stream-state.js +34 -0
- package/lib/public/modules/home-composer-focus.js +25 -0
- package/lib/public/modules/home-conversations-sheet.js +179 -0
- package/lib/public/modules/home-debate-activity.js +48 -0
- package/lib/public/modules/home-debate-controls.js +295 -0
- package/lib/public/modules/home-debate-launch.js +41 -0
- package/lib/public/modules/home-debate-live.js +284 -0
- package/lib/public/modules/home-debate-models.js +73 -0
- package/lib/public/modules/home-debate-planning.js +335 -0
- package/lib/public/modules/home-debates-archive.js +279 -0
- package/lib/public/modules/home-dock-resize.js +74 -0
- package/lib/public/modules/home-dock.js +446 -0
- package/lib/public/modules/home-mate-chat.js +496 -0
- package/lib/public/modules/home-mate-creation.js +109 -0
- package/lib/public/modules/home-mate-model-picker.js +281 -0
- package/lib/public/modules/home-mate-selection.js +38 -0
- package/lib/public/modules/home-mate-settings-menu.js +131 -0
- package/lib/public/modules/home-mate-settings.js +383 -0
- package/lib/public/modules/home-session-actions.js +373 -0
- package/lib/public/modules/home-session-model-confirmation.js +13 -0
- package/lib/public/modules/home-shell.js +9 -0
- package/lib/public/modules/home-sidebar-chat-list.js +63 -0
- package/lib/public/modules/home-sidebar.js +165 -0
- package/lib/public/modules/home-sub-surface.js +19 -0
- package/lib/public/modules/home-surface-boot.js +68 -0
- package/lib/public/modules/home-surface.js +141 -0
- package/lib/public/modules/home-tools.js +271 -0
- package/lib/public/modules/markdown.js +50 -2
- package/lib/public/modules/mate-management.js +86 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/mate-wizard.js +0 -7
- package/lib/public/modules/mention.js +17 -19
- package/lib/public/modules/pane-bridge.js +9 -0
- package/lib/public/modules/profile.js +4 -5
- package/lib/public/modules/project-logs-ambient.js +238 -0
- package/lib/public/modules/project-logs-render.js +359 -0
- package/lib/public/modules/project-logs.js +328 -0
- package/lib/public/modules/project-settings.js +34 -28
- package/lib/public/modules/project-switcher.js +11 -5
- package/lib/public/modules/project-workspace-assignment.js +23 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/search-clay-chat.js +402 -0
- package/lib/public/modules/server-settings.js +14 -12
- package/lib/public/modules/session-actions.js +1 -1
- package/lib/public/modules/sidebar-mates.js +56 -552
- package/lib/public/modules/sidebar-mobile.js +12 -73
- package/lib/public/modules/sidebar-presence.js +37 -0
- package/lib/public/modules/sidebar-sessions.js +25 -1
- package/lib/public/modules/split-pair-ui.js +13 -13
- package/lib/public/modules/split-view.js +7 -0
- package/lib/public/modules/sticky-notes.js +106 -53
- package/lib/public/modules/tool-input-composition.js +48 -0
- package/lib/public/modules/tool-llm-status.js +167 -0
- package/lib/public/modules/tool-palette.js +76 -4
- package/lib/public/modules/tool-renderer-advanced.js +285 -0
- package/lib/public/modules/tool-renderer-chart.js +158 -0
- package/lib/public/modules/tool-renderer-semantics.js +35 -0
- package/lib/public/modules/tool-renderer.js +464 -0
- package/lib/public/modules/tool-runtime.js +309 -0
- package/lib/public/modules/tool-ui-evaluator.js +97 -0
- package/lib/public/modules/tool-ui-tree.js +21 -0
- package/lib/public/modules/tui-attention.js +10 -0
- package/lib/public/modules/user-settings.js +16 -23
- package/lib/public/modules/vendor-login.js +287 -0
- package/lib/public/modules/worker-proposal.js +2 -2
- package/lib/public/modules/workspace-assignment-card.js +149 -0
- package/lib/public/modules/workspace-assignment-routing.js +4 -0
- package/lib/public/style.css +14 -2
- package/lib/runtime-env.js +65 -0
- package/lib/sdk-bridge.js +120 -32
- package/lib/sdk-message-processor.js +34 -13
- package/lib/server-home-capsule-creation.js +36 -0
- package/lib/server-home-chat-events.js +287 -0
- package/lib/server-home-chat.js +518 -0
- package/lib/server-home-clay-entry.js +65 -0
- package/lib/server-home-clay-session-links.js +26 -0
- package/lib/server-home-debate-planning.js +214 -0
- package/lib/server-home-debates.js +137 -0
- package/lib/server-home-mate-creation.js +261 -0
- package/lib/server-home-models.js +241 -0
- package/lib/server-home-preferences.js +61 -0
- package/lib/server-mates.js +37 -0
- package/lib/server-tools.js +377 -0
- package/lib/server.js +166 -59
- package/lib/session-pair-mcp-server.js +6 -6
- package/lib/sessions.js +22 -4
- package/lib/tool-capsule-source.js +142 -0
- package/lib/tool-control-mcp-server.js +126 -0
- package/lib/tool-llm.js +48 -0
- package/lib/tool-storage.js +77 -0
- package/lib/tool-ui-spec-advanced.js +82 -0
- package/lib/tool-ui-spec.js +261 -0
- package/lib/tools-registry.js +286 -0
- package/lib/users-home-dock-preferences.js +77 -0
- package/lib/users-home-surface-preferences.js +99 -0
- package/lib/users-preferences.js +4 -7
- package/lib/users.js +12 -0
- package/lib/whats-new-content.js +3 -3
- package/lib/workspace-assignment-service.js +420 -0
- package/lib/workspace-query-mcp-server.js +154 -0
- package/lib/workspace-query-service.js +492 -0
- package/lib/ws-schema.js +107 -12
- package/lib/yoke/adapters/acp.js +2 -1
- package/lib/yoke/adapters/antigravity.js +2 -1
- package/lib/yoke/adapters/claude-worker.js +11 -4
- package/lib/yoke/adapters/claude.js +141 -46
- package/lib/yoke/adapters/codex.js +79 -11
- package/lib/yoke/adapters/kiro.js +2 -1
- package/lib/yoke/claude-user-input.js +38 -0
- package/lib/yoke/codex-app-server.js +20 -2
- package/lib/yoke/codex-background-tasks.js +8 -2
- package/lib/yoke/codex-user-input.js +64 -0
- package/lib/yoke/complete-once.js +164 -0
- package/lib/yoke/index.js +5 -1
- package/lib/yoke/interface.js +19 -0
- package/lib/yoke/mcp-bridge-server.js +2 -6
- package/lib/yoke/user-input.js +254 -0
- package/package.json +4 -3
- package/lib/public/css/home-chat.css +0 -380
- package/lib/public/modules/home-chat.js +0 -438
- package/lib/server-clay-home.js +0 -245
package/lib/project-sessions.js
CHANGED
|
@@ -75,6 +75,7 @@ function attachSessions(ctx) {
|
|
|
75
75
|
var sendToSessionOthers = ctx.sendToSessionOthers;
|
|
76
76
|
var opts = ctx.opts;
|
|
77
77
|
var usersModule = ctx.usersModule;
|
|
78
|
+
var getProjectAccess = ctx.getProjectAccess || function () { return { visibility: "public" }; };
|
|
78
79
|
var userPresence = ctx.userPresence;
|
|
79
80
|
var pushModule = ctx.pushModule;
|
|
80
81
|
var getSessionForWs = ctx.getSessionForWs;
|
|
@@ -85,6 +86,60 @@ function attachSessions(ctx) {
|
|
|
85
86
|
var onProcessingChanged = ctx.onProcessingChanged;
|
|
86
87
|
var broadcastPresence = ctx.broadcastPresence;
|
|
87
88
|
var adapter = ctx.adapter;
|
|
89
|
+
|
|
90
|
+
function ownsSearchSession(ws, session) {
|
|
91
|
+
if (!session) return false;
|
|
92
|
+
if (usersModule && typeof usersModule.isMultiUser === "function" && usersModule.isMultiUser()) {
|
|
93
|
+
return !!(ws && ws._clayUser && usersModule.canAccessSession(ws._clayUser.id, session, getProjectAccess()));
|
|
94
|
+
}
|
|
95
|
+
return !session.ownerId;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function respondToAskUser(session, msg) {
|
|
99
|
+
if (!session || !msg || !msg.toolId) return false;
|
|
100
|
+
var toolId = msg.toolId;
|
|
101
|
+
var answers = msg.answers || {};
|
|
102
|
+
var pending = session.pendingAskUser && session.pendingAskUser[toolId];
|
|
103
|
+
if (!pending) return false;
|
|
104
|
+
if (pending.mode === "yoke") {
|
|
105
|
+
try { yoke.userInput.normalizeAnswers(pending.request.questions, answers); }
|
|
106
|
+
catch (e) { return false; }
|
|
107
|
+
}
|
|
108
|
+
delete session.pendingAskUser[toolId];
|
|
109
|
+
sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId, answers: answers });
|
|
110
|
+
if (pending.mode === "yoke" && typeof pending.respond === "function") {
|
|
111
|
+
pending.respond(answers);
|
|
112
|
+
} else if (pending.mode === "mcp") {
|
|
113
|
+
var answerText = formatAskUserAnswerAsMessage(pending.input, answers);
|
|
114
|
+
var userMsg = { type: "user_message", text: answerText, askUserAnswer: true };
|
|
115
|
+
session.history.push(userMsg);
|
|
116
|
+
sm.appendToSessionFile(session, userMsg);
|
|
117
|
+
sendToSession(session.localId, userMsg);
|
|
118
|
+
if (!session.isProcessing) {
|
|
119
|
+
session.isProcessing = true;
|
|
120
|
+
onProcessingChanged();
|
|
121
|
+
session.sentToolResults = {};
|
|
122
|
+
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
123
|
+
if (!session.queryInstance && !session.worker) sdk.startQuery(session, answerText, undefined, ensureProjectAccessForSession(session));
|
|
124
|
+
else sdk.pushMessage(session, answerText);
|
|
125
|
+
} else {
|
|
126
|
+
sdk.pushMessage(session, answerText);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
pending.resolve({ behavior: "deny", message: formatAskUserAnswerAsMessage(pending.input, answers) });
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function cancelPendingAskUser(session, reason) {
|
|
135
|
+
var pendingMap = session && session.pendingAskUser ? session.pendingAskUser : {};
|
|
136
|
+
Object.keys(pendingMap).forEach(function (toolId) {
|
|
137
|
+
var pending = pendingMap[toolId];
|
|
138
|
+
if (pending && pending.respond && typeof pending.respond.cancel === "function") pending.respond.cancel(reason || "Cancelled");
|
|
139
|
+
else if (pending && typeof pending.resolve === "function") pending.resolve({ behavior: "deny", message: reason || "Cancelled" });
|
|
140
|
+
});
|
|
141
|
+
if (session) session.pendingAskUser = {};
|
|
142
|
+
}
|
|
88
143
|
var getProjectList = ctx.getProjectList;
|
|
89
144
|
var getProjectCount = ctx.getProjectCount;
|
|
90
145
|
var getScheduleCount = ctx.getScheduleCount;
|
|
@@ -821,14 +876,19 @@ function attachSessions(ctx) {
|
|
|
821
876
|
}
|
|
822
877
|
|
|
823
878
|
if (msg.type === "search_sessions") {
|
|
824
|
-
var results = sm.searchSessions(msg.query || "")
|
|
879
|
+
var results = sm.searchSessions(msg.query || "", function (session) {
|
|
880
|
+
return ownsSearchSession(ws, session);
|
|
881
|
+
});
|
|
825
882
|
sendTo(ws, { type: "search_results", query: msg.query || "", results: results });
|
|
826
883
|
return true;
|
|
827
884
|
}
|
|
828
885
|
|
|
829
886
|
if (msg.type === "search_session_content") {
|
|
830
887
|
var targetSession = msg.id ? sm.sessions.get(msg.id) : getSessionForWs(ws);
|
|
831
|
-
if (!targetSession)
|
|
888
|
+
if (!ownsSearchSession(ws, targetSession)) {
|
|
889
|
+
sendTo(ws, { type: "search_content_results", query: msg.query || "", sessionId: msg.id || null, hits: [], total: 0 });
|
|
890
|
+
return true;
|
|
891
|
+
}
|
|
832
892
|
var contentResults = sm.searchSessionContent(targetSession.localId, msg.query || "");
|
|
833
893
|
var searchResp = { type: "search_content_results", query: msg.query || "", sessionId: targetSession.localId, hits: contentResults.hits, total: contentResults.total };
|
|
834
894
|
if (msg.source) searchResp.source = msg.source;
|
|
@@ -1256,7 +1316,7 @@ function attachSessions(ctx) {
|
|
|
1256
1316
|
session.blocks = {};
|
|
1257
1317
|
session.sentToolResults = {};
|
|
1258
1318
|
session.pendingPermissions = {};
|
|
1259
|
-
session
|
|
1319
|
+
cancelPendingAskUser(session, "Conversation rewound");
|
|
1260
1320
|
session.isProcessing = false;
|
|
1261
1321
|
onProcessingChanged();
|
|
1262
1322
|
|
|
@@ -1328,53 +1388,7 @@ function attachSessions(ctx) {
|
|
|
1328
1388
|
|
|
1329
1389
|
if (msg.type === "ask_user_response") {
|
|
1330
1390
|
var session = getSessionForWs(ws);
|
|
1331
|
-
if (
|
|
1332
|
-
var toolId = msg.toolId;
|
|
1333
|
-
var answers = msg.answers || {};
|
|
1334
|
-
var pending = session.pendingAskUser[toolId];
|
|
1335
|
-
if (!pending) return true;
|
|
1336
|
-
delete session.pendingAskUser[toolId];
|
|
1337
|
-
sm.sendAndRecord(session, { type: "ask_user_answered", toolId: toolId, answers: answers });
|
|
1338
|
-
|
|
1339
|
-
if (pending.mode === "mcp") {
|
|
1340
|
-
// Stateless MCP path: the tool already returned. Inject the user's
|
|
1341
|
-
// answer as a new user message so the conversation continues
|
|
1342
|
-
// naturally on the next turn. This matches how the mate would see
|
|
1343
|
-
// any other user input.
|
|
1344
|
-
var answerText = formatAskUserAnswerAsMessage(pending.input, answers);
|
|
1345
|
-
var userMsg = { type: "user_message", text: answerText };
|
|
1346
|
-
session.history.push(userMsg);
|
|
1347
|
-
sm.appendToSessionFile(session, userMsg);
|
|
1348
|
-
sendToSession(session.localId, userMsg);
|
|
1349
|
-
|
|
1350
|
-
if (!session.isProcessing) {
|
|
1351
|
-
session.isProcessing = true;
|
|
1352
|
-
onProcessingChanged();
|
|
1353
|
-
session.sentToolResults = {};
|
|
1354
|
-
sendToSession(session.localId, { type: "status", status: "processing" });
|
|
1355
|
-
if (!session.queryInstance && !session.worker) {
|
|
1356
|
-
sdk.startQuery(session, answerText, undefined, ensureProjectAccessForSession(session));
|
|
1357
|
-
} else {
|
|
1358
|
-
sdk.pushMessage(session, answerText);
|
|
1359
|
-
}
|
|
1360
|
-
} else {
|
|
1361
|
-
// Turn is still running; queue for the next turn.
|
|
1362
|
-
sdk.pushMessage(session, answerText);
|
|
1363
|
-
}
|
|
1364
|
-
} else {
|
|
1365
|
-
// Claude native AskUserQuestion path (built-in tool, intercepted via
|
|
1366
|
-
// canUseTool). In headless SDK mode the built-in can't render its own
|
|
1367
|
-
// answer UI: resolving "allow" lets it return an EMPTY result, so the
|
|
1368
|
-
// model continues the same turn with no answer ("I don't see an
|
|
1369
|
-
// answer"). Injected user messages also lose the race against that
|
|
1370
|
-
// empty result. Instead, deliver the answer as the tool_result by
|
|
1371
|
-
// denying with the formatted answer text — the SDK surfaces a deny
|
|
1372
|
-
// `message` to the model as the tool result, so the model receives the
|
|
1373
|
-
// user's choice in the same turn. (updatedInput.answers is not read by
|
|
1374
|
-
// the 0.3.x built-in, which is what caused answers to be dropped.)
|
|
1375
|
-
var nativeAnswerText = formatAskUserAnswerAsMessage(pending.input, answers);
|
|
1376
|
-
pending.resolve({ behavior: "deny", message: nativeAnswerText });
|
|
1377
|
-
}
|
|
1391
|
+
if (session) respondToAskUser(session, msg);
|
|
1378
1392
|
return true;
|
|
1379
1393
|
}
|
|
1380
1394
|
|
|
@@ -1443,7 +1457,7 @@ function attachSessions(ctx) {
|
|
|
1443
1457
|
session.isProcessing = false;
|
|
1444
1458
|
onProcessingChanged();
|
|
1445
1459
|
session.pendingPermissions = {};
|
|
1446
|
-
session
|
|
1460
|
+
cancelPendingAskUser(session, "Conversation restarted");
|
|
1447
1461
|
sm.broadcastSessionList();
|
|
1448
1462
|
setImmediate(function () {
|
|
1449
1463
|
if (session.abortController) {
|
|
@@ -2011,6 +2025,7 @@ function attachSessions(ctx) {
|
|
|
2011
2025
|
return {
|
|
2012
2026
|
handleSessionsMessage: handleSessionsMessage,
|
|
2013
2027
|
resolveSessionForView: resolveSessionForView,
|
|
2028
|
+
respondToAskUser: respondToAskUser,
|
|
2014
2029
|
};
|
|
2015
2030
|
}
|
|
2016
2031
|
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
// Vendor login flow coordinator.
|
|
2
|
+
//
|
|
3
|
+
// Owns the "the vendor CLI is not logged in" recovery path end to end so the
|
|
4
|
+
// client never has to guess. One login terminal per vendor per project is the
|
|
5
|
+
// single source of truth: repeated auth_required events (other sessions, split
|
|
6
|
+
// panes, a burst of 401 retries) re-use that record instead of spawning more
|
|
7
|
+
// terminals.
|
|
8
|
+
//
|
|
9
|
+
// Lifecycle:
|
|
10
|
+
// vendor_login_start -> create (or re-use) a PTY running the vendor login
|
|
11
|
+
// command, reply with vendor_login_ready
|
|
12
|
+
// PTY output/exit -> detect a successful login, restart the vendor's
|
|
13
|
+
// YOKE adapter so it re-reads the credential file,
|
|
14
|
+
// broadcast auth_refreshed, close the terminal
|
|
15
|
+
// vendor_login_cancel -> user dismissed the flow; kill the terminal
|
|
16
|
+
//
|
|
17
|
+
// The adapter restart is the actual bug fix: a Codex app-server is a long-lived
|
|
18
|
+
// child process that reads ~/.codex/auth.json once at spawn, so without a
|
|
19
|
+
// restart every query after a successful login keeps hitting 401 and re-arms
|
|
20
|
+
// auth_required forever.
|
|
21
|
+
|
|
22
|
+
var vendorRegistry = require("./yoke/vendor-registry");
|
|
23
|
+
var yoke = require("./yoke");
|
|
24
|
+
|
|
25
|
+
// Login CLIs print a success line and then hand the shell back, so the PTY
|
|
26
|
+
// itself does not exit. Watch the output stream instead. Deliberately narrow:
|
|
27
|
+
// "Successfully logged out" and MCP-server logins must not match.
|
|
28
|
+
var LOGIN_SUCCESS_PATTERNS = [
|
|
29
|
+
/successfully logged in(?![ \t]+to[ \t]+mcp)/i,
|
|
30
|
+
/logged in successfully/i,
|
|
31
|
+
/login successful/i,
|
|
32
|
+
/successfully authenticated/i,
|
|
33
|
+
/authentication successful/i,
|
|
34
|
+
/signed in successfully/i,
|
|
35
|
+
/signed in with your [a-z ]+ account/i,
|
|
36
|
+
/you(?:'re| are) (?:now )?(?:logged|signed) in/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Rolling window kept per flow so a success line split across PTY chunks is
|
|
40
|
+
// still matched, without buffering the whole login transcript.
|
|
41
|
+
var OUTPUT_TAIL_MAX = 4096;
|
|
42
|
+
|
|
43
|
+
// Grace period between detecting success and killing the terminal, so the user
|
|
44
|
+
// actually sees the confirmation line before the modal closes.
|
|
45
|
+
var CLOSE_AFTER_SUCCESS_MS = 2000;
|
|
46
|
+
|
|
47
|
+
var ANSI_PATTERN = /\x1b\[[0-9;?]*[ -\/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\r/g;
|
|
48
|
+
|
|
49
|
+
function stripAnsi(text) {
|
|
50
|
+
return String(text).replace(ANSI_PATTERN, "");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function looksLikeLoginSuccess(text) {
|
|
54
|
+
for (var i = 0; i < LOGIN_SUCCESS_PATTERNS.length; i++) {
|
|
55
|
+
if (LOGIN_SUCCESS_PATTERNS[i].test(text)) return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* ctx fields:
|
|
62
|
+
* slug, osUsers,
|
|
63
|
+
* sm, tm, adapters,
|
|
64
|
+
* send, sendTo,
|
|
65
|
+
* usersModule,
|
|
66
|
+
* getOsUserInfoForWs, getOsUserInfoForLinuxUser, getLinuxUserForSession,
|
|
67
|
+
* refreshVendorAuthEverywhere
|
|
68
|
+
*/
|
|
69
|
+
function attachVendorLogin(ctx) {
|
|
70
|
+
var slug = ctx.slug;
|
|
71
|
+
var osUsers = ctx.osUsers;
|
|
72
|
+
|
|
73
|
+
var sm = ctx.sm;
|
|
74
|
+
var tm = ctx.tm;
|
|
75
|
+
var adapters = ctx.adapters || {};
|
|
76
|
+
|
|
77
|
+
var send = ctx.send;
|
|
78
|
+
var sendTo = ctx.sendTo;
|
|
79
|
+
|
|
80
|
+
var usersModule = ctx.usersModule;
|
|
81
|
+
|
|
82
|
+
var getOsUserInfoForWs = ctx.getOsUserInfoForWs;
|
|
83
|
+
var getOsUserInfoForLinuxUser = ctx.getOsUserInfoForLinuxUser;
|
|
84
|
+
var getLinuxUserForSession = ctx.getLinuxUserForSession;
|
|
85
|
+
var refreshVendorAuthEverywhere = ctx.refreshVendorAuthEverywhere;
|
|
86
|
+
|
|
87
|
+
// vendor -> flow record. At most one live login terminal per vendor.
|
|
88
|
+
var _flows = Object.create(null);
|
|
89
|
+
|
|
90
|
+
function loginCommandFor(vendor) {
|
|
91
|
+
var info = vendorRegistry.getVendorInfo(vendor);
|
|
92
|
+
return (info && info.loginCommand) || "claude login";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function vendorDisplayName(vendor) {
|
|
96
|
+
var info = vendorRegistry.getVendorInfo(vendor);
|
|
97
|
+
return (info && info.displayName) || vendor || "Vendor";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function listFlows() {
|
|
101
|
+
var result = [];
|
|
102
|
+
var vendors = Object.keys(_flows);
|
|
103
|
+
for (var i = 0; i < vendors.length; i++) {
|
|
104
|
+
var flow = _flows[vendors[i]];
|
|
105
|
+
result.push({
|
|
106
|
+
vendor: flow.vendor,
|
|
107
|
+
terminalId: flow.terminalId,
|
|
108
|
+
startedAt: flow.startedAt,
|
|
109
|
+
completed: !!flow.completed,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function broadcastState() {
|
|
116
|
+
send({ type: "vendor_login_state", slug: slug, flows: listFlows() });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sendStateTo(ws) {
|
|
120
|
+
sendTo(ws, { type: "vendor_login_state", slug: slug, flows: listFlows() });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function hasTerminalPermission(ws) {
|
|
124
|
+
if (!ws || !ws._clayUser) return true;
|
|
125
|
+
if (!usersModule || typeof usersModule.getEffectivePermissions !== "function") return true;
|
|
126
|
+
var perms = usersModule.getEffectivePermissions(ws._clayUser, osUsers);
|
|
127
|
+
return !!(perms && perms.terminal);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The login terminal must write credentials to the same HOME the adapter
|
|
131
|
+
// will read them from. With OS-user isolation the adapter runs as the
|
|
132
|
+
// *session owner's* Linux user, which is not necessarily the connected
|
|
133
|
+
// client, so prefer the session identity when the requester is entitled to
|
|
134
|
+
// it. Without isolation both sides run as the daemon user and this resolves
|
|
135
|
+
// to null on both paths.
|
|
136
|
+
function resolveLoginIdentity(ws, msg) {
|
|
137
|
+
if (!osUsers) return { linuxUser: null, osUserInfo: null };
|
|
138
|
+
|
|
139
|
+
var sessionLinuxUser = null;
|
|
140
|
+
var sessionId = msg && msg.sessionId;
|
|
141
|
+
if (sessionId && sm && sm.sessions && typeof getLinuxUserForSession === "function") {
|
|
142
|
+
var session = sm.sessions.get(sessionId);
|
|
143
|
+
if (session) {
|
|
144
|
+
var requesterId = ws && ws._clayUser ? ws._clayUser.id : null;
|
|
145
|
+
var isOwner = !session.ownerId || (requesterId && String(session.ownerId) === String(requesterId));
|
|
146
|
+
var isAdmin = !!(ws && ws._clayUser && ws._clayUser.role === "admin");
|
|
147
|
+
if (isOwner || isAdmin) sessionLinuxUser = getLinuxUserForSession(session);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (sessionLinuxUser && typeof getOsUserInfoForLinuxUser === "function") {
|
|
152
|
+
return { linuxUser: sessionLinuxUser, osUserInfo: getOsUserInfoForLinuxUser(sessionLinuxUser) };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
var wsInfo = typeof getOsUserInfoForWs === "function" ? getOsUserInfoForWs(ws) : null;
|
|
156
|
+
return { linuxUser: wsInfo ? wsInfo.user : null, osUserInfo: wsInfo };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isFlowTerminalAlive(flow) {
|
|
160
|
+
return !!(flow && typeof flow.terminalId === "number" && tm.has(flow.terminalId));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function discardFlow(vendor) {
|
|
164
|
+
var flow = _flows[vendor];
|
|
165
|
+
if (!flow) return null;
|
|
166
|
+
delete _flows[vendor];
|
|
167
|
+
if (flow.closeTimer) {
|
|
168
|
+
clearTimeout(flow.closeTimer);
|
|
169
|
+
flow.closeTimer = null;
|
|
170
|
+
}
|
|
171
|
+
return flow;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Kill the PTY and drop it from the sidebar list. `flow.closing` keeps the
|
|
175
|
+
// terminal's own exit hook from re-entering finalization.
|
|
176
|
+
function closeFlowTerminal(flow) {
|
|
177
|
+
if (!flow || typeof flow.terminalId !== "number") return;
|
|
178
|
+
flow.closing = true;
|
|
179
|
+
try { tm.close(flow.terminalId); } catch (e) {}
|
|
180
|
+
send({ type: "term_list", terminals: tm.list() });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Restart the vendor adapter so the next query spawns a process that reads
|
|
184
|
+
// the credentials the login just wrote. Shared adapter instances (Claude) are
|
|
185
|
+
// reused by every project, so tearing one down here would kill unrelated
|
|
186
|
+
// sessions; those runtimes pick up new credentials on their own.
|
|
187
|
+
function refreshVendorAuth(vendor, linuxUser) {
|
|
188
|
+
var adapter = adapters[vendor];
|
|
189
|
+
if (!adapter || typeof adapter.shutdown !== "function") return Promise.resolve(false);
|
|
190
|
+
if (adapter.shared) {
|
|
191
|
+
console.log("[vendor-login] " + vendor + " adapter is shared; skipping restart for " + slug);
|
|
192
|
+
return Promise.resolve(false);
|
|
193
|
+
}
|
|
194
|
+
if (typeof adapter.refreshAuthIdentity === "function") {
|
|
195
|
+
return Promise.resolve(adapter.refreshAuthIdentity(linuxUser || null));
|
|
196
|
+
}
|
|
197
|
+
if (linuxUser) return Promise.resolve(false);
|
|
198
|
+
// adapter.shutdown() already fans out to every per-OS-user runtime it
|
|
199
|
+
// created (shutdownUserRuntimes), so one call covers isolated setups too.
|
|
200
|
+
return Promise.resolve()
|
|
201
|
+
.then(function () { return adapter.shutdown(); })
|
|
202
|
+
.then(function () {
|
|
203
|
+
console.log("[vendor-login] Restarted " + vendor + " adapter for " + slug + " after login");
|
|
204
|
+
return true;
|
|
205
|
+
})
|
|
206
|
+
.catch(function (err) {
|
|
207
|
+
console.error("[vendor-login] " + vendor + " adapter restart failed for " + slug + ":",
|
|
208
|
+
err && err.message ? err.message : err);
|
|
209
|
+
return false;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function finishFlow(vendor, succeeded) {
|
|
214
|
+
var flow = _flows[vendor];
|
|
215
|
+
if (!flow || flow.finishing) return;
|
|
216
|
+
flow.finishing = true;
|
|
217
|
+
flow.completed = !!succeeded;
|
|
218
|
+
|
|
219
|
+
// The next session must not reuse the pre-login "not authenticated"
|
|
220
|
+
// result. Re-check before restarting the runtime, which will then read
|
|
221
|
+
// the newly written credential file.
|
|
222
|
+
if (succeeded) yoke.invalidateAuthCache();
|
|
223
|
+
var refresh = Promise.resolve(false);
|
|
224
|
+
if (succeeded) {
|
|
225
|
+
refresh = typeof refreshVendorAuthEverywhere === "function"
|
|
226
|
+
? refreshVendorAuthEverywhere(vendor, flow.linuxUser || null)
|
|
227
|
+
: refreshVendorAuth(vendor, flow.linuxUser || null);
|
|
228
|
+
}
|
|
229
|
+
refresh.then(function (restarted) {
|
|
230
|
+
discardFlow(vendor);
|
|
231
|
+
closeFlowTerminal(flow);
|
|
232
|
+
if (succeeded) {
|
|
233
|
+
send({ type: "auth_refreshed", slug: slug, vendor: vendor, adapterRestarted: !!restarted });
|
|
234
|
+
}
|
|
235
|
+
broadcastState();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function handleFlowOutput(vendor, chunk) {
|
|
240
|
+
var flow = _flows[vendor];
|
|
241
|
+
if (!flow || flow.finishing || flow.succeeded) return;
|
|
242
|
+
flow.outputTail = (flow.outputTail + stripAnsi(chunk)).slice(-OUTPUT_TAIL_MAX);
|
|
243
|
+
if (!looksLikeLoginSuccess(flow.outputTail)) return;
|
|
244
|
+
|
|
245
|
+
flow.succeeded = true;
|
|
246
|
+
console.log("[vendor-login] Detected successful " + vendor + " login in " + slug);
|
|
247
|
+
// Let the success line land on screen before the terminal disappears.
|
|
248
|
+
flow.closeTimer = setTimeout(function () {
|
|
249
|
+
finishFlow(vendor, true);
|
|
250
|
+
}, CLOSE_AFTER_SUCCESS_MS);
|
|
251
|
+
if (flow.closeTimer && typeof flow.closeTimer.unref === "function") flow.closeTimer.unref();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Fires for both a PTY that exited on its own and one the user closed from
|
|
255
|
+
// the sidebar. Either way the flow is over; refresh auth if the command had
|
|
256
|
+
// already reported success.
|
|
257
|
+
function handleFlowExit(vendor) {
|
|
258
|
+
var flow = _flows[vendor];
|
|
259
|
+
if (!flow || flow.closing || flow.finishing) return;
|
|
260
|
+
finishFlow(vendor, !!flow.succeeded);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function startFlow(ws, vendor, msg) {
|
|
264
|
+
var identity = resolveLoginIdentity(ws, msg);
|
|
265
|
+
var command = loginCommandFor(vendor);
|
|
266
|
+
var flow = {
|
|
267
|
+
vendor: vendor,
|
|
268
|
+
terminalId: null,
|
|
269
|
+
startedAt: Date.now(),
|
|
270
|
+
linuxUser: identity.linuxUser,
|
|
271
|
+
outputTail: "",
|
|
272
|
+
succeeded: false,
|
|
273
|
+
finishing: false,
|
|
274
|
+
closing: false,
|
|
275
|
+
completed: false,
|
|
276
|
+
closeTimer: null,
|
|
277
|
+
};
|
|
278
|
+
// Registered before create() so the PTY's first output chunk (hooks fire
|
|
279
|
+
// synchronously from spawn) already finds the record.
|
|
280
|
+
_flows[vendor] = flow;
|
|
281
|
+
|
|
282
|
+
var terminal = null;
|
|
283
|
+
try {
|
|
284
|
+
terminal = tm.create(100, 30, identity.osUserInfo, ws, {
|
|
285
|
+
initialInput: command + "\n",
|
|
286
|
+
title: vendorDisplayName(vendor) + " login",
|
|
287
|
+
kind: "vendor-login",
|
|
288
|
+
onData: function (data) { handleFlowOutput(vendor, data); },
|
|
289
|
+
onExit: function () { handleFlowExit(vendor); },
|
|
290
|
+
});
|
|
291
|
+
} catch (e) {
|
|
292
|
+
console.error("[vendor-login] Failed to spawn " + vendor + " login terminal in " + slug + ":",
|
|
293
|
+
e && e.message ? e.message : e);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!terminal) {
|
|
297
|
+
delete _flows[vendor];
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
flow.terminalId = terminal.id;
|
|
302
|
+
tm.attach(terminal.id, ws);
|
|
303
|
+
send({ type: "term_list", terminals: tm.list() });
|
|
304
|
+
return flow;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function handleVendorLoginMessage(ws, msg) {
|
|
308
|
+
if (msg.type === "vendor_login_state_request") {
|
|
309
|
+
sendStateTo(ws);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (msg.type === "vendor_login_cancel") {
|
|
314
|
+
var cancelVendor = String(msg.vendor || "");
|
|
315
|
+
var cancelled = discardFlow(cancelVendor);
|
|
316
|
+
if (cancelled) closeFlowTerminal(cancelled);
|
|
317
|
+
broadcastState();
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (msg.type === "vendor_login_start") {
|
|
322
|
+
var vendor = String(msg.vendor || "claude");
|
|
323
|
+
if (!vendorRegistry.getVendorInfo(vendor)) {
|
|
324
|
+
sendTo(ws, { type: "vendor_login_error", vendor: vendor, error: "Unknown vendor: " + vendor });
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
if (!hasTerminalPermission(ws)) {
|
|
328
|
+
sendTo(ws, { type: "vendor_login_error", vendor: vendor, error: "Terminal access is not permitted" });
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
var existing = _flows[vendor];
|
|
333
|
+
if (existing && !isFlowTerminalAlive(existing)) {
|
|
334
|
+
// Terminal died without its exit hook clearing the record.
|
|
335
|
+
discardFlow(vendor);
|
|
336
|
+
existing = null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (existing) {
|
|
340
|
+
// An auto-triggered request (a fresh auth_required from another
|
|
341
|
+
// session or split pane) must not pop a second prompt; it only learns
|
|
342
|
+
// that a flow is already running. A deliberate request re-attaches.
|
|
343
|
+
if (!msg.auto) {
|
|
344
|
+
tm.attach(existing.terminalId, ws);
|
|
345
|
+
sendTo(ws, {
|
|
346
|
+
type: "vendor_login_ready",
|
|
347
|
+
slug: slug,
|
|
348
|
+
vendor: vendor,
|
|
349
|
+
terminalId: existing.terminalId,
|
|
350
|
+
reused: true,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
sendStateTo(ws);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
var started = startFlow(ws, vendor, msg);
|
|
358
|
+
if (!started) {
|
|
359
|
+
sendTo(ws, {
|
|
360
|
+
type: "vendor_login_error",
|
|
361
|
+
vendor: vendor,
|
|
362
|
+
error: "Cannot create terminal (node-pty not available or limit reached)",
|
|
363
|
+
});
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
console.log("[vendor-login] Started " + vendor + " login terminal " + started.terminalId + " in " + slug);
|
|
368
|
+
sendTo(ws, {
|
|
369
|
+
type: "vendor_login_ready",
|
|
370
|
+
slug: slug,
|
|
371
|
+
vendor: vendor,
|
|
372
|
+
terminalId: started.terminalId,
|
|
373
|
+
reused: false,
|
|
374
|
+
});
|
|
375
|
+
broadcastState();
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// The sidebar "close terminal" path goes through tm.close() directly, which
|
|
383
|
+
// fires our exit hook. This is the explicit entry point for callers that
|
|
384
|
+
// remove a terminal without going through the PTY.
|
|
385
|
+
function forgetTerminal(terminalId) {
|
|
386
|
+
var vendors = Object.keys(_flows);
|
|
387
|
+
for (var i = 0; i < vendors.length; i++) {
|
|
388
|
+
if (_flows[vendors[i]].terminalId === terminalId) {
|
|
389
|
+
handleFlowExit(vendors[i]);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function isLoginTerminal(terminalId) {
|
|
397
|
+
var vendors = Object.keys(_flows);
|
|
398
|
+
for (var i = 0; i < vendors.length; i++) {
|
|
399
|
+
if (_flows[vendors[i]].terminalId === terminalId) return true;
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
handleVendorLoginMessage: handleVendorLoginMessage,
|
|
406
|
+
sendStateTo: sendStateTo,
|
|
407
|
+
listFlows: listFlows,
|
|
408
|
+
forgetTerminal: forgetTerminal,
|
|
409
|
+
isLoginTerminal: isLoginTerminal,
|
|
410
|
+
refreshVendorAuth: refreshVendorAuth,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
module.exports = { attachVendorLogin: attachVendorLogin };
|