clay-server 2.14.0 → 2.15.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/README.md CHANGED
@@ -4,18 +4,11 @@
4
4
  <img src="media/phone.gif" alt="Clay on phone" width="300">
5
5
  </p>
6
6
 
7
- <h3 align="center">A multi-user web UI for Claude Code, built on the Agent SDK.</h3>
7
+ <h3 align="center">Turn Claude Code into a team workspace. Any device, one command.</h3>
8
8
 
9
9
  [![npm version](https://img.shields.io/npm/v/clay-server)](https://www.npmjs.com/package/clay-server) [![npm downloads](https://img.shields.io/npm/dw/clay-server)](https://www.npmjs.com/package/clay-server) [![GitHub stars](https://img.shields.io/github/stars/chadbyte/clay)](https://github.com/chadbyte/clay) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/chadbyte/clay/blob/main/LICENSE)
10
10
 
11
- Clay extends Claude Code from a single-user CLI into a multi-user, multi-session web platform. It runs locally as a daemon, serves a browser UI over WebSocket, and lets non-technical teammates use Claude Code without touching the terminal.
12
-
13
- No relay server in the cloud. Your machine is the server. Zero install — one command:
14
-
15
- ```bash
16
- npx clay-server
17
- # Scan the QR code to connect from any device
18
- ```
11
+ Clay gives Claude Code a browser UI that runs on any device. Use it from your phone, run it on macOS, Windows, or Linux. Invite teammates, manage multiple projects from one sidebar, and get push notifications when Claude needs you. Built on the official Claude Agent SDK, not a terminal parser. Your machine is the server. No cloud relay in between, no extra network surface.
19
12
 
20
13
  ---
21
14
 
@@ -65,6 +58,18 @@ If someone gets stuck, join their session to unblock them in real time. Permissi
65
58
 
66
59
  ---
67
60
 
61
+ ## Mates
62
+
63
+ Build your team, even if you're solo.
64
+
65
+ Mates are AI teammates you create through conversation. Interview them, give them a name, avatar, and role. A code reviewer, a marketing lead, a writing partner. They remember how you work together and carry context across sessions.
66
+
67
+ They sit in your sidebar next to your human teammates. DM them, bring them into projects, let them work autonomously. In a multi-user workspace, your whole team, human and AI, works in one place.
68
+
69
+ <!-- screenshot: sidebar with Mates visible -->
70
+
71
+ ---
72
+
68
73
  ## Mobile & notifications
69
74
 
70
75
  Phone, tablet, couch. All you need is a browser.
@@ -85,15 +90,13 @@ The scheduler kicks off agents at set times.
85
90
  Have it check open issues and submit PRs every morning at 8 AM.
86
91
  Or compile world news and email you a digest every day.
87
92
 
88
- Take it further with Ralph Loop autonomous iteration built into Clay. Define a task (`PROMPT.md`) and success criteria (`JUDGE.md`), then start the loop. The agent works, commits, and a judge evaluates the git diff as PASS or FAIL. On FAIL, a fresh session starts over. Each iteration has **no memory of previous conversations** only the code carries over. Based on [Geoffrey Huntley's Ralph Wiggum technique](https://ghuntley.com/loop/).
93
+ Take it further with Ralph Loop, an autonomous coding loop built into Clay. The agent works, commits, and a judge evaluates. If it fails, a fresh session starts over with no memory of the previous attempt. Only the code carries over. Based on [Geoffrey Huntley's Ralph Wiggum technique](https://ghuntley.com/loop/).
89
94
 
90
95
  ---
91
96
 
92
97
  ## Security & Privacy
93
98
 
94
- Clay turns your machine into the relay server. There is no intermediary server on the internet. No third-party service sits between your browser and your code.
95
-
96
- The best relay server to trust is the one that doesn't exist. Clay has none. Your data flows directly from your machine to the Anthropic API — exactly as it does when you use the CLI. No one intercepts, collects, or reroutes it. Clay adds a browser layer on top, not a middleman.
99
+ Your data flows directly from your machine to the Anthropic API, exactly as it does when you use the CLI. Clay adds a browser layer on top, not a middleman.
97
100
 
98
101
  PIN authentication, per-project/session permissions, and HTTPS are supported by default. For local network use, this is sufficient. For remote access, we recommend a VPN like Tailscale.
99
102
 
@@ -119,11 +122,12 @@ PIN authentication, per-project/session permissions, and HTTPS are supported by
119
122
 
120
123
  ## Key Features
121
124
 
125
+ * **Mates** - AI teammates with persistent identity, context, and memory. Create through an interview, DM them, collaborate across sessions.
122
126
  * **Multi-user** - Accounts, invitations, per-project/session permissions, real-time presence.
123
127
  * **Multi-agent** - Parallel agents per project, sidebar switching.
124
128
  * **Push notifications** - Approval, completion, error. Native-like PWA experience.
125
129
  * **Scheduler** - Cron-based automatic agent execution.
126
- * **Ralph Loop** - Autonomous coding loop with PROMPT.md + JUDGE.md. Iterates until the judge says PASS.
130
+ * **Ralph Loop** - Autonomous coding loop. The agent works, a judge evaluates, and it iterates until it passes.
127
131
  * **File browser** - File exploration, syntax highlighting, live reload.
128
132
  * **Built-in terminal** - Multi-tab terminal, mobile keyboard support.
129
133
  * **Session search** - Full-text search across all conversation history.
@@ -136,47 +140,29 @@ PIN authentication, per-project/session permissions, and HTTPS are supported by
136
140
  **"Is this just a terminal wrapper?"**
137
141
  No. Clay runs on the Claude Agent SDK. It doesn't wrap terminal output. It communicates directly with the agent through the SDK.
138
142
 
139
- **"Is it free? Open source?"**
140
- Free. MIT-licensed open source. All code is public.
141
-
142
- **"Do I need to install anything?"**
143
- No. `npx clay-server` runs it directly. No global install, no build step, no Docker. Node.js and Claude Code are the only prerequisites.
144
-
145
143
  **"Does my code leave my machine?"**
146
144
  The Clay server runs locally. Files stay local. Only Claude API calls go out, which is the same as using the CLI.
147
145
 
148
- **"Is it secure?"**
149
- PIN authentication, per-project/session permissions, and HTTPS are supported by default. See [Security & Privacy](#security--privacy) for details.
150
-
151
- **"What OS does it run on?"**
152
- Windows, Linux, and macOS are all supported.
153
-
154
146
  **"Can I continue a CLI session?"**
155
147
  Yes. Pick up a CLI session in the browser, or continue a browser session in the CLI.
156
148
 
157
149
  **"Does my existing CLAUDE.md work?"**
158
150
  Yes. If your project has a CLAUDE.md, it works in Clay as-is.
159
151
 
160
- **"Can I use it as an app?"**
161
- PWA is supported. On mobile, tap the download icon in the top-left corner to open the PWA install guide. Once installed, it provides a native-like experience.
162
-
163
- **"Can I use the terminal on mobile?"**
164
- Yes. Clay provides a built-in terminal with mobile keyboard support.
165
-
166
152
  **"Does each teammate need their own API key?"**
167
153
  No. Teammates share the Claude Code session logged in on the server. If needed, you can configure per-project environment variables to use different API keys.
168
154
 
169
155
  **"Does it work with MCP servers?"**
170
156
  Yes. MCP configurations from the CLI carry over as-is.
171
157
 
172
- **"How do I update?"**
173
- Auto-update is supported. When a new version is available, apply it with one click.
158
+ **"What are Mates?"**
159
+ AI teammates you create through a conversation. Each Mate has a name, avatar, personality, and persistent memory. They live in your sidebar and you can DM them or bring them into projects.
174
160
 
175
- **"Can I use other AI models besides Claude?"**
176
- Currently Clay is Claude Code only. Other model support is on the roadmap.
161
+ **"How is a Mate different from Claude Projects?"**
162
+ Claude Projects save prompts and files as context. A Mate is a teammate. It has its own identity formed through an interview, remembers how you work together across sessions, and exists alongside your human teammates in the workspace. You're not organizing prompts. You're building a team.
177
163
 
178
- **"Can I run it in Docker?"**
179
- There's no official Docker image yet, but it can run in a container with a Node.js environment.
164
+ **"Can I create multiple Mates?"**
165
+ Yes. Create as many as you need. A code reviewer, a writing partner, a project manager. Each one is independent.
180
166
 
181
167
  ---
182
168
 
@@ -222,8 +208,7 @@ npx clay-server --dev # Dev mode (foreground, auto-restart on lib/ change
222
208
 
223
209
  ## Architecture
224
210
 
225
- Clay is not a wrapper that intercepts stdio.
226
- It's a local relay server that drives Claude Code execution through the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) and streams it to the browser over WebSocket.
211
+ Clay drives Claude Code execution through the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) and streams it to the browser over WebSocket.
227
212
 
228
213
  ```mermaid
229
214
  graph LR
package/lib/mates.js CHANGED
@@ -98,6 +98,7 @@ function createMate(ctx, seedData) {
98
98
  avatarStyle: "bottts",
99
99
  avatarSeed: crypto.randomBytes(4).toString("hex"),
100
100
  },
101
+ bio: null,
101
102
  status: "interviewing",
102
103
  interviewProjectPath: null,
103
104
  };
@@ -133,6 +134,7 @@ function createMate(ctx, seedData) {
133
134
  claudeMd += "- Communication: " + seedData.communicationStyle.join(", ") + "\n";
134
135
  }
135
136
  claudeMd += "- Autonomy: " + (seedData.autonomy || "always_ask") + "\n";
137
+ claudeMd += TEAM_SECTION;
136
138
  claudeMd += crisisSafety.getSection();
137
139
  fs.writeFileSync(path.join(mateDir, "CLAUDE.md"), claudeMd);
138
140
 
@@ -264,6 +266,155 @@ function migrateLegacyMates() {
264
266
  }
265
267
  }
266
268
 
269
+ // --- Team awareness ---
270
+
271
+ var TEAM_MARKER = "<!-- TEAM_AWARENESS_MANAGED_BY_SYSTEM -->";
272
+
273
+ var TEAM_SECTION =
274
+ "\n\n" + TEAM_MARKER + "\n" +
275
+ "## Your Team\n\n" +
276
+ "**This section is managed by the system and cannot be removed.**\n\n" +
277
+ "You are one of several AI Mates in this workspace. Your teammates and their profiles are listed in `../mates.json`. " +
278
+ "Each teammate's identity and working style is described in their own directory:\n\n" +
279
+ "- `../{mate_id}/CLAUDE.md` — their identity, personality, and working style\n" +
280
+ "- `../{mate_id}/mate.yaml` — their metadata (name, role, status, activities)\n" +
281
+ "- `../common-knowledge.json` — shared knowledge registry; files listed here are readable by all mates\n\n" +
282
+ "Check the team registry when it would be relevant to know who else is available or what they do. " +
283
+ "You cannot message other Mates directly yet, but knowing your team helps you work with the user more effectively.\n";
284
+
285
+ function hasTeamSection(content) {
286
+ return content.indexOf(TEAM_MARKER) !== -1;
287
+ }
288
+
289
+ /**
290
+ * Enforce the team awareness section on a mate's CLAUDE.md.
291
+ * Inserts before the crisis safety section (if present), or appends at the end.
292
+ * Returns true if the file was modified.
293
+ */
294
+ function enforceTeamAwareness(filePath) {
295
+ if (!fs.existsSync(filePath)) return false;
296
+
297
+ var content = fs.readFileSync(filePath, "utf8");
298
+
299
+ // Check if already present and correct
300
+ var teamIdx = content.indexOf(TEAM_MARKER);
301
+ if (teamIdx !== -1) {
302
+ // Extract existing team section (up to next system marker or ## heading)
303
+ var afterTeam = content.substring(teamIdx);
304
+ var crisisIdx = afterTeam.indexOf(crisisSafety.MARKER);
305
+ var existing;
306
+ if (crisisIdx !== -1) {
307
+ existing = afterTeam.substring(0, crisisIdx).trimEnd();
308
+ } else {
309
+ existing = afterTeam.trimEnd();
310
+ }
311
+ if (existing === TEAM_SECTION.trimStart().trimEnd()) return false; // already correct
312
+
313
+ // Strip the existing team section
314
+ var endOfTeam = crisisIdx !== -1 ? teamIdx + crisisIdx : content.length;
315
+ content = content.substring(0, teamIdx).trimEnd() + content.substring(endOfTeam);
316
+ }
317
+
318
+ // Insert before crisis safety section if present, otherwise append
319
+ var crisisPos = content.indexOf(crisisSafety.MARKER);
320
+ if (crisisPos !== -1) {
321
+ content = content.substring(0, crisisPos).trimEnd() + TEAM_SECTION + "\n\n" + content.substring(crisisPos);
322
+ } else {
323
+ content = content.trimEnd() + TEAM_SECTION;
324
+ }
325
+
326
+ fs.writeFileSync(filePath, content, "utf8");
327
+ return true;
328
+ }
329
+
330
+ // --- Common knowledge registry ---
331
+
332
+ function commonKnowledgePath(ctx) {
333
+ return path.join(resolveMatesRoot(ctx), "common-knowledge.json");
334
+ }
335
+
336
+ function loadCommonKnowledge(ctx) {
337
+ try {
338
+ var raw = fs.readFileSync(commonKnowledgePath(ctx), "utf8");
339
+ return JSON.parse(raw);
340
+ } catch (e) {
341
+ return [];
342
+ }
343
+ }
344
+
345
+ function saveCommonKnowledge(ctx, entries) {
346
+ var filePath = commonKnowledgePath(ctx);
347
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
348
+ var tmpPath = filePath + ".tmp";
349
+ fs.writeFileSync(tmpPath, JSON.stringify(entries, null, 2));
350
+ fs.renameSync(tmpPath, filePath);
351
+ }
352
+
353
+ function promoteKnowledge(ctx, mateId, mateName, fileName) {
354
+ var entries = loadCommonKnowledge(ctx);
355
+ // Check if already promoted (same mateId + name)
356
+ for (var i = 0; i < entries.length; i++) {
357
+ if (entries[i].mateId === mateId && entries[i].name === fileName) {
358
+ return entries; // already promoted
359
+ }
360
+ }
361
+ entries.push({
362
+ name: fileName,
363
+ mateId: mateId,
364
+ mateName: mateName || null,
365
+ promotedAt: Date.now()
366
+ });
367
+ saveCommonKnowledge(ctx, entries);
368
+ return entries;
369
+ }
370
+
371
+ function depromoteKnowledge(ctx, mateId, fileName) {
372
+ var entries = loadCommonKnowledge(ctx);
373
+ entries = entries.filter(function (e) {
374
+ return !(e.mateId === mateId && e.name === fileName);
375
+ });
376
+ saveCommonKnowledge(ctx, entries);
377
+ return entries;
378
+ }
379
+
380
+ function getCommonKnowledgeForMate(ctx, mateId) {
381
+ var entries = loadCommonKnowledge(ctx);
382
+ var root = resolveMatesRoot(ctx);
383
+ var result = [];
384
+ for (var i = 0; i < entries.length; i++) {
385
+ var e = entries[i];
386
+ var filePath = path.join(root, e.mateId, "knowledge", e.name);
387
+ try {
388
+ var stat = fs.statSync(filePath);
389
+ result.push({
390
+ name: e.name,
391
+ size: stat.size,
392
+ mtime: stat.mtimeMs,
393
+ common: true,
394
+ ownMateId: e.mateId,
395
+ ownerName: e.mateName
396
+ });
397
+ } catch (err) {
398
+ // Source file deleted, skip (could clean up registry but not critical)
399
+ }
400
+ }
401
+ return result;
402
+ }
403
+
404
+ function readCommonKnowledgeFile(ctx, mateId, fileName) {
405
+ var root = resolveMatesRoot(ctx);
406
+ var filePath = path.join(root, mateId, "knowledge", path.basename(fileName));
407
+ return fs.readFileSync(filePath, "utf8");
408
+ }
409
+
410
+ function isPromoted(ctx, mateId, fileName) {
411
+ var entries = loadCommonKnowledge(ctx);
412
+ for (var i = 0; i < entries.length; i++) {
413
+ if (entries[i].mateId === mateId && entries[i].name === fileName) return true;
414
+ }
415
+ return false;
416
+ }
417
+
267
418
  // Format seed data as a human-readable context string
268
419
  function formatSeedContext(seedData) {
269
420
  if (!seedData) return "";
@@ -318,4 +469,12 @@ module.exports = {
318
469
  getMateDir: getMateDir,
319
470
  migrateLegacyMates: migrateLegacyMates,
320
471
  formatSeedContext: formatSeedContext,
472
+ enforceTeamAwareness: enforceTeamAwareness,
473
+ TEAM_MARKER: TEAM_MARKER,
474
+ loadCommonKnowledge: loadCommonKnowledge,
475
+ promoteKnowledge: promoteKnowledge,
476
+ depromoteKnowledge: depromoteKnowledge,
477
+ getCommonKnowledgeForMate: getCommonKnowledgeForMate,
478
+ readCommonKnowledgeFile: readCommonKnowledgeFile,
479
+ isPromoted: isPromoted,
321
480
  };
package/lib/project.js CHANGED
@@ -12,6 +12,7 @@ var { createLoopRegistry } = require("./scheduler");
12
12
  var usersModule = require("./users");
13
13
  var { resolveOsUserInfo, fsAsUser } = require("./os-users");
14
14
  var crisisSafety = require("./crisis-safety");
15
+ var matesModule = require("./mates");
15
16
  var MAX_UPLOAD_BYTES = 50 * 1024 * 1024; // 50 MB
16
17
 
17
18
  // Validate environment variable string (KEY=VALUE per line)
@@ -1287,11 +1288,30 @@ function createProjectContext(opts) {
1287
1288
  for (var ki = 0; ki < entries.length; ki++) {
1288
1289
  if (entries[ki].endsWith(".md") || entries[ki].endsWith(".jsonl")) {
1289
1290
  var stat = fs.statSync(path.join(knowledgeDir, entries[ki]));
1290
- files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs });
1291
+ files.push({ name: entries[ki], size: stat.size, mtime: stat.mtimeMs, common: false });
1291
1292
  }
1292
1293
  }
1293
1294
  } catch (e) { /* dir may not exist */ }
1294
1295
  files.sort(function (a, b) { return b.mtime - a.mtime; });
1296
+
1297
+ // For mate projects, check which files are promoted and include common files from other mates
1298
+ if (isMate) {
1299
+ var mateCtx = matesModule.buildMateCtx(opts.ownerId || null);
1300
+ var thisMateId = path.basename(cwd);
1301
+ // Tag promoted files
1302
+ for (var pi = 0; pi < files.length; pi++) {
1303
+ files[pi].promoted = matesModule.isPromoted(mateCtx, thisMateId, files[pi].name);
1304
+ }
1305
+ // Get common files from other mates
1306
+ var commonFiles = matesModule.getCommonKnowledgeForMate(mateCtx, thisMateId);
1307
+ // Filter out entries that belong to THIS mate (those are already in the list as promoted)
1308
+ for (var ci = 0; ci < commonFiles.length; ci++) {
1309
+ if (commonFiles[ci].ownMateId !== thisMateId) {
1310
+ files.push(commonFiles[ci]);
1311
+ }
1312
+ }
1313
+ }
1314
+
1295
1315
  sendTo(ws, { type: "knowledge_list", files: files });
1296
1316
  return;
1297
1317
  }
@@ -1299,12 +1319,24 @@ function createProjectContext(opts) {
1299
1319
  if (msg.type === "knowledge_read") {
1300
1320
  if (!msg.name) return;
1301
1321
  var safeName = path.basename(msg.name);
1302
- var filePath = path.join(cwd, "knowledge", safeName);
1303
- try {
1304
- var content = fs.readFileSync(filePath, "utf8");
1305
- sendTo(ws, { type: "knowledge_content", name: safeName, content: content });
1306
- } catch (e) {
1307
- sendTo(ws, { type: "knowledge_content", name: safeName, content: "", error: "File not found" });
1322
+ var filePath;
1323
+ if (msg.common && msg.ownMateId && isMate) {
1324
+ // Reading a common file from another mate
1325
+ var mateCtx = matesModule.buildMateCtx(opts.ownerId || null);
1326
+ try {
1327
+ var content = matesModule.readCommonKnowledgeFile(mateCtx, msg.ownMateId, safeName);
1328
+ sendTo(ws, { type: "knowledge_content", name: safeName, content: content, common: true, ownMateId: msg.ownMateId });
1329
+ } catch (e) {
1330
+ sendTo(ws, { type: "knowledge_content", name: safeName, content: "", error: "File not found", common: true });
1331
+ }
1332
+ } else {
1333
+ filePath = path.join(cwd, "knowledge", safeName);
1334
+ try {
1335
+ var content = fs.readFileSync(filePath, "utf8");
1336
+ sendTo(ws, { type: "knowledge_content", name: safeName, content: content });
1337
+ } catch (e) {
1338
+ sendTo(ws, { type: "knowledge_content", name: safeName, content: "", error: "File not found" });
1339
+ }
1308
1340
  }
1309
1341
  return;
1310
1342
  }
@@ -1328,6 +1360,19 @@ function createProjectContext(opts) {
1328
1360
  }
1329
1361
  } catch (e) {}
1330
1362
  files.sort(function (a, b) { return b.mtime - a.mtime; });
1363
+ // Tag files for mate projects
1364
+ if (isMate) {
1365
+ var mateCtx = matesModule.buildMateCtx(opts.ownerId || null);
1366
+ var thisMateId = path.basename(cwd);
1367
+ for (var pi = 0; pi < files.length; pi++) {
1368
+ files[pi].common = false;
1369
+ files[pi].promoted = matesModule.isPromoted(mateCtx, thisMateId, files[pi].name);
1370
+ }
1371
+ var commonFiles = matesModule.getCommonKnowledgeForMate(mateCtx, thisMateId);
1372
+ for (var ci = 0; ci < commonFiles.length; ci++) {
1373
+ if (commonFiles[ci].ownMateId !== thisMateId) files.push(commonFiles[ci]);
1374
+ }
1375
+ }
1331
1376
  sendTo(ws, { type: "knowledge_saved", name: safeName });
1332
1377
  sendTo(ws, { type: "knowledge_list", files: files });
1333
1378
  return;
@@ -1351,11 +1396,49 @@ function createProjectContext(opts) {
1351
1396
  }
1352
1397
  } catch (e) {}
1353
1398
  files.sort(function (a, b) { return b.mtime - a.mtime; });
1399
+ // Tag files for mate projects
1400
+ if (isMate) {
1401
+ var mateCtx = matesModule.buildMateCtx(opts.ownerId || null);
1402
+ var thisMateId = path.basename(cwd);
1403
+ for (var pi = 0; pi < files.length; pi++) {
1404
+ files[pi].common = false;
1405
+ files[pi].promoted = matesModule.isPromoted(mateCtx, thisMateId, files[pi].name);
1406
+ }
1407
+ var commonFiles = matesModule.getCommonKnowledgeForMate(mateCtx, thisMateId);
1408
+ for (var ci = 0; ci < commonFiles.length; ci++) {
1409
+ if (commonFiles[ci].ownMateId !== thisMateId) files.push(commonFiles[ci]);
1410
+ }
1411
+ }
1354
1412
  sendTo(ws, { type: "knowledge_deleted", name: safeName });
1355
1413
  sendTo(ws, { type: "knowledge_list", files: files });
1356
1414
  return;
1357
1415
  }
1358
1416
 
1417
+ if (msg.type === "knowledge_promote") {
1418
+ if (!isMate || !msg.name) return;
1419
+ var safeName = path.basename(msg.name);
1420
+ var mateCtx = matesModule.buildMateCtx(opts.ownerId || null);
1421
+ var thisMateId = path.basename(cwd);
1422
+ var mate = matesModule.getMate(mateCtx, thisMateId);
1423
+ var mateName = (mate && mate.name) || null;
1424
+ matesModule.promoteKnowledge(mateCtx, thisMateId, mateName, safeName);
1425
+ sendTo(ws, { type: "knowledge_promoted", name: safeName });
1426
+ // Re-send updated list (reuse knowledge_list logic)
1427
+ handleMessage(ws, { type: "knowledge_list" });
1428
+ return;
1429
+ }
1430
+
1431
+ if (msg.type === "knowledge_depromote") {
1432
+ if (!isMate || !msg.name) return;
1433
+ var safeName = path.basename(msg.name);
1434
+ var mateCtx = matesModule.buildMateCtx(opts.ownerId || null);
1435
+ var thisMateId = path.basename(cwd);
1436
+ matesModule.depromoteKnowledge(mateCtx, thisMateId, safeName);
1437
+ sendTo(ws, { type: "knowledge_depromoted", name: safeName });
1438
+ handleMessage(ws, { type: "knowledge_list" });
1439
+ return;
1440
+ }
1441
+
1359
1442
  if (msg.type === "push_subscribe") {
1360
1443
  if (pushModule && msg.subscription) pushModule.addSubscription(msg.subscription, msg.replaceEndpoint);
1361
1444
  return;
@@ -3911,10 +3994,11 @@ function createProjectContext(opts) {
3911
3994
  icon = newIcon || null;
3912
3995
  }
3913
3996
 
3914
- // Mate projects: watch CLAUDE.md and enforce crisis safety section
3997
+ // Mate projects: watch CLAUDE.md and enforce system-managed sections
3915
3998
  if (isMate) {
3916
3999
  var claudeMdPath = path.join(cwd, "CLAUDE.md");
3917
4000
  // Enforce immediately on startup
4001
+ try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
3918
4002
  try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
3919
4003
  // Watch for changes
3920
4004
  try {
@@ -3922,6 +4006,7 @@ function createProjectContext(opts) {
3922
4006
  if (crisisDebounce) clearTimeout(crisisDebounce);
3923
4007
  crisisDebounce = setTimeout(function () {
3924
4008
  crisisDebounce = null;
4009
+ try { matesModule.enforceTeamAwareness(claudeMdPath); } catch (e) {}
3925
4010
  try { crisisSafety.enforce(claudeMdPath); } catch (e) {}
3926
4011
  }, 500);
3927
4012
  });
package/lib/public/app.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
2
2
  import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal, parseEmojis } from './modules/markdown.js';
4
- import { initSidebar, renderSessionList, handleSearchResults, handleSearchContentResults, updateSessionPresence, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline, onHistoryPrepended, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge, updateSessionBadge, updateProjectBadge, closeDmUserPicker, spawnDustParticles } from './modules/sidebar.js';
4
+ import { initSidebar, renderSessionList, handleSearchResults, updateSessionPresence, updatePageTitle, populateCliSessionList, renderIconStrip, renderSidebarPresence, initIconStrip, getEmojiCategories, renderUserStrip, setCurrentDmUser, updateDmBadge, updateSessionBadge, updateProjectBadge, closeDmUserPicker, spawnDustParticles } from './modules/sidebar.js';
5
5
  import { initMateSidebar, showMateSidebar, hideMateSidebar, renderMateSessionList, updateMateSidebarProfile, handleMateSearchResults } from './modules/mate-sidebar.js';
6
6
  import { initMateKnowledge, requestKnowledgeList, renderKnowledgeList, handleKnowledgeContent, hideKnowledge } from './modules/mate-knowledge.js';
7
7
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid, addRewindButton } from './modules/rewind.js';
@@ -638,6 +638,7 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
638
638
  // Apply mate color to chat title bar and panels
639
639
  var mateColor = (targetUser.profile && targetUser.profile.avatarColor) || targetUser.avatarColor || "#7c3aed";
640
640
  document.body.style.setProperty("--mate-color", mateColor);
641
+ document.body.style.setProperty("--mate-color-tint", mateColor + "0a");
641
642
  document.body.classList.add("mate-dm-active");
642
643
  // Build mate avatar URL for DM bubble injection
643
644
  var mp = targetUser.profile || {};
@@ -741,6 +742,7 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
741
742
  }
742
743
  // Reset chat title bar and mate color
743
744
  document.body.style.removeProperty("--mate-color");
745
+ document.body.style.removeProperty("--mate-color-tint");
744
746
  document.body.classList.remove("mate-dm-active");
745
747
  delete document.body.dataset.mateAvatarUrl;
746
748
  delete document.body.dataset.myAvatarUrl;
@@ -851,7 +853,7 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
851
853
  handleKnowledgeContent(msg);
852
854
  return;
853
855
  }
854
- if (msg.type === "knowledge_saved" || msg.type === "knowledge_deleted") {
856
+ if (msg.type === "knowledge_saved" || msg.type === "knowledge_deleted" || msg.type === "knowledge_promoted" || msg.type === "knowledge_depromoted") {
855
857
  return; // list update follows separately
856
858
  }
857
859
  // On done: scan DOM for [[MATE_READY: name]], update name, strip marker
@@ -3258,7 +3260,6 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
3258
3260
  isUserScrolledUp = false;
3259
3261
  newMsgBtn.classList.add("hidden");
3260
3262
  setRewindMode(false);
3261
- removeSearchTimeline();
3262
3263
  setActivity(null);
3263
3264
  setStatus("connected");
3264
3265
  if (!loopActive) enableMainInput();
@@ -3484,10 +3485,6 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
3484
3485
  finalizeAssistantBlock();
3485
3486
  stopUrgentBlink();
3486
3487
  scrollToBottom();
3487
- var pendingQuery = getActiveSearchQuery();
3488
- if (pendingQuery) {
3489
- requestAnimationFrame(function() { buildSearchTimeline(pendingQuery); });
3490
- }
3491
3488
  // Scroll to tool element if navigating from file edit history
3492
3489
  var nav = getPendingNavigate();
3493
3490
  if (nav && (nav.toolId || nav.assistantUuid)) {
@@ -3732,8 +3729,6 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
3732
3729
  case "search_content_results":
3733
3730
  if (msg.source === "find_in_session") {
3734
3731
  handleFindInSessionResults(msg);
3735
- } else {
3736
- handleSearchContentResults(msg);
3737
3732
  }
3738
3733
  break;
3739
3734
 
@@ -4603,8 +4598,7 @@ import { initCommandPalette, handlePaletteSessionSwitch, setPaletteVersion } fro
4603
4598
  updateHistorySentinel();
4604
4599
  }
4605
4600
 
4606
- // Notify sidebar search that history was prepended (for pending scroll targets)
4607
- onHistoryPrepended();
4601
+ // Notify in-session search that history was prepended (for pending scroll targets)
4608
4602
  onSessionSearchHistoryPrepended();
4609
4603
  }
4610
4604
 
@@ -270,6 +270,7 @@
270
270
  #input-area {
271
271
  flex-shrink: 0;
272
272
  padding: 8px 16px calc(var(--safe-bottom) + 8px);
273
+ background: transparent;
273
274
  }
274
275
 
275
276
  #input-wrapper {
@@ -452,7 +453,13 @@
452
453
  display: flex;
453
454
  flex-wrap: wrap;
454
455
  gap: 6px;
455
- padding: 0 0 8px;
456
+ padding: 8px 6px;
457
+ position: absolute;
458
+ bottom: 100%;
459
+ left: 0;
460
+ right: 0;
461
+ z-index: 5;
462
+ background: transparent;
456
463
  }
457
464
 
458
465
  #suggestion-chips.hidden { display: none; }
@@ -472,6 +479,7 @@
472
479
  text-align: left;
473
480
  max-width: 100%;
474
481
  line-height: 1.3;
482
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
475
483
  }
476
484
 
477
485
  .suggestion-chip:hover {