patchwork-os 0.2.0-alpha.28 → 0.2.0-alpha.29
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/deploy/deploy-dashboard.sh +13 -5
- package/dist/activationMetrics.d.ts +67 -0
- package/dist/activationMetrics.js +255 -0
- package/dist/activationMetrics.js.map +1 -0
- package/dist/approvalHttp.d.ts +13 -0
- package/dist/approvalHttp.js +52 -0
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalQueue.d.ts +4 -0
- package/dist/approvalQueue.js +19 -0
- package/dist/approvalQueue.js.map +1 -1
- package/dist/bridge.js +85 -7
- package/dist/bridge.js.map +1 -1
- package/dist/commands/recipe.d.ts +15 -13
- package/dist/commands/recipe.js +248 -431
- package/dist/commands/recipe.js.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +58 -11
- package/dist/index.js.map +1 -1
- package/dist/recipes/chainedRunner.d.ts +35 -5
- package/dist/recipes/chainedRunner.js +153 -21
- package/dist/recipes/chainedRunner.js.map +1 -1
- package/dist/recipes/legacyRecipeCompat.js +5 -0
- package/dist/recipes/legacyRecipeCompat.js.map +1 -1
- package/dist/recipes/scheduler.js +3 -7
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schema.d.ts +17 -2
- package/dist/recipes/schemaGenerator.js +85 -4
- package/dist/recipes/schemaGenerator.js.map +1 -1
- package/dist/recipes/validation.d.ts +13 -0
- package/dist/recipes/validation.js +433 -0
- package/dist/recipes/validation.js.map +1 -0
- package/dist/recipes/yamlRunner.d.ts +8 -0
- package/dist/recipes/yamlRunner.js +147 -64
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +20 -5
- package/dist/recipesHttp.js +266 -13
- package/dist/recipesHttp.js.map +1 -1
- package/dist/schemas/recipe.v1.json +285 -5
- package/dist/server.d.ts +11 -0
- package/dist/server.js +107 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/templates/recipes/project-health-check.yaml +50 -0
|
@@ -191,6 +191,14 @@
|
|
|
191
191
|
"type": "string",
|
|
192
192
|
"description": "Template rendered after tool execution. Use $result to reference the tool output. Supports all template expressions."
|
|
193
193
|
},
|
|
194
|
+
"retry": {
|
|
195
|
+
"type": "number",
|
|
196
|
+
"description": "Number of times to retry this step on failure (overrides recipe-level on_error.retry)"
|
|
197
|
+
},
|
|
198
|
+
"retryDelay": {
|
|
199
|
+
"type": "number",
|
|
200
|
+
"description": "Milliseconds to wait between retries (default 1000)"
|
|
201
|
+
},
|
|
194
202
|
"agent": {
|
|
195
203
|
"type": "object",
|
|
196
204
|
"required": [
|
|
@@ -265,6 +273,14 @@
|
|
|
265
273
|
"type": "string",
|
|
266
274
|
"description": "Template rendered after tool execution. Use $result to reference the tool output. Supports all template expressions."
|
|
267
275
|
},
|
|
276
|
+
"retry": {
|
|
277
|
+
"type": "number",
|
|
278
|
+
"description": "Number of times to retry this step on failure (overrides recipe-level on_error.retry)"
|
|
279
|
+
},
|
|
280
|
+
"retryDelay": {
|
|
281
|
+
"type": "number",
|
|
282
|
+
"description": "Milliseconds to wait between retries (default 1000)"
|
|
283
|
+
},
|
|
268
284
|
"tool": {
|
|
269
285
|
"type": "string",
|
|
270
286
|
"not": {
|
|
@@ -278,8 +294,17 @@
|
|
|
278
294
|
},
|
|
279
295
|
{
|
|
280
296
|
"type": "object",
|
|
281
|
-
"
|
|
282
|
-
|
|
297
|
+
"anyOf": [
|
|
298
|
+
{
|
|
299
|
+
"required": [
|
|
300
|
+
"recipe"
|
|
301
|
+
]
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"required": [
|
|
305
|
+
"chain"
|
|
306
|
+
]
|
|
307
|
+
}
|
|
283
308
|
],
|
|
284
309
|
"properties": {
|
|
285
310
|
"id": {
|
|
@@ -314,10 +339,22 @@
|
|
|
314
339
|
"type": "string",
|
|
315
340
|
"description": "Template rendered after tool execution. Use $result to reference the tool output. Supports all template expressions."
|
|
316
341
|
},
|
|
342
|
+
"retry": {
|
|
343
|
+
"type": "number",
|
|
344
|
+
"description": "Number of times to retry this step on failure (overrides recipe-level on_error.retry)"
|
|
345
|
+
},
|
|
346
|
+
"retryDelay": {
|
|
347
|
+
"type": "number",
|
|
348
|
+
"description": "Milliseconds to wait between retries (default 1000)"
|
|
349
|
+
},
|
|
317
350
|
"recipe": {
|
|
318
351
|
"type": "string",
|
|
319
352
|
"description": "Nested recipe name or path"
|
|
320
353
|
},
|
|
354
|
+
"chain": {
|
|
355
|
+
"type": "string",
|
|
356
|
+
"description": "Alias for nested recipe name or path"
|
|
357
|
+
},
|
|
321
358
|
"vars": {
|
|
322
359
|
"type": "object",
|
|
323
360
|
"description": "Template variables passed into a nested recipe step",
|
|
@@ -330,6 +367,244 @@
|
|
|
330
367
|
"description": "Output key for the nested recipe result"
|
|
331
368
|
}
|
|
332
369
|
}
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
"type": "object",
|
|
373
|
+
"required": [
|
|
374
|
+
"parallel"
|
|
375
|
+
],
|
|
376
|
+
"properties": {
|
|
377
|
+
"id": {
|
|
378
|
+
"type": "string",
|
|
379
|
+
"description": "Optional group id — used as awaits target by later steps"
|
|
380
|
+
},
|
|
381
|
+
"awaits": {
|
|
382
|
+
"type": "array",
|
|
383
|
+
"description": "Step IDs that must complete before this group starts",
|
|
384
|
+
"items": {
|
|
385
|
+
"type": "string"
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
"parallel": {
|
|
389
|
+
"type": "array",
|
|
390
|
+
"description": "Run these steps concurrently. All must finish before later steps that await this group.",
|
|
391
|
+
"items": {
|
|
392
|
+
"oneOf": [
|
|
393
|
+
{
|
|
394
|
+
"type": "object",
|
|
395
|
+
"required": [
|
|
396
|
+
"agent"
|
|
397
|
+
],
|
|
398
|
+
"properties": {
|
|
399
|
+
"id": {
|
|
400
|
+
"type": "string",
|
|
401
|
+
"description": "Unique chained step identifier used for outputs and dependencies"
|
|
402
|
+
},
|
|
403
|
+
"awaits": {
|
|
404
|
+
"type": "array",
|
|
405
|
+
"description": "Step IDs that must complete before this step runs",
|
|
406
|
+
"items": {
|
|
407
|
+
"type": "string"
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
"when": {
|
|
411
|
+
"type": "string",
|
|
412
|
+
"description": "Template condition that controls whether this step runs"
|
|
413
|
+
},
|
|
414
|
+
"optional": {
|
|
415
|
+
"type": "boolean",
|
|
416
|
+
"description": "Whether step failure should be tolerated"
|
|
417
|
+
},
|
|
418
|
+
"risk": {
|
|
419
|
+
"type": "string",
|
|
420
|
+
"enum": [
|
|
421
|
+
"low",
|
|
422
|
+
"medium",
|
|
423
|
+
"high"
|
|
424
|
+
],
|
|
425
|
+
"description": "Risk level for this step"
|
|
426
|
+
},
|
|
427
|
+
"transform": {
|
|
428
|
+
"type": "string",
|
|
429
|
+
"description": "Template rendered after tool execution. Use $result to reference the tool output. Supports all template expressions."
|
|
430
|
+
},
|
|
431
|
+
"retry": {
|
|
432
|
+
"type": "number",
|
|
433
|
+
"description": "Number of times to retry this step on failure (overrides recipe-level on_error.retry)"
|
|
434
|
+
},
|
|
435
|
+
"retryDelay": {
|
|
436
|
+
"type": "number",
|
|
437
|
+
"description": "Milliseconds to wait between retries (default 1000)"
|
|
438
|
+
},
|
|
439
|
+
"agent": {
|
|
440
|
+
"type": "object",
|
|
441
|
+
"required": [
|
|
442
|
+
"prompt"
|
|
443
|
+
],
|
|
444
|
+
"description": "Agent step configuration",
|
|
445
|
+
"properties": {
|
|
446
|
+
"prompt": {
|
|
447
|
+
"type": "string"
|
|
448
|
+
},
|
|
449
|
+
"model": {
|
|
450
|
+
"type": "string"
|
|
451
|
+
},
|
|
452
|
+
"driver": {
|
|
453
|
+
"type": "string",
|
|
454
|
+
"enum": [
|
|
455
|
+
"claude",
|
|
456
|
+
"claude-code",
|
|
457
|
+
"api",
|
|
458
|
+
"openai",
|
|
459
|
+
"grok",
|
|
460
|
+
"gemini",
|
|
461
|
+
"anthropic"
|
|
462
|
+
]
|
|
463
|
+
},
|
|
464
|
+
"into": {
|
|
465
|
+
"type": "string"
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"type": "object",
|
|
473
|
+
"required": [
|
|
474
|
+
"tool"
|
|
475
|
+
],
|
|
476
|
+
"properties": {
|
|
477
|
+
"id": {
|
|
478
|
+
"type": "string",
|
|
479
|
+
"description": "Unique chained step identifier used for outputs and dependencies"
|
|
480
|
+
},
|
|
481
|
+
"awaits": {
|
|
482
|
+
"type": "array",
|
|
483
|
+
"description": "Step IDs that must complete before this step runs",
|
|
484
|
+
"items": {
|
|
485
|
+
"type": "string"
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
"when": {
|
|
489
|
+
"type": "string",
|
|
490
|
+
"description": "Template condition that controls whether this step runs"
|
|
491
|
+
},
|
|
492
|
+
"optional": {
|
|
493
|
+
"type": "boolean",
|
|
494
|
+
"description": "Whether step failure should be tolerated"
|
|
495
|
+
},
|
|
496
|
+
"risk": {
|
|
497
|
+
"type": "string",
|
|
498
|
+
"enum": [
|
|
499
|
+
"low",
|
|
500
|
+
"medium",
|
|
501
|
+
"high"
|
|
502
|
+
],
|
|
503
|
+
"description": "Risk level for this step"
|
|
504
|
+
},
|
|
505
|
+
"transform": {
|
|
506
|
+
"type": "string",
|
|
507
|
+
"description": "Template rendered after tool execution. Use $result to reference the tool output. Supports all template expressions."
|
|
508
|
+
},
|
|
509
|
+
"retry": {
|
|
510
|
+
"type": "number",
|
|
511
|
+
"description": "Number of times to retry this step on failure (overrides recipe-level on_error.retry)"
|
|
512
|
+
},
|
|
513
|
+
"retryDelay": {
|
|
514
|
+
"type": "number",
|
|
515
|
+
"description": "Milliseconds to wait between retries (default 1000)"
|
|
516
|
+
},
|
|
517
|
+
"tool": {
|
|
518
|
+
"type": "string",
|
|
519
|
+
"not": {
|
|
520
|
+
"enum": []
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
"into": {
|
|
524
|
+
"type": "string"
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
"type": "object",
|
|
530
|
+
"anyOf": [
|
|
531
|
+
{
|
|
532
|
+
"required": [
|
|
533
|
+
"recipe"
|
|
534
|
+
]
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
"required": [
|
|
538
|
+
"chain"
|
|
539
|
+
]
|
|
540
|
+
}
|
|
541
|
+
],
|
|
542
|
+
"properties": {
|
|
543
|
+
"id": {
|
|
544
|
+
"type": "string",
|
|
545
|
+
"description": "Unique chained step identifier used for outputs and dependencies"
|
|
546
|
+
},
|
|
547
|
+
"awaits": {
|
|
548
|
+
"type": "array",
|
|
549
|
+
"description": "Step IDs that must complete before this step runs",
|
|
550
|
+
"items": {
|
|
551
|
+
"type": "string"
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
"when": {
|
|
555
|
+
"type": "string",
|
|
556
|
+
"description": "Template condition that controls whether this step runs"
|
|
557
|
+
},
|
|
558
|
+
"optional": {
|
|
559
|
+
"type": "boolean",
|
|
560
|
+
"description": "Whether step failure should be tolerated"
|
|
561
|
+
},
|
|
562
|
+
"risk": {
|
|
563
|
+
"type": "string",
|
|
564
|
+
"enum": [
|
|
565
|
+
"low",
|
|
566
|
+
"medium",
|
|
567
|
+
"high"
|
|
568
|
+
],
|
|
569
|
+
"description": "Risk level for this step"
|
|
570
|
+
},
|
|
571
|
+
"transform": {
|
|
572
|
+
"type": "string",
|
|
573
|
+
"description": "Template rendered after tool execution. Use $result to reference the tool output. Supports all template expressions."
|
|
574
|
+
},
|
|
575
|
+
"retry": {
|
|
576
|
+
"type": "number",
|
|
577
|
+
"description": "Number of times to retry this step on failure (overrides recipe-level on_error.retry)"
|
|
578
|
+
},
|
|
579
|
+
"retryDelay": {
|
|
580
|
+
"type": "number",
|
|
581
|
+
"description": "Milliseconds to wait between retries (default 1000)"
|
|
582
|
+
},
|
|
583
|
+
"recipe": {
|
|
584
|
+
"type": "string",
|
|
585
|
+
"description": "Nested recipe name or path"
|
|
586
|
+
},
|
|
587
|
+
"chain": {
|
|
588
|
+
"type": "string",
|
|
589
|
+
"description": "Alias for nested recipe name or path"
|
|
590
|
+
},
|
|
591
|
+
"vars": {
|
|
592
|
+
"type": "object",
|
|
593
|
+
"description": "Template variables passed into a nested recipe step",
|
|
594
|
+
"additionalProperties": {
|
|
595
|
+
"type": "string"
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
"output": {
|
|
599
|
+
"type": "string",
|
|
600
|
+
"description": "Output key for the nested recipe result"
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
]
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
333
608
|
}
|
|
334
609
|
]
|
|
335
610
|
}
|
|
@@ -381,9 +656,14 @@
|
|
|
381
656
|
"properties": {
|
|
382
657
|
"retry": {
|
|
383
658
|
"type": "number",
|
|
384
|
-
"description": "Number of retries",
|
|
659
|
+
"description": "Number of retries per failing step (overridden by step.retry)",
|
|
385
660
|
"default": 0
|
|
386
661
|
},
|
|
662
|
+
"retryDelay": {
|
|
663
|
+
"type": "number",
|
|
664
|
+
"description": "Milliseconds between retries (overridden by step.retryDelay)",
|
|
665
|
+
"default": 1000
|
|
666
|
+
},
|
|
387
667
|
"fallback": {
|
|
388
668
|
"type": "string",
|
|
389
669
|
"enum": [
|
|
@@ -391,11 +671,11 @@
|
|
|
391
671
|
"abort",
|
|
392
672
|
"deliver_original"
|
|
393
673
|
],
|
|
394
|
-
"description": "
|
|
674
|
+
"description": "log_only / deliver_original: treat step failure as non-fatal (like optional); fail-open. abort (default): propagate failure."
|
|
395
675
|
},
|
|
396
676
|
"notify": {
|
|
397
677
|
"type": "boolean",
|
|
398
|
-
"description": "
|
|
678
|
+
"description": "Reserved. yamlRunner currently posts Slack notifications on any step failure when slack is connected; gating on this flag is not yet wired.",
|
|
399
679
|
"default": true
|
|
400
680
|
}
|
|
401
681
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -59,6 +59,17 @@ export declare class Server extends EventEmitter<ServerEvents> {
|
|
|
59
59
|
}) | null;
|
|
60
60
|
/** Patchwork: set by bridge to list installed recipes for the dashboard. */
|
|
61
61
|
recipesFn: (() => Record<string, unknown>) | null;
|
|
62
|
+
/** Patchwork: set by bridge to load raw recipe source content by name. */
|
|
63
|
+
loadRecipeContentFn: ((name: string) => {
|
|
64
|
+
content: string;
|
|
65
|
+
path: string;
|
|
66
|
+
} | null) | null;
|
|
67
|
+
/** Patchwork: set by bridge to save raw recipe source content by name. */
|
|
68
|
+
saveRecipeContentFn: ((name: string, content: string) => {
|
|
69
|
+
ok: boolean;
|
|
70
|
+
path?: string;
|
|
71
|
+
error?: string;
|
|
72
|
+
}) | null;
|
|
62
73
|
/** Patchwork: set by bridge to save a new recipe draft to disk. */
|
|
63
74
|
saveRecipeFn: ((draft: RecipeDraft) => {
|
|
64
75
|
ok: boolean;
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,8 @@ import http from "node:http";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { WebSocket, WebSocketServer as WsServer } from "ws";
|
|
6
|
-
import {
|
|
6
|
+
import { computeSummary as computeActivationSummary, loadMetrics as loadActivationMetrics, } from "./activationMetrics.js";
|
|
7
|
+
import { handleApprovalsStream, routeApprovalRequest } from "./approvalHttp.js";
|
|
7
8
|
import { getApprovalQueue } from "./approvalQueue.js";
|
|
8
9
|
import { saveBridgeConfigDriver } from "./config.js";
|
|
9
10
|
import { timingSafeStringEqual } from "./crypto.js";
|
|
@@ -90,6 +91,10 @@ export class Server extends EventEmitter {
|
|
|
90
91
|
tasksFn = null;
|
|
91
92
|
/** Patchwork: set by bridge to list installed recipes for the dashboard. */
|
|
92
93
|
recipesFn = null;
|
|
94
|
+
/** Patchwork: set by bridge to load raw recipe source content by name. */
|
|
95
|
+
loadRecipeContentFn = null;
|
|
96
|
+
/** Patchwork: set by bridge to save raw recipe source content by name. */
|
|
97
|
+
saveRecipeContentFn = null;
|
|
93
98
|
/** Patchwork: set by bridge to save a new recipe draft to disk. */
|
|
94
99
|
saveRecipeFn = null;
|
|
95
100
|
/** Patchwork: set by bridge to query the recipe run audit log. */
|
|
@@ -1511,6 +1516,22 @@ export class Server extends EventEmitter {
|
|
|
1511
1516
|
});
|
|
1512
1517
|
return;
|
|
1513
1518
|
}
|
|
1519
|
+
if (parsedUrl.pathname === "/activation-metrics" &&
|
|
1520
|
+
req.method === "GET") {
|
|
1521
|
+
try {
|
|
1522
|
+
const metrics = loadActivationMetrics();
|
|
1523
|
+
const summary = computeActivationSummary(metrics);
|
|
1524
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1525
|
+
res.end(JSON.stringify({ metrics, summary }));
|
|
1526
|
+
}
|
|
1527
|
+
catch (err) {
|
|
1528
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1529
|
+
res.end(JSON.stringify({
|
|
1530
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1531
|
+
}));
|
|
1532
|
+
}
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1514
1535
|
if (parsedUrl.pathname === "/runs" && req.method === "GET") {
|
|
1515
1536
|
try {
|
|
1516
1537
|
const sp = parsedUrl.searchParams;
|
|
@@ -1662,6 +1683,60 @@ export class Server extends EventEmitter {
|
|
|
1662
1683
|
});
|
|
1663
1684
|
return;
|
|
1664
1685
|
}
|
|
1686
|
+
const recipeContentMatch = /^\/recipes\/([^/]+)$/.exec(parsedUrl.pathname);
|
|
1687
|
+
if (recipeContentMatch && req.method === "GET") {
|
|
1688
|
+
const name = decodeURIComponent(recipeContentMatch[1]);
|
|
1689
|
+
if (!this.loadRecipeContentFn) {
|
|
1690
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1691
|
+
res.end(JSON.stringify({ ok: false, error: "Recipe content unavailable" }));
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
const result = this.loadRecipeContentFn(name);
|
|
1695
|
+
if (!result) {
|
|
1696
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1697
|
+
res.end(JSON.stringify({ ok: false, error: "Recipe not found" }));
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1701
|
+
res.end(JSON.stringify(result));
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (recipeContentMatch && req.method === "PUT") {
|
|
1705
|
+
const name = decodeURIComponent(recipeContentMatch[1]);
|
|
1706
|
+
const chunks = [];
|
|
1707
|
+
req.on("data", (c) => chunks.push(c));
|
|
1708
|
+
req.on("end", () => {
|
|
1709
|
+
try {
|
|
1710
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
1711
|
+
if (typeof body.content !== "string") {
|
|
1712
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1713
|
+
res.end(JSON.stringify({
|
|
1714
|
+
ok: false,
|
|
1715
|
+
error: "content (string) required",
|
|
1716
|
+
}));
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
if (!this.saveRecipeContentFn) {
|
|
1720
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1721
|
+
res.end(JSON.stringify({
|
|
1722
|
+
ok: false,
|
|
1723
|
+
error: "Recipe content saving unavailable",
|
|
1724
|
+
}));
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const result = this.saveRecipeContentFn(name, body.content);
|
|
1728
|
+
res.writeHead(result.ok ? 200 : 400, {
|
|
1729
|
+
"Content-Type": "application/json",
|
|
1730
|
+
});
|
|
1731
|
+
res.end(JSON.stringify(result));
|
|
1732
|
+
}
|
|
1733
|
+
catch {
|
|
1734
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1735
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1665
1740
|
if (req.url === "/recipes" && req.method === "GET") {
|
|
1666
1741
|
try {
|
|
1667
1742
|
const data = this.recipesFn?.() ?? { recipesDir: null, recipes: [] };
|
|
@@ -1777,6 +1852,29 @@ export class Server extends EventEmitter {
|
|
|
1777
1852
|
cfg.driver = driver;
|
|
1778
1853
|
saveBridgeConfigDriver(driver, this.bridgeConfigPath);
|
|
1779
1854
|
}
|
|
1855
|
+
if (body.model !== undefined) {
|
|
1856
|
+
const validModels = [
|
|
1857
|
+
"claude",
|
|
1858
|
+
"openai",
|
|
1859
|
+
"gemini",
|
|
1860
|
+
"grok",
|
|
1861
|
+
"local",
|
|
1862
|
+
];
|
|
1863
|
+
if (!validModels.includes(body.model)) {
|
|
1864
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1865
|
+
res.end(JSON.stringify({
|
|
1866
|
+
error: `model must be one of: ${validModels.join(", ")}`,
|
|
1867
|
+
}));
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
cfg.model = body.model;
|
|
1871
|
+
if (body.model === "local") {
|
|
1872
|
+
if (body.localEndpoint !== undefined)
|
|
1873
|
+
cfg.localEndpoint = body.localEndpoint.trim() || undefined;
|
|
1874
|
+
if (body.localModel !== undefined)
|
|
1875
|
+
cfg.localModel = body.localModel.trim() || undefined;
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1780
1878
|
if (body.apiKey) {
|
|
1781
1879
|
const { provider, key } = body.apiKey;
|
|
1782
1880
|
const validProviders = ["anthropic", "openai", "google", "xai"];
|
|
@@ -1808,7 +1906,9 @@ export class Server extends EventEmitter {
|
|
|
1808
1906
|
this.pushServiceBaseUrl =
|
|
1809
1907
|
body.pushServiceBaseUrl.trim() || undefined;
|
|
1810
1908
|
}
|
|
1811
|
-
const restartRequired = driverRaw !== undefined ||
|
|
1909
|
+
const restartRequired = driverRaw !== undefined ||
|
|
1910
|
+
body.apiKey !== undefined ||
|
|
1911
|
+
body.model !== undefined;
|
|
1812
1912
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1813
1913
|
res.end(JSON.stringify({ ok: true, restartRequired }));
|
|
1814
1914
|
}
|
|
@@ -1874,6 +1974,11 @@ export class Server extends EventEmitter {
|
|
|
1874
1974
|
}
|
|
1875
1975
|
return;
|
|
1876
1976
|
}
|
|
1977
|
+
// SSE stream for live approval queue updates.
|
|
1978
|
+
if (parsedUrl.pathname === "/approvals/stream" && req.method === "GET") {
|
|
1979
|
+
handleApprovalsStream(res, { queue: getApprovalQueue() }, parsedUrl.searchParams.get("session"));
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1877
1982
|
// Patchwork approval surface — PreToolUse hook + dashboard approve/reject.
|
|
1878
1983
|
// Bearer auth already checked above.
|
|
1879
1984
|
if (parsedUrl.pathname === "/approvals" ||
|