@actiondock/core 2.0.1 → 2.0.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.
@@ -1,19 +1,124 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { existsSync } from "node:fs";
3
3
  import { resolve } from "node:path";
4
- import { filterByIntent } from "../filter";
4
+ import { runDoctorChecks } from "../doctor/doctor";
5
+ import { filterByIntent, filterWithFallbackInfo } from "../filter";
5
6
  import {
6
7
  findProjectRoot,
7
8
  loadActions,
8
9
  loadPlaybooks,
9
10
  loadProjectConfig,
10
11
  } from "../project/loader";
11
- import { listLinkedPackages, resolveActionProject } from "../registry/registry";
12
+ import {
13
+ getRegistryStatus,
14
+ listLinkedPackages,
15
+ resolveActionProject,
16
+ resolvePackageRoot,
17
+ resolvePlaybookProject,
18
+ } from "../registry/registry";
12
19
  import { ActionRunner } from "../runtime/runner";
20
+ import type { RuntimeStorage } from "../storage/types";
21
+ import { createServer as createNodeHttpServer, type IncomingMessage, type ServerResponse } from "node:http";
22
+ import { Readable } from "node:stream";
23
+ import { pipeline } from "node:stream/promises";
13
24
  import { InvalidJsonError, readJsonBody, RequestTooLargeError } from "./body";
14
25
  import { ServerRuntimeRegistry } from "./runtime-registry";
15
26
  import { isLoopbackHost, resolveCorsHeaders, verifyBearerToken } from "./security";
16
- import type { ActionDockServerInstance, ServerOptions } from "./types";
27
+ import type { ActionDockServerInstance, CoreHttpServerFactory, CoreHttpServerInstance, ServerOptions } from "./types";
28
+
29
+ let customHttpServerFactory: CoreHttpServerFactory | undefined;
30
+
31
+ /**
32
+ * 注册自定义 HTTP 服务端工厂(用于 Node.js / Bun 运行时环境适配)。
33
+ */
34
+ export function setHttpServerFactory(factory: CoreHttpServerFactory): void {
35
+ customHttpServerFactory = factory;
36
+ }
37
+
38
+ /**
39
+ * 根据当前运行时环境启动标准 Web Request/Response 兼容的 HTTP 服务。
40
+ */
41
+ export function launchHttpServer(
42
+ port: number,
43
+ host: string,
44
+ fetchHandler: (req: Request) => Promise<Response>
45
+ ): CoreHttpServerInstance {
46
+ if (customHttpServerFactory) {
47
+ return customHttpServerFactory({ port, host, fetch: fetchHandler }) as CoreHttpServerInstance;
48
+ }
49
+
50
+ // 若处于原生 Bun 运行时
51
+ if (typeof (globalThis as any).Bun !== "undefined" && typeof (globalThis as any).Bun.serve === "function") {
52
+ const bunServer = (globalThis as any).Bun.serve({
53
+ port,
54
+ hostname: host,
55
+ fetch: fetchHandler,
56
+ });
57
+ return {
58
+ port: bunServer.port,
59
+ stop: (closeActive?: boolean) => bunServer.stop(closeActive),
60
+ };
61
+ }
62
+
63
+ // Node.js 原生 node:http 兜底实现
64
+ const srv = createNodeHttpServer(async (req: IncomingMessage, res: ServerResponse) => {
65
+ try {
66
+ const protocol = (req.socket as any)?.encrypted ? "https" : "http";
67
+ const hostHeader = req.headers.host || "127.0.0.1";
68
+ const url = new URL(req.url || "/", `${protocol}://${hostHeader}`).href;
69
+
70
+ const headers = new Headers();
71
+ for (const [k, v] of Object.entries(req.headers)) {
72
+ if (v === undefined) continue;
73
+ if (Array.isArray(v)) {
74
+ for (const item of v) headers.append(k, item);
75
+ } else {
76
+ headers.set(k, v);
77
+ }
78
+ }
79
+
80
+ const method = (req.method || "GET").toUpperCase();
81
+ const hasBody = method !== "GET" && method !== "HEAD";
82
+ const init: RequestInit = { method, headers };
83
+ if (hasBody) {
84
+ (init as any).body = Readable.toWeb(req);
85
+ (init as any).duplex = "half";
86
+ }
87
+
88
+ const webReq = new Request(url, init);
89
+ const webRes = await fetchHandler(webReq);
90
+
91
+ res.statusCode = webRes.status;
92
+ if (webRes.statusText) res.statusMessage = webRes.statusText;
93
+ webRes.headers.forEach((v, k) => res.setHeader(k, v));
94
+
95
+ if (!webRes.body) {
96
+ res.end();
97
+ return;
98
+ }
99
+ await pipeline(Readable.fromWeb(webRes.body as any), res);
100
+ } catch (err: any) {
101
+ if (!res.headersSent) {
102
+ res.statusCode = 500;
103
+ res.end(JSON.stringify({ error: err?.message || String(err) }));
104
+ } else {
105
+ res.destroy(err);
106
+ }
107
+ }
108
+ });
109
+
110
+ srv.listen(port, host);
111
+ const addr = srv.address();
112
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
113
+
114
+ return {
115
+ port: actualPort,
116
+ stop: () => {
117
+ srv.close();
118
+ (srv as any).closeAllConnections?.();
119
+ },
120
+ };
121
+ }
17
122
 
18
123
  /**
19
124
  * 辅助函数:快速构造带 CORS 头的 JSON HTTP 响应。
@@ -70,20 +175,83 @@ function findRunAcrossStorages(
70
175
  return null;
71
176
  }
72
177
 
178
+ /**
179
+ * 辅助函数:依据 package 参数或项目上下文解析目标 Storage 实例与根目录。
180
+ */
181
+ function resolveStorageForPackage(
182
+ packageIdOrPath: string | undefined,
183
+ runtimeRegistry: ServerRuntimeRegistry,
184
+ projectRoot?: string | null,
185
+ customHome?: string
186
+ ): { packageId: string; storage: RuntimeStorage; projectRoot?: string } {
187
+ if (packageIdOrPath) {
188
+ const root = resolvePackageRoot(packageIdOrPath, customHome);
189
+ if (root) {
190
+ const config = loadProjectConfig(root);
191
+ return {
192
+ packageId: config.id,
193
+ storage: runtimeRegistry.getStorage(config.id, root),
194
+ projectRoot: root,
195
+ };
196
+ }
197
+ return {
198
+ packageId: packageIdOrPath,
199
+ storage: runtimeRegistry.getStorage(packageIdOrPath),
200
+ };
201
+ }
202
+
203
+ if (projectRoot) {
204
+ const config = loadProjectConfig(projectRoot);
205
+ return {
206
+ packageId: config.id,
207
+ storage: runtimeRegistry.getStorage(config.id, projectRoot),
208
+ projectRoot,
209
+ };
210
+ }
211
+
212
+ const linked = listLinkedPackages(customHome);
213
+ if (linked.length > 0) {
214
+ const first = linked[0];
215
+ return {
216
+ packageId: first.id,
217
+ storage: runtimeRegistry.getStorage(first.id, first.path),
218
+ projectRoot: first.path,
219
+ };
220
+ }
221
+
222
+ return {
223
+ packageId: "default",
224
+ storage: runtimeRegistry.getStorage("default"),
225
+ };
226
+ }
227
+
73
228
  /**
74
229
  * 启动 ActionDock 2.0 原生轻量级 HTTP Runner 服务端。
75
230
  *
76
- * 提供 RESTful API 供跨机器或远端 AI Agent 执行 Action:
77
- * - `GET /api/v1/health` : 健康检查与就绪状态
78
- * - `GET /api/v1/info` : 项目元数据、Action Playbook 概览
79
- * - `GET /api/v1/actions`: 列出可用 Actions(支持 ?intent= 过滤)
80
- * - `GET /api/v1/actions/:id`: 查看单个 Action Schema 详情
81
- * - `POST /api/v1/actions/:id/run`: 同步或异步(202 Accepted)执行 Action
82
- * - `GET /api/v1/runs/:runId`: 查询历史或异步任务运行状态
83
- * - `POST /api/v1/runs/:runId/cancel`: 中断在途任务
84
- *
85
- * @param options 服务端启动配置参数
86
- * @returns ActionDockServerInstance 服务端实例句柄
231
+ * 全面暴露 RESTful 调度与运维接口:
232
+ * - GET /api/v1/health : 健康检查与就绪状态
233
+ * - GET /api/v1/info : 统一自省探索(支持 ?intent= 智能决议、?package= 详情下钻、?tree=true 工作区拓扑)
234
+ * - GET /api/v1/doctor : 深度环境与依赖诊断
235
+ * - GET /api/v1/actions : 列出可用 Actions(支持 ?intent= ?package=)
236
+ * - GET /api/v1/actions/:id : 查看单个 Action 的 Schema 详情
237
+ * - POST /api/v1/actions/:id/run : 同步或异步执行 Action
238
+ * - GET /api/v1/playbooks : 列出可用 Playbook 工作流
239
+ * - GET /api/v1/playbooks/:id : 查看 Playbook 步骤流程与 SOP 规程正文
240
+ * - GET /api/v1/runs : 多维检索历史运行记录
241
+ * - POST /api/v1/runs/clear : 批量清理历史运行记录
242
+ * - GET /api/v1/runs/:runId : 查询历史或异步任务运行状态
243
+ * - POST /api/v1/runs/:runId/cancel : 中断在途任务
244
+ * - GET /api/v1/runs/:runId/stream : 基于 SSE 实时长任务日志与进度流推送
245
+ * - GET /api/v1/state : 检索状态键名列表
246
+ * - GET /api/v1/state/:key : 获取指定状态值
247
+ * - PUT /api/v1/state/:key : 写入持久化状态(支持 TTL)
248
+ * - DELETE /api/v1/state/:key : 删除指定状态
249
+ * - POST /api/v1/state/clear : 清空状态数据
250
+ * - GET /api/v1/config : 获取配置项规格与当前值(敏感凭证脱敏)
251
+ * - PUT /api/v1/config : 持久化更新配置
252
+ * - DELETE /api/v1/config/:key : 删除配置项
253
+ * - GET /api/v1/config/env : 检查环境变量满足率
254
+ * - ALL /mcp : 一体化 MCP Streamable HTTP 协议端点(可选)
87
255
  */
88
256
  export function startActionDockServer(
89
257
  options: ServerOptions = {}
@@ -105,11 +273,8 @@ export function startActionDockServer(
105
273
 
106
274
  const runtimeRegistry = new ServerRuntimeRegistry();
107
275
 
108
- const server = Bun.serve({
109
- port,
110
- hostname: host,
111
- async fetch(req) {
112
- const origin = req.headers.get("origin");
276
+ const server = launchHttpServer(port, host, async (req) => {
277
+ const origin = req.headers.get("origin");
113
278
  const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
114
279
 
115
280
  if (req.method === "OPTIONS") {
@@ -122,7 +287,7 @@ export function startActionDockServer(
122
287
  const url = new URL(req.url);
123
288
  const pathname = url.pathname;
124
289
 
125
- // 1. Health Check (supports /api/v1/health and /health)
290
+ // 0. Health Check (supports /api/v1/health and /health)
126
291
  if (pathname === "/api/v1/health" || pathname === "/health") {
127
292
  if (!verifyBearerToken(req, token)) {
128
293
  return jsonResponse(
@@ -149,7 +314,30 @@ export function startActionDockServer(
149
314
  return jsonResponse(healthData, 200, corsHeaders);
150
315
  }
151
316
 
152
- // Check authentication for remaining endpoints
317
+ // 1. MCP Unified Gateway: /mcp
318
+ if (
319
+ options.enableMcp !== false &&
320
+ options.mcpHandler &&
321
+ (pathname === "/mcp" || pathname.startsWith("/mcp/"))
322
+ ) {
323
+ if (!verifyBearerToken(req, token)) {
324
+ return jsonResponse(
325
+ {
326
+ ok: false,
327
+ error: {
328
+ code: "UNAUTHORIZED",
329
+ message: "Invalid or missing Bearer token",
330
+ },
331
+ },
332
+ 401,
333
+ corsHeaders
334
+ );
335
+ }
336
+ const mcpRes = await options.mcpHandler(req);
337
+ if (mcpRes) return mcpRes;
338
+ }
339
+
340
+ // 鉴权检查(后续所有 /api/v1/* 均要求鉴权)
153
341
  if (!verifyBearerToken(req, token)) {
154
342
  return jsonResponse(
155
343
  {
@@ -164,40 +352,202 @@ export function startActionDockServer(
164
352
  );
165
353
  }
166
354
 
167
- // 2. Info: GET /api/v1/info
355
+ // 2. Info 自省: GET /api/v1/info
168
356
  if (pathname === "/api/v1/info" && req.method === "GET") {
169
357
  try {
358
+ const isTree = url.searchParams.get("tree") === "true";
359
+ if (isTree) {
360
+ const status = getRegistryStatus(customHome);
361
+ return jsonResponse(
362
+ { ok: true, type: "tree", ...status },
363
+ 200,
364
+ corsHeaders
365
+ );
366
+ }
367
+
368
+ const intent = url.searchParams.get("intent") || undefined;
369
+ const targetPkg = url.searchParams.get("package") || undefined;
370
+
371
+ // 收集所有候选包全量元数据
372
+ const aggregatedPackages: any[] = [];
373
+
170
374
  if (projectRoot) {
171
- const config = loadProjectConfig(projectRoot);
172
- const actions = await loadActions(projectRoot, config.actionsDir);
173
- const playbooks = loadPlaybooks(projectRoot, config.playbooksDir);
174
- const infoData: Record<string, unknown> = {
175
- ok: true,
176
- id: config.id,
177
- name: config.name,
178
- version: config.version,
179
- description: config.description,
180
- actionsCount: actions.size,
181
- playbooksCount: playbooks.size,
182
- actions: Array.from(actions.keys()),
183
- playbooks: Array.from(playbooks.keys()),
184
- };
185
- if (options.exposeDebugInfo) {
186
- infoData.projectRoot = projectRoot;
375
+ try {
376
+ const config = loadProjectConfig(projectRoot);
377
+ const actions = await loadActions(projectRoot, config.actionsDir);
378
+ const playbooks = loadPlaybooks(projectRoot, config.playbooksDir);
379
+ aggregatedPackages.push({
380
+ id: config.id,
381
+ name: config.name,
382
+ version: config.version,
383
+ description: config.description || "",
384
+ path: projectRoot,
385
+ actionsCount: actions.size,
386
+ playbooksCount: playbooks.size,
387
+ actions: Array.from(actions.entries()).map(([id, a]) => ({
388
+ id,
389
+ description: a.description || "",
390
+ inputSchema: a.inputSchema || null,
391
+ outputSchema: a.outputSchema || null,
392
+ })),
393
+ playbooks: Array.from(playbooks.entries()).map(([id, p]) => ({
394
+ id,
395
+ description: p.description || "",
396
+ actions: p.actions || [],
397
+ })),
398
+ configDeclared: config.config || {},
399
+ });
400
+ } catch {
401
+ // 忽略解析异常
187
402
  }
188
- return jsonResponse(infoData, 200, corsHeaders);
189
403
  } else {
190
404
  const linked = listLinkedPackages(customHome);
405
+ for (const pkg of linked) {
406
+ if (!existsSync(pkg.path)) continue;
407
+ try {
408
+ const config = loadProjectConfig(pkg.path);
409
+ const actions = await loadActions(pkg.path, config.actionsDir);
410
+ const playbooks = loadPlaybooks(pkg.path, config.playbooksDir);
411
+ aggregatedPackages.push({
412
+ id: config.id,
413
+ name: config.name,
414
+ version: config.version,
415
+ description: config.description || "",
416
+ path: pkg.path,
417
+ actionsCount: actions.size,
418
+ playbooksCount: playbooks.size,
419
+ actions: Array.from(actions.entries()).map(([id, a]) => ({
420
+ id,
421
+ description: a.description || "",
422
+ inputSchema: a.inputSchema || null,
423
+ outputSchema: a.outputSchema || null,
424
+ })),
425
+ playbooks: Array.from(playbooks.entries()).map(([id, p]) => ({
426
+ id,
427
+ description: p.description || "",
428
+ actions: p.actions || [],
429
+ })),
430
+ configDeclared: config.config || {},
431
+ });
432
+ } catch {
433
+ // 忽略故障包
434
+ }
435
+ }
436
+ }
437
+
438
+ // 显式指定 package 详情下钻
439
+ if (targetPkg) {
440
+ const matched = aggregatedPackages.find(
441
+ (p) => p.id === targetPkg || p.path === targetPkg
442
+ );
443
+ if (!matched) {
444
+ return jsonResponse(
445
+ {
446
+ ok: false,
447
+ error: {
448
+ code: "PACKAGE_NOT_FOUND",
449
+ message: `Package '${targetPkg}' not found on remote server`,
450
+ },
451
+ },
452
+ 404,
453
+ corsHeaders
454
+ );
455
+ }
456
+ return jsonResponse(
457
+ {
458
+ ok: true,
459
+ type: "package_detail",
460
+ ...matched,
461
+ ...(options.exposeDebugInfo ? { projectRoot: matched.path } : {}),
462
+ },
463
+ 200,
464
+ corsHeaders
465
+ );
466
+ }
467
+
468
+ // 意图过滤与决议
469
+ if (intent) {
470
+ const filterRes = filterWithFallbackInfo(
471
+ aggregatedPackages,
472
+ intent,
473
+ [
474
+ (p) => p.id,
475
+ (p) => p.name,
476
+ (p) => p.description,
477
+ (p) => p.actions.map((a: any) => a.id),
478
+ (p) => p.actions.map((a: any) => a.description),
479
+ (p) => p.playbooks.map((pb: any) => pb.id),
480
+ (p) => p.playbooks.map((pb: any) => pb.description),
481
+ ],
482
+ true
483
+ );
484
+
485
+ // 唯一命中智能展开详情
486
+ if (filterRes.matchedCount === 1) {
487
+ const single = filterRes.items[0];
488
+ return jsonResponse(
489
+ {
490
+ ok: true,
491
+ type: "package_detail",
492
+ isSingleMatch: true,
493
+ ...single,
494
+ ...(options.exposeDebugInfo ? { projectRoot: single.path } : {}),
495
+ },
496
+ 200,
497
+ corsHeaders
498
+ );
499
+ }
500
+
191
501
  return jsonResponse(
192
502
  {
193
503
  ok: true,
194
- version: "2.0.0",
195
- linkedPackages: linked,
504
+ type: "package_list",
505
+ isFallback: filterRes.isFallback,
506
+ packages: filterRes.items,
196
507
  },
197
508
  200,
198
509
  corsHeaders
199
510
  );
200
511
  }
512
+
513
+ // 单包模式直接展开详情
514
+ if (projectRoot && aggregatedPackages.length === 1) {
515
+ const p = aggregatedPackages[0];
516
+ return jsonResponse(
517
+ {
518
+ ok: true,
519
+ type: "package_detail",
520
+ id: p.id,
521
+ name: p.name,
522
+ version: p.version,
523
+ description: p.description,
524
+ actionsCount: p.actionsCount,
525
+ playbooksCount: p.playbooksCount,
526
+ actions: p.actions.map((a: any) => a.id),
527
+ actionsDetail: p.actions,
528
+ playbooks: p.playbooks.map((pb: any) => pb.id),
529
+ playbooksDetail: p.playbooks,
530
+ configDeclared: p.configDeclared,
531
+ linkedPackages: listLinkedPackages(customHome),
532
+ ...(options.exposeDebugInfo ? { projectRoot } : {}),
533
+ },
534
+ 200,
535
+ corsHeaders
536
+ );
537
+ }
538
+
539
+ // 全局注册表概览
540
+ return jsonResponse(
541
+ {
542
+ ok: true,
543
+ type: "package_list",
544
+ version: "2.0.0",
545
+ packages: aggregatedPackages,
546
+ linkedPackages: listLinkedPackages(customHome),
547
+ },
548
+ 200,
549
+ corsHeaders
550
+ );
201
551
  } catch (err: any) {
202
552
  return jsonResponse(
203
553
  {
@@ -210,7 +560,26 @@ export function startActionDockServer(
210
560
  }
211
561
  }
212
562
 
213
- // 3. Actions List: GET /api/v1/actions
563
+ // 3. Doctor 深度体检: GET /api/v1/doctor
564
+ if (pathname === "/api/v1/doctor" && req.method === "GET") {
565
+ try {
566
+ const targetPkg = url.searchParams.get("package") || undefined;
567
+ const report = await runDoctorChecks({
568
+ cwd: projectRoot || process.cwd(),
569
+ packageIdOrPath: targetPkg,
570
+ customHome,
571
+ });
572
+ return jsonResponse({ ok: true, report }, 200, corsHeaders);
573
+ } catch (err: any) {
574
+ return jsonResponse(
575
+ { ok: false, error: { code: "DOCTOR_ERROR", message: err.message } },
576
+ 500,
577
+ corsHeaders
578
+ );
579
+ }
580
+ }
581
+
582
+ // 4. Actions List: GET /api/v1/actions
214
583
  if (pathname === "/api/v1/actions" && req.method === "GET") {
215
584
  try {
216
585
  const actionList: Array<{
@@ -251,15 +620,21 @@ export function startActionDockServer(
251
620
  }
252
621
 
253
622
  const intent = url.searchParams.get("intent");
254
- const filtered = intent
255
- ? filterByIntent(
256
- actionList,
257
- intent,
258
- [(a) => a.id, (a) => a.description, (a) => a.packageId],
259
- false
260
- )
623
+ const targetPkg = url.searchParams.get("package");
624
+
625
+ let filtered = targetPkg
626
+ ? actionList.filter((a) => a.packageId === targetPkg)
261
627
  : actionList;
262
628
 
629
+ if (intent) {
630
+ filtered = filterByIntent(
631
+ filtered,
632
+ intent,
633
+ [(a) => a.id, (a) => a.description, (a) => a.packageId],
634
+ false
635
+ );
636
+ }
637
+
263
638
  return jsonResponse(filtered, 200, corsHeaders);
264
639
  } catch (err: any) {
265
640
  return jsonResponse(
@@ -273,7 +648,7 @@ export function startActionDockServer(
273
648
  }
274
649
  }
275
650
 
276
- // 4. Action Show: GET /api/v1/actions/:id
651
+ // 5. Action Show: GET /api/v1/actions/:id
277
652
  const actionShowMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)$/);
278
653
  if (actionShowMatch && req.method === "GET") {
279
654
  const actionId = decodeURIComponent(actionShowMatch[1]);
@@ -323,7 +698,7 @@ export function startActionDockServer(
323
698
  }
324
699
  }
325
700
 
326
- // 5. Action Run: POST /api/v1/actions/:id/run
701
+ // 6. Action Run: POST /api/v1/actions/:id/run
327
702
  const actionRunMatch = pathname.match(/^\/api\/v1\/actions\/([^/]+)\/run$/);
328
703
  if (actionRunMatch && req.method === "POST") {
329
704
  const actionId = decodeURIComponent(actionRunMatch[1]);
@@ -336,10 +711,7 @@ export function startActionDockServer(
336
711
  {
337
712
  ok: false,
338
713
  runId: randomUUID(),
339
- error: {
340
- code: "REQUEST_TOO_LARGE",
341
- message: err.message,
342
- },
714
+ error: { code: "REQUEST_TOO_LARGE", message: err.message },
343
715
  },
344
716
  413,
345
717
  corsHeaders
@@ -350,10 +722,7 @@ export function startActionDockServer(
350
722
  {
351
723
  ok: false,
352
724
  runId: randomUUID(),
353
- error: {
354
- code: "INVALID_JSON",
355
- message: err.message,
356
- },
725
+ error: { code: "INVALID_JSON", message: err.message },
357
726
  },
358
727
  400,
359
728
  corsHeaders
@@ -363,10 +732,7 @@ export function startActionDockServer(
363
732
  {
364
733
  ok: false,
365
734
  runId: randomUUID(),
366
- error: {
367
- code: "INVALID_JSON",
368
- message: `Failed to parse request body: ${err.message}`,
369
- },
735
+ error: { code: "INVALID_JSON", message: `Failed to parse request body: ${err.message}` },
370
736
  },
371
737
  400,
372
738
  corsHeaders
@@ -391,30 +757,49 @@ export function startActionDockServer(
391
757
  actions,
392
758
  });
393
759
 
394
- const executionMode = body?.execution?.mode || "sync";
760
+ const isAsync = body?.execution?.mode === "async" || body?.async === true;
395
761
  const timeoutMs =
396
762
  typeof body?.execution?.timeoutMs === "number" && body.execution.timeoutMs > 0
397
763
  ? body.execution.timeoutMs
398
764
  : undefined;
399
765
 
400
- if (executionMode === "async") {
766
+ if (isAsync) {
401
767
  const handle = runner.start(resolved.actionId, body?.input || {}, {
402
768
  timeoutMs,
403
769
  });
404
770
  runtimeRegistry.executionManager.register(handle);
405
771
 
772
+ // 监听结算以广播 SSE 完成事件
773
+ handle.result
774
+ .then((res) => {
775
+ runtimeRegistry.emit(handle.runId, { type: "finish", data: res });
776
+ })
777
+ .catch((err) => {
778
+ runtimeRegistry.emit(handle.runId, {
779
+ type: "finish",
780
+ data: {
781
+ ok: false,
782
+ error: {
783
+ code: "ACTION_EXECUTION_ERROR",
784
+ message: err?.message || String(err),
785
+ },
786
+ },
787
+ });
788
+ });
789
+
406
790
  return jsonResponse(
407
791
  {
408
792
  ok: true,
409
793
  runId: handle.runId,
410
794
  status: "running",
795
+ streamUrl: `/api/v1/runs/${handle.runId}/stream`,
411
796
  },
412
797
  202,
413
798
  corsHeaders
414
799
  );
415
800
  }
416
801
 
417
- // Sync execution mode
802
+ // 同步执行模式
418
803
  const handle = runner.start(resolved.actionId, body?.input || {}, {
419
804
  signal: req.signal,
420
805
  timeoutMs,
@@ -439,7 +824,289 @@ export function startActionDockServer(
439
824
  }
440
825
  }
441
826
 
442
- // 6. Run Show: GET /api/v1/runs/:runId
827
+ // 7. Playbooks List: GET /api/v1/playbooks
828
+ if (pathname === "/api/v1/playbooks" && req.method === "GET") {
829
+ try {
830
+ const pbList: Array<{
831
+ id: string;
832
+ description: string;
833
+ actions: string[];
834
+ packageId: string;
835
+ filePath: string;
836
+ }> = [];
837
+
838
+ const targetPkg = url.searchParams.get("package");
839
+ const intent = url.searchParams.get("intent");
840
+
841
+ const roots: Array<{ root: string; packageId: string }> = [];
842
+ if (projectRoot) {
843
+ const cfg = loadProjectConfig(projectRoot);
844
+ roots.push({ root: projectRoot, packageId: cfg.id });
845
+ }
846
+ const linked = listLinkedPackages(customHome);
847
+ for (const pkg of linked) {
848
+ if (projectRoot && pkg.path === projectRoot) continue;
849
+ if (!existsSync(pkg.path)) continue;
850
+ roots.push({ root: pkg.path, packageId: pkg.id });
851
+ }
852
+
853
+ for (const item of roots) {
854
+ if (targetPkg && item.packageId !== targetPkg && item.root !== targetPkg) continue;
855
+ try {
856
+ const cfg = loadProjectConfig(item.root);
857
+ const pbs = loadPlaybooks(item.root, cfg.playbooksDir);
858
+ for (const [id, pb] of pbs.entries()) {
859
+ pbList.push({
860
+ id,
861
+ description: pb.description || "",
862
+ actions: pb.actions || [],
863
+ packageId: item.packageId,
864
+ filePath: pb.filePath,
865
+ });
866
+ }
867
+ } catch {}
868
+ }
869
+
870
+ const filtered = intent
871
+ ? filterByIntent(
872
+ pbList,
873
+ intent,
874
+ [(p) => p.id, (p) => p.description, (p) => p.packageId, (p) => p.actions],
875
+ false
876
+ )
877
+ : pbList;
878
+
879
+ return jsonResponse(filtered, 200, corsHeaders);
880
+ } catch (err: any) {
881
+ return jsonResponse(
882
+ { ok: false, error: { code: "PLAYBOOKS_LIST_ERROR", message: err.message } },
883
+ 500,
884
+ corsHeaders
885
+ );
886
+ }
887
+ }
888
+
889
+ // 8. Playbook Show: GET /api/v1/playbooks/:id
890
+ const pbShowMatch = pathname.match(/^\/api\/v1\/playbooks\/([^/]+)$/);
891
+ if (pbShowMatch && req.method === "GET") {
892
+ const pbId = decodeURIComponent(pbShowMatch[1]);
893
+ try {
894
+ const resolved = resolvePlaybookProject(
895
+ pbId,
896
+ projectRoot || process.cwd(),
897
+ customHome
898
+ );
899
+ const pb = resolved.playbook;
900
+ return jsonResponse(
901
+ {
902
+ ok: true,
903
+ id: pb.id,
904
+ packageId: resolved.packageId,
905
+ description: pb.description || "",
906
+ actions: pb.actions || [],
907
+ filePath: pb.filePath,
908
+ content: pb.content,
909
+ },
910
+ 200,
911
+ corsHeaders
912
+ );
913
+ } catch (err: any) {
914
+ return jsonResponse(
915
+ { ok: false, error: { code: "PLAYBOOK_NOT_FOUND", message: err.message } },
916
+ 404,
917
+ corsHeaders
918
+ );
919
+ }
920
+ }
921
+
922
+ // 9. Runs List: GET /api/v1/runs
923
+ if (pathname === "/api/v1/runs" && req.method === "GET") {
924
+ try {
925
+ const status = url.searchParams.get("status") || undefined;
926
+ const actionId = url.searchParams.get("actionId") || undefined;
927
+ const packageId = url.searchParams.get("packageId") || undefined;
928
+ const intent = url.searchParams.get("intent") || undefined;
929
+ const limit = parseInt(url.searchParams.get("limit") || "50", 10);
930
+
931
+ const allRuns: any[] = [];
932
+ const seenRunIds = new Set<string>();
933
+
934
+ const candidateStorages: Array<{ packageId: string; storage: RuntimeStorage }> = [];
935
+ if (projectRoot) {
936
+ try {
937
+ const cfg = loadProjectConfig(projectRoot);
938
+ candidateStorages.push({
939
+ packageId: cfg.id,
940
+ storage: runtimeRegistry.getStorage(cfg.id, projectRoot),
941
+ });
942
+ } catch {}
943
+ }
944
+ const linked = listLinkedPackages(customHome);
945
+ for (const pkg of linked) {
946
+ if (projectRoot && pkg.path === projectRoot) continue;
947
+ if (!existsSync(pkg.path)) continue;
948
+ candidateStorages.push({
949
+ packageId: pkg.id,
950
+ storage: runtimeRegistry.getStorage(pkg.id, pkg.path),
951
+ });
952
+ }
953
+
954
+ for (const item of candidateStorages) {
955
+ if (packageId && item.packageId !== packageId) continue;
956
+ try {
957
+ const records = item.storage.listRuns({ actionId, limit });
958
+ for (const r of records) {
959
+ if (!seenRunIds.has(r.id)) {
960
+ seenRunIds.add(r.id);
961
+ if (status && r.status !== status) continue;
962
+ allRuns.push(r);
963
+ }
964
+ }
965
+ } catch {}
966
+ }
967
+
968
+ allRuns.sort((a, b) => (b.startedAt || "").localeCompare(a.startedAt || ""));
969
+
970
+ const filtered = intent
971
+ ? filterByIntent(
972
+ allRuns,
973
+ intent,
974
+ [(r) => r.id, (r) => r.actionId, (r) => r.status, (r) => r.packageId],
975
+ false
976
+ )
977
+ : allRuns;
978
+
979
+ const sliced = filtered.slice(0, limit);
980
+ return jsonResponse(
981
+ { ok: true, total: filtered.length, items: sliced },
982
+ 200,
983
+ corsHeaders
984
+ );
985
+ } catch (err: any) {
986
+ return jsonResponse(
987
+ { ok: false, error: { code: "RUNS_LIST_ERROR", message: err.message } },
988
+ 500,
989
+ corsHeaders
990
+ );
991
+ }
992
+ }
993
+
994
+ // 10. Runs Clear: POST /api/v1/runs/clear or DELETE /api/v1/runs
995
+ if (
996
+ (pathname === "/api/v1/runs/clear" && req.method === "POST") ||
997
+ (pathname === "/api/v1/runs" && req.method === "DELETE")
998
+ ) {
999
+ try {
1000
+ let body: any = {};
1001
+ if (req.method === "POST" || req.headers.get("content-type")?.includes("json")) {
1002
+ body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
1003
+ }
1004
+ const packageId = url.searchParams.get("packageId") || body.packageId || undefined;
1005
+ const actionId = url.searchParams.get("actionId") || body.actionId || undefined;
1006
+ const status = url.searchParams.get("status") || body.status || undefined;
1007
+
1008
+ let clearedCount = 0;
1009
+ const candidateStorages: RuntimeStorage[] = [];
1010
+ if (projectRoot) {
1011
+ try {
1012
+ const cfg = loadProjectConfig(projectRoot);
1013
+ candidateStorages.push(runtimeRegistry.getStorage(cfg.id, projectRoot));
1014
+ } catch {}
1015
+ }
1016
+ const linked = listLinkedPackages(customHome);
1017
+ for (const pkg of linked) {
1018
+ if (projectRoot && pkg.path === projectRoot) continue;
1019
+ if (!existsSync(pkg.path)) continue;
1020
+ if (packageId && pkg.id !== packageId) continue;
1021
+ candidateStorages.push(runtimeRegistry.getStorage(pkg.id, pkg.path));
1022
+ }
1023
+
1024
+ for (const storage of candidateStorages) {
1025
+ clearedCount += storage.clearRuns({ actionId, status });
1026
+ }
1027
+
1028
+ return jsonResponse({ ok: true, clearedCount }, 200, corsHeaders);
1029
+ } catch (err: any) {
1030
+ return jsonResponse(
1031
+ { ok: false, error: { code: "RUNS_CLEAR_ERROR", message: err.message } },
1032
+ 500,
1033
+ corsHeaders
1034
+ );
1035
+ }
1036
+ }
1037
+
1038
+ // 11. Run Stream (SSE): GET /api/v1/runs/:runId/stream
1039
+ const runStreamMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/stream$/);
1040
+ if (runStreamMatch && req.method === "GET") {
1041
+ const runId = decodeURIComponent(runStreamMatch[1]);
1042
+ const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
1043
+ const activeHandle = runtimeRegistry.executionManager.get(runId);
1044
+
1045
+ if (!found && !activeHandle) {
1046
+ return jsonResponse(
1047
+ { ok: false, error: { code: "RUN_NOT_FOUND", message: `Run '${runId}' not found` } },
1048
+ 404,
1049
+ corsHeaders
1050
+ );
1051
+ }
1052
+
1053
+ const stream = new ReadableStream({
1054
+ start(controller) {
1055
+ const encoder = new TextEncoder();
1056
+ const sendEvent = (event: string, data: any) => {
1057
+ try {
1058
+ controller.enqueue(
1059
+ encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`)
1060
+ );
1061
+ } catch {}
1062
+ };
1063
+
1064
+ if (activeHandle) {
1065
+ sendEvent("status", { runId, status: "running" });
1066
+ const unsubscribe = runtimeRegistry.subscribe(runId, (evt) => {
1067
+ sendEvent(evt.type, evt.data);
1068
+ if (evt.type === "finish") {
1069
+ try { controller.close(); } catch {}
1070
+ }
1071
+ });
1072
+
1073
+ activeHandle.result.then(
1074
+ (res) => {
1075
+ sendEvent("finish", res);
1076
+ try { controller.close(); } catch {}
1077
+ },
1078
+ (err) => {
1079
+ sendEvent("finish", {
1080
+ ok: false,
1081
+ error: { message: err?.message || String(err) },
1082
+ });
1083
+ try { controller.close(); } catch {}
1084
+ }
1085
+ );
1086
+
1087
+ req.signal.addEventListener("abort", () => {
1088
+ unsubscribe();
1089
+ try { controller.close(); } catch {}
1090
+ });
1091
+ } else if (found) {
1092
+ sendEvent("finish", found.run);
1093
+ controller.close();
1094
+ }
1095
+ },
1096
+ });
1097
+
1098
+ return new Response(stream, {
1099
+ status: 200,
1100
+ headers: {
1101
+ "Content-Type": "text/event-stream",
1102
+ "Cache-Control": "no-cache",
1103
+ "Connection": "keep-alive",
1104
+ ...corsHeaders,
1105
+ },
1106
+ });
1107
+ }
1108
+
1109
+ // 12. Run Show: GET /api/v1/runs/:runId
443
1110
  const runShowMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)$/);
444
1111
  if (runShowMatch && req.method === "GET") {
445
1112
  const runId = decodeURIComponent(runShowMatch[1]);
@@ -462,7 +1129,7 @@ export function startActionDockServer(
462
1129
  return jsonResponse(found.run, 200, corsHeaders);
463
1130
  }
464
1131
 
465
- // 7. Run Cancel: POST /api/v1/runs/:runId/cancel
1132
+ // 13. Run Cancel: POST /api/v1/runs/:runId/cancel
466
1133
  const runCancelMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/cancel$/);
467
1134
  if (runCancelMatch && req.method === "POST") {
468
1135
  const runId = decodeURIComponent(runCancelMatch[1]);
@@ -475,7 +1142,6 @@ export function startActionDockServer(
475
1142
 
476
1143
  const reason = body?.reason || "Cancelled by client request";
477
1144
 
478
- // 1. Try cancelling in-memory active handle
479
1145
  const activeHandle = runtimeRegistry.executionManager.get(runId);
480
1146
  if (activeHandle) {
481
1147
  const cancelled = runtimeRegistry.executionManager.cancel(runId, reason);
@@ -492,9 +1158,7 @@ export function startActionDockServer(
492
1158
  }
493
1159
  }
494
1160
 
495
- // 2. Check storage for run status
496
1161
  const found = findRunAcrossStorages(runId, runtimeRegistry, projectRoot, customHome);
497
-
498
1162
  if (!found) {
499
1163
  return jsonResponse(
500
1164
  {
@@ -524,7 +1188,6 @@ export function startActionDockServer(
524
1188
  );
525
1189
  }
526
1190
 
527
- // Running in storage but no longer in memory
528
1191
  storage.updateRun(runId, "cancelled", undefined, {
529
1192
  code: "ACTION_CANCELLED",
530
1193
  message: reason,
@@ -541,6 +1204,263 @@ export function startActionDockServer(
541
1204
  );
542
1205
  }
543
1206
 
1207
+ // 14. State List: GET /api/v1/state
1208
+ if (pathname === "/api/v1/state" && req.method === "GET") {
1209
+ try {
1210
+ const pkgParam = url.searchParams.get("package") || undefined;
1211
+ const nsParam = url.searchParams.get("namespace") ?? undefined;
1212
+ const prefix = url.searchParams.get("prefix") || "";
1213
+
1214
+ const { packageId, storage } = resolveStorageForPackage(
1215
+ pkgParam,
1216
+ runtimeRegistry,
1217
+ projectRoot,
1218
+ customHome
1219
+ );
1220
+ const keys = await storage.listStateKeys(nsParam !== undefined ? nsParam : null, prefix);
1221
+ return jsonResponse({ ok: true, packageId, keys }, 200, corsHeaders);
1222
+ } catch (err: any) {
1223
+ return jsonResponse(
1224
+ { ok: false, error: { code: "STATE_LIST_ERROR", message: err.message } },
1225
+ 500,
1226
+ corsHeaders
1227
+ );
1228
+ }
1229
+ }
1230
+
1231
+ // 15. State Clear: POST /api/v1/state/clear
1232
+ if (pathname === "/api/v1/state/clear" && req.method === "POST") {
1233
+ try {
1234
+ const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
1235
+ const pkgParam = url.searchParams.get("package") || body.package || undefined;
1236
+ const { packageId, storage } = resolveStorageForPackage(
1237
+ pkgParam,
1238
+ runtimeRegistry,
1239
+ projectRoot,
1240
+ customHome
1241
+ );
1242
+ const clearedCount = await storage.clearState({
1243
+ namespace: body.namespace ?? (url.searchParams.get("namespace") || undefined),
1244
+ all: Boolean(body.all ?? url.searchParams.get("all") === "true"),
1245
+ prefix: body.prefix ?? (url.searchParams.get("prefix") || undefined),
1246
+ });
1247
+ return jsonResponse({ ok: true, packageId, clearedCount }, 200, corsHeaders);
1248
+ } catch (err: any) {
1249
+ return jsonResponse(
1250
+ { ok: false, error: { code: "STATE_CLEAR_ERROR", message: err.message } },
1251
+ 500,
1252
+ corsHeaders
1253
+ );
1254
+ }
1255
+ }
1256
+
1257
+ // 16. State Key CRUD: GET / PUT / POST / DELETE /api/v1/state/:key
1258
+ const stateKeyMatch = pathname.match(/^\/api\/v1\/state\/([^/]+)$/);
1259
+ if (stateKeyMatch) {
1260
+ const key = decodeURIComponent(stateKeyMatch[1]);
1261
+ const pkgParam = url.searchParams.get("package") || undefined;
1262
+ const nsParam = url.searchParams.get("namespace") || undefined;
1263
+
1264
+ const { packageId, storage } = resolveStorageForPackage(
1265
+ pkgParam,
1266
+ runtimeRegistry,
1267
+ projectRoot,
1268
+ customHome
1269
+ );
1270
+
1271
+ if (req.method === "GET") {
1272
+ const entry = await storage.findState(key, nsParam);
1273
+ if (!entry || entry.value === undefined) {
1274
+ return jsonResponse(
1275
+ { ok: false, error: { code: "STATE_KEY_NOT_FOUND", message: `State key '${key}' not found` } },
1276
+ 404,
1277
+ corsHeaders
1278
+ );
1279
+ }
1280
+ return jsonResponse(
1281
+ {
1282
+ ok: true,
1283
+ packageId,
1284
+ key: entry.key,
1285
+ namespace: entry.namespace,
1286
+ value: entry.value,
1287
+ expiresAt: entry.expiresAt,
1288
+ },
1289
+ 200,
1290
+ corsHeaders
1291
+ );
1292
+ }
1293
+
1294
+ if (req.method === "PUT" || req.method === "POST") {
1295
+ const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
1296
+ const val = body.value !== undefined ? body.value : body;
1297
+ const ttl = typeof body.ttl === "number" ? body.ttl : undefined;
1298
+ const namespace = body.namespace || nsParam || "";
1299
+
1300
+ let actualKey = key;
1301
+ let ns = namespace;
1302
+ if (!ns && key.includes(":")) {
1303
+ const idx = key.indexOf(":");
1304
+ ns = key.slice(0, idx);
1305
+ actualKey = key.slice(idx + 1);
1306
+ }
1307
+
1308
+ await storage.setState(ns, actualKey, val, ttl);
1309
+ return jsonResponse(
1310
+ { ok: true, packageId, key: actualKey, namespace: ns, message: "updated" },
1311
+ 200,
1312
+ corsHeaders
1313
+ );
1314
+ }
1315
+
1316
+ if (req.method === "DELETE") {
1317
+ const deleted = await storage.deleteStateSmart(key, nsParam);
1318
+ if (!deleted) {
1319
+ return jsonResponse(
1320
+ { ok: false, error: { code: "STATE_KEY_NOT_FOUND", message: `State key '${key}' not found` } },
1321
+ 404,
1322
+ corsHeaders
1323
+ );
1324
+ }
1325
+ return jsonResponse({ ok: true, packageId, key, deleted: true }, 200, corsHeaders);
1326
+ }
1327
+ }
1328
+
1329
+ // 17. Config Env Check: GET /api/v1/config/env
1330
+ if (pathname === "/api/v1/config/env" && req.method === "GET") {
1331
+ try {
1332
+ const pkgParam = url.searchParams.get("package") || undefined;
1333
+ const { packageId, projectRoot: root } = resolveStorageForPackage(
1334
+ pkgParam,
1335
+ runtimeRegistry,
1336
+ projectRoot,
1337
+ customHome
1338
+ );
1339
+ if (!root) {
1340
+ return jsonResponse({ ok: true, packageId, envChecks: [] }, 200, corsHeaders);
1341
+ }
1342
+ const cfg = loadProjectConfig(root);
1343
+ const declared = cfg.config || {};
1344
+ const envChecks: any[] = [];
1345
+ for (const [k, def] of Object.entries(declared)) {
1346
+ const envKeys = [
1347
+ k,
1348
+ `ACTIONDOCK_${k}`,
1349
+ `${packageId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_${k}`,
1350
+ ];
1351
+ const foundEnv = envKeys.find((ek) => process.env[ek] !== undefined);
1352
+ envChecks.push({
1353
+ key: k,
1354
+ required: def.default === undefined,
1355
+ satisfied: Boolean(foundEnv || def.default !== undefined),
1356
+ matchedEnv: foundEnv || null,
1357
+ hasDefault: def.default !== undefined,
1358
+ secret: Boolean(def.secret),
1359
+ });
1360
+ }
1361
+ return jsonResponse({ ok: true, packageId, envChecks }, 200, corsHeaders);
1362
+ } catch (err: any) {
1363
+ return jsonResponse(
1364
+ { ok: false, error: { code: "CONFIG_ENV_ERROR", message: err.message } },
1365
+ 500,
1366
+ corsHeaders
1367
+ );
1368
+ }
1369
+ }
1370
+
1371
+ // 18. Config Query: GET /api/v1/config
1372
+ if (pathname === "/api/v1/config" && req.method === "GET") {
1373
+ try {
1374
+ const pkgParam = url.searchParams.get("package") || undefined;
1375
+ const { packageId, storage, projectRoot: root } = resolveStorageForPackage(
1376
+ pkgParam,
1377
+ runtimeRegistry,
1378
+ projectRoot,
1379
+ customHome
1380
+ );
1381
+ const stored = storage.listConfig();
1382
+ let declared: Record<string, any> = {};
1383
+ if (root) {
1384
+ try {
1385
+ const cfg = loadProjectConfig(root);
1386
+ declared = cfg.config || {};
1387
+ } catch {}
1388
+ }
1389
+ const maskedValues: Record<string, any> = {};
1390
+ for (const [k, v] of Object.entries(stored)) {
1391
+ if (declared[k]?.secret) {
1392
+ maskedValues[k] = "******";
1393
+ } else {
1394
+ maskedValues[k] = v;
1395
+ }
1396
+ }
1397
+ return jsonResponse(
1398
+ { ok: true, packageId, declared, values: maskedValues },
1399
+ 200,
1400
+ corsHeaders
1401
+ );
1402
+ } catch (err: any) {
1403
+ return jsonResponse(
1404
+ { ok: false, error: { code: "CONFIG_LIST_ERROR", message: err.message } },
1405
+ 500,
1406
+ corsHeaders
1407
+ );
1408
+ }
1409
+ }
1410
+
1411
+ // 19. Config Update: PUT / POST /api/v1/config
1412
+ if (pathname === "/api/v1/config" && (req.method === "PUT" || req.method === "POST")) {
1413
+ try {
1414
+ const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
1415
+ const pkgParam = url.searchParams.get("package") || body.package || undefined;
1416
+ const { packageId, storage } = resolveStorageForPackage(
1417
+ pkgParam,
1418
+ runtimeRegistry,
1419
+ projectRoot,
1420
+ customHome
1421
+ );
1422
+ const key = body.key;
1423
+ if (!key) {
1424
+ return jsonResponse(
1425
+ { ok: false, error: { code: "INVALID_ARGUMENT", message: "Config 'key' is required" } },
1426
+ 400,
1427
+ corsHeaders
1428
+ );
1429
+ }
1430
+ storage.setConfig(key, body.value);
1431
+ return jsonResponse({ ok: true, packageId, key, message: "updated" }, 200, corsHeaders);
1432
+ } catch (err: any) {
1433
+ return jsonResponse(
1434
+ { ok: false, error: { code: "CONFIG_SET_ERROR", message: err.message } },
1435
+ 500,
1436
+ corsHeaders
1437
+ );
1438
+ }
1439
+ }
1440
+
1441
+ // 20. Config Delete: DELETE /api/v1/config/:key
1442
+ const configKeyMatch = pathname.match(/^\/api\/v1\/config\/([^/]+)$/);
1443
+ if (configKeyMatch && req.method === "DELETE") {
1444
+ try {
1445
+ const key = decodeURIComponent(configKeyMatch[1]);
1446
+ const pkgParam = url.searchParams.get("package") || undefined;
1447
+ const { packageId, storage } = resolveStorageForPackage(
1448
+ pkgParam,
1449
+ runtimeRegistry,
1450
+ projectRoot,
1451
+ customHome
1452
+ );
1453
+ const deleted = storage.deleteConfig(key);
1454
+ return jsonResponse({ ok: true, packageId, key, deleted }, 200, corsHeaders);
1455
+ } catch (err: any) {
1456
+ return jsonResponse(
1457
+ { ok: false, error: { code: "CONFIG_DELETE_ERROR", message: err.message } },
1458
+ 500,
1459
+ corsHeaders
1460
+ );
1461
+ }
1462
+ }
1463
+
544
1464
  // 404 Not Found
545
1465
  return jsonResponse(
546
1466
  {
@@ -553,8 +1473,8 @@ export function startActionDockServer(
553
1473
  404,
554
1474
  corsHeaders
555
1475
  );
556
- },
557
- });
1476
+ }
1477
+ );
558
1478
 
559
1479
  const actualHost = host === "0.0.0.0" ? "127.0.0.1" : host;
560
1480
  const url = `http://${actualHost}:${server.port}`;