clay-server 2.5.1 → 2.6.0

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.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ console.warn("\x1b[33m⚠ 'claude-relay' is deprecated and will be removed in a future version.\x1b[0m");
4
+ console.warn("\x1b[33m Use \x1b[1mnpx clay-server\x1b[22m instead.\x1b[0m\n");
5
+
6
+ require("./cli.js");
package/bin/cli.js CHANGED
@@ -7,11 +7,8 @@ var { execSync, execFileSync, spawn } = require("child_process");
7
7
  var qrcode = require("qrcode-terminal");
8
8
  var net = require("net");
9
9
 
10
- // Detect dev mode before loading config (env must be set before require)
10
+ // Detect dev mode (no separate storage dev and prod share ~/.clay)
11
11
  var _isDev = (process.argv[1] && path.basename(process.argv[1]) === "clay-dev") || process.argv.includes("--dev");
12
- if (_isDev) {
13
- process.env.CLAY_HOME = path.join(os.homedir(), ".clay-dev");
14
- }
15
12
 
16
13
  var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
17
14
  var { sendIPCCommand } = require("../lib/ipc");
@@ -1250,7 +1247,18 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1250
1247
  // Only include cwd if explicitly requested
1251
1248
  if (addCwd) {
1252
1249
  var slug = generateSlug(cwd, []);
1253
- allProjects.push({ path: cwd, slug: slug, addedAt: Date.now() });
1250
+ var cwdEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1251
+ // Restore title/icon from .clayrc if available
1252
+ var cwdRc = loadClayrc();
1253
+ var cwdRecent = cwdRc.recentProjects || [];
1254
+ for (var cr = 0; cr < cwdRecent.length; cr++) {
1255
+ if (cwdRecent[cr].path === cwd) {
1256
+ if (cwdRecent[cr].title) cwdEntry.title = cwdRecent[cr].title;
1257
+ if (cwdRecent[cr].icon) cwdEntry.icon = cwdRecent[cr].icon;
1258
+ break;
1259
+ }
1260
+ }
1261
+ allProjects.push(cwdEntry);
1254
1262
  usedSlugs.push(slug);
1255
1263
  }
1256
1264
 
@@ -1262,7 +1270,7 @@ async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
1262
1270
  if (!fs.existsSync(rp.path)) continue; // skip missing directories
1263
1271
  var rpSlug = generateSlug(rp.path, usedSlugs);
1264
1272
  usedSlugs.push(rpSlug);
1265
- allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, addedAt: rp.addedAt || Date.now() });
1273
+ allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() });
1266
1274
  }
1267
1275
  }
1268
1276
 
@@ -1350,19 +1358,29 @@ async function devMode(pin, keepAwake, existingPinHash) {
1350
1358
  }
1351
1359
 
1352
1360
  var slug = generateSlug(cwd, []);
1353
- var allProjects = [{ path: cwd, slug: slug, addedAt: Date.now() }];
1361
+ var cwdDevEntry = { path: cwd, slug: slug, addedAt: Date.now() };
1354
1362
 
1355
1363
  // Restore previous projects
1356
1364
  var rc = loadClayrc();
1357
1365
  var restorable = (rc.recentProjects || []).filter(function (p) {
1358
1366
  return p.path !== cwd && fs.existsSync(p.path);
1359
1367
  });
1368
+ // Restore title/icon for cwd from .clayrc
1369
+ var rcAll = rc.recentProjects || [];
1370
+ for (var ci = 0; ci < rcAll.length; ci++) {
1371
+ if (rcAll[ci].path === cwd) {
1372
+ if (rcAll[ci].title) cwdDevEntry.title = rcAll[ci].title;
1373
+ if (rcAll[ci].icon) cwdDevEntry.icon = rcAll[ci].icon;
1374
+ break;
1375
+ }
1376
+ }
1377
+ var allProjects = [cwdDevEntry];
1360
1378
  var usedSlugs = [slug];
1361
1379
  for (var ri = 0; ri < restorable.length; ri++) {
1362
1380
  var rp = restorable[ri];
1363
1381
  var rpSlug = generateSlug(rp.path, usedSlugs);
1364
1382
  usedSlugs.push(rpSlug);
1365
- allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, addedAt: rp.addedAt || Date.now() });
1383
+ allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() });
1366
1384
  }
1367
1385
 
1368
1386
  var config = {
@@ -2,14 +2,9 @@ var fs = require("fs");
2
2
  var path = require("path");
3
3
  var os = require("os");
4
4
  var readline = require("readline");
5
+ var utils = require("./utils");
5
6
 
6
- /**
7
- * Compute the encoded project directory name used by the Claude CLI.
8
- * Replaces all "/" with "-", e.g. "/Users/foo/project" -> "-Users-foo-project"
9
- */
10
- function encodeCwd(cwd) {
11
- return cwd.replace(/\//g, "-");
12
- }
7
+ var encodeCwd = utils.encodeCwd;
13
8
 
14
9
  /**
15
10
  * Parse the first ~20 lines of a CLI session JSONL file to extract metadata.
package/lib/config.js CHANGED
@@ -18,6 +18,54 @@ if (!fs.existsSync(CLAY_HOME) && fs.existsSync(LEGACY_HOME)) {
18
18
  }
19
19
  }
20
20
 
21
+ // Auto-migrate dev sessions: merge ~/.clay-dev/sessions/ into ~/.clay/sessions/
22
+ var CLAY_DEV_HOME = path.join(os.homedir(), ".clay-dev");
23
+ var devSessionsRoot = path.join(CLAY_DEV_HOME, "sessions");
24
+ if (fs.existsSync(devSessionsRoot)) {
25
+ try {
26
+ var prodSessionsRoot = path.join(CLAY_HOME, "sessions");
27
+ fs.mkdirSync(prodSessionsRoot, { recursive: true });
28
+ var projectDirs = fs.readdirSync(devSessionsRoot);
29
+ var totalMigrated = 0;
30
+ for (var di = 0; di < projectDirs.length; di++) {
31
+ var srcDir = path.join(devSessionsRoot, projectDirs[di]);
32
+ if (!fs.statSync(srcDir).isDirectory()) continue;
33
+ var destDir = path.join(prodSessionsRoot, projectDirs[di]);
34
+ fs.mkdirSync(destDir, { recursive: true });
35
+ var files = fs.readdirSync(srcDir);
36
+ for (var fi = 0; fi < files.length; fi++) {
37
+ if (!files[fi].endsWith(".jsonl")) continue;
38
+ var srcFile = path.join(srcDir, files[fi]);
39
+ var destFile = path.join(destDir, files[fi]);
40
+ if (fs.existsSync(destFile)) continue;
41
+ try {
42
+ fs.renameSync(srcFile, destFile);
43
+ totalMigrated++;
44
+ } catch (e2) {
45
+ try {
46
+ fs.copyFileSync(srcFile, destFile);
47
+ fs.unlinkSync(srcFile);
48
+ totalMigrated++;
49
+ } catch (e3) {}
50
+ }
51
+ }
52
+ // Clean up empty project dir
53
+ try {
54
+ if (fs.readdirSync(srcDir).length === 0) fs.rmdirSync(srcDir);
55
+ } catch (e4) {}
56
+ }
57
+ if (totalMigrated > 0) {
58
+ console.log("[config] Migrated " + totalMigrated + " dev session(s) from " + devSessionsRoot + " → " + prodSessionsRoot);
59
+ }
60
+ // Clean up empty dev sessions root
61
+ try {
62
+ if (fs.readdirSync(devSessionsRoot).length === 0) fs.rmdirSync(devSessionsRoot);
63
+ } catch (e5) {}
64
+ } catch (e) {
65
+ console.error("[config] Dev session migration failed:", e.message);
66
+ }
67
+ }
68
+
21
69
  var CONFIG_DIR = CLAY_HOME;
22
70
  var CLAYRC_PATH = path.join(os.homedir(), ".clayrc");
23
71
  var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json");
@@ -28,7 +76,7 @@ function configPath() {
28
76
 
29
77
  function socketPath() {
30
78
  if (process.platform === "win32") {
31
- var pipeName = process.env.CLAY_HOME ? "clay-dev-daemon" : "clay-daemon";
79
+ var pipeName = "clay-daemon";
32
80
  return "\\\\.\\pipe\\" + pipeName;
33
81
  }
34
82
  return path.join(CONFIG_DIR, "daemon.sock");
@@ -191,28 +239,52 @@ function syncClayrc(projects) {
191
239
  byPath[p.path].slug = p.slug;
192
240
  byPath[p.path].lastUsed = Date.now();
193
241
  if (p.title) byPath[p.path].title = p.title;
194
- else delete byPath[p.path].title;
242
+ else if ("title" in p) delete byPath[p.path].title;
243
+ if (p.icon) byPath[p.path].icon = p.icon;
244
+ else if ("icon" in p) delete byPath[p.path].icon;
195
245
  } else {
196
246
  // New entry
197
247
  byPath[p.path] = {
198
248
  path: p.path,
199
249
  slug: p.slug,
200
250
  title: p.title || undefined,
251
+ icon: p.icon || undefined,
201
252
  addedAt: p.addedAt || Date.now(),
202
253
  lastUsed: Date.now(),
203
254
  };
204
255
  }
205
256
  }
206
257
 
207
- // Rebuild array, sorted by lastUsed descending
208
- var all = Object.keys(byPath).map(function (k) { return byPath[k]; });
209
- all.sort(function (a, b) { return (b.lastUsed || 0) - (a.lastUsed || 0); });
258
+ // Active projects first, preserving config order (user's drag-and-drop order),
259
+ // then inactive recent projects sorted by lastUsed descending
260
+ var activePaths = {};
261
+ var ordered = [];
262
+ for (var k = 0; k < projects.length; k++) {
263
+ activePaths[projects[k].path] = true;
264
+ if (byPath[projects[k].path]) ordered.push(byPath[projects[k].path]);
265
+ }
266
+ var inactive = [];
267
+ var paths = Object.keys(byPath);
268
+ for (var m = 0; m < paths.length; m++) {
269
+ if (!activePaths[paths[m]]) inactive.push(byPath[paths[m]]);
270
+ }
271
+ inactive.sort(function (a, b) { return (b.lastUsed || 0) - (a.lastUsed || 0); });
272
+ var all = ordered.concat(inactive);
210
273
 
211
274
  // Keep at most 20 recent projects
212
275
  rc.recentProjects = all.slice(0, 20);
213
276
  saveClayrc(rc);
214
277
  }
215
278
 
279
+ function removeFromClayrc(projectPath) {
280
+ var rc = loadClayrc();
281
+ var before = (rc.recentProjects || []).length;
282
+ rc.recentProjects = (rc.recentProjects || []).filter(function (p) {
283
+ return p.path !== projectPath;
284
+ });
285
+ if (rc.recentProjects.length !== before) saveClayrc(rc);
286
+ }
287
+
216
288
  module.exports = {
217
289
  CONFIG_DIR: CONFIG_DIR,
218
290
  configPath: configPath,
@@ -234,4 +306,5 @@ module.exports = {
234
306
  loadClayrc: loadClayrc,
235
307
  saveClayrc: saveClayrc,
236
308
  syncClayrc: syncClayrc,
309
+ removeFromClayrc: removeFromClayrc,
237
310
  };
package/lib/daemon.js CHANGED
@@ -22,7 +22,7 @@ delete process.env.CLAUDECODE;
22
22
 
23
23
  var fs = require("fs");
24
24
  var path = require("path");
25
- var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo } = require("./config");
25
+ var { loadConfig, saveConfig, socketPath, generateSlug, syncClayrc, removeFromClayrc, writeCrashInfo, readCrashInfo, clearCrashInfo } = require("./config");
26
26
  var { createIPCServer } = require("./ipc");
27
27
  var { createServer, generateAuthToken } = require("./server");
28
28
 
@@ -117,9 +117,16 @@ var relay = createServer({
117
117
  if (config.projects[j].slug === slug) { found = true; break; }
118
118
  }
119
119
  if (!found) return { ok: false, error: "Project not found" };
120
+ // Find path before removing so we can clean up .clayrc
121
+ var removedPath = null;
122
+ for (var rj = 0; rj < config.projects.length; rj++) {
123
+ if (config.projects[rj].slug === slug) { removedPath = config.projects[rj].path; break; }
124
+ }
120
125
  relay.removeProject(slug);
121
126
  config.projects = config.projects.filter(function (p) { return p.slug !== slug; });
122
127
  saveConfig(config);
128
+ // Remove from .clayrc so it doesn't appear in restore prompt
129
+ if (removedPath) { try { removeFromClayrc(removedPath); } catch (e) {} }
123
130
  try { syncClayrc(config.projects); } catch (e) {}
124
131
  console.log("[daemon] Removed project (web):", slug);
125
132
  relay.broadcastAll({
@@ -129,6 +136,215 @@ var relay = createServer({
129
136
  });
130
137
  return { ok: true };
131
138
  },
139
+ onReorderProjects: function (slugs) {
140
+ // Build a slug->project map from current projects
141
+ var projectMap = {};
142
+ for (var j = 0; j < config.projects.length; j++) {
143
+ projectMap[config.projects[j].slug] = config.projects[j];
144
+ }
145
+ // Reorder based on the slugs array
146
+ var reordered = [];
147
+ for (var k = 0; k < slugs.length; k++) {
148
+ if (projectMap[slugs[k]]) {
149
+ reordered.push(projectMap[slugs[k]]);
150
+ delete projectMap[slugs[k]];
151
+ }
152
+ }
153
+ // Append any remaining projects not in slugs (safety)
154
+ var remaining = Object.keys(projectMap);
155
+ for (var m = 0; m < remaining.length; m++) {
156
+ reordered.push(projectMap[remaining[m]]);
157
+ }
158
+ config.projects = reordered;
159
+ saveConfig(config);
160
+ try { syncClayrc(config.projects); } catch (e) {}
161
+ // Also reorder the in-memory Map so getProjects() returns the new order
162
+ relay.reorderProjects(slugs);
163
+ relay.broadcastAll({
164
+ type: "projects_updated",
165
+ projects: relay.getProjects(),
166
+ projectCount: config.projects.length,
167
+ });
168
+ return { ok: true };
169
+ },
170
+ onSetProjectTitle: function (slug, newTitle) {
171
+ relay.setProjectTitle(slug, newTitle);
172
+ for (var ti = 0; ti < config.projects.length; ti++) {
173
+ if (config.projects[ti].slug === slug) {
174
+ if (newTitle) {
175
+ config.projects[ti].title = newTitle;
176
+ } else {
177
+ delete config.projects[ti].title;
178
+ }
179
+ break;
180
+ }
181
+ }
182
+ saveConfig(config);
183
+ try { syncClayrc(config.projects); } catch (e) {}
184
+ relay.broadcastAll({
185
+ type: "projects_updated",
186
+ projects: relay.getProjects(),
187
+ projectCount: config.projects.length,
188
+ });
189
+ return { ok: true };
190
+ },
191
+ onSetProjectIcon: function (slug, newIcon) {
192
+ relay.setProjectIcon(slug, newIcon);
193
+ for (var ii = 0; ii < config.projects.length; ii++) {
194
+ if (config.projects[ii].slug === slug) {
195
+ if (newIcon) {
196
+ config.projects[ii].icon = newIcon;
197
+ } else {
198
+ delete config.projects[ii].icon;
199
+ }
200
+ break;
201
+ }
202
+ }
203
+ saveConfig(config);
204
+ try { syncClayrc(config.projects); } catch (e) {}
205
+ relay.broadcastAll({
206
+ type: "projects_updated",
207
+ projects: relay.getProjects(),
208
+ projectCount: config.projects.length,
209
+ });
210
+ return { ok: true };
211
+ },
212
+ onGetProjectEnv: function (slug) {
213
+ for (var ei = 0; ei < config.projects.length; ei++) {
214
+ if (config.projects[ei].slug === slug) {
215
+ return { envrc: config.projects[ei].envrc || "" };
216
+ }
217
+ }
218
+ return { envrc: "" };
219
+ },
220
+ onSetProjectEnv: function (slug, envrc) {
221
+ for (var ei = 0; ei < config.projects.length; ei++) {
222
+ if (config.projects[ei].slug === slug) {
223
+ if (envrc) {
224
+ config.projects[ei].envrc = envrc;
225
+ } else {
226
+ delete config.projects[ei].envrc;
227
+ }
228
+ saveConfig(config);
229
+ return { ok: true };
230
+ }
231
+ }
232
+ return { ok: false, error: "Project not found" };
233
+ },
234
+ onGetSharedEnv: function () {
235
+ return { envrc: config.sharedEnv || "" };
236
+ },
237
+ onSetSharedEnv: function (envrc) {
238
+ if (envrc) {
239
+ config.sharedEnv = envrc;
240
+ } else {
241
+ delete config.sharedEnv;
242
+ }
243
+ saveConfig(config);
244
+ return { ok: true };
245
+ },
246
+ onGetServerDefaultEffort: function () {
247
+ return { effort: config.defaultEffort || null };
248
+ },
249
+ onSetServerDefaultEffort: function (effort) {
250
+ if (effort) {
251
+ config.defaultEffort = effort;
252
+ } else {
253
+ delete config.defaultEffort;
254
+ }
255
+ saveConfig(config);
256
+ return { ok: true };
257
+ },
258
+ onGetProjectDefaultEffort: function (slug) {
259
+ for (var i = 0; i < config.projects.length; i++) {
260
+ if (config.projects[i].slug === slug) {
261
+ return { effort: config.projects[i].defaultEffort || null };
262
+ }
263
+ }
264
+ return { effort: null };
265
+ },
266
+ onSetProjectDefaultEffort: function (slug, effort) {
267
+ for (var i = 0; i < config.projects.length; i++) {
268
+ if (config.projects[i].slug === slug) {
269
+ if (effort) {
270
+ config.projects[i].defaultEffort = effort;
271
+ } else {
272
+ delete config.projects[i].defaultEffort;
273
+ }
274
+ saveConfig(config);
275
+ return { ok: true };
276
+ }
277
+ }
278
+ return { ok: false, error: "Project not found" };
279
+ },
280
+ onGetServerDefaultModel: function () {
281
+ return { model: config.defaultModel || null };
282
+ },
283
+ onSetServerDefaultModel: function (model) {
284
+ if (model) {
285
+ config.defaultModel = model;
286
+ } else {
287
+ delete config.defaultModel;
288
+ }
289
+ saveConfig(config);
290
+ return { ok: true };
291
+ },
292
+ onGetProjectDefaultModel: function (slug) {
293
+ for (var i = 0; i < config.projects.length; i++) {
294
+ if (config.projects[i].slug === slug) {
295
+ return { model: config.projects[i].defaultModel || null };
296
+ }
297
+ }
298
+ return { model: null };
299
+ },
300
+ onSetProjectDefaultModel: function (slug, model) {
301
+ for (var i = 0; i < config.projects.length; i++) {
302
+ if (config.projects[i].slug === slug) {
303
+ if (model) {
304
+ config.projects[i].defaultModel = model;
305
+ } else {
306
+ delete config.projects[i].defaultModel;
307
+ }
308
+ saveConfig(config);
309
+ return { ok: true };
310
+ }
311
+ }
312
+ return { ok: false, error: "Project not found" };
313
+ },
314
+ onGetServerDefaultMode: function () {
315
+ return { mode: config.defaultMode || null };
316
+ },
317
+ onSetServerDefaultMode: function (mode) {
318
+ if (mode) {
319
+ config.defaultMode = mode;
320
+ } else {
321
+ delete config.defaultMode;
322
+ }
323
+ saveConfig(config);
324
+ return { ok: true };
325
+ },
326
+ onGetProjectDefaultMode: function (slug) {
327
+ for (var i = 0; i < config.projects.length; i++) {
328
+ if (config.projects[i].slug === slug) {
329
+ return { mode: config.projects[i].defaultMode || null };
330
+ }
331
+ }
332
+ return { mode: null };
333
+ },
334
+ onSetProjectDefaultMode: function (slug, mode) {
335
+ for (var i = 0; i < config.projects.length; i++) {
336
+ if (config.projects[i].slug === slug) {
337
+ if (mode) {
338
+ config.projects[i].defaultMode = mode;
339
+ } else {
340
+ delete config.projects[i].defaultMode;
341
+ }
342
+ saveConfig(config);
343
+ return { ok: true };
344
+ }
345
+ }
346
+ return { ok: false, error: "Project not found" };
347
+ },
132
348
  onGetDaemonConfig: function () {
133
349
  return {
134
350
  port: config.port,
@@ -179,7 +395,7 @@ for (var i = 0; i < projects.length; i++) {
179
395
  var p = projects[i];
180
396
  if (fs.existsSync(p.path)) {
181
397
  console.log("[daemon] Adding project:", p.slug, "→", p.path);
182
- relay.addProject(p.path, p.slug, p.title);
398
+ relay.addProject(p.path, p.slug, p.title, p.icon);
183
399
  } else {
184
400
  console.log("[daemon] Skipping missing project:", p.path);
185
401
  }
@@ -207,6 +423,11 @@ var ipc = createIPCServer(socketPath(), function (msg) {
207
423
  saveConfig(config);
208
424
  try { syncClayrc(config.projects); } catch (e) {}
209
425
  console.log("[daemon] Added project:", slug, "→", absPath);
426
+ relay.broadcastAll({
427
+ type: "projects_updated",
428
+ projects: relay.getProjects(),
429
+ projectCount: config.projects.length,
430
+ });
210
431
  return { ok: true, slug: slug };
211
432
  }
212
433
 
@@ -228,6 +449,11 @@ var ipc = createIPCServer(socketPath(), function (msg) {
228
449
  saveConfig(config);
229
450
  try { syncClayrc(config.projects); } catch (e) {}
230
451
  console.log("[daemon] Removed project:", target);
452
+ relay.broadcastAll({
453
+ type: "projects_updated",
454
+ projects: relay.getProjects(),
455
+ projectCount: config.projects.length,
456
+ });
231
457
  return { ok: true };
232
458
  }
233
459
 
@@ -266,6 +492,11 @@ var ipc = createIPCServer(socketPath(), function (msg) {
266
492
  saveConfig(config);
267
493
  try { syncClayrc(config.projects); } catch (e) {}
268
494
  console.log("[daemon] Project title:", msg.slug, "→", newTitle || "(default)");
495
+ relay.broadcastAll({
496
+ type: "projects_updated",
497
+ projects: relay.getProjects(),
498
+ projectCount: config.projects.length,
499
+ });
269
500
  return { ok: true };
270
501
  }
271
502
 
package/lib/notes.js CHANGED
@@ -2,12 +2,13 @@ var fs = require("fs");
2
2
  var path = require("path");
3
3
  var crypto = require("crypto");
4
4
  var config = require("./config");
5
+ var utils = require("./utils");
5
6
 
6
7
  function createNotesManager(opts) {
7
8
  var cwd = opts.cwd;
8
9
 
9
- // Storage path: ~/.claude-relay/notes/{encodedCwd}.json
10
- var encodedCwd = cwd.replace(/\//g, "-");
10
+ // Storage path: ~/.clay/notes/{encodedCwd}.json
11
+ var encodedCwd = utils.encodeCwd(cwd);
11
12
  var notesDir = path.join(config.CONFIG_DIR, "notes");
12
13
  var notesFile = path.join(notesDir, encodedCwd + ".json");
13
14