@modelworks/quack 0.0.0-stage → 0.2.8

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.
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Project-level `.quack/` directory management: init, config/state read/write.
3
+ *
4
+ * @module
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ /** The `.quack/` directory name, placed at the project root. */
9
+ export const QUACK_DIR = ".quack";
10
+ /** Path to the `.quack/` directory in the current working directory. */
11
+ export function quackDir() {
12
+ return path.join(process.cwd(), QUACK_DIR);
13
+ }
14
+ /** Path to `.quack/config.json`. */
15
+ export function configPath() {
16
+ return path.join(quackDir(), "config.json");
17
+ }
18
+ /** Path to `.quack/state.json`. */
19
+ export function statePath() {
20
+ return path.join(quackDir(), "state.json");
21
+ }
22
+ /** Does a `.quack/` directory exist in the current project? */
23
+ export function isQuackProject() {
24
+ return fs.existsSync(quackDir()) && fs.existsSync(configPath());
25
+ }
26
+ /** Assert the current directory is a quack project; throw a friendly error if not. */
27
+ export function requireQuackProject() {
28
+ if (!isQuackProject()) {
29
+ throw new Error("Not a quack project. Run `quack init` first.");
30
+ }
31
+ }
32
+ /** Empty config template. `default_agent` defaults to `opencode`. */
33
+ function emptyConfig() {
34
+ return { version: 1, default_agent: "opencode", circuits: [] };
35
+ }
36
+ /** Empty state template. */
37
+ function emptyState() {
38
+ return { circuits: {} };
39
+ }
40
+ /** `.gitignore` contents for `.quack/` — ignores `state.json` (runs/ is committed). */
41
+ const GITIGNORE_CONTENTS = "state.json\ndebug.log\n";
42
+ /**
43
+ * `quack init` — create the `.quack/` directory with empty config,
44
+ * state, and `.gitignore`.
45
+ *
46
+ * Errors if `.quack/` already exists, unless `force` is `true`.
47
+ */
48
+ export function initProject(force = false) {
49
+ const dir = quackDir();
50
+ if (fs.existsSync(dir)) {
51
+ if (!force) {
52
+ throw new Error("Already a quack project. Use `--force` to overwrite.");
53
+ }
54
+ fs.rmSync(dir, { recursive: true });
55
+ }
56
+ fs.mkdirSync(dir, { recursive: true });
57
+ fs.mkdirSync(path.join(dir, "runs"), { recursive: true });
58
+ fs.writeFileSync(configPath(), JSON.stringify(emptyConfig(), null, 2) + "\n");
59
+ fs.writeFileSync(statePath(), JSON.stringify(emptyState(), null, 2) + "\n");
60
+ fs.writeFileSync(path.join(dir, ".gitignore"), GITIGNORE_CONTENTS);
61
+ console.log(`Initialized quack project in ${dir}`);
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // Config read/write (`.quack/config.json` — committable)
65
+ // ---------------------------------------------------------------------------
66
+ /** Read the project config. Throws if `.quack/` doesn't exist. */
67
+ export function readConfig() {
68
+ requireQuackProject();
69
+ const raw = fs.readFileSync(configPath(), "utf8");
70
+ return JSON.parse(raw);
71
+ }
72
+ /** Write the project config (atomic-ish: write then sync). */
73
+ export function writeConfig(config) {
74
+ requireQuackProject();
75
+ fs.writeFileSync(configPath(), JSON.stringify(config, null, 2) + "\n");
76
+ }
77
+ // ---------------------------------------------------------------------------
78
+ // State read/write (`.quack/state.json` — gitignored, per-user)
79
+ // ---------------------------------------------------------------------------
80
+ /**
81
+ * Read the project state.
82
+ *
83
+ * Returns an empty state if `.quack/` doesn't exist or `state.json`
84
+ * is missing (e.g. a freshly cloned repo where only `config.json`
85
+ * was committed). The caller is responsible for auto-syncing state
86
+ * from config when needed.
87
+ */
88
+ export function readState() {
89
+ if (!isQuackProject()) {
90
+ return emptyState();
91
+ }
92
+ try {
93
+ const raw = fs.readFileSync(statePath(), "utf8");
94
+ return JSON.parse(raw);
95
+ }
96
+ catch {
97
+ return emptyState();
98
+ }
99
+ }
100
+ /** Write the project state. */
101
+ export function writeState(state) {
102
+ requireQuackProject();
103
+ fs.writeFileSync(statePath(), JSON.stringify(state, null, 2) + "\n");
104
+ }
105
+ // ---------------------------------------------------------------------------
106
+ // Convenience operations
107
+ // ---------------------------------------------------------------------------
108
+ /** Add a circuit entry to the config (no-op if already present). Returns `true` if added. */
109
+ export function addCircuitToConfig(entry) {
110
+ const config = readConfig();
111
+ if (config.circuits.some((c) => c.circuit_uid === entry.circuit_uid)) {
112
+ return false;
113
+ }
114
+ config.circuits.push(entry);
115
+ writeConfig(config);
116
+ return true;
117
+ }
118
+ /** Remove a circuit from config by `circuit_uid`. Returns `true` if removed. */
119
+ export function removeCircuitFromConfig(circuitUid) {
120
+ const config = readConfig();
121
+ const before = config.circuits.length;
122
+ config.circuits = config.circuits.filter((c) => c.circuit_uid !== circuitUid);
123
+ if (config.circuits.length === before)
124
+ return false;
125
+ writeConfig(config);
126
+ return true;
127
+ }
128
+ /** Upsert a circuit's state entry (keyed by `circuit_uid`). */
129
+ export function upsertCircuitState(entry) {
130
+ const state = readState();
131
+ state.circuits[entry.circuit_uid] = entry;
132
+ writeState(state);
133
+ }
134
+ /** Remove a circuit's state entry. Returns `true` if removed. */
135
+ export function removeCircuitState(circuitUid) {
136
+ const state = readState();
137
+ if (!(circuitUid in state.circuits))
138
+ return false;
139
+ delete state.circuits[circuitUid];
140
+ writeState(state);
141
+ return true;
142
+ }
143
+ /** Get a circuit's state entry, or `null` if not present. */
144
+ export function getCircuitState(circuitUid) {
145
+ const state = readState();
146
+ return state.circuits[circuitUid] ?? null;
147
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Local HTTP reverse proxy.
3
+ *
4
+ * Forwards every request to the same path on `apiBase`, injecting
5
+ * `Authorization: Bearer <token>` and streaming the response body.
6
+ * On 401, refreshes the access token and retries once.
7
+ *
8
+ * @module
9
+ */
10
+ import type { ProxyHandle, ProxyOptions } from "./types.js";
11
+ /**
12
+ * Start a local HTTP reverse proxy on a random port (`127.0.0.1`).
13
+ *
14
+ * Resolves with a handle containing the assigned port and a `close()`
15
+ * method. The proxy lives for as long as quack runs; `launch.ts` closes
16
+ * it when the spawned opencode process exits.
17
+ */
18
+ export declare function startProxy(opts: ProxyOptions): Promise<ProxyHandle>;
package/dist/proxy.js ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Local HTTP reverse proxy.
3
+ *
4
+ * Forwards every request to the same path on `apiBase`, injecting
5
+ * `Authorization: Bearer <token>` and streaming the response body.
6
+ * On 401, refreshes the access token and retries once.
7
+ *
8
+ * @module
9
+ */
10
+ import http from "node:http";
11
+ /**
12
+ * Start a local HTTP reverse proxy on a random port (`127.0.0.1`).
13
+ *
14
+ * Resolves with a handle containing the assigned port and a `close()`
15
+ * method. The proxy lives for as long as quack runs; `launch.ts` closes
16
+ * it when the spawned opencode process exits.
17
+ */
18
+ export function startProxy(opts) {
19
+ return new Promise((resolve, reject) => {
20
+ const server = http.createServer(async (req, res) => {
21
+ const startTime = Date.now();
22
+ // Flush debug lines immediately so we see them even if the request hangs.
23
+ const debug = (line) => {
24
+ if (opts.onDebug)
25
+ opts.onDebug(line);
26
+ };
27
+ // TCP-level connection log — fires for EVERY request, even before
28
+ // HTTP parsing. This tells us if the request reached the proxy at all.
29
+ if (opts.onDebug) {
30
+ opts.onDebug(`[tcp] connection: ${req.method} ${req.url} from ${req.socket.remoteAddress}:${req.socket.remotePort}`);
31
+ }
32
+ try {
33
+ const targetUrl = new URL(req.url ?? "/", opts.apiBase);
34
+ // Debug: log incoming request immediately (before body reading).
35
+ debug(`=== ${new Date().toISOString()} ===`);
36
+ debug(`${req.method} ${req.url}`);
37
+ debug(`→ forwarded to ${targetUrl.href}`);
38
+ debug(`content-length: ${req.headers["content-length"] ?? "(none)"}`);
39
+ debug(`transfer-encoding: ${req.headers["transfer-encoding"] ?? "(none)"}`);
40
+ // Read the full request body before forwarding (needed for 401 retry).
41
+ const chunks = [];
42
+ for await (const chunk of req)
43
+ chunks.push(chunk);
44
+ const body = chunks.length > 0 ? Buffer.concat(chunks) : null;
45
+ debug(`body read: ${body ? body.length + " bytes" : "(empty)"}`);
46
+ // Build forwarded headers — strip client auth + host, inject our own.
47
+ const headers = {};
48
+ for (const [key, value] of Object.entries(req.headers)) {
49
+ const lower = key.toLowerCase();
50
+ if (lower === "authorization" || lower === "host")
51
+ continue;
52
+ headers[key] = Array.isArray(value) ? value.join(", ") : value ?? "";
53
+ }
54
+ headers.authorization = `Bearer ${opts.getAccessToken()}`;
55
+ headers.host = targetUrl.host;
56
+ if (opts.onDebug) {
57
+ debug(`--- request headers (incoming) ---`);
58
+ for (const [k, v] of Object.entries(req.headers)) {
59
+ const val = Array.isArray(v) ? v.join(", ") : v ?? "";
60
+ // Mask the real token in the debug log.
61
+ const masked = k.toLowerCase() === "authorization"
62
+ ? `Bearer ${val.slice(7, 12)}...`
63
+ : val;
64
+ debug(` ${k}: ${masked}`);
65
+ }
66
+ debug(`--- request body ---`);
67
+ debug(body ? body.toString("utf8").slice(0, 2000) : "(empty)");
68
+ }
69
+ // Forward request.
70
+ let upstream = await fetch(targetUrl, {
71
+ method: req.method,
72
+ headers,
73
+ body: body ?? undefined,
74
+ });
75
+ // Token refresh on 401 — retry once.
76
+ if (upstream.status === 401) {
77
+ debug(`--- 401 received, refreshing token and retrying ---`);
78
+ const refreshed = await opts.refreshToken();
79
+ if (refreshed) {
80
+ headers.authorization = `Bearer ${opts.getAccessToken()}`;
81
+ upstream = await fetch(targetUrl, {
82
+ method: req.method,
83
+ headers,
84
+ body: body ?? undefined,
85
+ });
86
+ }
87
+ }
88
+ // Forward response status + headers.
89
+ res.statusCode = upstream.status;
90
+ for (const [key, value] of upstream.headers) {
91
+ res.setHeader(key, value);
92
+ }
93
+ // Capture the circuit run UID for history saving.
94
+ const runUid = upstream.headers.get("x-circuit-run-uid");
95
+ const onRunComplete = opts.onRunComplete;
96
+ if (opts.onDebug) {
97
+ const elapsed = Date.now() - startTime;
98
+ debug(`--- response ${upstream.status} (${elapsed}ms) ---`);
99
+ debug(`--- response headers ---`);
100
+ for (const [k, v] of upstream.headers) {
101
+ debug(` ${k}: ${v}`);
102
+ }
103
+ }
104
+ // Stream the response body (SSE, NDJSON, JSON).
105
+ // Cast: `@types/node`'s `Readable.fromWeb` and the global
106
+ // `ReadableStream` from `fetch` have structurally incompatible
107
+ // `pipeThrough` types. Runtime is correct.
108
+ if (upstream.body) {
109
+ if (opts.onDebug) {
110
+ // Capture streamed body for debug logging.
111
+ let bodyBytes = 0;
112
+ const decoder = new TextDecoder();
113
+ let debugBody = "";
114
+ const reader = upstream.body.getReader();
115
+ const pump = async () => {
116
+ try {
117
+ for (;;) {
118
+ const { done, value } = await reader.read();
119
+ if (done)
120
+ break;
121
+ bodyBytes += value.length;
122
+ if (debugBody.length < 2000) {
123
+ debugBody += decoder.decode(value, { stream: true });
124
+ }
125
+ res.write(value);
126
+ }
127
+ res.end();
128
+ }
129
+ catch (err) {
130
+ debug(`--- stream error: ${err.message} ---`);
131
+ if (!res.writableEnded)
132
+ res.end();
133
+ }
134
+ debug(`--- response body (${bodyBytes} bytes) ---`);
135
+ debug(debugBody.slice(0, 2000) + (bodyBytes > 2000 ? "..." : ""));
136
+ const totalMs = Date.now() - startTime;
137
+ debug(`=== END ${req.method} ${req.url} ${upstream.status} (${totalMs}ms) ===\n`);
138
+ // Fire-and-forget: save run history after stream completes.
139
+ if (runUid && onRunComplete)
140
+ onRunComplete(runUid);
141
+ };
142
+ pump();
143
+ }
144
+ else {
145
+ const reader = upstream.body.getReader();
146
+ const pumpPlain = async () => {
147
+ try {
148
+ for (;;) {
149
+ const { done, value } = await reader.read();
150
+ if (done)
151
+ break;
152
+ res.write(value);
153
+ }
154
+ res.end();
155
+ }
156
+ catch {
157
+ if (!res.writableEnded)
158
+ res.end();
159
+ }
160
+ // Fire-and-forget: save run history after stream completes.
161
+ if (runUid && onRunComplete)
162
+ onRunComplete(runUid);
163
+ };
164
+ pumpPlain();
165
+ }
166
+ }
167
+ else {
168
+ res.end();
169
+ if (opts.onDebug) {
170
+ debug(`--- response body (empty) ---`);
171
+ const totalMs = Date.now() - startTime;
172
+ debug(`=== END ${req.method} ${req.url} ${upstream.status} (${totalMs}ms) ===\n`);
173
+ }
174
+ // Fire-and-forget: save run history even for empty-body responses.
175
+ if (runUid && onRunComplete)
176
+ onRunComplete(runUid);
177
+ }
178
+ opts.onLog?.(req.method ?? "?", targetUrl.pathname, upstream.status);
179
+ }
180
+ catch (err) {
181
+ debug(`--- proxy error: ${err.message} ---`);
182
+ if (!res.headersSent) {
183
+ res.statusCode = 502;
184
+ res.setHeader("content-type", "application/json");
185
+ res.end(JSON.stringify({
186
+ error: "proxy_error",
187
+ message: err.message,
188
+ }));
189
+ }
190
+ }
191
+ });
192
+ server.listen(0, "127.0.0.1", () => {
193
+ const addr = server.address();
194
+ if (!addr || typeof addr === "object" && !("port" in addr)) {
195
+ reject(new Error("Could not bind proxy port"));
196
+ return;
197
+ }
198
+ const port = addr.port;
199
+ resolve({ port, close: () => server.close() });
200
+ });
201
+ server.on("error", reject);
202
+ });
203
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Shared types for @modelworks/quack.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * Stored OAuth credentials for the authenticated user.
8
+ *
9
+ * Persisted at `~/.config/quack/credentials.json` (chmod 0600).
10
+ * The `api_base` field is stored here so token refresh hits the
11
+ * right server (supports `QUACK_API_BASE` / `--api-base` overrides).
12
+ */
13
+ export interface Credentials {
14
+ access_token: string;
15
+ refresh_token: string;
16
+ /** Unix timestamp (seconds) when the access token expires. */
17
+ expires_at: number;
18
+ token_type: string;
19
+ user_uid: string;
20
+ /** Base URL of the ModelWorks API, e.g. `https://modelworks.ai`. */
21
+ api_base: string;
22
+ }
23
+ /**
24
+ * Dynamic Client Registration record.
25
+ *
26
+ * Persisted at `~/.config/quack/client.json`. Created once on first
27
+ * `quack login`, reused on subsequent logins.
28
+ */
29
+ export interface ClientRegistration {
30
+ client_id: string;
31
+ }
32
+ /**
33
+ * One entry in `.quack/config.json`'s `circuits` array.
34
+ *
35
+ * Committable and shareable across a team — contains only public
36
+ * metadata, no user-specific state.
37
+ */
38
+ export interface CircuitConfigEntry {
39
+ url: string;
40
+ circuit_uid: string;
41
+ name: string;
42
+ /** ISO 8601 timestamp when the circuit was added. */
43
+ added_at: string;
44
+ }
45
+ /**
46
+ * One entry in `.quack/state.json`'s `circuits` map, keyed by `circuit_uid`.
47
+ *
48
+ * Gitignored, per-user — a snapshot from `quack add` / `quack refresh`
49
+ * / `quack opencode` auto-sync. Holds everything `quack opencode`
50
+ * needs to build the opencode config without re-fetching.
51
+ */
52
+ export interface CircuitStateEntry {
53
+ circuit_uid: string;
54
+ /** The RUNNING version resolved at add/refresh time. */
55
+ version_uid: string;
56
+ circuit_name: string;
57
+ /** Owner's username (e.g. `alice`), for display as `<username>/<circuit_name>`. */
58
+ owner_username: string;
59
+ /**
60
+ * Adapter types the circuit supports, e.g.
61
+ * `["OPEN_AI_SSE", "MODELWORKS_HTTP", "MCP"]`.
62
+ *
63
+ * Discovered from the circuit version's `json_blob` `adapters`
64
+ * array when visible (public/owner), or from listing existing
65
+ * adaptors when `json_blob` is hidden (protected, non-owner).
66
+ */
67
+ adapters: string[];
68
+ /**
69
+ * UID (`ca-xxx`) of the temporal OpenAI SSE adaptor quack created
70
+ * for this circuit, or `null` if the circuit isn't SSE-compatible.
71
+ */
72
+ sse_adaptor_uid: string | null;
73
+ /**
74
+ * Map of adapter type → adaptor UID (`ca-xxx`) for each temporal
75
+ * adaptor quack created. Currently only `OPEN_AI_SSE` is created
76
+ * by quack, but existing adaptors discovered via the list endpoint
77
+ * are also stored here.
78
+ */
79
+ adaptor_uids: Record<string, string>;
80
+ /** ISO 8601 timestamp of the last resolution. */
81
+ resolved_at: string;
82
+ }
83
+ /**
84
+ * `.quack/config.json` — the project-level, committable config.
85
+ */
86
+ export interface QuackConfig {
87
+ version: number;
88
+ /**
89
+ * Agent to launch when the user runs bare `quack` (no args).
90
+ * Optional — if absent, the fallback chain (last_agent →
91
+ * interactive pick → error) resolves the agent.
92
+ */
93
+ default_agent?: string;
94
+ /**
95
+ * Circuit UID (`c-xxx`) to use as the default model in OpenCode.
96
+ * Team-shared — committed to `config.json`. Set via `quack circuit use`
97
+ * or auto-set to the first SSE-compatible circuit on `quack circuit add`.
98
+ * At launch, resolved to an OpenCode model ID and injected as `model`.
99
+ */
100
+ default_circuit?: string;
101
+ circuits: CircuitConfigEntry[];
102
+ }
103
+ /**
104
+ * `.quack/state.json` — the per-user, gitignored cached state.
105
+ */
106
+ export interface QuackState {
107
+ circuits: Record<string, CircuitStateEntry>;
108
+ /**
109
+ * The last agent successfully launched by bare `quack`.
110
+ * Per-user convenience — used as a fallback when
111
+ * `default_agent` is not set in config.json.
112
+ */
113
+ last_agent?: string;
114
+ }
115
+ /**
116
+ * Parsed circuit URL or bare UID.
117
+ */
118
+ export interface ParsedCircuitRef {
119
+ circuit_uid: string;
120
+ version_uid: string | null;
121
+ }
122
+ /**
123
+ * A circuit version record as returned by the ModelWorks API
124
+ * (`GET /circuits/<uid>/versions` or `GET /circuit-versions/<uid>`).
125
+ *
126
+ * Only the fields quack reads are typed here. `json_blob` is optional
127
+ * because it's hidden for protected circuits when the caller is not
128
+ * the owner (see `circuit_version.rs` visibility rules).
129
+ */
130
+ export interface CircuitVersionPublic {
131
+ uid: string;
132
+ circuit_uid: string;
133
+ state: string;
134
+ json_blob: string | null;
135
+ updated_at: string;
136
+ }
137
+ /**
138
+ * A circuit record as returned by `GET /circuits/<uid>`.
139
+ */
140
+ export interface CircuitPublic {
141
+ uid: string;
142
+ name: string;
143
+ user_uid: string;
144
+ is_private: boolean;
145
+ is_protected: boolean;
146
+ }
147
+ /**
148
+ * A circuit adaptor record as returned by
149
+ * `GET /circuit-adaptors/by-circuit-version/<version_uid>`.
150
+ */
151
+ export interface CircuitAdaptorPublic {
152
+ uid: string;
153
+ circuit_version_uid: string;
154
+ adapter_type: string;
155
+ user_uid: string | null;
156
+ user_token_uid: string | null;
157
+ }
158
+ /**
159
+ * A circuit run record stored locally at `.quack/runs/cr-xxx.json`.
160
+ *
161
+ * Committed to the repo (team-shared). Contains only public metadata —
162
+ * no request body, no response body, nothing sensitive.
163
+ */
164
+ export interface RunRecord {
165
+ run_uid: string;
166
+ circuit_uid: string;
167
+ circuit_name: string;
168
+ owner_username: string;
169
+ caller_username: string;
170
+ version_uid: string;
171
+ status: string;
172
+ created_at: string;
173
+ completed_at: string;
174
+ duration_ms: number | null;
175
+ cost_credits: number | null;
176
+ input_tokens: number | null;
177
+ output_tokens: number | null;
178
+ }
179
+ /**
180
+ * Handle to a running local proxy.
181
+ */
182
+ export interface ProxyHandle {
183
+ port: number;
184
+ close: () => void;
185
+ }
186
+ /**
187
+ * Options for {@link startProxy}.
188
+ */
189
+ export interface ProxyOptions {
190
+ /** Upstream API base URL, e.g. `https://modelworks.ai`. */
191
+ apiBase: string;
192
+ /** Returns the current access token (refreshed if needed before calling). */
193
+ getAccessToken: () => string;
194
+ /** Refreshes the access token. Returns `true` on success. */
195
+ refreshToken: () => Promise<boolean>;
196
+ /** Optional request logger for `--verbose`. */
197
+ onLog?: (method: string, path: string, status: number) => void;
198
+ /** Optional debug logger — full request/response details to a string sink. */
199
+ onDebug?: (entry: string) => void;
200
+ /**
201
+ * Called after a response stream completes, if the response included
202
+ * an `X-Circuit-Run-Uid` header. Fire-and-forget — the proxy does not
203
+ * await this. Used to fetch and save the run record locally.
204
+ */
205
+ onRunComplete?: (runUid: string) => void;
206
+ }
207
+ /**
208
+ * The OpenCode config that quack injects via `OPENCODE_CONFIG_CONTENT`.
209
+ *
210
+ * Only the two top-level keys quack touches are typed here.
211
+ */
212
+ export interface OpenCodeConfig {
213
+ $schema?: string;
214
+ /** Default model ID, e.g. `circuit-wandb-glm-52/wandb-glm-52`. */
215
+ model?: string;
216
+ mcp?: {
217
+ servers: Record<string, OpenCodeMcpServer>;
218
+ };
219
+ providers?: Record<string, OpenCodeProvider>;
220
+ }
221
+ export interface OpenCodeMcpServer {
222
+ type: "remote";
223
+ url: string;
224
+ oauth: false;
225
+ protocol: "auto" | "2026-07-28" | "legacy";
226
+ headers?: Record<string, string>;
227
+ }
228
+ export interface OpenCodeProvider {
229
+ name: string;
230
+ package: string;
231
+ env?: string[];
232
+ settings: {
233
+ baseURL: string;
234
+ apiKey?: string;
235
+ };
236
+ models: Record<string, {
237
+ name: string;
238
+ }>;
239
+ }
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared types for @modelworks/quack.
3
+ *
4
+ * @module
5
+ */
6
+ export {};
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@modelworks/quack",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.8",
4
+ "description": "Launch AI coding agents (OpenCode, and more) with ModelWorks circuits as model providers. OAuth login, local proxy with auth injection, run history, and git-like project config.",
5
+ "type": "module",
6
+ "bin": {
7
+ "quack": "dist/bin.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "scripts": {
13
+ "prebuild": "node scripts/build-info.mjs",
14
+ "build": "tsc",
15
+ "prepublishOnly": "node scripts/build-info.mjs && tsc",
16
+ "dev": "tsc --watch",
17
+ "test": "tsc -p tsconfig.test.json && node --test dist-test/test/*.test.js",
18
+ "test:run": "node --test dist-test/test/*.test.js"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.20.5",
22
+ "typescript": "^5.5.0"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "license": "MIT",
31
+ "keywords": [
32
+ "modelworks",
33
+ "opencode",
34
+ "ai",
35
+ "circuit",
36
+ "proxy",
37
+ "mcp",
38
+ "cli",
39
+ "launcher"
40
+ ],
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/modelworks-ai/quack.git"
44
+ },
45
+ "homepage": "https://github.com/modelworks-ai/quack",
46
+ "bugs": {
47
+ "url": "https://github.com/modelworks-ai/quack/issues"
48
+ },
49
+ "dependencies": {
50
+ "commander": "^15.0.0"
51
+ }
52
+ }