@worca/ui 0.41.0 → 0.43.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.
- package/app/main.bundle.js +2625 -1971
- package/app/main.bundle.js.map +4 -4
- package/app/protocol.js +2 -0
- package/app/styles.css +1356 -6
- package/package.json +2 -2
- package/server/app.js +90 -0
- package/server/dispatch-defaults.js +5 -5
- package/server/dispatch-events-aggregator.js +27 -13
- package/server/dispatch-migration.js +35 -1
- package/server/events-jsonl-reader.js +93 -0
- package/server/file-access-aggregator.js +553 -0
- package/server/graph-query-aggregator.js +165 -0
- package/server/integrations/renderers.js +11 -0
- package/server/process-manager.js +86 -0
- package/server/project-routes.js +16 -3
- package/server/schemas/keys.json +5 -0
- package/server/template-prompts.js +136 -0
- package/server/templates-routes.js +303 -49
- package/server/watcher.js +122 -40
- package/server/ws-broadcaster.js +5 -4
- package/server/ws-message-router.js +23 -2
- package/server/ws-status-watcher.js +5 -1
|
@@ -39,9 +39,10 @@ import {
|
|
|
39
39
|
} from 'node:fs';
|
|
40
40
|
import { tmpdir } from 'node:os';
|
|
41
41
|
import { dirname, join } from 'node:path';
|
|
42
|
-
import { Router } from 'express';
|
|
42
|
+
import { raw as expressRaw, Router } from 'express';
|
|
43
43
|
import { atomicWriteSync } from './atomic-write.js';
|
|
44
44
|
import { templatesDir } from './paths.js';
|
|
45
|
+
import { buildPromptsModel } from './template-prompts.js';
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Match template IDs: lowercase alphanumeric, hyphens, and underscores,
|
|
@@ -52,15 +53,26 @@ import { templatesDir } from './paths.js';
|
|
|
52
53
|
const TEMPLATE_RE = /^[a-z0-9_-]{1,64}$/;
|
|
53
54
|
const TIERS = ['project', 'user', 'builtin'];
|
|
54
55
|
const MUTABLE_TIERS = ['project', 'user'];
|
|
56
|
+
const _OVERLAY_NAME_RE = /^[a-z0-9._-]{1,64}\.(md|block\.md)$/;
|
|
55
57
|
export { TEMPLATE_RE, TIERS };
|
|
56
58
|
|
|
57
59
|
function isValidTier(tier) {
|
|
58
60
|
return TIERS.includes(tier);
|
|
59
61
|
}
|
|
60
|
-
function
|
|
62
|
+
function _isMutableTier(tier) {
|
|
61
63
|
return MUTABLE_TIERS.includes(tier);
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
function hasOverlays(tmplDir) {
|
|
67
|
+
const agentsDir = join(tmplDir, 'agents');
|
|
68
|
+
if (!existsSync(agentsDir)) return false;
|
|
69
|
+
try {
|
|
70
|
+
return readdirSync(agentsDir).some((f) => _OVERLAY_NAME_RE.test(f));
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
64
76
|
function readTemplateJson(dirPath) {
|
|
65
77
|
const manifestPath = join(dirPath, 'template.json');
|
|
66
78
|
if (!existsSync(manifestPath)) return null;
|
|
@@ -112,6 +124,7 @@ function listTemplatesFlat(projectRoot) {
|
|
|
112
124
|
const manifest = readTemplateJson(join(dir, entry.name));
|
|
113
125
|
if (!manifest) continue;
|
|
114
126
|
const id = manifest.id || entry.name;
|
|
127
|
+
const tmplDir = join(dir, entry.name);
|
|
115
128
|
out.push({
|
|
116
129
|
tier,
|
|
117
130
|
id,
|
|
@@ -122,6 +135,7 @@ function listTemplatesFlat(projectRoot) {
|
|
|
122
135
|
tags: manifest.tags || [],
|
|
123
136
|
created_at: manifest.created_at,
|
|
124
137
|
builtin: manifest.builtin === true || tier === 'builtin',
|
|
138
|
+
has_overlays: hasOverlays(tmplDir),
|
|
125
139
|
});
|
|
126
140
|
}
|
|
127
141
|
}
|
|
@@ -190,6 +204,8 @@ function runWorcaTemplates(projectRoot, args, opts = {}) {
|
|
|
190
204
|
combined.includes('invalid')
|
|
191
205
|
) {
|
|
192
206
|
code = 'validation_error';
|
|
207
|
+
} else if (combined.includes('partial_rename')) {
|
|
208
|
+
code = 'partial_rename';
|
|
193
209
|
}
|
|
194
210
|
const e = new Error(stderr.trim() || err.message || 'worca CLI failed');
|
|
195
211
|
e.cliCode = code;
|
|
@@ -281,26 +297,153 @@ function duplicateTemplateViaCli(projectRoot, srcId, dstId, dstTier) {
|
|
|
281
297
|
]);
|
|
282
298
|
}
|
|
283
299
|
|
|
284
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Export a template bundle. Returns `{ json, data }` where `json` is
|
|
302
|
+
* true for JSON bundles (data is parsed object) and false for zip bundles
|
|
303
|
+
* (data is raw Buffer with filename in `filename`).
|
|
304
|
+
*
|
|
305
|
+
* `mode` is 'standalone' (default — self-contained config + resolved prompts)
|
|
306
|
+
* or 'delta' (sparse overlay). Standalone materialises a prompt set even when
|
|
307
|
+
* the template has no on-disk overlays, so the CLI may emit a zip in cases the
|
|
308
|
+
* old on-disk `hasOverlays` check would have missed. We therefore sniff the
|
|
309
|
+
* produced file's magic bytes instead of predicting the format.
|
|
310
|
+
*/
|
|
311
|
+
function exportBundle(projectRoot, id, mode = 'standalone') {
|
|
312
|
+
const normalizedMode = mode === 'delta' ? 'delta' : 'standalone';
|
|
285
313
|
const dir = mkdtempSync(join(tmpdir(), 'worca-bundle-'));
|
|
286
|
-
const bundlePath = join(dir, `${id}.json`);
|
|
287
314
|
try {
|
|
315
|
+
const bundlePath = join(dir, `${id}-bundle.out`);
|
|
316
|
+
// --include-models / --include-pricing: bundle the project's worca.models
|
|
317
|
+
// and worca.pricing entries that the template references (e.g. a custom
|
|
318
|
+
// "glm-ds" alias). The CLI already filters to *referenced* aliases via
|
|
319
|
+
// collect_referenced_model_aliases, so built-ins the importer already has
|
|
320
|
+
// (opus/sonnet/haiku) aren't shipped redundantly. Without these flags a
|
|
321
|
+
// bundle that names a custom alias is broken on import.
|
|
288
322
|
runWorcaTemplates(projectRoot, [
|
|
289
323
|
'export',
|
|
290
324
|
'--to',
|
|
291
325
|
bundlePath,
|
|
292
326
|
'--templates',
|
|
293
327
|
id,
|
|
328
|
+
'--mode',
|
|
329
|
+
normalizedMode,
|
|
330
|
+
'--include-models',
|
|
331
|
+
'--include-pricing',
|
|
294
332
|
]);
|
|
295
333
|
if (existsSync(bundlePath)) {
|
|
296
|
-
|
|
334
|
+
const buf = readFileSync(bundlePath);
|
|
335
|
+
// ZIP local-file-header magic: 'PK\x03\x04'.
|
|
336
|
+
const isZip =
|
|
337
|
+
buf.length >= 4 &&
|
|
338
|
+
buf[0] === 0x50 &&
|
|
339
|
+
buf[1] === 0x4b &&
|
|
340
|
+
buf[2] === 0x03 &&
|
|
341
|
+
buf[3] === 0x04;
|
|
342
|
+
if (isZip) {
|
|
343
|
+
return { json: false, filename: `${id}-bundle.zip`, data: buf };
|
|
344
|
+
}
|
|
345
|
+
return { json: true, data: JSON.parse(buf.toString('utf8')) };
|
|
297
346
|
}
|
|
298
|
-
return { templates: [{ id }] };
|
|
347
|
+
return { json: true, data: { templates: [{ id }] } };
|
|
299
348
|
} finally {
|
|
300
349
|
cleanupTemp(dir);
|
|
301
350
|
}
|
|
302
351
|
}
|
|
303
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Export a single template to a (secret) GitHub gist via the CLI and return the
|
|
355
|
+
* gist URL. Delegates to `worca templates export --to gist`, which shells out to
|
|
356
|
+
* `gh gist create` and prints the URL on stdout. Throws (via runWorcaTemplates)
|
|
357
|
+
* with the CLI stderr as the message when gh is unavailable or gist creation
|
|
358
|
+
* fails — the caller maps that to a JSON error. Gist export only supports
|
|
359
|
+
* overlay-free templates (the CLI rejects overlays); the UI hides the button
|
|
360
|
+
* for templates with overlays, matching that constraint.
|
|
361
|
+
*/
|
|
362
|
+
function exportGist(projectRoot, id) {
|
|
363
|
+
const stdout = runWorcaTemplates(
|
|
364
|
+
projectRoot,
|
|
365
|
+
[
|
|
366
|
+
'export',
|
|
367
|
+
'--to',
|
|
368
|
+
'gist',
|
|
369
|
+
'--templates',
|
|
370
|
+
id,
|
|
371
|
+
'--include-models',
|
|
372
|
+
'--include-pricing',
|
|
373
|
+
],
|
|
374
|
+
{ timeout: 30000 },
|
|
375
|
+
);
|
|
376
|
+
// The CLI prints the gist URL (from `gh gist create`) as its final stdout line.
|
|
377
|
+
const match = stdout.match(/https?:\/\/\S+/);
|
|
378
|
+
return match ? match[0] : null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function handleZipImport(req, res) {
|
|
382
|
+
const dstTier = req.query.dst_tier;
|
|
383
|
+
if (!isValidTier(dstTier)) {
|
|
384
|
+
return res.status(400).json({
|
|
385
|
+
ok: false,
|
|
386
|
+
error: `dst_tier must be one of: ${TIERS.join(', ')}`,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
if (rejectBuiltinWrite(res, dstTier, 'import to')) return;
|
|
390
|
+
|
|
391
|
+
const zipBuffer = req.body;
|
|
392
|
+
const tmpPath = join(
|
|
393
|
+
tmpdir(),
|
|
394
|
+
`worca-import-${Math.random().toString(36).slice(2)}.zip`,
|
|
395
|
+
);
|
|
396
|
+
try {
|
|
397
|
+
writeFileSync(tmpPath, zipBuffer);
|
|
398
|
+
const stdout = runWorcaTemplates(
|
|
399
|
+
req.project.projectRoot,
|
|
400
|
+
['import', '--from', tmpPath, '--scope', dstTier, '--non-interactive'],
|
|
401
|
+
{ timeout: 60000 },
|
|
402
|
+
);
|
|
403
|
+
// Best-effort summary: the CLI may print imported template ids to stdout
|
|
404
|
+
const imported = [];
|
|
405
|
+
for (const line of (stdout || '').split('\n')) {
|
|
406
|
+
const m = line.match(/imported[:\s]+([a-z0-9_-]+)/i);
|
|
407
|
+
if (m) imported.push({ id: m[1], tier: dstTier });
|
|
408
|
+
}
|
|
409
|
+
res.json({ ok: true, count: imported.length || 1, imported });
|
|
410
|
+
} catch (err) {
|
|
411
|
+
res
|
|
412
|
+
.status(statusForCliCode(err.cliCode))
|
|
413
|
+
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
414
|
+
} finally {
|
|
415
|
+
try {
|
|
416
|
+
rmSync(tmpPath, { force: true });
|
|
417
|
+
} catch {
|
|
418
|
+
/* ignore cleanup errors */
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function handleJsonImport(req, res) {
|
|
424
|
+
const { bundle, dst_tier: dstTier } = req.body || {};
|
|
425
|
+
if (!bundle || typeof bundle !== 'object') {
|
|
426
|
+
return res
|
|
427
|
+
.status(400)
|
|
428
|
+
.json({ ok: false, error: 'bundle must be a JSON object' });
|
|
429
|
+
}
|
|
430
|
+
if (!isValidTier(dstTier)) {
|
|
431
|
+
return res.status(400).json({
|
|
432
|
+
ok: false,
|
|
433
|
+
error: `dst_tier must be one of: ${TIERS.join(', ')}`,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (rejectBuiltinWrite(res, dstTier, 'import to')) return;
|
|
437
|
+
try {
|
|
438
|
+
const result = importBundle(req.project.projectRoot, bundle, dstTier);
|
|
439
|
+
res.json({ ok: true, ...result });
|
|
440
|
+
} catch (err) {
|
|
441
|
+
res
|
|
442
|
+
.status(statusForCliCode(err.cliCode))
|
|
443
|
+
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
304
447
|
function importBundle(projectRoot, bundle, tier) {
|
|
305
448
|
if (!bundle || !Array.isArray(bundle.templates)) {
|
|
306
449
|
throw new Error('Bundle must contain a "templates" array');
|
|
@@ -434,31 +577,21 @@ export function createTemplatesRoutes() {
|
|
|
434
577
|
* doesn't match the literal "import" path segment as a tier
|
|
435
578
|
* parameter (which would 400 with "tier must be one of …").
|
|
436
579
|
*
|
|
437
|
-
*
|
|
580
|
+
* Accepts two content types:
|
|
581
|
+
* application/zip — raw zip bytes; dst_tier as query param
|
|
582
|
+
* application/json — { bundle: {templates: [...]}, dst_tier }
|
|
438
583
|
*/
|
|
439
|
-
router.post(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return res
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
if (rejectBuiltinWrite(res, dstTier, 'import to')) return;
|
|
453
|
-
try {
|
|
454
|
-
const result = importBundle(req.project.projectRoot, bundle, dstTier);
|
|
455
|
-
res.json({ ok: true, ...result });
|
|
456
|
-
} catch (err) {
|
|
457
|
-
res
|
|
458
|
-
.status(statusForCliCode(err.cliCode))
|
|
459
|
-
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
460
|
-
}
|
|
461
|
-
});
|
|
584
|
+
router.post(
|
|
585
|
+
'/templates/import',
|
|
586
|
+
expressRaw({ type: 'application/zip', limit: '1mb' }),
|
|
587
|
+
(req, res) => {
|
|
588
|
+
const ctype = req.headers['content-type'] || '';
|
|
589
|
+
if (ctype.startsWith('application/zip')) {
|
|
590
|
+
return handleZipImport(req, res);
|
|
591
|
+
}
|
|
592
|
+
return handleJsonImport(req, res);
|
|
593
|
+
},
|
|
594
|
+
);
|
|
462
595
|
|
|
463
596
|
/**
|
|
464
597
|
* POST /api/projects/:projectId/templates/validate
|
|
@@ -611,9 +744,11 @@ export function createTemplatesRoutes() {
|
|
|
611
744
|
* POST /api/projects/:projectId/templates/:tier/:id/rename
|
|
612
745
|
* Body: { dst_tier, dst_id }
|
|
613
746
|
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
747
|
+
* Delegates to `worca templates rename` — a single CLI call that runs
|
|
748
|
+
* duplicate → pointer-rewrite → delete atomically in one process.
|
|
749
|
+
* partial_rename (500) is surfaced when the CLI reports that duplicate
|
|
750
|
+
* succeeded but delete failed (exit code 3, stderr contains
|
|
751
|
+
* "partial_rename").
|
|
617
752
|
*/
|
|
618
753
|
router.post('/templates/:tier/:id/rename', (req, res) => {
|
|
619
754
|
const { tier: srcTier, id: srcId } = req.params;
|
|
@@ -646,25 +781,33 @@ export function createTemplatesRoutes() {
|
|
|
646
781
|
}
|
|
647
782
|
|
|
648
783
|
try {
|
|
649
|
-
|
|
784
|
+
runWorcaTemplates(projectRoot, [
|
|
785
|
+
'rename',
|
|
786
|
+
'--src-id',
|
|
787
|
+
srcId,
|
|
788
|
+
'--src-scope',
|
|
789
|
+
srcTier,
|
|
790
|
+
'--dst-id',
|
|
791
|
+
dstId,
|
|
792
|
+
'--dst-scope',
|
|
793
|
+
dstTier,
|
|
794
|
+
]);
|
|
650
795
|
} catch (err) {
|
|
796
|
+
if (err.cliCode === 'partial_rename') {
|
|
797
|
+
return res.status(500).json({
|
|
798
|
+
ok: false,
|
|
799
|
+
code: 'partial_rename',
|
|
800
|
+
error: `Renamed to "${dstId}" (${dstTier}) but failed to remove the source "${srcId}" (${srcTier}): ${err.message}`,
|
|
801
|
+
src_tier: srcTier,
|
|
802
|
+
src_id: srcId,
|
|
803
|
+
dst_tier: dstTier,
|
|
804
|
+
dst_id: dstId,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
651
807
|
return res
|
|
652
808
|
.status(statusForCliCode(err.cliCode))
|
|
653
809
|
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
654
810
|
}
|
|
655
|
-
try {
|
|
656
|
-
deleteTemplateViaCli(projectRoot, srcTier, srcId);
|
|
657
|
-
} catch (err) {
|
|
658
|
-
return res.status(500).json({
|
|
659
|
-
ok: false,
|
|
660
|
-
code: 'partial_rename',
|
|
661
|
-
error: `Renamed to "${dstId}" (${dstTier}) but failed to remove the source "${srcId}" (${srcTier}): ${err.message}`,
|
|
662
|
-
src_tier: srcTier,
|
|
663
|
-
src_id: srcId,
|
|
664
|
-
dst_tier: dstTier,
|
|
665
|
-
dst_id: dstId,
|
|
666
|
-
});
|
|
667
|
-
}
|
|
668
811
|
res.json({
|
|
669
812
|
ok: true,
|
|
670
813
|
src_tier: srcTier,
|
|
@@ -676,20 +819,131 @@ export function createTemplatesRoutes() {
|
|
|
676
819
|
|
|
677
820
|
/**
|
|
678
821
|
* GET /api/projects/:projectId/templates/:tier/:id/bundle
|
|
822
|
+
*
|
|
823
|
+
* Optional ?mode=standalone|delta (default standalone). Standalone emits a
|
|
824
|
+
* self-contained bundle (config materialised + prompts resolved); delta emits
|
|
825
|
+
* the sparse overlay. Output format (zip vs JSON) is auto-detected.
|
|
679
826
|
*/
|
|
680
827
|
router.get('/templates/:tier/:id/bundle', (req, res) => {
|
|
681
828
|
const { tier, id } = req.params;
|
|
682
829
|
if (rejectInvalidTierId(res, tier, id)) return;
|
|
830
|
+
const mode = req.query.mode === 'delta' ? 'delta' : 'standalone';
|
|
831
|
+
try {
|
|
832
|
+
const result = exportBundle(req.project.projectRoot, id, mode);
|
|
833
|
+
if (!result.json) {
|
|
834
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
835
|
+
res.setHeader(
|
|
836
|
+
'Content-Disposition',
|
|
837
|
+
`attachment; filename="${result.filename}"`,
|
|
838
|
+
);
|
|
839
|
+
res.send(result.data);
|
|
840
|
+
} else {
|
|
841
|
+
res.json({ ok: true, bundle: result.data });
|
|
842
|
+
}
|
|
843
|
+
} catch (err) {
|
|
844
|
+
res
|
|
845
|
+
.status(statusForCliCode(err.cliCode))
|
|
846
|
+
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* POST /api/projects/:projectId/templates/:tier/:id/bundle?format=gist
|
|
852
|
+
*
|
|
853
|
+
* Create a (secret) GitHub gist from the template bundle and return its URL.
|
|
854
|
+
* Gist creation is a mutation (it shells out to `gh gist create`), so it is a
|
|
855
|
+
* POST distinct from the GET bundle download above. Only `?format=gist` is
|
|
856
|
+
* accepted here; zip/JSON downloads use the GET route. The client copies the
|
|
857
|
+
* returned `gist_url` to the clipboard so the template can be shared and
|
|
858
|
+
* imported elsewhere via `worca templates import --from <url>`.
|
|
859
|
+
*/
|
|
860
|
+
router.post('/templates/:tier/:id/bundle', (req, res) => {
|
|
861
|
+
const { tier, id } = req.params;
|
|
862
|
+
if (rejectInvalidTierId(res, tier, id)) return;
|
|
863
|
+
if (req.query.format !== 'gist') {
|
|
864
|
+
return res.status(400).json({
|
|
865
|
+
ok: false,
|
|
866
|
+
error: 'unsupported bundle POST format — use ?format=gist',
|
|
867
|
+
});
|
|
868
|
+
}
|
|
683
869
|
try {
|
|
684
|
-
const
|
|
685
|
-
|
|
870
|
+
const gistUrl = exportGist(req.project.projectRoot, id);
|
|
871
|
+
if (!gistUrl) {
|
|
872
|
+
return res.status(502).json({
|
|
873
|
+
ok: false,
|
|
874
|
+
error: 'gist created but no URL was returned by gh',
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
res.json({ ok: true, gist_url: gistUrl });
|
|
686
878
|
} catch (err) {
|
|
879
|
+
// gh-missing / gist-create failures arrive here with the CLI stderr as the
|
|
880
|
+
// message (it mentions "gh"/"gist", which the client maps to a friendly
|
|
881
|
+
// "GitHub CLI is not available" toast).
|
|
687
882
|
res
|
|
688
883
|
.status(statusForCliCode(err.cliCode))
|
|
689
884
|
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
690
885
|
}
|
|
691
886
|
});
|
|
692
887
|
|
|
888
|
+
/**
|
|
889
|
+
* GET /api/projects/:projectId/templates/:tier/:id/overlays
|
|
890
|
+
*
|
|
891
|
+
* Returns every overlay .md file from <tmpl>/agents/, keyed by filename.
|
|
892
|
+
*/
|
|
893
|
+
router.get('/templates/:tier/:id/overlays', (req, res) => {
|
|
894
|
+
const { tier, id } = req.params;
|
|
895
|
+
if (rejectInvalidTierId(res, tier, id)) return;
|
|
896
|
+
const { projectRoot } = req.project;
|
|
897
|
+
const tierDir = dirForTier(projectRoot, tier);
|
|
898
|
+
const tmplDir = join(tierDir, id);
|
|
899
|
+
if (!existsSync(join(tmplDir, 'template.json'))) {
|
|
900
|
+
return res.status(404).json({
|
|
901
|
+
ok: false,
|
|
902
|
+
error: `Template "${id}" not found in ${tier} scope`,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
const agentsDir = join(tmplDir, 'agents');
|
|
907
|
+
const overlays = {};
|
|
908
|
+
if (existsSync(agentsDir)) {
|
|
909
|
+
for (const f of readdirSync(agentsDir)) {
|
|
910
|
+
if (!_OVERLAY_NAME_RE.test(f)) continue;
|
|
911
|
+
try {
|
|
912
|
+
overlays[f] = readFileSync(join(agentsDir, f), 'utf8');
|
|
913
|
+
} catch {
|
|
914
|
+
/* skip unreadable files */
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
res.json({ ok: true, overlays });
|
|
919
|
+
} catch (err) {
|
|
920
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* GET /api/projects/:projectId/templates/:tier/:id/prompts
|
|
926
|
+
*
|
|
927
|
+
* Effective per-stage prompt model for the editor's "Prompts" tab: each agent
|
|
928
|
+
* `*.md` and user-prompt `*.block.md` resolved against the built-in core
|
|
929
|
+
* prompts, classified as 'builtin' (fallback), 'pipeline' (replace), or
|
|
930
|
+
* 'extends' (append/overwrite merge). Unlike /overlays this is never empty —
|
|
931
|
+
* a template with no overlays still shows every built-in prompt. Tolerant of a
|
|
932
|
+
* missing template dir (returns core-only) so the tab works for new drafts.
|
|
933
|
+
*/
|
|
934
|
+
router.get('/templates/:tier/:id/prompts', (req, res) => {
|
|
935
|
+
const { tier, id } = req.params;
|
|
936
|
+
if (rejectInvalidTierId(res, tier, id)) return;
|
|
937
|
+
const { projectRoot } = req.project;
|
|
938
|
+
const coreDir = join(projectRoot, '.claude', 'worca', 'agents', 'core');
|
|
939
|
+
const overlayDir = join(dirForTier(projectRoot, tier), id, 'agents');
|
|
940
|
+
try {
|
|
941
|
+
res.json({ ok: true, prompts: buildPromptsModel(coreDir, overlayDir) });
|
|
942
|
+
} catch (err) {
|
|
943
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
|
|
693
947
|
/**
|
|
694
948
|
* PUT /api/projects/:projectId/default-template
|
|
695
949
|
*
|