clay-server 2.37.0-beta.2 → 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.
- package/lib/builtin-mates.js +128 -8
- package/lib/clay-history-mcp-server.js +313 -0
- package/lib/daemon.js +8 -4
- package/lib/mates.js +24 -0
- package/lib/project.js +19 -0
- package/lib/public/css/home-hub.css +33 -0
- package/lib/public/modules/app-dm.js +3 -2
- package/lib/public/modules/app-home-hub.js +44 -4
- package/lib/public/modules/sidebar-mates.js +5 -0
- package/lib/sdk-bridge.js +7 -0
- package/lib/server-mates.js +10 -4
- package/lib/server.js +29 -0
- package/package.json +1 -1
package/lib/builtin-mates.js
CHANGED
|
@@ -8,7 +8,42 @@
|
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
10
|
var BUILTIN_MATES = [
|
|
11
|
-
// ----
|
|
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
|
-
//
|
|
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
|
-
|
|
989
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 <
|
|
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
|
-
})(
|
|
572
|
+
})(visibleMates[i]);
|
|
565
573
|
}
|
|
566
574
|
}
|
|
567
575
|
|
|
568
576
|
export function showHomeHub() {
|
|
569
|
-
|
|
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) {
|
package/lib/server-mates.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
116
|
-
var coreMateKeys = ["
|
|
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
|
-
|
|
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