@xynogen/pix-9router 0.3.10 → 0.3.14

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 CHANGED
@@ -35,7 +35,7 @@ export ROUTER_API_KEY="your-key-here"
35
35
 
36
36
  - **Provider**: on load, fetches `/models` from the router and registers them with Pi. The [modelgrep](https://modelgrep.com) catalog (via [`@xynogen/pix-data`](https://github.com/xynogen/pix-mono/tree/main/packages/pix-data)'s shared cache) is used internally to fill missing context window / modality fields where the router response omits them. Model list is cached at `~/.cache/pi/9router.json` (TTL 30 min).
37
37
  - **fetch / search**: POST to `/web/fetch` and `/search` on the router (which proxies to exa). If the router is unreachable, falls back to `curl` (for `fetch`, raw URL fetch; for `search`, the same `/search` endpoint via curl). Tool output is rendered dimmed so fetched web content reads like faded context rather than primary output.
38
- - **transcribe**: POST to `/audio/transcriptions` (Deepgram Nova 3 by default). Accepts any audio file path; falls back to curl.
38
+ - **transcribe**: POST to `/audio/transcriptions` (Deepgram Nova 3 by default). Accepts any audio file path; falls back to curl. Pass the optional `output_file` to write the full transcription to disk (parent dirs created, path pre-flight checked) and return only a short path summary to the model — useful for long results; without it the text is returned inline (truncated to 50,000 chars).
39
39
 
40
40
  ## Compact results
41
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-9router",
3
- "version": "0.3.10",
3
+ "version": "0.3.14",
4
4
  "description": "Pi extension — 9Router provider + fetch/search tools via router API",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -43,9 +43,9 @@
43
43
  "@earendil-works/pi-tui": "*"
44
44
  },
45
45
  "dependencies": {
46
- "@xynogen/pix-data": "^0.4.0",
47
- "@xynogen/pix-pretty": "^1.7.21",
46
+ "@xynogen/pix-data": "^0.4.3",
47
+ "@xynogen/pix-pretty": "^1.11.2",
48
48
  "typebox": "^1.1.38",
49
- "@xynogen/pix-runtime": "^0.4.0"
49
+ "@xynogen/pix-runtime": "^0.5.3"
50
50
  }
51
51
  }
package/src/fetch.ts CHANGED
@@ -241,10 +241,12 @@ export default function registerFetch(pi: ExtensionAPI): void {
241
241
  ),
242
242
  parameters: Type.Object({
243
243
  url: Type.String({ description: "URL to fetch" }),
244
- format: StringEnum(["markdown", "text", "html"] as const, {
245
- description:
246
- 'Required choice. Enter exactly "markdown" for readable Markdown, "text" for plain text, or "html" for raw HTML.',
247
- }),
244
+ format: Type.Optional(
245
+ StringEnum(["markdown", "text", "html"] as const, {
246
+ description:
247
+ 'Optional choice. Enter exactly "markdown" for readable Markdown, "text" for plain text, or "html" for raw HTML. Default is "markdown".',
248
+ }),
249
+ ),
248
250
  max_characters: Type.Optional(
249
251
  Type.Number({
250
252
  description: "Max characters (default 1000, 0 = unlimited)",
@@ -101,6 +101,30 @@ describe("fetch execution metadata", () => {
101
101
  expect(result.content).toEqual(originalContent);
102
102
  });
103
103
 
104
+ it("defaults format to markdown when omitted (schema now optional)", async () => {
105
+ let sentFormat: unknown;
106
+ const raw = JSON.stringify({ url: "https://example.com", content: { text: "hello" } });
107
+ const result = await executeFetch(
108
+ // No `format` — valid now that the schema no longer requires it.
109
+ { url: "https://example.com" },
110
+ undefined,
111
+ undefined,
112
+ {
113
+ apiPost: async (_path, body) => {
114
+ sentFormat = (body as { format?: unknown }).format;
115
+ return raw;
116
+ },
117
+ curl: async () => {
118
+ throw new Error("curl should not run");
119
+ },
120
+ },
121
+ );
122
+
123
+ // The default flows through to both the API request and the metadata.
124
+ expect(sentFormat).toBe("markdown");
125
+ expect(result.details).toMatchObject({ outcome: "success", format: "markdown" });
126
+ });
127
+
104
128
  it("reports curl fallback metadata while preserving the fallback banner", async () => {
105
129
  const result = await executeFetch(
106
130
  { url: "https://example.com", format: "html", max_characters: 1000 },
@@ -4,6 +4,7 @@ import registerSearch from "./search.ts";
4
4
 
5
5
  function captureParameters(register: (pi: never) => void): {
6
6
  properties: Record<string, { type?: string; enum?: string[]; description?: string }>;
7
+ required?: string[];
7
8
  } {
8
9
  let parameters: unknown;
9
10
  register({
@@ -14,6 +15,7 @@ function captureParameters(register: (pi: never) => void): {
14
15
  if (!parameters) throw new Error("tool parameters not captured");
15
16
  return parameters as {
16
17
  properties: Record<string, { type?: string; enum?: string[]; description?: string }>;
18
+ required?: string[];
17
19
  };
18
20
  }
19
21
 
@@ -38,3 +40,21 @@ describe("enum-like tool parameters", () => {
38
40
  expect(format.description).toContain('"html"');
39
41
  });
40
42
  });
43
+
44
+ describe("fetch parameter requiredness", () => {
45
+ test("only url is required; format is optional and defaults to markdown", () => {
46
+ const params = captureParameters(registerFetch);
47
+
48
+ // url must be required, format must NOT be — this is the exact validator
49
+ // bug the optionality fix addresses (executeFetch already defaults format
50
+ // to "markdown"), so guard against a regression back to a required format.
51
+ expect(params.required).toEqual(["url"]);
52
+ expect(params.required).not.toContain("format");
53
+
54
+ // format stays a fully described enum in properties even while optional.
55
+ const format = params.properties.format;
56
+ if (!format) throw new Error("format schema not found");
57
+ expect(format.enum).toEqual(["markdown", "text", "html"]);
58
+ expect(format.description?.toLowerCase()).toContain("default");
59
+ });
60
+ });