clay-server 2.10.0 → 2.11.0-beta.2

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/dm.js ADDED
@@ -0,0 +1,135 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+ var config = require("./config");
4
+
5
+ var DM_DIR = path.join(config.CONFIG_DIR, "dm");
6
+
7
+ // Ensure dm directory exists
8
+ function ensureDmDir() {
9
+ fs.mkdirSync(DM_DIR, { recursive: true });
10
+ config.chmodSafe(DM_DIR, 0o700);
11
+ }
12
+
13
+ // Generate deterministic DM key from two user IDs (sorted, order-independent)
14
+ function dmKey(userId1, userId2) {
15
+ return [userId1, userId2].sort().join(":");
16
+ }
17
+
18
+ // File path for a DM conversation
19
+ function dmFilePath(key) {
20
+ // Replace : with _ for safe filename
21
+ return path.join(DM_DIR, key.replace(/:/g, "_") + ".jsonl");
22
+ }
23
+
24
+ // Load DM history from JSONL file
25
+ function loadHistory(key) {
26
+ var filePath = dmFilePath(key);
27
+ if (!fs.existsSync(filePath)) return [];
28
+ try {
29
+ var content = fs.readFileSync(filePath, "utf8").trim();
30
+ if (!content) return [];
31
+ var lines = content.split("\n");
32
+ var messages = [];
33
+ for (var i = 0; i < lines.length; i++) {
34
+ if (!lines[i].trim()) continue;
35
+ try {
36
+ messages.push(JSON.parse(lines[i]));
37
+ } catch (e) {
38
+ // skip malformed lines
39
+ }
40
+ }
41
+ return messages;
42
+ } catch (e) {
43
+ return [];
44
+ }
45
+ }
46
+
47
+ // Append a message to DM JSONL file
48
+ function appendMessage(key, message) {
49
+ ensureDmDir();
50
+ var filePath = dmFilePath(key);
51
+ var line = JSON.stringify(message) + "\n";
52
+ fs.appendFileSync(filePath, line);
53
+ }
54
+
55
+ // Open a DM conversation (find or create)
56
+ function openDm(userId1, userId2) {
57
+ var key = dmKey(userId1, userId2);
58
+ var history = loadHistory(key);
59
+ return { dmKey: key, messages: history };
60
+ }
61
+
62
+ // Send a DM message
63
+ function sendMessage(key, fromUserId, text) {
64
+ var message = {
65
+ type: "dm_message",
66
+ ts: Date.now(),
67
+ from: fromUserId,
68
+ text: text,
69
+ };
70
+ appendMessage(key, message);
71
+ return message;
72
+ }
73
+
74
+ // Get list of all DM conversations for a user
75
+ // Returns: [{ dmKey, otherUserId, lastMessage, lastTs }]
76
+ function getDmList(userId) {
77
+ ensureDmDir();
78
+ var files;
79
+ try {
80
+ files = fs.readdirSync(DM_DIR).filter(function (f) {
81
+ return f.endsWith(".jsonl");
82
+ });
83
+ } catch (e) {
84
+ return [];
85
+ }
86
+
87
+ var dms = [];
88
+ for (var i = 0; i < files.length; i++) {
89
+ // Reconstruct dmKey from filename (replace _ back to :)
90
+ var key = files[i].replace(".jsonl", "").replace(/_/g, ":");
91
+ var parts = key.split(":");
92
+ if (parts.length !== 2) continue;
93
+
94
+ // Check if this user is a participant
95
+ var idx = parts.indexOf(userId);
96
+ if (idx === -1) continue;
97
+
98
+ var otherUserId = parts[idx === 0 ? 1 : 0];
99
+
100
+ // Get last message
101
+ var messages = loadHistory(key);
102
+ var lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
103
+
104
+ dms.push({
105
+ dmKey: key,
106
+ otherUserId: otherUserId,
107
+ lastMessage: lastMessage ? lastMessage.text : null,
108
+ lastTs: lastMessage ? lastMessage.ts : 0,
109
+ messageCount: messages.length,
110
+ });
111
+ }
112
+
113
+ // Sort by most recent activity
114
+ dms.sort(function (a, b) {
115
+ return b.lastTs - a.lastTs;
116
+ });
117
+
118
+ return dms;
119
+ }
120
+
121
+ // Extension point: check if a user is a mate (AI persona)
122
+ // Returns false for now - will be implemented when Mates feature is added
123
+ function isMate(userId) {
124
+ return false;
125
+ }
126
+
127
+ module.exports = {
128
+ dmKey: dmKey,
129
+ openDm: openDm,
130
+ sendMessage: sendMessage,
131
+ getDmList: getDmList,
132
+ loadHistory: loadHistory,
133
+ isMate: isMate,
134
+ DM_DIR: DM_DIR,
135
+ };
package/lib/project.js CHANGED
@@ -979,7 +979,7 @@ function createProjectContext(opts) {
979
979
  if (sm.currentModel) {
980
980
  sendTo(ws, { type: "model_info", model: sm.currentModel, models: sm.availableModels || [] });
981
981
  }
982
- sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
982
+ sendTo(ws, { type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
983
983
  sendTo(ws, { type: "term_list", terminals: tm.list() });
984
984
  sendTo(ws, { type: "notes_list", notes: nm.list() });
985
985
  sendTo(ws, { type: "loop_registry_updated", records: getHubSchedules() });
@@ -1137,6 +1137,14 @@ function createProjectContext(opts) {
1137
1137
  }
1138
1138
 
1139
1139
  function handleMessage(ws, msg) {
1140
+ // --- DM messages (delegated to server-level handler) ---
1141
+ if (msg.type === "dm_open" || msg.type === "dm_send" || msg.type === "dm_list" || msg.type === "dm_typing") {
1142
+ if (typeof opts.onDmMessage === "function") {
1143
+ opts.onDmMessage(ws, msg);
1144
+ }
1145
+ return;
1146
+ }
1147
+
1140
1148
  if (msg.type === "push_subscribe") {
1141
1149
  if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint);
1142
1150
  return;
@@ -1179,17 +1187,31 @@ function createProjectContext(opts) {
1179
1187
  if (msg.type === "resume_session") {
1180
1188
  if (!msg.cliSessionId) return;
1181
1189
  var cliSess = require("./cli-sessions");
1182
- cliSess.readCliSessionHistory(cwd, msg.cliSessionId).then(function (history) {
1183
- var title = "Resumed session";
1184
- for (var i = 0; i < history.length; i++) {
1185
- if (history[i].type === "user_message" && history[i].text) {
1186
- title = history[i].text.substring(0, 50);
1187
- break;
1190
+ // Try SDK for title first, then fall back to manual parsing
1191
+ var titlePromise = getSDK().then(function(sdkMod) {
1192
+ return sdkMod.getSessionInfo(msg.cliSessionId, { dir: cwd });
1193
+ }).then(function(info) {
1194
+ return (info && info.summary) ? info.summary.substring(0, 100) : null;
1195
+ }).catch(function() { return null; });
1196
+
1197
+ Promise.all([
1198
+ cliSess.readCliSessionHistory(cwd, msg.cliSessionId),
1199
+ titlePromise
1200
+ ]).then(function(results) {
1201
+ var history = results[0];
1202
+ var sdkTitle = results[1];
1203
+ var title = sdkTitle || "Resumed session";
1204
+ if (!sdkTitle) {
1205
+ for (var i = 0; i < history.length; i++) {
1206
+ if (history[i].type === "user_message" && history[i].text) {
1207
+ title = history[i].text.substring(0, 50);
1208
+ break;
1209
+ }
1188
1210
  }
1189
1211
  }
1190
1212
  var resumed = sm.resumeSession(msg.cliSessionId, { history: history, title: title }, ws);
1191
1213
  if (resumed) ws._clayActiveSession = resumed.localId;
1192
- }).catch(function () {
1214
+ }).catch(function() {
1193
1215
  var resumed = sm.resumeSession(msg.cliSessionId, undefined, ws);
1194
1216
  if (resumed) ws._clayActiveSession = resumed.localId;
1195
1217
  });
@@ -1197,9 +1219,7 @@ function createProjectContext(opts) {
1197
1219
  }
1198
1220
 
1199
1221
  if (msg.type === "list_cli_sessions") {
1200
- var cliSessions = require("./cli-sessions");
1201
1222
  var _fs = require("fs");
1202
- var _path = require("path");
1203
1223
  // Collect session IDs already in relay (in-memory + persisted on disk)
1204
1224
  var relayIds = {};
1205
1225
  sm.sessions.forEach(function (s) {
@@ -1214,13 +1234,34 @@ function createProjectContext(opts) {
1214
1234
  }
1215
1235
  }
1216
1236
  } catch (e) {}
1217
- cliSessions.listCliSessions(cwd).then(function (sessions) {
1218
- var filtered = sessions.filter(function (s) {
1237
+
1238
+ getSDK().then(function(sdkMod) {
1239
+ return sdkMod.listSessions({ dir: cwd });
1240
+ }).then(function(sdkSessions) {
1241
+ var filtered = sdkSessions.filter(function(s) {
1219
1242
  return !relayIds[s.sessionId];
1243
+ }).map(function(s) {
1244
+ return {
1245
+ sessionId: s.sessionId,
1246
+ firstPrompt: s.summary || s.firstPrompt || "",
1247
+ model: null,
1248
+ gitBranch: s.gitBranch || null,
1249
+ startTime: s.createdAt ? new Date(s.createdAt).toISOString() : null,
1250
+ lastActivity: s.lastModified ? new Date(s.lastModified).toISOString() : null,
1251
+ };
1220
1252
  });
1221
1253
  sendTo(ws, { type: "cli_session_list", sessions: filtered });
1222
- }).catch(function () {
1223
- sendTo(ws, { type: "cli_session_list", sessions: [] });
1254
+ }).catch(function() {
1255
+ // Fallback to manual parsing if SDK fails
1256
+ var cliSessions = require("./cli-sessions");
1257
+ cliSessions.listCliSessions(cwd).then(function(sessions) {
1258
+ var filtered = sessions.filter(function(s) {
1259
+ return !relayIds[s.sessionId];
1260
+ });
1261
+ sendTo(ws, { type: "cli_session_list", sessions: filtered });
1262
+ }).catch(function() {
1263
+ sendTo(ws, { type: "cli_session_list", sessions: [] });
1264
+ });
1224
1265
  });
1225
1266
  return;
1226
1267
  }
@@ -1256,6 +1297,14 @@ function createProjectContext(opts) {
1256
1297
  s.title = String(msg.title).substring(0, 100);
1257
1298
  sm.saveSessionFile(s);
1258
1299
  sm.broadcastSessionList();
1300
+ // Sync title to SDK session
1301
+ if (s.cliSessionId) {
1302
+ getSDK().then(function(sdk) {
1303
+ sdk.renameSession(s.cliSessionId, s.title, { dir: cwd }).catch(function(e) {
1304
+ console.error("[project] SDK renameSession failed:", e.message);
1305
+ });
1306
+ }).catch(function() {});
1307
+ }
1259
1308
  }
1260
1309
  return;
1261
1310
  }
@@ -1380,7 +1429,7 @@ function createProjectContext(opts) {
1380
1429
  if (msg.type === "set_permission_mode" && msg.mode) {
1381
1430
  // When dangerouslySkipPermissions is active, don't allow UI to change mode
1382
1431
  if (dangerouslySkipPermissions) {
1383
- send({ type: "config_state", model: sm.currentModel || "", mode: "bypassPermissions", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1432
+ send({ type: "config_state", model: sm.currentModel || "", mode: "bypassPermissions", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1384
1433
  return;
1385
1434
  }
1386
1435
  sm.currentPermissionMode = msg.mode;
@@ -1388,7 +1437,7 @@ function createProjectContext(opts) {
1388
1437
  if (session) {
1389
1438
  sdk.setPermissionMode(session, msg.mode);
1390
1439
  }
1391
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1440
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1392
1441
  return;
1393
1442
  }
1394
1443
 
@@ -1402,7 +1451,7 @@ function createProjectContext(opts) {
1402
1451
  if (session) {
1403
1452
  sdk.setPermissionMode(session, msg.mode);
1404
1453
  }
1405
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1454
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1406
1455
  }
1407
1456
  return;
1408
1457
  }
@@ -1417,14 +1466,18 @@ function createProjectContext(opts) {
1417
1466
  if (session) {
1418
1467
  sdk.setPermissionMode(session, msg.mode);
1419
1468
  }
1420
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1469
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1421
1470
  }
1422
1471
  return;
1423
1472
  }
1424
1473
 
1425
1474
  if (msg.type === "set_effort" && msg.effort) {
1426
1475
  sm.currentEffort = msg.effort;
1427
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
1476
+ var session = getSessionForWs(ws);
1477
+ if (session) {
1478
+ sdk.setEffort(session, msg.effort);
1479
+ }
1480
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1428
1481
  return;
1429
1482
  }
1430
1483
 
@@ -1433,7 +1486,7 @@ function createProjectContext(opts) {
1433
1486
  opts.onSetServerDefaultEffort(msg.effort);
1434
1487
  }
1435
1488
  sm.currentEffort = msg.effort;
1436
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
1489
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1437
1490
  return;
1438
1491
  }
1439
1492
 
@@ -1442,13 +1495,20 @@ function createProjectContext(opts) {
1442
1495
  opts.onSetProjectDefaultEffort(slug, msg.effort);
1443
1496
  }
1444
1497
  sm.currentEffort = msg.effort;
1445
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [] });
1498
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort, betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1446
1499
  return;
1447
1500
  }
1448
1501
 
1449
1502
  if (msg.type === "set_betas") {
1450
1503
  sm.currentBetas = msg.betas || [];
1451
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas });
1504
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas, thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1505
+ return;
1506
+ }
1507
+
1508
+ if (msg.type === "set_thinking") {
1509
+ sm.currentThinking = msg.thinking || "adaptive";
1510
+ if (msg.budgetTokens) sm.currentThinkingBudget = msg.budgetTokens;
1511
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode || "default", effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1452
1512
  return;
1453
1513
  }
1454
1514
 
@@ -1549,6 +1609,34 @@ function createProjectContext(opts) {
1549
1609
  return;
1550
1610
  }
1551
1611
 
1612
+ if (msg.type === "fork_session" && msg.uuid) {
1613
+ var session = getSessionForWs(ws);
1614
+ if (!session || !session.cliSessionId) {
1615
+ sendTo(ws, { type: "error", text: "Cannot fork: no CLI session" });
1616
+ return;
1617
+ }
1618
+ var forkCliId = session.cliSessionId;
1619
+ var forkTitle = (session.title || "New Session") + " (fork)";
1620
+ getSDK().then(function(sdkMod) {
1621
+ return sdkMod.forkSession(forkCliId, {
1622
+ upToMessageId: msg.uuid,
1623
+ dir: cwd,
1624
+ });
1625
+ }).then(function(result) {
1626
+ var cliSess = require("./cli-sessions");
1627
+ return cliSess.readCliSessionHistory(cwd, result.sessionId).then(function(history) {
1628
+ var forked = sm.resumeSession(result.sessionId, { history: history, title: forkTitle }, ws);
1629
+ if (forked) {
1630
+ ws._clayActiveSession = forked.localId;
1631
+ sendTo(ws, { type: "fork_complete", sessionId: forked.localId });
1632
+ }
1633
+ });
1634
+ }).catch(function(e) {
1635
+ sendTo(ws, { type: "error", text: "Fork failed: " + (e.message || e) });
1636
+ });
1637
+ return;
1638
+ }
1639
+
1552
1640
  if (msg.type === "ask_user_response") {
1553
1641
  var session = getSessionForWs(ws);
1554
1642
  if (!session) return;
@@ -1583,7 +1671,7 @@ function createProjectContext(opts) {
1583
1671
  if (decision === "allow_accept_edits") {
1584
1672
  sdk.setPermissionMode(session, "acceptEdits");
1585
1673
  sm.currentPermissionMode = "acceptEdits";
1586
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1674
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1587
1675
  pending.resolve({ behavior: "allow", updatedInput: pending.toolInput });
1588
1676
  sm.sendAndRecord(session, { type: "permission_resolved", requestId: requestId, decision: decision });
1589
1677
  return;
@@ -1612,7 +1700,7 @@ function createProjectContext(opts) {
1612
1700
 
1613
1701
  // Update permission mode for the new session
1614
1702
  sm.currentPermissionMode = "acceptEdits";
1615
- send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [] });
1703
+ send({ type: "config_state", model: sm.currentModel || "", mode: sm.currentPermissionMode, effort: sm.currentEffort || "medium", betas: sm.currentBetas || [], thinking: sm.currentThinking || "adaptive", thinkingBudget: sm.currentThinkingBudget || 10000 });
1616
1704
 
1617
1705
  // Build prompt from plan content (sent from client) or plan file path
1618
1706
  var clientPlanContent = msg.planContent || "";
@@ -1705,6 +1793,26 @@ function createProjectContext(opts) {
1705
1793
  return;
1706
1794
  }
1707
1795
 
1796
+ // --- MCP elicitation response ---
1797
+ if (msg.type === "elicitation_response") {
1798
+ var session = getSessionForWs(ws);
1799
+ if (!session) return;
1800
+ var pending = session.pendingElicitations && session.pendingElicitations[msg.requestId];
1801
+ if (!pending) return;
1802
+ delete session.pendingElicitations[msg.requestId];
1803
+ if (msg.action === "accept") {
1804
+ pending.resolve({ action: "accept", content: msg.content || {} });
1805
+ } else {
1806
+ pending.resolve({ action: "reject" });
1807
+ }
1808
+ sm.sendAndRecord(session, {
1809
+ type: "elicitation_resolved",
1810
+ requestId: msg.requestId,
1811
+ action: msg.action,
1812
+ });
1813
+ return;
1814
+ }
1815
+
1708
1816
  // --- Browse directories (for add-project autocomplete) ---
1709
1817
  if (msg.type === "browse_dir") {
1710
1818
  var rawPath = (msg.path || "").replace(/^~/, process.env.HOME || "/");
@@ -2654,6 +2762,14 @@ function createProjectContext(opts) {
2654
2762
  session.title = (msg.text || "Image").substring(0, 50);
2655
2763
  sm.saveSessionFile(session);
2656
2764
  sm.broadcastSessionList();
2765
+ // Sync auto-title to SDK
2766
+ if (session.cliSessionId) {
2767
+ getSDK().then(function(sdk) {
2768
+ sdk.renameSession(session.cliSessionId, session.title, { dir: cwd }).catch(function(e) {
2769
+ console.error("[project] SDK renameSession failed:", e.message);
2770
+ });
2771
+ }).catch(function() {});
2772
+ }
2657
2773
  }
2658
2774
 
2659
2775
  var fullText = msg.text || "";
@@ -3154,6 +3270,8 @@ function createProjectContext(opts) {
3154
3270
  },
3155
3271
  warmup: function () {
3156
3272
  sdk.warmup();
3273
+ // Migrate existing relay session titles to SDK format (one-time, async)
3274
+ sm.migrateSessionTitles(getSDK, cwd);
3157
3275
  },
3158
3276
  destroy: destroy,
3159
3277
  };