forge-openclaw-plugin 0.2.3

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.
@@ -0,0 +1,1152 @@
1
+ import { canBootstrapOperatorSession, callForgeApi, expectForgeSuccess, readJsonRequestBody, readSingleHeaderValue, requireApiToken, writeForgeProxyResponse, writePluginError, writeJsonResponse } from "./api-client.js";
2
+ import { FORGE_PLUGIN_ROUTE_EXCLUSIONS, makeApiRouteKey } from "./parity.js";
3
+ function passthroughSearch(path, url) {
4
+ return `${path}${url.search}`;
5
+ }
6
+ function encodePathSegment(value) {
7
+ return encodeURIComponent(decodeURIComponent(value));
8
+ }
9
+ function methodNotAllowed(response, allowedMethods) {
10
+ response.setHeader("allow", allowedMethods.join(", "));
11
+ writeJsonResponse(response, 405, {
12
+ ok: false,
13
+ error: {
14
+ code: "forge_plugin_method_not_allowed",
15
+ message: `Allowed methods: ${allowedMethods.join(", ")}`
16
+ }
17
+ });
18
+ }
19
+ function routeNotFound(response, pathname) {
20
+ writeJsonResponse(response, 404, {
21
+ ok: false,
22
+ error: {
23
+ code: "forge_plugin_route_not_found",
24
+ message: `No Forge plugin route matches ${pathname}`
25
+ }
26
+ });
27
+ }
28
+ async function forwardOperation(request, response, config, operation, match, url) {
29
+ if (operation.requiresToken) {
30
+ requireApiToken(config);
31
+ }
32
+ const body = operation.requestBody === "json" ? await readJsonRequestBody(request, { emptyObject: true }) : undefined;
33
+ const result = await callForgeApi({
34
+ baseUrl: config.baseUrl,
35
+ apiToken: config.apiToken,
36
+ actorLabel: config.actorLabel,
37
+ timeoutMs: config.timeoutMs,
38
+ method: operation.method,
39
+ path: operation.target(match, url),
40
+ body,
41
+ idempotencyKey: readSingleHeaderValue(request.headers, "idempotency-key"),
42
+ extraHeaders: {
43
+ "if-match": readSingleHeaderValue(request.headers, "if-match")
44
+ }
45
+ });
46
+ writeForgeProxyResponse(response, result);
47
+ }
48
+ async function handleGroup(request, response, config, group) {
49
+ try {
50
+ const method = (request.method ?? "GET").toUpperCase();
51
+ const url = new URL(request.url ?? group.path, "http://openclaw.local");
52
+ const pathname = url.pathname;
53
+ const matchingOperations = group.operations
54
+ .map((operation) => {
55
+ const match = pathname.match(operation.pattern);
56
+ return match ? { operation, match } : null;
57
+ })
58
+ .filter((entry) => entry !== null);
59
+ if (matchingOperations.length === 0) {
60
+ routeNotFound(response, pathname);
61
+ return;
62
+ }
63
+ const matchedOperation = matchingOperations.find((entry) => entry.operation.method === method);
64
+ if (!matchedOperation) {
65
+ methodNotAllowed(response, [...new Set(matchingOperations.map((entry) => entry.operation.method))]);
66
+ return;
67
+ }
68
+ await forwardOperation(request, response, config, matchedOperation.operation, matchedOperation.match, url);
69
+ }
70
+ catch (error) {
71
+ writePluginError(response, error);
72
+ }
73
+ }
74
+ const exact = (path, operation) => ({
75
+ path,
76
+ match: "exact",
77
+ operations: [{ ...operation, pattern: new RegExp(`^${path.replaceAll("/", "\\/")}$`) }]
78
+ });
79
+ export const FORGE_PLUGIN_ROUTE_GROUPS = [
80
+ exact("/forge/v1/health", {
81
+ method: "GET",
82
+ upstreamPath: "/api/v1/health",
83
+ target: (_match, url) => passthroughSearch("/api/v1/health", url)
84
+ }),
85
+ exact("/forge/v1/openapi.json", {
86
+ method: "GET",
87
+ upstreamPath: "/api/v1/openapi.json",
88
+ target: (_match, url) => passthroughSearch("/api/v1/openapi.json", url)
89
+ }),
90
+ exact("/forge/v1/context", {
91
+ method: "GET",
92
+ upstreamPath: "/api/v1/context",
93
+ target: (_match, url) => passthroughSearch("/api/v1/context", url)
94
+ }),
95
+ exact("/forge/v1/domains", {
96
+ method: "GET",
97
+ upstreamPath: "/api/v1/domains",
98
+ target: (_match, url) => passthroughSearch("/api/v1/domains", url)
99
+ }),
100
+ {
101
+ path: "/forge/v1/goals",
102
+ match: "prefix",
103
+ operations: [
104
+ {
105
+ method: "GET",
106
+ pattern: /^\/forge\/v1\/goals$/,
107
+ upstreamPath: "/api/v1/goals",
108
+ target: (_match, url) => passthroughSearch("/api/v1/goals", url)
109
+ },
110
+ {
111
+ method: "POST",
112
+ pattern: /^\/forge\/v1\/goals$/,
113
+ upstreamPath: "/api/v1/goals",
114
+ requestBody: "json",
115
+ requiresToken: true,
116
+ target: (_match, url) => passthroughSearch("/api/v1/goals", url)
117
+ },
118
+ {
119
+ method: "GET",
120
+ pattern: /^\/forge\/v1\/goals\/([^/]+)$/,
121
+ upstreamPath: "/api/v1/goals/:id",
122
+ target: (match, url) => passthroughSearch(`/api/v1/goals/${encodePathSegment(match[1] ?? "")}`, url)
123
+ },
124
+ {
125
+ method: "PATCH",
126
+ pattern: /^\/forge\/v1\/goals\/([^/]+)$/,
127
+ upstreamPath: "/api/v1/goals/:id",
128
+ requestBody: "json",
129
+ requiresToken: true,
130
+ target: (match, url) => passthroughSearch(`/api/v1/goals/${encodePathSegment(match[1] ?? "")}`, url)
131
+ },
132
+ {
133
+ method: "DELETE",
134
+ pattern: /^\/forge\/v1\/goals\/([^/]+)$/,
135
+ upstreamPath: "/api/v1/goals/:id",
136
+ requiresToken: true,
137
+ target: (match, url) => passthroughSearch(`/api/v1/goals/${encodePathSegment(match[1] ?? "")}`, url)
138
+ }
139
+ ]
140
+ },
141
+ {
142
+ path: "/forge/v1/projects",
143
+ match: "prefix",
144
+ operations: [
145
+ {
146
+ method: "GET",
147
+ pattern: /^\/forge\/v1\/projects$/,
148
+ upstreamPath: "/api/v1/projects",
149
+ target: (_match, url) => passthroughSearch("/api/v1/projects", url)
150
+ },
151
+ {
152
+ method: "POST",
153
+ pattern: /^\/forge\/v1\/projects$/,
154
+ upstreamPath: "/api/v1/projects",
155
+ requestBody: "json",
156
+ requiresToken: true,
157
+ target: (_match, url) => passthroughSearch("/api/v1/projects", url)
158
+ },
159
+ {
160
+ method: "GET",
161
+ pattern: /^\/forge\/v1\/projects\/([^/]+)$/,
162
+ upstreamPath: "/api/v1/projects/:id",
163
+ target: (match, url) => passthroughSearch(`/api/v1/projects/${encodePathSegment(match[1] ?? "")}`, url)
164
+ },
165
+ {
166
+ method: "PATCH",
167
+ pattern: /^\/forge\/v1\/projects\/([^/]+)$/,
168
+ upstreamPath: "/api/v1/projects/:id",
169
+ requestBody: "json",
170
+ requiresToken: true,
171
+ target: (match, url) => passthroughSearch(`/api/v1/projects/${encodePathSegment(match[1] ?? "")}`, url)
172
+ },
173
+ {
174
+ method: "DELETE",
175
+ pattern: /^\/forge\/v1\/projects\/([^/]+)$/,
176
+ upstreamPath: "/api/v1/projects/:id",
177
+ requiresToken: true,
178
+ target: (match, url) => passthroughSearch(`/api/v1/projects/${encodePathSegment(match[1] ?? "")}`, url)
179
+ },
180
+ {
181
+ method: "GET",
182
+ pattern: /^\/forge\/v1\/projects\/([^/]+)\/board$/,
183
+ upstreamPath: "/api/v1/projects/:id/board",
184
+ target: (match, url) => passthroughSearch(`/api/v1/projects/${encodePathSegment(match[1] ?? "")}/board`, url)
185
+ }
186
+ ]
187
+ },
188
+ {
189
+ path: "/forge/v1/tasks",
190
+ match: "prefix",
191
+ operations: [
192
+ {
193
+ method: "GET",
194
+ pattern: /^\/forge\/v1\/tasks$/,
195
+ upstreamPath: "/api/v1/tasks",
196
+ target: (_match, url) => passthroughSearch("/api/v1/tasks", url)
197
+ },
198
+ {
199
+ method: "POST",
200
+ pattern: /^\/forge\/v1\/tasks$/,
201
+ upstreamPath: "/api/v1/tasks",
202
+ requestBody: "json",
203
+ requiresToken: true,
204
+ target: (_match, url) => passthroughSearch("/api/v1/tasks", url)
205
+ },
206
+ {
207
+ method: "GET",
208
+ pattern: /^\/forge\/v1\/tasks\/([^/]+)$/,
209
+ upstreamPath: "/api/v1/tasks/:id",
210
+ target: (match, url) => passthroughSearch(`/api/v1/tasks/${encodePathSegment(match[1] ?? "")}`, url)
211
+ },
212
+ {
213
+ method: "PATCH",
214
+ pattern: /^\/forge\/v1\/tasks\/([^/]+)$/,
215
+ upstreamPath: "/api/v1/tasks/:id",
216
+ requestBody: "json",
217
+ requiresToken: true,
218
+ target: (match, url) => passthroughSearch(`/api/v1/tasks/${encodePathSegment(match[1] ?? "")}`, url)
219
+ },
220
+ {
221
+ method: "DELETE",
222
+ pattern: /^\/forge\/v1\/tasks\/([^/]+)$/,
223
+ upstreamPath: "/api/v1/tasks/:id",
224
+ requiresToken: true,
225
+ target: (match, url) => passthroughSearch(`/api/v1/tasks/${encodePathSegment(match[1] ?? "")}`, url)
226
+ },
227
+ {
228
+ method: "GET",
229
+ pattern: /^\/forge\/v1\/tasks\/([^/]+)\/context$/,
230
+ upstreamPath: "/api/v1/tasks/:id/context",
231
+ target: (match, url) => passthroughSearch(`/api/v1/tasks/${encodePathSegment(match[1] ?? "")}/context`, url)
232
+ },
233
+ {
234
+ method: "POST",
235
+ pattern: /^\/forge\/v1\/tasks\/([^/]+)\/runs$/,
236
+ upstreamPath: "/api/v1/tasks/:id/runs",
237
+ requestBody: "json",
238
+ requiresToken: true,
239
+ target: (match, url) => passthroughSearch(`/api/v1/tasks/${encodePathSegment(match[1] ?? "")}/runs`, url)
240
+ },
241
+ {
242
+ method: "POST",
243
+ pattern: /^\/forge\/v1\/tasks\/([^/]+)\/uncomplete$/,
244
+ upstreamPath: "/api/v1/tasks/:id/uncomplete",
245
+ requestBody: "json",
246
+ requiresToken: true,
247
+ target: (match, url) => passthroughSearch(`/api/v1/tasks/${encodePathSegment(match[1] ?? "")}/uncomplete`, url)
248
+ }
249
+ ]
250
+ },
251
+ {
252
+ path: "/forge/v1/task-runs",
253
+ match: "prefix",
254
+ operations: [
255
+ {
256
+ method: "GET",
257
+ pattern: /^\/forge\/v1\/task-runs$/,
258
+ upstreamPath: "/api/v1/task-runs",
259
+ target: (_match, url) => passthroughSearch("/api/v1/task-runs", url)
260
+ },
261
+ {
262
+ method: "POST",
263
+ pattern: /^\/forge\/v1\/task-runs\/([^/]+)\/heartbeat$/,
264
+ upstreamPath: "/api/v1/task-runs/:id/heartbeat",
265
+ requestBody: "json",
266
+ requiresToken: true,
267
+ target: (match, url) => passthroughSearch(`/api/v1/task-runs/${encodePathSegment(match[1] ?? "")}/heartbeat`, url)
268
+ },
269
+ {
270
+ method: "POST",
271
+ pattern: /^\/forge\/v1\/task-runs\/([^/]+)\/complete$/,
272
+ upstreamPath: "/api/v1/task-runs/:id/complete",
273
+ requestBody: "json",
274
+ requiresToken: true,
275
+ target: (match, url) => passthroughSearch(`/api/v1/task-runs/${encodePathSegment(match[1] ?? "")}/complete`, url)
276
+ },
277
+ {
278
+ method: "POST",
279
+ pattern: /^\/forge\/v1\/task-runs\/([^/]+)\/focus$/,
280
+ upstreamPath: "/api/v1/task-runs/:id/focus",
281
+ requestBody: "json",
282
+ requiresToken: true,
283
+ target: (match, url) => passthroughSearch(`/api/v1/task-runs/${encodePathSegment(match[1] ?? "")}/focus`, url)
284
+ },
285
+ {
286
+ method: "POST",
287
+ pattern: /^\/forge\/v1\/task-runs\/([^/]+)\/release$/,
288
+ upstreamPath: "/api/v1/task-runs/:id/release",
289
+ requestBody: "json",
290
+ requiresToken: true,
291
+ target: (match, url) => passthroughSearch(`/api/v1/task-runs/${encodePathSegment(match[1] ?? "")}/release`, url)
292
+ }
293
+ ]
294
+ },
295
+ {
296
+ path: "/forge/v1/tags",
297
+ match: "prefix",
298
+ operations: [
299
+ {
300
+ method: "GET",
301
+ pattern: /^\/forge\/v1\/tags$/,
302
+ upstreamPath: "/api/v1/tags",
303
+ target: (_match, url) => passthroughSearch("/api/v1/tags", url)
304
+ },
305
+ {
306
+ method: "POST",
307
+ pattern: /^\/forge\/v1\/tags$/,
308
+ upstreamPath: "/api/v1/tags",
309
+ requestBody: "json",
310
+ requiresToken: true,
311
+ target: (_match, url) => passthroughSearch("/api/v1/tags", url)
312
+ },
313
+ {
314
+ method: "GET",
315
+ pattern: /^\/forge\/v1\/tags\/([^/]+)$/,
316
+ upstreamPath: "/api/v1/tags/:id",
317
+ target: (match, url) => passthroughSearch(`/api/v1/tags/${encodePathSegment(match[1] ?? "")}`, url)
318
+ },
319
+ {
320
+ method: "PATCH",
321
+ pattern: /^\/forge\/v1\/tags\/([^/]+)$/,
322
+ upstreamPath: "/api/v1/tags/:id",
323
+ requestBody: "json",
324
+ requiresToken: true,
325
+ target: (match, url) => passthroughSearch(`/api/v1/tags/${encodePathSegment(match[1] ?? "")}`, url)
326
+ },
327
+ {
328
+ method: "DELETE",
329
+ pattern: /^\/forge\/v1\/tags\/([^/]+)$/,
330
+ upstreamPath: "/api/v1/tags/:id",
331
+ requiresToken: true,
332
+ target: (match, url) => passthroughSearch(`/api/v1/tags/${encodePathSegment(match[1] ?? "")}`, url)
333
+ }
334
+ ]
335
+ },
336
+ {
337
+ path: "/forge/v1/activity",
338
+ match: "prefix",
339
+ operations: [
340
+ {
341
+ method: "GET",
342
+ pattern: /^\/forge\/v1\/activity$/,
343
+ upstreamPath: "/api/v1/activity",
344
+ target: (_match, url) => passthroughSearch("/api/v1/activity", url)
345
+ },
346
+ {
347
+ method: "POST",
348
+ pattern: /^\/forge\/v1\/activity\/([^/]+)\/remove$/,
349
+ upstreamPath: "/api/v1/activity/:id/remove",
350
+ requestBody: "json",
351
+ requiresToken: true,
352
+ target: (match, url) => passthroughSearch(`/api/v1/activity/${encodePathSegment(match[1] ?? "")}/remove`, url)
353
+ }
354
+ ]
355
+ },
356
+ {
357
+ path: "/forge/v1/metrics",
358
+ match: "prefix",
359
+ operations: [
360
+ {
361
+ method: "GET",
362
+ pattern: /^\/forge\/v1\/metrics$/,
363
+ upstreamPath: "/api/v1/metrics",
364
+ target: (_match, url) => passthroughSearch("/api/v1/metrics", url)
365
+ },
366
+ {
367
+ method: "GET",
368
+ pattern: /^\/forge\/v1\/metrics\/xp$/,
369
+ upstreamPath: "/api/v1/metrics/xp",
370
+ target: (_match, url) => passthroughSearch("/api/v1/metrics/xp", url)
371
+ }
372
+ ]
373
+ },
374
+ {
375
+ path: "/forge/v1/operator",
376
+ match: "prefix",
377
+ operations: [
378
+ {
379
+ method: "GET",
380
+ pattern: /^\/forge\/v1\/operator\/overview$/,
381
+ upstreamPath: "/api/v1/operator/overview",
382
+ target: (_match, url) => passthroughSearch("/api/v1/operator/overview", url)
383
+ },
384
+ {
385
+ method: "GET",
386
+ pattern: /^\/forge\/v1\/operator\/context$/,
387
+ upstreamPath: "/api/v1/operator/context",
388
+ target: (_match, url) => passthroughSearch("/api/v1/operator/context", url)
389
+ },
390
+ {
391
+ method: "POST",
392
+ pattern: /^\/forge\/v1\/operator\/log-work$/,
393
+ upstreamPath: "/api/v1/operator/log-work",
394
+ requestBody: "json",
395
+ requiresToken: true,
396
+ target: (_match, url) => passthroughSearch("/api/v1/operator/log-work", url)
397
+ }
398
+ ]
399
+ },
400
+ {
401
+ path: "/forge/v1/comments",
402
+ match: "prefix",
403
+ operations: [
404
+ {
405
+ method: "GET",
406
+ pattern: /^\/forge\/v1\/comments$/,
407
+ upstreamPath: "/api/v1/comments",
408
+ target: (_match, url) => passthroughSearch("/api/v1/comments", url)
409
+ },
410
+ {
411
+ method: "POST",
412
+ pattern: /^\/forge\/v1\/comments$/,
413
+ upstreamPath: "/api/v1/comments",
414
+ requestBody: "json",
415
+ requiresToken: true,
416
+ target: (_match, url) => passthroughSearch("/api/v1/comments", url)
417
+ },
418
+ {
419
+ method: "PATCH",
420
+ pattern: /^\/forge\/v1\/comments\/([^/]+)$/,
421
+ upstreamPath: "/api/v1/comments/:id",
422
+ requestBody: "json",
423
+ requiresToken: true,
424
+ target: (match, url) => passthroughSearch(`/api/v1/comments/${encodePathSegment(match[1] ?? "")}`, url)
425
+ },
426
+ {
427
+ method: "GET",
428
+ pattern: /^\/forge\/v1\/comments\/([^/]+)$/,
429
+ upstreamPath: "/api/v1/comments/:id",
430
+ target: (match, url) => passthroughSearch(`/api/v1/comments/${encodePathSegment(match[1] ?? "")}`, url)
431
+ },
432
+ {
433
+ method: "DELETE",
434
+ pattern: /^\/forge\/v1\/comments\/([^/]+)$/,
435
+ upstreamPath: "/api/v1/comments/:id",
436
+ requiresToken: true,
437
+ target: (match, url) => passthroughSearch(`/api/v1/comments/${encodePathSegment(match[1] ?? "")}`, url)
438
+ }
439
+ ]
440
+ },
441
+ {
442
+ path: "/forge/v1/insights",
443
+ match: "prefix",
444
+ operations: [
445
+ {
446
+ method: "GET",
447
+ pattern: /^\/forge\/v1\/insights$/,
448
+ upstreamPath: "/api/v1/insights",
449
+ target: (_match, url) => passthroughSearch("/api/v1/insights", url)
450
+ },
451
+ {
452
+ method: "POST",
453
+ pattern: /^\/forge\/v1\/insights$/,
454
+ upstreamPath: "/api/v1/insights",
455
+ requestBody: "json",
456
+ requiresToken: true,
457
+ target: (_match, url) => passthroughSearch("/api/v1/insights", url)
458
+ },
459
+ {
460
+ method: "GET",
461
+ pattern: /^\/forge\/v1\/insights\/([^/]+)$/,
462
+ upstreamPath: "/api/v1/insights/:id",
463
+ target: (match, url) => passthroughSearch(`/api/v1/insights/${encodePathSegment(match[1] ?? "")}`, url)
464
+ },
465
+ {
466
+ method: "PATCH",
467
+ pattern: /^\/forge\/v1\/insights\/([^/]+)$/,
468
+ upstreamPath: "/api/v1/insights/:id",
469
+ requestBody: "json",
470
+ requiresToken: true,
471
+ target: (match, url) => passthroughSearch(`/api/v1/insights/${encodePathSegment(match[1] ?? "")}`, url)
472
+ },
473
+ {
474
+ method: "DELETE",
475
+ pattern: /^\/forge\/v1\/insights\/([^/]+)$/,
476
+ upstreamPath: "/api/v1/insights/:id",
477
+ requiresToken: true,
478
+ target: (match, url) => passthroughSearch(`/api/v1/insights/${encodePathSegment(match[1] ?? "")}`, url)
479
+ },
480
+ {
481
+ method: "POST",
482
+ pattern: /^\/forge\/v1\/insights\/([^/]+)\/feedback$/,
483
+ upstreamPath: "/api/v1/insights/:id/feedback",
484
+ requestBody: "json",
485
+ requiresToken: true,
486
+ target: (match, url) => passthroughSearch(`/api/v1/insights/${encodePathSegment(match[1] ?? "")}/feedback`, url)
487
+ }
488
+ ]
489
+ },
490
+ {
491
+ path: "/forge/v1/psyche",
492
+ match: "prefix",
493
+ operations: [
494
+ {
495
+ method: "GET",
496
+ pattern: /^\/forge\/v1\/psyche\/overview$/,
497
+ upstreamPath: "/api/v1/psyche/overview",
498
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/overview", url)
499
+ },
500
+ {
501
+ method: "GET",
502
+ pattern: /^\/forge\/v1\/psyche\/values$/,
503
+ upstreamPath: "/api/v1/psyche/values",
504
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/values", url)
505
+ },
506
+ {
507
+ method: "POST",
508
+ pattern: /^\/forge\/v1\/psyche\/values$/,
509
+ upstreamPath: "/api/v1/psyche/values",
510
+ requestBody: "json",
511
+ requiresToken: true,
512
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/values", url)
513
+ },
514
+ {
515
+ method: "GET",
516
+ pattern: /^\/forge\/v1\/psyche\/values\/([^/]+)$/,
517
+ upstreamPath: "/api/v1/psyche/values/:id",
518
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/values/${encodePathSegment(match[1] ?? "")}`, url)
519
+ },
520
+ {
521
+ method: "PATCH",
522
+ pattern: /^\/forge\/v1\/psyche\/values\/([^/]+)$/,
523
+ upstreamPath: "/api/v1/psyche/values/:id",
524
+ requestBody: "json",
525
+ requiresToken: true,
526
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/values/${encodePathSegment(match[1] ?? "")}`, url)
527
+ },
528
+ {
529
+ method: "DELETE",
530
+ pattern: /^\/forge\/v1\/psyche\/values\/([^/]+)$/,
531
+ upstreamPath: "/api/v1/psyche/values/:id",
532
+ requiresToken: true,
533
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/values/${encodePathSegment(match[1] ?? "")}`, url)
534
+ },
535
+ {
536
+ method: "GET",
537
+ pattern: /^\/forge\/v1\/psyche\/patterns$/,
538
+ upstreamPath: "/api/v1/psyche/patterns",
539
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/patterns", url)
540
+ },
541
+ {
542
+ method: "POST",
543
+ pattern: /^\/forge\/v1\/psyche\/patterns$/,
544
+ upstreamPath: "/api/v1/psyche/patterns",
545
+ requestBody: "json",
546
+ requiresToken: true,
547
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/patterns", url)
548
+ },
549
+ {
550
+ method: "GET",
551
+ pattern: /^\/forge\/v1\/psyche\/patterns\/([^/]+)$/,
552
+ upstreamPath: "/api/v1/psyche/patterns/:id",
553
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/patterns/${encodePathSegment(match[1] ?? "")}`, url)
554
+ },
555
+ {
556
+ method: "PATCH",
557
+ pattern: /^\/forge\/v1\/psyche\/patterns\/([^/]+)$/,
558
+ upstreamPath: "/api/v1/psyche/patterns/:id",
559
+ requestBody: "json",
560
+ requiresToken: true,
561
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/patterns/${encodePathSegment(match[1] ?? "")}`, url)
562
+ },
563
+ {
564
+ method: "DELETE",
565
+ pattern: /^\/forge\/v1\/psyche\/patterns\/([^/]+)$/,
566
+ upstreamPath: "/api/v1/psyche/patterns/:id",
567
+ requiresToken: true,
568
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/patterns/${encodePathSegment(match[1] ?? "")}`, url)
569
+ },
570
+ {
571
+ method: "GET",
572
+ pattern: /^\/forge\/v1\/psyche\/behaviors$/,
573
+ upstreamPath: "/api/v1/psyche/behaviors",
574
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/behaviors", url)
575
+ },
576
+ {
577
+ method: "POST",
578
+ pattern: /^\/forge\/v1\/psyche\/behaviors$/,
579
+ upstreamPath: "/api/v1/psyche/behaviors",
580
+ requestBody: "json",
581
+ requiresToken: true,
582
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/behaviors", url)
583
+ },
584
+ {
585
+ method: "GET",
586
+ pattern: /^\/forge\/v1\/psyche\/behaviors\/([^/]+)$/,
587
+ upstreamPath: "/api/v1/psyche/behaviors/:id",
588
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/behaviors/${encodePathSegment(match[1] ?? "")}`, url)
589
+ },
590
+ {
591
+ method: "PATCH",
592
+ pattern: /^\/forge\/v1\/psyche\/behaviors\/([^/]+)$/,
593
+ upstreamPath: "/api/v1/psyche/behaviors/:id",
594
+ requestBody: "json",
595
+ requiresToken: true,
596
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/behaviors/${encodePathSegment(match[1] ?? "")}`, url)
597
+ },
598
+ {
599
+ method: "DELETE",
600
+ pattern: /^\/forge\/v1\/psyche\/behaviors\/([^/]+)$/,
601
+ upstreamPath: "/api/v1/psyche/behaviors/:id",
602
+ requiresToken: true,
603
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/behaviors/${encodePathSegment(match[1] ?? "")}`, url)
604
+ },
605
+ {
606
+ method: "GET",
607
+ pattern: /^\/forge\/v1\/psyche\/schema-catalog$/,
608
+ upstreamPath: "/api/v1/psyche/schema-catalog",
609
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/schema-catalog", url)
610
+ },
611
+ {
612
+ method: "GET",
613
+ pattern: /^\/forge\/v1\/psyche\/beliefs$/,
614
+ upstreamPath: "/api/v1/psyche/beliefs",
615
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/beliefs", url)
616
+ },
617
+ {
618
+ method: "POST",
619
+ pattern: /^\/forge\/v1\/psyche\/beliefs$/,
620
+ upstreamPath: "/api/v1/psyche/beliefs",
621
+ requestBody: "json",
622
+ requiresToken: true,
623
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/beliefs", url)
624
+ },
625
+ {
626
+ method: "GET",
627
+ pattern: /^\/forge\/v1\/psyche\/beliefs\/([^/]+)$/,
628
+ upstreamPath: "/api/v1/psyche/beliefs/:id",
629
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/beliefs/${encodePathSegment(match[1] ?? "")}`, url)
630
+ },
631
+ {
632
+ method: "PATCH",
633
+ pattern: /^\/forge\/v1\/psyche\/beliefs\/([^/]+)$/,
634
+ upstreamPath: "/api/v1/psyche/beliefs/:id",
635
+ requestBody: "json",
636
+ requiresToken: true,
637
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/beliefs/${encodePathSegment(match[1] ?? "")}`, url)
638
+ },
639
+ {
640
+ method: "DELETE",
641
+ pattern: /^\/forge\/v1\/psyche\/beliefs\/([^/]+)$/,
642
+ upstreamPath: "/api/v1/psyche/beliefs/:id",
643
+ requiresToken: true,
644
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/beliefs/${encodePathSegment(match[1] ?? "")}`, url)
645
+ },
646
+ {
647
+ method: "GET",
648
+ pattern: /^\/forge\/v1\/psyche\/modes$/,
649
+ upstreamPath: "/api/v1/psyche/modes",
650
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/modes", url)
651
+ },
652
+ {
653
+ method: "POST",
654
+ pattern: /^\/forge\/v1\/psyche\/modes$/,
655
+ upstreamPath: "/api/v1/psyche/modes",
656
+ requestBody: "json",
657
+ requiresToken: true,
658
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/modes", url)
659
+ },
660
+ {
661
+ method: "GET",
662
+ pattern: /^\/forge\/v1\/psyche\/modes\/([^/]+)$/,
663
+ upstreamPath: "/api/v1/psyche/modes/:id",
664
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/modes/${encodePathSegment(match[1] ?? "")}`, url)
665
+ },
666
+ {
667
+ method: "PATCH",
668
+ pattern: /^\/forge\/v1\/psyche\/modes\/([^/]+)$/,
669
+ upstreamPath: "/api/v1/psyche/modes/:id",
670
+ requestBody: "json",
671
+ requiresToken: true,
672
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/modes/${encodePathSegment(match[1] ?? "")}`, url)
673
+ },
674
+ {
675
+ method: "DELETE",
676
+ pattern: /^\/forge\/v1\/psyche\/modes\/([^/]+)$/,
677
+ upstreamPath: "/api/v1/psyche/modes/:id",
678
+ requiresToken: true,
679
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/modes/${encodePathSegment(match[1] ?? "")}`, url)
680
+ },
681
+ {
682
+ method: "GET",
683
+ pattern: /^\/forge\/v1\/psyche\/mode-guides$/,
684
+ upstreamPath: "/api/v1/psyche/mode-guides",
685
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/mode-guides", url)
686
+ },
687
+ {
688
+ method: "POST",
689
+ pattern: /^\/forge\/v1\/psyche\/mode-guides$/,
690
+ upstreamPath: "/api/v1/psyche/mode-guides",
691
+ requestBody: "json",
692
+ requiresToken: true,
693
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/mode-guides", url)
694
+ },
695
+ {
696
+ method: "GET",
697
+ pattern: /^\/forge\/v1\/psyche\/mode-guides\/([^/]+)$/,
698
+ upstreamPath: "/api/v1/psyche/mode-guides/:id",
699
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/mode-guides/${encodePathSegment(match[1] ?? "")}`, url)
700
+ },
701
+ {
702
+ method: "PATCH",
703
+ pattern: /^\/forge\/v1\/psyche\/mode-guides\/([^/]+)$/,
704
+ upstreamPath: "/api/v1/psyche/mode-guides/:id",
705
+ requestBody: "json",
706
+ requiresToken: true,
707
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/mode-guides/${encodePathSegment(match[1] ?? "")}`, url)
708
+ },
709
+ {
710
+ method: "DELETE",
711
+ pattern: /^\/forge\/v1\/psyche\/mode-guides\/([^/]+)$/,
712
+ upstreamPath: "/api/v1/psyche/mode-guides/:id",
713
+ requiresToken: true,
714
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/mode-guides/${encodePathSegment(match[1] ?? "")}`, url)
715
+ },
716
+ {
717
+ method: "GET",
718
+ pattern: /^\/forge\/v1\/psyche\/event-types$/,
719
+ upstreamPath: "/api/v1/psyche/event-types",
720
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/event-types", url)
721
+ },
722
+ {
723
+ method: "POST",
724
+ pattern: /^\/forge\/v1\/psyche\/event-types$/,
725
+ upstreamPath: "/api/v1/psyche/event-types",
726
+ requestBody: "json",
727
+ requiresToken: true,
728
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/event-types", url)
729
+ },
730
+ {
731
+ method: "GET",
732
+ pattern: /^\/forge\/v1\/psyche\/event-types\/([^/]+)$/,
733
+ upstreamPath: "/api/v1/psyche/event-types/:id",
734
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/event-types/${encodePathSegment(match[1] ?? "")}`, url)
735
+ },
736
+ {
737
+ method: "PATCH",
738
+ pattern: /^\/forge\/v1\/psyche\/event-types\/([^/]+)$/,
739
+ upstreamPath: "/api/v1/psyche/event-types/:id",
740
+ requestBody: "json",
741
+ requiresToken: true,
742
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/event-types/${encodePathSegment(match[1] ?? "")}`, url)
743
+ },
744
+ {
745
+ method: "DELETE",
746
+ pattern: /^\/forge\/v1\/psyche\/event-types\/([^/]+)$/,
747
+ upstreamPath: "/api/v1/psyche/event-types/:id",
748
+ requiresToken: true,
749
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/event-types/${encodePathSegment(match[1] ?? "")}`, url)
750
+ },
751
+ {
752
+ method: "GET",
753
+ pattern: /^\/forge\/v1\/psyche\/emotions$/,
754
+ upstreamPath: "/api/v1/psyche/emotions",
755
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/emotions", url)
756
+ },
757
+ {
758
+ method: "POST",
759
+ pattern: /^\/forge\/v1\/psyche\/emotions$/,
760
+ upstreamPath: "/api/v1/psyche/emotions",
761
+ requestBody: "json",
762
+ requiresToken: true,
763
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/emotions", url)
764
+ },
765
+ {
766
+ method: "GET",
767
+ pattern: /^\/forge\/v1\/psyche\/emotions\/([^/]+)$/,
768
+ upstreamPath: "/api/v1/psyche/emotions/:id",
769
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/emotions/${encodePathSegment(match[1] ?? "")}`, url)
770
+ },
771
+ {
772
+ method: "PATCH",
773
+ pattern: /^\/forge\/v1\/psyche\/emotions\/([^/]+)$/,
774
+ upstreamPath: "/api/v1/psyche/emotions/:id",
775
+ requestBody: "json",
776
+ requiresToken: true,
777
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/emotions/${encodePathSegment(match[1] ?? "")}`, url)
778
+ },
779
+ {
780
+ method: "DELETE",
781
+ pattern: /^\/forge\/v1\/psyche\/emotions\/([^/]+)$/,
782
+ upstreamPath: "/api/v1/psyche/emotions/:id",
783
+ requiresToken: true,
784
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/emotions/${encodePathSegment(match[1] ?? "")}`, url)
785
+ },
786
+ {
787
+ method: "GET",
788
+ pattern: /^\/forge\/v1\/psyche\/reports$/,
789
+ upstreamPath: "/api/v1/psyche/reports",
790
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/reports", url)
791
+ },
792
+ {
793
+ method: "POST",
794
+ pattern: /^\/forge\/v1\/psyche\/reports$/,
795
+ upstreamPath: "/api/v1/psyche/reports",
796
+ requestBody: "json",
797
+ requiresToken: true,
798
+ target: (_match, url) => passthroughSearch("/api/v1/psyche/reports", url)
799
+ },
800
+ {
801
+ method: "GET",
802
+ pattern: /^\/forge\/v1\/psyche\/reports\/([^/]+)$/,
803
+ upstreamPath: "/api/v1/psyche/reports/:id",
804
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/reports/${encodePathSegment(match[1] ?? "")}`, url)
805
+ },
806
+ {
807
+ method: "PATCH",
808
+ pattern: /^\/forge\/v1\/psyche\/reports\/([^/]+)$/,
809
+ upstreamPath: "/api/v1/psyche/reports/:id",
810
+ requestBody: "json",
811
+ requiresToken: true,
812
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/reports/${encodePathSegment(match[1] ?? "")}`, url)
813
+ },
814
+ {
815
+ method: "DELETE",
816
+ pattern: /^\/forge\/v1\/psyche\/reports\/([^/]+)$/,
817
+ upstreamPath: "/api/v1/psyche/reports/:id",
818
+ requiresToken: true,
819
+ target: (match, url) => passthroughSearch(`/api/v1/psyche/reports/${encodePathSegment(match[1] ?? "")}`, url)
820
+ }
821
+ ]
822
+ },
823
+ {
824
+ path: "/forge/v1/approval-requests",
825
+ match: "prefix",
826
+ operations: [
827
+ {
828
+ method: "GET",
829
+ pattern: /^\/forge\/v1\/approval-requests$/,
830
+ upstreamPath: "/api/v1/approval-requests",
831
+ target: (_match, url) => passthroughSearch("/api/v1/approval-requests", url)
832
+ },
833
+ {
834
+ method: "POST",
835
+ pattern: /^\/forge\/v1\/approval-requests\/([^/]+)\/approve$/,
836
+ upstreamPath: "/api/v1/approval-requests/:id/approve",
837
+ requestBody: "json",
838
+ requiresToken: true,
839
+ target: (match, url) => passthroughSearch(`/api/v1/approval-requests/${encodePathSegment(match[1] ?? "")}/approve`, url)
840
+ },
841
+ {
842
+ method: "POST",
843
+ pattern: /^\/forge\/v1\/approval-requests\/([^/]+)\/reject$/,
844
+ upstreamPath: "/api/v1/approval-requests/:id/reject",
845
+ requestBody: "json",
846
+ requiresToken: true,
847
+ target: (match, url) => passthroughSearch(`/api/v1/approval-requests/${encodePathSegment(match[1] ?? "")}/reject`, url)
848
+ }
849
+ ]
850
+ },
851
+ {
852
+ path: "/forge/v1/agents",
853
+ match: "prefix",
854
+ operations: [
855
+ {
856
+ method: "GET",
857
+ pattern: /^\/forge\/v1\/agents\/onboarding$/,
858
+ upstreamPath: "/api/v1/agents/onboarding",
859
+ target: (_match, url) => passthroughSearch("/api/v1/agents/onboarding", url)
860
+ },
861
+ {
862
+ method: "GET",
863
+ pattern: /^\/forge\/v1\/agents$/,
864
+ upstreamPath: "/api/v1/agents",
865
+ target: (_match, url) => passthroughSearch("/api/v1/agents", url)
866
+ },
867
+ {
868
+ method: "GET",
869
+ pattern: /^\/forge\/v1\/agents\/([^/]+)\/actions$/,
870
+ upstreamPath: "/api/v1/agents/:id/actions",
871
+ target: (match, url) => passthroughSearch(`/api/v1/agents/${encodePathSegment(match[1] ?? "")}/actions`, url)
872
+ }
873
+ ]
874
+ },
875
+ exact("/forge/v1/agent-actions", {
876
+ method: "POST",
877
+ upstreamPath: "/api/v1/agent-actions",
878
+ requestBody: "json",
879
+ requiresToken: true,
880
+ target: (_match, url) => passthroughSearch("/api/v1/agent-actions", url)
881
+ }),
882
+ {
883
+ path: "/forge/v1/rewards",
884
+ match: "prefix",
885
+ operations: [
886
+ {
887
+ method: "GET",
888
+ pattern: /^\/forge\/v1\/rewards\/rules$/,
889
+ upstreamPath: "/api/v1/rewards/rules",
890
+ target: (_match, url) => passthroughSearch("/api/v1/rewards/rules", url)
891
+ },
892
+ {
893
+ method: "GET",
894
+ pattern: /^\/forge\/v1\/rewards\/rules\/([^/]+)$/,
895
+ upstreamPath: "/api/v1/rewards/rules/:id",
896
+ target: (match, url) => passthroughSearch(`/api/v1/rewards/rules/${encodePathSegment(match[1] ?? "")}`, url)
897
+ },
898
+ {
899
+ method: "PATCH",
900
+ pattern: /^\/forge\/v1\/rewards\/rules\/([^/]+)$/,
901
+ upstreamPath: "/api/v1/rewards/rules/:id",
902
+ requestBody: "json",
903
+ requiresToken: true,
904
+ target: (match, url) => passthroughSearch(`/api/v1/rewards/rules/${encodePathSegment(match[1] ?? "")}`, url)
905
+ },
906
+ {
907
+ method: "GET",
908
+ pattern: /^\/forge\/v1\/rewards\/ledger$/,
909
+ upstreamPath: "/api/v1/rewards/ledger",
910
+ target: (_match, url) => passthroughSearch("/api/v1/rewards/ledger", url)
911
+ },
912
+ {
913
+ method: "POST",
914
+ pattern: /^\/forge\/v1\/rewards\/bonus$/,
915
+ upstreamPath: "/api/v1/rewards/bonus",
916
+ requestBody: "json",
917
+ requiresToken: true,
918
+ target: (_match, url) => passthroughSearch("/api/v1/rewards/bonus", url)
919
+ }
920
+ ]
921
+ },
922
+ {
923
+ path: "/forge/v1/events",
924
+ match: "prefix",
925
+ operations: [
926
+ {
927
+ method: "GET",
928
+ pattern: /^\/forge\/v1\/events$/,
929
+ upstreamPath: "/api/v1/events",
930
+ target: (_match, url) => passthroughSearch("/api/v1/events", url)
931
+ },
932
+ {
933
+ method: "GET",
934
+ pattern: /^\/forge\/v1\/events\/meta$/,
935
+ upstreamPath: "/api/v1/events/meta",
936
+ target: (_match, url) => passthroughSearch("/api/v1/events/meta", url)
937
+ }
938
+ ]
939
+ },
940
+ {
941
+ path: "/forge/v1/reviews",
942
+ match: "prefix",
943
+ operations: [
944
+ {
945
+ method: "GET",
946
+ pattern: /^\/forge\/v1\/reviews\/weekly$/,
947
+ upstreamPath: "/api/v1/reviews/weekly",
948
+ target: (_match, url) => passthroughSearch("/api/v1/reviews/weekly", url)
949
+ }
950
+ ]
951
+ },
952
+ {
953
+ path: "/forge/v1/settings",
954
+ match: "exact",
955
+ operations: [
956
+ {
957
+ method: "GET",
958
+ pattern: /^\/forge\/v1\/settings$/,
959
+ upstreamPath: "/api/v1/settings",
960
+ target: (_match, url) => passthroughSearch("/api/v1/settings", url)
961
+ },
962
+ {
963
+ method: "PATCH",
964
+ pattern: /^\/forge\/v1\/settings$/,
965
+ upstreamPath: "/api/v1/settings",
966
+ requestBody: "json",
967
+ requiresToken: true,
968
+ target: (_match, url) => passthroughSearch("/api/v1/settings", url)
969
+ }
970
+ ]
971
+ },
972
+ exact("/forge/v1/settings/bin", {
973
+ method: "GET",
974
+ upstreamPath: "/api/v1/settings/bin",
975
+ target: (_match, url) => passthroughSearch("/api/v1/settings/bin", url)
976
+ }),
977
+ {
978
+ path: "/forge/v1/entities",
979
+ match: "prefix",
980
+ operations: [
981
+ {
982
+ method: "POST",
983
+ pattern: /^\/forge\/v1\/entities\/create$/,
984
+ upstreamPath: "/api/v1/entities/create",
985
+ requestBody: "json",
986
+ requiresToken: true,
987
+ target: (_match, url) => passthroughSearch("/api/v1/entities/create", url)
988
+ },
989
+ {
990
+ method: "POST",
991
+ pattern: /^\/forge\/v1\/entities\/update$/,
992
+ upstreamPath: "/api/v1/entities/update",
993
+ requestBody: "json",
994
+ requiresToken: true,
995
+ target: (_match, url) => passthroughSearch("/api/v1/entities/update", url)
996
+ },
997
+ {
998
+ method: "POST",
999
+ pattern: /^\/forge\/v1\/entities\/delete$/,
1000
+ upstreamPath: "/api/v1/entities/delete",
1001
+ requestBody: "json",
1002
+ requiresToken: true,
1003
+ target: (_match, url) => passthroughSearch("/api/v1/entities/delete", url)
1004
+ },
1005
+ {
1006
+ method: "POST",
1007
+ pattern: /^\/forge\/v1\/entities\/restore$/,
1008
+ upstreamPath: "/api/v1/entities/restore",
1009
+ requestBody: "json",
1010
+ requiresToken: true,
1011
+ target: (_match, url) => passthroughSearch("/api/v1/entities/restore", url)
1012
+ },
1013
+ {
1014
+ method: "POST",
1015
+ pattern: /^\/forge\/v1\/entities\/search$/,
1016
+ upstreamPath: "/api/v1/entities/search",
1017
+ requestBody: "json",
1018
+ requiresToken: true,
1019
+ target: (_match, url) => passthroughSearch("/api/v1/entities/search", url)
1020
+ }
1021
+ ]
1022
+ }
1023
+ ];
1024
+ export function collectMirroredApiRouteKeys() {
1025
+ return new Set(FORGE_PLUGIN_ROUTE_GROUPS.flatMap((group) => group.operations.map((operation) => makeApiRouteKey(operation.method, operation.upstreamPath))));
1026
+ }
1027
+ export function buildRouteParityReport(pathMap) {
1028
+ const mirrored = collectMirroredApiRouteKeys();
1029
+ const excluded = new Set(FORGE_PLUGIN_ROUTE_EXCLUSIONS.map((route) => makeApiRouteKey(route.method, route.path)));
1030
+ const allRelevant = Object.entries(pathMap)
1031
+ .flatMap(([path, methods]) => Object.keys(methods).map((method) => makeApiRouteKey(method, path)))
1032
+ .filter((key) => key.startsWith("GET /api/v1") ||
1033
+ key.startsWith("POST /api/v1") ||
1034
+ key.startsWith("PATCH /api/v1") ||
1035
+ key.startsWith("DELETE /api/v1"));
1036
+ const uncovered = allRelevant.filter((key) => !mirrored.has(key) && !excluded.has(key));
1037
+ return {
1038
+ mirrored: [...mirrored].sort(),
1039
+ excluded: [...excluded].sort(),
1040
+ uncovered: uncovered.sort()
1041
+ };
1042
+ }
1043
+ export function registerForgePluginRoutes(api, config) {
1044
+ for (const group of FORGE_PLUGIN_ROUTE_GROUPS) {
1045
+ api.registerHttpRoute({
1046
+ path: group.path,
1047
+ auth: "plugin",
1048
+ match: group.match,
1049
+ handler: (request, response) => handleGroup(request, response, config, group)
1050
+ });
1051
+ }
1052
+ }
1053
+ function createCliAction(config, path) {
1054
+ return async () => {
1055
+ const result = await callForgeApi({
1056
+ baseUrl: config.baseUrl,
1057
+ apiToken: config.apiToken,
1058
+ actorLabel: config.actorLabel,
1059
+ timeoutMs: config.timeoutMs,
1060
+ method: "GET",
1061
+ path
1062
+ });
1063
+ const data = expectForgeSuccess(result);
1064
+ console.log(JSON.stringify(data, null, 2));
1065
+ };
1066
+ }
1067
+ async function runDoctor(config) {
1068
+ const [health, overview, onboarding, routeParity] = await Promise.all([
1069
+ runReadOnly(config, "/api/v1/health"),
1070
+ runReadOnly(config, "/api/v1/operator/overview"),
1071
+ runReadOnly(config, "/api/v1/agents/onboarding"),
1072
+ runRouteCheck(config)
1073
+ ]);
1074
+ const overviewBody = typeof overview === "object" && overview !== null && "overview" in overview && typeof overview.overview === "object" && overview.overview !== null
1075
+ ? overview.overview
1076
+ : null;
1077
+ const capabilities = overviewBody && typeof overviewBody.capabilities === "object" && overviewBody.capabilities !== null
1078
+ ? overviewBody.capabilities
1079
+ : null;
1080
+ const overviewWarnings = Array.isArray(overviewBody?.warnings) ? overviewBody.warnings.filter((entry) => typeof entry === "string") : [];
1081
+ const uncoveredRoutes = routeParity.uncovered;
1082
+ const warnings = [];
1083
+ const canBootstrap = canBootstrapOperatorSession(config.baseUrl);
1084
+ if (config.apiToken.trim().length === 0 && canBootstrap) {
1085
+ warnings.push("Forge apiToken is blank, but this base URL can bootstrap a local or Tailscale operator session for protected reads and writes.");
1086
+ }
1087
+ else if (config.apiToken.trim().length === 0) {
1088
+ warnings.push("Forge apiToken is missing, and this base URL cannot use local or Tailscale operator-session bootstrap. Protected writes will fail.");
1089
+ }
1090
+ if (overviewWarnings.length > 0) {
1091
+ warnings.push(...overviewWarnings);
1092
+ }
1093
+ if (capabilities && capabilities.canReadPsyche === false) {
1094
+ warnings.push("The configured token cannot read Psyche state. Sensitive reflection routes and summaries will stay partial.");
1095
+ }
1096
+ if (capabilities && capabilities.canManageRewards === false) {
1097
+ warnings.push("The configured token cannot manage rewards. Reward-rule tuning and manual bonus XP are unavailable.");
1098
+ }
1099
+ if (uncoveredRoutes.length > 0) {
1100
+ warnings.push(`Plugin parity is incomplete for ${uncoveredRoutes.length} stable API route${uncoveredRoutes.length === 1 ? "" : "s"}. Run forge route-check.`);
1101
+ }
1102
+ return {
1103
+ ok: (config.apiToken.trim().length > 0 || canBootstrap) && uncoveredRoutes.length === 0,
1104
+ baseUrl: config.baseUrl,
1105
+ actorLabel: config.actorLabel,
1106
+ apiTokenConfigured: config.apiToken.trim().length > 0,
1107
+ operatorSessionBootstrapAvailable: canBootstrap,
1108
+ warnings,
1109
+ health,
1110
+ overview,
1111
+ onboarding,
1112
+ routeParity
1113
+ };
1114
+ }
1115
+ async function runReadOnly(config, path) {
1116
+ return expectForgeSuccess(await callForgeApi({
1117
+ baseUrl: config.baseUrl,
1118
+ apiToken: config.apiToken,
1119
+ actorLabel: config.actorLabel,
1120
+ timeoutMs: config.timeoutMs,
1121
+ method: "GET",
1122
+ path
1123
+ }));
1124
+ }
1125
+ async function runRouteCheck(config) {
1126
+ const openapi = await runReadOnly(config, "/api/v1/openapi.json");
1127
+ const pathMap = typeof openapi === "object" && openapi !== null && "paths" in openapi && typeof openapi.paths === "object" && openapi.paths !== null
1128
+ ? openapi.paths
1129
+ : {};
1130
+ return buildRouteParityReport(pathMap);
1131
+ }
1132
+ export function registerForgePluginCli(api, config) {
1133
+ api.registerCli?.(({ program }) => {
1134
+ const command = program.command("forge").description("Inspect and operate Forge through the OpenClaw plugin");
1135
+ command.command("health").description("Check Forge health").action(createCliAction(config, "/api/v1/health"));
1136
+ command.command("context").description("Fetch the Forge operating context").action(createCliAction(config, "/api/v1/context"));
1137
+ command.command("overview").description("Fetch the one-shot Forge operator overview").action(createCliAction(config, "/api/v1/operator/overview"));
1138
+ command.command("openapi").description("Print the live Forge OpenAPI document").action(createCliAction(config, "/api/v1/openapi.json"));
1139
+ command.command("goals").description("List Forge life goals").action(createCliAction(config, "/api/v1/goals"));
1140
+ command.command("projects").description("List Forge projects").action(createCliAction(config, "/api/v1/projects"));
1141
+ command.command("metrics-xp").description("Inspect Forge XP metrics").action(createCliAction(config, "/api/v1/metrics/xp"));
1142
+ command.command("doctor").description("Run plugin connectivity and onboarding diagnostics").action(async () => {
1143
+ console.log(JSON.stringify(await runDoctor(config), null, 2));
1144
+ });
1145
+ command.command("onboarding").description("Print the Forge agent onboarding contract").action(createCliAction(config, "/api/v1/agents/onboarding"));
1146
+ command.command("comments").description("List all visible Forge comments").action(createCliAction(config, "/api/v1/comments"));
1147
+ command.command("psyche-overview").description("Inspect the Psyche overview read model").action(createCliAction(config, "/api/v1/psyche/overview"));
1148
+ command.command("route-check").description("Compare plugin route coverage against the live Forge OpenAPI paths").action(async () => {
1149
+ console.log(JSON.stringify(await runRouteCheck(config), null, 2));
1150
+ });
1151
+ }, { commands: ["forge"] });
1152
+ }