@modelworks/quack 0.2.8 → 0.2.9

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/dist/bin.js CHANGED
@@ -247,7 +247,7 @@ async function cmdCircuitAdd(url) {
247
247
  }
248
248
  }
249
249
  async function cmdCircuitRemove(ref) {
250
- const result = await removeCircuit(ref);
250
+ const result = await removeCircuit(ref, effectiveApiBase());
251
251
  console.log(`Removed circuit '${result.name}' (${result.circuit_uid}).`);
252
252
  // Clear default_circuit if it pointed to the removed circuit.
253
253
  const config = readConfig();
@@ -404,6 +404,19 @@ async function cmdAgentDescribe(name) {
404
404
  console.log(`Default: yes`);
405
405
  }
406
406
  }
407
+ async function cmdAgentSet(name) {
408
+ const agent = getAgent(name);
409
+ if (!agent) {
410
+ console.error(`Unknown agent: ${name}`);
411
+ console.error("Run `quack agent list` to see available agents.");
412
+ process.exit(1);
413
+ }
414
+ requireQuackProject();
415
+ const config = readConfig();
416
+ config.default_agent = name;
417
+ writeConfig(config);
418
+ console.log(`Default agent set to ${GREEN}${name}${RESET}.`);
419
+ }
407
420
  // ---------------------------------------------------------------------------
408
421
  // Interactive picker
409
422
  // ---------------------------------------------------------------------------
@@ -713,6 +726,12 @@ function buildCli() {
713
726
  .action(async (name) => {
714
727
  await cmdAgentDescribe(name);
715
728
  });
729
+ agent
730
+ .command("set <name>")
731
+ .description("Set the default agent (written to .quack/config.json)")
732
+ .action(async (name) => {
733
+ await cmdAgentSet(name);
734
+ });
716
735
  // auth group
717
736
  const auth = program
718
737
  .command("auth")
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.2.8",
3
- "build": "20261025131502",
4
- "commit": "afaf6a2",
5
- "commitLong": "afaf6a2cd8edc9c225df99eb55daa81944de4761",
2
+ "version": "0.2.9",
3
+ "build": "20261027101714",
4
+ "commit": "a1daf81",
5
+ "commitLong": "a1daf819f41569ed710ee0697ec56db7ab370339",
6
6
  "dirty": false,
7
- "builtAt": "2026-10-25T13:15:02.360Z"
7
+ "builtAt": "2026-10-27T10:17:14.504Z"
8
8
  }
package/dist/circuit.d.ts CHANGED
@@ -25,6 +25,7 @@ export declare function fromApiAdapterType(apiType: string): string;
25
25
  * Accepts:
26
26
  * - `https://modelworks.ai/circuits/<circuit_uid>`
27
27
  * - `https://modelworks.ai/circuits/<circuit_uid>/versions/<version_uid>`
28
+ * - `https://modelworks.ai/<username>/<circuitName>` (slug-based URL)
28
29
  * - `<circuit_uid>` or `c-xxx`
29
30
  * - `<version_uid>` or `cv-xxx`
30
31
  */
@@ -99,7 +100,7 @@ export declare function addCircuit(input: string, apiBase: string): Promise<AddR
99
100
  * Public adaptors are NOT deleted — they belong to the circuit (system-created),
100
101
  * not to the quack user. Other users of the same circuit rely on them.
101
102
  */
102
- export declare function removeCircuit(input: string): Promise<{
103
+ export declare function removeCircuit(input: string, apiBase: string): Promise<{
103
104
  circuit_uid: string;
104
105
  name: string;
105
106
  }>;
package/dist/circuit.js CHANGED
@@ -59,16 +59,17 @@ export function fromApiAdapterType(apiType) {
59
59
  * Accepts:
60
60
  * - `https://modelworks.ai/circuits/<circuit_uid>`
61
61
  * - `https://modelworks.ai/circuits/<circuit_uid>/versions/<version_uid>`
62
+ * - `https://modelworks.ai/<username>/<circuitName>` (slug-based URL)
62
63
  * - `<circuit_uid>` or `c-xxx`
63
64
  * - `<version_uid>` or `cv-xxx`
64
65
  */
65
66
  export function parseCircuitRef(input) {
66
67
  // Bare UIDs first.
67
68
  if (input.startsWith("cv-")) {
68
- return { circuit_uid: "", version_uid: input };
69
+ return { circuit_uid: "", version_uid: input, slug: null };
69
70
  }
70
71
  if (input.startsWith("c-")) {
71
- return { circuit_uid: input, version_uid: null };
72
+ return { circuit_uid: input, version_uid: null, slug: null };
72
73
  }
73
74
  // URLs.
74
75
  try {
@@ -76,16 +77,21 @@ export function parseCircuitRef(input) {
76
77
  const parts = url.pathname.split("/").filter(Boolean);
77
78
  // Expected: ["circuits", "<circuit_uid>"] or ["circuits", "<circuit_uid>", "versions", "<version_uid>"]
78
79
  const circuitsIdx = parts.indexOf("circuits");
79
- if (circuitsIdx === -1 || circuitsIdx + 1 >= parts.length) {
80
- throw new Error(`Could not find circuit UID in URL: ${input}`);
80
+ if (circuitsIdx !== -1 && circuitsIdx + 1 < parts.length) {
81
+ const circuit_uid = parts[circuitsIdx + 1];
82
+ let version_uid = null;
83
+ const versionsIdx = parts.indexOf("versions", circuitsIdx);
84
+ if (versionsIdx !== -1 && versionsIdx + 1 < parts.length) {
85
+ version_uid = parts[versionsIdx + 1];
86
+ }
87
+ return { circuit_uid, version_uid, slug: null };
81
88
  }
82
- const circuit_uid = parts[circuitsIdx + 1];
83
- let version_uid = null;
84
- const versionsIdx = parts.indexOf("versions", circuitsIdx);
85
- if (versionsIdx !== -1 && versionsIdx + 1 < parts.length) {
86
- version_uid = parts[versionsIdx + 1];
89
+ // Slug-based URL: ["<username>", "<circuitName>"]
90
+ if (parts.length === 2) {
91
+ const slug = `${parts[0]}/${parts[1]}`;
92
+ return { circuit_uid: "", version_uid: null, slug };
87
93
  }
88
- return { circuit_uid, version_uid };
94
+ throw new Error(`Could not find circuit UID in URL: ${input}`);
89
95
  }
90
96
  catch (err) {
91
97
  throw new Error(`Invalid circuit reference: ${input}. Use a circuit URL, c-xxx, or cv-xxx. (${err.message})`);
@@ -156,6 +162,16 @@ export async function getUsername(apiBase, userUid) {
156
162
  const payload = await parseJson(res);
157
163
  return unwrap(payload).username ?? "unknown";
158
164
  }
165
+ /** Resolve a slug (`"username/circuitName"`) to a circuit UID via the API. */
166
+ async function resolveSlug(apiBase, slug) {
167
+ const res = await authedFetch(apiBase, `/api/slug/${slug}`);
168
+ const data = unwrap(await parseJson(res));
169
+ if (data.entity_type !== "circuit") {
170
+ throw new Error(`'${slug}' resolved to a ${data.entity_type}, not a circuit. ` +
171
+ `Use a circuit URL, c-xxx, or cv-xxx.`);
172
+ }
173
+ return data.entity_uid;
174
+ }
159
175
  /**
160
176
  * Resolve a circuit reference to a RUNNING version.
161
177
  *
@@ -165,6 +181,11 @@ export async function getUsername(apiBase, userUid) {
165
181
  */
166
182
  export async function resolveRunningVersion(apiBase, ref) {
167
183
  let circuitUid = ref.circuit_uid;
184
+ // Slug-based URL (e.g. https://modelworks.ai/openscale/deepseek-v41-flash):
185
+ // resolve the slug to a circuit UID via the API before proceeding.
186
+ if (!circuitUid && ref.slug) {
187
+ circuitUid = await resolveSlug(apiBase, ref.slug);
188
+ }
168
189
  // If only a version UID was given, fetch the version first to discover
169
190
  // its parent circuit UID.
170
191
  if (!circuitUid && ref.version_uid) {
@@ -327,11 +348,25 @@ export async function addCircuit(input, apiBase) {
327
348
  * Public adaptors are NOT deleted — they belong to the circuit (system-created),
328
349
  * not to the quack user. Other users of the same circuit rely on them.
329
350
  */
330
- export async function removeCircuit(input) {
351
+ export async function removeCircuit(input, apiBase) {
331
352
  const ref = parseCircuitRef(input);
332
- const circuitUid = ref.circuit_uid;
333
- // Find the circuit name from config.
353
+ let circuitUid = ref.circuit_uid;
334
354
  const config = readConfig();
355
+ // For slug-based URLs, try matching by the stored `url` first (no API call).
356
+ // If that fails, resolve the slug via the API to get the circuit UID.
357
+ if (ref.slug && !circuitUid) {
358
+ const byUrl = config.circuits.find((c) => c.url === input);
359
+ if (byUrl) {
360
+ circuitUid = byUrl.circuit_uid;
361
+ }
362
+ else {
363
+ circuitUid = await resolveSlug(apiBase, ref.slug);
364
+ }
365
+ }
366
+ if (!circuitUid) {
367
+ throw new Error("Could not resolve circuit UID from the given reference.");
368
+ }
369
+ // Find the circuit name from config.
335
370
  const entry = config.circuits.find((c) => c.circuit_uid === circuitUid);
336
371
  if (!entry) {
337
372
  throw new Error(`Circuit ${circuitUid} is not in this project.`);
package/dist/install.js CHANGED
@@ -266,6 +266,10 @@ async function main() {
266
266
  fs.mkdirSync(binDir, { recursive: true });
267
267
  fs.rmSync(distDir, { recursive: true, force: true });
268
268
  copyDir(sourceDist, distDir);
269
+ // Write a minimal package.json so Node treats dist/ as ESM.
270
+ // (The source package.json with "type": "module" is at the tarball
271
+ // root, not inside dist/, so it doesn't get copied.)
272
+ fs.writeFileSync(path.join(distDir, "package.json"), JSON.stringify({ type: "module" }, null, 2) + "\n");
269
273
  spinner.succeed(`Installed to ${installDir}`);
270
274
  }
271
275
  catch (err) {
package/dist/proxy.js CHANGED
@@ -25,9 +25,9 @@ export function startProxy(opts) {
25
25
  opts.onDebug(line);
26
26
  };
27
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.
28
+ // HTTP parsing.
29
29
  if (opts.onDebug) {
30
- opts.onDebug(`[tcp] connection: ${req.method} ${req.url} from ${req.socket.remoteAddress}:${req.socket.remotePort}`);
30
+ opts.onDebug(`[tcp] ${new Date().toISOString()} ${req.method} ${req.url} from ${req.socket.remoteAddress}:${req.socket.remotePort}`);
31
31
  }
32
32
  try {
33
33
  const targetUrl = new URL(req.url ?? "/", opts.apiBase);
@@ -41,8 +41,45 @@ export function startProxy(opts) {
41
41
  const chunks = [];
42
42
  for await (const chunk of req)
43
43
  chunks.push(chunk);
44
- const body = chunks.length > 0 ? Buffer.concat(chunks) : null;
44
+ let body = chunks.length > 0 ? Buffer.concat(chunks) : null;
45
45
  debug(`body read: ${body ? body.length + " bytes" : "(empty)"}`);
46
+ // Strip output token limits from circuit adaptor query requests.
47
+ //
48
+ // OpenCode's openai-compatible provider sends a low default output
49
+ // token limit (~100-128). Reasoning models like DeepSeek V4.1 Flash
50
+ // spend this budget on reasoning before producing content, resulting
51
+ // in empty responses. Stripping lets the circuit's own default_value
52
+ // (e.g. 128000) apply instead.
53
+ //
54
+ // Different SDKs use different field names for the same concept:
55
+ // - OpenAI Chat Completions API: max_tokens / max_completion_tokens
56
+ // - AI SDK: maxOutputTokens
57
+ const isAdaptorQuery = req.method === "POST" &&
58
+ /\/api\/circuit-adaptors\/[^/]+\/query/.test(req.url ?? "");
59
+ if (isAdaptorQuery && body) {
60
+ try {
61
+ const parsed = JSON.parse(body.toString("utf8"));
62
+ let stripped = false;
63
+ for (const key of [
64
+ "max_tokens",
65
+ "max_completion_tokens",
66
+ "max_output_tokens",
67
+ "maxOutputTokens",
68
+ ]) {
69
+ if (key in parsed) {
70
+ delete parsed[key];
71
+ stripped = true;
72
+ }
73
+ }
74
+ if (stripped) {
75
+ body = Buffer.from(JSON.stringify(parsed), "utf8");
76
+ debug(`stripped output token limit from request body`);
77
+ }
78
+ }
79
+ catch {
80
+ // Not JSON or malformed — forward as-is.
81
+ }
82
+ }
46
83
  // Build forwarded headers — strip client auth + host, inject our own.
47
84
  const headers = {};
48
85
  for (const [key, value] of Object.entries(req.headers)) {
@@ -53,6 +90,9 @@ export function startProxy(opts) {
53
90
  }
54
91
  headers.authorization = `Bearer ${opts.getAccessToken()}`;
55
92
  headers.host = targetUrl.host;
93
+ // Update content-length if we modified the body.
94
+ if (body !== null)
95
+ headers["content-length"] = String(body.length);
56
96
  if (opts.onDebug) {
57
97
  debug(`--- request headers (incoming) ---`);
58
98
  for (const [k, v] of Object.entries(req.headers)) {
@@ -88,6 +128,17 @@ export function startProxy(opts) {
88
128
  // Forward response status + headers.
89
129
  res.statusCode = upstream.status;
90
130
  for (const [key, value] of upstream.headers) {
131
+ // Rewrite content-type for circuit adaptor SSE responses.
132
+ // The API returns `application/stream+json` but OpenCode's
133
+ // openai-compatible provider expects `text/event-stream` to
134
+ // parse SSE chunks. Without this, the provider doesn't
135
+ // recognize the stream and the response appears empty.
136
+ if (key.toLowerCase() === "content-type" &&
137
+ isAdaptorQuery &&
138
+ value.includes("application/stream+json")) {
139
+ res.setHeader(key, "text/event-stream; charset=utf-8");
140
+ continue;
141
+ }
91
142
  res.setHeader(key, value);
92
143
  }
93
144
  // Capture the circuit run UID for history saving.
package/dist/types.d.ts CHANGED
@@ -114,10 +114,16 @@ export interface QuackState {
114
114
  }
115
115
  /**
116
116
  * Parsed circuit URL or bare UID.
117
+ *
118
+ * For slug-based URLs (`/{username}/{circuitName}`), `slug` is set and
119
+ * `circuit_uid` is empty — the caller must resolve the slug to a circuit
120
+ * UID via the API (`GET /api/slug/{slug}`).
117
121
  */
118
122
  export interface ParsedCircuitRef {
119
123
  circuit_uid: string;
120
124
  version_uid: string | null;
125
+ /** `"username/circuitName"` for slug-based URLs, null for UID-based refs. */
126
+ slug: string | null;
121
127
  }
122
128
  /**
123
129
  * A circuit version record as returned by the ModelWorks API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelworks/quack",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
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
5
  "type": "module",
6
6
  "bin": {