From 26925ae644e69a8aece3501f693c8cd8633582a1 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Fri, 12 Jun 2026 22:17:16 -0700 Subject: [PATCH] feat(runtime-sdk): add fleet helper client Refs #3163. Adds the @codewhale/runtime-sdk workspace with typed fleet Runtime API helpers, protocol-shaped TypeScript declarations, JSON/SSE event fixture handling, and typed RuntimeCapabilityError failures for create/event-stream endpoints that the Rust API has not exposed yet. Documents the SDK contract in docs/RUNTIME_API.md and wires npm workspace verification through npm test --workspace @codewhale/runtime-sdk. --- docs/RUNTIME_API.md | 45 ++++ npm/runtime-sdk/README.md | 35 +++ npm/runtime-sdk/index.d.ts | 245 +++++++++++++++++++++ npm/runtime-sdk/index.js | 198 +++++++++++++++++ npm/runtime-sdk/package.json | 30 +++ npm/runtime-sdk/test/fleet-client.test.mjs | 174 +++++++++++++++ package-lock.json | 66 ++++++ package.json | 6 + 8 files changed, 799 insertions(+) create mode 100644 npm/runtime-sdk/README.md create mode 100644 npm/runtime-sdk/index.d.ts create mode 100644 npm/runtime-sdk/index.js create mode 100644 npm/runtime-sdk/package.json create mode 100644 npm/runtime-sdk/test/fleet-client.test.mjs diff --git a/docs/RUNTIME_API.md b/docs/RUNTIME_API.md index 59c4d301..ce3a88f6 100644 --- a/docs/RUNTIME_API.md +++ b/docs/RUNTIME_API.md @@ -450,6 +450,51 @@ User-supplied origins **stack on top of** the built-in defaults; they do not replace them. Wildcard origins are not supported — the explicit allow-list model is preserved. Added in v0.8.10 (#561). +## Runtime SDK Fleet Helpers + +The v0.8.60 Runtime SDK fixture lives in `npm/runtime-sdk` and is exposed as +the `@codewhale/runtime-sdk` workspace package. It is deliberately thin: every +helper calls the local Rust Runtime API and therefore cannot bypass CodeWhale's +sandbox, approval prompts, provider configuration, or fleet ledger authority. + +```js +import { createRuntimeClient } from "@codewhale/runtime-sdk"; + +const client = createRuntimeClient({ + baseUrl: "http://127.0.0.1:7878", + token: process.env.CODEWHALE_RUNTIME_TOKEN, +}); + +const { runs } = await client.listFleetRuns(); +const workers = await client.listFleetWorkers(runs[0].id); +await client.restartWorker(workers.workers[0].worker_id); +``` + +Fleet helpers cover the v0.8.60 HTTP surface: + +| Helper | Runtime API route | +|---|---| +| `listFleetRuns()` | `GET /v1/fleet/runs` | +| `getFleetRun(runId)` | `GET /v1/fleet/runs/{run_id}` | +| `listFleetWorkers(runId)` | `GET /v1/fleet/runs/{run_id}/workers` | +| `getFleetWorker(workerId)` | `GET /v1/fleet/workers/{worker_id}` | +| `interruptWorker(workerId)` | `POST /v1/fleet/workers/{worker_id}/interrupt` | +| `restartWorker(workerId)` | `POST /v1/fleet/workers/{worker_id}/restart` | +| `stopFleetRun(runId)` | `POST /v1/fleet/runs/{run_id}/stop` | + +`createFleetRun(spec)` and `fleetEvents(runId)` are typed ahead of the current +Rust routes so editor/web clients can code against the intended SDK contract. +Until the Runtime API exposes `POST /v1/fleet/runs` and a fleet event stream, +the SDK raises `RuntimeCapabilityError` with stable capability strings +(`fleet_run_create`, `fleet_event_stream`) instead of surfacing those gaps as +generic fetch failures. + +Verification: + +```bash +npm test --workspace @codewhale/runtime-sdk +``` + ## Session lifecycle (native UI supervision) | Operation | Endpoint | diff --git a/npm/runtime-sdk/README.md b/npm/runtime-sdk/README.md new file mode 100644 index 00000000..725517d0 --- /dev/null +++ b/npm/runtime-sdk/README.md @@ -0,0 +1,35 @@ +# @codewhale/runtime-sdk + +Small JavaScript helpers and TypeScript declarations for CodeWhale's local +Runtime API. The package is intentionally transport-only: it never bypasses the +Rust runtime, sandbox, approvals, provider configuration, or fleet ledger. + +```js +import { createRuntimeClient } from "@codewhale/runtime-sdk"; + +const client = createRuntimeClient({ + baseUrl: "http://127.0.0.1:7878", + token: process.env.CODEWHALE_RUNTIME_TOKEN, +}); + +const { runs } = await client.listFleetRuns(); +const workers = await client.listFleetWorkers(runs[0].id); +await client.interruptWorker(workers.workers[0].worker_id); +``` + +## Fleet Helpers + +- `listFleetRuns()` +- `getFleetRun(runId)` +- `listFleetWorkers(runId)` +- `getFleetWorker(workerId)` +- `interruptWorker(workerId)` +- `restartWorker(workerId)` +- `stopFleetRun(runId)` +- `fleetEvents(runId)` +- `createFleetRun(spec)` + +`fleetEvents` and `createFleetRun` are typed ahead of the current v0.8.60 Rust +Runtime API. If the local runtime does not expose those endpoints, the helpers +raise `RuntimeCapabilityError` with a stable `capability` string instead of a +generic fetch failure. diff --git a/npm/runtime-sdk/index.d.ts b/npm/runtime-sdk/index.d.ts new file mode 100644 index 00000000..71bf2b54 --- /dev/null +++ b/npm/runtime-sdk/index.d.ts @@ -0,0 +1,245 @@ +export type FleetRunId = string; +export type FleetRunStatus = + | "pending" + | "queued" + | "running" + | "paused" + | "completed" + | "failed" + | "cancelled"; + +export type FleetWorkerStatus = + | "unknown" + | "online" + | "busy" + | "offline" + | "unhealthy" + | "draining" + | "retired"; + +export type FleetArtifactKind = + | "log" + | "patch" + | "test_result" + | "report" + | "checkpoint" + | "receipt" + | string; + +export interface FleetStatusSummary { + runs: number; + queued: number; + running: number; + completed: number; + partial: number; + failed: number; + restarted: number; + escalated: number; + transport_failed: number; + task_failed: number; + verifier_failed: number; + cancelled: number; + stale: number; + workers: Record; +} + +export interface FleetTaskStatusSummary { + task_id: string; + status: "enqueued" | "leased" | "completed" | "failed" | "cancelled"; + leased_to?: string | null; + attempts: number; +} + +export interface FleetRunSummary { + id: string; + name: string; + status: FleetStatusSummary; + task_count: number; + worker_count: number; + tasks: FleetTaskStatusSummary[]; + labels: Record; + created_at: string; + updated_at?: string | null; + completed_at?: string | null; +} + +export interface FleetRunDetail extends FleetRunSummary { + task_specs: FleetTaskSpec[]; + worker_specs: FleetWorkerSpec[]; +} + +export interface FleetRunsResponse { + status: FleetStatusSummary; + runs: FleetRunSummary[]; +} + +export interface FleetTaskSpec { + id: string; + name: string; + description?: string | null; + objective?: string | null; + instructions: string; + worker?: FleetTaskWorkerProfile | null; + workspace?: FleetWorkspaceRequirements | null; + input_files?: string[]; + context?: string[]; + budget?: FleetTaskBudget | null; + tags?: string[]; + expected_artifacts?: FleetArtifactKind[]; + scorer?: Record | null; + retry_policy?: Record | null; + alert_policy?: FleetAlertPolicy | null; + timeout_seconds?: number | null; + metadata?: Record; +} + +export interface FleetTaskWorkerProfile { + role?: string | null; + tool_profile?: string | null; + tools?: string[]; + capabilities?: string[]; +} + +export interface FleetWorkspaceRequirements { + root?: string | null; + required_files?: string[]; + writable_paths?: string[]; + environment?: FleetEnvironmentRequirements | null; +} + +export interface FleetEnvironmentRequirements { + required?: string[]; + allowlist?: string[]; +} + +export interface FleetTaskBudget { + max_tokens?: number | null; + max_tool_calls?: number | null; + max_seconds?: number | null; +} + +export interface FleetAlertPolicy { + events?: string[]; + channels?: Array>; + after_attempts?: number | null; + after_minutes_stale?: number | null; +} + +export interface FleetWorkerSpec { + id: string; + name: string; + host: Record; + labels?: Record; + capabilities?: string[]; + max_concurrent_tasks?: number | null; +} + +export interface FleetArtifactRef { + kind: FleetArtifactKind; + path: string; + checksum?: string | null; + mime_type?: string | null; + size_bytes?: number | null; +} + +export type FleetWorkerEventPayload = + | { state: "queued" } + | { state: "leased"; lease_expires_at?: string | null } + | { state: "starting" } + | { state: "running" } + | { state: "model_wait"; model?: string | null } + | { state: "running_tool"; tool: string; call_id?: string | null } + | { state: "heartbeat"; cpu_percent?: number | null; memory_mb?: number | null } + | ({ state: "artifact" } & FleetArtifactRef) + | { state: "completed"; exit_code?: number | null; summary?: string | null } + | { state: "failed"; reason: string; recoverable?: boolean } + | { state: "cancelled"; cancelled_by?: string | null } + | { state: "interrupted"; signal?: string | null } + | { state: "stale"; last_heartbeat_at?: string | null } + | { state: "restarted"; restart_count?: number } + | { state: "escalated"; channel: string; alert_id?: string | null }; + +export interface FleetWorkerEvent { + seq: number; + run_id: string; + worker_id: string; + task_id: string; + timestamp: string; + label?: string; + payload: FleetWorkerEventPayload; + extra?: Record; +} + +export interface FleetWorkerInspection { + worker_id: string; + status: FleetWorkerStatus; + run_id?: string | null; + task_id?: string | null; + objective?: string | null; + role?: string | null; + host?: Record | null; + latest_heartbeat_at?: string | null; + latest_event?: FleetWorkerEvent | null; + artifacts: FleetArtifactRef[]; + last_error?: string | null; + alert_state?: Record | null; +} + +export interface FleetWorkersResponse { + run_id: string; + workers: FleetWorkerInspection[]; +} + +export interface FleetWorkerActionResponse { + action: "interrupt" | "restart"; + worker: FleetWorkerInspection; +} + +export interface StopFleetRunResponse { + action: "stop"; + run_id: string; + stopped: number; + status: FleetStatusSummary; +} + +export interface RuntimeClientOptions { + baseUrl?: string; + token?: string; + fetch?: typeof fetch; +} + +export interface FleetRunCreateSpec { + name?: string; + task_specs?: FleetTaskSpec[]; + worker_specs?: FleetWorkerSpec[]; + labels?: Record; +} + +export class RuntimeApiError extends Error { + status?: number; + method?: string; + path?: string; + body?: string; +} + +export class RuntimeCapabilityError extends RuntimeApiError { + capability: string; +} + +export class CodeWhaleRuntimeClient { + constructor(options?: RuntimeClientOptions); + createFleetRun(spec: FleetRunCreateSpec | Record): Promise; + listFleetRuns(): Promise; + getFleetRun(runId: FleetRunId): Promise; + listFleetWorkers(runId: FleetRunId): Promise; + getFleetWorker(workerId: string): Promise; + interruptWorker(workerId: string): Promise; + restartWorker(workerId: string): Promise; + stopFleetRun(runId: FleetRunId): Promise; + fleetEvents( + runId: FleetRunId, + options?: { path?: string }, + ): AsyncIterable; +} + +export function createRuntimeClient(options?: RuntimeClientOptions): CodeWhaleRuntimeClient; diff --git a/npm/runtime-sdk/index.js b/npm/runtime-sdk/index.js new file mode 100644 index 00000000..dbe22405 --- /dev/null +++ b/npm/runtime-sdk/index.js @@ -0,0 +1,198 @@ +const DEFAULT_BASE_URL = "http://127.0.0.1:7878"; + +export class RuntimeApiError extends Error { + constructor(message, options = {}) { + super(message); + this.name = "RuntimeApiError"; + this.status = options.status; + this.method = options.method; + this.path = options.path; + this.body = options.body; + } +} + +export class RuntimeCapabilityError extends RuntimeApiError { + constructor(capability, message, options = {}) { + super(message, options); + this.name = "RuntimeCapabilityError"; + this.capability = capability; + } +} + +export class CodeWhaleRuntimeClient { + constructor(options = {}) { + this.baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL); + this.token = options.token ?? null; + this.fetchImpl = options.fetch ?? globalThis.fetch; + if (typeof this.fetchImpl !== "function") { + throw new TypeError("CodeWhaleRuntimeClient requires a fetch implementation"); + } + } + + async createFleetRun(spec) { + return this.#jsonRequest("/v1/fleet/runs", { + method: "POST", + body: spec, + capability: "fleet_run_create", + }); + } + + async listFleetRuns() { + return this.#jsonRequest("/v1/fleet/runs"); + } + + async getFleetRun(runId) { + return this.#jsonRequest(`/v1/fleet/runs/${segment(runId)}`); + } + + async listFleetWorkers(runId) { + return this.#jsonRequest(`/v1/fleet/runs/${segment(runId)}/workers`); + } + + async getFleetWorker(workerId) { + return this.#jsonRequest(`/v1/fleet/workers/${segment(workerId)}`); + } + + async interruptWorker(workerId) { + return this.#jsonRequest(`/v1/fleet/workers/${segment(workerId)}/interrupt`, { + method: "POST", + }); + } + + async restartWorker(workerId) { + return this.#jsonRequest(`/v1/fleet/workers/${segment(workerId)}/restart`, { + method: "POST", + }); + } + + async stopFleetRun(runId) { + return this.#jsonRequest(`/v1/fleet/runs/${segment(runId)}/stop`, { + method: "POST", + }); + } + + async *fleetEvents(runId, options = {}) { + const path = options.path ?? `/v1/fleet/runs/${segment(runId)}/events`; + const response = await this.#rawRequest(path, { + method: "GET", + capability: "fleet_event_stream", + }); + const contentType = response.headers.get("content-type") ?? ""; + if (contentType.includes("application/json")) { + const payload = await response.json(); + const events = Array.isArray(payload) ? payload : (payload.events ?? []); + for (const event of events) { + yield event; + } + return; + } + if (!response.body) { + throw new RuntimeApiError("Runtime API event response did not include a readable body", { + method: "GET", + path, + }); + } + for await (const event of parseEventStream(response.body)) { + yield event; + } + } + + async #jsonRequest(path, options = {}) { + const response = await this.#rawRequest(path, options); + if (response.status === 204) { + return null; + } + return response.json(); + } + + async #rawRequest(path, options = {}) { + const method = options.method ?? "GET"; + const headers = new Headers(options.headers); + headers.set("accept", options.accept ?? "application/json"); + if (this.token) { + headers.set("authorization", `Bearer ${this.token}`); + } + const init = { method, headers }; + if (options.body !== undefined) { + headers.set("content-type", "application/json"); + init.body = JSON.stringify(options.body); + } + + const response = await this.fetchImpl(new URL(path, this.baseUrl), init); + if (response.ok) { + return response; + } + + const body = await readErrorBody(response); + const errorOptions = { status: response.status, method, path, body }; + if (options.capability && [404, 405, 501].includes(response.status)) { + throw new RuntimeCapabilityError( + options.capability, + `Runtime API capability '${options.capability}' is not available at ${method} ${path}`, + errorOptions, + ); + } + throw new RuntimeApiError( + `Runtime API request failed (${response.status}) for ${method} ${path}`, + errorOptions, + ); + } +} + +export function createRuntimeClient(options = {}) { + return new CodeWhaleRuntimeClient(options); +} + +function normalizeBaseUrl(value) { + return value.endsWith("/") ? value : `${value}/`; +} + +function segment(value) { + if (value === null || value === undefined || String(value).trim() === "") { + throw new TypeError("Runtime API path segment must be a non-empty value"); + } + return encodeURIComponent(String(value)); +} + +async function readErrorBody(response) { + try { + const text = await response.text(); + return text.length > 4096 ? `${text.slice(0, 4096)}...` : text; + } catch { + return ""; + } +} + +async function* parseEventStream(body) { + const decoder = new TextDecoder(); + let buffer = ""; + for await (const chunk of body) { + buffer += decoder.decode(chunk, { stream: true }); + let boundary; + while ((boundary = buffer.indexOf("\n\n")) >= 0) { + const frame = buffer.slice(0, boundary); + buffer = buffer.slice(boundary + 2); + const event = parseSseFrame(frame); + if (event !== undefined) { + yield event; + } + } + } + buffer += decoder.decode(); + const event = parseSseFrame(buffer); + if (event !== undefined) { + yield event; + } +} + +function parseSseFrame(frame) { + const data = frame + .split(/\r?\n/) + .filter((line) => line.startsWith("data:")) + .map((line) => line.slice("data:".length).trimStart()) + .join("\n"); + if (!data || data === "[DONE]") { + return undefined; + } + return JSON.parse(data); +} diff --git a/npm/runtime-sdk/package.json b/npm/runtime-sdk/package.json new file mode 100644 index 00000000..ba61b37a --- /dev/null +++ b/npm/runtime-sdk/package.json @@ -0,0 +1,30 @@ +{ + "name": "@codewhale/runtime-sdk", + "version": "0.8.60", + "description": "Typed JavaScript helpers for CodeWhale Runtime API fleet endpoints.", + "license": "MIT", + "type": "module", + "main": "index.js", + "types": "index.d.ts", + "exports": { + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "files": [ + "index.js", + "index.d.ts", + "README.md", + "package.json" + ], + "scripts": { + "test": "node --test test/*.test.mjs && node --check index.js" + }, + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/npm/runtime-sdk/test/fleet-client.test.mjs b/npm/runtime-sdk/test/fleet-client.test.mjs new file mode 100644 index 00000000..31fa3c99 --- /dev/null +++ b/npm/runtime-sdk/test/fleet-client.test.mjs @@ -0,0 +1,174 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + CodeWhaleRuntimeClient, + RuntimeApiError, + RuntimeCapabilityError, + createRuntimeClient, +} from "../index.js"; + +function jsonResponse(body, init = {}) { + return new Response(JSON.stringify(body), { + status: init.status ?? 200, + headers: { "content-type": "application/json", ...(init.headers ?? {}) }, + }); +} + +function fakeFetch(responseFactory) { + const calls = []; + const fetch = async (url, init) => { + calls.push({ url: url.toString(), init }); + return responseFactory(url, init, calls.length); + }; + fetch.calls = calls; + return fetch; +} + +test("listFleetRuns calls the Runtime API with bearer auth", async () => { + const fetch = fakeFetch(() => + jsonResponse({ + status: { runs: 1, workers: {} }, + runs: [{ id: "run-1", name: "smoke", tasks: [], labels: {} }], + }), + ); + const client = createRuntimeClient({ + baseUrl: "http://127.0.0.1:7878", + token: "token-1", + fetch, + }); + + const response = await client.listFleetRuns(); + + assert.equal(response.runs[0].id, "run-1"); + assert.equal(fetch.calls[0].url, "http://127.0.0.1:7878/v1/fleet/runs"); + assert.equal(fetch.calls[0].init.method, "GET"); + assert.equal(fetch.calls[0].init.headers.get("authorization"), "Bearer token-1"); +}); + +test("worker and run actions use POST endpoints", async () => { + const fetch = fakeFetch((url) => + jsonResponse( + url.pathname.endsWith("/stop") + ? { + action: "stop", + run_id: "run-1", + stopped: 1, + status: { runs: 1, workers: {} }, + } + : { + action: url.pathname.endsWith("/restart") ? "restart" : "interrupt", + worker: { worker_id: "w1", artifacts: [] }, + }, + ), + ); + const client = new CodeWhaleRuntimeClient({ fetch }); + + await client.interruptWorker("w1"); + await client.restartWorker("w1"); + await client.stopFleetRun("run-1"); + + assert.deepEqual( + fetch.calls.map((call) => [new URL(call.url).pathname, call.init.method]), + [ + ["/v1/fleet/workers/w1/interrupt", "POST"], + ["/v1/fleet/workers/w1/restart", "POST"], + ["/v1/fleet/runs/run-1/stop", "POST"], + ], + ); +}); + +test("unsupported fleet capabilities raise typed errors", async () => { + const fetch = fakeFetch(() => jsonResponse({ error: "not found" }, { status: 404 })); + const client = new CodeWhaleRuntimeClient({ fetch }); + + await assert.rejects( + () => client.createFleetRun({ name: "future" }), + (error) => + error instanceof RuntimeCapabilityError && + error.capability === "fleet_run_create" && + error.status === 404, + ); + + await assert.rejects( + async () => { + for await (const _event of client.fleetEvents("run-1")) { + throw new Error("unexpected event"); + } + }, + (error) => + error instanceof RuntimeCapabilityError && + error.capability === "fleet_event_stream" && + error.status === 404, + ); +}); + +test("fleetEvents can replay JSON event fixtures when the API exposes them", async () => { + const fetch = fakeFetch(() => + jsonResponse({ + events: [ + { + seq: 1, + run_id: "run-1", + worker_id: "w1", + task_id: "task-1", + timestamp: "2026-06-13T00:00:00Z", + label: "running", + payload: { state: "running" }, + }, + ], + }), + ); + const client = new CodeWhaleRuntimeClient({ fetch }); + + const events = []; + for await (const event of client.fleetEvents("run-1", { path: "/v1/fleet/runs/run-1/events" })) { + events.push(event); + } + + assert.equal(events.length, 1); + assert.equal(events[0].payload.state, "running"); +}); + +test("fleetEvents parses text/event-stream frames", async () => { + const encoder = new TextEncoder(); + const body = new ReadableStream({ + start(controller) { + controller.enqueue( + encoder.encode( + 'data: {"seq":2,"run_id":"run-1","worker_id":"w1","task_id":"task-1","timestamp":"2026-06-13T00:00:01Z","label":"heartbeat","payload":{"state":"heartbeat","memory_mb":128}}\n\n', + ), + ); + controller.close(); + }, + }); + const fetch = fakeFetch( + () => + new Response(body, { + status: 200, + headers: { "content-type": "text/event-stream" }, + }), + ); + const client = new CodeWhaleRuntimeClient({ fetch }); + + const events = []; + for await (const event of client.fleetEvents("run-1")) { + events.push(event); + } + + assert.equal(events.length, 1); + assert.equal(events[0].payload.state, "heartbeat"); + assert.equal(events[0].payload.memory_mb, 128); +}); + +test("ordinary HTTP errors remain RuntimeApiError", async () => { + const fetch = fakeFetch(() => jsonResponse({ error: "bad" }, { status: 500 })); + const client = new CodeWhaleRuntimeClient({ fetch }); + + await assert.rejects( + () => client.getFleetRun("run-1"), + (error) => + error instanceof RuntimeApiError && + !(error instanceof RuntimeCapabilityError) && + error.status === 500, + ); +}); diff --git a/package-lock.json b/package-lock.json index cbfc7a7e..8dc2658e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,11 @@ "requires": true, "packages": { "": { + "workspaces": [ + "npm/codewhale", + "npm/deepseek-tui", + "npm/runtime-sdk" + ], "devDependencies": { "wrangler": "^4.94.0" } @@ -119,6 +124,10 @@ "node": ">=16" } }, + "node_modules/@codewhale/runtime-sdk": { + "resolved": "npm/runtime-sdk", + "link": true + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1207,6 +1216,10 @@ "dev": true, "license": "MIT" }, + "node_modules/codewhale": { + "resolved": "npm/codewhale", + "link": true + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -1221,6 +1234,10 @@ "url": "https://opencollective.com/express" } }, + "node_modules/deepseek-tui": { + "resolved": "npm/deepseek-tui", + "link": true + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1605,6 +1622,55 @@ "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } + }, + "npm/codewhale": { + "version": "0.8.59", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Hmbown" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/hmbown" + } + ], + "hasInstallScript": true, + "license": "MIT", + "bin": { + "codew": "bin/codew.js", + "codewhale": "bin/codewhale.js", + "codewhale-tui": "bin/codewhale-tui.js" + }, + "engines": { + "node": ">=18" + } + }, + "npm/deepseek-tui": { + "version": "0.8.49", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/Hmbown" + }, + { + "type": "buymeacoffee", + "url": "https://www.buymeacoffee.com/hmbown" + } + ], + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "npm/runtime-sdk": { + "name": "@codewhale/runtime-sdk", + "version": "0.8.60", + "license": "MIT", + "engines": { + "node": ">=18" + } } } } diff --git a/package.json b/package.json index 48cd75c6..5d129cf8 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,10 @@ { + "private": true, + "workspaces": [ + "npm/codewhale", + "npm/deepseek-tui", + "npm/runtime-sdk" + ], "devDependencies": { "wrangler": "^4.94.0" }