@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
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # king-ai
2
+
3
+ King AI is a local BYOA multi-agent collaboration system. It connects remote agent runtimes to Claude and Codex running on your own machine, then layers team roles, task routing, review handoffs, claims, and human decision gates on top of those local engines.
4
+
5
+ Repository: `sukbearai/king-ai`.
6
+
7
+ Packages:
8
+
9
+ - CLI: `@suwujs/king-ai`, exposing the `king-ai` command.
10
+ - GUI worker app: `@king-ai/gui-worker`.
11
+
12
+ The primary CLI command is `king-ai`. Local runtime state lives under `~/.king-ai`; existing `~/.king/computer.json` is read only as a one-time upgrade fallback when the new config has not been written yet.
13
+
14
+ ## Install And Develop
15
+
16
+ ```sh
17
+ pnpm install
18
+ pnpm verify
19
+ pnpm dev -- agent computer --doctor
20
+ ```
21
+
22
+ The GUI worker can be run locally with:
23
+
24
+ ```sh
25
+ pnpm gui:dev
26
+ ```
27
+
28
+ ## Rename And Migration Notes
29
+
30
+ This project was renamed to `king-ai`. New installs and generated commands should use only `king-ai`, `@suwujs/king-ai`, `@king-ai/gui-worker`, `KING_AI_*`, `king-ai://`, and `~/.king-ai`.
31
+
32
+ The old `king` command is not exposed as a bin alias. The only retained legacy behavior is reading `~/.king/computer.json` when the new `~/.king-ai/computer.json` has not been written yet, so an already paired computer can migrate without losing its token.
33
+
34
+ ## Architecture
35
+
36
+ ```text
37
+ +------------------------------------------------+
38
+ | Remote Runtime Server |
39
+ | pair / roster / inbox / wake-stream / status |
40
+ +------------------------+-----------------------+
41
+ |
42
+ v
43
+ +--------------------------------------------------------------------------------+
44
+ | Local Machine |
45
+ | |
46
+ | +----------------------+ +----------------------------------------+ |
47
+ | | king-ai CLI |----->| King AI daemon | |
48
+ | | status / run / logs | | pairing, heartbeat, SSE, host SDK | |
49
+ | +----------------------+ +-------------------+--------------------+ |
50
+ | | |
51
+ | v |
52
+ | +----------------------------------------+ |
53
+ | | agent runner | |
54
+ | | triage, prompts, session reuse | |
55
+ | +-------------------+--------------------+ |
56
+ | | |
57
+ | +-------------------------+---------------------+ |
58
+ | | | |
59
+ | v v |
60
+ | +--------------------+ +--------------------+ |
61
+ | | Claude CLI | | Codex CLI | |
62
+ | +---------+----------+ +----------+---------+ |
63
+ | | | |
64
+ | +------------------+-------------------+ |
65
+ | | |
66
+ | v |
67
+ | +----------------------------------------+ |
68
+ | | per-agent home | |
69
+ | | skills, runtime shim, state files | |
70
+ | +-------------------+--------------------+ |
71
+ | | |
72
+ | v |
73
+ | +----------------------------------------+ |
74
+ | | allowed local workspaces | |
75
+ | | source repos, worktrees, artifacts | |
76
+ | +----------------------------------------+ |
77
+ +--------------------------------------------------------------------------------+
78
+ ```
79
+
80
+ ## Multi-Role Collaboration
81
+
82
+ King AI models agent work as a small software team, not just a single chat bot. A conversation can run in single-agent, team, or custom team mode with a coordinator and selected team agents.
83
+
84
+ The default role templates are `planner`, `builder`, `reviewer`, `tester`, `ops`, `researcher`, `doc-writer`, and `summarizer`. Each role carries responsibilities, capability hints, handoff policy, and permission rules. Built-in workflow scenarios such as `repo-takeover`, `bug-investigation`, `product-design`, `release-check`, and `research-brief` materialize those roles into assigned tasks with acceptance criteria.
85
+
86
+ Routing is capability-first when possible. New work can be assigned to the role whose capabilities best match the request; completed work can automatically create a review, handoff, or human decision card. Routing modes include `one-of-us`, `each`, `review-required`, and `human-decision`.
87
+
88
+ Runtime coordination uses shared conversation state. Agents are prompted to check `king-ai glance` before posting, use cards or claims before taking shared work, avoid duplicate replies, and trust current board state over memory. This keeps multiple local Claude/Codex-backed teammates from racing on the same deliverable.
89
+
90
+ ## Collaboration Governance
91
+
92
+ King AI is automation-first. Team roles, permission rules, and human-decision gates are a collaboration governance layer for routing, audit, and handoff discipline; they are not the primary security boundary.
93
+
94
+ Host commands apply role governance only when an actor role is supplied with `--role` or `KING_AI_TEAM_ROLE`. Without a role, trusted local automation can continue without being blocked. `human-decision` records a decision card and asks for an approval marker, but the trusted local operator can bypass role governance for unattended automation by not supplying a role.
95
+
96
+ Use OS account isolation, local workspace boundaries, runtime tokens, host command allowlists, destructive-command confirmation, and per-agent homes as the security boundary.
@@ -0,0 +1,9 @@
1
+ import type { AgentConfig, EngineId } from "./types.js";
2
+ export type AgentConfigWarningCode = "idle-cached-without-resume" | "missing-preferred-engine" | "unknown-lifecycle";
3
+ export interface AgentConfigWarning {
4
+ code: AgentConfigWarningCode;
5
+ severity: "warning";
6
+ summary: string;
7
+ detail: string;
8
+ }
9
+ export declare function validateAgentConfig(agent: AgentConfig, effectiveEngine: EngineId | string, availableEngines?: EngineId[]): AgentConfigWarning[];
@@ -0,0 +1,30 @@
1
+ import { normalizeAgentLifecycle } from "./lifecycle.js";
2
+ export function validateAgentConfig(agent, effectiveEngine, availableEngines = []) {
3
+ const warnings = [];
4
+ const lifecycle = normalizeAgentLifecycle(agent.lifecycle);
5
+ if (agent.lifecycle && agent.lifecycle !== lifecycle) {
6
+ warnings.push({
7
+ code: "unknown-lifecycle",
8
+ severity: "warning",
9
+ summary: `unknown lifecycle ${agent.lifecycle} normalized to ${lifecycle}`,
10
+ detail: "The daemon only understands on-demand, 24/7, idle_cached, and disabled."
11
+ });
12
+ }
13
+ if (agent.engine && !availableEngines.includes(agent.engine)) {
14
+ warnings.push({
15
+ code: "missing-preferred-engine",
16
+ severity: "warning",
17
+ summary: `${agent.engine} is requested but not installed on this machine`,
18
+ detail: `The daemon will use ${effectiveEngine || "the first available engine"} until ${agent.engine} is available.`
19
+ });
20
+ }
21
+ if (lifecycle === "idle_cached" && effectiveEngine !== "claude") {
22
+ warnings.push({
23
+ code: "idle-cached-without-resume",
24
+ severity: "warning",
25
+ summary: "idle_cached has engine-specific resume semantics",
26
+ detail: "Claude uses CLI session resume. Codex may reuse an app-server thread when available, but the daemon treats it as best-effort and falls back to a fresh thread."
27
+ });
28
+ }
29
+ return warnings;
30
+ }
@@ -0,0 +1,4 @@
1
+ export declare function api<T>(serverUrl: string, path: string, init?: RequestInit): Promise<T>;
2
+ export declare function tenantHeader(tenantId?: string): Record<string, string>;
3
+ export declare function runtimePost<T>(serverUrl: string, path: string, token: string, body: unknown, tenantId?: string): Promise<T | null>;
4
+ export declare function runtimeGet<T>(serverUrl: string, path: string, token: string, tenantId?: string): Promise<T | null>;
@@ -0,0 +1,48 @@
1
+ export async function api(serverUrl, path, init = {}) {
2
+ const res = await fetch(`${serverUrl}${path}`, {
3
+ ...init,
4
+ headers: {
5
+ "Content-Type": "application/json",
6
+ ...(init.headers ?? {})
7
+ }
8
+ });
9
+ if (!res.ok) {
10
+ const body = await res.text().catch(() => "");
11
+ throw new Error(`${init.method ?? "GET"} ${path} -> HTTP ${res.status} ${body.slice(0, 200)}`);
12
+ }
13
+ return (await res.json());
14
+ }
15
+ export function tenantHeader(tenantId) {
16
+ return tenantId ? { "X-King-AI-Tenant": tenantId } : {};
17
+ }
18
+ export async function runtimePost(serverUrl, path, token, body, tenantId) {
19
+ try {
20
+ const res = await fetch(`${serverUrl}/runtime${path}`, {
21
+ method: "POST",
22
+ headers: {
23
+ "Content-Type": "application/json",
24
+ Authorization: `Bearer ${token}`,
25
+ ...tenantHeader(tenantId)
26
+ },
27
+ body: JSON.stringify(body)
28
+ });
29
+ return res.ok ? (await res.json().catch(() => null)) : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ export async function runtimeGet(serverUrl, path, token, tenantId) {
36
+ try {
37
+ const res = await fetch(`${serverUrl}/runtime${path}`, {
38
+ headers: {
39
+ Authorization: `Bearer ${token}`,
40
+ ...tenantHeader(tenantId)
41
+ }
42
+ });
43
+ return res.ok ? (await res.json().catch(() => null)) : null;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
@@ -0,0 +1,45 @@
1
+ export type AttachmentKind = "image" | "file" | "audio" | "video" | "unknown";
2
+ export type AttachmentDecision = "accepted" | "rejected" | "skipped";
3
+ export interface RuntimeAttachmentInput {
4
+ id?: string;
5
+ name?: string;
6
+ kind?: string;
7
+ mime?: string;
8
+ size?: number;
9
+ sha256?: string;
10
+ url?: string;
11
+ filePath?: string;
12
+ required?: boolean;
13
+ source?: string;
14
+ }
15
+ export interface RuntimeAttachment {
16
+ id: string;
17
+ name: string;
18
+ kind: AttachmentKind;
19
+ mime: string;
20
+ size: number;
21
+ sha256?: string;
22
+ url?: string;
23
+ filePath?: string;
24
+ source?: string;
25
+ required: boolean;
26
+ decision: AttachmentDecision;
27
+ rejectionReason?: string;
28
+ localPath?: string;
29
+ }
30
+ export interface AttachmentPolicy {
31
+ maxCount: number;
32
+ maxBytes: number;
33
+ maxFileBytes: number;
34
+ imageMaxBytes: number;
35
+ allowedImageMimes: string[];
36
+ allowedFileMimes: string[];
37
+ }
38
+ export declare const DEFAULT_ATTACHMENT_POLICY: AttachmentPolicy;
39
+ export declare function normalizeRuntimeAttachments(input: unknown, policy?: Partial<AttachmentPolicy>): RuntimeAttachment[];
40
+ export declare function requiredAttachmentsRejected(attachments: readonly RuntimeAttachment[]): RuntimeAttachment[];
41
+ export declare function formatAttachmentPrompt(attachments: readonly RuntimeAttachment[]): string;
42
+ export declare function mediaCacheDir(root?: string): string;
43
+ export declare function cacheLocalAttachment(attachment: RuntimeAttachment, root?: string): Promise<RuntimeAttachment>;
44
+ export declare function cacheLocalAttachments(attachments: readonly RuntimeAttachment[], root?: string): Promise<RuntimeAttachment[]>;
45
+ export declare function gcMediaCache(root?: string, maxAgeMs?: number): Promise<number>;
@@ -0,0 +1,322 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
4
+ import { basename, join, resolve } from "node:path";
5
+ import { CONFIG_DIR } from "./paths.js";
6
+ export const DEFAULT_ATTACHMENT_POLICY = {
7
+ maxCount: 10,
8
+ maxBytes: 100 * 1024 * 1024,
9
+ maxFileBytes: 25 * 1024 * 1024,
10
+ imageMaxBytes: 25 * 1024 * 1024,
11
+ allowedImageMimes: ["image/jpeg", "image/png", "image/webp", "image/gif"],
12
+ allowedFileMimes: [
13
+ "application/javascript",
14
+ "application/json",
15
+ "application/pdf",
16
+ "application/typescript",
17
+ "application/zip",
18
+ "text/css",
19
+ "text/csv",
20
+ "text/html",
21
+ "text/javascript",
22
+ "text/jsx",
23
+ "text/markdown",
24
+ "text/plain",
25
+ "text/tsv",
26
+ "text/tsx",
27
+ "text/xml",
28
+ "application/xml",
29
+ "application/xhtml+xml"
30
+ ]
31
+ };
32
+ const SAFE_EXT_BY_MIME = {
33
+ "application/javascript": "js",
34
+ "application/json": "json",
35
+ "application/pdf": "pdf",
36
+ "application/typescript": "ts",
37
+ "application/zip": "zip",
38
+ "image/gif": "gif",
39
+ "image/jpeg": "jpg",
40
+ "image/png": "png",
41
+ "image/webp": "webp",
42
+ "text/css": "css",
43
+ "text/csv": "csv",
44
+ "text/html": "html",
45
+ "text/javascript": "js",
46
+ "text/jsx": "jsx",
47
+ "text/markdown": "md",
48
+ "text/plain": "txt",
49
+ "text/tsv": "tsv",
50
+ "text/tsx": "tsx",
51
+ "text/xml": "xml",
52
+ "application/xml": "xml",
53
+ "application/xhtml+xml": "html"
54
+ };
55
+ export function normalizeRuntimeAttachments(input, policy = {}) {
56
+ const merged = { ...DEFAULT_ATTACHMENT_POLICY, ...policy };
57
+ const rows = Array.isArray(input) ? input : [];
58
+ let acceptedCount = 0;
59
+ let acceptedBytes = 0;
60
+ return rows.map((row, index) => {
61
+ const attachment = normalizeRuntimeAttachment(row, index);
62
+ const rejection = attachmentDecision(attachment, merged, acceptedCount, acceptedBytes);
63
+ if (rejection) {
64
+ return { ...attachment, decision: rejection.decision, rejectionReason: rejection.reason };
65
+ }
66
+ acceptedCount += 1;
67
+ acceptedBytes += attachment.size;
68
+ return { ...attachment, decision: "accepted" };
69
+ });
70
+ }
71
+ export function requiredAttachmentsRejected(attachments) {
72
+ return attachments.filter((attachment) => attachment.required && attachment.decision !== "accepted");
73
+ }
74
+ export function formatAttachmentPrompt(attachments) {
75
+ const accepted = attachments.filter((attachment) => attachment.decision === "accepted");
76
+ const rejected = attachments.filter((attachment) => attachment.decision !== "accepted");
77
+ const lines = [];
78
+ if (accepted.length) {
79
+ lines.push("Runtime attachments:");
80
+ for (const attachment of accepted) {
81
+ const location = attachment.localPath ?? attachment.filePath ?? attachment.url ?? "(unavailable)";
82
+ const pathHint = attachment.localPath || attachment.filePath ? ` @${location}` : ` ${location}`;
83
+ lines.push(`- [${attachment.name}] ${attachment.mime} ${attachment.size}B${pathHint}`);
84
+ }
85
+ }
86
+ if (rejected.length) {
87
+ lines.push("Rejected or skipped attachments:");
88
+ for (const attachment of rejected) {
89
+ lines.push(`- ${attachment.name}: ${attachment.rejectionReason ?? attachment.decision}`);
90
+ }
91
+ }
92
+ return lines.join("\n");
93
+ }
94
+ export function mediaCacheDir(root = CONFIG_DIR) {
95
+ return join(root, "media");
96
+ }
97
+ export async function cacheLocalAttachment(attachment, root = mediaCacheDir()) {
98
+ if (attachment.decision !== "accepted")
99
+ return attachment;
100
+ if (attachment.filePath)
101
+ return cacheFilePathAttachment(attachment, root);
102
+ if (attachment.url)
103
+ return cacheUrlAttachment(attachment, root);
104
+ return attachment;
105
+ }
106
+ async function cacheFilePathAttachment(attachment, root) {
107
+ const filePath = attachment.filePath;
108
+ if (!filePath)
109
+ return attachment;
110
+ const source = resolve(filePath);
111
+ const sourceStat = await stat(source);
112
+ if (!sourceStat.isFile()) {
113
+ return { ...attachment, decision: "rejected", rejectionReason: "not-a-file" };
114
+ }
115
+ const sha256 = attachment.sha256 ?? await hashFile(source);
116
+ const ext = safeExtensionForMime(attachment.mime);
117
+ const target = join(root, `${sha256}.${ext}`);
118
+ await mkdir(root, { recursive: true });
119
+ try {
120
+ await stat(target);
121
+ }
122
+ catch {
123
+ const tmp = join(root, `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
124
+ await copyByRename(source, tmp, target);
125
+ }
126
+ return {
127
+ ...attachment,
128
+ size: sourceStat.size,
129
+ sha256,
130
+ localPath: target
131
+ };
132
+ }
133
+ async function cacheUrlAttachment(attachment, root) {
134
+ const url = parseDownloadUrl(attachment.url);
135
+ if (!url)
136
+ return { ...attachment, decision: "rejected", rejectionReason: "invalid-url" };
137
+ const res = await fetch(url, { redirect: "follow" });
138
+ if (!res.ok)
139
+ return { ...attachment, decision: "rejected", rejectionReason: `download-failed-${res.status}` };
140
+ const contentType = res.headers.get("Content-Type")?.split(";")[0]?.trim().toLowerCase();
141
+ if (contentType && contentType !== attachment.mime) {
142
+ return { ...attachment, decision: "rejected", rejectionReason: "mime-mismatch" };
143
+ }
144
+ const contentLength = Number(res.headers.get("Content-Length") ?? "");
145
+ const maxBytes = attachment.kind === "image" ? DEFAULT_ATTACHMENT_POLICY.imageMaxBytes : DEFAULT_ATTACHMENT_POLICY.maxFileBytes;
146
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
147
+ return { ...attachment, decision: "rejected", rejectionReason: "download-too-large" };
148
+ }
149
+ const bytes = new Uint8Array(await res.arrayBuffer());
150
+ if (bytes.byteLength > maxBytes)
151
+ return { ...attachment, decision: "rejected", rejectionReason: "download-too-large" };
152
+ const expectedSize = attachment.size > 0 ? attachment.size : bytes.byteLength;
153
+ if (bytes.byteLength !== expectedSize) {
154
+ return { ...attachment, decision: "rejected", rejectionReason: "size-mismatch" };
155
+ }
156
+ const sha256 = hashBytes(bytes);
157
+ if (attachment.sha256 && attachment.sha256 !== sha256) {
158
+ return { ...attachment, decision: "rejected", rejectionReason: "sha256-mismatch" };
159
+ }
160
+ const ext = safeExtensionForMime(attachment.mime);
161
+ const target = join(root, `${sha256}.${ext}`);
162
+ await mkdir(root, { recursive: true });
163
+ try {
164
+ await stat(target);
165
+ }
166
+ catch {
167
+ const tmp = join(root, `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
168
+ await writeFile(tmp, bytes);
169
+ await rename(tmp, target);
170
+ }
171
+ return {
172
+ ...attachment,
173
+ size: bytes.byteLength,
174
+ sha256,
175
+ localPath: target
176
+ };
177
+ }
178
+ export async function cacheLocalAttachments(attachments, root = mediaCacheDir()) {
179
+ const out = [];
180
+ for (const attachment of attachments) {
181
+ out.push(await cacheLocalAttachment(attachment, root));
182
+ }
183
+ return out;
184
+ }
185
+ export async function gcMediaCache(root = mediaCacheDir(), maxAgeMs = 24 * 60 * 60 * 1000) {
186
+ const cutoff = Date.now() - maxAgeMs;
187
+ let removed = 0;
188
+ for (const file of await listFiles(root)) {
189
+ try {
190
+ const info = await stat(file);
191
+ if (info.isFile() && info.mtimeMs < cutoff) {
192
+ await rm(file, { force: true });
193
+ removed += 1;
194
+ }
195
+ }
196
+ catch {
197
+ // Ignore files that disappear during GC.
198
+ }
199
+ }
200
+ return removed;
201
+ }
202
+ function normalizeRuntimeAttachment(value, index) {
203
+ const row = value && typeof value === "object" ? value : {};
204
+ const name = cleanString(row.name) || (row.filePath ? basename(row.filePath) : `attachment-${index + 1}`);
205
+ const mime = normalizeAttachmentMime(cleanString(row.mime), name);
206
+ const kind = normalizeAttachmentKind(row.kind, mime);
207
+ const size = Number.isFinite(row.size) && Number(row.size) > 0 ? Math.floor(Number(row.size)) : 0;
208
+ return {
209
+ id: cleanString(row.id) || `attachment-${index + 1}`,
210
+ name,
211
+ kind,
212
+ mime,
213
+ size,
214
+ sha256: cleanString(row.sha256),
215
+ url: cleanString(row.url),
216
+ filePath: cleanString(row.filePath),
217
+ source: cleanString(row.source),
218
+ required: row.required === true,
219
+ decision: "accepted"
220
+ };
221
+ }
222
+ function attachmentDecision(attachment, policy, acceptedCount, acceptedBytes) {
223
+ if (!attachment.filePath && !attachment.url)
224
+ return { decision: "rejected", reason: "missing-location" };
225
+ if (attachment.kind === "audio" || attachment.kind === "video")
226
+ return { decision: "skipped", reason: "unsupported-kind" };
227
+ if (attachment.kind === "image" && !policy.allowedImageMimes.includes(attachment.mime))
228
+ return { decision: "rejected", reason: "unsupported-image-mime" };
229
+ if (attachment.kind === "file" && !policy.allowedFileMimes.includes(attachment.mime))
230
+ return { decision: "rejected", reason: "unsupported-file-mime" };
231
+ if (acceptedCount >= policy.maxCount)
232
+ return { decision: "rejected", reason: "too-many-attachments" };
233
+ if (attachment.size > policy.maxFileBytes)
234
+ return { decision: "rejected", reason: "file-too-large" };
235
+ if (attachment.kind === "image" && attachment.size > policy.imageMaxBytes)
236
+ return { decision: "rejected", reason: "image-too-large" };
237
+ if (acceptedBytes + attachment.size > policy.maxBytes)
238
+ return { decision: "rejected", reason: "run-too-large" };
239
+ return null;
240
+ }
241
+ function normalizeAttachmentKind(value, mime) {
242
+ if (value === "image" || value === "file" || value === "audio" || value === "video")
243
+ return value;
244
+ if (mime.startsWith("image/"))
245
+ return "image";
246
+ if (mime.startsWith("audio/"))
247
+ return "audio";
248
+ if (mime.startsWith("video/"))
249
+ return "video";
250
+ if (mime !== "application/octet-stream")
251
+ return "file";
252
+ return "unknown";
253
+ }
254
+ function safeExtensionForMime(mime) {
255
+ return SAFE_EXT_BY_MIME[mime] ?? "bin";
256
+ }
257
+ function normalizeAttachmentMime(value, name) {
258
+ const normalized = value?.toLowerCase() || "";
259
+ if (normalized && normalized !== "application/octet-stream")
260
+ return normalized;
261
+ return mimeFromName(name) ?? (normalized || "application/octet-stream");
262
+ }
263
+ function mimeFromName(name) {
264
+ const ext = name.toLowerCase().split(".").pop();
265
+ return {
266
+ css: "text/css",
267
+ csv: "text/csv",
268
+ htm: "text/html",
269
+ html: "text/html",
270
+ js: "text/javascript",
271
+ json: "application/json",
272
+ jsx: "text/jsx",
273
+ md: "text/markdown",
274
+ mjs: "text/javascript",
275
+ ts: "application/typescript",
276
+ tsx: "text/tsx",
277
+ tsv: "text/tsv",
278
+ txt: "text/plain",
279
+ xhtml: "application/xhtml+xml",
280
+ xml: "text/xml"
281
+ }[ext ?? ""];
282
+ }
283
+ function cleanString(value) {
284
+ return typeof value === "string" && value.trim() ? value.trim().slice(0, 1000) : undefined;
285
+ }
286
+ async function copyByRename(source, tmp, target) {
287
+ const { copyFile } = await import("node:fs/promises");
288
+ await copyFile(source, tmp);
289
+ await rename(tmp, target);
290
+ }
291
+ async function hashFile(path) {
292
+ const hash = createHash("sha256");
293
+ for await (const chunk of createReadStream(path))
294
+ hash.update(chunk);
295
+ return hash.digest("hex");
296
+ }
297
+ function hashBytes(bytes) {
298
+ return createHash("sha256").update(bytes).digest("hex");
299
+ }
300
+ function parseDownloadUrl(value) {
301
+ if (!value)
302
+ return null;
303
+ try {
304
+ const url = new URL(value);
305
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : null;
306
+ }
307
+ catch {
308
+ return null;
309
+ }
310
+ }
311
+ async function listFiles(root) {
312
+ const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
313
+ const files = [];
314
+ for (const entry of entries) {
315
+ const full = join(root, entry.name);
316
+ if (entry.isDirectory())
317
+ files.push(...await listFiles(full));
318
+ else if (entry.isFile())
319
+ files.push(full);
320
+ }
321
+ return files;
322
+ }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import type { CommandName } from "./paths.js";
3
+ import type { KingScenarioTemplate } from "./team-workflow.js";
4
+ export declare function commandNameFromArgv(argv0?: string): CommandName;
5
+ export declare function versionText(commandName: string, version?: string): string;
6
+ export declare function defaultServerForCommand(_commandName: string): string;
7
+ export declare function hasExplicitServerArg(args: string[]): boolean;
8
+ export declare function computerHelpText(defaultServer?: string, commandName?: string): string;
9
+ export declare function normalizeComputerArgs(args: string[]): string[];
10
+ export declare function shouldRunAfterPair(serviceInstalled: boolean): boolean;
11
+ export declare function materializeTeamScenario(scenario: KingScenarioTemplate, outputDir: string, actorRole?: string): Promise<{
12
+ scenario: string;
13
+ outputDir: string;
14
+ cards: unknown[];
15
+ }>;
16
+ export declare function formatMaterializedTeamScenario(result: {
17
+ scenario: string;
18
+ outputDir: string;
19
+ cards: unknown[];
20
+ }): string;