@webmcp-auto-ui/agent 2.5.27 → 2.5.28

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.
@@ -1134,7 +1134,7 @@ Do NOT render any widget.
1134
1134
  id: create-interactive-notebook-playbook
1135
1135
  name: Create an interactive notebook playbook
1136
1136
  components_used: [notebook-compact, notebook-workspace, notebook-document, notebook-editorial]
1137
- when: the user wants to experiment with data, prototype a small analysis, share a reusable scenario, let others fork and try a dataset, or prepare a hackathon-ready playground. Keywords include "playground", "playbook", "experiment", "try", "prototype", "hackathon", "share a notebook", "template", "starter".
1137
+ when: the user wants to experiment with data, prototype a small analysis, share a reusable scenario, or prepare a hackathon-ready playground. Keywords include "playground", "playbook", "experiment", "try", "prototype", "hackathon", "share a notebook", "template", "starter", "publish".
1138
1138
  servers: [autoui]
1139
1139
  layout:
1140
1140
  type: single
@@ -1148,6 +1148,7 @@ The user asks for a **notebook-like interactive playground** that combines text,
1148
1148
  - "I want to prototype a small analysis"
1149
1149
  - "Set up a hackathon starter"
1150
1150
  - "Make a reusable template for exploring CSVs / this API / these tables"
1151
+ - "Publish this analysis as a short memo"
1151
1152
 
1152
1153
  This recipe applies across domains (parliamentary data, biodiversity, news, business datasets, etc.) — it only prescribes the **shape** of the answer, not its content.
1153
1154
 
@@ -1160,8 +1161,8 @@ Choose one of the four \`notebook-*\` widgets based on the user's implicit inten
1160
1161
  | Layout | Use when |
1161
1162
  |---|---|
1162
1163
  | \`notebook-compact\` | Quick data exploration, reactive dataflow with named outputs, minimal chrome. **Default for most "playground" and "hackathon" requests.** |
1163
- | \`notebook-workspace\` | The user expects a multi-cell analyst workspace with sources, cell navigation, and a "publish" step. Use when they mention "dashboard", "app", "workspace". |
1164
- | \`notebook-document\` | The user plans to share and discuss with a team. Use when "collaborate", "review", "comment" appear. |
1164
+ | \`notebook-workspace\` | The user expects a multi-cell analyst workspace with sources, cell navigation, \`run all\`, and a \`publish\` step. Use when they mention "dashboard", "app", "workspace", "publish". |
1165
+ | \`notebook-document\` | The user plans to share and discuss with a team. Use when "collaborate", "review", "comment", "reply" appear. |
1165
1166
  | \`notebook-editorial\` | The user wants a polished, article-like final deliverable mixing prose and code. Use for "memo", "report", "writeup", "blog-style". |
1166
1167
 
1167
1168
  When in doubt, pick \`notebook-compact\`.
@@ -1172,8 +1173,8 @@ Never create an empty notebook. Always seed with 3–5 cells that give the user
1172
1173
 
1173
1174
  1. **First cell: markdown** — title + one-sentence context of what the notebook is for
1174
1175
  2. **Second cell: markdown or code** — if an MCP data source is connected, a starter query that returns something visible (e.g. \`SELECT * FROM {table} LIMIT 10\`). Otherwise a markdown cell describing the next step
1175
- 3. **Third cell: code** — a transformation or a visualization that uses the output of step 2. Use \`varname\` on the SQL cell (\`varname: "rows"\`) and reference it in the JS cell
1176
- 4. **Last cell: markdown** — a short "to you to play" note inviting the user to add cells, edit, or fork
1176
+ 3. **Third cell: code** — a transformation or a visualization that uses the output of step 2. Use \`varname\` on the SQL cell (\`varname: "rows"\`) and reference it in the JS cell — this activates the **reactive dataflow** (the downstream JS cell is flagged stale automatically when its upstream re-runs)
1177
+ 4. **Last cell: markdown** — a short "to you to play" note inviting the user to add cells or edit
1177
1178
 
1178
1179
  Example seed for a generic data playground:
1179
1180
 
@@ -1198,14 +1199,61 @@ If a specific MCP server is connected, replace the generic \`source\` and \`sele
1198
1199
 
1199
1200
  Always keep queries **short** and **limited** so the first run returns quickly and visually.
1200
1201
 
1201
- ### Step 4 Share and fork
1202
+ SQL cells are dispatched automatically to the server's \`*_query_sql\` tool (first match). JS cells run in a Web Worker with upstream named outputs injected as scope.
1203
+
1204
+ ### Step 4 — Exporting & publishing
1205
+
1206
+ All four notebook layouts share the same \`share\` button in the toolbar, offering **four export formats**:
1207
+
1208
+ | Format | What it does |
1209
+ |---|---|
1210
+ | **Hyperskill link** | Copies both the canonical Hyperskill URL and a short domain-scoped URL (\`?n=<token>\`). The short URL opens the read-only public viewer at \`nb.hyperskills.net\`. |
1211
+ | **Markdown** | Downloads a \`.md\` file containing the notebook content. |
1212
+ | **PNG** | Snapshots the rendered notebook to an image. |
1213
+ | **JSON** | Exports the full widget state — re-importable for programmatic reuse. |
1214
+
1215
+ **Layout-specific share affordances:**
1216
+ - \`notebook-workspace\` has a dedicated \`publish\` button (primary, accent-coloured) that flips \`mode: 'view'\`, tags the notebook \`published\` (from \`draft\`), and copies the Hyperskill link in one gesture. Use this when the user wants a clean hand-off.
1217
+ - \`notebook-document\` shows a single \`share\` link (live invite/collaboration is not available in this build; presence avatars only render when the \`presence\` param is explicitly provided).
1218
+
1219
+ ### Step 5 — Working with connected data servers
1220
+
1221
+ When one or more MCP data servers are connected, every notebook layout exposes a **collapsible left pane** (bookmark-bar styling, collapsed by default) that lists:
1222
+ - **Recipes** published by each server (\`{server}_list_recipes()\`)
1223
+ - **Tools / tables** exposed by each server (\`{server}_list_tools()\`)
1224
+
1225
+ Clicking any recipe opens a viewer modal. Each fenced code block inside the recipe has a \`↳ inject\` button that drops the snippet into the notebook as a new cell — the user never has to copy-paste.
1226
+
1227
+ Two toolbar buttons flank the left pane on every layout:
1228
+ - **\`+ md\`** — 3-tab modal (New / File / URL) to create a markdown cell from scratch, from a local \`.md\` file, or from a URL
1229
+ - **\`+ recipe\`** — 3-tab modal (Browser / File / URL) to import a recipe from a connected server, a local \`.recipe.md\` file, or a URL
1230
+
1231
+ Pass the server metadata via the \`servers:\` param so these affordances populate correctly:
1232
+
1233
+ \`\`\`ts
1234
+ widget_display({
1235
+ name: 'notebook-compact',
1236
+ params: {
1237
+ title: '...',
1238
+ cells: [...],
1239
+ servers: [{ name: 'tricoteuses', url: 'https://...', kind: 'data' }]
1240
+ }
1241
+ })
1242
+ \`\`\`
1243
+
1244
+ **Filter rule**: only MCP *data* servers (\`kind: 'data'\`) belong in \`servers:\`. Do NOT include WebMCP UI servers such as \`autoui\` — they expose no queryable data.
1245
+
1246
+ ### Step 6 — Hand-off guidance
1202
1247
 
1203
1248
  After creating the notebook, mention to the user that they can:
1204
- - Click \`share\` in the toolbar to open the export modal (hyperskill link for in-session sharing, markdown/png/json for external export)
1205
- - Switch to \`view\` mode (read-only, no controls visible) when presenting to someone
1206
- - Access the \`⟲ history\` panel to see the trace of edits, and restore deleted cells
1249
+ - **Share in four formats** via the toolbar \`share\` button (Hyperskill / Markdown / PNG / JSON)
1250
+ - **Switch to \`view\` mode** (read-only) when presenting
1251
+ - Use **\`run all\`** (workspace) or **\`publish\`** (workspace) for one-shot execution and publication
1252
+ - **Reply** to margin comments in a document layout (\`+ reply\` under each comment)
1253
+ - Access the \`⟲ history\` panel to see the edit trace and restore deleted cells
1254
+ - **Import recipes** from connected MCP servers via the left pane or the \`+ recipe\` modal
1207
1255
 
1208
- For hackathon contexts, prefer seeding a **document** layout (comments + avatars) so participants feel they are joining a shared space.
1256
+ For hackathon contexts, prefer seeding a \`notebook-document\` layout so participants can leave margin comments and replies on cells (presence stays opt-in pass a \`presence\` array only if you have real editors to show).
1209
1257
 
1210
1258
  ## Examples
1211
1259
 
@@ -1217,20 +1265,34 @@ widget_display({name: "notebook-compact", params: {
1217
1265
  cells: [
1218
1266
  {type: "md", content: "### CSV playground\\n\\nRun the SQL cell to see the first rows, then iterate."},
1219
1267
  {type: "sql", content: "select * from source limit 20", varname: "rows"},
1220
- {type: "js", content: "// summarize, chart, or filter rows here"}
1268
+ {type: "js", content: "// summarize, chart, or filter rows here\\nconsole.table(rows)"}
1269
+ ]
1270
+ }})
1271
+ \`\`\`
1272
+
1273
+ ### Publishable analyst workspace
1274
+ \`\`\`
1275
+ // user: "Set up an analysis I can publish to the team as an app"
1276
+ widget_display({name: "notebook-workspace", params: {
1277
+ title: "Sales review",
1278
+ cells: [
1279
+ {type: "md", name: "intro", content: "What this analysis covers."},
1280
+ {type: "sql", name: "fetch_sales", content: "select * from sales limit 100"},
1281
+ {type: "js", name: "plot", content: "// chart the rows"}
1221
1282
  ]
1222
1283
  }})
1284
+ // Then tell the user: click \`run all\` to execute, then \`publish\` to flip to view mode and copy the Hyperskill link.
1223
1285
  \`\`\`
1224
1286
 
1225
- ### Collaborative analysis
1287
+ ### Collaborative analysis with comments
1226
1288
  \`\`\`
1227
- // user: "Set up a notebook my team can edit together"
1289
+ // user: "Set up a notebook my team can edit and comment on"
1228
1290
  widget_display({name: "notebook-document", params: {
1229
1291
  title: "Team analysis",
1230
1292
  cells: [
1231
1293
  {type: "md", content: "Kick-off: describe the question here."},
1232
- {type: "sql", content: "select * from source limit 10"},
1233
- {type: "md", content: "Your findings: add thoughts, highlights (<mark>key sentence</mark>), and comments on the code cells above."}
1294
+ {type: "sql", content: "select * from source limit 10", comment: {who: "reviewer", when: "2m", body: "Should we filter to last quarter only?"}},
1295
+ {type: "md", content: "Your findings: add thoughts, <mark>highlights</mark>, and reply to the comment on the query above."}
1234
1296
  ]
1235
1297
  }})
1236
1298
  \`\`\`
@@ -1257,8 +1319,10 @@ widget_display({name: "notebook-editorial", params: {
1257
1319
  - **Empty notebook**: never call \`widget_display\` without at least 3 seed cells. The user expects something they can immediately run.
1258
1320
  - **Wrong layout for the intent**: do not use \`notebook-editorial\` for quick exploration — it signals "finished article" and intimidates. Use \`notebook-compact\` unless the user explicitly asks for a publication feel.
1259
1321
  - **Heavy initial queries**: always \`LIMIT 10\` or \`LIMIT 20\` in seed SQL cells. Users will expand later if needed.
1260
- - **Missing \`varname\` on SQL cells** (in compact layout): the named output is what the compact layout showcases. Without it, the notebook loses half its reactive story.
1261
- - **Inventing UUIDs or fork IDs**: leave \`id\` and \`forkId\` unset — the widget generates sensible defaults. Only pass \`id\` when restoring an existing notebook.
1322
+ - **Missing \`varname\` on SQL cells** (in compact layout): the named output is what the compact layout showcases, and it drives the stale-flag dataflow. Without it, the notebook loses half its reactive story.
1323
+ - **Inventing UUIDs**: leave \`id\` unset — the widget generates a sensible default. Only pass \`id\` when restoring an existing notebook.
1324
+ - **Faking presence**: do not pass a \`presence\` array to \`notebook-document\` unless there are real editors to show. Presence is opt-in by design — empty \`presence\` hides the avatar row entirely.
1325
+ - **Including \`autoui\` in \`servers:\`**: only MCP *data* servers (\`kind: 'data'\`) belong there. UI servers like \`autoui\` would pollute the left pane.
1262
1326
  `,
1263
1327
  'parlementaire-profile': `---
1264
1328
  id: display-parliamentary-profile-with-hemicycle-and-votes
@@ -2,7 +2,7 @@
2
2
  id: create-interactive-notebook-playbook
3
3
  name: Create an interactive notebook playbook
4
4
  components_used: [notebook-compact, notebook-workspace, notebook-document, notebook-editorial]
5
- when: the user wants to experiment with data, prototype a small analysis, share a reusable scenario, let others fork and try a dataset, or prepare a hackathon-ready playground. Keywords include "playground", "playbook", "experiment", "try", "prototype", "hackathon", "share a notebook", "template", "starter".
5
+ when: the user wants to experiment with data, prototype a small analysis, share a reusable scenario, or prepare a hackathon-ready playground. Keywords include "playground", "playbook", "experiment", "try", "prototype", "hackathon", "share a notebook", "template", "starter", "publish".
6
6
  servers: [autoui]
7
7
  layout:
8
8
  type: single
@@ -16,6 +16,7 @@ The user asks for a **notebook-like interactive playground** that combines text,
16
16
  - "I want to prototype a small analysis"
17
17
  - "Set up a hackathon starter"
18
18
  - "Make a reusable template for exploring CSVs / this API / these tables"
19
+ - "Publish this analysis as a short memo"
19
20
 
20
21
  This recipe applies across domains (parliamentary data, biodiversity, news, business datasets, etc.) — it only prescribes the **shape** of the answer, not its content.
21
22
 
@@ -28,8 +29,8 @@ Choose one of the four `notebook-*` widgets based on the user's implicit intent:
28
29
  | Layout | Use when |
29
30
  |---|---|
30
31
  | `notebook-compact` | Quick data exploration, reactive dataflow with named outputs, minimal chrome. **Default for most "playground" and "hackathon" requests.** |
31
- | `notebook-workspace` | The user expects a multi-cell analyst workspace with sources, cell navigation, and a "publish" step. Use when they mention "dashboard", "app", "workspace". |
32
- | `notebook-document` | The user plans to share and discuss with a team. Use when "collaborate", "review", "comment" appear. |
32
+ | `notebook-workspace` | The user expects a multi-cell analyst workspace with sources, cell navigation, `run all`, and a `publish` step. Use when they mention "dashboard", "app", "workspace", "publish". |
33
+ | `notebook-document` | The user plans to share and discuss with a team. Use when "collaborate", "review", "comment", "reply" appear. |
33
34
  | `notebook-editorial` | The user wants a polished, article-like final deliverable mixing prose and code. Use for "memo", "report", "writeup", "blog-style". |
34
35
 
35
36
  When in doubt, pick `notebook-compact`.
@@ -40,8 +41,8 @@ Never create an empty notebook. Always seed with 3–5 cells that give the user
40
41
 
41
42
  1. **First cell: markdown** — title + one-sentence context of what the notebook is for
42
43
  2. **Second cell: markdown or code** — if an MCP data source is connected, a starter query that returns something visible (e.g. `SELECT * FROM {table} LIMIT 10`). Otherwise a markdown cell describing the next step
43
- 3. **Third cell: code** — a transformation or a visualization that uses the output of step 2. Use `varname` on the SQL cell (`varname: "rows"`) and reference it in the JS cell
44
- 4. **Last cell: markdown** — a short "to you to play" note inviting the user to add cells, edit, or fork
44
+ 3. **Third cell: code** — a transformation or a visualization that uses the output of step 2. Use `varname` on the SQL cell (`varname: "rows"`) and reference it in the JS cell — this activates the **reactive dataflow** (the downstream JS cell is flagged stale automatically when its upstream re-runs)
45
+ 4. **Last cell: markdown** — a short "to you to play" note inviting the user to add cells or edit
45
46
 
46
47
  Example seed for a generic data playground:
47
48
 
@@ -66,14 +67,61 @@ If a specific MCP server is connected, replace the generic `source` and `select
66
67
 
67
68
  Always keep queries **short** and **limited** so the first run returns quickly and visually.
68
69
 
69
- ### Step 4 Share and fork
70
+ SQL cells are dispatched automatically to the server's `*_query_sql` tool (first match). JS cells run in a Web Worker with upstream named outputs injected as scope.
71
+
72
+ ### Step 4 — Exporting & publishing
73
+
74
+ All four notebook layouts share the same `share` button in the toolbar, offering **four export formats**:
75
+
76
+ | Format | What it does |
77
+ |---|---|
78
+ | **Hyperskill link** | Copies both the canonical Hyperskill URL and a short domain-scoped URL (`?n=<token>`). The short URL opens the read-only public viewer at `nb.hyperskills.net`. |
79
+ | **Markdown** | Downloads a `.md` file containing the notebook content. |
80
+ | **PNG** | Snapshots the rendered notebook to an image. |
81
+ | **JSON** | Exports the full widget state — re-importable for programmatic reuse. |
82
+
83
+ **Layout-specific share affordances:**
84
+ - `notebook-workspace` has a dedicated `publish` button (primary, accent-coloured) that flips `mode: 'view'`, tags the notebook `published` (from `draft`), and copies the Hyperskill link in one gesture. Use this when the user wants a clean hand-off.
85
+ - `notebook-document` shows a single `share` link (live invite/collaboration is not available in this build; presence avatars only render when the `presence` param is explicitly provided).
86
+
87
+ ### Step 5 — Working with connected data servers
88
+
89
+ When one or more MCP data servers are connected, every notebook layout exposes a **collapsible left pane** (bookmark-bar styling, collapsed by default) that lists:
90
+ - **Recipes** published by each server (`{server}_list_recipes()`)
91
+ - **Tools / tables** exposed by each server (`{server}_list_tools()`)
92
+
93
+ Clicking any recipe opens a viewer modal. Each fenced code block inside the recipe has a `↳ inject` button that drops the snippet into the notebook as a new cell — the user never has to copy-paste.
94
+
95
+ Two toolbar buttons flank the left pane on every layout:
96
+ - **`+ md`** — 3-tab modal (New / File / URL) to create a markdown cell from scratch, from a local `.md` file, or from a URL
97
+ - **`+ recipe`** — 3-tab modal (Browser / File / URL) to import a recipe from a connected server, a local `.recipe.md` file, or a URL
98
+
99
+ Pass the server metadata via the `servers:` param so these affordances populate correctly:
100
+
101
+ ```ts
102
+ widget_display({
103
+ name: 'notebook-compact',
104
+ params: {
105
+ title: '...',
106
+ cells: [...],
107
+ servers: [{ name: 'tricoteuses', url: 'https://...', kind: 'data' }]
108
+ }
109
+ })
110
+ ```
111
+
112
+ **Filter rule**: only MCP *data* servers (`kind: 'data'`) belong in `servers:`. Do NOT include WebMCP UI servers such as `autoui` — they expose no queryable data.
113
+
114
+ ### Step 6 — Hand-off guidance
70
115
 
71
116
  After creating the notebook, mention to the user that they can:
72
- - Click `share` in the toolbar to open the export modal (hyperskill link for in-session sharing, markdown/png/json for external export)
73
- - Switch to `view` mode (read-only, no controls visible) when presenting to someone
74
- - Access the `⟲ history` panel to see the trace of edits, and restore deleted cells
117
+ - **Share in four formats** via the toolbar `share` button (Hyperskill / Markdown / PNG / JSON)
118
+ - **Switch to `view` mode** (read-only) when presenting
119
+ - Use **`run all`** (workspace) or **`publish`** (workspace) for one-shot execution and publication
120
+ - **Reply** to margin comments in a document layout (`+ reply` under each comment)
121
+ - Access the `⟲ history` panel to see the edit trace and restore deleted cells
122
+ - **Import recipes** from connected MCP servers via the left pane or the `+ recipe` modal
75
123
 
76
- For hackathon contexts, prefer seeding a **document** layout (comments + avatars) so participants feel they are joining a shared space.
124
+ For hackathon contexts, prefer seeding a `notebook-document` layout so participants can leave margin comments and replies on cells (presence stays opt-in pass a `presence` array only if you have real editors to show).
77
125
 
78
126
  ## Examples
79
127
 
@@ -85,20 +133,34 @@ widget_display({name: "notebook-compact", params: {
85
133
  cells: [
86
134
  {type: "md", content: "### CSV playground\n\nRun the SQL cell to see the first rows, then iterate."},
87
135
  {type: "sql", content: "select * from source limit 20", varname: "rows"},
88
- {type: "js", content: "// summarize, chart, or filter rows here"}
136
+ {type: "js", content: "// summarize, chart, or filter rows here\nconsole.table(rows)"}
137
+ ]
138
+ }})
139
+ ```
140
+
141
+ ### Publishable analyst workspace
142
+ ```
143
+ // user: "Set up an analysis I can publish to the team as an app"
144
+ widget_display({name: "notebook-workspace", params: {
145
+ title: "Sales review",
146
+ cells: [
147
+ {type: "md", name: "intro", content: "What this analysis covers."},
148
+ {type: "sql", name: "fetch_sales", content: "select * from sales limit 100"},
149
+ {type: "js", name: "plot", content: "// chart the rows"}
89
150
  ]
90
151
  }})
152
+ // Then tell the user: click `run all` to execute, then `publish` to flip to view mode and copy the Hyperskill link.
91
153
  ```
92
154
 
93
- ### Collaborative analysis
155
+ ### Collaborative analysis with comments
94
156
  ```
95
- // user: "Set up a notebook my team can edit together"
157
+ // user: "Set up a notebook my team can edit and comment on"
96
158
  widget_display({name: "notebook-document", params: {
97
159
  title: "Team analysis",
98
160
  cells: [
99
161
  {type: "md", content: "Kick-off: describe the question here."},
100
- {type: "sql", content: "select * from source limit 10"},
101
- {type: "md", content: "Your findings: add thoughts, highlights (<mark>key sentence</mark>), and comments on the code cells above."}
162
+ {type: "sql", content: "select * from source limit 10", comment: {who: "reviewer", when: "2m", body: "Should we filter to last quarter only?"}},
163
+ {type: "md", content: "Your findings: add thoughts, <mark>highlights</mark>, and reply to the comment on the query above."}
102
164
  ]
103
165
  }})
104
166
  ```
@@ -125,5 +187,7 @@ widget_display({name: "notebook-editorial", params: {
125
187
  - **Empty notebook**: never call `widget_display` without at least 3 seed cells. The user expects something they can immediately run.
126
188
  - **Wrong layout for the intent**: do not use `notebook-editorial` for quick exploration — it signals "finished article" and intimidates. Use `notebook-compact` unless the user explicitly asks for a publication feel.
127
189
  - **Heavy initial queries**: always `LIMIT 10` or `LIMIT 20` in seed SQL cells. Users will expand later if needed.
128
- - **Missing `varname` on SQL cells** (in compact layout): the named output is what the compact layout showcases. Without it, the notebook loses half its reactive story.
129
- - **Inventing UUIDs or fork IDs**: leave `id` and `forkId` unset — the widget generates sensible defaults. Only pass `id` when restoring an existing notebook.
190
+ - **Missing `varname` on SQL cells** (in compact layout): the named output is what the compact layout showcases, and it drives the stale-flag dataflow. Without it, the notebook loses half its reactive story.
191
+ - **Inventing UUIDs**: leave `id` unset — the widget generates a sensible default. Only pass `id` when restoring an existing notebook.
192
+ - **Faking presence**: do not pass a `presence` array to `notebook-document` unless there are real editors to show. Presence is opt-in by design — empty `presence` hides the avatar row entirely.
193
+ - **Including `autoui` in `servers:`**: only MCP *data* servers (`kind: 'data'`) belong there. UI servers like `autoui` would pollute the left pane.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Shared Hawk proxy handler — used by apps' /api/hawk/+server.ts
3
+ * Hawk = OpenAI-compatible endpoint at https://hawk.hyperskills.net/v1
4
+ * Bearer token lives server-side in HAWK_API_KEY env var.
5
+ */
6
+ function sanitizeId(id: string | undefined): string {
7
+ if (!id) return 'x';
8
+ const clean = id.replace(/[^a-zA-Z0-9]/g, '');
9
+ return clean || 'x';
10
+ }
11
+
12
+ /**
13
+ * Some llama-cpp chat templates (Qwen, Mistral family) enforce alphanumeric
14
+ * tool_call IDs via Jinja raise_exception. Gemma's template drops tool_use_id
15
+ * entirely (cf. gemma4-prompt-builder.ts:194) — safe passthrough.
16
+ * Mirrors the convention of sanitizeServerName (tool-layers.ts:24).
17
+ */
18
+ function needsIdSanitize(model: string | null | undefined): boolean {
19
+ if (!model) return false;
20
+ return /^(qwen|mistral|ministral|devstral|codestral|bielik)/.test(model);
21
+ }
22
+
23
+ export async function hawkProxy(
24
+ body: Record<string, unknown>,
25
+ apiKey: string,
26
+ model?: string | null,
27
+ ): Promise<Response> {
28
+ if (!apiKey) {
29
+ return new Response('HAWK_API_KEY missing', { status: 500 });
30
+ }
31
+ const m = model ?? 'qwen35-4b';
32
+ const cloned = JSON.parse(JSON.stringify(body)) as Record<string, unknown>;
33
+ if (needsIdSanitize(m) && Array.isArray(cloned.messages)) {
34
+ for (const msg of cloned.messages as Array<Record<string, unknown>>) {
35
+ if (msg.role === 'assistant' && Array.isArray(msg.tool_calls)) {
36
+ for (const tc of msg.tool_calls as Array<Record<string, unknown>>) {
37
+ tc.id = sanitizeId(tc.id as string | undefined);
38
+ }
39
+ } else if (msg.role === 'tool' && typeof msg.tool_call_id === 'string') {
40
+ msg.tool_call_id = sanitizeId(msg.tool_call_id);
41
+ }
42
+ }
43
+ }
44
+ const res = await fetch('https://hawk.hyperskills.net/v1/chat/completions', {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ 'Authorization': `Bearer ${apiKey}`,
49
+ },
50
+ body: JSON.stringify({ ...cloned, model: m }),
51
+ });
52
+ if (!res.ok) return new Response(await res.text(), { status: res.status });
53
+ return Response.json(await res.json());
54
+ }
@@ -0,0 +1,2 @@
1
+ export { llmProxy } from './llmProxy.js';
2
+ export { hawkProxy } from './hawkProxy.js';
@@ -252,13 +252,112 @@ export async function loadOrDownloadModel(
252
252
  /**
253
253
  * Remove every cached file for a given repo. Silently no-ops if the repo
254
254
  * directory does not exist.
255
+ *
256
+ * Accepts either the original `owner/name` form OR the sanitized key form
257
+ * (`owner__name`). Both are tried so UIs that list cached repos via
258
+ * `listCachedModels()` (which only knows the sanitized key) can delete.
255
259
  */
256
260
  export async function clearModelCache(repo: string): Promise<void> {
257
261
  try {
258
262
  const root = await navigator.storage.getDirectory();
259
263
  const modelsDir = await root.getDirectoryHandle('webmcp-models', { create: false });
260
- const repoKey = sanitizeRepoKey(repo);
261
- await modelsDir.removeEntry(repoKey, { recursive: true });
264
+ const candidates = new Set<string>([repo, sanitizeRepoKey(repo)]);
265
+ for (const key of candidates) {
266
+ try { await modelsDir.removeEntry(key, { recursive: true }); } catch { /* not present */ }
267
+ }
268
+ } catch {
269
+ // Nothing to clear
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Info about a single cached model repo in OPFS.
275
+ *
276
+ * Note: `repo` is the sanitized folder name as it appears on disk
277
+ * (e.g. `google__gemma-3n-E2B-it-litert-preview`). The original `owner/name`
278
+ * is not recoverable after sanitization.
279
+ */
280
+ export interface CachedModelInfo {
281
+ repo: string;
282
+ size: number;
283
+ fileCount: number;
284
+ lastModified: number;
285
+ }
286
+
287
+ /**
288
+ * Recursively sum file sizes under a directory handle, tracking count and
289
+ * max lastModified. Ignores entries that fail to enumerate.
290
+ */
291
+ export async function walkDirectoryStats(
292
+ dir: FileSystemDirectoryHandle,
293
+ ): Promise<{ size: number; fileCount: number; lastModified: number }> {
294
+ let size = 0;
295
+ let fileCount = 0;
296
+ let lastModified = 0;
297
+ try {
298
+ const iter = dir as unknown as { entries: () => AsyncIterable<[string, FileSystemHandle]> };
299
+ for await (const [, handle] of iter.entries()) {
300
+ if (handle.kind === 'file') {
301
+ try {
302
+ const f = await (handle as FileSystemFileHandle).getFile();
303
+ size += f.size;
304
+ fileCount += 1;
305
+ if (f.lastModified > lastModified) lastModified = f.lastModified;
306
+ } catch { /* skip */ }
307
+ } else if (handle.kind === 'directory') {
308
+ const sub = await walkDirectoryStats(handle as FileSystemDirectoryHandle);
309
+ size += sub.size;
310
+ fileCount += sub.fileCount;
311
+ if (sub.lastModified > lastModified) lastModified = sub.lastModified;
312
+ }
313
+ }
314
+ } catch { /* iteration unsupported */ }
315
+ return { size, fileCount, lastModified };
316
+ }
317
+
318
+ /**
319
+ * List every cached model repo in OPFS with cumulative size, file count and
320
+ * last-modified timestamp. Returns `[]` if `webmcp-models` does not exist
321
+ * or if OPFS itself is unavailable.
322
+ */
323
+ export async function listCachedModels(): Promise<CachedModelInfo[]> {
324
+ try {
325
+ if (!navigator.storage?.getDirectory) return [];
326
+ const root = await navigator.storage.getDirectory();
327
+ let modelsDir: FileSystemDirectoryHandle;
328
+ try {
329
+ modelsDir = await root.getDirectoryHandle('webmcp-models', { create: false });
330
+ } catch {
331
+ return [];
332
+ }
333
+ const out: CachedModelInfo[] = [];
334
+ const iter = modelsDir as unknown as { entries: () => AsyncIterable<[string, FileSystemHandle]> };
335
+ try {
336
+ for await (const [name, handle] of iter.entries()) {
337
+ if (handle.kind !== 'directory') continue;
338
+ const stats = await walkDirectoryStats(handle as FileSystemDirectoryHandle);
339
+ if (stats.size === 0 || stats.fileCount === 0) {
340
+ // Orphan directory (e.g. worker bug that created an empty repo key).
341
+ try { await modelsDir.removeEntry(name, { recursive: true }); } catch { /* best-effort */ }
342
+ continue;
343
+ }
344
+ out.push({ repo: name, size: stats.size, fileCount: stats.fileCount, lastModified: stats.lastModified });
345
+ }
346
+ } catch { /* iteration unsupported */ }
347
+ out.sort((a, b) => b.size - a.size);
348
+ return out;
349
+ } catch {
350
+ return [];
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Nuke the whole `webmcp-models` directory. No-op if it does not exist.
356
+ */
357
+ export async function clearAllModelCaches(): Promise<void> {
358
+ try {
359
+ const root = await navigator.storage.getDirectory();
360
+ await root.removeEntry('webmcp-models', { recursive: true });
262
361
  } catch {
263
362
  // Nothing to clear
264
363
  }