@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,3 @@
1
+ import type { ComputerConfig } from "./types.js";
2
+ export declare function loadConfig(): Promise<ComputerConfig | null>;
3
+ export declare function saveConfig(config: ComputerConfig): Promise<void>;
@@ -0,0 +1,20 @@
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { CONFIG_DIR, CONFIG_PATH, LEGACY_CONFIG_PATH } from "./paths.js";
3
+ export async function loadConfig() {
4
+ try {
5
+ return JSON.parse(await readFile(CONFIG_PATH, "utf8"));
6
+ }
7
+ catch {
8
+ try {
9
+ return JSON.parse(await readFile(LEGACY_CONFIG_PATH, "utf8"));
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ }
16
+ export async function saveConfig(config) {
17
+ await mkdir(CONFIG_DIR, { recursive: true });
18
+ await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
19
+ await chmod(CONFIG_PATH, 0o600);
20
+ }
@@ -0,0 +1,11 @@
1
+ export interface ParsedCron {
2
+ minutes: Set<number>;
3
+ hours: Set<number>;
4
+ daysOfMonth: Set<number>;
5
+ months: Set<number>;
6
+ daysOfWeek: Set<number>;
7
+ }
8
+ export declare function parseCron(expression: string): ParsedCron;
9
+ export declare function parseCronField(field: string, min: number, max: number): Set<number>;
10
+ export declare function matchesCron(schedule: ParsedCron, date: Date): boolean;
11
+ export declare function cronMatches(expression: string, date?: Date): boolean;
@@ -0,0 +1,65 @@
1
+ export function parseCron(expression) {
2
+ const parts = expression.trim().split(/\s+/);
3
+ if (parts.length !== 5) {
4
+ throw new Error(`Invalid cron expression "${expression}": expected 5 fields, got ${parts.length}`);
5
+ }
6
+ return {
7
+ minutes: parseCronField(parts[0] ?? "", 0, 59),
8
+ hours: parseCronField(parts[1] ?? "", 0, 23),
9
+ daysOfMonth: parseCronField(parts[2] ?? "", 1, 31),
10
+ months: parseCronField(parts[3] ?? "", 1, 12),
11
+ daysOfWeek: parseCronField(parts[4] ?? "", 0, 6)
12
+ };
13
+ }
14
+ export function parseCronField(field, min, max) {
15
+ const values = new Set();
16
+ for (const part of field.split(",")) {
17
+ if (!part)
18
+ throw new Error("Invalid empty cron field part");
19
+ addCronPart(values, part, min, max);
20
+ }
21
+ return values;
22
+ }
23
+ function addCronPart(values, part, min, max) {
24
+ const [rangePart = "", stepPart] = part.split("/");
25
+ const step = stepPart == null ? 1 : Number.parseInt(stepPart, 10);
26
+ if (!Number.isInteger(step) || step <= 0)
27
+ throw new Error(`Invalid step in cron field: "${part}"`);
28
+ let start = min;
29
+ let end = max;
30
+ if (rangePart !== "*") {
31
+ if (rangePart.includes("-")) {
32
+ const [rawStart, rawEnd] = rangePart.split("-");
33
+ start = parseCronNumber(rawStart, min, max, part);
34
+ end = parseCronNumber(rawEnd, min, max, part);
35
+ if (start > end)
36
+ throw new Error(`Invalid range in cron field: "${part}"`);
37
+ }
38
+ else {
39
+ start = parseCronNumber(rangePart, min, max, part);
40
+ end = stepPart == null ? start : max;
41
+ }
42
+ }
43
+ for (let value = start; value <= end; value += step) {
44
+ if (value < min || value > max)
45
+ throw new Error(`Invalid value in cron field: "${part}" (valid range: ${min}-${max})`);
46
+ values.add(value);
47
+ }
48
+ }
49
+ function parseCronNumber(value, min, max, part) {
50
+ const parsed = Number.parseInt(value ?? "", 10);
51
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
52
+ throw new Error(`Invalid value in cron field: "${part}" (valid range: ${min}-${max})`);
53
+ }
54
+ return parsed;
55
+ }
56
+ export function matchesCron(schedule, date) {
57
+ return (schedule.minutes.has(date.getMinutes()) &&
58
+ schedule.hours.has(date.getHours()) &&
59
+ schedule.daysOfMonth.has(date.getDate()) &&
60
+ schedule.months.has(date.getMonth() + 1) &&
61
+ schedule.daysOfWeek.has(date.getDay()));
62
+ }
63
+ export function cronMatches(expression, date = new Date()) {
64
+ return matchesCron(parseCron(expression), date);
65
+ }
@@ -0,0 +1,36 @@
1
+ import type { EngineId } from "./types.js";
2
+ export declare function anyRunnerBusy(runners: Iterable<{
3
+ isBusy: boolean;
4
+ }>): boolean;
5
+ export declare function shouldExitForUpdate(args: {
6
+ updateReady: boolean;
7
+ shuttingDown: boolean;
8
+ anyBusy: boolean;
9
+ }): boolean;
10
+ export declare function installProcessErrorLogging(): void;
11
+ export declare function resolveHostName(base: string | undefined | null, platform: NodeJS.Platform, platformNames?: string[]): string;
12
+ export declare function detectHostName(): Promise<string>;
13
+ export declare function missingEngineMessage(): string;
14
+ export interface PairLocator {
15
+ code: string;
16
+ serverUrl?: string;
17
+ tenantId?: string;
18
+ }
19
+ export declare function parsePairLocator(value: string): PairLocator;
20
+ export interface DoctorProbe {
21
+ ok: boolean;
22
+ detail?: string;
23
+ }
24
+ export interface DoctorResult {
25
+ id: EngineId;
26
+ installed: boolean;
27
+ path?: string;
28
+ big?: DoctorProbe;
29
+ small?: DoctorProbe;
30
+ }
31
+ export declare function doctorExitCode(results: DoctorResult[]): number;
32
+ export declare function formatDoctorReport(results: DoctorResult[], version?: string): string;
33
+ export declare function collectDoctorResults(): Promise<DoctorResult[]>;
34
+ export declare function doPair(code: string, serverUrl: string, preferredEngine?: EngineId, tenantId?: string): Promise<void>;
35
+ export declare function runDoctor(): Promise<void>;
36
+ export declare function doRun(serverOverride?: string, tenantOverride?: string): Promise<void>;
@@ -0,0 +1,373 @@
1
+ import { hostname } from "node:os";
2
+ import { mkdtemp } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { execFile } from "node:child_process";
7
+ import { api, tenantHeader } from "./api.js";
8
+ import { loadConfig, saveConfig } from "./config.js";
9
+ import { detectEngines, getAdapter } from "./engine.js";
10
+ import { CURRENT_VERSION, HEARTBEAT_PATH } from "./paths.js";
11
+ import { checkForUpdate, recordRunningState, rotateLogsIfNeeded, writeRunningState } from "./service.js";
12
+ import { AgentRunner } from "./runner.js";
13
+ import { engineInstallAdvice, engineRemediationAdvice, formatRemediationBlock } from "./remediation.js";
14
+ import { detectLocalCapabilities } from "./workspace.js";
15
+ import { normalizeAgentLifecycle, shouldHostAgent } from "./lifecycle.js";
16
+ import { FileHeartbeat } from "./heartbeat.js";
17
+ import { validateAgentConfig } from "./agent-config-validation.js";
18
+ const AGENT_POLL_MS = Number(process.env.KING_AI_AGENT_POLL_MS) || 5_000;
19
+ const HEARTBEAT_MS = Number(process.env.KING_AI_HEARTBEAT_MS) || 30_000;
20
+ const SHUTDOWN_GRACE_MS = Number(process.env.KING_AI_SHUTDOWN_GRACE_MS) || 15_000;
21
+ const UPDATE_CHECK_MS = Number(process.env.KING_AI_UPDATE_CHECK_MS) || 6 * 60 * 60 * 1000;
22
+ const IDLE_UPDATE_CHECK_MS = Number(process.env.KING_AI_IDLE_UPDATE_CHECK_MS) || 30_000;
23
+ const LOG_ROTATE_MS = Number(process.env.KING_AI_LOG_ROTATE_MS) || 5 * 60 * 1000;
24
+ const SUPERVISED = process.env.KING_AI_SUPERVISED === "1";
25
+ const execFileP = promisify(execFile);
26
+ export function anyRunnerBusy(runners) {
27
+ for (const runner of runners) {
28
+ if (runner.isBusy)
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ export function shouldExitForUpdate(args) {
34
+ return args.updateReady && !args.shuttingDown && !args.anyBusy;
35
+ }
36
+ export function installProcessErrorLogging() {
37
+ process.on("unhandledRejection", (reason) => {
38
+ console.error("[king-ai] unhandledRejection (kept alive):", reason instanceof Error ? reason.stack || reason.message : reason);
39
+ });
40
+ process.on("uncaughtException", (err) => {
41
+ console.error("[king-ai] uncaughtException (kept alive):", err instanceof Error ? err.stack || err.message : err);
42
+ });
43
+ }
44
+ export function resolveHostName(base, platform, platformNames = []) {
45
+ if (base && base.toLowerCase() !== "localhost")
46
+ return base;
47
+ if (platform === "darwin") {
48
+ const found = platformNames.map((name) => name.trim()).find(Boolean);
49
+ if (found)
50
+ return found;
51
+ }
52
+ return base || "My computer";
53
+ }
54
+ export async function detectHostName() {
55
+ const base = hostname();
56
+ const platformNames = [];
57
+ if (process.platform === "darwin") {
58
+ for (const key of ["ComputerName", "LocalHostName"]) {
59
+ try {
60
+ const { stdout } = await execFileP("scutil", ["--get", key]);
61
+ platformNames.push(stdout);
62
+ }
63
+ catch {
64
+ // Keep fallback behavior portable.
65
+ }
66
+ }
67
+ }
68
+ return resolveHostName(base, process.platform, platformNames);
69
+ }
70
+ export function missingEngineMessage() {
71
+ return [
72
+ "no supported local agent engine found on PATH",
73
+ "",
74
+ "Install and sign in to at least one of:",
75
+ " - Claude Code: install the `claude` CLI, then run `claude` once to sign in",
76
+ " - Codex: install the `codex` CLI, then run `codex` once to sign in",
77
+ "",
78
+ "After that, rerun:",
79
+ " king-ai agent computer --pair <code>"
80
+ ].join("\n");
81
+ }
82
+ export function parsePairLocator(value) {
83
+ const trimmed = value.trim();
84
+ if (!trimmed.startsWith("king-ai://pair?"))
85
+ return { code: trimmed };
86
+ const url = new URL(trimmed);
87
+ const code = url.searchParams.get("code")?.trim();
88
+ const serverUrl = url.searchParams.get("server")?.trim().replace(/\/+$/, "");
89
+ const tenantId = url.searchParams.get("tenant")?.trim() || undefined;
90
+ if (!code)
91
+ throw new Error("pair locator is missing code");
92
+ if (serverUrl)
93
+ new URL(serverUrl);
94
+ return { code, serverUrl: serverUrl || undefined, tenantId };
95
+ }
96
+ export function doctorExitCode(results) {
97
+ return results.some((result) => result.big?.ok && result.small?.ok) ? 0 : 1;
98
+ }
99
+ export function formatDoctorReport(results, version = CURRENT_VERSION) {
100
+ const lines = [
101
+ `king-ai ${version} engine doctor`,
102
+ "probing local engines (big brain = main reasoning, small brain = triage cerebellum)...",
103
+ ""
104
+ ];
105
+ let anyUsable = false;
106
+ for (const result of results) {
107
+ if (!result.installed) {
108
+ lines.push(`x ${result.id} - not found on PATH`);
109
+ for (const line of formatRemediationBlock(engineInstallAdvice(result.id)).split("\n"))
110
+ lines.push(` ${line}`);
111
+ lines.push("");
112
+ continue;
113
+ }
114
+ lines.push(`o ${result.id} - ${result.path ?? "on PATH"}`);
115
+ for (const [label, probe] of [
116
+ ["big brain ", result.big],
117
+ ["small brain", result.small]
118
+ ]) {
119
+ if (!probe)
120
+ continue;
121
+ if (probe.ok) {
122
+ lines.push(` ok ${label}`);
123
+ }
124
+ else {
125
+ const detail = probe.detail ?? "unknown failure";
126
+ lines.push(` x ${label} FAILED: ${detail}`);
127
+ for (const line of formatRemediationBlock(engineRemediationAdvice(result.id, detail)).split("\n"))
128
+ lines.push(` ${line}`);
129
+ }
130
+ }
131
+ if (result.big?.ok && result.small?.ok)
132
+ anyUsable = true;
133
+ lines.push("");
134
+ }
135
+ if (!anyUsable) {
136
+ lines.push("x no engine has BOTH brains healthy - this machine cannot currently run a BYOA agent.");
137
+ lines.push(" Fix one engine above, then re-run: king-ai agent computer --doctor");
138
+ }
139
+ return lines.join("\n").trimEnd();
140
+ }
141
+ export async function collectDoctorResults() {
142
+ const engines = await detectEngines();
143
+ const results = [];
144
+ for (const id of ["claude", "codex"]) {
145
+ if (!engines.includes(id)) {
146
+ results.push({ id, installed: false });
147
+ continue;
148
+ }
149
+ const adapter = getAdapter(id);
150
+ const cwd = await mkdtemp(join(tmpdir(), "king-doctor-"));
151
+ const controller = new AbortController();
152
+ const timeout = setTimeout(() => controller.abort(), 60_000);
153
+ try {
154
+ const big = await adapter.probe({ cwd, env: process.env, tier: "big", signal: controller.signal });
155
+ const small = await adapter.probe({ cwd, env: process.env, tier: "small", signal: controller.signal });
156
+ results.push({
157
+ id,
158
+ installed: true,
159
+ path: adapter.bin,
160
+ big: { ok: !big.error, detail: big.error },
161
+ small: { ok: !small.error, detail: small.error }
162
+ });
163
+ }
164
+ finally {
165
+ clearTimeout(timeout);
166
+ }
167
+ }
168
+ return results;
169
+ }
170
+ export async function doPair(code, serverUrl, preferredEngine, tenantId) {
171
+ const locator = parsePairLocator(code);
172
+ const resolvedServerUrl = locator.serverUrl ?? serverUrl;
173
+ const resolvedTenantId = locator.tenantId ?? tenantId;
174
+ const detected = await detectEngines();
175
+ if (detected.length === 0)
176
+ throw new Error(missingEngineMessage());
177
+ const engines = preferredEngine ? [preferredEngine, ...detected.filter((id) => id !== preferredEngine)] : detected;
178
+ if (preferredEngine && !detected.includes(preferredEngine)) {
179
+ throw new Error(`--engine ${preferredEngine} chosen, but ${preferredEngine} is not installed on this machine. Installed: ${detected.join(", ") || "none"}.`);
180
+ }
181
+ const paired = await api(resolvedServerUrl, "/api/computers/pair", {
182
+ method: "POST",
183
+ headers: tenantHeader(resolvedTenantId),
184
+ body: JSON.stringify({ code: locator.code, hostName: await detectHostName(), engines, version: CURRENT_VERSION, capabilities: detectLocalCapabilities() })
185
+ });
186
+ const savedTenantId = paired.tenantId ?? resolvedTenantId;
187
+ await saveConfig({ serverUrl: resolvedServerUrl, computerId: paired.computerId, deviceToken: paired.deviceToken, ...(savedTenantId ? { tenantId: savedTenantId } : {}) });
188
+ console.log(`paired as ${paired.computerId}${savedTenantId ? ` tenant=${savedTenantId}` : ""}; default engine: ${engines[0] ?? "none"}; available engines: ${engines.join(", ") || "none"}`);
189
+ }
190
+ export async function runDoctor() {
191
+ const results = await collectDoctorResults();
192
+ console.log(formatDoctorReport(results));
193
+ if (doctorExitCode(results) !== 0)
194
+ process.exitCode = 1;
195
+ }
196
+ export async function doRun(serverOverride, tenantOverride) {
197
+ installProcessErrorLogging();
198
+ const cfg = await loadConfig();
199
+ if (!cfg)
200
+ throw new Error("not paired. Run: king-ai agent computer --pair <code> --server <url>");
201
+ const runtimeCfg = { ...cfg, serverUrl: serverOverride ?? cfg.serverUrl, tenantId: tenantOverride ?? cfg.tenantId };
202
+ const available = await detectEngines();
203
+ if (available.length === 0)
204
+ throw new Error(missingEngineMessage());
205
+ console.log(`king-ai ${CURRENT_VERSION} starting ${runtimeCfg.computerId} @ ${runtimeCfg.serverUrl}`);
206
+ const fileHeartbeat = new FileHeartbeat(HEARTBEAT_PATH, {
207
+ pid: process.pid,
208
+ runId: `${runtimeCfg.computerId}-${Date.now()}`,
209
+ version: CURRENT_VERSION,
210
+ computerId: runtimeCfg.computerId,
211
+ serverUrl: runtimeCfg.serverUrl
212
+ });
213
+ fileHeartbeat.write();
214
+ const capabilities = detectLocalCapabilities();
215
+ await writeRunningState({
216
+ serverUrl: runtimeCfg.serverUrl,
217
+ computerId: runtimeCfg.computerId,
218
+ capabilities,
219
+ agents: [],
220
+ event: { at: new Date().toISOString(), kind: "daemon.started", detail: `${runtimeCfg.computerId} @ ${runtimeCfg.serverUrl}` }
221
+ });
222
+ await rotateLogsIfNeeded();
223
+ const runners = new Map();
224
+ const publishRunningAgents = (extra = []) => {
225
+ recordRunningState({
226
+ capabilities: detectLocalCapabilities(),
227
+ agents: [
228
+ ...[...runners.values()].map((runner) => runner.runningState()),
229
+ ...extra
230
+ ]
231
+ });
232
+ };
233
+ const sync = async () => {
234
+ let agents;
235
+ try {
236
+ agents = await api(runtimeCfg.serverUrl, "/api/computers/me/agents", {
237
+ headers: { Authorization: `Bearer ${runtimeCfg.deviceToken}`, ...tenantHeader(runtimeCfg.tenantId) }
238
+ });
239
+ }
240
+ catch (err) {
241
+ console.warn(`agent sync failed: ${err instanceof Error ? err.message : String(err)}`);
242
+ return;
243
+ }
244
+ const disabledAgents = [];
245
+ for (const agent of agents) {
246
+ const lifecycle = normalizeAgentLifecycle(agent.lifecycle);
247
+ if (!shouldHostAgent(lifecycle)) {
248
+ const existing = runners.get(agent.id);
249
+ if (existing) {
250
+ existing.stop();
251
+ runners.delete(agent.id);
252
+ }
253
+ disabledAgents.push({ ...agent, lifecycle });
254
+ recordRunningState({
255
+ event: { at: new Date().toISOString(), kind: "agent.disabled", detail: agent.id }
256
+ });
257
+ continue;
258
+ }
259
+ const engine = agent.engine && available.includes(agent.engine) ? agent.engine : available[0];
260
+ const existing = runners.get(agent.id);
261
+ if (existing?.configMatches(agent, engine))
262
+ continue;
263
+ existing?.stop();
264
+ const runner = new AgentRunner(runtimeCfg, { ...agent, lifecycle }, engine, available, () => publishRunningAgents());
265
+ runners.set(agent.id, runner);
266
+ console.log(`hosting ${agent.name} (${agent.id}) on ${engine} lifecycle=${lifecycle}`);
267
+ recordRunningState({
268
+ event: { at: new Date().toISOString(), kind: "agent.hosting", detail: `${agent.id} on ${engine} lifecycle=${lifecycle}` }
269
+ });
270
+ await runner.start();
271
+ }
272
+ const live = new Set(agents.filter((a) => shouldHostAgent(normalizeAgentLifecycle(a.lifecycle))).map((a) => a.id));
273
+ for (const [id, runner] of runners) {
274
+ if (!live.has(id)) {
275
+ runner.stop();
276
+ runners.delete(id);
277
+ recordRunningState({
278
+ event: { at: new Date().toISOString(), kind: "agent.removed", detail: id }
279
+ });
280
+ }
281
+ }
282
+ publishRunningAgents(disabledAgents.map((agent) => ({
283
+ id: agent.id,
284
+ name: agent.name,
285
+ engine: agent.engine ?? "disabled",
286
+ lifecycle: "disabled",
287
+ status: "disabled",
288
+ configWarnings: validateAgentConfig(agent, agent.engine ?? "disabled", available),
289
+ updatedAt: new Date().toISOString()
290
+ })));
291
+ recordRunningState({
292
+ lastSyncAt: new Date().toISOString()
293
+ });
294
+ };
295
+ const heartbeat = () => {
296
+ const at = new Date().toISOString();
297
+ const capabilities = detectLocalCapabilities();
298
+ return fetch(`${runtimeCfg.serverUrl}/api/computers/heartbeat`, {
299
+ method: "POST",
300
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${runtimeCfg.deviceToken}`, ...tenantHeader(runtimeCfg.tenantId) },
301
+ body: JSON.stringify({ version: CURRENT_VERSION, capabilities })
302
+ }).then(() => {
303
+ fileHeartbeat.tick();
304
+ recordRunningState({ lastHeartbeatAt: at, capabilities });
305
+ }).catch(() => {
306
+ fileHeartbeat.tick();
307
+ recordRunningState({ event: { at, kind: "heartbeat.failed", detail: runtimeCfg.serverUrl } });
308
+ });
309
+ };
310
+ await heartbeat();
311
+ await sync();
312
+ const syncTimer = setInterval(() => void sync(), AGENT_POLL_MS);
313
+ const heartbeatTimer = setInterval(() => void heartbeat(), HEARTBEAT_MS);
314
+ const logRotateTimer = setInterval(() => void rotateLogsIfNeeded(), LOG_ROTATE_MS);
315
+ logRotateTimer.unref?.();
316
+ let updateTimer = null;
317
+ let idleUpdateTimer = null;
318
+ let updateReady = false;
319
+ let shuttingDown = false;
320
+ const shutdown = async (why) => {
321
+ if (shuttingDown)
322
+ return;
323
+ shuttingDown = true;
324
+ clearInterval(syncTimer);
325
+ clearInterval(heartbeatTimer);
326
+ clearInterval(logRotateTimer);
327
+ if (updateTimer)
328
+ clearInterval(updateTimer);
329
+ if (idleUpdateTimer)
330
+ clearInterval(idleUpdateTimer);
331
+ for (const runner of runners.values())
332
+ runner.beginStop();
333
+ const deadline = Date.now() + SHUTDOWN_GRACE_MS;
334
+ if (anyRunnerBusy(runners.values())) {
335
+ console.log(`${why}: waiting up to ${Math.round(SHUTDOWN_GRACE_MS / 1000)}s for in-flight turn(s) to finish`);
336
+ }
337
+ while (anyRunnerBusy(runners.values()) && Date.now() < deadline) {
338
+ await new Promise((resolve) => setTimeout(resolve, 500));
339
+ }
340
+ for (const runner of runners.values())
341
+ runner.stop();
342
+ console.log(`shutting down (${why})`);
343
+ process.exit(0);
344
+ };
345
+ const exitForUpdateWhenIdle = () => {
346
+ if (!shouldExitForUpdate({ updateReady, shuttingDown, anyBusy: anyRunnerBusy(runners.values()) }))
347
+ return;
348
+ void shutdown("auto-update");
349
+ };
350
+ const runUpdateCheck = () => {
351
+ void checkForUpdate().then((latest) => {
352
+ if (!latest)
353
+ return;
354
+ if (SUPERVISED) {
355
+ if (!updateReady) {
356
+ updateReady = true;
357
+ console.log(`king-ai ${latest} available; will restart when idle`);
358
+ }
359
+ exitForUpdateWhenIdle();
360
+ }
361
+ else {
362
+ console.log(`king-ai ${latest} available. Restart to update, or install the background service for automatic restarts.`);
363
+ }
364
+ });
365
+ };
366
+ updateTimer = setInterval(runUpdateCheck, UPDATE_CHECK_MS);
367
+ updateTimer.unref?.();
368
+ idleUpdateTimer = setInterval(exitForUpdateWhenIdle, IDLE_UPDATE_CHECK_MS);
369
+ idleUpdateTimer.unref?.();
370
+ setTimeout(runUpdateCheck, 60_000).unref?.();
371
+ process.on("SIGINT", () => void shutdown("SIGINT"));
372
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
373
+ }
@@ -0,0 +1,32 @@
1
+ import type { EngineAdapter, EngineId } from "./types.js";
2
+ export declare function splitExtraArgs(raw: string | undefined): string[];
3
+ export declare function formatEngineLogLine(engine: EngineId, line: string): string | null;
4
+ export declare function binOnPath(bin: string): Promise<boolean>;
5
+ export declare function personaHeader(persona: {
6
+ id: string;
7
+ name: string;
8
+ role?: string;
9
+ }): string;
10
+ export declare function claudeStreamUserMessage(text: string): string;
11
+ export interface CodexAppEventState {
12
+ activeTurnId: string | null;
13
+ steerGate: boolean;
14
+ }
15
+ export interface CodexAppEventResult {
16
+ logs: string[];
17
+ activeTurnId: string | null;
18
+ steerGate: boolean;
19
+ turnCompletedError?: string;
20
+ usage?: unknown;
21
+ threadId?: string;
22
+ }
23
+ export declare function reduceCodexAppEvent(state: CodexAppEventState, msg: Record<string, unknown>): CodexAppEventResult;
24
+ export declare const ADAPTERS: Record<EngineId, EngineAdapter>;
25
+ export declare function getAdapter(id: EngineId): EngineAdapter;
26
+ export declare function detectEngines(): Promise<EngineId[]>;
27
+ export declare function parseTriage(text: string): {
28
+ actionable: boolean;
29
+ reason?: string;
30
+ promptNote?: string;
31
+ responseMode?: "me" | "each" | "one-of-us";
32
+ } | null;