@try-works/dsh-model-as-tool 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.
Files changed (79) hide show
  1. package/README.md +51 -0
  2. package/bin/dsh-tool-mode-mcp-stdio.mjs +80 -0
  3. package/catalog/desert-ant-models.json +77 -13
  4. package/lib/adapters/index.d.ts +19 -6
  5. package/lib/adapters/index.d.ts.map +1 -1
  6. package/lib/adapters/index.js +28 -13
  7. package/lib/adapters/tongue.d.ts +11 -3
  8. package/lib/adapters/tongue.d.ts.map +1 -1
  9. package/lib/adapters/tongue.js +12 -4
  10. package/lib/adapters/types.d.ts +11 -0
  11. package/lib/adapters/types.d.ts.map +1 -1
  12. package/lib/adapters/voz.d.ts +7 -1
  13. package/lib/adapters/voz.d.ts.map +1 -1
  14. package/lib/adapters/voz.js +16 -4
  15. package/lib/asset-acquisition.d.ts +137 -0
  16. package/lib/asset-acquisition.d.ts.map +1 -0
  17. package/lib/asset-acquisition.js +161 -0
  18. package/lib/authority.d.ts +153 -0
  19. package/lib/authority.d.ts.map +1 -0
  20. package/lib/authority.js +194 -0
  21. package/lib/bindings.d.ts +115 -0
  22. package/lib/bindings.d.ts.map +1 -0
  23. package/lib/bindings.js +239 -0
  24. package/lib/catalog.d.ts.map +1 -1
  25. package/lib/catalog.js +57 -0
  26. package/lib/contract-payload.d.ts +47 -0
  27. package/lib/contract-payload.d.ts.map +1 -0
  28. package/lib/contract-payload.js +137 -0
  29. package/lib/deployment-coordinator.d.ts +213 -0
  30. package/lib/deployment-coordinator.d.ts.map +1 -0
  31. package/lib/deployment-coordinator.js +274 -0
  32. package/lib/harness-budgets.d.ts +158 -0
  33. package/lib/harness-budgets.d.ts.map +1 -0
  34. package/lib/harness-budgets.js +152 -0
  35. package/lib/index.d.ts +41 -0
  36. package/lib/index.d.ts.map +1 -1
  37. package/lib/index.js +424 -16
  38. package/lib/licensing.d.ts +221 -0
  39. package/lib/licensing.d.ts.map +1 -0
  40. package/lib/licensing.js +188 -0
  41. package/lib/mcp/host-binding.d.ts +91 -0
  42. package/lib/mcp/host-binding.d.ts.map +1 -0
  43. package/lib/mcp/host-binding.js +133 -0
  44. package/lib/mcp/http.d.ts +130 -0
  45. package/lib/mcp/http.d.ts.map +1 -0
  46. package/lib/mcp/http.js +274 -0
  47. package/lib/mcp/protocol.d.ts +261 -0
  48. package/lib/mcp/protocol.d.ts.map +1 -0
  49. package/lib/mcp/protocol.js +387 -0
  50. package/lib/mcp/server.d.ts +115 -0
  51. package/lib/mcp/server.d.ts.map +1 -0
  52. package/lib/mcp/server.js +200 -0
  53. package/lib/mcp/stdio.d.ts +80 -0
  54. package/lib/mcp/stdio.d.ts.map +1 -0
  55. package/lib/mcp/stdio.js +176 -0
  56. package/lib/mcp/subscriptions.d.ts +135 -0
  57. package/lib/mcp/subscriptions.d.ts.map +1 -0
  58. package/lib/mcp/subscriptions.js +207 -0
  59. package/lib/provider-bridge.d.ts +69 -0
  60. package/lib/provider-bridge.d.ts.map +1 -0
  61. package/lib/provider-bridge.js +218 -0
  62. package/lib/publication.d.ts +160 -0
  63. package/lib/publication.d.ts.map +1 -0
  64. package/lib/publication.js +173 -0
  65. package/lib/receipt.d.ts +208 -0
  66. package/lib/receipt.d.ts.map +1 -0
  67. package/lib/receipt.js +150 -0
  68. package/lib/registrar.d.ts +13 -0
  69. package/lib/registrar.d.ts.map +1 -1
  70. package/lib/registrar.js +20 -3
  71. package/lib/resource-governance.d.ts +204 -0
  72. package/lib/resource-governance.d.ts.map +1 -0
  73. package/lib/resource-governance.js +213 -0
  74. package/lib/server-lifecycle.d.ts +175 -0
  75. package/lib/server-lifecycle.d.ts.map +1 -0
  76. package/lib/server-lifecycle.js +269 -0
  77. package/lib/types.d.ts +21 -0
  78. package/lib/types.d.ts.map +1 -1
  79. package/package.json +9 -2
package/README.md CHANGED
@@ -137,6 +137,57 @@ npm i @desert-ant-labs/voz onnxruntime-node # transcription on Windows/Linux/m
137
137
 
138
138
  The plugin never bundles or re-hosts model weights.
139
139
 
140
+ ## What gets downloaded, and when
141
+
142
+ Three separate things arrive at three separate times. Measured sizes are from a
143
+ Windows x64 install of this package.
144
+
145
+ **1. The plugin itself — ~58 kB, from npm, at install.** `lib/`, the two catalog
146
+ JSON files, `cordis.patch.yml` and this README. It contains **no model weights
147
+ and no inference runtime**.
148
+
149
+ **2. The model SDK packages — from npm, at install, only if you asked for them.**
150
+ They are `optionalDependencies`, so a failed or skipped download does not fail the
151
+ install; the capability resolver simply reports the model as unavailable.
152
+
153
+ | Package | Size on disk | Contains |
154
+ | --- | --- | --- |
155
+ | `@desert-ant-labs/tongue` | 2.1 MB | **The model itself** — `tongue_int8.bin` (2.0 MB) ships inside the tarball |
156
+ | `@desert-ant-labs/voz` | 44.2 MB | The WebAssembly runtime (`VozWeb.wasm`), not the weights |
157
+ | `onnxruntime-node` | 287.1 MB | The prebuilt inference engine for this platform |
158
+
159
+ **3. The weights — from Hugging Face, on first call, not at install.** Each SDK
160
+ pins one revision and verifies the download, then caches it. Voz fetches about
161
+ **367 MB** on its first transcription:
162
+
163
+ ```
164
+ ~/.cache/desert-ant-models/desert-ant-labs/voz 367 MB (first call)
165
+ ```
166
+
167
+ Tongue never downloads anything: its model is inside the npm package.
168
+
169
+ Point an SDK at a pre-populated folder to run offline (each adapter accepts a
170
+ `directory`), and set `HF_TOKEN` for a gated repo. Because the licence forbids
171
+ redistribution, the plugin never bundles or mirrors weights — it downloads them
172
+ the way the vendor intends, once, into the vendor's own cache.
173
+
174
+ ### Install cost is real, so it is opt-out
175
+
176
+ Installing with the optional dependencies present pulls roughly **333 MB** before
177
+ any model runs, and a first Voz call adds **367 MB** more. If that is unwanted,
178
+ install the plugin with them skipped and add only what you use:
179
+
180
+ ```sh
181
+ npm i @try-works/dsh-model-as-tool --omit=optional
182
+ npm i @desert-ant-labs/tongue # 2 MB, and its model is included
183
+ ```
184
+
185
+ **Watch out for the other model packages.** They ship prebuilt native cores for
186
+ `linux-x64`, `linux-arm64` and `darwin-arm64` inside one tarball — roughly 50 MB
187
+ each — so `@desert-ant-labs/redact` is ~166 MB of which the great majority is
188
+ Linux and macOS binaries that can never load on Windows. Installing one on
189
+ Windows buys ~130 MB that cannot run.
190
+
140
191
  ## Development
141
192
 
142
193
  ```sh
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The `dsh-tool-mode-mcp-stdio` executable (MCP-03, P26-08).
4
+ *
5
+ * This file owns the one property that cannot be tested from the library: that
6
+ * STDOUT CARRIES PROTOCOL TRAFFIC AND NOTHING ELSE. Everything written here goes
7
+ * through two explicitly separate streams, and nothing in this process writes to
8
+ * stdout except a protocol frame — not a banner, not a version, not a warning.
9
+ *
10
+ * The endpoint and token come from the environment, which is how an MCP client
11
+ * launches a stdio server. A missing token is reported on STDERR and the process
12
+ * exits non-zero: an unattended launch must fail with guidance rather than hang
13
+ * waiting for a credential nobody will supply (P26-16).
14
+ */
15
+
16
+ import { createStdioBridge } from '../lib/mcp/stdio.js'
17
+
18
+ const url = process.env.DSH_TOOL_MODE_MCP_URL ?? 'http://127.0.0.1:3765/mcp'
19
+ const token = process.env.DSH_TOOL_MODE_MCP_TOKEN
20
+
21
+ /** Diagnostics. NEVER stdout. */
22
+ const diagnostic = line => process.stderr.write(`${line}\n`)
23
+
24
+ if (token === undefined || token.length === 0) {
25
+ // P26-16: unattended operation must fail with setup guidance, never hang.
26
+ diagnostic('dsh-tool-mode-mcp-stdio: no credential configured.')
27
+ diagnostic(' Set DSH_TOOL_MODE_MCP_TOKEN to the token the operator configured for this client,')
28
+ diagnostic(' and DSH_TOOL_MODE_MCP_URL if the endpoint is not http://127.0.0.1:3765/mcp.')
29
+ process.exit(2)
30
+ }
31
+
32
+ const bridge = createStdioBridge(
33
+ {
34
+ url,
35
+ token,
36
+ fetch: async (target, init) => {
37
+ const response = await fetch(target, {
38
+ method: init.method,
39
+ headers: init.headers,
40
+ body: init.body,
41
+ signal: init.signal,
42
+ })
43
+ const headers = {}
44
+ response.headers.forEach((value, key) => { headers[key] = value })
45
+ return { status: response.status, headers, body: await response.text() }
46
+ },
47
+ },
48
+ {
49
+ // The ONLY writer to stdout in this process.
50
+ protocol: line => process.stdout.write(`${line}\n`),
51
+ diagnostic,
52
+ },
53
+ )
54
+
55
+ // Newline-delimited input. Chunk boundaries are not frame boundaries, so a partial
56
+ // line is buffered until its newline arrives.
57
+ let buffer = ''
58
+ process.stdin.setEncoding('utf8')
59
+ process.stdin.on('data', chunk => {
60
+ buffer += chunk
61
+ let index = buffer.indexOf('\n')
62
+ while (index !== -1) {
63
+ const line = buffer.slice(0, index)
64
+ buffer = buffer.slice(index + 1)
65
+ void bridge.handleLine(line).then(output => {
66
+ if (output !== undefined) process.stdout.write(`${output}\n`)
67
+ }).catch(error => {
68
+ // A bridge fault is a diagnostic; the protocol channel stays clean.
69
+ diagnostic(`bridge error: ${error instanceof Error ? error.message : String(error)}`)
70
+ })
71
+ index = buffer.indexOf('\n')
72
+ }
73
+ })
74
+
75
+ process.stdin.on('end', () => {
76
+ // P26-08: EOF shuts down BRIDGE-OWNED requests without killing the shared host.
77
+ bridge.close()
78
+ diagnostic('stdin closed; bridge-owned requests aborted')
79
+ process.exit(0)
80
+ })
@@ -4,6 +4,7 @@
4
4
  "name": "Align",
5
5
  "summary": "Word-timestamp refinement for any transcript, on device, from 0.7MB.",
6
6
  "capability": "audio.word-alignment",
7
+ "contract": null,
7
8
  "input": "audio",
8
9
  "output": "{ words: [{ text, start, end }] } aligned against the audio",
9
10
  "languages": null,
@@ -39,6 +40,7 @@
39
40
  "name": "Clear",
40
41
  "summary": "On-device speech enhancement: denoise, dereverb and loudness-normalize to 48 kHz.",
41
42
  "capability": "audio.speech-enhancement",
43
+ "contract": null,
42
44
  "input": "audio",
43
45
  "output": "A cleaned audio file path plus processing metadata",
44
46
  "languages": null,
@@ -74,6 +76,7 @@
74
76
  "name": "Clips",
75
77
  "summary": "Clip selection that turns a long recording into ranked shorts and highlights from a transcript.",
76
78
  "capability": "video.clip-selection",
79
+ "contract": null,
77
80
  "input": "video",
78
81
  "output": "{ clips: [{ index, start, end, title, file }] } ranked by interest",
79
82
  "languages": null,
@@ -109,6 +112,7 @@
109
112
  "name": "Ear",
110
113
  "summary": "On-device spoken language identification across 102 languages from a short audio stretch.",
111
114
  "capability": "audio.spoken-language-identification",
115
+ "contract": null,
112
116
  "input": "audio",
113
117
  "output": "{ language, confidence, candidates: [{ language, probability }] }",
114
118
  "languages": 102,
@@ -143,6 +147,7 @@
143
147
  "name": "Emo",
144
148
  "summary": "Type a task or message and get the emoji that fit, in 22 languages, on the device.",
145
149
  "capability": "text.emoji-suggestion",
150
+ "contract": null,
146
151
  "input": "text",
147
152
  "output": "[{ emoji, confidence }] ranked suggestions",
148
153
  "languages": 22,
@@ -163,7 +168,10 @@
163
168
  "description": "Suggest emoji that fit a piece of text, in 22 languages, running entirely on device.",
164
169
  "params": {
165
170
  "text": "The text to suggest emoji for.",
166
- "limit": { "description": "Maximum number of suggestions to return (default 5).", "type": "integer" }
171
+ "limit": {
172
+ "description": "Maximum number of suggestions to return (default 5).",
173
+ "type": "integer"
174
+ }
167
175
  },
168
176
  "returns": "Ranked emoji suggestions with confidence scores."
169
177
  },
@@ -178,6 +186,7 @@
178
186
  "name": "Eye",
179
187
  "summary": "On-device frame scoring that tells you which shot to keep, what is in it, and which shots repeat.",
180
188
  "capability": "image.frame-scoring",
189
+ "contract": null,
181
190
  "input": "image",
182
191
  "output": "[{ frame, score, labels, duplicate_of }]",
183
192
  "languages": null,
@@ -197,8 +206,14 @@
197
206
  "name": "score_frames",
198
207
  "description": "Score a burst or clip of frames to choose the best shot, describe what is in it and detect repeated shots.",
199
208
  "params": {
200
- "image_paths": { "description": "Absolute paths of the frames to score.", "type": "string[]" },
201
- "top": { "description": "How many top frames to return (default 5).", "type": "integer" }
209
+ "image_paths": {
210
+ "description": "Absolute paths of the frames to score.",
211
+ "type": "string[]"
212
+ },
213
+ "top": {
214
+ "description": "How many top frames to return (default 5).",
215
+ "type": "integer"
216
+ }
202
217
  },
203
218
  "returns": "Frames ranked by score, with labels and duplicate grouping."
204
219
  },
@@ -213,6 +228,7 @@
213
228
  "name": "Face",
214
229
  "summary": "On-device face matching that finds the same person across a photo library or through a video.",
215
230
  "capability": "image.face-matching",
231
+ "contract": null,
216
232
  "input": "image",
217
233
  "output": "[{ path, faces: [{ box, match_id }] }]",
218
234
  "languages": null,
@@ -232,7 +248,10 @@
232
248
  "name": "match_faces",
233
249
  "description": "Match the same person across a set of images or a video, entirely on device, with no image leaving the machine.",
234
250
  "params": {
235
- "image_paths": { "description": "Absolute paths of the images to match faces across.", "type": "string[]" },
251
+ "image_paths": {
252
+ "description": "Absolute paths of the images to match faces across.",
253
+ "type": "string[]"
254
+ },
236
255
  "video_path": "Optional video to track faces through instead of still images."
237
256
  },
238
257
  "returns": "Detected face boxes grouped by person identity."
@@ -248,6 +267,7 @@
248
267
  "name": "Gist",
249
268
  "summary": "On-device topic tagging across a 36-topic taxonomy, in 101 languages, with no server call.",
250
269
  "capability": "text.topic-tagging",
270
+ "contract": null,
251
271
  "input": "text",
252
272
  "output": "{ topics: [{ topic, score }] }",
253
273
  "languages": 101,
@@ -268,7 +288,10 @@
268
288
  "description": "Tag a title, post or article with the topics it covers, from a fixed 36-topic taxonomy across 101 languages.",
269
289
  "params": {
270
290
  "text": "The text to tag.",
271
- "limit": { "description": "Maximum number of topics to return (default 5).", "type": "integer" }
291
+ "limit": {
292
+ "description": "Maximum number of topics to return (default 5).",
293
+ "type": "integer"
294
+ }
272
295
  },
273
296
  "returns": "Ranked topic labels from the fixed taxonomy with scores."
274
297
  },
@@ -283,6 +306,7 @@
283
306
  "name": "Moderator",
284
307
  "summary": "On-device NSFW detection that flags nudity before upload, trained only on licensed and synthetic data.",
285
308
  "capability": "image.nsfw-detection",
309
+ "contract": null,
286
310
  "input": "image",
287
311
  "output": "{ nsfw, score, labels: [...] }",
288
312
  "languages": null,
@@ -302,7 +326,10 @@
302
326
  "name": "detect_nsfw",
303
327
  "description": "Check images for nudity or sexual content on device before they are uploaded or shown.",
304
328
  "params": {
305
- "image_paths": { "description": "Absolute paths of the images to check.", "type": "string[]" }
329
+ "image_paths": {
330
+ "description": "Absolute paths of the images to check.",
331
+ "type": "string[]"
332
+ }
306
333
  },
307
334
  "returns": "A per-image NSFW verdict with score and labels."
308
335
  },
@@ -317,6 +344,7 @@
317
344
  "name": "Redact",
318
345
  "summary": "On-device redaction that strips personal data as you type, in 27 languages, with restore support.",
319
346
  "capability": "text.pii-redaction",
347
+ "contract": null,
320
348
  "input": "text",
321
349
  "output": "{ redactedText, items: [{ label, original, placeholder, confidence }] }",
322
350
  "languages": 27,
@@ -337,8 +365,14 @@
337
365
  "description": "Replace personal data (names, emails, phone numbers, addresses, card numbers and more) in text with stable placeholders, on device, before the text reaches any remote service.",
338
366
  "params": {
339
367
  "text": "The text to redact.",
340
- "labels": { "description": "Optional list of entity labels to restrict redaction to, for example [\"EMAIL\", \"PHONE\"].", "type": "string[]" },
341
- "minimum_confidence": { "description": "Optional confidence floor between 0 and 1 (default 0.6).", "type": "number" }
368
+ "labels": {
369
+ "description": "Optional list of entity labels to restrict redaction to, for example [\"EMAIL\", \"PHONE\"].",
370
+ "type": "string[]"
371
+ },
372
+ "minimum_confidence": {
373
+ "description": "Optional confidence floor between 0 and 1 (default 0.6).",
374
+ "type": "number"
375
+ }
342
376
  },
343
377
  "returns": "The redacted text plus the placeholder map, so originals can be restored after a remote round trip."
344
378
  },
@@ -353,6 +387,7 @@
353
387
  "name": "Schemer",
354
388
  "summary": "Extract typed fields from any text against your own schema, on device, with empty fields left empty.",
355
389
  "capability": "text.structured-extraction",
390
+ "contract": null,
356
391
  "input": "text",
357
392
  "output": "An object matching the caller-supplied schema",
358
393
  "languages": null,
@@ -388,6 +423,7 @@
388
423
  "name": "Shapes",
389
424
  "summary": "On-device shape recognition that turns one hand-drawn stroke into a rectangle, ellipse, triangle, star or line in under 10ms.",
390
425
  "capability": "stroke.shape-recognition",
426
+ "contract": null,
391
427
  "input": "stroke",
392
428
  "output": "{ shape, confidence, candidates: [...] }",
393
429
  "languages": null,
@@ -423,6 +459,7 @@
423
459
  "name": "Title",
424
460
  "summary": "On-device titles and descriptions: a short factual title and a one- or two-sentence description for any passage.",
425
461
  "capability": "text.title-generation",
462
+ "contract": null,
426
463
  "input": "text",
427
464
  "output": "{ title, description, isEmpty }",
428
465
  "languages": null,
@@ -443,7 +480,10 @@
443
480
  "description": "Write a short factual title and a one- or two-sentence description for a passage of text: a landing-page section, a documentation page or an article.",
444
481
  "params": {
445
482
  "text": "The passage to title.",
446
- "max_tokens": { "description": "Optional generation cap in tokens (default 96).", "type": "integer" }
483
+ "max_tokens": {
484
+ "description": "Optional generation cap in tokens (default 96).",
485
+ "type": "integer"
486
+ }
447
487
  },
448
488
  "returns": "A title and a description, or an empty card when the model returned neither."
449
489
  },
@@ -458,6 +498,7 @@
458
498
  "name": "Tongue",
459
499
  "summary": "On-device language identification for short text across 84 languages, from just a few words.",
460
500
  "capability": "text.language-identification",
501
+ "contract": "text.language_detect",
461
502
  "input": "text",
462
503
  "output": "{ language, reliability, candidates: [{ language, probability }], isTooCloseToCall }",
463
504
  "languages": 84,
@@ -478,7 +519,10 @@
478
519
  "description": "Identify the language of a short piece of text across 84 languages, on device, in microseconds. Works from as little as three words.",
479
520
  "params": {
480
521
  "text": "The text whose language to identify.",
481
- "top": { "description": "How many runner-up language candidates to include (default 3).", "type": "integer" }
522
+ "top": {
523
+ "description": "How many runner-up language candidates to include (default 3).",
524
+ "type": "integer"
525
+ }
482
526
  },
483
527
  "returns": "The detected language code, a reliability verdict, and the top scoring candidates with probabilities."
484
528
  },
@@ -486,6 +530,10 @@
486
530
  "name": "desert-ant-tongue",
487
531
  "description": "Detect the language of short text on device with the Desert Ant Tongue model.",
488
532
  "whenToUse": "Use before translating, routing to a locale-specific template, or checking that generated copy is in the expected language."
533
+ },
534
+ "provider": {
535
+ "extension": "@try-works/extension-desert-ant-tongue",
536
+ "modelId": "tongue"
489
537
  }
490
538
  },
491
539
  {
@@ -493,6 +541,7 @@
493
541
  "name": "Toxic",
494
542
  "summary": "On-device moderation that flags hateful, abusive and threatening text in 23 European languages.",
495
543
  "capability": "text.toxicity-detection",
544
+ "contract": null,
496
545
  "input": "text",
497
546
  "output": "{ toxic, score, labels: [...] }",
498
547
  "languages": 23,
@@ -513,7 +562,10 @@
513
562
  "description": "Flag hateful, abusive and threatening text in 23 European languages, on device, before the message is sent.",
514
563
  "params": {
515
564
  "text": "The text to check.",
516
- "minimum_confidence": { "description": "Optional confidence floor between 0 and 1.", "type": "number" }
565
+ "minimum_confidence": {
566
+ "description": "Optional confidence floor between 0 and 1.",
567
+ "type": "number"
568
+ }
517
569
  },
518
570
  "returns": "A toxicity verdict with score and the matched labels."
519
571
  },
@@ -528,6 +580,7 @@
528
580
  "name": "Uhm",
529
581
  "summary": "On-device filler detection that marks every um, uh and hmm to within 20ms, an hour of audio in 12s.",
530
582
  "capability": "audio.filler-detection",
583
+ "contract": null,
531
584
  "input": "audio",
532
585
  "output": "[{ start, end, word }] filler spans to cut",
533
586
  "languages": null,
@@ -562,6 +615,7 @@
562
615
  "name": "Voz",
563
616
  "summary": "Transcribe 10 minutes of audio in 2s on an iPhone, 14x faster than Whisper, with word timestamps.",
564
617
  "capability": "audio.speech-recognition",
618
+ "contract": "stt.transcribe",
565
619
  "input": "audio",
566
620
  "output": "{ text, words: [{ text, start, end }], realtimeFactor }",
567
621
  "languages": 25,
@@ -572,7 +626,9 @@
572
626
  "windowsSupport": "onnxruntime-node",
573
627
  "npmPackage": "@desert-ant-labs/voz",
574
628
  "npmSubpath": null,
575
- "requiresRuntime": ["onnxruntime-node"],
629
+ "requiresRuntime": [
630
+ "onnxruntime-node"
631
+ ],
576
632
  "cliVerb": "voz",
577
633
  "license": "source-available",
578
634
  "docsUrl": "https://desertant.com/models/voz/",
@@ -582,7 +638,10 @@
582
638
  "description": "Transcribe an audio or video file on device with word-level timestamps across 25 languages. Nothing is uploaded and no API key is needed.",
583
639
  "params": {
584
640
  "audio_path": "Absolute path to the audio or video file, or a 16 kHz mono WAV.",
585
- "include_words": { "description": "Whether to include the per-word timestamps (default true).", "type": "boolean" }
641
+ "include_words": {
642
+ "description": "Whether to include the per-word timestamps (default true).",
643
+ "type": "boolean"
644
+ }
586
645
  },
587
646
  "returns": "The transcript text, optional per-word start and end times, and the measured realtime factor."
588
647
  },
@@ -590,6 +649,10 @@
590
649
  "name": "desert-ant-voz",
591
650
  "description": "Transcribe audio on device with word timestamps using the Desert Ant Voz model.",
592
651
  "whenToUse": "Use to get a transcript or captions from a recording locally, without uploading audio to a hosted API."
652
+ },
653
+ "provider": {
654
+ "extension": "@try-works/extension-desert-ant-voz",
655
+ "modelId": "voz"
593
656
  }
594
657
  },
595
658
  {
@@ -597,6 +660,7 @@
597
660
  "name": "Who",
598
661
  "summary": "On-device speaker labeling that turns a recording into per-person turns with face boxes and timestamps.",
599
662
  "capability": "audio.speaker-labeling",
663
+ "contract": null,
600
664
  "input": "audio",
601
665
  "output": "[{ speaker, start, end, face_box }] turns",
602
666
  "languages": null,
@@ -11,13 +11,20 @@
11
11
  */
12
12
  import type { ModelCatalogEntry } from '../types.ts';
13
13
  import type { AdapterFactory, ModelAdapter } from './types.ts';
14
+ import type { ProviderBridge } from '../provider-bridge.ts';
14
15
  import { type TransportOptions } from '../transport.ts';
15
16
  /**
16
- * Every hand-written adapter this plugin ships, in priority order.
17
+ * Hand-written adapters living inside the host.
17
18
  *
18
- * Each factory returns undefined for the entries it does not serve, so the
19
- * lookup is a linear scan rather than a switch, and a model with no adapter yet
20
- * simply has no tool.
19
+ * EMPTY, and it must stay that way (BASE-09). Every model-specific implementation
20
+ * is now an independently versioned extension package reached through the public
21
+ * extension API: Tongue is `@try-works/extension-desert-ant-tongue` and Voz is
22
+ * `@try-works/extension-desert-ant-voz`. The host holds no adapter for any model,
23
+ * so a first-party package has no privilege a published one lacks.
24
+ *
25
+ * The parameter is kept rather than deleted because a test may still inject
26
+ * factories, and an empty list DOCUMENTED as needing to stay empty is a stronger
27
+ * statement than a removed hook.
21
28
  */
22
29
  export declare const ADAPTER_FACTORIES: readonly AdapterFactory[];
23
30
  /**
@@ -37,12 +44,18 @@ export interface TransportSelection extends TransportOptions {
37
44
  * A hand-written adapter always wins for the model it serves: an in-process
38
45
  * call is faster and needs no external service. Everything else falls to the
39
46
  * transport the capability resolver selected.
47
+ *
48
+ * The lookup is ASYNC because an extension-backed model is reached by importing
49
+ * and activating its package, which cannot happen in a synchronous call. The
50
+ * order is deliberate: a declared extension binding wins, then a hand-written
51
+ * factory, then a transport.
40
52
  * @param entry - the catalog entry.
41
53
  * @param options - the resolved route plus transport settings.
42
54
  * @param factories - hand-written adapter factories, defaulting to the shipped set.
43
- * @returns the adapter, or undefined when no factory or transport serves this model.
55
+ * @param bridge - the extension provider bridge, when extensions are available.
56
+ * @returns the adapter, or undefined when no extension, factory or transport serves this model.
44
57
  */
45
- export declare function createAdapter(entry: ModelCatalogEntry, options?: TransportSelection, factories?: readonly AdapterFactory[]): ModelAdapter | undefined;
58
+ export declare function createAdapter(entry: ModelCatalogEntry, options?: TransportSelection, factories?: readonly AdapterFactory[], bridge?: ProviderBridge): ModelAdapter | undefined;
46
59
  export type { AdapterFactory, AdapterInvocation, JsonValue, ModelAdapter } from './types.ts';
47
60
  export type { CommandTransport, TransportOptions } from '../transport.ts';
48
61
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAG9D,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,iBAAiB,CAAA;AAExB;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,cAAc,EAGtD,CAAA;AAED;;;;;;GAMG;AACH,MAAM,WAAW,kBAAmB,SAAQ,gBAAgB;IAC1D,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE,kBAA4C,EACrD,SAAS,GAAE,SAAS,cAAc,EAAsB,GACvD,YAAY,GAAG,SAAS,CAY1B;AAED,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC5F,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAC3D,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,iBAAiB,CAAA;AAExB;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,cAAc,EAAO,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,WAAW,kBAAmB,SAAQ,gBAAgB;IAC1D,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CACvB;AAmBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE,kBAA4C,EACrD,SAAS,GAAE,SAAS,cAAc,EAAsB,EACxD,MAAM,CAAC,EAAE,cAAc,GACtB,YAAY,GAAG,SAAS,CAqB1B;AAED,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC5F,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA"}
@@ -9,20 +9,21 @@
9
9
  *
10
10
  * @module @try-works/dsh-model-as-tool/adapters/index
11
11
  */
12
- import { createTongueAdapter } from "./tongue.js";
13
- import { createVozAdapter } from "./voz.js";
14
12
  import { createCliTransport, createRemoteTransport, } from "../transport.js";
15
13
  /**
16
- * Every hand-written adapter this plugin ships, in priority order.
14
+ * Hand-written adapters living inside the host.
17
15
  *
18
- * Each factory returns undefined for the entries it does not serve, so the
19
- * lookup is a linear scan rather than a switch, and a model with no adapter yet
20
- * simply has no tool.
16
+ * EMPTY, and it must stay that way (BASE-09). Every model-specific implementation
17
+ * is now an independently versioned extension package reached through the public
18
+ * extension API: Tongue is `@try-works/extension-desert-ant-tongue` and Voz is
19
+ * `@try-works/extension-desert-ant-voz`. The host holds no adapter for any model,
20
+ * so a first-party package has no privilege a published one lacks.
21
+ *
22
+ * The parameter is kept rather than deleted because a test may still inject
23
+ * factories, and an empty list DOCUMENTED as needing to stay empty is a stronger
24
+ * statement than a removed hook.
21
25
  */
22
- export const ADAPTER_FACTORIES = [
23
- createTongueAdapter,
24
- createVozAdapter,
25
- ];
26
+ export const ADAPTER_FACTORIES = [];
26
27
  /**
27
28
  * Wrap a command transport as a model adapter.
28
29
  *
@@ -45,23 +46,37 @@ function transportAdapter(entry, transport) {
45
46
  * A hand-written adapter always wins for the model it serves: an in-process
46
47
  * call is faster and needs no external service. Everything else falls to the
47
48
  * transport the capability resolver selected.
49
+ *
50
+ * The lookup is ASYNC because an extension-backed model is reached by importing
51
+ * and activating its package, which cannot happen in a synchronous call. The
52
+ * order is deliberate: a declared extension binding wins, then a hand-written
53
+ * factory, then a transport.
48
54
  * @param entry - the catalog entry.
49
55
  * @param options - the resolved route plus transport settings.
50
56
  * @param factories - hand-written adapter factories, defaulting to the shipped set.
51
- * @returns the adapter, or undefined when no factory or transport serves this model.
57
+ * @param bridge - the extension provider bridge, when extensions are available.
58
+ * @returns the adapter, or undefined when no extension, factory or transport serves this model.
52
59
  */
53
- export function createAdapter(entry, options = { route: 'npm-native' }, factories = ADAPTER_FACTORIES) {
60
+ export function createAdapter(entry, options = { route: 'npm-native' }, factories = ADAPTER_FACTORIES, bridge) {
61
+ // A declared extension binding is the model's own statement of who implements
62
+ // it, so it is consulted before any host-side knowledge.
63
+ if (bridge !== undefined && entry.provider !== undefined) {
64
+ return bridge.adapterFor(entry);
65
+ }
54
66
  for (const factory of factories) {
55
67
  const adapter = factory(entry);
56
68
  if (adapter !== undefined)
57
69
  return adapter;
58
70
  }
59
- // No hand-written adapter: the model is reachable only through a transport.
71
+ // No extension and no factory: the model is reachable only through a transport.
60
72
  if (options.route === 'cli')
61
73
  return transportAdapter(entry, createCliTransport(entry, options));
62
74
  if (options.route === 'remote') {
63
75
  return transportAdapter(entry, createRemoteTransport(entry, options));
64
76
  }
77
+ // No extension, no factory and no transport serves this model. Returning
78
+ // undefined is correct: a model with no implementation simply has no tool,
79
+ // rather than falling back to something that cannot run it.
65
80
  return undefined;
66
81
  }
67
82
  //# sourceMappingURL=index.js.map
@@ -11,13 +11,21 @@
11
11
  */
12
12
  import type { ModelCatalogEntry } from '../types.ts';
13
13
  import { type ModelAdapter } from './types.ts';
14
+ /** The npm package this adapter knows how to call. */
15
+ export declare const TONGUE_PACKAGE = "@desert-ant-labs/tongue";
14
16
  /**
15
17
  * Build the Tongue adapter.
16
18
  *
17
- * The model is loaded once and reused, because `Tongue.load()` parses the
18
- * bundled 2 MB weight file and detection afterwards is synchronous.
19
+ * The factory is keyed on the PACKAGE, not the catalog id: an adapter's job is to
20
+ * know how to call one SDK, so any catalog row bound to this package can share it.
21
+ * Keying on the model id instead would mean a second conforming implementation of
22
+ * the same contract could never be served — exactly the model-name switch BASE-02
23
+ * and EXT-01 exist to prevent.
24
+ *
25
+ * The model is loaded once and reused, because `Tongue.load()` parses the bundled
26
+ * 2 MB weight file and detection afterwards is synchronous.
19
27
  * @param entry - the catalog entry this adapter serves.
20
- * @returns the adapter, or undefined if the entry is not Tongue.
28
+ * @returns the adapter, or undefined if the entry does not use this SDK.
21
29
  */
22
30
  export declare function createTongueAdapter(entry: ModelCatalogEntry): ModelAdapter | undefined;
23
31
  //# sourceMappingURL=tongue.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tongue.d.ts","sourceRoot":"","sources":["../../src/adapters/tongue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,YAAY,CAAA;AA4BnB;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,GAAG,SAAS,CAwDtF"}
1
+ {"version":3,"file":"tongue.d.ts","sourceRoot":"","sources":["../../src/adapters/tongue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AACpD,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,YAAY,CAAA;AAwBnB,sDAAsD;AACtD,eAAO,MAAM,cAAc,4BAA4B,CAAA;AAMvD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,iBAAiB,GAAG,YAAY,GAAG,SAAS,CAwDtF"}
@@ -18,19 +18,27 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
18
18
  return path;
19
19
  };
20
20
  import { optionalInteger, requireString, throwIfAborted, } from "./types.js";
21
+ /** The npm package this adapter knows how to call. */
22
+ export const TONGUE_PACKAGE = '@desert-ant-labs/tongue';
21
23
  /** Default number of runner-up candidates reported to the model. */
22
24
  const DEFAULT_TOP = 3;
23
25
  const MAX_TOP = 20;
24
26
  /**
25
27
  * Build the Tongue adapter.
26
28
  *
27
- * The model is loaded once and reused, because `Tongue.load()` parses the
28
- * bundled 2 MB weight file and detection afterwards is synchronous.
29
+ * The factory is keyed on the PACKAGE, not the catalog id: an adapter's job is to
30
+ * know how to call one SDK, so any catalog row bound to this package can share it.
31
+ * Keying on the model id instead would mean a second conforming implementation of
32
+ * the same contract could never be served — exactly the model-name switch BASE-02
33
+ * and EXT-01 exist to prevent.
34
+ *
35
+ * The model is loaded once and reused, because `Tongue.load()` parses the bundled
36
+ * 2 MB weight file and detection afterwards is synchronous.
29
37
  * @param entry - the catalog entry this adapter serves.
30
- * @returns the adapter, or undefined if the entry is not Tongue.
38
+ * @returns the adapter, or undefined if the entry does not use this SDK.
31
39
  */
32
40
  export function createTongueAdapter(entry) {
33
- if (entry.id !== 'tongue')
41
+ if (entry.npmPackage !== TONGUE_PACKAGE)
34
42
  return undefined;
35
43
  let loaded;
36
44
  const load = async () => {
@@ -10,6 +10,7 @@
10
10
  * @module @try-works/dsh-model-as-tool/adapters/types
11
11
  */
12
12
  import type { ModelCatalogEntry } from '../types.ts';
13
+ import type { InvocationAuthority } from '../authority.ts';
13
14
  /** A lossless JSON value, the only thing a tool body may return. */
14
15
  export type JsonValue = null | boolean | number | string | JsonValue[] | {
15
16
  [key: string]: JsonValue;
@@ -20,6 +21,16 @@ export interface AdapterInvocation {
20
21
  readonly args: Readonly<Record<string, unknown>>;
21
22
  /** Caller cancellation, forwarded by the tool body. */
22
23
  readonly signal: AbortSignal;
24
+ /**
25
+ * Trusted authority for this call (BASE-05).
26
+ *
27
+ * An implementation MUST resolve a caller-supplied path through
28
+ * `authority.resolvePath` rather than opening the argument directly: the
29
+ * authority is constructed by the host and cannot be supplied or widened by a
30
+ * request. It is optional only so a test can drive an adapter without a grant;
31
+ * a host always provides one.
32
+ */
33
+ readonly authority?: InvocationAuthority;
23
34
  }
24
35
  /** One model's invocation strategy. */
25
36
  export interface ModelAdapter {