@worca/ui 0.41.0 → 0.42.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 +2667 -2024
- package/app/main.bundle.js.map +4 -4
- package/app/protocol.js +2 -0
- package/app/styles.css +1320 -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 +481 -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 +287 -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,137 @@ 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`);
|
|
288
316
|
runWorcaTemplates(projectRoot, [
|
|
289
317
|
'export',
|
|
290
318
|
'--to',
|
|
291
319
|
bundlePath,
|
|
292
320
|
'--templates',
|
|
293
321
|
id,
|
|
322
|
+
'--mode',
|
|
323
|
+
normalizedMode,
|
|
294
324
|
]);
|
|
295
325
|
if (existsSync(bundlePath)) {
|
|
296
|
-
|
|
326
|
+
const buf = readFileSync(bundlePath);
|
|
327
|
+
// ZIP local-file-header magic: 'PK\x03\x04'.
|
|
328
|
+
const isZip =
|
|
329
|
+
buf.length >= 4 &&
|
|
330
|
+
buf[0] === 0x50 &&
|
|
331
|
+
buf[1] === 0x4b &&
|
|
332
|
+
buf[2] === 0x03 &&
|
|
333
|
+
buf[3] === 0x04;
|
|
334
|
+
if (isZip) {
|
|
335
|
+
return { json: false, filename: `${id}-bundle.zip`, data: buf };
|
|
336
|
+
}
|
|
337
|
+
return { json: true, data: JSON.parse(buf.toString('utf8')) };
|
|
297
338
|
}
|
|
298
|
-
return { templates: [{ id }] };
|
|
339
|
+
return { json: true, data: { templates: [{ id }] } };
|
|
299
340
|
} finally {
|
|
300
341
|
cleanupTemp(dir);
|
|
301
342
|
}
|
|
302
343
|
}
|
|
303
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Export a single template to a (secret) GitHub gist via the CLI and return the
|
|
347
|
+
* gist URL. Delegates to `worca templates export --to gist`, which shells out to
|
|
348
|
+
* `gh gist create` and prints the URL on stdout. Throws (via runWorcaTemplates)
|
|
349
|
+
* with the CLI stderr as the message when gh is unavailable or gist creation
|
|
350
|
+
* fails — the caller maps that to a JSON error. Gist export only supports
|
|
351
|
+
* overlay-free templates (the CLI rejects overlays); the UI hides the button
|
|
352
|
+
* for templates with overlays, matching that constraint.
|
|
353
|
+
*/
|
|
354
|
+
function exportGist(projectRoot, id) {
|
|
355
|
+
const stdout = runWorcaTemplates(
|
|
356
|
+
projectRoot,
|
|
357
|
+
['export', '--to', 'gist', '--templates', id],
|
|
358
|
+
{ timeout: 30000 },
|
|
359
|
+
);
|
|
360
|
+
// The CLI prints the gist URL (from `gh gist create`) as its final stdout line.
|
|
361
|
+
const match = stdout.match(/https?:\/\/\S+/);
|
|
362
|
+
return match ? match[0] : null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function handleZipImport(req, res) {
|
|
366
|
+
const dstTier = req.query.dst_tier;
|
|
367
|
+
if (!isValidTier(dstTier)) {
|
|
368
|
+
return res.status(400).json({
|
|
369
|
+
ok: false,
|
|
370
|
+
error: `dst_tier must be one of: ${TIERS.join(', ')}`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (rejectBuiltinWrite(res, dstTier, 'import to')) return;
|
|
374
|
+
|
|
375
|
+
const zipBuffer = req.body;
|
|
376
|
+
const tmpPath = join(
|
|
377
|
+
tmpdir(),
|
|
378
|
+
`worca-import-${Math.random().toString(36).slice(2)}.zip`,
|
|
379
|
+
);
|
|
380
|
+
try {
|
|
381
|
+
writeFileSync(tmpPath, zipBuffer);
|
|
382
|
+
const stdout = runWorcaTemplates(
|
|
383
|
+
req.project.projectRoot,
|
|
384
|
+
['import', '--from', tmpPath, '--scope', dstTier, '--non-interactive'],
|
|
385
|
+
{ timeout: 60000 },
|
|
386
|
+
);
|
|
387
|
+
// Best-effort summary: the CLI may print imported template ids to stdout
|
|
388
|
+
const imported = [];
|
|
389
|
+
for (const line of (stdout || '').split('\n')) {
|
|
390
|
+
const m = line.match(/imported[:\s]+([a-z0-9_-]+)/i);
|
|
391
|
+
if (m) imported.push({ id: m[1], tier: dstTier });
|
|
392
|
+
}
|
|
393
|
+
res.json({ ok: true, count: imported.length || 1, imported });
|
|
394
|
+
} catch (err) {
|
|
395
|
+
res
|
|
396
|
+
.status(statusForCliCode(err.cliCode))
|
|
397
|
+
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
398
|
+
} finally {
|
|
399
|
+
try {
|
|
400
|
+
rmSync(tmpPath, { force: true });
|
|
401
|
+
} catch {
|
|
402
|
+
/* ignore cleanup errors */
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function handleJsonImport(req, res) {
|
|
408
|
+
const { bundle, dst_tier: dstTier } = req.body || {};
|
|
409
|
+
if (!bundle || typeof bundle !== 'object') {
|
|
410
|
+
return res
|
|
411
|
+
.status(400)
|
|
412
|
+
.json({ ok: false, error: 'bundle must be a JSON object' });
|
|
413
|
+
}
|
|
414
|
+
if (!isValidTier(dstTier)) {
|
|
415
|
+
return res.status(400).json({
|
|
416
|
+
ok: false,
|
|
417
|
+
error: `dst_tier must be one of: ${TIERS.join(', ')}`,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
if (rejectBuiltinWrite(res, dstTier, 'import to')) return;
|
|
421
|
+
try {
|
|
422
|
+
const result = importBundle(req.project.projectRoot, bundle, dstTier);
|
|
423
|
+
res.json({ ok: true, ...result });
|
|
424
|
+
} catch (err) {
|
|
425
|
+
res
|
|
426
|
+
.status(statusForCliCode(err.cliCode))
|
|
427
|
+
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
304
431
|
function importBundle(projectRoot, bundle, tier) {
|
|
305
432
|
if (!bundle || !Array.isArray(bundle.templates)) {
|
|
306
433
|
throw new Error('Bundle must contain a "templates" array');
|
|
@@ -434,31 +561,21 @@ export function createTemplatesRoutes() {
|
|
|
434
561
|
* doesn't match the literal "import" path segment as a tier
|
|
435
562
|
* parameter (which would 400 with "tier must be one of …").
|
|
436
563
|
*
|
|
437
|
-
*
|
|
564
|
+
* Accepts two content types:
|
|
565
|
+
* application/zip — raw zip bytes; dst_tier as query param
|
|
566
|
+
* application/json — { bundle: {templates: [...]}, dst_tier }
|
|
438
567
|
*/
|
|
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
|
-
});
|
|
568
|
+
router.post(
|
|
569
|
+
'/templates/import',
|
|
570
|
+
expressRaw({ type: 'application/zip', limit: '1mb' }),
|
|
571
|
+
(req, res) => {
|
|
572
|
+
const ctype = req.headers['content-type'] || '';
|
|
573
|
+
if (ctype.startsWith('application/zip')) {
|
|
574
|
+
return handleZipImport(req, res);
|
|
575
|
+
}
|
|
576
|
+
return handleJsonImport(req, res);
|
|
577
|
+
},
|
|
578
|
+
);
|
|
462
579
|
|
|
463
580
|
/**
|
|
464
581
|
* POST /api/projects/:projectId/templates/validate
|
|
@@ -611,9 +728,11 @@ export function createTemplatesRoutes() {
|
|
|
611
728
|
* POST /api/projects/:projectId/templates/:tier/:id/rename
|
|
612
729
|
* Body: { dst_tier, dst_id }
|
|
613
730
|
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
731
|
+
* Delegates to `worca templates rename` — a single CLI call that runs
|
|
732
|
+
* duplicate → pointer-rewrite → delete atomically in one process.
|
|
733
|
+
* partial_rename (500) is surfaced when the CLI reports that duplicate
|
|
734
|
+
* succeeded but delete failed (exit code 3, stderr contains
|
|
735
|
+
* "partial_rename").
|
|
617
736
|
*/
|
|
618
737
|
router.post('/templates/:tier/:id/rename', (req, res) => {
|
|
619
738
|
const { tier: srcTier, id: srcId } = req.params;
|
|
@@ -646,25 +765,33 @@ export function createTemplatesRoutes() {
|
|
|
646
765
|
}
|
|
647
766
|
|
|
648
767
|
try {
|
|
649
|
-
|
|
768
|
+
runWorcaTemplates(projectRoot, [
|
|
769
|
+
'rename',
|
|
770
|
+
'--src-id',
|
|
771
|
+
srcId,
|
|
772
|
+
'--src-scope',
|
|
773
|
+
srcTier,
|
|
774
|
+
'--dst-id',
|
|
775
|
+
dstId,
|
|
776
|
+
'--dst-scope',
|
|
777
|
+
dstTier,
|
|
778
|
+
]);
|
|
650
779
|
} catch (err) {
|
|
780
|
+
if (err.cliCode === 'partial_rename') {
|
|
781
|
+
return res.status(500).json({
|
|
782
|
+
ok: false,
|
|
783
|
+
code: 'partial_rename',
|
|
784
|
+
error: `Renamed to "${dstId}" (${dstTier}) but failed to remove the source "${srcId}" (${srcTier}): ${err.message}`,
|
|
785
|
+
src_tier: srcTier,
|
|
786
|
+
src_id: srcId,
|
|
787
|
+
dst_tier: dstTier,
|
|
788
|
+
dst_id: dstId,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
651
791
|
return res
|
|
652
792
|
.status(statusForCliCode(err.cliCode))
|
|
653
793
|
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
654
794
|
}
|
|
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
795
|
res.json({
|
|
669
796
|
ok: true,
|
|
670
797
|
src_tier: srcTier,
|
|
@@ -676,13 +803,27 @@ export function createTemplatesRoutes() {
|
|
|
676
803
|
|
|
677
804
|
/**
|
|
678
805
|
* GET /api/projects/:projectId/templates/:tier/:id/bundle
|
|
806
|
+
*
|
|
807
|
+
* Optional ?mode=standalone|delta (default standalone). Standalone emits a
|
|
808
|
+
* self-contained bundle (config materialised + prompts resolved); delta emits
|
|
809
|
+
* the sparse overlay. Output format (zip vs JSON) is auto-detected.
|
|
679
810
|
*/
|
|
680
811
|
router.get('/templates/:tier/:id/bundle', (req, res) => {
|
|
681
812
|
const { tier, id } = req.params;
|
|
682
813
|
if (rejectInvalidTierId(res, tier, id)) return;
|
|
814
|
+
const mode = req.query.mode === 'delta' ? 'delta' : 'standalone';
|
|
683
815
|
try {
|
|
684
|
-
const
|
|
685
|
-
|
|
816
|
+
const result = exportBundle(req.project.projectRoot, id, mode);
|
|
817
|
+
if (!result.json) {
|
|
818
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
819
|
+
res.setHeader(
|
|
820
|
+
'Content-Disposition',
|
|
821
|
+
`attachment; filename="${result.filename}"`,
|
|
822
|
+
);
|
|
823
|
+
res.send(result.data);
|
|
824
|
+
} else {
|
|
825
|
+
res.json({ ok: true, bundle: result.data });
|
|
826
|
+
}
|
|
686
827
|
} catch (err) {
|
|
687
828
|
res
|
|
688
829
|
.status(statusForCliCode(err.cliCode))
|
|
@@ -690,6 +831,103 @@ export function createTemplatesRoutes() {
|
|
|
690
831
|
}
|
|
691
832
|
});
|
|
692
833
|
|
|
834
|
+
/**
|
|
835
|
+
* POST /api/projects/:projectId/templates/:tier/:id/bundle?format=gist
|
|
836
|
+
*
|
|
837
|
+
* Create a (secret) GitHub gist from the template bundle and return its URL.
|
|
838
|
+
* Gist creation is a mutation (it shells out to `gh gist create`), so it is a
|
|
839
|
+
* POST distinct from the GET bundle download above. Only `?format=gist` is
|
|
840
|
+
* accepted here; zip/JSON downloads use the GET route. The client copies the
|
|
841
|
+
* returned `gist_url` to the clipboard so the template can be shared and
|
|
842
|
+
* imported elsewhere via `worca templates import --from <url>`.
|
|
843
|
+
*/
|
|
844
|
+
router.post('/templates/:tier/:id/bundle', (req, res) => {
|
|
845
|
+
const { tier, id } = req.params;
|
|
846
|
+
if (rejectInvalidTierId(res, tier, id)) return;
|
|
847
|
+
if (req.query.format !== 'gist') {
|
|
848
|
+
return res.status(400).json({
|
|
849
|
+
ok: false,
|
|
850
|
+
error: 'unsupported bundle POST format — use ?format=gist',
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
try {
|
|
854
|
+
const gistUrl = exportGist(req.project.projectRoot, id);
|
|
855
|
+
if (!gistUrl) {
|
|
856
|
+
return res.status(502).json({
|
|
857
|
+
ok: false,
|
|
858
|
+
error: 'gist created but no URL was returned by gh',
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
res.json({ ok: true, gist_url: gistUrl });
|
|
862
|
+
} catch (err) {
|
|
863
|
+
// gh-missing / gist-create failures arrive here with the CLI stderr as the
|
|
864
|
+
// message (it mentions "gh"/"gist", which the client maps to a friendly
|
|
865
|
+
// "GitHub CLI is not available" toast).
|
|
866
|
+
res
|
|
867
|
+
.status(statusForCliCode(err.cliCode))
|
|
868
|
+
.json({ ok: false, error: err.message, code: err.cliCode });
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* GET /api/projects/:projectId/templates/:tier/:id/overlays
|
|
874
|
+
*
|
|
875
|
+
* Returns every overlay .md file from <tmpl>/agents/, keyed by filename.
|
|
876
|
+
*/
|
|
877
|
+
router.get('/templates/:tier/:id/overlays', (req, res) => {
|
|
878
|
+
const { tier, id } = req.params;
|
|
879
|
+
if (rejectInvalidTierId(res, tier, id)) return;
|
|
880
|
+
const { projectRoot } = req.project;
|
|
881
|
+
const tierDir = dirForTier(projectRoot, tier);
|
|
882
|
+
const tmplDir = join(tierDir, id);
|
|
883
|
+
if (!existsSync(join(tmplDir, 'template.json'))) {
|
|
884
|
+
return res.status(404).json({
|
|
885
|
+
ok: false,
|
|
886
|
+
error: `Template "${id}" not found in ${tier} scope`,
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
const agentsDir = join(tmplDir, 'agents');
|
|
891
|
+
const overlays = {};
|
|
892
|
+
if (existsSync(agentsDir)) {
|
|
893
|
+
for (const f of readdirSync(agentsDir)) {
|
|
894
|
+
if (!_OVERLAY_NAME_RE.test(f)) continue;
|
|
895
|
+
try {
|
|
896
|
+
overlays[f] = readFileSync(join(agentsDir, f), 'utf8');
|
|
897
|
+
} catch {
|
|
898
|
+
/* skip unreadable files */
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
res.json({ ok: true, overlays });
|
|
903
|
+
} catch (err) {
|
|
904
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* GET /api/projects/:projectId/templates/:tier/:id/prompts
|
|
910
|
+
*
|
|
911
|
+
* Effective per-stage prompt model for the editor's "Prompts" tab: each agent
|
|
912
|
+
* `*.md` and user-prompt `*.block.md` resolved against the built-in core
|
|
913
|
+
* prompts, classified as 'builtin' (fallback), 'pipeline' (replace), or
|
|
914
|
+
* 'extends' (append/overwrite merge). Unlike /overlays this is never empty —
|
|
915
|
+
* a template with no overlays still shows every built-in prompt. Tolerant of a
|
|
916
|
+
* missing template dir (returns core-only) so the tab works for new drafts.
|
|
917
|
+
*/
|
|
918
|
+
router.get('/templates/:tier/:id/prompts', (req, res) => {
|
|
919
|
+
const { tier, id } = req.params;
|
|
920
|
+
if (rejectInvalidTierId(res, tier, id)) return;
|
|
921
|
+
const { projectRoot } = req.project;
|
|
922
|
+
const coreDir = join(projectRoot, '.claude', 'worca', 'agents', 'core');
|
|
923
|
+
const overlayDir = join(dirForTier(projectRoot, tier), id, 'agents');
|
|
924
|
+
try {
|
|
925
|
+
res.json({ ok: true, prompts: buildPromptsModel(coreDir, overlayDir) });
|
|
926
|
+
} catch (err) {
|
|
927
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
693
931
|
/**
|
|
694
932
|
* PUT /api/projects/:projectId/default-template
|
|
695
933
|
*
|