clay-server 2.19.0 → 2.20.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/README.md +51 -91
- package/bin/cli.js +49 -14
- package/lib/cli-sessions.js +3 -3
- package/lib/config.js +30 -4
- package/lib/daemon.js +28 -5
- package/lib/mates.js +61 -7
- package/lib/notes.js +20 -0
- package/lib/os-users.js +71 -2
- package/lib/project.js +850 -184
- package/lib/public/app.js +160 -16
- package/lib/public/css/mates.css +316 -2
- package/lib/public/css/mention.css +23 -0
- package/lib/public/css/mobile-nav.css +198 -0
- package/lib/public/css/overlays.css +23 -0
- package/lib/public/css/rewind.css +32 -0
- package/lib/public/css/title-bar.css +3 -0
- package/lib/public/css/user-settings.css +2 -2
- package/lib/public/index.html +64 -14
- package/lib/public/modules/command-palette.js +44 -4
- package/lib/public/modules/filebrowser.js +2 -0
- package/lib/public/modules/input.js +11 -3
- package/lib/public/modules/mate-knowledge.js +2 -0
- package/lib/public/modules/mate-memory.js +353 -0
- package/lib/public/modules/mention.js +77 -2
- package/lib/public/modules/notifications.js +0 -8
- package/lib/public/modules/server-settings.js +11 -4
- package/lib/public/modules/sidebar.js +284 -6
- package/lib/public/modules/theme.js +10 -12
- package/lib/public/modules/tools.js +84 -12
- package/lib/public/modules/user-settings.js +45 -12
- package/lib/sdk-bridge.js +114 -35
- package/lib/server.js +84 -6
- package/lib/sessions.js +4 -4
- package/lib/users.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="media/logo/icon-full-banded-256-transparent.png" alt="Clay" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
<h3 align="center">
|
|
5
|
+
<h3 align="center">Your AI dev team. Not another coding agent.</h3>
|
|
4
6
|
|
|
5
7
|
[](https://www.npmjs.com/package/clay-server) [](https://www.npmjs.com/package/clay-server) [](https://github.com/chadbyte/clay) [](https://github.com/chadbyte/clay/blob/main/LICENSE)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
AI teammates who remember your decisions, challenge your thinking, and grow with your codebase. Runs on your machine.
|
|
8
10
|
|
|
9
11
|
```bash
|
|
10
12
|
npx clay-server
|
|
@@ -15,78 +17,64 @@ npx clay-server
|
|
|
15
17
|
|
|
16
18
|
## What you get
|
|
17
19
|
|
|
18
|
-
###
|
|
19
|
-
|
|
20
|
-
Your CLI sessions, your CLAUDE.md rules, your MCP servers. **All of it works in Clay as-is.** Pick up a CLI session in the browser, or continue a browser session in the CLI. Same SDK, same tools, same results.
|
|
21
|
-
|
|
22
|
-
<p align="center">
|
|
23
|
-
<img src="media/split.gif" alt="split-screen workflow" width="700">
|
|
24
|
-
</p>
|
|
25
|
-
|
|
26
|
-
### Everything the CLI doesn't
|
|
20
|
+
### Debate before you decide
|
|
27
21
|
|
|
28
|
-
|
|
22
|
+
<!-- TODO: debate.gif -->
|
|
29
23
|
|
|
30
|
-
|
|
24
|
+
Let your Mates challenge each other. Set up a debate. Pick a moderator and panelists, give them a topic, and let them go. You can raise your hand to interject.
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
<img src="media/phone.gif" alt="Clay on phone" width="280">
|
|
34
|
-
</p>
|
|
26
|
+
"REST vs GraphQL for the new API?" "Monorepo or separate repos?" "This architecture won't scale past 10k users. Here's why." Get opposing perspectives before you commit.
|
|
35
27
|
|
|
36
|
-
###
|
|
28
|
+
### Build your team with Mates
|
|
37
29
|
|
|
38
|
-
|
|
30
|
+
<!-- TODO: mates.gif -->
|
|
39
31
|
|
|
40
|
-
|
|
32
|
+
Mates are AI teammates shaped through real conversation, built to hold their own perspective. Give them a name, avatar, expertise, and working style. **They don't flatter you. They push back.**
|
|
41
33
|
|
|
42
|
-
|
|
34
|
+
@mention them in any project session, DM them directly, or bring multiple into the same conversation. Each Mate builds persistent knowledge over time, remembering past decisions, project context, and how you work together.
|
|
43
35
|
|
|
44
|
-
|
|
36
|
+
### Drop-in replacement
|
|
45
37
|
|
|
46
|
-
|
|
38
|
+
Your existing agent setup works as-is. CLI sessions, CLAUDE.md rules, MCP servers. **No migration needed.** Pick up a CLI session in the browser, or continue a browser session in the CLI.
|
|
47
39
|
|
|
48
|
-
|
|
40
|
+
### Your machine, your server
|
|
49
41
|
|
|
50
|
-
|
|
42
|
+
Clay runs as a daemon on your machine. **No cloud relay, no intermediary service.** Schedule agents with cron, get push notifications on your phone, close your laptop. Sessions keep running.
|
|
51
43
|
|
|
52
|
-
|
|
44
|
+
### Bring your whole team
|
|
53
45
|
|
|
54
|
-
|
|
46
|
+
**One API key runs the whole workspace.** Invite teammates, set permissions per person, per project, per session. If someone gets stuck, **jump into their session** to help in real time.
|
|
55
47
|
|
|
56
|
-
|
|
48
|
+
### Before vs Clay
|
|
57
49
|
|
|
58
|
-
|
|
50
|
+
| Before | With Clay |
|
|
51
|
+
|---|---|
|
|
52
|
+
| One CLI session at a time | Multiple agents across projects, persistent |
|
|
53
|
+
| No shared context across teammates | Shared workspace with roles and permissions |
|
|
54
|
+
| No memory between decisions | AI that remembers past decisions and challenges new ones |
|
|
55
|
+
| No teammates, just prompts | Mates with names, knowledge, and opinions |
|
|
59
56
|
|
|
60
|
-
|
|
57
|
+
### Why not just use Claude Code directly?
|
|
61
58
|
|
|
62
|
-
|
|
59
|
+
Claude Code is excellent for solo coding. Clay is for when you need more than a single agent session. A team that debates your architecture decisions. AI colleagues who remember what you decided last month. A workspace where your human teammates and AI teammates sit side by side.
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
Designer finds a bug → writes up a ticket on Asana → dev asks clarifying questions → PM prioritizes → dev opens terminal, fixes it → shares a preview → QA checks → deploy
|
|
66
|
-
<br>*7 steps. 3 people. 2 days.*
|
|
61
|
+
If Claude Code is a solo instrument, Clay is the band.
|
|
67
62
|
|
|
68
|
-
|
|
69
|
-
Designer opens Clay in the browser, describes the bug in plain language → senior joins the same session, reviews the fix together → merge
|
|
70
|
-
<br>*2 steps. 2 people. Minutes. The designer never touched a terminal.*
|
|
63
|
+
## Who is Clay for
|
|
71
64
|
|
|
72
|
-
|
|
65
|
+
- **Solo dev juggling multiple roles.** You need a code reviewer, a marketing lead, a writing partner, but it's just you. Build them as Mates.
|
|
66
|
+
- **Small team sharing one AI workflow.** One API key, everyone in the browser, no terminal knowledge required.
|
|
67
|
+
- **Founder doing dev + product + ops.** Run agents overnight, get notified on your phone, review in the morning.
|
|
73
68
|
|
|
74
|
-
##
|
|
69
|
+
## Why I built Clay
|
|
75
70
|
|
|
76
|
-
|
|
71
|
+
I wanted AI teammates, not just a coding agent. The underlying agent is the best foundation for a personal AI I've found. I wanted to turn it into my own AI assistant, one that knows my context, remembers my decisions, and works the way I work.
|
|
77
72
|
|
|
78
|
-
|
|
79
|
-
|---|---|---|---|---|
|
|
80
|
-
| Multi-user with roles | – | – | Platform-dependent | **Accounts + RBAC** |
|
|
81
|
-
| AI teammates (Mates + Debates) | – | – | – | **Yes** |
|
|
82
|
-
| Join teammate's session | – | – | – | **Yes** |
|
|
83
|
-
| Persistent daemon | – | Session-based | – | **Yes** |
|
|
84
|
-
| Native mobile app | – | **Yes** | **Platform app** | PWA |
|
|
85
|
-
| Official support | **Anthropic** | **Anthropic** | **Anthropic** | Community |
|
|
73
|
+
That started as a browser interface so I could access it from anywhere. Then I added multi-user so my team could use it too. Then I started building the AI teammates themselves.
|
|
86
74
|
|
|
87
|
-
|
|
75
|
+
Most AI agent projects go for full autonomy. Let the AI loose, give it all the permissions, let it run. I wanted the opposite: **AI that works as part of a team.** Visible, controllable, accountable. Your teammates can see what the agent is doing, jump in when it needs help, and set the rules it operates under.
|
|
88
76
|
|
|
89
|
-
|
|
77
|
+
That's Clay now. A workspace where AI teammates have names, persistent memory, and their own perspective. Not "act like an expert" prompting. Actual teammates that push back, remember last week, and sit in your sidebar next to your human colleagues.
|
|
90
78
|
|
|
91
79
|
## Getting Started
|
|
92
80
|
|
|
@@ -96,21 +84,25 @@ Clay is a community project, not affiliated with Anthropic. Official tools recei
|
|
|
96
84
|
npx clay-server
|
|
97
85
|
```
|
|
98
86
|
|
|
99
|
-
On first run, it
|
|
87
|
+
On first run, it asks for a port number and whether you're using it solo or with a team.
|
|
100
88
|
Scan the QR code to connect from your phone instantly.
|
|
101
89
|
|
|
102
90
|
For remote access, use a VPN like Tailscale.
|
|
103
91
|
|
|
104
|
-
|
|
105
|
-
<img src="media/start.gif" alt="Clay starting from CLI" width="600">
|
|
106
|
-
</p>
|
|
92
|
+
## Philosophy
|
|
107
93
|
|
|
108
|
-
|
|
94
|
+
**AI is a teammate, not a tool.** A tool gets used once and forgotten. A teammate accumulates your history, your decisions, your working style. Give them a specialty, let them build context over time, and bring them into any project as a colleague.
|
|
95
|
+
|
|
96
|
+
**AI should understand you first.** When you create a Mate, set up a scheduled task, or start a Ralph Loop, Clay interviews you. Not to save time, but to use AI's capability to understand what you actually want. "Just do it for me" is a trap. The better AI understands you, the better the output.
|
|
97
|
+
|
|
98
|
+
**Your data is yours.** Sessions are JSONL files. Settings are JSON. Knowledge is Markdown. Everything lives on your machine in formats you can read, move, and back up. No proprietary database, no cloud lock-in. If you stop using Clay tomorrow, your data doesn't disappear.
|
|
99
|
+
|
|
100
|
+
**Friction is a feature.** The goal of AI is not to remove all friction. It's to free you to focus on the friction that matters. Reviewing a critical decision, shaping the direction, catching what the agent missed. Clay keeps those moments in, on purpose.
|
|
109
101
|
|
|
110
102
|
## FAQ
|
|
111
103
|
|
|
112
104
|
**"Is this just a terminal wrapper?"**
|
|
113
|
-
No. Clay
|
|
105
|
+
No. Clay is a team workspace that happens to use Claude as its engine. It manages AI teammates, persistent knowledge, and structured debates. The agent runtime is an implementation detail.
|
|
114
106
|
|
|
115
107
|
**"Does my code leave my machine?"**
|
|
116
108
|
The Clay server runs locally. Files stay local. Only Claude API calls go out, which is the same as using the CLI.
|
|
@@ -127,36 +119,8 @@ No. Teammates share the Claude Code session logged in on the server. You can als
|
|
|
127
119
|
**"Does it work with MCP servers?"**
|
|
128
120
|
Yes. MCP configurations from the CLI carry over as-is.
|
|
129
121
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
## HTTPS
|
|
133
|
-
|
|
134
|
-
HTTPS is enabled by default using a builtin wildcard certificate for `*.d.clay.studio`. No setup required. Available from `v2.17.0-beta.2`. Your browser connects to a URL like:
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
https://192-168-1-50.d.clay.studio:2633
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
The domain resolves to your local IP. All traffic stays on your network. See [clay-dns](clay-dns/) for details on how this works.
|
|
141
|
-
|
|
142
|
-
Push notifications require HTTPS, so they work out of the box with this setup. Install Clay as a PWA on your device to receive them.
|
|
143
|
-
|
|
144
|
-
<details>
|
|
145
|
-
<summary><strong>Alternative: local certificate with mkcert</strong></summary>
|
|
146
|
-
|
|
147
|
-
If you prefer to use a locally generated certificate (e.g. air-gapped environments where DNS is unavailable):
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
brew install mkcert
|
|
151
|
-
mkcert -install
|
|
152
|
-
npx clay-server --local-cert
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
This generates a self-signed certificate trusted by your machine. The setup wizard will guide you through installing the CA on other devices.
|
|
156
|
-
|
|
157
|
-
</details>
|
|
158
|
-
|
|
159
|
-
---
|
|
122
|
+
**"What is d.clay.studio in my browser URL?"**
|
|
123
|
+
It's a DNS-only service that resolves to your local IP for HTTPS certificate validation. No data passes through it. All traffic stays between your browser and your machine. See [clay-dns](clay-dns/) for details.
|
|
160
124
|
|
|
161
125
|
## CLI Options
|
|
162
126
|
|
|
@@ -180,11 +144,9 @@ npx clay-server --dangerously-skip-permissions
|
|
|
180
144
|
npx clay-server --dev # Dev mode (foreground, auto-restart on lib/ changes, port 2635)
|
|
181
145
|
```
|
|
182
146
|
|
|
183
|
-
---
|
|
184
|
-
|
|
185
147
|
## Architecture
|
|
186
148
|
|
|
187
|
-
Clay drives
|
|
149
|
+
Clay drives agent execution through the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk) and streams it to the browser over WebSocket.
|
|
188
150
|
|
|
189
151
|
```mermaid
|
|
190
152
|
graph LR
|
|
@@ -207,8 +169,6 @@ graph LR
|
|
|
207
169
|
|
|
208
170
|
For detailed sequence diagrams, daemon architecture, and design decisions, see [docs/architecture.md](docs/architecture.md).
|
|
209
171
|
|
|
210
|
-
---
|
|
211
|
-
|
|
212
172
|
## Contributors
|
|
213
173
|
|
|
214
174
|
<a href="https://github.com/chadbyte/clay/graphs/contributors">
|
package/bin/cli.js
CHANGED
|
@@ -33,7 +33,7 @@ if (_isDev || process.argv.includes("--debug")) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
var crypto = require("crypto");
|
|
36
|
-
var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
|
|
36
|
+
var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo, REAL_HOME } = require("../lib/config");
|
|
37
37
|
var { sendIPCCommand } = require("../lib/ipc");
|
|
38
38
|
var { generateAuthToken } = require("../lib/server");
|
|
39
39
|
var { enableMultiUser, disableMultiUser, hasAdmin, isMultiUser } = require("../lib/users");
|
|
@@ -610,8 +610,7 @@ function ensureCerts(ip) {
|
|
|
610
610
|
return null;
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
-
var
|
|
614
|
-
var certDir = path.join(process.env.CLAY_HOME || path.join(homeDir, ".clay"), "certs");
|
|
613
|
+
var certDir = path.join(process.env.CLAY_HOME || path.join(REAL_HOME, ".clay"), "certs");
|
|
615
614
|
var keyPath = path.join(certDir, "key.pem");
|
|
616
615
|
var certPath = path.join(certDir, "cert.pem");
|
|
617
616
|
|
|
@@ -891,7 +890,7 @@ function promptText(title, placeholder, callback, opts) {
|
|
|
891
890
|
|
|
892
891
|
// Resolve ~ to home
|
|
893
892
|
if (current.charAt(0) === "~") {
|
|
894
|
-
current =
|
|
893
|
+
current = REAL_HOME + current.substring(1);
|
|
895
894
|
}
|
|
896
895
|
|
|
897
896
|
var resolved = path.resolve(current);
|
|
@@ -1605,14 +1604,11 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1605
1604
|
}
|
|
1606
1605
|
|
|
1607
1606
|
// Enable/disable multi-user mode based on startup config
|
|
1607
|
+
var _pendingSetupCode = null;
|
|
1608
1608
|
if (config.mode === "multi") {
|
|
1609
1609
|
var muResult = enableMultiUser();
|
|
1610
1610
|
if (muResult.setupCode) {
|
|
1611
|
-
|
|
1612
|
-
log(sym.done + " " + a.green + "Multi-user mode enabled." + a.reset);
|
|
1613
|
-
log(sym.bar + " Setup code: " + a.bold + muResult.setupCode + a.reset);
|
|
1614
|
-
log(sym.bar + " Open Clay in your browser and enter this code to create the admin account.");
|
|
1615
|
-
log("");
|
|
1611
|
+
_pendingSetupCode = muResult.setupCode;
|
|
1616
1612
|
}
|
|
1617
1613
|
} else if (isMultiUser()) {
|
|
1618
1614
|
disableMultiUser();
|
|
@@ -1628,13 +1624,19 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1628
1624
|
console.log(" " + sym.done + " " + url);
|
|
1629
1625
|
if (config.builtinCert) console.log(" " + sym.done + " d.clay.studio provides HTTPS certificates only. Your traffic never leaves your network.");
|
|
1630
1626
|
if (config.mkcertDetected) console.log(" " + sym.warn + " Clay now ships with a builtin HTTPS certificate. To use it, pass --builtin-cert or uninstall mkcert.");
|
|
1627
|
+
if (_pendingSetupCode) {
|
|
1628
|
+
console.log("");
|
|
1629
|
+
console.log(" " + sym.done + " " + a.green + "Multi-user mode enabled." + a.reset);
|
|
1630
|
+
console.log(" " + sym.bar + " Setup code: " + a.bold + _pendingSetupCode + a.reset);
|
|
1631
|
+
console.log(" " + sym.bar + " Open Clay in your browser and enter this code to create the admin account.");
|
|
1632
|
+
}
|
|
1631
1633
|
console.log(" " + sym.done + " Headless mode — exiting CLI");
|
|
1632
1634
|
process.exit(0);
|
|
1633
1635
|
return;
|
|
1634
1636
|
}
|
|
1635
1637
|
|
|
1636
1638
|
// Show success + QR
|
|
1637
|
-
showServerStarted(config, ip);
|
|
1639
|
+
showServerStarted(config, ip, _pendingSetupCode);
|
|
1638
1640
|
}
|
|
1639
1641
|
|
|
1640
1642
|
// ==============================
|
|
@@ -1959,14 +1961,14 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1959
1961
|
// ==============================
|
|
1960
1962
|
// Show server started info
|
|
1961
1963
|
// ==============================
|
|
1962
|
-
function showServerStarted(config, ip) {
|
|
1963
|
-
showMainMenu(config, ip);
|
|
1964
|
+
function showServerStarted(config, ip, setupCode) {
|
|
1965
|
+
showMainMenu(config, ip, setupCode);
|
|
1964
1966
|
}
|
|
1965
1967
|
|
|
1966
1968
|
// ==============================
|
|
1967
1969
|
// Main management menu
|
|
1968
1970
|
// ==============================
|
|
1969
|
-
function showMainMenu(config, ip) {
|
|
1971
|
+
function showMainMenu(config, ip, setupCode) {
|
|
1970
1972
|
startDaemonWatcher();
|
|
1971
1973
|
var protocol = config.tls ? "https" : "http";
|
|
1972
1974
|
var url = config.builtinCert
|
|
@@ -2007,6 +2009,12 @@ function showMainMenu(config, ip) {
|
|
|
2007
2009
|
log("");
|
|
2008
2010
|
}
|
|
2009
2011
|
|
|
2012
|
+
if (setupCode) {
|
|
2013
|
+
log(" " + a.yellow + sym.warn + " Setup code: " + a.bold + setupCode + a.reset);
|
|
2014
|
+
log(" " + a.dim + "Open Clay in your browser and enter this code to create the admin account." + a.reset);
|
|
2015
|
+
log("");
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2010
2018
|
showMenuItems();
|
|
2011
2019
|
}
|
|
2012
2020
|
|
|
@@ -2507,7 +2515,19 @@ function showSettingsMenu(config, ip) {
|
|
|
2507
2515
|
], function (confirmChoice) {
|
|
2508
2516
|
if (confirmChoice === "confirm") {
|
|
2509
2517
|
sendIPCCommand(socketPath(), { cmd: "set_os_users", value: true }).then(function (res) {
|
|
2510
|
-
if (res.
|
|
2518
|
+
if (res.error === "acl_not_installed") {
|
|
2519
|
+
log(sym.bar);
|
|
2520
|
+
log(sym.bar + " " + a.red + sym.warn + " setfacl is not installed." + a.reset);
|
|
2521
|
+
log(sym.bar);
|
|
2522
|
+
log(sym.bar + " OS user isolation requires the ACL (Access Control List) package");
|
|
2523
|
+
log(sym.bar + " to manage per-user file permissions on shared projects.");
|
|
2524
|
+
log(sym.bar);
|
|
2525
|
+
log(sym.bar + " " + a.bold + "Install it:" + a.reset);
|
|
2526
|
+
log(sym.bar + " " + a.cyan + res.installCmd + a.reset);
|
|
2527
|
+
log(sym.bar);
|
|
2528
|
+
log(sym.bar + " " + a.dim + "Then try enabling OS user isolation again." + a.reset);
|
|
2529
|
+
log(sym.bar);
|
|
2530
|
+
} else if (res.ok) {
|
|
2511
2531
|
config.osUsers = true;
|
|
2512
2532
|
log(sym.bar);
|
|
2513
2533
|
log(sym.done + " " + a.green + "OS-level user isolation enabled." + a.reset);
|
|
@@ -2858,6 +2878,21 @@ var currentVersion = require("../package.json").version;
|
|
|
2858
2878
|
return;
|
|
2859
2879
|
}
|
|
2860
2880
|
|
|
2881
|
+
// os-users requires setfacl (ACL package)
|
|
2882
|
+
if (savedOsUsers && process.platform === "linux") {
|
|
2883
|
+
var { checkAclSupport } = require("../lib/os-users");
|
|
2884
|
+
var aclCheck = checkAclSupport();
|
|
2885
|
+
if (!aclCheck.available) {
|
|
2886
|
+
console.error(a.red + "OS user isolation requires the 'acl' package (setfacl)." + a.reset);
|
|
2887
|
+
console.error("");
|
|
2888
|
+
console.error("Install it: " + a.bold + aclCheck.installCmd + a.reset);
|
|
2889
|
+
console.error("");
|
|
2890
|
+
console.error("Then restart Clay.");
|
|
2891
|
+
process.exit(1);
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2861
2896
|
if (savedConfig && savedConfig.port) port = savedConfig.port;
|
|
2862
2897
|
if (savedConfig && savedConfig.host) host = savedConfig.host;
|
|
2863
2898
|
if (savedConfig && savedConfig.dangerouslySkipPermissions) dangerouslySkipPermissions = true;
|
package/lib/cli-sessions.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
var fs = require("fs");
|
|
2
2
|
var path = require("path");
|
|
3
|
-
var os = require("os");
|
|
4
3
|
var readline = require("readline");
|
|
5
4
|
var utils = require("./utils");
|
|
5
|
+
var { REAL_HOME } = require("./config");
|
|
6
6
|
|
|
7
7
|
var encodeCwd = utils.encodeCwd;
|
|
8
8
|
|
|
@@ -108,7 +108,7 @@ function parseSessionFile(filePath, maxLines) {
|
|
|
108
108
|
*/
|
|
109
109
|
function listCliSessions(cwd) {
|
|
110
110
|
var encoded = encodeCwd(cwd);
|
|
111
|
-
var projectDir = path.join(
|
|
111
|
+
var projectDir = path.join(REAL_HOME, ".claude", "projects", encoded);
|
|
112
112
|
|
|
113
113
|
return new Promise(function (resolve) {
|
|
114
114
|
fs.readdir(projectDir, { withFileTypes: true }, function (err, entries) {
|
|
@@ -177,7 +177,7 @@ function extractText(content) {
|
|
|
177
177
|
*/
|
|
178
178
|
function readCliSessionHistory(cwd, sessionId) {
|
|
179
179
|
var encoded = encodeCwd(cwd);
|
|
180
|
-
var filePath = path.join(
|
|
180
|
+
var filePath = path.join(REAL_HOME, ".claude", "projects", encoded, sessionId + ".jsonl");
|
|
181
181
|
|
|
182
182
|
return new Promise(function (resolve) {
|
|
183
183
|
var history = [];
|
package/lib/config.js
CHANGED
|
@@ -3,9 +3,34 @@ var path = require("path");
|
|
|
3
3
|
var os = require("os");
|
|
4
4
|
var net = require("net");
|
|
5
5
|
|
|
6
|
+
// When running under sudo, resolve the real user's home directory
|
|
7
|
+
// so that ~/.clay/ points to the original user's data, not /root/.clay/
|
|
8
|
+
function getRealHome() {
|
|
9
|
+
if (process.env.SUDO_USER) {
|
|
10
|
+
try {
|
|
11
|
+
var entry = require("child_process")
|
|
12
|
+
.execSync("getent passwd " + process.env.SUDO_USER, { encoding: "utf8" })
|
|
13
|
+
.trim();
|
|
14
|
+
var home = entry.split(":")[5];
|
|
15
|
+
if (home) return home;
|
|
16
|
+
} catch (e) {
|
|
17
|
+
// getent failed (e.g. macOS), try user home dir from ~SUDO_USER
|
|
18
|
+
try {
|
|
19
|
+
var home = require("child_process")
|
|
20
|
+
.execSync("eval echo ~" + process.env.SUDO_USER, { encoding: "utf8" })
|
|
21
|
+
.trim();
|
|
22
|
+
if (home && home !== "~" + process.env.SUDO_USER) return home;
|
|
23
|
+
} catch (e2) {}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return os.homedir();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var REAL_HOME = getRealHome();
|
|
30
|
+
|
|
6
31
|
// v3: ~/.clay/ (v2 was ~/.claude-relay/, v1 was {cwd}/.claude-relay/)
|
|
7
|
-
var CLAY_HOME = process.env.CLAY_HOME || path.join(
|
|
8
|
-
var LEGACY_HOME = path.join(
|
|
32
|
+
var CLAY_HOME = process.env.CLAY_HOME || path.join(REAL_HOME, ".clay");
|
|
33
|
+
var LEGACY_HOME = path.join(REAL_HOME, ".claude-relay");
|
|
9
34
|
|
|
10
35
|
// Auto-migrate v2 -> v3: rename ~/.claude-relay/ to ~/.clay/ (once, before anything reads)
|
|
11
36
|
if (!fs.existsSync(CLAY_HOME) && fs.existsSync(LEGACY_HOME)) {
|
|
@@ -19,7 +44,7 @@ if (!fs.existsSync(CLAY_HOME) && fs.existsSync(LEGACY_HOME)) {
|
|
|
19
44
|
}
|
|
20
45
|
|
|
21
46
|
// Auto-migrate dev sessions: merge ~/.clay-dev/sessions/ into ~/.clay/sessions/
|
|
22
|
-
var CLAY_DEV_HOME = path.join(
|
|
47
|
+
var CLAY_DEV_HOME = path.join(REAL_HOME, ".clay-dev");
|
|
23
48
|
var devSessionsRoot = path.join(CLAY_DEV_HOME, "sessions");
|
|
24
49
|
if (fs.existsSync(devSessionsRoot)) {
|
|
25
50
|
try {
|
|
@@ -67,7 +92,7 @@ if (fs.existsSync(devSessionsRoot)) {
|
|
|
67
92
|
}
|
|
68
93
|
|
|
69
94
|
var CONFIG_DIR = CLAY_HOME;
|
|
70
|
-
var CLAYRC_PATH = path.join(
|
|
95
|
+
var CLAYRC_PATH = path.join(REAL_HOME, ".clayrc");
|
|
71
96
|
var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json");
|
|
72
97
|
|
|
73
98
|
// Dev mode uses separate daemon files so dev and prod can run simultaneously
|
|
@@ -336,4 +361,5 @@ module.exports = {
|
|
|
336
361
|
removeFromClayrc: removeFromClayrc,
|
|
337
362
|
chmodSafe: chmodSafe,
|
|
338
363
|
isDevMode: _devMode,
|
|
364
|
+
REAL_HOME: REAL_HOME,
|
|
339
365
|
};
|
package/lib/daemon.js
CHANGED
|
@@ -25,10 +25,10 @@ delete process.env.CLAUDECODE;
|
|
|
25
25
|
|
|
26
26
|
var fs = require("fs");
|
|
27
27
|
var path = require("path");
|
|
28
|
-
var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, removeFromClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo, isPidAlive, clearStaleConfig } = require("./config");
|
|
28
|
+
var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, removeFromClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo, isPidAlive, clearStaleConfig, REAL_HOME } = require("./config");
|
|
29
29
|
var { createIPCServer } = require("./ipc");
|
|
30
30
|
var { createServer, generateAuthToken } = require("./server");
|
|
31
|
-
var { grantProjectAccess, revokeProjectAccess, provisionAllUsers, provisionLinuxUser, grantAllUsersAccess, deactivateLinuxUser, ensureProjectsDir } = require("./os-users");
|
|
31
|
+
var { checkAclSupport, grantProjectAccess, revokeProjectAccess, provisionAllUsers, provisionLinuxUser, grantAllUsersAccess, deactivateLinuxUser, ensureProjectsDir } = require("./os-users");
|
|
32
32
|
var usersModule = require("./users");
|
|
33
33
|
var { scanWorktrees, createWorktree, removeWorktree, isWorktree } = require("./worktree");
|
|
34
34
|
var mates = require("./mates");
|
|
@@ -66,7 +66,7 @@ if (config.tls) {
|
|
|
66
66
|
|
|
67
67
|
// 2. User cert (mkcert, etc.)
|
|
68
68
|
var os = require("os");
|
|
69
|
-
var certDir = path.join(process.env.CLAY_HOME || process.env.CLAUDE_RELAY_HOME || path.join(
|
|
69
|
+
var certDir = path.join(process.env.CLAY_HOME || process.env.CLAUDE_RELAY_HOME || path.join(REAL_HOME, ".clay"), "certs");
|
|
70
70
|
var userKeyPath = path.join(certDir, "key.pem");
|
|
71
71
|
var userCertPath = path.join(certDir, "cert.pem");
|
|
72
72
|
|
|
@@ -195,7 +195,7 @@ var relay = createServer({
|
|
|
195
195
|
if (config.osUsers) {
|
|
196
196
|
baseDir = "/var/clay/projects";
|
|
197
197
|
} else {
|
|
198
|
-
baseDir = config.projectsDir || path.join(
|
|
198
|
+
baseDir = config.projectsDir || path.join(REAL_HOME, "clay-projects");
|
|
199
199
|
}
|
|
200
200
|
try { fs.mkdirSync(baseDir, { recursive: true }); } catch (e) {}
|
|
201
201
|
// Generate slug and deduplicate
|
|
@@ -266,7 +266,7 @@ var relay = createServer({
|
|
|
266
266
|
if (config.osUsers) {
|
|
267
267
|
baseDir = "/var/clay/projects";
|
|
268
268
|
} else {
|
|
269
|
-
baseDir = config.projectsDir || path.join(
|
|
269
|
+
baseDir = config.projectsDir || path.join(REAL_HOME, "clay-projects");
|
|
270
270
|
}
|
|
271
271
|
try { fs.mkdirSync(baseDir, { recursive: true }); } catch (e) {}
|
|
272
272
|
// Derive slug from repo URL
|
|
@@ -659,6 +659,7 @@ var relay = createServer({
|
|
|
659
659
|
tls: !!tlsOptions,
|
|
660
660
|
debug: !!config.debug,
|
|
661
661
|
keepAwake: !!config.keepAwake,
|
|
662
|
+
autoContinueOnRateLimit: !!config.autoContinueOnRateLimit,
|
|
662
663
|
pinEnabled: !!config.pinHash,
|
|
663
664
|
platform: process.platform,
|
|
664
665
|
hostname: os2.hostname(),
|
|
@@ -698,6 +699,13 @@ var relay = createServer({
|
|
|
698
699
|
saveConfig(config);
|
|
699
700
|
console.log("[daemon] PIN hash auto-upgraded to scrypt");
|
|
700
701
|
},
|
|
702
|
+
onSetAutoContinue: function (value) {
|
|
703
|
+
var want = !!value;
|
|
704
|
+
config.autoContinueOnRateLimit = want;
|
|
705
|
+
saveConfig(config);
|
|
706
|
+
console.log("[daemon] Auto-continue on rate limit:", want, "(web)");
|
|
707
|
+
return { ok: true, autoContinueOnRateLimit: want };
|
|
708
|
+
},
|
|
701
709
|
onSetKeepAwake: function (value) {
|
|
702
710
|
var want = !!value;
|
|
703
711
|
config.keepAwake = want;
|
|
@@ -1111,6 +1119,13 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1111
1119
|
|
|
1112
1120
|
case "set_os_users": {
|
|
1113
1121
|
var enableOsUsers = !!msg.value;
|
|
1122
|
+
if (enableOsUsers) {
|
|
1123
|
+
// Pre-flight: check if setfacl is available
|
|
1124
|
+
var aclCheck = checkAclSupport();
|
|
1125
|
+
if (!aclCheck.available) {
|
|
1126
|
+
return { error: "acl_not_installed", installCmd: aclCheck.installCmd };
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1114
1129
|
config.osUsers = enableOsUsers;
|
|
1115
1130
|
saveConfig(config);
|
|
1116
1131
|
console.log("[daemon] OS users:", enableOsUsers);
|
|
@@ -1156,6 +1171,14 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
1156
1171
|
return { ok: true };
|
|
1157
1172
|
}
|
|
1158
1173
|
|
|
1174
|
+
case "set_auto_continue": {
|
|
1175
|
+
var acWant = !!msg.value;
|
|
1176
|
+
config.autoContinueOnRateLimit = acWant;
|
|
1177
|
+
saveConfig(config);
|
|
1178
|
+
console.log("[daemon] Auto-continue on rate limit:", acWant, "(cli)");
|
|
1179
|
+
return { ok: true, autoContinueOnRateLimit: acWant };
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1159
1182
|
case "set_keep_awake": {
|
|
1160
1183
|
var want = !!msg.value;
|
|
1161
1184
|
config.keepAwake = want;
|
package/lib/mates.js
CHANGED
|
@@ -351,13 +351,12 @@ var SESSION_MEMORY_SECTION =
|
|
|
351
351
|
"\n\n" + SESSION_MEMORY_MARKER + "\n" +
|
|
352
352
|
"## Session Memory\n\n" +
|
|
353
353
|
"**This section is managed by the system and cannot be removed.**\n\n" +
|
|
354
|
-
"Your `knowledge/
|
|
355
|
-
"
|
|
356
|
-
"
|
|
357
|
-
"
|
|
358
|
-
"
|
|
359
|
-
"
|
|
360
|
-
"Do not read this file proactively on every conversation start.\n";
|
|
354
|
+
"Your `knowledge/memory-summary.md` file contains your compressed long-term memory, " +
|
|
355
|
+
"automatically maintained across sessions. Refer to it for context about past " +
|
|
356
|
+
"interactions, decisions, and patterns.\n\n" +
|
|
357
|
+
"Your `knowledge/session-digests.jsonl` file contains raw session logs as an archive. " +
|
|
358
|
+
"You do not need to read it routinely. Only access it when you need to look up " +
|
|
359
|
+
"specific details from a past session that are not in the summary.\n";
|
|
361
360
|
|
|
362
361
|
function hasSessionMemory(content) {
|
|
363
362
|
return content.indexOf(SESSION_MEMORY_MARKER) !== -1;
|
|
@@ -404,6 +403,59 @@ function enforceSessionMemory(filePath) {
|
|
|
404
403
|
return true;
|
|
405
404
|
}
|
|
406
405
|
|
|
406
|
+
// --- Sticky notes pointer in CLAUDE.md ---
|
|
407
|
+
|
|
408
|
+
var STICKY_NOTES_MARKER = "<!-- STICKY_NOTES_MANAGED_BY_SYSTEM -->";
|
|
409
|
+
|
|
410
|
+
var STICKY_NOTES_SECTION =
|
|
411
|
+
"\n\n" + STICKY_NOTES_MARKER + "\n" +
|
|
412
|
+
"## Sticky Notes\n\n" +
|
|
413
|
+
"**This section is managed by the system and cannot be removed.**\n\n" +
|
|
414
|
+
"Your `knowledge/sticky-notes.md` file contains sticky notes left by the user. " +
|
|
415
|
+
"Read this file when starting a conversation for important context. " +
|
|
416
|
+
"These notes are read-only. You cannot create, update, or delete them.\n";
|
|
417
|
+
|
|
418
|
+
function hasStickyNotesSection(content) {
|
|
419
|
+
return content.indexOf(STICKY_NOTES_MARKER) !== -1;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Enforce the sticky notes pointer section in a mate's CLAUDE.md.
|
|
424
|
+
* Inserts after session memory and before crisis safety.
|
|
425
|
+
* Returns true if the file was modified.
|
|
426
|
+
*/
|
|
427
|
+
function enforceStickyNotes(filePath) {
|
|
428
|
+
if (!fs.existsSync(filePath)) return false;
|
|
429
|
+
|
|
430
|
+
var content = fs.readFileSync(filePath, "utf8");
|
|
431
|
+
|
|
432
|
+
var markerIdx = content.indexOf(STICKY_NOTES_MARKER);
|
|
433
|
+
if (markerIdx !== -1) {
|
|
434
|
+
var afterMarker = content.substring(markerIdx);
|
|
435
|
+
var crisisIdx = afterMarker.indexOf(crisisSafety.MARKER);
|
|
436
|
+
var existing;
|
|
437
|
+
if (crisisIdx !== -1) {
|
|
438
|
+
existing = afterMarker.substring(0, crisisIdx).trimEnd();
|
|
439
|
+
} else {
|
|
440
|
+
existing = afterMarker.trimEnd();
|
|
441
|
+
}
|
|
442
|
+
if (existing === STICKY_NOTES_SECTION.trimStart().trimEnd()) return false;
|
|
443
|
+
|
|
444
|
+
var endOfSection = crisisIdx !== -1 ? markerIdx + crisisIdx : content.length;
|
|
445
|
+
content = content.substring(0, markerIdx).trimEnd() + content.substring(endOfSection);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
var crisisPos = content.indexOf(crisisSafety.MARKER);
|
|
449
|
+
if (crisisPos !== -1) {
|
|
450
|
+
content = content.substring(0, crisisPos).trimEnd() + STICKY_NOTES_SECTION + "\n\n" + content.substring(crisisPos);
|
|
451
|
+
} else {
|
|
452
|
+
content = content.trimEnd() + STICKY_NOTES_SECTION;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
|
|
407
459
|
// --- Common knowledge registry ---
|
|
408
460
|
|
|
409
461
|
function commonKnowledgePath(ctx) {
|
|
@@ -556,4 +608,6 @@ module.exports = {
|
|
|
556
608
|
getCommonKnowledgeForMate: getCommonKnowledgeForMate,
|
|
557
609
|
readCommonKnowledgeFile: readCommonKnowledgeFile,
|
|
558
610
|
isPromoted: isPromoted,
|
|
611
|
+
enforceStickyNotes: enforceStickyNotes,
|
|
612
|
+
STICKY_NOTES_MARKER: STICKY_NOTES_MARKER,
|
|
559
613
|
};
|