mindwire 0.1.0 → 0.1.2

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.
package/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # mindwire
2
+
3
+ A typed TypeScript client for the **mindwire daemon** — one SDK for every coding-agent harness
4
+ (Claude Code, Codex, Grok Build, opencode). The daemon normalizes each agent to a single
5
+ protocol; this package is a thin, dependency-free client over its REST + SSE surface.
6
+
7
+ - **Embedded by default.** On a server runtime (Node/Bun/Deno) the SDK auto-spawns the bundled
8
+ `mindwired` daemon on loopback — nothing to install or deploy. Pass a `target` to run the daemon
9
+ somewhere else: `remote(url)`, or `ssh(...)` / `docker(...)` / `oblien(...)` to provision one.
10
+ - **Zero runtime dependencies.** Uses the platform `fetch` and WHATWG streams — Node 18+, Bun, Deno, browsers.
11
+ - **One protocol, any agent.** Pick the harness per client or per call with `agent`.
12
+ - **Streaming-first.** A turn is an async-iterable of unified events (`text`, `thinking`, `tool_use`, `result`, …).
13
+ - **Faithful types.** Every shape mirrors the daemon's Go wire structs 1:1.
14
+
15
+ > 📚 **Guides & architecture:** [mindwire.sh/docs](https://mindwire.sh/docs). This page is the SDK reference.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm i mindwire
21
+ # or: bun add mindwire / pnpm add mindwire
22
+ ```
23
+
24
+ ## Quick start
25
+
26
+ ```ts
27
+ import { Mindwire } from "mindwire";
28
+
29
+ // Embedded (default): auto-spawns the bundled daemon on first use. No server to run.
30
+ const mw = new Mindwire({ agent: "claude-code" });
31
+
32
+ // What can this daemon run?
33
+ const { agents } = await mw.catalog();
34
+
35
+ // Start a turn and stream the unified event feed.
36
+ const run = await mw.turn({ chatId: "chat-1", message: "add a /healthz endpoint" });
37
+
38
+ for await (const ev of run) {
39
+ switch (ev.type) {
40
+ case "text": process.stdout.write(ev.text ?? ""); break;
41
+ case "tool_use": console.log(`\n↳ ${ev.tool?.name}`, ev.tool?.input); break;
42
+ case "result": console.log(`\n✓ $${ev.result?.costUsd} · ${ev.result?.numTurns} turns`); break;
43
+ case "error": console.error("error:", ev.error); break;
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### Await a turn instead of streaming
49
+
50
+ ```ts
51
+ const run = await mw.turn({ chatId: "chat-1", message: "run the tests" });
52
+ const { result } = await run.wait(); // throws RunFailedError on error/cancelled
53
+ console.log(result?.text);
54
+ ```
55
+
56
+ ### Cancel a turn
57
+
58
+ ```ts
59
+ const run = await mw.turn({ chatId: "chat-1", message: "big refactor" });
60
+ setTimeout(() => run.cancel(), 5_000);
61
+ for await (const ev of run) { /* … */ }
62
+ ```
63
+
64
+ ## Targets — where the daemon runs
65
+
66
+ **One `new Mindwire` = one instance = one daemon = one environment.** Every agent that instance runs
67
+ (via `agent`, `withAgent`, or a per-call `agent`) shares that one daemon's filesystem and state. To
68
+ isolate agents, create a **second** `Mindwire` with its own `target` — a separate daemon. Omit
69
+ `target` for the zero-config embedded default.
70
+
71
+ The `target` option is a factory; pick where the daemon runs and how the SDK reaches it:
72
+
73
+ ```ts
74
+ import { Mindwire, local, remote, ssh, docker, oblien } from "mindwire";
75
+
76
+ // local (default) — Node/Bun/Deno. Auto-spawns the bundled `mindwired` on a loopback port on the
77
+ // first call. Clients with the same config share one daemon; distinct configs get distinct daemons.
78
+ new Mindwire({ agent: "claude-code" }); // zero-config
79
+ new Mindwire({ target: local({ cwd: "/path/to/repo", statePath: ".mindwire-state.json", bin: "/opt/mindwired" }) });
80
+
81
+ // remote — connect to a daemon already running over HTTP. Required in the browser/edge.
82
+ new Mindwire({ target: remote("https://mindwire.yourco.com", { token: process.env.MINDWIRE_TOKEN }) });
83
+
84
+ // ssh — provision + run the daemon on a bare box; the SDK reaches it over a local port-forward tunnel.
85
+ new Mindwire({ target: ssh({ host: "box.internal", username: "root" }) });
86
+
87
+ // docker — run in a container (local socket or a remote engine via `engine`).
88
+ new Mindwire({ target: docker({ image: "my/agent-image" }) });
89
+
90
+ // oblien — provision an Oblien sandbox and route through its gateway.
91
+ new Mindwire({ target: oblien({ clientId: process.env.OBLIEN_CLIENT_ID, clientSecret: process.env.OBLIEN_CLIENT_SECRET }) });
92
+ ```
93
+
94
+ Any object implementing `Target` can also be passed as `target` (bring-your-own).
95
+
96
+ ### Provisioning logs & `ensure()`
97
+
98
+ `ssh` / `docker` / `oblien` prep the box on connect — upload the daemon binary, launch it, health-poll
99
+ until ready. Pass a `logger` to watch each step, and `await mw.ensure()` to provision eagerly instead
100
+ of lazily on the first request (idempotent — it awaits the same work the first call would):
101
+
102
+ ```ts
103
+ const mw = new Mindwire({
104
+ target: ssh({ host: "box.internal", username: "root" }),
105
+ logger: (e) => console.log(`[${e.phase}] ${e.message}`), // connect · probe · upload · launch · ready · skip · error
106
+ });
107
+ await mw.ensure(); // resolves once the daemon is healthy; then turns stream as usual
108
+ ```
109
+
110
+ Binary discovery for the `local` daemon, in order: the `bin` option → `$MINDWIRE_DAEMON` → a
111
+ verified SDK-matched binary downloaded from the GitHub Release → `mindwired` on `PATH`. For local
112
+ development, set `$MINDWIRE_DAEMON` to a `go build`-produced binary.
113
+
114
+ `ssh` / `docker` / `oblien` require an optional peer (`ssh2` / `dockerode` / `oblien`) — install only
115
+ the one you use. Importing `mindwire` never loads them.
116
+
117
+ ## Switching harnesses
118
+
119
+ Every agent-scoped call takes an optional `agent`, and `withAgent` scopes a whole client:
120
+
121
+ ```ts
122
+ await mw.agent({ agent: "codex" }); // one-off override
123
+ const cx = mw.withAgent("codex"); // scoped client, shared transport
124
+ const info = await cx.agent();
125
+ console.log(info.capabilities.protocol); // "cli" for claude/codex
126
+ ```
127
+
128
+ ## Auth (step-flow)
129
+
130
+ The daemon declares auth methods per agent; the client walks a generic `begin → step → status` flow.
131
+
132
+ ```ts
133
+ const methods = await mw.auth.methods(); // e.g. [{ id: "apiKey", … }, { id: "login", interactive: true }]
134
+
135
+ // API-key method:
136
+ await mw.auth.begin("apiKey");
137
+ await mw.auth.step({ apiKey: "sk-ant-…" });
138
+
139
+ // Interactive login: begin() returns a URL; poll step() until complete.
140
+ let state = await mw.auth.begin("login");
141
+ while (state.status === "pending") {
142
+ console.log("open:", state.url);
143
+ await new Promise((r) => setTimeout(r, 1500));
144
+ state = await mw.auth.step({});
145
+ }
146
+ ```
147
+
148
+ ## API surface
149
+
150
+ | Method | Endpoint |
151
+ |---|---|
152
+ | `mw.health()` | `GET /healthz` |
153
+ | `mw.catalog()` | `GET /catalog` |
154
+ | `mw.agent(scoped?)` | `GET /agent` |
155
+ | `mw.doctor(scoped?)` | `GET /doctor` |
156
+ | `mw.setup() / update() / setupStatus()` | `POST /setup` · `POST /update` · `GET /setup` |
157
+ | `mw.getConfig() / setConfig(values)` | `GET /config` · `PUT /config` |
158
+ | `mw.auth.methods() / begin() / step() / status()` | `/auth/*` |
159
+ | `mw.chats()` | `GET /chats` |
160
+ | `mw.messages(chatId, { limit, before })` | `GET /chats/{id}/messages` |
161
+ | `mw.latestRun(chatId)` | `GET /chats/{id}/run` |
162
+ | `mw.turn({ chatId, message, cwd? })` → `Run` | `POST /turns` |
163
+ | `mw.run(id)` → `Run` | `GET /runs/{id}` |
164
+ | `run.stream()` / `for await (…of run)` | `GET /runs/{id}/stream` (SSE) |
165
+ | `run.cancel()` | `POST /runs/{id}/cancel` |
166
+ | `run.wait()` / `run.refresh()` | stream to completion / re-fetch |
167
+ | `mw.getNotifyConfig() / setNotifyConfig()` | `/notify/config` |
168
+ | `mw.notifications({ signal })` | `GET /notify/stream` (SSE) |
169
+
170
+ Agent-scoped methods accept `{ agent }` to override the client default. Errors are thrown as
171
+ `ApiError` (with `.status` and parsed `.body`); a failed run awaited via `run.wait()` throws
172
+ `RunFailedError`.
173
+
174
+ ## Development
175
+
176
+ ```bash
177
+ bun install
178
+ bun run build # tsup → dist (ESM + CJS + .d.ts)
179
+ bun run typecheck # tsc --noEmit
180
+ bun test # client + SSE parser tests (mocked fetch)
181
+ ```
182
+
183
+ License: Apache-2.0.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The models.dev catalog — the reference list of which providers and models exist.
3
+ *
4
+ * This is deliberately NOT bundled: models.dev is a hosted JSON API and staying its client (rather than
5
+ * embedding a snapshot) is the whole point — the list is always current, and the SDK stays a thin catalog
6
+ * client with no committed multi-megabyte blob. The raw feed (`https://models.dev/api.json`, ~2 MB across
7
+ * ~190 providers) is fetched once per process, projected to {@link CatalogProvider}/{@link ModelInfo}, and
8
+ * memoized with a soft TTL. Uses `globalThis.fetch`, so it works in Node and the browser.
9
+ *
10
+ * This is pure catalog data — "which models exist" — and knows nothing about the daemon, a selected agent,
11
+ * or which providers are configured/authenticated. The daemon's job is only to relay a chosen provider's
12
+ * settings to the harness; the catalog lives here.
13
+ */
14
+ import type { CatalogProvider, ModelInfo } from "../types.js";
15
+ /** The canonical models.dev catalog endpoint. */
16
+ export declare const MODELS_DEV_URL = "https://models.dev/api.json";
17
+ /** Options accepted by every catalog function. All optional; defaults hit models.dev via global fetch. */
18
+ export interface CatalogOptions {
19
+ /** Override the endpoint (e.g. a mirror or a pinned snapshot). Defaults to {@link MODELS_DEV_URL}. */
20
+ url?: string;
21
+ /** Override the fetch implementation (tests, a proxy transport). Defaults to `globalThis.fetch`. */
22
+ fetch?: typeof globalThis.fetch;
23
+ /** Abort signal for the underlying request. */
24
+ signal?: AbortSignal;
25
+ }
26
+ /**
27
+ * Load (and memoize) the full models.dev catalog, projected to {@link CatalogProvider}[] sorted by name.
28
+ * Cached in-process for {@link TTL_MS}; concurrent calls share one in-flight request. Throws if the fetch
29
+ * fails or returns a non-2xx — callers (and the console) surface that as an error state.
30
+ */
31
+ export declare function loadCatalog(opts?: CatalogOptions): Promise<CatalogProvider[]>;
32
+ /** Every provider in the models.dev catalog, each with its models, sorted by provider name. */
33
+ export declare function catalogProviders(opts?: CatalogOptions): Promise<CatalogProvider[]>;
34
+ /** One provider by id (e.g. `"openai"`), or `undefined` if the catalog has no such provider. */
35
+ export declare function catalogProvider(id: string, opts?: CatalogOptions): Promise<CatalogProvider | undefined>;
36
+ /** Flat list of models — all providers, or just one when `provider` is given. */
37
+ export declare function catalogModels(provider?: string, opts?: CatalogOptions): Promise<ModelInfo[]>;
38
+ /** A single model by (provider, id), or `undefined` if absent. */
39
+ export declare function lookupModel(provider: string, id: string, opts?: CatalogOptions): Promise<ModelInfo | undefined>;
40
+ /** Drop the in-process cache so the next call re-fetches. Mainly for tests and manual refresh. */
41
+ export declare function clearCatalogCache(): void;