clay-server 2.27.0-beta.9 → 2.27.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.
Files changed (71) hide show
  1. package/README.md +10 -0
  2. package/lib/daemon-projects.js +164 -0
  3. package/lib/daemon.js +13 -126
  4. package/lib/mates-identity.js +132 -0
  5. package/lib/mates-knowledge.js +113 -0
  6. package/lib/mates-prompts.js +398 -0
  7. package/lib/mates.js +40 -599
  8. package/lib/project-connection.js +2 -0
  9. package/lib/project-http.js +4 -2
  10. package/lib/project-loop.js +110 -48
  11. package/lib/project-mate-interaction.js +4 -0
  12. package/lib/project-notifications.js +210 -0
  13. package/lib/project-sessions.js +5 -2
  14. package/lib/project-user-message.js +2 -1
  15. package/lib/project.js +26 -2
  16. package/lib/public/app.js +1193 -8517
  17. package/lib/public/css/command-palette.css +14 -0
  18. package/lib/public/css/loop.css +301 -0
  19. package/lib/public/css/notifications-center.css +190 -0
  20. package/lib/public/css/rewind.css +6 -0
  21. package/lib/public/index.html +89 -35
  22. package/lib/public/modules/app-connection.js +160 -0
  23. package/lib/public/modules/app-cursors.js +473 -0
  24. package/lib/public/modules/app-debate-ui.js +389 -0
  25. package/lib/public/modules/app-dm.js +627 -0
  26. package/lib/public/modules/app-favicon.js +212 -0
  27. package/lib/public/modules/app-header.js +229 -0
  28. package/lib/public/modules/app-home-hub.js +600 -0
  29. package/lib/public/modules/app-loop-ui.js +589 -0
  30. package/lib/public/modules/app-loop-wizard.js +439 -0
  31. package/lib/public/modules/app-messages.js +1560 -0
  32. package/lib/public/modules/app-misc.js +299 -0
  33. package/lib/public/modules/app-notifications.js +372 -0
  34. package/lib/public/modules/app-panels.js +888 -0
  35. package/lib/public/modules/app-projects.js +798 -0
  36. package/lib/public/modules/app-rate-limit.js +451 -0
  37. package/lib/public/modules/app-rendering.js +597 -0
  38. package/lib/public/modules/app-skills-install.js +234 -0
  39. package/lib/public/modules/command-palette.js +27 -4
  40. package/lib/public/modules/input.js +31 -20
  41. package/lib/public/modules/scheduler-config.js +1532 -0
  42. package/lib/public/modules/scheduler-history.js +79 -0
  43. package/lib/public/modules/scheduler.js +33 -1554
  44. package/lib/public/modules/session-search.js +13 -1
  45. package/lib/public/modules/sidebar-mates.js +812 -0
  46. package/lib/public/modules/sidebar-mobile.js +1269 -0
  47. package/lib/public/modules/sidebar-projects.js +1449 -0
  48. package/lib/public/modules/sidebar-sessions.js +986 -0
  49. package/lib/public/modules/sidebar.js +232 -4591
  50. package/lib/public/modules/store.js +27 -0
  51. package/lib/public/modules/ws-ref.js +7 -0
  52. package/lib/public/style.css +1 -0
  53. package/lib/sdk-bridge.js +96 -717
  54. package/lib/sdk-message-processor.js +587 -0
  55. package/lib/sdk-message-queue.js +42 -0
  56. package/lib/sdk-skill-discovery.js +131 -0
  57. package/lib/server-admin.js +712 -0
  58. package/lib/server-auth.js +737 -0
  59. package/lib/server-dm.js +221 -0
  60. package/lib/server-mates.js +281 -0
  61. package/lib/server-palette.js +110 -0
  62. package/lib/server-settings.js +479 -0
  63. package/lib/server-skills.js +280 -0
  64. package/lib/server.js +246 -2755
  65. package/lib/sessions.js +11 -4
  66. package/lib/users-auth.js +146 -0
  67. package/lib/users-permissions.js +118 -0
  68. package/lib/users-preferences.js +210 -0
  69. package/lib/users.js +48 -398
  70. package/lib/ws-schema.js +498 -0
  71. package/package.json +1 -1
@@ -0,0 +1,479 @@
1
+ var fs = require("fs");
2
+ var path = require("path");
3
+
4
+ function attachSettings(ctx) {
5
+ var users = ctx.users;
6
+ var mates = ctx.mates;
7
+ var getMultiUserFromReq = ctx.getMultiUserFromReq;
8
+ var projects = ctx.projects;
9
+ var opts = ctx.opts;
10
+ var CONFIG_DIR = ctx.CONFIG_DIR;
11
+
12
+ var profilePath = path.join(CONFIG_DIR, "profile.json");
13
+
14
+ function handleRequest(req, res, fullUrl) {
15
+ // GET /api/profile
16
+ if (req.method === "GET" && fullUrl === "/api/profile") {
17
+ if (users.isMultiUser()) {
18
+ var mu = getMultiUserFromReq(req);
19
+ if (!mu) {
20
+ res.writeHead(401, { "Content-Type": "application/json" });
21
+ res.end('{"error":"unauthorized"}');
22
+ return true;
23
+ }
24
+ var profile = mu.profile || { name: "", lang: "en-US", avatarColor: "#7c3aed", avatarStyle: "thumbs", avatarSeed: "", avatarCustom: "" };
25
+ profile.username = mu.username;
26
+ profile.userId = mu.id;
27
+ profile.role = mu.role;
28
+ profile.autoContinueOnRateLimit = !!mu.autoContinueOnRateLimit;
29
+ profile.chatLayout = mu.chatLayout || "channel";
30
+ profile.mateOnboardingShown = !!mu.mateOnboardingShown;
31
+ res.writeHead(200, { "Content-Type": "application/json" });
32
+ res.end(JSON.stringify(profile));
33
+ return true;
34
+ }
35
+ var profile = { name: "", lang: "en-US", avatarColor: "#7c3aed", avatarStyle: "thumbs", avatarSeed: "", avatarCustom: "" };
36
+ try {
37
+ var raw = fs.readFileSync(profilePath, "utf8");
38
+ var saved = JSON.parse(raw);
39
+ if (saved.name !== undefined) profile.name = saved.name;
40
+ if (saved.lang) profile.lang = saved.lang;
41
+ if (saved.avatarColor) profile.avatarColor = saved.avatarColor;
42
+ if (saved.avatarStyle) profile.avatarStyle = saved.avatarStyle;
43
+ if (saved.avatarSeed) profile.avatarSeed = saved.avatarSeed;
44
+ if (saved.avatarCustom) profile.avatarCustom = saved.avatarCustom;
45
+ } catch (e) { /* file doesn't exist yet */ }
46
+ // Single-user settings from daemon config
47
+ if (typeof opts.onGetDaemonConfig === "function") {
48
+ var dc = opts.onGetDaemonConfig();
49
+ profile.autoContinueOnRateLimit = !!dc.autoContinueOnRateLimit;
50
+ profile.chatLayout = dc.chatLayout || "channel";
51
+ profile.mateOnboardingShown = !!dc.mateOnboardingShown;
52
+ }
53
+ // Check if custom avatar file exists
54
+ try {
55
+ var avatarFiles = fs.readdirSync(path.join(CONFIG_DIR, "avatars"));
56
+ for (var afi = 0; afi < avatarFiles.length; afi++) {
57
+ if (avatarFiles[afi].startsWith("default.")) {
58
+ profile.avatarCustom = "/api/avatar/default?v=" + fs.statSync(path.join(CONFIG_DIR, "avatars", avatarFiles[afi])).mtimeMs;
59
+ break;
60
+ }
61
+ }
62
+ } catch (e) {}
63
+ res.writeHead(200, { "Content-Type": "application/json" });
64
+ res.end(JSON.stringify(profile));
65
+ return true;
66
+ }
67
+
68
+ // PUT /api/profile
69
+ if (req.method === "PUT" && fullUrl === "/api/profile") {
70
+ var body = "";
71
+ req.on("data", function (chunk) { body += chunk; });
72
+ req.on("end", function () {
73
+ try {
74
+ var data = JSON.parse(body);
75
+ var profile = {};
76
+ if (typeof data.name === "string") profile.name = data.name.substring(0, 50);
77
+ if (typeof data.lang === "string") profile.lang = data.lang.substring(0, 10);
78
+ if (typeof data.avatarColor === "string" && /^#[0-9a-fA-F]{6}$/.test(data.avatarColor)) {
79
+ profile.avatarColor = data.avatarColor;
80
+ }
81
+ if (typeof data.avatarStyle === "string") profile.avatarStyle = data.avatarStyle.substring(0, 30);
82
+ if (typeof data.avatarSeed === "string") profile.avatarSeed = data.avatarSeed.substring(0, 30);
83
+ if (typeof data.avatarCustom === "string") profile.avatarCustom = data.avatarCustom;
84
+ if (data.avatarCustom === null || data.avatarCustom === "") profile.avatarCustom = undefined;
85
+ if (users.isMultiUser()) {
86
+ var mu = getMultiUserFromReq(req);
87
+ if (!mu) {
88
+ res.writeHead(401, { "Content-Type": "application/json" });
89
+ res.end('{"error":"unauthorized"}');
90
+ return;
91
+ }
92
+ users.updateUserProfile(mu.id, profile);
93
+ // Broadcast updated avatar/presence to all projects
94
+ projects.forEach(function (pCtx) {
95
+ pCtx.refreshUserProfile(mu.id);
96
+ });
97
+ } else {
98
+ fs.writeFileSync(profilePath, JSON.stringify(profile, null, 2), "utf8");
99
+ if (process.platform !== "win32") {
100
+ try { fs.chmodSync(profilePath, 0o600); } catch (chmodErr) {}
101
+ }
102
+ }
103
+ res.writeHead(200, { "Content-Type": "application/json" });
104
+ res.end(JSON.stringify(profile));
105
+ } catch (e) {
106
+ res.writeHead(400, { "Content-Type": "application/json" });
107
+ res.end(JSON.stringify({ error: "Invalid request" }));
108
+ }
109
+ });
110
+ return true;
111
+ }
112
+
113
+ // Upload custom avatar image
114
+ if (req.method === "POST" && fullUrl === "/api/avatar") {
115
+ var chunks = [];
116
+ var totalSize = 0;
117
+ var maxSize = 2 * 1024 * 1024; // 2MB
118
+ req.on("data", function (chunk) {
119
+ totalSize += chunk.length;
120
+ if (totalSize <= maxSize) chunks.push(chunk);
121
+ });
122
+ req.on("end", function () {
123
+ if (totalSize > maxSize) {
124
+ res.writeHead(413, { "Content-Type": "application/json" });
125
+ res.end('{"error":"File too large (max 2MB)"}');
126
+ return;
127
+ }
128
+ var raw = Buffer.concat(chunks);
129
+ // Detect content type from magic bytes
130
+ var ct = null;
131
+ if (raw[0] === 0xFF && raw[1] === 0xD8) ct = "image/jpeg";
132
+ else if (raw[0] === 0x89 && raw[1] === 0x50) ct = "image/png";
133
+ else if (raw[0] === 0x47 && raw[1] === 0x49) ct = "image/gif";
134
+ else if (raw[0] === 0x52 && raw[1] === 0x49) ct = "image/webp";
135
+ if (!ct) {
136
+ res.writeHead(400, { "Content-Type": "application/json" });
137
+ res.end('{"error":"Unsupported image format"}');
138
+ return;
139
+ }
140
+ var ext = ct.split("/")[1] === "jpeg" ? "jpg" : ct.split("/")[1];
141
+ var avatarDir = path.join(CONFIG_DIR, "avatars");
142
+ fs.mkdirSync(avatarDir, { recursive: true });
143
+
144
+ var userId = "default";
145
+ if (users.isMultiUser()) {
146
+ var mu = getMultiUserFromReq(req);
147
+ if (!mu) {
148
+ res.writeHead(401, { "Content-Type": "application/json" });
149
+ res.end('{"error":"unauthorized"}');
150
+ return;
151
+ }
152
+ userId = mu.id;
153
+ }
154
+ var filename = userId + "." + ext;
155
+ // Remove old avatar files for this user
156
+ try {
157
+ var existing = fs.readdirSync(avatarDir);
158
+ for (var ei = 0; ei < existing.length; ei++) {
159
+ if (existing[ei].startsWith(userId + ".")) {
160
+ fs.unlinkSync(path.join(avatarDir, existing[ei]));
161
+ }
162
+ }
163
+ } catch (e) {}
164
+ var avatarFilePath = path.join(avatarDir, filename);
165
+ fs.writeFileSync(avatarFilePath, raw);
166
+ try { fs.chmodSync(avatarFilePath, 0o644); } catch (e) {}
167
+ try { fs.chmodSync(avatarDir, 0o755); } catch (e) {}
168
+ res.writeHead(200, { "Content-Type": "application/json" });
169
+ res.end(JSON.stringify({ ok: true, avatar: "/api/avatar/" + userId + "?v=" + Date.now() }));
170
+ });
171
+ return true;
172
+ }
173
+
174
+ // Serve custom avatar image
175
+ if (req.method === "GET" && fullUrl.startsWith("/api/avatar/")) {
176
+ var avatarUserId = fullUrl.split("/api/avatar/")[1].split("?")[0];
177
+ var avatarDir = path.join(CONFIG_DIR, "avatars");
178
+ try {
179
+ var files = fs.readdirSync(avatarDir);
180
+ var match = null;
181
+ for (var fi = 0; fi < files.length; fi++) {
182
+ if (files[fi].startsWith(avatarUserId + ".")) {
183
+ match = files[fi];
184
+ break;
185
+ }
186
+ }
187
+ if (match) {
188
+ var ext = match.split(".").pop();
189
+ var ctMap = { jpg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp" };
190
+ res.writeHead(200, {
191
+ "Content-Type": ctMap[ext] || "application/octet-stream",
192
+ "Cache-Control": "public, max-age=31536000, immutable",
193
+ });
194
+ res.end(fs.readFileSync(path.join(avatarDir, match)));
195
+ return true;
196
+ }
197
+ } catch (e) {}
198
+ res.writeHead(404, { "Content-Type": "application/json" });
199
+ res.end('{"error":"not found"}');
200
+ return true;
201
+ }
202
+
203
+ // Upload custom avatar for a mate
204
+ if (req.method === "POST" && fullUrl.startsWith("/api/mate-avatar/")) {
205
+ var mateIdFromUrl = fullUrl.split("/api/mate-avatar/")[1].split("?")[0];
206
+ if (!mateIdFromUrl) {
207
+ res.writeHead(400, { "Content-Type": "application/json" });
208
+ res.end('{"error":"Missing mate ID"}');
209
+ return true;
210
+ }
211
+ var chunks = [];
212
+ var totalSize = 0;
213
+ var maxSize = 2 * 1024 * 1024; // 2MB
214
+ req.on("data", function (chunk) {
215
+ totalSize += chunk.length;
216
+ if (totalSize <= maxSize) chunks.push(chunk);
217
+ });
218
+ req.on("end", function () {
219
+ if (totalSize > maxSize) {
220
+ res.writeHead(413, { "Content-Type": "application/json" });
221
+ res.end('{"error":"File too large (max 2MB)"}');
222
+ return;
223
+ }
224
+ var raw = Buffer.concat(chunks);
225
+ var ct = null;
226
+ if (raw[0] === 0xFF && raw[1] === 0xD8) ct = "image/jpeg";
227
+ else if (raw[0] === 0x89 && raw[1] === 0x50) ct = "image/png";
228
+ else if (raw[0] === 0x47 && raw[1] === 0x49) ct = "image/gif";
229
+ else if (raw[0] === 0x52 && raw[1] === 0x49) ct = "image/webp";
230
+ if (!ct) {
231
+ res.writeHead(400, { "Content-Type": "application/json" });
232
+ res.end('{"error":"Unsupported image format"}');
233
+ return;
234
+ }
235
+ var userId = null;
236
+ if (users.isMultiUser()) {
237
+ var mu = getMultiUserFromReq(req);
238
+ if (!mu) {
239
+ res.writeHead(401, { "Content-Type": "application/json" });
240
+ res.end('{"error":"unauthorized"}');
241
+ return;
242
+ }
243
+ userId = mu.id;
244
+ }
245
+ var mateCtx = mates.buildMateCtx(userId);
246
+ var mate = mates.getMate(mateCtx, mateIdFromUrl);
247
+ if (!mate) {
248
+ res.writeHead(404, { "Content-Type": "application/json" });
249
+ res.end('{"error":"Mate not found"}');
250
+ return;
251
+ }
252
+ var ext = ct.split("/")[1] === "jpeg" ? "jpg" : ct.split("/")[1];
253
+ var avatarDir = path.join(CONFIG_DIR, "mate-avatars");
254
+ fs.mkdirSync(avatarDir, { recursive: true });
255
+ var filename = mateIdFromUrl + "." + ext;
256
+ // Remove old avatar files for this mate
257
+ try {
258
+ var existing = fs.readdirSync(avatarDir);
259
+ for (var ei = 0; ei < existing.length; ei++) {
260
+ if (existing[ei].startsWith(mateIdFromUrl + ".")) {
261
+ fs.unlinkSync(path.join(avatarDir, existing[ei]));
262
+ }
263
+ }
264
+ } catch (e) {}
265
+ var mateAvatarFilePath = path.join(avatarDir, filename);
266
+ fs.writeFileSync(mateAvatarFilePath, raw);
267
+ try { fs.chmodSync(mateAvatarFilePath, 0o644); } catch (e) {}
268
+ try { fs.chmodSync(avatarDir, 0o755); } catch (e) {}
269
+ var avatarPath = "/api/mate-avatar/" + mateIdFromUrl + "?v=" + Date.now();
270
+ // Update mate profile with custom avatar URL
271
+ var profile = mate.profile || {};
272
+ profile.avatarCustom = avatarPath;
273
+ mates.updateMate(mateCtx, mateIdFromUrl, { profile: profile });
274
+ res.writeHead(200, { "Content-Type": "application/json" });
275
+ res.end(JSON.stringify({ ok: true, avatar: avatarPath }));
276
+ });
277
+ return true;
278
+ }
279
+
280
+ // Serve custom mate avatar image
281
+ if (req.method === "GET" && fullUrl.startsWith("/api/mate-avatar/")) {
282
+ var mateAvatarId = fullUrl.split("/api/mate-avatar/")[1].split("?")[0];
283
+ var mateAvatarDir = path.join(CONFIG_DIR, "mate-avatars");
284
+ try {
285
+ var files = fs.readdirSync(mateAvatarDir);
286
+ var match = null;
287
+ for (var fi = 0; fi < files.length; fi++) {
288
+ if (files[fi].startsWith(mateAvatarId + ".")) {
289
+ match = files[fi];
290
+ break;
291
+ }
292
+ }
293
+ if (match) {
294
+ var ext = match.split(".").pop();
295
+ var ctMap = { jpg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp" };
296
+ res.writeHead(200, {
297
+ "Content-Type": ctMap[ext] || "application/octet-stream",
298
+ "Cache-Control": "public, max-age=31536000, immutable",
299
+ });
300
+ res.end(fs.readFileSync(path.join(mateAvatarDir, match)));
301
+ return true;
302
+ }
303
+ } catch (e) {}
304
+ res.writeHead(404, { "Content-Type": "application/json" });
305
+ res.end('{"error":"not found"}');
306
+ return true;
307
+ }
308
+
309
+ // Change own PIN (multi-user mode)
310
+ if (req.method === "PUT" && fullUrl === "/api/user/pin") {
311
+ if (!users.isMultiUser()) {
312
+ res.writeHead(404, { "Content-Type": "application/json" });
313
+ res.end('{"error":"Not found"}');
314
+ return true;
315
+ }
316
+ var mu = getMultiUserFromReq(req);
317
+ if (!mu) {
318
+ res.writeHead(401, { "Content-Type": "application/json" });
319
+ res.end('{"error":"unauthorized"}');
320
+ return true;
321
+ }
322
+ var body = "";
323
+ req.on("data", function (chunk) { body += chunk; });
324
+ req.on("end", function () {
325
+ try {
326
+ var data = JSON.parse(body);
327
+ if (!data.newPin || typeof data.newPin !== "string" || !/^\d{6}$/.test(data.newPin)) {
328
+ res.writeHead(400, { "Content-Type": "application/json" });
329
+ res.end('{"error":"PIN must be exactly 6 digits"}');
330
+ return;
331
+ }
332
+ var result = users.updateUserPin(mu.id, data.newPin);
333
+ if (result.error) {
334
+ res.writeHead(400, { "Content-Type": "application/json" });
335
+ res.end(JSON.stringify({ error: result.error }));
336
+ return;
337
+ }
338
+ res.writeHead(200, { "Content-Type": "application/json" });
339
+ res.end('{"ok":true}');
340
+ } catch (e) {
341
+ res.writeHead(400, { "Content-Type": "application/json" });
342
+ res.end('{"error":"Invalid request"}');
343
+ }
344
+ });
345
+ return true;
346
+ }
347
+
348
+ // PUT /api/user/auto-continue
349
+ if (req.method === "PUT" && fullUrl === "/api/user/auto-continue") {
350
+ var mu = getMultiUserFromReq(req);
351
+ if (!mu) {
352
+ // Single-user: use daemon config fallback
353
+ var body = "";
354
+ req.on("data", function (chunk) { body += chunk; });
355
+ req.on("end", function () {
356
+ try {
357
+ var data = JSON.parse(body);
358
+ if (typeof opts.onSetAutoContinue === "function") {
359
+ opts.onSetAutoContinue(!!data.enabled);
360
+ }
361
+ res.writeHead(200, { "Content-Type": "application/json" });
362
+ res.end(JSON.stringify({ ok: true, autoContinueOnRateLimit: !!data.enabled }));
363
+ } catch (e) {
364
+ res.writeHead(400, { "Content-Type": "application/json" });
365
+ res.end('{"error":"Invalid request"}');
366
+ }
367
+ });
368
+ return true;
369
+ }
370
+ var body = "";
371
+ req.on("data", function (chunk) { body += chunk; });
372
+ req.on("end", function () {
373
+ try {
374
+ var data = JSON.parse(body);
375
+ var result = users.setAutoContinue(mu.id, !!data.enabled);
376
+ if (result.error) {
377
+ res.writeHead(400, { "Content-Type": "application/json" });
378
+ res.end(JSON.stringify({ error: result.error }));
379
+ return;
380
+ }
381
+ res.writeHead(200, { "Content-Type": "application/json" });
382
+ res.end(JSON.stringify({ ok: true, autoContinueOnRateLimit: result.autoContinueOnRateLimit }));
383
+ } catch (e) {
384
+ res.writeHead(400, { "Content-Type": "application/json" });
385
+ res.end('{"error":"Invalid request"}');
386
+ }
387
+ });
388
+ return true;
389
+ }
390
+
391
+ // PUT /api/user/chat-layout
392
+ if (req.method === "PUT" && fullUrl === "/api/user/chat-layout") {
393
+ var mu = getMultiUserFromReq(req);
394
+ if (!mu) {
395
+ // Single-user: save to daemon config
396
+ var body = "";
397
+ req.on("data", function (chunk) { body += chunk; });
398
+ req.on("end", function () {
399
+ try {
400
+ var data = JSON.parse(body);
401
+ var val = (data.layout === "bubble") ? "bubble" : "channel";
402
+ if (typeof opts.onSetChatLayout === "function") {
403
+ opts.onSetChatLayout(val);
404
+ }
405
+ res.writeHead(200, { "Content-Type": "application/json" });
406
+ res.end(JSON.stringify({ ok: true, chatLayout: val }));
407
+ } catch (e) {
408
+ res.writeHead(400, { "Content-Type": "application/json" });
409
+ res.end('{"error":"Invalid request"}');
410
+ }
411
+ });
412
+ return true;
413
+ }
414
+ var body = "";
415
+ req.on("data", function (chunk) { body += chunk; });
416
+ req.on("end", function () {
417
+ try {
418
+ var data = JSON.parse(body);
419
+ var result = users.setChatLayout(mu.id, data.layout);
420
+ if (result.error) {
421
+ res.writeHead(400, { "Content-Type": "application/json" });
422
+ res.end(JSON.stringify({ error: result.error }));
423
+ return;
424
+ }
425
+ res.writeHead(200, { "Content-Type": "application/json" });
426
+ res.end(JSON.stringify({ ok: true, chatLayout: result.chatLayout }));
427
+ } catch (e) {
428
+ res.writeHead(400, { "Content-Type": "application/json" });
429
+ res.end('{"error":"Invalid request"}');
430
+ }
431
+ });
432
+ return true;
433
+ }
434
+
435
+ // POST /api/user/mate-onboarded
436
+ if (req.method === "POST" && fullUrl === "/api/user/mate-onboarded") {
437
+ var mu = getMultiUserFromReq(req);
438
+ if (!mu) {
439
+ // Single-user: save to daemon config
440
+ if (typeof opts.onSetMateOnboarded === "function") {
441
+ opts.onSetMateOnboarded();
442
+ }
443
+ res.writeHead(200, { "Content-Type": "application/json" });
444
+ res.end('{"ok":true}');
445
+ } else {
446
+ users.setMateOnboarded(mu.id);
447
+ res.writeHead(200, { "Content-Type": "application/json" });
448
+ res.end('{"ok":true}');
449
+ }
450
+ return true;
451
+ }
452
+
453
+ // GET /api/user/auto-continue
454
+ if (req.method === "GET" && fullUrl === "/api/user/auto-continue") {
455
+ var mu = getMultiUserFromReq(req);
456
+ if (!mu) {
457
+ // Single-user: read from daemon config
458
+ var enabled = false;
459
+ if (typeof opts.onGetDaemonConfig === "function") {
460
+ var dc = opts.onGetDaemonConfig();
461
+ enabled = !!dc.autoContinueOnRateLimit;
462
+ }
463
+ res.writeHead(200, { "Content-Type": "application/json" });
464
+ res.end(JSON.stringify({ autoContinueOnRateLimit: enabled }));
465
+ return true;
466
+ }
467
+ var val = users.getAutoContinue(mu.id);
468
+ res.writeHead(200, { "Content-Type": "application/json" });
469
+ res.end(JSON.stringify({ autoContinueOnRateLimit: val }));
470
+ return true;
471
+ }
472
+
473
+ return false;
474
+ }
475
+
476
+ return { handleRequest: handleRequest };
477
+ }
478
+
479
+ module.exports = { attachSettings: attachSettings };