@lifeaitools/clauth 1.9.2 → 1.16.9
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/.clauth-skill/SKILL.md +80 -22
- package/README.md +85 -42
- package/cli/api.js +47 -36
- package/cli/assets/codevelop/launcher-active.cmd.template +20 -20
- package/cli/assets/codevelop/launcher-static.cmd.template +7 -7
- package/cli/assets/codevelop/windows-terminal.profiles.json +48 -48
- package/cli/assets/watchdog.ps1 +42 -42
- package/cli/commands/agent-cron.js +396 -0
- package/cli/commands/agent-pool.js +1745 -0
- package/cli/commands/codevelop.js +1190 -1190
- package/cli/commands/doctor.js +302 -302
- package/cli/commands/install.js +10 -10
- package/cli/commands/invite.js +175 -175
- package/cli/commands/join.js +179 -179
- package/cli/commands/npm.js +182 -182
- package/cli/commands/scrub.js +205 -109
- package/cli/commands/scrub.test.js +115 -0
- package/cli/commands/serve.js +11716 -10733
- package/cli/commands/watchdog.js +209 -209
- package/cli/conf-path.js +21 -21
- package/cli/index.js +1089 -1085
- package/cli/lib/fs-git.js +282 -282
- package/cli/recovery.js +101 -101
- package/cli/studio-debug.js +487 -424
- package/cli/watchdog-registry.js +209 -209
- package/cli/watchdog-registry.test.js +89 -89
- package/install.ps1 +21 -21
- package/package.json +65 -61
- package/scripts/postinstall.js +164 -164
- package/supabase/migrations/001_clauth_schema.sql +12 -12
- package/supabase/migrations/003_clauth_config.sql +13 -13
- package/supabase/migrations/003_machine_enrollments.sql +39 -39
package/cli/studio-debug.js
CHANGED
|
@@ -1,424 +1,487 @@
|
|
|
1
|
-
import crypto from "crypto";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import os from "os";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import { spawn } from "child_process";
|
|
6
|
-
|
|
7
|
-
const DEFAULT_POLL_TIMEOUT = 600_000;
|
|
8
|
-
const MAX_POLL_TIMEOUT = 600_000;
|
|
9
|
-
const MAX_EVENT_QUEUE = 100;
|
|
10
|
-
const MAX_SNIPPET = 2_000;
|
|
11
|
-
|
|
12
|
-
const APP_TARGETS = {
|
|
13
|
-
studio_test: {
|
|
14
|
-
appSlug: "studio_test",
|
|
15
|
-
brandSlug: "studio_test",
|
|
16
|
-
packageName: "@regen/studio",
|
|
17
|
-
command: "pnpm --filter @regen/studio dev",
|
|
18
|
-
url: "http://localhost:3011/editor/local-test-target",
|
|
19
|
-
},
|
|
20
|
-
"studio-test": {
|
|
21
|
-
appSlug: "studio_test",
|
|
22
|
-
brandSlug: "studio_test",
|
|
23
|
-
packageName: "@regen/studio",
|
|
24
|
-
command: "pnpm --filter @regen/studio dev",
|
|
25
|
-
url: "http://localhost:3011/editor/local-test-target",
|
|
26
|
-
},
|
|
27
|
-
prt: {
|
|
28
|
-
appSlug: "prt",
|
|
29
|
-
brandSlug: "prt",
|
|
30
|
-
packageName: "@regen/prt-portal",
|
|
31
|
-
command: "pnpm --filter @regen/prt-portal dev",
|
|
32
|
-
url: "http://localhost:3006",
|
|
33
|
-
},
|
|
34
|
-
"prt-portal": {
|
|
35
|
-
appSlug: "prt",
|
|
36
|
-
brandSlug: "prt",
|
|
37
|
-
packageName: "@regen/prt-portal",
|
|
38
|
-
command: "pnpm --filter @regen/prt-portal dev",
|
|
39
|
-
url: "http://localhost:3006",
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function nowIso() {
|
|
44
|
-
return new Date().toISOString();
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function safeString(value, max = MAX_SNIPPET) {
|
|
48
|
-
if (typeof value !== "string") return value;
|
|
49
|
-
return value.length > max ? value.slice(0, max) : value;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function normalizeEvent(session, body) {
|
|
53
|
-
const type = body.type || body.mode || "direct_edit";
|
|
54
|
-
const id = body.id || `evt_${crypto.randomUUID()}`;
|
|
55
|
-
const target = body.target && typeof body.target === "object" ? { ...body.target } : {};
|
|
56
|
-
if (typeof target.textSnippet === "string") target.textSnippet = safeString(target.textSnippet, 500);
|
|
57
|
-
if (typeof target.outerHTMLSnippet === "string") target.outerHTMLSnippet = safeString(target.outerHTMLSnippet, MAX_SNIPPET);
|
|
58
|
-
if (typeof target.outerHTML === "string" && !target.outerHTMLSnippet) {
|
|
59
|
-
target.outerHTMLSnippet = safeString(target.outerHTML, MAX_SNIPPET);
|
|
60
|
-
delete target.outerHTML;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
type,
|
|
65
|
-
id,
|
|
66
|
-
mode: body.mode || type,
|
|
67
|
-
sessionId: session.sessionId,
|
|
68
|
-
brandSlug: session.brandSlug,
|
|
69
|
-
appSlug: session.appSlug,
|
|
70
|
-
target,
|
|
71
|
-
reference: body.reference || null,
|
|
72
|
-
instruction: safeString(body.instruction || body.prompt || "", 4_000),
|
|
73
|
-
createdAt: nowIso(),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function readBody(req, maxBytes = 128 * 1024) {
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
let data = "";
|
|
80
|
-
req.on("data", chunk => {
|
|
81
|
-
data += chunk;
|
|
82
|
-
if (data.length > maxBytes) reject(new Error("Body too large"));
|
|
83
|
-
});
|
|
84
|
-
req.on("end", () => {
|
|
85
|
-
if (!data.trim()) return resolve({});
|
|
86
|
-
try {
|
|
87
|
-
resolve(JSON.parse(data));
|
|
88
|
-
} catch {
|
|
89
|
-
reject(new Error("Invalid JSON"));
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
req.on("error", reject);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function writeJson(res, status, data, cors) {
|
|
97
|
-
res.writeHead(status, { "Content-Type": "application/json", ...cors });
|
|
98
|
-
res.end(JSON.stringify(data));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function isUrlReachable(url) {
|
|
102
|
-
try {
|
|
103
|
-
const response = await fetch(url, {
|
|
104
|
-
method: "GET",
|
|
105
|
-
signal: AbortSignal.timeout(1_500),
|
|
106
|
-
});
|
|
107
|
-
return response.status < 500;
|
|
108
|
-
} catch {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function resolveTarget(input = {}) {
|
|
114
|
-
const slug = String(input.appSlug || input.brandSlug || "prt").toLowerCase();
|
|
115
|
-
const configured = APP_TARGETS[slug];
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const parts = command
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
fs.
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
session.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_POLL_TIMEOUT = 600_000;
|
|
8
|
+
const MAX_POLL_TIMEOUT = 600_000;
|
|
9
|
+
const MAX_EVENT_QUEUE = 100;
|
|
10
|
+
const MAX_SNIPPET = 2_000;
|
|
11
|
+
|
|
12
|
+
const APP_TARGETS = {
|
|
13
|
+
studio_test: {
|
|
14
|
+
appSlug: "studio_test",
|
|
15
|
+
brandSlug: "studio_test",
|
|
16
|
+
packageName: "@regen/studio",
|
|
17
|
+
command: "pnpm --filter @regen/studio dev",
|
|
18
|
+
url: "http://localhost:3011/editor/local-test-target",
|
|
19
|
+
},
|
|
20
|
+
"studio-test": {
|
|
21
|
+
appSlug: "studio_test",
|
|
22
|
+
brandSlug: "studio_test",
|
|
23
|
+
packageName: "@regen/studio",
|
|
24
|
+
command: "pnpm --filter @regen/studio dev",
|
|
25
|
+
url: "http://localhost:3011/editor/local-test-target",
|
|
26
|
+
},
|
|
27
|
+
prt: {
|
|
28
|
+
appSlug: "prt",
|
|
29
|
+
brandSlug: "prt",
|
|
30
|
+
packageName: "@regen/prt-portal",
|
|
31
|
+
command: "pnpm --filter @regen/prt-portal dev",
|
|
32
|
+
url: "http://localhost:3006",
|
|
33
|
+
},
|
|
34
|
+
"prt-portal": {
|
|
35
|
+
appSlug: "prt",
|
|
36
|
+
brandSlug: "prt",
|
|
37
|
+
packageName: "@regen/prt-portal",
|
|
38
|
+
command: "pnpm --filter @regen/prt-portal dev",
|
|
39
|
+
url: "http://localhost:3006",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function nowIso() {
|
|
44
|
+
return new Date().toISOString();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function safeString(value, max = MAX_SNIPPET) {
|
|
48
|
+
if (typeof value !== "string") return value;
|
|
49
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeEvent(session, body) {
|
|
53
|
+
const type = body.type || body.mode || "direct_edit";
|
|
54
|
+
const id = body.id || `evt_${crypto.randomUUID()}`;
|
|
55
|
+
const target = body.target && typeof body.target === "object" ? { ...body.target } : {};
|
|
56
|
+
if (typeof target.textSnippet === "string") target.textSnippet = safeString(target.textSnippet, 500);
|
|
57
|
+
if (typeof target.outerHTMLSnippet === "string") target.outerHTMLSnippet = safeString(target.outerHTMLSnippet, MAX_SNIPPET);
|
|
58
|
+
if (typeof target.outerHTML === "string" && !target.outerHTMLSnippet) {
|
|
59
|
+
target.outerHTMLSnippet = safeString(target.outerHTML, MAX_SNIPPET);
|
|
60
|
+
delete target.outerHTML;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
type,
|
|
65
|
+
id,
|
|
66
|
+
mode: body.mode || type,
|
|
67
|
+
sessionId: session.sessionId,
|
|
68
|
+
brandSlug: session.brandSlug,
|
|
69
|
+
appSlug: session.appSlug,
|
|
70
|
+
target,
|
|
71
|
+
reference: body.reference || null,
|
|
72
|
+
instruction: safeString(body.instruction || body.prompt || "", 4_000),
|
|
73
|
+
createdAt: nowIso(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readBody(req, maxBytes = 128 * 1024) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
let data = "";
|
|
80
|
+
req.on("data", chunk => {
|
|
81
|
+
data += chunk;
|
|
82
|
+
if (data.length > maxBytes) reject(new Error("Body too large"));
|
|
83
|
+
});
|
|
84
|
+
req.on("end", () => {
|
|
85
|
+
if (!data.trim()) return resolve({});
|
|
86
|
+
try {
|
|
87
|
+
resolve(JSON.parse(data));
|
|
88
|
+
} catch {
|
|
89
|
+
reject(new Error("Invalid JSON"));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
req.on("error", reject);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeJson(res, status, data, cors) {
|
|
97
|
+
res.writeHead(status, { "Content-Type": "application/json", ...cors });
|
|
98
|
+
res.end(JSON.stringify(data));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function isUrlReachable(url) {
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(url, {
|
|
104
|
+
method: "GET",
|
|
105
|
+
signal: AbortSignal.timeout(1_500),
|
|
106
|
+
});
|
|
107
|
+
return response.status < 500;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveTarget(input = {}) {
|
|
114
|
+
const slug = String(input.appSlug || input.brandSlug || "prt").toLowerCase();
|
|
115
|
+
const configured = APP_TARGETS[slug];
|
|
116
|
+
// No registration required — any appSlug proceeds. Sessions without a devUrl
|
|
117
|
+
// or devCommand simply have no iframe target; the picker still works via extension.
|
|
118
|
+
|
|
119
|
+
const base = configured || {
|
|
120
|
+
appSlug: slug,
|
|
121
|
+
brandSlug: input.brandSlug || slug,
|
|
122
|
+
command: input.devCommand,
|
|
123
|
+
url: input.devUrl,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
appSlug: input.appSlug || base.appSlug,
|
|
128
|
+
brandSlug: input.brandSlug || base.brandSlug || input.appSlug || base.appSlug,
|
|
129
|
+
command: input.devCommand || base.command,
|
|
130
|
+
url: input.devUrl || base.url,
|
|
131
|
+
cwd: input.cwd || input.repoRoot || process.cwd(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function splitCommand(command) {
|
|
136
|
+
if (!command || typeof command !== "string") return null;
|
|
137
|
+
const parts = command.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
138
|
+
return parts.map(part => part.replace(/^"|"$/g, ""));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function startDevProcess(target, logFile) {
|
|
142
|
+
const parts = splitCommand(target.command);
|
|
143
|
+
if (!parts || parts.length === 0) {
|
|
144
|
+
return { started: false, error: "missing_command" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const logDir = path.join(os.tmpdir(), "clauth-studio-debug");
|
|
148
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
149
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
150
|
+
const outPath = path.join(logDir, `${target.appSlug}-${stamp}.out.log`);
|
|
151
|
+
const errPath = path.join(logDir, `${target.appSlug}-${stamp}.err.log`);
|
|
152
|
+
const out = fs.openSync(outPath, "a");
|
|
153
|
+
const err = fs.openSync(errPath, "a");
|
|
154
|
+
|
|
155
|
+
const proc = spawn(parts[0], parts.slice(1), {
|
|
156
|
+
cwd: target.cwd,
|
|
157
|
+
env: process.env,
|
|
158
|
+
stdio: ["ignore", out, err],
|
|
159
|
+
shell: process.platform === "win32",
|
|
160
|
+
detached: true,
|
|
161
|
+
windowsHide: true,
|
|
162
|
+
});
|
|
163
|
+
proc.unref();
|
|
164
|
+
try {
|
|
165
|
+
fs.appendFileSync(logFile, `[${nowIso()}] studio-debug dev start pid=${proc.pid} command=${target.command}\n`);
|
|
166
|
+
} catch {}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
started: true,
|
|
170
|
+
pid: proc.pid,
|
|
171
|
+
command: target.command,
|
|
172
|
+
url: target.url,
|
|
173
|
+
stdout: outPath,
|
|
174
|
+
stderr: errPath,
|
|
175
|
+
process: proc,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export class StudioDebugSessionStore {
|
|
180
|
+
constructor({ port = 52437, logFile = path.join(os.tmpdir(), "clauth-serve.log"), dispatchAgent = null } = {}) {
|
|
181
|
+
this.port = port;
|
|
182
|
+
this.logFile = logFile;
|
|
183
|
+
this.sessions = new Map();
|
|
184
|
+
this.dispatchAgent = dispatchAgent;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async start(input = {}) {
|
|
188
|
+
const target = resolveTarget(input);
|
|
189
|
+
if (target.error) {
|
|
190
|
+
return { ok: false, error: target.error, message: target.message, status: "error" };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const shouldLaunch = input.launchDevServer !== false;
|
|
194
|
+
const reachable = await isUrlReachable(target.url);
|
|
195
|
+
const launch = reachable
|
|
196
|
+
? { started: false, command: target.command, url: target.url, alreadyRunning: true }
|
|
197
|
+
: shouldLaunch
|
|
198
|
+
? startDevProcess(target, this.logFile)
|
|
199
|
+
: { started: false, command: target.command, url: target.url, skipped: true };
|
|
200
|
+
|
|
201
|
+
const sessionId = input.sessionId || `studio-${crypto.randomUUID()}`;
|
|
202
|
+
const token = crypto.randomBytes(24).toString("base64url");
|
|
203
|
+
const session = {
|
|
204
|
+
sessionId,
|
|
205
|
+
token,
|
|
206
|
+
brandSlug: target.brandSlug,
|
|
207
|
+
appSlug: target.appSlug,
|
|
208
|
+
repoRoot: input.repoRoot || target.cwd,
|
|
209
|
+
cwd: target.cwd,
|
|
210
|
+
devCommand: target.command,
|
|
211
|
+
devUrl: target.url,
|
|
212
|
+
modeDefault: input.modeDefault || "direct_edit",
|
|
213
|
+
status: "waiting_for_agent",
|
|
214
|
+
createdAt: nowIso(),
|
|
215
|
+
updatedAt: nowIso(),
|
|
216
|
+
stopped: false,
|
|
217
|
+
events: [],
|
|
218
|
+
replies: [],
|
|
219
|
+
pendingPolls: [],
|
|
220
|
+
launch,
|
|
221
|
+
};
|
|
222
|
+
this.sessions.set(sessionId, session);
|
|
223
|
+
|
|
224
|
+
const relayBaseUrl = `http://127.0.0.1:${this.port}/studio/debug/${sessionId}`;
|
|
225
|
+
const claudeBaseUrl = `http://127.0.0.1:${this.port}/studio/claude/${sessionId}`;
|
|
226
|
+
const pollCommand = `node scripts/studio-debug-poll.mjs --session ${sessionId} --token ${token} --relay ${claudeBaseUrl}`;
|
|
227
|
+
|
|
228
|
+
// When dispatchAgent is wired, events dispatch directly — no polling agent needed.
|
|
229
|
+
if (this.dispatchAgent) {
|
|
230
|
+
session.status = "waiting_for_event";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
ok: true,
|
|
235
|
+
sessionId,
|
|
236
|
+
token,
|
|
237
|
+
devUrl: target.url,
|
|
238
|
+
relayBaseUrl,
|
|
239
|
+
status: session.status,
|
|
240
|
+
launch: {
|
|
241
|
+
started: !!launch.started,
|
|
242
|
+
alreadyRunning: !!launch.alreadyRunning,
|
|
243
|
+
command: launch.command,
|
|
244
|
+
url: launch.url,
|
|
245
|
+
pid: launch.pid || null,
|
|
246
|
+
},
|
|
247
|
+
pollCommand,
|
|
248
|
+
claudeBaseUrl,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
get(sessionId) {
|
|
253
|
+
return this.sessions.get(sessionId) || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
authenticate(sessionId, token) {
|
|
257
|
+
const session = this.get(sessionId);
|
|
258
|
+
if (!session) return { error: "not_found" };
|
|
259
|
+
if (session.token !== token) return { error: "unauthorized" };
|
|
260
|
+
return { session };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
submitEvent(sessionId, body) {
|
|
264
|
+
const auth = this.authenticate(sessionId, body.token);
|
|
265
|
+
if (auth.error) return auth;
|
|
266
|
+
const session = auth.session;
|
|
267
|
+
const event = normalizeEvent(session, body);
|
|
268
|
+
|
|
269
|
+
session.updatedAt = nowIso();
|
|
270
|
+
session.status = "event_pending";
|
|
271
|
+
|
|
272
|
+
// If a dispatchAgent handler exists, spawn a full headless Claude agent.
|
|
273
|
+
if (this.dispatchAgent) {
|
|
274
|
+
// Stash images from the event body so the dispatcher can write them to tmp
|
|
275
|
+
if (Array.isArray(body.images) && body.images.length > 0) {
|
|
276
|
+
session._pendingImages = body.images;
|
|
277
|
+
}
|
|
278
|
+
const prompt = [
|
|
279
|
+
`You are a Studio local-debug agent. Make ONE source edit to the codebase.`,
|
|
280
|
+
``,
|
|
281
|
+
`App: ${session.appSlug} (brand: ${session.brandSlug})`,
|
|
282
|
+
`Repo: ${session.repoRoot || session.cwd}`,
|
|
283
|
+
`Dev URL: ${session.devUrl || "unknown"}`,
|
|
284
|
+
``,
|
|
285
|
+
`Edit event:`,
|
|
286
|
+
` Mode: ${event.mode || "direct_edit"}`,
|
|
287
|
+
` Selector: ${event.selector || "unknown"}`,
|
|
288
|
+
` Instruction: ${event.instruction || "Make the requested edit."}`,
|
|
289
|
+
event.textSnippet ? ` Current text: "${event.textSnippet}"` : "",
|
|
290
|
+
event.textEdit ? ` New text: "${event.textEdit.newText || ""}"` : "",
|
|
291
|
+
event.file ? ` File hint: ${event.file}` : "",
|
|
292
|
+
event.notes ? ` Notes: ${event.notes}` : "",
|
|
293
|
+
``,
|
|
294
|
+
`Find the source file in the repo that renders this element and make the edit.`,
|
|
295
|
+
`After editing, output a JSON object: {"status":"done","message":"<what you did>","filesChanged":["<path>"]}`,
|
|
296
|
+
].filter(Boolean).join("\n");
|
|
297
|
+
|
|
298
|
+
this.dispatchAgent(event.id, session.token, null, session, prompt)
|
|
299
|
+
.then(result => {
|
|
300
|
+
let parsed = {};
|
|
301
|
+
try { parsed = JSON.parse((result?.stdout || result?.output || "").trim()); } catch {}
|
|
302
|
+
const reply = {
|
|
303
|
+
eventId: event.id,
|
|
304
|
+
status: parsed.status || "done",
|
|
305
|
+
message: parsed.message || result?.stdout?.slice(0, 500) || "Agent completed",
|
|
306
|
+
filesChanged: parsed.filesChanged || [],
|
|
307
|
+
createdAt: nowIso(),
|
|
308
|
+
};
|
|
309
|
+
session.replies.push(reply);
|
|
310
|
+
session.status = "done";
|
|
311
|
+
session.updatedAt = nowIso();
|
|
312
|
+
})
|
|
313
|
+
.catch(() => {
|
|
314
|
+
session.replies.push({
|
|
315
|
+
eventId: event.id,
|
|
316
|
+
status: "error",
|
|
317
|
+
message: "Agent dispatch failed",
|
|
318
|
+
filesChanged: [],
|
|
319
|
+
createdAt: nowIso(),
|
|
320
|
+
});
|
|
321
|
+
session.status = "error";
|
|
322
|
+
session.updatedAt = nowIso();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return { ok: true, eventId: event.id, status: session.status };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Legacy path: queue for a polling agent
|
|
329
|
+
if (session.pendingPolls.length > 0) {
|
|
330
|
+
const poll = session.pendingPolls.shift();
|
|
331
|
+
poll(event);
|
|
332
|
+
} else {
|
|
333
|
+
session.events.push(event);
|
|
334
|
+
if (session.events.length > MAX_EVENT_QUEUE) session.events.shift();
|
|
335
|
+
}
|
|
336
|
+
return { ok: true, eventId: event.id, status: session.status };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
poll(sessionId, token, timeoutMs = DEFAULT_POLL_TIMEOUT) {
|
|
340
|
+
const auth = this.authenticate(sessionId, token);
|
|
341
|
+
if (auth.error) return Promise.resolve(auth);
|
|
342
|
+
const session = auth.session;
|
|
343
|
+
if (session.events.length > 0) {
|
|
344
|
+
session.status = "agent_received";
|
|
345
|
+
session.updatedAt = nowIso();
|
|
346
|
+
return Promise.resolve(session.events.shift());
|
|
347
|
+
}
|
|
348
|
+
if (session.stopped) return Promise.resolve({ type: "stopped" });
|
|
349
|
+
|
|
350
|
+
const timeout = Math.min(Math.max(Number(timeoutMs) || DEFAULT_POLL_TIMEOUT, 1), MAX_POLL_TIMEOUT);
|
|
351
|
+
session.status = "waiting_for_event";
|
|
352
|
+
session.updatedAt = nowIso();
|
|
353
|
+
|
|
354
|
+
return new Promise(resolve => {
|
|
355
|
+
const timer = setTimeout(() => {
|
|
356
|
+
session.pendingPolls = session.pendingPolls.filter(fn => fn !== finish);
|
|
357
|
+
resolve({ type: "timeout" });
|
|
358
|
+
}, timeout);
|
|
359
|
+
const finish = event => {
|
|
360
|
+
clearTimeout(timer);
|
|
361
|
+
session.status = "agent_received";
|
|
362
|
+
session.updatedAt = nowIso();
|
|
363
|
+
resolve(event);
|
|
364
|
+
};
|
|
365
|
+
session.pendingPolls.push(finish);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
reply(sessionId, body) {
|
|
370
|
+
const auth = this.authenticate(sessionId, body.token);
|
|
371
|
+
if (auth.error) return auth;
|
|
372
|
+
const session = auth.session;
|
|
373
|
+
const reply = {
|
|
374
|
+
eventId: body.eventId,
|
|
375
|
+
status: body.status || body.type || "done",
|
|
376
|
+
message: body.message || "",
|
|
377
|
+
filesChanged: Array.isArray(body.filesChanged)
|
|
378
|
+
? body.filesChanged
|
|
379
|
+
: body.file
|
|
380
|
+
? [body.file]
|
|
381
|
+
: [],
|
|
382
|
+
createdAt: nowIso(),
|
|
383
|
+
};
|
|
384
|
+
session.replies.push(reply);
|
|
385
|
+
session.status = reply.status === "done" ? "done" : reply.status;
|
|
386
|
+
session.updatedAt = nowIso();
|
|
387
|
+
return { ok: true, status: session.status, reply };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
status(sessionId, token) {
|
|
391
|
+
const auth = this.authenticate(sessionId, token);
|
|
392
|
+
if (auth.error) return auth;
|
|
393
|
+
const session = auth.session;
|
|
394
|
+
return {
|
|
395
|
+
ok: true,
|
|
396
|
+
sessionId: session.sessionId,
|
|
397
|
+
brandSlug: session.brandSlug,
|
|
398
|
+
appSlug: session.appSlug,
|
|
399
|
+
devUrl: session.devUrl,
|
|
400
|
+
status: session.status,
|
|
401
|
+
createdAt: session.createdAt,
|
|
402
|
+
updatedAt: session.updatedAt,
|
|
403
|
+
pendingEvents: session.events.length,
|
|
404
|
+
pendingPolls: session.pendingPolls.length,
|
|
405
|
+
replies: session.replies,
|
|
406
|
+
launch: {
|
|
407
|
+
started: !!session.launch?.started,
|
|
408
|
+
alreadyRunning: !!session.launch?.alreadyRunning,
|
|
409
|
+
command: session.launch?.command || session.devCommand,
|
|
410
|
+
url: session.launch?.url || session.devUrl,
|
|
411
|
+
pid: session.launch?.pid || null,
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
stop(sessionId, token) {
|
|
417
|
+
const auth = this.authenticate(sessionId, token);
|
|
418
|
+
if (auth.error) return auth;
|
|
419
|
+
const session = auth.session;
|
|
420
|
+
session.stopped = true;
|
|
421
|
+
session.status = "stopped";
|
|
422
|
+
session.updatedAt = nowIso();
|
|
423
|
+
if (session.launch?.process && !session.launch.process.killed) {
|
|
424
|
+
try {
|
|
425
|
+
session.launch.process.kill();
|
|
426
|
+
} catch {}
|
|
427
|
+
}
|
|
428
|
+
for (const poll of session.pendingPolls.splice(0)) poll({ type: "stopped" });
|
|
429
|
+
this.sessions.delete(sessionId);
|
|
430
|
+
return { ok: true, status: "stopped", sessionId };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function createStudioDebugRuntime(options) {
|
|
435
|
+
const store = new StudioDebugSessionStore(options);
|
|
436
|
+
|
|
437
|
+
async function handle(req, res, url, cors) {
|
|
438
|
+
const reqPath = url.pathname;
|
|
439
|
+
const method = req.method;
|
|
440
|
+
|
|
441
|
+
if (method === "POST" && reqPath === "/studio/debug/start") {
|
|
442
|
+
try {
|
|
443
|
+
const body = await readBody(req);
|
|
444
|
+
const result = await store.start(body);
|
|
445
|
+
return writeJson(res, result.ok ? 200 : 400, result, cors);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
return writeJson(res, 400, { ok: false, error: err.message }, cors);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const debugMatch = reqPath.match(/^\/studio\/debug\/([^/]+)\/(events|status|stop)$/);
|
|
452
|
+
const claudeMatch = reqPath.match(/^\/studio\/claude\/([^/]+)\/(poll|reply|status)$/);
|
|
453
|
+
if (!debugMatch && !claudeMatch) return false;
|
|
454
|
+
const [, sessionId, action] = debugMatch || claudeMatch;
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
if (method === "POST" && action === "events") {
|
|
458
|
+
const result = store.submitEvent(sessionId, await readBody(req));
|
|
459
|
+
return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
|
|
460
|
+
}
|
|
461
|
+
if (method === "GET" && action === "poll") {
|
|
462
|
+
const result = await store.poll(sessionId, url.searchParams.get("token"), url.searchParams.get("timeout"));
|
|
463
|
+
return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
|
|
464
|
+
}
|
|
465
|
+
if (method === "POST" && action === "reply") {
|
|
466
|
+
const result = store.reply(sessionId, await readBody(req));
|
|
467
|
+
return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
|
|
468
|
+
}
|
|
469
|
+
if (method === "GET" && action === "status") {
|
|
470
|
+
const result = store.status(sessionId, url.searchParams.get("token"));
|
|
471
|
+
return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
|
|
472
|
+
}
|
|
473
|
+
if (method === "POST" && action === "stop") {
|
|
474
|
+
const body = await readBody(req);
|
|
475
|
+
const result = store.stop(sessionId, body.token || url.searchParams.get("token"));
|
|
476
|
+
return writeJson(res, result.error === "unauthorized" ? 401 : result.error ? 404 : 200, result, cors);
|
|
477
|
+
}
|
|
478
|
+
return writeJson(res, 405, { ok: false, error: "method_not_allowed" }, cors);
|
|
479
|
+
} catch (err) {
|
|
480
|
+
return writeJson(res, 400, { ok: false, error: err.message }, cors);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { store, handle };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export const studioDebugTargets = APP_TARGETS;
|