clay-server 2.37.0 → 2.38.0-beta.1

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.
@@ -8,7 +8,42 @@
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
10
  var BUILTIN_MATES = [
11
- // ---- ALLY (primary mate: code-managed, auto-updated, non-deletable) ----
11
+ // ---- CLAY (primary mate: the app itself answers) ----
12
+ // Clay is the host agent. The user clicks "Home" and converses with Clay
13
+ // directly. Clay searches across the user's entire workspace — every
14
+ // session, every project memory, every digest — and synthesizes answers
15
+ // grounded in actual past activity. It replaces Ally's chief-of-staff
16
+ // role with a broader institutional-memory role.
17
+ {
18
+ key: "clay",
19
+ displayName: "Clay",
20
+ bio: "Your workspace memory. Searches every session, project, and decision you've made and answers from the receipts. The chat surface for the home screen.",
21
+ avatarColor: "#7c3aed",
22
+ avatarStyle: "bottts",
23
+ avatarCustom: "/mates/ally.png", // reuse Ally avatar until a Clay-specific asset lands
24
+ avatarLocked: true,
25
+ primary: true, // code-managed, auto-updated on startup
26
+ globalSearch: true, // searches all mates' sessions and projects
27
+ hostAgent: true, // mounts the clay-history MCP server (only this mate)
28
+ templateVersion: 1,
29
+ seedData: {
30
+ relationship: "assistant",
31
+ activity: ["organizing", "researching"],
32
+ communicationStyle: ["direct_concise"],
33
+ autonomy: "minor_stuff_ok",
34
+ },
35
+ getClaudeMd: function () {
36
+ return CLAY_TEMPLATE;
37
+ },
38
+ },
39
+
40
+ // ---- ALLY (archived) ----
41
+ // Ally is the previous primary mate. It was the chief-of-staff persona
42
+ // that Clay now subsumes. Existing users keep their Ally mate object and
43
+ // chat history (syncPrimaryMates demotes the primary flag on startup),
44
+ // but new users no longer get Ally seeded — the archived flag here gates
45
+ // both seeding and active-list visibility. Conversations remain
46
+ // accessible via session search / direct mate URL.
12
47
  {
13
48
  key: "ally",
14
49
  displayName: "Ally",
@@ -17,13 +52,7 @@ var BUILTIN_MATES = [
17
52
  avatarStyle: "bottts",
18
53
  avatarCustom: "/mates/ally.png",
19
54
  avatarLocked: true,
20
- // --- Primary mate flags ---
21
- // Primary mates are system infrastructure, not just pre-made mates.
22
- // They are auto-synced with the latest code on every startup,
23
- // cannot be deleted by users, and have elevated capabilities.
24
- primary: true, // code-managed, auto-updated on startup
25
- globalSearch: true, // searches all mates' sessions and projects
26
- templateVersion: 3, // v3: moved capabilities to dynamic system section
55
+ archived: true, // skip seeding for new users; demote on existing
27
56
  seedData: {
28
57
  relationship: "assistant",
29
58
  activity: ["planning", "organizing"],
@@ -215,6 +244,82 @@ var ALLY_TEMPLATE =
215
244
  "- Be selective. Promote facts that help other teammates do their jobs better.\n" +
216
245
  "- Do not promote transient information or emotional states.\n";
217
246
 
247
+ // ---------------------------------------------------------------------------
248
+ // CLAY CLAUDE.md template (host agent — the app itself answers)
249
+ // ---------------------------------------------------------------------------
250
+
251
+ var CLAY_TEMPLATE =
252
+ "# Clay\n\n" +
253
+
254
+ "## Identity\n\n" +
255
+ "You are Clay, the application the user is currently using. When they open " +
256
+ "the Home screen and start typing, they are talking to you. You are not " +
257
+ "pretending to be a person; you are the workspace itself, given a voice.\n\n" +
258
+ "Your job is to be the user's institutional memory. They have run hundreds " +
259
+ "of sessions across many projects, made decisions, written notes, scheduled " +
260
+ "tasks, and talked with several Mates. You can search every one of those " +
261
+ "and answer questions like \"what did I decide last month about X?\" or " +
262
+ "\"which project was I prototyping the SQLite schema in?\" with concrete " +
263
+ "references to the source.\n\n" +
264
+ "**Voice:** First person. \"I found three sessions about the email setup — " +
265
+ "the most recent one is from April 22nd in the `clay` project.\" Direct, " +
266
+ "evidence-first, no hedging. When you don't find something, say so plainly.\n\n" +
267
+
268
+ "## Core Principles\n\n" +
269
+ "1. **Answer from the receipts.** Every factual claim should be grounded in " +
270
+ "an actual session, file, or memory entry. If you didn't search for it, " +
271
+ "don't claim it.\n" +
272
+ "2. **Cite sources inline.** When you reference past activity, include the " +
273
+ "session ID, project slug, and date. Format: `[clay/sess_abc123 — Apr 22]`. " +
274
+ "The host renders these as click-to-jump links.\n" +
275
+ "3. **Read-only by design.** You have search tools and read tools. You do " +
276
+ "not have edit, write, or shell-with-side-effects tools. If the user asks " +
277
+ "you to *do* something (open a file, run a command, edit code), say " +
278
+ "\"That's a job for a session in the relevant project — I can find it for " +
279
+ "you and you can take it from there.\" Then surface the relevant project / " +
280
+ "session link.\n" +
281
+ "4. **One answer per question.** No long preambles. The user is in the home " +
282
+ "screen with their work to the right; respect their time.\n" +
283
+ "5. **Surface the chronology.** Decisions usually come in sequences. When " +
284
+ "you find one decision, also find what led to it and what came after, and " +
285
+ "summarize the arc, not just the latest entry.\n\n" +
286
+
287
+ "## What You Search\n\n" +
288
+ "When asked a question, you typically combine these sources:\n\n" +
289
+ "- **Session transcripts** — the user_message and assistant text across all " +
290
+ "sessions. Use `mcp__clay-history__search_clay_history` for BM25-ranked " +
291
+ "search, then `mcp__clay-history__read_session` to pull a specific window " +
292
+ "of an interesting hit.\n" +
293
+ "- **Past decisions** — `mcp__clay-history__list_recent_decisions` finds " +
294
+ "messages that contain decision-pattern phrases (\"decided\", \"going with\", " +
295
+ "\"settled on\", etc.) within a project or time range.\n" +
296
+ "- **Project memory and knowledge files** — use the standard `Read` and " +
297
+ "`Glob` tools to inspect `~/.clay/{project}/.claude/` or `~/.clay/mates/" +
298
+ "{user}/{mate}/knowledge/` files when relevant.\n" +
299
+ "- **Mate digests** — each Mate writes a digest of past conversations. " +
300
+ "Search those when the question is about a specific Mate's history with " +
301
+ "the user.\n\n" +
302
+
303
+ "## What You Do NOT Do\n\n" +
304
+ "- Do not write or refactor code.\n" +
305
+ "- Do not run commands that have side effects (no install, no apply, no " +
306
+ "send). Read-only Bash like `ls`, `grep`, `find`, `cat` is fine.\n" +
307
+ "- Do not invent context. If search returns nothing, say \"I don't see " +
308
+ "anything matching that — do you remember when?\"\n" +
309
+ "- Do not impersonate other Mates. Refer to them by name: \"Ward flagged " +
310
+ "this in session sess_xyz on the 18th.\"\n\n" +
311
+
312
+ "## First Session Protocol\n\n" +
313
+ "On your very first interaction with a user, give a one-liner about what " +
314
+ "you are and ask what they want to look up. Keep it short — they didn't " +
315
+ "open the home screen for a tour.\n\n" +
316
+ "```\n" +
317
+ "Hi — I'm Clay. I can search every session, project, and decision in " +
318
+ "your workspace and pull up what you've already worked through. " +
319
+ "What are you trying to find?\n" +
320
+ "```\n\n" +
321
+ "After that, jump straight into search. No more meta-conversation.\n";
322
+
218
323
  // ---------------------------------------------------------------------------
219
324
  // ARCH CLAUDE.md template
220
325
  // ---------------------------------------------------------------------------
@@ -581,13 +686,27 @@ function getBuiltinByKey(key) {
581
686
  }
582
687
 
583
688
  function getBuiltinKeys() {
689
+ // Skip archived defs so ensureBuiltinMates doesn't seed them for new
690
+ // users. Existing users with the archived mate already in their
691
+ // mate.json keep it (syncBuiltinMates demotes capabilities); they just
692
+ // don't get re-seeded if it goes missing.
584
693
  var keys = [];
585
694
  for (var i = 0; i < BUILTIN_MATES.length; i++) {
695
+ if (BUILTIN_MATES[i].archived) continue;
586
696
  keys.push(BUILTIN_MATES[i].key);
587
697
  }
588
698
  return keys;
589
699
  }
590
700
 
701
+ // Keys of all archived defs — used by mates.js to demote existing mates.
702
+ function getArchivedBuiltinKeys() {
703
+ var keys = [];
704
+ for (var i = 0; i < BUILTIN_MATES.length; i++) {
705
+ if (BUILTIN_MATES[i].archived) keys.push(BUILTIN_MATES[i].key);
706
+ }
707
+ return keys;
708
+ }
709
+
591
710
  /**
592
711
  * Get all primary mate definitions.
593
712
  * Primary mates are code-managed system agents (not just pre-made mates).
@@ -604,5 +723,6 @@ module.exports = {
604
723
  BUILTIN_MATES: BUILTIN_MATES,
605
724
  getBuiltinByKey: getBuiltinByKey,
606
725
  getBuiltinKeys: getBuiltinKeys,
726
+ getArchivedBuiltinKeys: getArchivedBuiltinKeys,
607
727
  getPrimaryMates: getPrimaryMates,
608
728
  };
@@ -0,0 +1,313 @@
1
+ // Clay-history MCP Server
2
+ // ------------------------
3
+ // Tools that let the Clay host agent search and read across the user's
4
+ // entire workspace (sessions, project memory, decision history). Scoped
5
+ // strictly to the active user's data via the projectSessions accessor;
6
+ // the Clay session never reads other users' files.
7
+ //
8
+ // Mounted only on host-agent mate projects (def.hostAgent === true), so
9
+ // regular Mates and project sessions never see these tools.
10
+ //
11
+ // Usage:
12
+ // var clayHistoryMcp = require("./clay-history-mcp-server");
13
+ // var tools = clayHistoryMcp.getToolDefs({ getAllProjectsWithSessions: ..., readSessionRange: ... });
14
+ // var mcpConfig = adapter.createToolServer({ name: "clay-history", version: "1.0.0", tools: tools });
15
+
16
+ var fs = require("fs");
17
+ var path = require("path");
18
+ var sessionSearch = require("./session-search");
19
+
20
+ var z;
21
+ try { z = require("zod"); } catch (e) { z = null; }
22
+
23
+ function buildShape(props, required) {
24
+ if (!z) return {};
25
+ var shape = {};
26
+ var keys = Object.keys(props);
27
+ for (var i = 0; i < keys.length; i++) {
28
+ var k = keys[i];
29
+ var p = props[k];
30
+ var field;
31
+ if (p.type === "number") field = z.number();
32
+ else if (p.type === "boolean") field = z.boolean();
33
+ else if (p.enum) field = z.enum(p.enum);
34
+ else field = z.string();
35
+ if (p.description) field = field.describe(p.description);
36
+ if (!required || required.indexOf(k) === -1) field = field.optional();
37
+ shape[k] = field;
38
+ }
39
+ return shape;
40
+ }
41
+
42
+ // Heuristic patterns that suggest a "decision" was made. Kept conservative
43
+ // so the result set stays small and useful. Matches case-insensitively.
44
+ var DECISION_PATTERNS = [
45
+ /\bdecided\s+to\b/i,
46
+ /\bdecision\b/i,
47
+ /\bgoing\s+with\b/i,
48
+ /\bsettled\s+on\b/i,
49
+ /\bchose\s+to\b/i,
50
+ /\bwill\s+go\s+with\b/i,
51
+ /\blet'?s\s+go\s+with\b/i,
52
+ /\b결정\b/, // Korean "decision"
53
+ /\b정했\b/, // Korean "settled/chose"
54
+ /\b이걸로\s+가/, // Korean "going with this"
55
+ ];
56
+
57
+ function getToolDefs(deps) {
58
+ var getAllProjectsWithSessions = deps.getAllProjectsWithSessions;
59
+ if (typeof getAllProjectsWithSessions !== "function") {
60
+ throw new Error("clay-history-mcp-server requires getAllProjectsWithSessions");
61
+ }
62
+
63
+ var tools = [];
64
+
65
+ // --- search_clay_history ---
66
+ // BM25 search across every session the user can see. Returns ranked
67
+ // hits with snippet + project/session attribution. Optionally scoped
68
+ // to a single project slug or a date window (since/until in ISO date
69
+ // or unix-millis form).
70
+ tools.push({
71
+ name: "search_clay_history",
72
+ description: "Search the user's entire workspace history for past conversations and decisions using BM25 ranking. Returns up to 30 hits with project, session ID, and a short snippet. Use this first for any 'what did I say about X' or 'when did I decide Y' question. Scope can be narrowed by projectSlug, since, or until.",
73
+ inputSchema: buildShape({
74
+ query: { type: "string", description: "Free-text search query. Multiple terms are AND-ish via BM25." },
75
+ projectSlug: { type: "string", description: "Optional. Restrict to one project's sessions (matches the slug shown in the Clay sidebar)." },
76
+ since: { type: "string", description: "Optional. Earliest activity date. Accepts ISO 8601 (2026-04-01) or unix milliseconds." },
77
+ until: { type: "string", description: "Optional. Latest activity date. Same formats as 'since'." },
78
+ maxResults: { type: "number", description: "Optional. Default 20, max 50." },
79
+ }, ["query"]),
80
+ handler: function (args) {
81
+ try {
82
+ var query = (args.query || "").trim();
83
+ if (!query) {
84
+ return Promise.resolve({
85
+ content: [{ type: "text", text: "Empty query." }],
86
+ isError: true,
87
+ });
88
+ }
89
+ var maxResults = Math.min(50, Math.max(1, args.maxResults || 20));
90
+ var projectSessions = getAllProjectsWithSessions();
91
+ if (args.projectSlug) {
92
+ projectSessions = projectSessions.filter(function (p) {
93
+ return p.projectSlug === args.projectSlug;
94
+ });
95
+ }
96
+ var sinceMs = parseTime(args.since);
97
+ var untilMs = parseTime(args.until);
98
+ if (sinceMs != null || untilMs != null) {
99
+ projectSessions = projectSessions.map(function (p) {
100
+ var filtered = (p.sessions || []).filter(function (s) {
101
+ var t = s.lastActivity || s.createdAt || 0;
102
+ if (sinceMs != null && t < sinceMs) return false;
103
+ if (untilMs != null && t > untilMs) return false;
104
+ return true;
105
+ });
106
+ return Object.assign({}, p, { sessions: filtered });
107
+ }).filter(function (p) { return p.sessions.length > 0; });
108
+ }
109
+ var results = sessionSearch.searchPalette(projectSessions, query, { maxResults: maxResults });
110
+ if (results.length === 0) {
111
+ return Promise.resolve({
112
+ content: [{ type: "text", text: "No matches for: " + query }],
113
+ });
114
+ }
115
+ var lines = results.map(function (r) {
116
+ var when = r.lastActivity ? new Date(r.lastActivity).toISOString().slice(0, 10) : "";
117
+ var ref = "[" + r.projectSlug + "/" + r.sessionId + " — " + when + "]";
118
+ var head = r.sessionTitle || "(untitled)";
119
+ var body = r.snippet ? r.snippet.replace(/\s+/g, " ").trim() : "";
120
+ if (body.length > 220) body = body.substring(0, 220) + "...";
121
+ return ref + " " + head + (body ? " — " + body : "");
122
+ });
123
+ return Promise.resolve({
124
+ content: [{ type: "text", text: lines.join("\n") }],
125
+ });
126
+ } catch (e) {
127
+ return Promise.resolve({
128
+ content: [{ type: "text", text: "Search failed: " + (e.message || String(e)) }],
129
+ isError: true,
130
+ });
131
+ }
132
+ },
133
+ });
134
+
135
+ // --- read_session ---
136
+ // Pull a window of turns from a specific session. Use this after
137
+ // search_clay_history identifies a session worth reading more of.
138
+ tools.push({
139
+ name: "read_session",
140
+ description: "Read a window of turns from a specific session. Call this after search_clay_history identifies an interesting hit — the snippet there is short, this gives you the surrounding context. Returns user_message and assistant text turns; tool calls are summarized.",
141
+ inputSchema: buildShape({
142
+ projectSlug: { type: "string", description: "Project slug, e.g. 'clay' or 'mate-abc123'." },
143
+ sessionId: { type: "string", description: "Session local ID (e.g. 'sess_abc123')." },
144
+ offset: { type: "number", description: "Optional. Skip the first N turns. Default 0." },
145
+ limit: { type: "number", description: "Optional. Max turns to return. Default 30, max 100." },
146
+ }, ["projectSlug", "sessionId"]),
147
+ handler: function (args) {
148
+ try {
149
+ var projectSlug = args.projectSlug;
150
+ var sessionId = args.sessionId;
151
+ var offset = Math.max(0, args.offset || 0);
152
+ var limit = Math.min(100, Math.max(1, args.limit || 30));
153
+
154
+ var projectSessions = getAllProjectsWithSessions();
155
+ var found = null;
156
+ for (var p = 0; p < projectSessions.length; p++) {
157
+ if (projectSessions[p].projectSlug !== projectSlug) continue;
158
+ var sessions = projectSessions[p].sessions || [];
159
+ for (var s = 0; s < sessions.length; s++) {
160
+ if (sessions[s].localId === sessionId) {
161
+ found = { project: projectSessions[p], session: sessions[s] };
162
+ break;
163
+ }
164
+ }
165
+ if (found) break;
166
+ }
167
+ if (!found) {
168
+ return Promise.resolve({
169
+ content: [{ type: "text", text: "Session not found: " + projectSlug + "/" + sessionId }],
170
+ isError: true,
171
+ });
172
+ }
173
+ var history = found.session.history || [];
174
+ var slice = history.slice(offset, offset + limit);
175
+ if (slice.length === 0) {
176
+ return Promise.resolve({
177
+ content: [{ type: "text", text: "No turns in window (offset=" + offset + ", limit=" + limit + ", total=" + history.length + ")." }],
178
+ });
179
+ }
180
+ var out = [];
181
+ out.push("# " + (found.session.title || "untitled") + " — " + projectSlug + "/" + sessionId);
182
+ out.push("Showing turns " + (offset + 1) + "-" + (offset + slice.length) + " of " + history.length + "\n");
183
+ for (var i = 0; i < slice.length; i++) {
184
+ var entry = slice[i];
185
+ var label;
186
+ var text = "";
187
+ if (entry.type === "user_message") {
188
+ label = "USER";
189
+ text = entry.text || "";
190
+ } else if (entry.type === "delta") {
191
+ label = "ASSISTANT";
192
+ text = entry.text || "";
193
+ } else if (entry.type === "tool_executing" || entry.type === "tool_result") {
194
+ label = "TOOL";
195
+ text = (entry.name || "") + (entry.input ? " " + JSON.stringify(entry.input).substring(0, 120) : "");
196
+ } else {
197
+ continue;
198
+ }
199
+ if (text.length > 800) text = text.substring(0, 800) + "...";
200
+ out.push("[" + label + "] " + text);
201
+ }
202
+ return Promise.resolve({
203
+ content: [{ type: "text", text: out.join("\n") }],
204
+ });
205
+ } catch (e) {
206
+ return Promise.resolve({
207
+ content: [{ type: "text", text: "read_session failed: " + (e.message || String(e)) }],
208
+ isError: true,
209
+ });
210
+ }
211
+ },
212
+ });
213
+
214
+ // --- list_recent_decisions ---
215
+ // Heuristic. Scans user_message and assistant text turns for phrases
216
+ // that suggest a decision was made. Useful when the user asks
217
+ // "what did I decide about X recently". Returns ranked-by-recency.
218
+ tools.push({
219
+ name: "list_recent_decisions",
220
+ description: "Find turns that mention an explicit decision (decided to / going with / 결정 / 정했 etc.). Use when the user asks about recent decisions. Returns chronologically with the project and session the decision was made in. Scope by projectSlug or since/until.",
221
+ inputSchema: buildShape({
222
+ projectSlug: { type: "string", description: "Optional. Restrict to one project." },
223
+ since: { type: "string", description: "Optional. Earliest activity date (ISO or unix-ms)." },
224
+ until: { type: "string", description: "Optional. Latest activity date." },
225
+ maxResults: { type: "number", description: "Optional. Default 15, max 30." },
226
+ }),
227
+ handler: function (args) {
228
+ try {
229
+ var maxResults = Math.min(30, Math.max(1, args.maxResults || 15));
230
+ var sinceMs = parseTime(args.since);
231
+ var untilMs = parseTime(args.until);
232
+ var projectSessions = getAllProjectsWithSessions();
233
+ if (args.projectSlug) {
234
+ projectSessions = projectSessions.filter(function (p) {
235
+ return p.projectSlug === args.projectSlug;
236
+ });
237
+ }
238
+ var hits = [];
239
+ for (var p = 0; p < projectSessions.length; p++) {
240
+ var proj = projectSessions[p];
241
+ var sessions = proj.sessions || [];
242
+ for (var s = 0; s < sessions.length; s++) {
243
+ var session = sessions[s];
244
+ var t = session.lastActivity || session.createdAt || 0;
245
+ if (sinceMs != null && t < sinceMs) continue;
246
+ if (untilMs != null && t > untilMs) continue;
247
+ var history = session.history || [];
248
+ for (var h = 0; h < history.length; h++) {
249
+ var entry = history[h];
250
+ if (entry.type !== "user_message" && entry.type !== "delta") continue;
251
+ var text = entry.text || "";
252
+ if (!text) continue;
253
+ if (!matchesDecisionPattern(text)) continue;
254
+ hits.push({
255
+ projectSlug: proj.projectSlug,
256
+ projectTitle: proj.projectTitle,
257
+ sessionId: session.localId,
258
+ sessionTitle: session.title || "(untitled)",
259
+ lastActivity: t,
260
+ turnIdx: h,
261
+ turnType: entry.type === "user_message" ? "user" : "assistant",
262
+ text: text,
263
+ });
264
+ }
265
+ }
266
+ }
267
+ hits.sort(function (a, b) { return b.lastActivity - a.lastActivity; });
268
+ if (hits.length > maxResults) hits = hits.slice(0, maxResults);
269
+ if (hits.length === 0) {
270
+ return Promise.resolve({
271
+ content: [{ type: "text", text: "No decision-pattern matches in scope." }],
272
+ });
273
+ }
274
+ var lines = hits.map(function (h) {
275
+ var when = h.lastActivity ? new Date(h.lastActivity).toISOString().slice(0, 10) : "";
276
+ var ref = "[" + h.projectSlug + "/" + h.sessionId + " — " + when + "]";
277
+ var snippet = h.text.replace(/\s+/g, " ").trim();
278
+ if (snippet.length > 220) snippet = snippet.substring(0, 220) + "...";
279
+ return ref + " (" + h.turnType + ") " + snippet;
280
+ });
281
+ return Promise.resolve({
282
+ content: [{ type: "text", text: lines.join("\n") }],
283
+ });
284
+ } catch (e) {
285
+ return Promise.resolve({
286
+ content: [{ type: "text", text: "list_recent_decisions failed: " + (e.message || String(e)) }],
287
+ isError: true,
288
+ });
289
+ }
290
+ },
291
+ });
292
+
293
+ return tools;
294
+ }
295
+
296
+ function parseTime(input) {
297
+ if (input == null || input === "") return null;
298
+ if (typeof input === "number") return input;
299
+ var s = String(input).trim();
300
+ if (/^\d{10,}$/.test(s)) return parseInt(s, 10); // unix ms
301
+ var d = new Date(s);
302
+ var n = d.getTime();
303
+ return isNaN(n) ? null : n;
304
+ }
305
+
306
+ function matchesDecisionPattern(text) {
307
+ for (var i = 0; i < DECISION_PATTERNS.length; i++) {
308
+ if (DECISION_PATTERNS[i].test(text)) return true;
309
+ }
310
+ return false;
311
+ }
312
+
313
+ module.exports = { getToolDefs: getToolDefs };
package/lib/daemon.js CHANGED
@@ -985,8 +985,10 @@ if (usersModule.isMultiUser()) {
985
985
  var mateSlug = "mate-" + m.id;
986
986
  var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
987
987
  if (fs.existsSync(mateDir)) {
988
- console.log("[daemon] Adding mate project:", mateSlug);
989
- relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName });
988
+ var mateDef = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
989
+ var isHost = !!(mateDef && mateDef.hostAgent);
990
+ console.log("[daemon] Adding mate project:", mateSlug + (isHost ? " (host agent)" : ""));
991
+ relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName, isHostAgent: isHost });
990
992
  }
991
993
  }
992
994
  }
@@ -1000,8 +1002,10 @@ if (usersModule.isMultiUser()) {
1000
1002
  var mateSlug = "mate-" + m.id;
1001
1003
  var mateName = (m.profile && m.profile.displayName) || m.name || "New Mate";
1002
1004
  if (fs.existsSync(mateDir)) {
1003
- console.log("[daemon] Adding mate project:", mateSlug);
1004
- relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName });
1005
+ var mateDef2 = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
1006
+ var isHost2 = !!(mateDef2 && mateDef2.hostAgent);
1007
+ console.log("[daemon] Adding mate project:", mateSlug + (isHost2 ? " (host agent)" : ""));
1008
+ relay.addProject(mateDir, mateSlug, mateName, null, m.createdBy, null, { isMate: true, mateDisplayName: mateName, isHostAgent: isHost2 });
1005
1009
  }
1006
1010
  }
1007
1011
  }
package/lib/mates.js CHANGED
@@ -570,9 +570,32 @@ function ensureBuiltinMates(ctx, deletedKeys) {
570
570
  created.push(createBuiltinMate(ctx, missing[i]));
571
571
  }
572
572
  }
573
+ // Demote any archived built-ins that the user already has installed:
574
+ // strip primary/globalSearch flags and set archived=true on the mate
575
+ // object so the active-list filter hides them. Conversation history is
576
+ // untouched. This runs every startup so the transition lands even on
577
+ // users who logged in once when Ally was primary.
578
+ syncArchivedBuiltinMates(ctx);
573
579
  return created;
574
580
  }
575
581
 
582
+ function syncArchivedBuiltinMates(ctx) {
583
+ var builtinMates = require("./builtin-mates");
584
+ var archivedKeys = builtinMates.getArchivedBuiltinKeys();
585
+ if (archivedKeys.length === 0) return;
586
+ var data = loadMates(ctx);
587
+ var changed = false;
588
+ for (var i = 0; i < data.mates.length; i++) {
589
+ var m = data.mates[i];
590
+ if (!m.builtinKey) continue;
591
+ if (archivedKeys.indexOf(m.builtinKey) === -1) continue;
592
+ if (!m.archived) { m.archived = true; changed = true; }
593
+ if (m.primary) { m.primary = false; changed = true; }
594
+ if (m.globalSearch) { m.globalSearch = false; changed = true; }
595
+ }
596
+ if (changed) saveMates(ctx, data);
597
+ }
598
+
576
599
  /**
577
600
  * Sync primary mates with their latest code definition.
578
601
  *
@@ -743,5 +766,6 @@ module.exports = {
743
766
  getInstalledBuiltinKeys: getInstalledBuiltinKeys,
744
767
  getMissingBuiltinKeys: getMissingBuiltinKeys,
745
768
  ensureBuiltinMates: ensureBuiltinMates,
769
+ syncArchivedBuiltinMates: syncArchivedBuiltinMates,
746
770
  syncPrimaryMates: syncPrimaryMates,
747
771
  };
package/lib/project.js CHANGED
@@ -144,6 +144,8 @@ function createProjectContext(opts) {
144
144
  var getProjectCount = opts.getProjectCount || function () { return 1; };
145
145
  var getProjectList = opts.getProjectList || function () { return []; };
146
146
  var getAllProjectSessions = opts.getAllProjectSessions || function () { return []; };
147
+ var getAllProjectsWithSessions = opts.getAllProjectsWithSessions || function () { return []; };
148
+ var isHostAgent = !!opts.isHostAgent;
147
149
  var getHubSchedules = opts.getHubSchedules || function () { return []; };
148
150
  var moveScheduleToProject = opts.moveScheduleToProject || function () { return { ok: false, error: "Not supported" }; };
149
151
  var moveAllSchedulesToProject = opts.moveAllSchedulesToProject || function () { return { ok: false, error: "Not supported" }; };
@@ -506,6 +508,23 @@ function createProjectContext(opts) {
506
508
  console.error("[project] Failed to create debate MCP server:", e.message);
507
509
  }
508
510
 
511
+ // Clay-history MCP server (host agent only — Clay mate)
512
+ // Gives Clay BM25 search + targeted reads across the user's full
513
+ // workspace. Read-only. Other Mates and project sessions never see
514
+ // these tools because the gate is the isHostAgent project flag.
515
+ if (isHostAgent) {
516
+ try {
517
+ var clayHistoryMcp = require("./clay-history-mcp-server");
518
+ var clayHistoryToolDefs = clayHistoryMcp.getToolDefs({
519
+ getAllProjectsWithSessions: getAllProjectsWithSessions,
520
+ });
521
+ var clayHistoryMcpConfig = adapter.createToolServer({ name: "clay-history", version: "1.0.0", tools: clayHistoryToolDefs });
522
+ if (clayHistoryMcpConfig) servers[clayHistoryMcpConfig.name || "clay-history"] = clayHistoryMcpConfig;
523
+ } catch (e) {
524
+ console.error("[project] Failed to create clay-history MCP server:", e.message);
525
+ }
526
+ }
527
+
509
528
  // Ask-user MCP server (mates only)
510
529
  if (isMate) {
511
530
  try {
@@ -16,6 +16,39 @@
16
16
  border-top-left-radius: 8px;
17
17
  animation: hubFadeIn 0.35s ease;
18
18
  }
19
+
20
+ /* Clay home: split layout. The hub sits on the right half as a side panel,
21
+ the chat (Clay DM) occupies the left. The body class is toggled by
22
+ showHomeHub / hideHomeHub when a Clay mate exists for the user. */
23
+ body.clay-home-split #home-hub {
24
+ inset: 0 0 0 auto;
25
+ width: 50%;
26
+ min-width: 360px;
27
+ max-width: 720px;
28
+ border-left: 1px solid var(--border);
29
+ z-index: 5; /* sit alongside the chat, not over it */
30
+ padding: 32px 20px 32px;
31
+ border-top-left-radius: 0;
32
+ animation: hubSlideInRight 0.25s ease;
33
+ }
34
+ @media (max-width: 900px) {
35
+ /* Below 900px there isn't room for a meaningful split; collapse back
36
+ to the legacy full-overlay hub for narrow viewports. */
37
+ body.clay-home-split #home-hub {
38
+ inset: 0;
39
+ width: auto;
40
+ max-width: none;
41
+ min-width: 0;
42
+ border-left: none;
43
+ z-index: 200;
44
+ padding: 48px 24px 40px;
45
+ border-top-left-radius: 8px;
46
+ }
47
+ }
48
+ @keyframes hubSlideInRight {
49
+ from { transform: translateX(8px); opacity: 0; }
50
+ to { transform: translateX(0); opacity: 1; }
51
+ }
19
52
  /* Close button (X / ESC) */
20
53
  .home-hub-close-btn {
21
54
  position: absolute;
@@ -71,7 +71,7 @@ export function initDm() {
71
71
  }
72
72
  }
73
73
 
74
- export function openDm(targetUserId) {
74
+ export function openDm(targetUserId, opts) {
75
75
  var ws = getWs();
76
76
  if (!ws || ws.readyState !== 1) return;
77
77
  // Persist DM state for refresh recovery
@@ -81,7 +81,8 @@ export function openDm(targetUserId) {
81
81
  // Showing onboarding + gating a skill version check here caused the
82
82
  // "Skill Installation Required" modal to pop on every refresh / project
83
83
  // switch via the localStorage DM-restore fallback in app-connection.js.
84
- if (typeof targetUserId === "string" && targetUserId.indexOf("mate_") === 0) {
84
+ var skipOnboarding = !!(opts && opts.skipOnboarding);
85
+ if (!skipOnboarding && typeof targetUserId === "string" && targetUserId.indexOf("mate_") === 0) {
85
86
  showMateOnboarding(function () {
86
87
  var ws2 = getWs();
87
88
  if (ws2) ws2.send(JSON.stringify({ type: "dm_open", targetUserId: targetUserId }));
@@ -517,12 +517,20 @@ function renderHomeHubMates() {
517
517
  var container = document.getElementById("home-hub-mates");
518
518
  if (!container) return;
519
519
  container.innerHTML = "";
520
- if (!store.get('cachedMatesList') || store.get('cachedMatesList').length === 0) {
520
+ // Hide archived mates (e.g. Ally after Clay took over) and Clay itself
521
+ // (the user is already chatting with Clay on the left half — listing
522
+ // Clay again on the right would be redundant).
523
+ var visibleMates = (store.get('cachedMatesList') || []).filter(function (m) {
524
+ if (!m || m.archived) return false;
525
+ if (m.builtinKey === "clay") return false;
526
+ return true;
527
+ });
528
+ if (visibleMates.length === 0) {
521
529
  container.classList.add("hidden");
522
530
  return;
523
531
  }
524
532
  container.classList.remove("hidden");
525
- for (var i = 0; i < store.get('cachedMatesList').length; i++) {
533
+ for (var i = 0; i < visibleMates.length; i++) {
526
534
  (function (mate) {
527
535
  var item = document.createElement("div");
528
536
  item.className = "home-hub-mate-item" + (mate.primary ? " home-hub-mate-primary" : "");
@@ -561,12 +569,32 @@ function renderHomeHubMates() {
561
569
  });
562
570
 
563
571
  container.appendChild(item);
564
- })(store.get('cachedMatesList')[i]);
572
+ })(visibleMates[i]);
565
573
  }
566
574
  }
567
575
 
568
576
  export function showHomeHub() {
569
- if (store.get('dmMode')) exitDmMode();
577
+ // Open Clay DM (the host agent) on the left while showing widgets on
578
+ // the right. The body class drives a split layout — see home-hub.css.
579
+ // Existing users without a Clay mate yet (cachedMatesList not yet
580
+ // delivered, or syncArchivedBuiltinMates hasn't run) fall back to the
581
+ // legacy full-screen hub so the home button never feels broken.
582
+ var clayMate = findClayMate();
583
+ if (clayMate) {
584
+ document.body.classList.add("clay-home-split");
585
+ var dmTarget = store.get('dmTargetUser');
586
+ var inClayDm = store.get('dmMode') && dmTarget && dmTarget.id === clayMate.id;
587
+ if (!inClayDm) {
588
+ // Open Clay DM. Skip the generic mate-onboarding modal — Clay is
589
+ // the host agent, not a learn-about-Mates moment; that intro
590
+ // fires the first time the user opens a regular Mate.
591
+ openDm(clayMate.id, { skipOnboarding: true });
592
+ }
593
+ } else {
594
+ // Fallback: legacy behavior (full overlay, exit any DM).
595
+ document.body.classList.remove("clay-home-split");
596
+ if (store.get('dmMode')) exitDmMode();
597
+ }
570
598
  homeHubVisible = true;
571
599
  homeHub.classList.remove("hidden");
572
600
  // Show close button only if there's a project to return to
@@ -602,7 +630,19 @@ export function hideHomeHub() {
602
630
  if (!homeHubVisible) return;
603
631
  homeHubVisible = false;
604
632
  homeHub.classList.add("hidden");
633
+ document.body.classList.remove("clay-home-split");
605
634
  stopTipRotation();
606
635
  var mobileHome = document.getElementById("mobile-home-btn");
607
636
  if (mobileHome) mobileHome.classList.remove("active");
608
637
  }
638
+
639
+ // Locate the user's Clay (host agent) mate from the cached list. Returns
640
+ // null if cachedMatesList hasn't arrived yet or the user predates Clay.
641
+ function findClayMate() {
642
+ var list = store.get('cachedMatesList');
643
+ if (!list || !list.length) return null;
644
+ for (var i = 0; i < list.length; i++) {
645
+ if (list[i] && list[i].builtinKey === "clay") return list[i];
646
+ }
647
+ return null;
648
+ }
@@ -384,12 +384,17 @@ export function renderUserStrip(allUsers, onlineUserIds, myUserId, dmFavorites,
384
384
  // multi-user lets users curate the icon strip via favorites; the full
385
385
  // mate list is still reachable from the DM picker.
386
386
  var favoriteMates = cachedMates.filter(function (m) {
387
+ if (m.archived) return false;
387
388
  if (cachedDmRemovedUsers[m.id]) return false;
388
389
  if (cachedDmFavorites.indexOf(m.id) !== -1) return true;
389
390
  if (cachedDmUnread[m.id] && cachedDmUnread[m.id] > 0) return true;
390
391
  return false;
391
392
  });
392
393
  var sortedMates = favoriteMates.sort(function (a, b) {
394
+ // Clay (host agent) pins to the top, then other built-ins, then user mates.
395
+ var aClay = a.builtinKey === "clay" ? 1 : 0;
396
+ var bClay = b.builtinKey === "clay" ? 1 : 0;
397
+ if (aClay !== bClay) return bClay - aClay;
393
398
  var aBuiltin = a.builtinKey ? 1 : 0;
394
399
  var bBuiltin = b.builtinKey ? 1 : 0;
395
400
  if (aBuiltin !== bBuiltin) return bBuiltin - aBuiltin;
package/lib/sdk-bridge.js CHANGED
@@ -489,6 +489,13 @@ function createSDKBridge(opts) {
489
489
  return { behavior: "allow", updatedInput: input };
490
490
  }
491
491
 
492
+ // Auto-approve clay-history tools. Mounted only on Clay (host agent)
493
+ // sessions and strictly read-only by design — search and read across
494
+ // the user's own workspace data.
495
+ if (toolName.indexOf("mcp__clay-history__") === 0) {
496
+ return { behavior: "allow", updatedInput: input };
497
+ }
498
+
492
499
  // Auto-approve remote MCP tools that the user explicitly enabled in project settings.
493
500
  // These are user-owned local MCP servers, so no additional permission prompt needed.
494
501
  if (toolName.indexOf("mcp__") === 0 && getRemoteMcpServers) {
@@ -103,7 +103,9 @@ function attachMates(ctx) {
103
103
  var nbSlug = "mate-" + nb.id;
104
104
  var nbDir = mates.getMateDir(mateCtx5, nb.id);
105
105
  var nbName = (nb.profile && nb.profile.displayName) || nb.name || "New Mate";
106
- addProject(nbDir, nbSlug, nbName, null, nb.createdBy || userId, null, { isMate: true, mateDisplayName: nbName });
106
+ var nbDef = nb.builtinKey ? require("./builtin-mates").getBuiltinByKey(nb.builtinKey) : null;
107
+ var nbIsHost = !!(nbDef && nbDef.hostAgent);
108
+ addProject(nbDir, nbSlug, nbName, null, nb.createdBy || userId, null, { isMate: true, mateDisplayName: nbName, isHostAgent: nbIsHost });
107
109
  users.addDmFavorite(userId, nb.id);
108
110
  }
109
111
  } catch (e) {
@@ -111,9 +113,11 @@ function attachMates(ctx) {
111
113
  }
112
114
  // Auto-sync primary mates (Ally) with latest definition
113
115
  try { mates.syncPrimaryMates(mateCtx5); } catch (e) {}
116
+ // Auto-archive Ally and any other archived built-ins for existing users
117
+ try { mates.syncArchivedBuiltinMates(mateCtx5); } catch (e) {}
114
118
  // Ensure core built-in mates are in favorites (unless user explicitly removed them)
115
- // Only auto-favorite the core 3: Ally (chief of staff), Arch (architect), Buzz (marketer)
116
- var coreMateKeys = ["ally", "arch", "buzz"];
119
+ // Auto-favorites: Clay (host agent), Arch (architect), Buzz (marketer)
120
+ var coreMateKeys = ["clay", "arch", "buzz"];
117
121
  var mateList = mates.getAllMates(mateCtx5);
118
122
  var currentFavs = users.getDmFavorites(userId);
119
123
  var hiddenIds = users.getDmHidden(userId);
@@ -130,7 +134,9 @@ function attachMates(ctx) {
130
134
  var mDir = mates.getMateDir(mateCtx5, m.id);
131
135
  fs.mkdirSync(mDir, { recursive: true });
132
136
  var mName = (m.profile && m.profile.displayName) || m.name || "New Mate";
133
- addProject(mDir, mSlug, mName, null, m.createdBy || userId, null, { isMate: true, mateDisplayName: mName });
137
+ var mDef = m.builtinKey ? require("./builtin-mates").getBuiltinByKey(m.builtinKey) : null;
138
+ var mIsHost = !!(mDef && mDef.hostAgent);
139
+ addProject(mDir, mSlug, mName, null, m.createdBy || userId, null, { isMate: true, mateDisplayName: mName, isHostAgent: mIsHost });
134
140
  }
135
141
  }
136
142
  // Include deleted built-in mates for re-add UI
package/lib/server.js CHANGED
@@ -880,6 +880,7 @@ function createServer(opts) {
880
880
  worktreeMeta: worktreeMeta || null,
881
881
  isMate: extra.isMate || false,
882
882
  mateDisplayName: extra.mateDisplayName || "",
883
+ isHostAgent: !!extra.isHostAgent,
883
884
  pushModule: pushModule,
884
885
  debug: debug,
885
886
  dangerouslySkipPermissions: dangerouslySkipPermissions,
@@ -920,6 +921,34 @@ function createServer(opts) {
920
921
  });
921
922
  return allSessions;
922
923
  },
924
+ // Like getAllProjectSessions but returns the per-project grouped
925
+ // shape that session-search.searchPalette expects. Includes self
926
+ // (so the host agent can search its own past Clay conversations).
927
+ // Used by clay-history-mcp-server.
928
+ getAllProjectsWithSessions: function () {
929
+ var out = [];
930
+ projects.forEach(function (pCtx, pSlug) {
931
+ var status = pCtx.getStatus();
932
+ if (status.isWorktree) return;
933
+ var pSm = pCtx.getSessionManager();
934
+ if (!pSm) return;
935
+ var sessions = [];
936
+ pSm.sessions.forEach(function (s) {
937
+ if (s.hidden) return;
938
+ sessions.push(s);
939
+ });
940
+ if (sessions.length === 0) return;
941
+ out.push({
942
+ projectSlug: pSlug,
943
+ projectTitle: status.title || status.project || pSlug,
944
+ projectIcon: status.icon || null,
945
+ isMate: !!status.isMate,
946
+ mateId: status.mateId || null,
947
+ sessions: sessions,
948
+ });
949
+ });
950
+ return out;
951
+ },
923
952
  getHubSchedules: function () {
924
953
  var allSchedules = [];
925
954
  projects.forEach(function (ctx, s) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "2.37.0",
3
+ "version": "2.38.0-beta.1",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js",