@suwujs/king-ai 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +96 -0
  2. package/dist/src/agent-config-validation.d.ts +9 -0
  3. package/dist/src/agent-config-validation.js +30 -0
  4. package/dist/src/api.d.ts +4 -0
  5. package/dist/src/api.js +48 -0
  6. package/dist/src/attachments.d.ts +45 -0
  7. package/dist/src/attachments.js +322 -0
  8. package/dist/src/cli.d.ts +20 -0
  9. package/dist/src/cli.js +1697 -0
  10. package/dist/src/config.d.ts +3 -0
  11. package/dist/src/config.js +20 -0
  12. package/dist/src/cron.d.ts +11 -0
  13. package/dist/src/cron.js +65 -0
  14. package/dist/src/daemon.d.ts +36 -0
  15. package/dist/src/daemon.js +373 -0
  16. package/dist/src/engine.d.ts +32 -0
  17. package/dist/src/engine.js +1014 -0
  18. package/dist/src/heartbeat.d.ts +18 -0
  19. package/dist/src/heartbeat.js +28 -0
  20. package/dist/src/host-api.d.ts +40 -0
  21. package/dist/src/host-api.js +59 -0
  22. package/dist/src/host-control.d.ts +48 -0
  23. package/dist/src/host-control.js +1279 -0
  24. package/dist/src/host-export.d.ts +50 -0
  25. package/dist/src/host-export.js +187 -0
  26. package/dist/src/host-feedback.d.ts +78 -0
  27. package/dist/src/host-feedback.js +178 -0
  28. package/dist/src/host-home.d.ts +13 -0
  29. package/dist/src/host-home.js +54 -0
  30. package/dist/src/host-ledger.d.ts +261 -0
  31. package/dist/src/host-ledger.js +554 -0
  32. package/dist/src/host-loop-events.d.ts +69 -0
  33. package/dist/src/host-loop-events.js +288 -0
  34. package/dist/src/host-permission.d.ts +36 -0
  35. package/dist/src/host-permission.js +180 -0
  36. package/dist/src/host-policy.d.ts +15 -0
  37. package/dist/src/host-policy.js +36 -0
  38. package/dist/src/host-run-executor.d.ts +13 -0
  39. package/dist/src/host-run-executor.js +221 -0
  40. package/dist/src/host-run-heartbeat.d.ts +40 -0
  41. package/dist/src/host-run-heartbeat.js +103 -0
  42. package/dist/src/host-run-layout.d.ts +17 -0
  43. package/dist/src/host-run-layout.js +387 -0
  44. package/dist/src/host-run-meta.d.ts +41 -0
  45. package/dist/src/host-run-meta.js +115 -0
  46. package/dist/src/host-run-spec.d.ts +149 -0
  47. package/dist/src/host-run-spec.js +465 -0
  48. package/dist/src/host-runs.d.ts +77 -0
  49. package/dist/src/host-runs.js +195 -0
  50. package/dist/src/host-sdk.d.ts +412 -0
  51. package/dist/src/host-sdk.js +628 -0
  52. package/dist/src/host-server.d.ts +26 -0
  53. package/dist/src/host-server.js +921 -0
  54. package/dist/src/host-timeline.d.ts +24 -0
  55. package/dist/src/host-timeline.js +161 -0
  56. package/dist/src/jsonl.d.ts +13 -0
  57. package/dist/src/jsonl.js +47 -0
  58. package/dist/src/lifecycle.d.ts +5 -0
  59. package/dist/src/lifecycle.js +18 -0
  60. package/dist/src/message-routing.d.ts +32 -0
  61. package/dist/src/message-routing.js +119 -0
  62. package/dist/src/paths.d.ts +19 -0
  63. package/dist/src/paths.js +26 -0
  64. package/dist/src/project-profile.d.ts +49 -0
  65. package/dist/src/project-profile.js +356 -0
  66. package/dist/src/remediation.d.ts +14 -0
  67. package/dist/src/remediation.js +114 -0
  68. package/dist/src/remote-devices.d.ts +41 -0
  69. package/dist/src/remote-devices.js +156 -0
  70. package/dist/src/remote-diagnostics.d.ts +39 -0
  71. package/dist/src/remote-diagnostics.js +199 -0
  72. package/dist/src/remote-ssh.d.ts +39 -0
  73. package/dist/src/remote-ssh.js +129 -0
  74. package/dist/src/run-stream.d.ts +57 -0
  75. package/dist/src/run-stream.js +119 -0
  76. package/dist/src/runner.d.ts +131 -0
  77. package/dist/src/runner.js +1161 -0
  78. package/dist/src/runtime-data.d.ts +68 -0
  79. package/dist/src/runtime-data.js +172 -0
  80. package/dist/src/service.d.ts +114 -0
  81. package/dist/src/service.js +631 -0
  82. package/dist/src/shared-skills.d.ts +26 -0
  83. package/dist/src/shared-skills.js +85 -0
  84. package/dist/src/shim.d.ts +1 -0
  85. package/dist/src/shim.js +64 -0
  86. package/dist/src/skill-check.d.ts +17 -0
  87. package/dist/src/skill-check.js +158 -0
  88. package/dist/src/sse.d.ts +9 -0
  89. package/dist/src/sse.js +36 -0
  90. package/dist/src/team-routing.d.ts +55 -0
  91. package/dist/src/team-routing.js +131 -0
  92. package/dist/src/team-workflow.d.ts +78 -0
  93. package/dist/src/team-workflow.js +253 -0
  94. package/dist/src/text.d.ts +7 -0
  95. package/dist/src/text.js +27 -0
  96. package/dist/src/types.d.ts +98 -0
  97. package/dist/src/types.js +1 -0
  98. package/dist/src/usage.d.ts +116 -0
  99. package/dist/src/usage.js +350 -0
  100. package/dist/src/workspace.d.ts +9 -0
  101. package/dist/src/workspace.js +56 -0
  102. package/dist/src/worktree.d.ts +47 -0
  103. package/dist/src/worktree.js +201 -0
  104. package/package.json +63 -0
@@ -0,0 +1,921 @@
1
+ import { createServer } from "node:http";
2
+ import { buildHostStatusSnapshot, formatHostStatusSnapshot } from "./host-api.js";
3
+ import { listSafeHostExecutorCommands } from "./host-run-executor.js";
4
+ import { listHostCommands, runHostCommand } from "./host-control.js";
5
+ import { readHostRunHeartbeat } from "./host-run-heartbeat.js";
6
+ import { readHostRunMeta } from "./host-run-meta.js";
7
+ import { readHostTimeline } from "./host-timeline.js";
8
+ import { listHostRunRequests } from "./host-runs.js";
9
+ import { readRunningState } from "./service.js";
10
+ import { tokenBudgetFromEnv, usagePricingFromEnv } from "./usage.js";
11
+ export const DEFAULT_HOST_SERVER_HOST = "127.0.0.1";
12
+ export const DEFAULT_HOST_SERVER_PORT = 8799;
13
+ const HOST_RESOURCE_ENDPOINTS = [
14
+ "GET /health",
15
+ "GET /capabilities",
16
+ "GET /status",
17
+ "GET /host/snapshot",
18
+ "GET /host/stream",
19
+ "GET /status/stream",
20
+ "GET /status.txt",
21
+ "GET /events",
22
+ "GET /timeline",
23
+ "GET /timeline/stream",
24
+ "GET /usage",
25
+ "GET /expenses",
26
+ "GET /doctor",
27
+ "GET /commands",
28
+ "POST /commands/run",
29
+ "POST /runs/plan",
30
+ "POST /runs/preflight",
31
+ "POST /runs/prepare-layout",
32
+ "GET /runs",
33
+ "GET /runs/stream",
34
+ "POST /runs",
35
+ "GET /runs/:id",
36
+ "GET /runs/:id/stream",
37
+ "PATCH /runs/:id",
38
+ "DELETE /runs/:id",
39
+ "POST /runs/:id/execute",
40
+ "GET /runs/:id/events",
41
+ "POST /runs/:id/events",
42
+ "GET /runs/:id/results",
43
+ "GET /runs/:id/heartbeat",
44
+ "GET /runs/:id/meta",
45
+ "POST /runs/execute",
46
+ "POST /exports/plan",
47
+ "POST /exports",
48
+ "GET /policy/:command",
49
+ "POST /policy/:command",
50
+ "GET /remote/config",
51
+ "PUT /remote/config",
52
+ "GET /remote/devices",
53
+ "POST /remote/devices",
54
+ "PATCH /remote/devices/:id",
55
+ "DELETE /remote/devices/:id",
56
+ "POST /remote/devices/:id/probe",
57
+ "POST /remote/devices/:id/profile"
58
+ ];
59
+ const HOST_STREAM_ENDPOINTS = [
60
+ "GET /host/stream",
61
+ "GET /status/stream",
62
+ "GET /timeline/stream",
63
+ "GET /runs/stream",
64
+ "GET /runs/:id/stream"
65
+ ];
66
+ export function hostServerPortFromEnv(env = process.env) {
67
+ const raw = env.KING_AI_HOST_PORT;
68
+ if (!raw)
69
+ return DEFAULT_HOST_SERVER_PORT;
70
+ const port = Number.parseInt(raw, 10);
71
+ if (!Number.isFinite(port) || port < 0 || port > 65535) {
72
+ throw new Error("host server port must be between 0 and 65535");
73
+ }
74
+ return port;
75
+ }
76
+ export function normalizeHostServerHost(host = DEFAULT_HOST_SERVER_HOST) {
77
+ const value = host.trim();
78
+ if (value === "127.0.0.1" || value === "::1" || value === "localhost")
79
+ return value;
80
+ throw new Error("host server only supports localhost bindings: 127.0.0.1, ::1, or localhost");
81
+ }
82
+ function localhostCorsOrigin(req) {
83
+ const origin = req.headers.origin;
84
+ if (!origin || Array.isArray(origin))
85
+ return undefined;
86
+ try {
87
+ const url = new URL(origin);
88
+ if (url.protocol !== "http:" && url.protocol !== "https:")
89
+ return undefined;
90
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]")
91
+ return origin;
92
+ }
93
+ catch {
94
+ return undefined;
95
+ }
96
+ return undefined;
97
+ }
98
+ function hostResponseHeaders(req, headers = {}) {
99
+ const origin = req ? localhostCorsOrigin(req) : undefined;
100
+ return {
101
+ ...headers,
102
+ ...(origin ? {
103
+ "Access-Control-Allow-Origin": origin,
104
+ "Access-Control-Allow-Methods": "GET,HEAD,POST,PATCH,DELETE,OPTIONS",
105
+ "Access-Control-Allow-Headers": "Content-Type,Accept",
106
+ "Access-Control-Max-Age": "600"
107
+ } : {})
108
+ };
109
+ }
110
+ function applyHostCorsHeaders(req, res) {
111
+ const origin = localhostCorsOrigin(req);
112
+ if (!origin)
113
+ return;
114
+ const headers = hostResponseHeaders(req);
115
+ for (const [key, value] of Object.entries(headers)) {
116
+ res.setHeader(key, value);
117
+ }
118
+ }
119
+ function sendOptions(req, res) {
120
+ const origin = localhostCorsOrigin(req);
121
+ if (!origin && req.headers.origin) {
122
+ sendJson(res, 403, { ok: false, error: "origin not allowed" });
123
+ return;
124
+ }
125
+ res.writeHead(204, hostResponseHeaders(req));
126
+ res.end();
127
+ }
128
+ function sendJson(res, status, value, headOnly = false) {
129
+ const body = JSON.stringify(value, null, 2);
130
+ res.writeHead(status, {
131
+ "Content-Type": "application/json; charset=utf-8",
132
+ "Content-Length": Buffer.byteLength(body)
133
+ });
134
+ if (!headOnly)
135
+ res.end(body);
136
+ else
137
+ res.end();
138
+ }
139
+ function sendText(res, status, value, headOnly = false) {
140
+ res.writeHead(status, {
141
+ "Content-Type": "text/plain; charset=utf-8",
142
+ "Content-Length": Buffer.byteLength(value)
143
+ });
144
+ if (!headOnly)
145
+ res.end(value);
146
+ else
147
+ res.end();
148
+ }
149
+ function sendSseEvent(res, event, value) {
150
+ res.write(`event: ${event}\n`);
151
+ res.write(`data: ${JSON.stringify(value)}\n\n`);
152
+ }
153
+ async function readJsonBody(req) {
154
+ const chunks = [];
155
+ let length = 0;
156
+ for await (const chunk of req) {
157
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
158
+ length += buffer.length;
159
+ if (length > 64 * 1024)
160
+ throw new Error("request body too large");
161
+ chunks.push(buffer);
162
+ }
163
+ const body = Buffer.concat(chunks).toString("utf8").trim();
164
+ if (!body)
165
+ return {};
166
+ return JSON.parse(body);
167
+ }
168
+ function isHostCommandRequest(value) {
169
+ return Boolean(value && typeof value === "object" && typeof value.command === "string");
170
+ }
171
+ function hostCommandHttpStatus(result) {
172
+ if (result.exitCode === 66)
173
+ return 404;
174
+ return result.ok || result.exitCode === 1 ? 200 : 400;
175
+ }
176
+ async function runHostCommandRoute(res, runCommand, request, headOnly = false) {
177
+ try {
178
+ const result = await runCommand(request);
179
+ sendJson(res, hostCommandHttpStatus(result), result, headOnly);
180
+ }
181
+ catch (err) {
182
+ sendJson(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) }, headOnly);
183
+ }
184
+ }
185
+ function decodePathPart(value) {
186
+ try {
187
+ return decodeURIComponent(value);
188
+ }
189
+ catch {
190
+ return value;
191
+ }
192
+ }
193
+ function normalizeStatusStreamInterval(value) {
194
+ const n = Number(value ?? process.env.KING_AI_HOST_STATUS_STREAM_INTERVAL_MS ?? 1000);
195
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1000;
196
+ }
197
+ function normalizeTimelineLimit(value) {
198
+ if (value === undefined || value === null || value === "")
199
+ return 20;
200
+ const n = Number.parseInt(String(value), 10);
201
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 20;
202
+ }
203
+ function buildHostCapabilities() {
204
+ const commands = listHostCommands();
205
+ return {
206
+ ok: true,
207
+ service: "king-ai host",
208
+ readOnly: true,
209
+ localhostOnly: true,
210
+ remoteApi: false,
211
+ cors: {
212
+ enabled: true,
213
+ allowedOrigins: ["http://localhost:*", "http://127.0.0.1:*", "http://[::1]:*", "https://localhost:*", "https://127.0.0.1:*", "https://[::1]:*"]
214
+ },
215
+ resources: [...HOST_RESOURCE_ENDPOINTS],
216
+ streams: [...HOST_STREAM_ENDPOINTS],
217
+ commands,
218
+ safeExecutorCommands: listSafeHostExecutorCommands(),
219
+ destructiveCommands: commands.filter((entry) => entry.destructive).map((entry) => entry.name),
220
+ commandEnvelope: {
221
+ path: "/commands/run",
222
+ method: "POST"
223
+ }
224
+ };
225
+ }
226
+ export function createHostStatusServer(options = {}) {
227
+ const readState = options.readState ?? readRunningState;
228
+ const tokenBudget = options.tokenBudget ?? tokenBudgetFromEnv;
229
+ const usagePricing = options.usagePricing ?? usagePricingFromEnv;
230
+ const readTimeline = options.readTimeline ?? ((limit) => readHostTimeline({ limit }));
231
+ const readRuns = options.readRuns ?? ((input) => listHostRunRequests(input));
232
+ const runCommand = options.runCommand ?? ((request) => runHostCommand(request, { readState, tokenBudget, usagePricing, recordTimeline: true }));
233
+ return createServer(async (req, res) => {
234
+ try {
235
+ const method = req.method ?? "GET";
236
+ if (method === "OPTIONS") {
237
+ sendOptions(req, res);
238
+ return;
239
+ }
240
+ applyHostCorsHeaders(req, res);
241
+ const headOnly = method === "HEAD";
242
+ const url = new URL(req.url ?? "/", "http://localhost");
243
+ if (url.pathname === "/commands/run") {
244
+ if (method !== "POST") {
245
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
246
+ return;
247
+ }
248
+ const body = await readJsonBody(req);
249
+ if (!isHostCommandRequest(body)) {
250
+ sendJson(res, 400, { ok: false, error: "command is required" });
251
+ return;
252
+ }
253
+ await runHostCommandRoute(res, runCommand, body, headOnly);
254
+ return;
255
+ }
256
+ if (url.pathname === "/runs") {
257
+ if (method === "GET" || method === "HEAD") {
258
+ await runHostCommandRoute(res, runCommand, {
259
+ command: "run-requests",
260
+ format: "json",
261
+ input: {
262
+ limit: url.searchParams.get("limit") ?? undefined,
263
+ status: url.searchParams.get("status") ?? undefined
264
+ }
265
+ }, headOnly);
266
+ return;
267
+ }
268
+ if (method === "POST") {
269
+ await runHostCommandRoute(res, runCommand, {
270
+ command: "submit-run",
271
+ format: "json",
272
+ input: await readJsonBody(req)
273
+ });
274
+ return;
275
+ }
276
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
277
+ return;
278
+ }
279
+ if (url.pathname === "/runs/execute") {
280
+ if (method !== "POST") {
281
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
282
+ return;
283
+ }
284
+ const body = await readJsonBody(req);
285
+ await runHostCommandRoute(res, runCommand, {
286
+ command: "execute-run",
287
+ format: "json",
288
+ input: body && typeof body === "object" ? body : {}
289
+ });
290
+ return;
291
+ }
292
+ if (url.pathname === "/runs/stream") {
293
+ if (method !== "GET" && method !== "HEAD") {
294
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
295
+ return;
296
+ }
297
+ if (headOnly) {
298
+ res.writeHead(200, {
299
+ "Content-Type": "text/event-stream; charset=utf-8",
300
+ "Cache-Control": "no-store",
301
+ "Connection": "keep-alive"
302
+ });
303
+ res.end();
304
+ return;
305
+ }
306
+ res.writeHead(200, {
307
+ "Content-Type": "text/event-stream; charset=utf-8",
308
+ "Cache-Control": "no-store",
309
+ "Connection": "keep-alive"
310
+ });
311
+ const interval = normalizeStatusStreamInterval(url.searchParams.get("interval"));
312
+ const limit = normalizeTimelineLimit(url.searchParams.get("limit"));
313
+ const status = url.searchParams.get("status") ?? undefined;
314
+ let closed = false;
315
+ let busy = false;
316
+ const sendRuns = async () => {
317
+ if (closed || busy)
318
+ return;
319
+ busy = true;
320
+ try {
321
+ sendSseEvent(res, "runs", { requests: await readRuns({ limit, status: status }) });
322
+ }
323
+ catch (err) {
324
+ sendSseEvent(res, "error", { error: err instanceof Error ? err.message : String(err) });
325
+ }
326
+ finally {
327
+ busy = false;
328
+ }
329
+ };
330
+ const timer = setInterval(() => void sendRuns(), interval);
331
+ timer.unref?.();
332
+ req.once("close", () => {
333
+ closed = true;
334
+ clearInterval(timer);
335
+ });
336
+ await sendRuns();
337
+ return;
338
+ }
339
+ if (url.pathname === "/runs/plan" || url.pathname === "/runs/preflight" || url.pathname === "/runs/prepare-layout") {
340
+ if (method !== "POST") {
341
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
342
+ return;
343
+ }
344
+ await runHostCommandRoute(res, runCommand, {
345
+ command: url.pathname === "/runs/plan"
346
+ ? "plan-run"
347
+ : url.pathname === "/runs/preflight"
348
+ ? "preflight"
349
+ : "prepare-run-layout",
350
+ format: "json",
351
+ input: await readJsonBody(req)
352
+ });
353
+ return;
354
+ }
355
+ const runPath = url.pathname.match(/^\/runs\/([^/]+)(?:\/([^/]+))?$/);
356
+ if (runPath) {
357
+ const id = decodePathPart(runPath[1] ?? "");
358
+ const action = runPath[2] ? decodePathPart(runPath[2]) : undefined;
359
+ if (!id) {
360
+ sendJson(res, 400, { ok: false, error: "run id is required" }, headOnly);
361
+ return;
362
+ }
363
+ if (action === "stream") {
364
+ if (method !== "GET" && method !== "HEAD") {
365
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
366
+ return;
367
+ }
368
+ if (headOnly) {
369
+ res.writeHead(200, {
370
+ "Content-Type": "text/event-stream; charset=utf-8",
371
+ "Cache-Control": "no-store",
372
+ "Connection": "keep-alive"
373
+ });
374
+ res.end();
375
+ return;
376
+ }
377
+ res.writeHead(200, {
378
+ "Content-Type": "text/event-stream; charset=utf-8",
379
+ "Cache-Control": "no-store",
380
+ "Connection": "keep-alive"
381
+ });
382
+ const interval = normalizeStatusStreamInterval(url.searchParams.get("interval"));
383
+ let closed = false;
384
+ let busy = false;
385
+ const sendRun = async () => {
386
+ if (closed || busy)
387
+ return;
388
+ busy = true;
389
+ try {
390
+ const request = (await readRuns({ limit: 100 })).find((entry) => entry.id === id) ?? null;
391
+ const outputDir = request?.spec.options?.outputDir;
392
+ const [heartbeat, meta] = outputDir
393
+ ? await Promise.all([
394
+ readHostRunHeartbeat({ outputDir }),
395
+ readHostRunMeta({ outputDir })
396
+ ])
397
+ : [null, null];
398
+ sendSseEvent(res, "run", {
399
+ request,
400
+ heartbeat: heartbeat?.heartbeat ?? null,
401
+ meta: meta?.meta ?? null
402
+ });
403
+ }
404
+ catch (err) {
405
+ sendSseEvent(res, "error", { error: err instanceof Error ? err.message : String(err) });
406
+ }
407
+ finally {
408
+ busy = false;
409
+ }
410
+ };
411
+ const timer = setInterval(() => void sendRun(), interval);
412
+ timer.unref?.();
413
+ req.once("close", () => {
414
+ closed = true;
415
+ clearInterval(timer);
416
+ });
417
+ await sendRun();
418
+ return;
419
+ }
420
+ if (!action && (method === "GET" || method === "HEAD")) {
421
+ await runHostCommandRoute(res, runCommand, {
422
+ command: "run-request",
423
+ format: "json",
424
+ input: { id }
425
+ }, headOnly);
426
+ return;
427
+ }
428
+ if (!action && method === "PATCH") {
429
+ await runHostCommandRoute(res, runCommand, {
430
+ command: "update-run",
431
+ format: "json",
432
+ input: { ...await readJsonBody(req), id }
433
+ });
434
+ return;
435
+ }
436
+ if (!action && method === "DELETE") {
437
+ const body = await readJsonBody(req);
438
+ await runHostCommandRoute(res, runCommand, {
439
+ command: "cancel-run",
440
+ format: "json",
441
+ input: { ...(body && typeof body === "object" ? body : {}), id }
442
+ });
443
+ return;
444
+ }
445
+ if (action === "execute" && method === "POST") {
446
+ const body = await readJsonBody(req);
447
+ await runHostCommandRoute(res, runCommand, {
448
+ command: "execute-run",
449
+ format: "json",
450
+ input: { ...(body && typeof body === "object" ? body : {}), id }
451
+ });
452
+ return;
453
+ }
454
+ if (action === "events" && method === "POST") {
455
+ const body = await readJsonBody(req);
456
+ await runHostCommandRoute(res, runCommand, {
457
+ command: "emit-run-event",
458
+ format: "json",
459
+ input: { ...(body && typeof body === "object" ? body : {}), id }
460
+ });
461
+ return;
462
+ }
463
+ if (action === "events" && (method === "GET" || method === "HEAD")) {
464
+ await runHostCommandRoute(res, runCommand, {
465
+ command: "watch-run",
466
+ format: "json",
467
+ input: {
468
+ id,
469
+ tail: url.searchParams.get("tail") ?? undefined,
470
+ type: url.searchParams.get("type") ?? undefined,
471
+ agent: url.searchParams.get("agent") ?? undefined,
472
+ classification: url.searchParams.get("classification") ?? undefined,
473
+ file: url.searchParams.get("file") ?? undefined,
474
+ outputDir: url.searchParams.get("outputDir") ?? undefined,
475
+ writeResults: parseOptionalBoolean(url.searchParams.get("writeResults"))
476
+ }
477
+ }, headOnly);
478
+ return;
479
+ }
480
+ if (action === "results" && (method === "GET" || method === "HEAD")) {
481
+ await runHostCommandRoute(res, runCommand, {
482
+ command: "run-results",
483
+ format: "json",
484
+ input: {
485
+ id,
486
+ file: url.searchParams.get("file") ?? undefined,
487
+ outputDir: url.searchParams.get("outputDir") ?? undefined,
488
+ resultsFile: url.searchParams.get("resultsFile") ?? undefined
489
+ }
490
+ }, headOnly);
491
+ return;
492
+ }
493
+ if (action === "heartbeat" && (method === "GET" || method === "HEAD")) {
494
+ await runHostCommandRoute(res, runCommand, {
495
+ command: "run-heartbeat",
496
+ format: "json",
497
+ input: {
498
+ id,
499
+ file: url.searchParams.get("file") ?? undefined,
500
+ outputDir: url.searchParams.get("outputDir") ?? undefined
501
+ }
502
+ }, headOnly);
503
+ return;
504
+ }
505
+ if (action === "meta" && (method === "GET" || method === "HEAD")) {
506
+ await runHostCommandRoute(res, runCommand, {
507
+ command: "run-meta",
508
+ format: "json",
509
+ input: {
510
+ id,
511
+ file: url.searchParams.get("file") ?? undefined,
512
+ outputDir: url.searchParams.get("outputDir") ?? undefined
513
+ }
514
+ }, headOnly);
515
+ return;
516
+ }
517
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
518
+ return;
519
+ }
520
+ if (url.pathname === "/exports/plan" || url.pathname === "/exports") {
521
+ if (method !== "POST") {
522
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
523
+ return;
524
+ }
525
+ await runHostCommandRoute(res, runCommand, {
526
+ command: url.pathname === "/exports/plan" ? "plan-export" : "export",
527
+ format: "json",
528
+ input: await readJsonBody(req)
529
+ });
530
+ return;
531
+ }
532
+ const policyPath = url.pathname.match(/^\/policy\/([^/]+)$/);
533
+ if (policyPath) {
534
+ const command = decodePathPart(policyPath[1] ?? "");
535
+ if (!command) {
536
+ sendJson(res, 400, { ok: false, error: "policy command is required" }, headOnly);
537
+ return;
538
+ }
539
+ if (method !== "GET" && method !== "HEAD" && method !== "POST") {
540
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
541
+ return;
542
+ }
543
+ const input = method === "POST" ? await readJsonBody(req) : {};
544
+ await runHostCommandRoute(res, runCommand, {
545
+ command: "policy",
546
+ format: "json",
547
+ input: { ...(input && typeof input === "object" ? input : {}), command }
548
+ }, headOnly);
549
+ return;
550
+ }
551
+ if (url.pathname === "/remote/config") {
552
+ if (method === "GET" || method === "HEAD") {
553
+ await runHostCommandRoute(res, runCommand, {
554
+ command: "remote-config-get",
555
+ format: "json",
556
+ input: { revealSecrets: true }
557
+ }, headOnly);
558
+ return;
559
+ }
560
+ if (method === "PUT" || method === "POST") {
561
+ await runHostCommandRoute(res, runCommand, {
562
+ command: "remote-config-save",
563
+ format: "json",
564
+ input: await readJsonBody(req)
565
+ });
566
+ return;
567
+ }
568
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
569
+ return;
570
+ }
571
+ if (url.pathname === "/remote/devices") {
572
+ if (method === "GET" || method === "HEAD") {
573
+ await runHostCommandRoute(res, runCommand, {
574
+ command: "remote-list",
575
+ format: "json"
576
+ }, headOnly);
577
+ return;
578
+ }
579
+ if (method === "POST") {
580
+ await runHostCommandRoute(res, runCommand, {
581
+ command: "remote-save-device",
582
+ format: "json",
583
+ input: await readJsonBody(req)
584
+ });
585
+ return;
586
+ }
587
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
588
+ return;
589
+ }
590
+ const remoteDevicePath = url.pathname.match(/^\/remote\/devices\/([^/]+)(?:\/([^/]+))?$/);
591
+ if (remoteDevicePath) {
592
+ const id = decodePathPart(remoteDevicePath[1] ?? "");
593
+ const action = remoteDevicePath[2] ? decodePathPart(remoteDevicePath[2]) : undefined;
594
+ if (!id) {
595
+ sendJson(res, 400, { ok: false, error: "remote device id is required" }, headOnly);
596
+ return;
597
+ }
598
+ if (!action && method === "PATCH") {
599
+ const body = await readJsonBody(req);
600
+ await runHostCommandRoute(res, runCommand, {
601
+ command: "remote-save-device",
602
+ format: "json",
603
+ input: { ...(body && typeof body === "object" ? body : {}), id }
604
+ });
605
+ return;
606
+ }
607
+ if (!action && method === "DELETE") {
608
+ await runHostCommandRoute(res, runCommand, {
609
+ command: "remote-delete-device",
610
+ format: "json",
611
+ input: { id }
612
+ });
613
+ return;
614
+ }
615
+ if (action === "default" && method === "POST") {
616
+ await runHostCommandRoute(res, runCommand, {
617
+ command: "remote-default-device",
618
+ format: "json",
619
+ input: { id }
620
+ });
621
+ return;
622
+ }
623
+ if ((action === "probe" || action === "profile") && method === "POST") {
624
+ const body = await readJsonBody(req);
625
+ await runHostCommandRoute(res, runCommand, {
626
+ command: action === "probe" ? "remote-probe" : "remote-profile",
627
+ format: "json",
628
+ input: { ...(body && typeof body === "object" ? body : {}), device: id }
629
+ });
630
+ return;
631
+ }
632
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
633
+ return;
634
+ }
635
+ if (method !== "GET" && method !== "HEAD") {
636
+ sendJson(res, 405, { ok: false, error: "method not allowed" }, headOnly);
637
+ return;
638
+ }
639
+ if (url.pathname === "/" || url.pathname === "/health") {
640
+ sendJson(res, 200, {
641
+ ok: true,
642
+ service: "king-ai host",
643
+ readOnly: true,
644
+ commands: listHostCommands().map((entry) => entry.name)
645
+ }, headOnly);
646
+ return;
647
+ }
648
+ if (url.pathname === "/capabilities") {
649
+ sendJson(res, 200, buildHostCapabilities(), headOnly);
650
+ return;
651
+ }
652
+ if (url.pathname === "/host/snapshot") {
653
+ const limit = normalizeTimelineLimit(url.searchParams.get("limit"));
654
+ const timelineLimit = normalizeTimelineLimit(url.searchParams.get("timelineLimit") ?? url.searchParams.get("limit"));
655
+ const runLimit = normalizeTimelineLimit(url.searchParams.get("runLimit") ?? url.searchParams.get("limit"));
656
+ const runStatus = url.searchParams.get("status") ?? undefined;
657
+ sendJson(res, 200, {
658
+ ok: true,
659
+ status: buildHostStatusSnapshot(await readState(), tokenBudget(), usagePricing()),
660
+ capabilities: buildHostCapabilities(),
661
+ timeline: await readTimeline(timelineLimit ?? limit),
662
+ runs: await readRuns({ limit: runLimit ?? limit, status: runStatus })
663
+ }, headOnly);
664
+ return;
665
+ }
666
+ if (url.pathname === "/host/stream") {
667
+ if (headOnly) {
668
+ res.writeHead(200, {
669
+ "Content-Type": "text/event-stream; charset=utf-8",
670
+ "Cache-Control": "no-store",
671
+ "Connection": "keep-alive"
672
+ });
673
+ res.end();
674
+ return;
675
+ }
676
+ res.writeHead(200, {
677
+ "Content-Type": "text/event-stream; charset=utf-8",
678
+ "Cache-Control": "no-store",
679
+ "Connection": "keep-alive"
680
+ });
681
+ const interval = normalizeStatusStreamInterval(url.searchParams.get("interval") ?? options.statusStreamIntervalMs);
682
+ const timelineLimit = normalizeTimelineLimit(url.searchParams.get("timelineLimit") ?? url.searchParams.get("limit"));
683
+ const runLimit = normalizeTimelineLimit(url.searchParams.get("runLimit") ?? url.searchParams.get("limit"));
684
+ const runStatus = url.searchParams.get("status") ?? undefined;
685
+ let closed = false;
686
+ let busy = false;
687
+ const sendHostFrame = async () => {
688
+ if (closed || busy)
689
+ return;
690
+ busy = true;
691
+ try {
692
+ sendSseEvent(res, "status", buildHostStatusSnapshot(await readState(), tokenBudget(), usagePricing()));
693
+ sendSseEvent(res, "timeline", await readTimeline(timelineLimit));
694
+ sendSseEvent(res, "runs", { requests: await readRuns({ limit: runLimit, status: runStatus }) });
695
+ }
696
+ catch (err) {
697
+ sendSseEvent(res, "error", { error: err instanceof Error ? err.message : String(err) });
698
+ }
699
+ finally {
700
+ busy = false;
701
+ }
702
+ };
703
+ const timer = setInterval(() => void sendHostFrame(), interval);
704
+ timer.unref?.();
705
+ req.once("close", () => {
706
+ closed = true;
707
+ clearInterval(timer);
708
+ });
709
+ await sendHostFrame();
710
+ return;
711
+ }
712
+ if (url.pathname === "/commands") {
713
+ sendJson(res, 200, {
714
+ ok: true,
715
+ commands: listHostCommands()
716
+ }, headOnly);
717
+ return;
718
+ }
719
+ if (url.pathname === "/timeline") {
720
+ await runHostCommandRoute(res, runCommand, {
721
+ command: "timeline",
722
+ format: "json",
723
+ input: {
724
+ limit: url.searchParams.get("limit") ?? undefined
725
+ }
726
+ }, headOnly);
727
+ return;
728
+ }
729
+ if (url.pathname === "/timeline/stream") {
730
+ if (headOnly) {
731
+ res.writeHead(200, {
732
+ "Content-Type": "text/event-stream; charset=utf-8",
733
+ "Cache-Control": "no-store",
734
+ "Connection": "keep-alive"
735
+ });
736
+ res.end();
737
+ return;
738
+ }
739
+ res.writeHead(200, {
740
+ "Content-Type": "text/event-stream; charset=utf-8",
741
+ "Cache-Control": "no-store",
742
+ "Connection": "keep-alive"
743
+ });
744
+ const interval = normalizeStatusStreamInterval(url.searchParams.get("interval"));
745
+ const limit = normalizeTimelineLimit(url.searchParams.get("limit"));
746
+ let closed = false;
747
+ let busy = false;
748
+ const sendTimeline = async () => {
749
+ if (closed || busy)
750
+ return;
751
+ busy = true;
752
+ try {
753
+ sendSseEvent(res, "timeline", await readTimeline(limit));
754
+ }
755
+ catch (err) {
756
+ sendSseEvent(res, "error", { error: err instanceof Error ? err.message : String(err) });
757
+ }
758
+ finally {
759
+ busy = false;
760
+ }
761
+ };
762
+ const timer = setInterval(() => void sendTimeline(), interval);
763
+ timer.unref?.();
764
+ req.once("close", () => {
765
+ closed = true;
766
+ clearInterval(timer);
767
+ });
768
+ await sendTimeline();
769
+ return;
770
+ }
771
+ if (url.pathname === "/usage" || url.pathname === "/expenses" || url.pathname === "/doctor") {
772
+ await runHostCommandRoute(res, runCommand, {
773
+ command: url.pathname === "/usage" ? "usage" : url.pathname === "/expenses" ? "expenses" : "doctor",
774
+ format: "json"
775
+ }, headOnly);
776
+ return;
777
+ }
778
+ if (url.pathname === "/status/stream") {
779
+ if (headOnly) {
780
+ res.writeHead(200, {
781
+ "Content-Type": "text/event-stream; charset=utf-8",
782
+ "Cache-Control": "no-store",
783
+ "Connection": "keep-alive"
784
+ });
785
+ res.end();
786
+ return;
787
+ }
788
+ res.writeHead(200, {
789
+ "Content-Type": "text/event-stream; charset=utf-8",
790
+ "Cache-Control": "no-store",
791
+ "Connection": "keep-alive"
792
+ });
793
+ const interval = normalizeStatusStreamInterval(url.searchParams.get("interval") ?? options.statusStreamIntervalMs);
794
+ let closed = false;
795
+ let busy = false;
796
+ const sendSnapshot = async () => {
797
+ if (closed || busy)
798
+ return;
799
+ busy = true;
800
+ try {
801
+ sendSseEvent(res, "status", buildHostStatusSnapshot(await readState(), tokenBudget(), usagePricing()));
802
+ }
803
+ catch (err) {
804
+ sendSseEvent(res, "error", { error: err instanceof Error ? err.message : String(err) });
805
+ }
806
+ finally {
807
+ busy = false;
808
+ }
809
+ };
810
+ const timer = setInterval(() => void sendSnapshot(), interval);
811
+ timer.unref?.();
812
+ req.once("close", () => {
813
+ closed = true;
814
+ clearInterval(timer);
815
+ });
816
+ await sendSnapshot();
817
+ return;
818
+ }
819
+ const snapshot = buildHostStatusSnapshot(await readState(), tokenBudget(), usagePricing());
820
+ if (url.pathname === "/status") {
821
+ sendJson(res, 200, snapshot, headOnly);
822
+ return;
823
+ }
824
+ if (url.pathname === "/status.txt") {
825
+ sendText(res, 200, `${formatHostStatusSnapshot(snapshot)}\n`, headOnly);
826
+ return;
827
+ }
828
+ if (url.pathname === "/events") {
829
+ sendJson(res, 200, {
830
+ ok: snapshot.ok,
831
+ events: snapshot.events
832
+ }, headOnly);
833
+ return;
834
+ }
835
+ sendJson(res, 404, { ok: false, error: "not found" }, headOnly);
836
+ }
837
+ catch (err) {
838
+ sendJson(res, 500, { ok: false, error: err instanceof Error ? err.message : String(err) });
839
+ }
840
+ });
841
+ }
842
+ export async function startHostStatusServer(options = {}) {
843
+ const host = normalizeHostServerHost(options.host);
844
+ const port = options.port ?? hostServerPortFromEnv();
845
+ const server = createHostStatusServer(options);
846
+ await new Promise((resolve, reject) => {
847
+ const onError = (err) => {
848
+ server.off("listening", onListening);
849
+ reject(err);
850
+ };
851
+ const onListening = () => {
852
+ server.off("error", onError);
853
+ resolve();
854
+ };
855
+ server.once("error", onError);
856
+ server.once("listening", onListening);
857
+ server.listen(port, host);
858
+ });
859
+ attachHostRunAutoExecutor(server, options);
860
+ return server;
861
+ }
862
+ export async function serveHostStatus(options = {}) {
863
+ const server = await startHostStatusServer(options);
864
+ const address = server.address();
865
+ const host = normalizeHostServerHost(options.host);
866
+ const port = typeof address === "object" && address ? address.port : options.port ?? hostServerPortFromEnv();
867
+ console.log(`host status server listening on http://${host}:${port}`);
868
+ console.log("read-only endpoints: /health, /capabilities, /status, /host/snapshot, /host/stream, /status/stream, /status.txt, /events, /timeline, /timeline/stream, /usage, /expenses, /doctor, /commands");
869
+ console.log("controlled command endpoint: POST /commands/run");
870
+ console.log("host run endpoints: POST /runs/plan, POST /runs/preflight, POST /runs/prepare-layout, GET/POST /runs, GET /runs/stream, GET/PATCH /runs/:id, GET /runs/:id/stream, GET /runs/:id/events, GET /runs/:id/results, GET /runs/:id/heartbeat, GET /runs/:id/meta, POST /runs/:id/execute");
871
+ console.log("host export endpoints: POST /exports/plan, POST /exports");
872
+ console.log("host policy endpoints: GET/POST /policy/:command");
873
+ if (options.executeRuns)
874
+ console.log(`host run auto-executor enabled every ${normalizeExecuteRunsInterval(options.executeRunsIntervalMs)}ms`);
875
+ await new Promise((resolve) => {
876
+ const stop = () => {
877
+ server.close(() => resolve());
878
+ };
879
+ process.once("SIGINT", stop);
880
+ process.once("SIGTERM", stop);
881
+ });
882
+ }
883
+ function attachHostRunAutoExecutor(server, options) {
884
+ if (!options.executeRuns)
885
+ return;
886
+ const runCommand = options.runCommand ?? ((request) => runHostCommand(request, {
887
+ readState: options.readState ?? readRunningState,
888
+ tokenBudget: options.tokenBudget ?? tokenBudgetFromEnv,
889
+ usagePricing: options.usagePricing ?? usagePricingFromEnv,
890
+ recordTimeline: true
891
+ }));
892
+ let busy = false;
893
+ const tick = () => {
894
+ if (busy)
895
+ return;
896
+ busy = true;
897
+ void runCommand({ command: "execute-run", format: "json" }).catch((err) => {
898
+ console.warn(`host run auto-executor failed: ${err instanceof Error ? err.message : String(err)}`);
899
+ }).finally(() => {
900
+ busy = false;
901
+ });
902
+ };
903
+ const timer = setInterval(tick, normalizeExecuteRunsInterval(options.executeRunsIntervalMs));
904
+ timer.unref?.();
905
+ server.once("close", () => clearInterval(timer));
906
+ tick();
907
+ }
908
+ function normalizeExecuteRunsInterval(value) {
909
+ const n = Number(value ?? process.env.KING_AI_HOST_EXECUTE_RUNS_INTERVAL_MS ?? 1000);
910
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 1000;
911
+ }
912
+ function parseOptionalBoolean(value) {
913
+ if (value === null || value === "")
914
+ return undefined;
915
+ const normalized = value.toLowerCase();
916
+ if (normalized === "true" || normalized === "1" || normalized === "yes")
917
+ return true;
918
+ if (normalized === "false" || normalized === "0" || normalized === "no")
919
+ return false;
920
+ return undefined;
921
+ }