@ryuhq/sdk 0.1.14 → 0.2.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 (105) hide show
  1. package/README.md +69 -7
  2. package/dist/action.cjs +839 -0
  3. package/dist/action.d.cts +88 -0
  4. package/dist/action.d.ts +88 -0
  5. package/dist/action.js +8 -0
  6. package/dist/agent-plugin.cjs +46 -0
  7. package/dist/agent-plugin.d.cts +43 -33
  8. package/dist/agent-plugin.d.ts +43 -33
  9. package/dist/agent-plugin.js +1 -1
  10. package/dist/agent.cjs +16 -0
  11. package/dist/agent.d.cts +377 -1
  12. package/dist/agent.d.ts +377 -1
  13. package/dist/agent.js +4 -2
  14. package/dist/app-B0Z9Ew_R.d.cts +141 -0
  15. package/dist/app-C-BDJwfG.d.ts +141 -0
  16. package/dist/builder.cjs +1194 -0
  17. package/dist/builder.d.cts +217 -0
  18. package/dist/builder.d.ts +217 -0
  19. package/dist/builder.js +29 -0
  20. package/dist/chunk-4TPUZDTI.js +94 -0
  21. package/dist/chunk-BC3A7HMO.js +164 -0
  22. package/dist/chunk-FZSFZOIN.js +200 -0
  23. package/dist/{chunk-ODFEUVPW.js → chunk-HACAGK65.js} +1 -18
  24. package/dist/chunk-HLKJZAFK.js +361 -0
  25. package/dist/chunk-IKEDLLFY.js +19 -0
  26. package/dist/{chunk-G6FLVEC4.js → chunk-JX6DDRXV.js} +46 -0
  27. package/dist/{chunk-AO2KJRDD.js → chunk-NZKVOSC2.js} +192 -3
  28. package/dist/chunk-QYFUNJOH.js +83 -0
  29. package/dist/chunk-SN2QBJUF.js +148 -0
  30. package/dist/chunk-TLDPEGC7.js +21 -0
  31. package/dist/chunk-TXSHHZF2.js +0 -0
  32. package/dist/chunk-VLIRNNAE.js +154 -0
  33. package/dist/{chunk-MTUBUPIV.js → chunk-Z57QDDJR.js} +8 -227
  34. package/dist/chunk-ZTJWBRUL.js +282 -0
  35. package/dist/cli.cjs +347 -18
  36. package/dist/cli.js +128 -23
  37. package/dist/client-D5U6ssPc.d.cts +84 -0
  38. package/dist/client-D5U6ssPc.d.ts +84 -0
  39. package/dist/index.cjs +810 -86
  40. package/dist/index.d.cts +19 -634
  41. package/dist/index.d.ts +19 -634
  42. package/dist/index.js +82 -699
  43. package/dist/manifest.cjs +200 -4
  44. package/dist/manifest.d.cts +260 -5
  45. package/dist/manifest.d.ts +260 -5
  46. package/dist/manifest.js +15 -1
  47. package/dist/mcp/client.cjs +180 -0
  48. package/dist/mcp/client.d.cts +49 -0
  49. package/dist/mcp/client.d.ts +49 -0
  50. package/dist/mcp/client.js +10 -0
  51. package/dist/mcp/server.cjs +370 -0
  52. package/dist/mcp/server.d.cts +127 -0
  53. package/dist/mcp/server.d.ts +127 -0
  54. package/dist/mcp/server.js +9 -0
  55. package/dist/mcp.cjs +376 -0
  56. package/dist/mcp.d.cts +2 -0
  57. package/dist/mcp.d.ts +2 -0
  58. package/dist/mcp.js +17 -0
  59. package/dist/model.cjs +141 -0
  60. package/dist/model.d.cts +33 -0
  61. package/dist/model.d.ts +33 -0
  62. package/dist/model.js +18 -0
  63. package/dist/plugin.cjs +46 -0
  64. package/dist/plugin.d.cts +215 -0
  65. package/dist/plugin.d.ts +215 -0
  66. package/dist/plugin.js +8 -0
  67. package/dist/runnable.cjs +1438 -0
  68. package/dist/runnable.d.cts +274 -0
  69. package/dist/runnable.d.ts +274 -0
  70. package/dist/runnable.js +34 -0
  71. package/dist/{index-B6SkaAjJ.d.ts → tool-AjkdFvhE.d.ts} +88 -461
  72. package/dist/{index-BvAB5eMk.d.cts → tool-CgzW92O_.d.cts} +88 -461
  73. package/package.json +48 -3
  74. package/src/agent/loop.test.ts +4 -4
  75. package/src/agent/tools.ts +3 -3
  76. package/src/agent-plugin.test.ts +58 -0
  77. package/src/agent-plugin.ts +109 -36
  78. package/src/builder.ts +9 -0
  79. package/src/cli/dev.test.ts +26 -47
  80. package/src/cli/dev.ts +10 -2
  81. package/src/cli-security.test.ts +109 -0
  82. package/src/cli.ts +141 -25
  83. package/src/contracts-lockstep.test.ts +16 -2
  84. package/src/exports-lockstep.test.ts +93 -0
  85. package/src/generated/plugin-manifest.ts +322 -29
  86. package/src/index.ts +53 -0
  87. package/src/manifest-schema.test.ts +30 -1
  88. package/src/manifest.fixtures.test.ts +22 -6
  89. package/src/manifest.test.ts +165 -10
  90. package/src/manifest.ts +454 -188
  91. package/src/mcp/index.ts +18 -0
  92. package/src/mcp/server.ts +2 -1
  93. package/src/model/index.ts +22 -0
  94. package/src/plugin/ryu-plugin.ts +82 -0
  95. package/src/runnable/action.test.ts +128 -0
  96. package/src/runnable/action.ts +202 -0
  97. package/src/runnable/app.test.ts +2 -0
  98. package/src/runnable/app.ts +50 -14
  99. package/src/runnable/index.ts +20 -3
  100. package/src/runnable/primitives.test.ts +34 -0
  101. package/src/runnable/primitives.ts +116 -0
  102. package/src/runnable/runnable-types.ts +3 -0
  103. package/src/runnable/tool.ts +36 -5
  104. package/src/runnable/turn-hook.ts +8 -3
  105. package/src/slash-command.test.ts +69 -0
@@ -0,0 +1,154 @@
1
+ // src/mcp/client.ts
2
+ import { spawn } from "child_process";
3
+ import { createInterface } from "readline";
4
+ var MCP_PROTOCOL_VERSION = "2024-11-05";
5
+ var RPC_TIMEOUT_MS = 6e4;
6
+ var McpConnection = class _McpConnection {
7
+ proc;
8
+ nextId = 1;
9
+ closed = false;
10
+ waiters = [];
11
+ constructor(proc) {
12
+ this.proc = proc;
13
+ }
14
+ /** Spawn the server and complete the MCP `initialize` handshake. */
15
+ static async connect(cmd) {
16
+ const env = { ...process.env, ...cmd.env ?? {} };
17
+ const proc = spawn(cmd.command, cmd.args ?? [], {
18
+ stdio: ["pipe", "pipe", "pipe"],
19
+ env
20
+ });
21
+ if (!(proc.stdin && proc.stdout)) {
22
+ proc.kill();
23
+ throw new Error(`MCP server '${cmd.command}' stdin/stdout unavailable`);
24
+ }
25
+ if (proc.stderr) {
26
+ proc.stderr.on("data", (chunk) => {
27
+ process.stderr.write(`[mcp-server] ${chunk.toString()}`);
28
+ });
29
+ }
30
+ const conn = new _McpConnection(proc);
31
+ const rl = createInterface({
32
+ input: proc.stdout,
33
+ crlfDelay: Number.POSITIVE_INFINITY
34
+ });
35
+ rl.on("line", (rawLine) => {
36
+ const trimmed = rawLine.trim();
37
+ if (!trimmed) {
38
+ return;
39
+ }
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(trimmed);
43
+ } catch {
44
+ return;
45
+ }
46
+ if (parsed.id === void 0 || parsed.id === null) {
47
+ return;
48
+ }
49
+ const idx = conn.waiters.findIndex((w) => w.id === parsed.id);
50
+ if (idx === -1) {
51
+ return;
52
+ }
53
+ const [waiter] = conn.waiters.splice(idx, 1);
54
+ if (!waiter) {
55
+ return;
56
+ }
57
+ clearTimeout(waiter.timer);
58
+ if (parsed.error) {
59
+ waiter.reject(new Error(`MCP error: ${JSON.stringify(parsed.error)}`));
60
+ } else {
61
+ waiter.resolve(parsed.result ?? null);
62
+ }
63
+ });
64
+ rl.on("close", () => {
65
+ for (const waiter of conn.waiters.splice(0)) {
66
+ clearTimeout(waiter.timer);
67
+ waiter.reject(new Error("MCP server closed the connection"));
68
+ }
69
+ });
70
+ await conn.request("initialize", {
71
+ protocolVersion: MCP_PROTOCOL_VERSION,
72
+ capabilities: {},
73
+ clientInfo: { name: "ryu-sdk", version: "0.0.1" }
74
+ });
75
+ conn.notify("notifications/initialized", {});
76
+ return conn;
77
+ }
78
+ /** Send a JSON-RPC request and return the `result` field. */
79
+ request(method, params) {
80
+ const id = this.nextId++;
81
+ const frame = JSON.stringify({
82
+ jsonrpc: "2.0",
83
+ id,
84
+ method,
85
+ params
86
+ });
87
+ this.write(frame);
88
+ return new Promise((resolve, reject) => {
89
+ const timer = setTimeout(
90
+ () => reject(new Error(`MCP request '${method}' timed out`)),
91
+ RPC_TIMEOUT_MS
92
+ );
93
+ this.waiters.push({ id, resolve, reject, timer });
94
+ });
95
+ }
96
+ /** Send a JSON-RPC notification (no response expected). */
97
+ notify(method, params) {
98
+ const frame = JSON.stringify({ jsonrpc: "2.0", method, params });
99
+ this.write(frame);
100
+ }
101
+ write(frame) {
102
+ if (this.closed) {
103
+ return;
104
+ }
105
+ this.proc.stdin?.write(`${frame}
106
+ `);
107
+ }
108
+ /** Graceful shutdown: close stdin, then kill. */
109
+ async shutdown() {
110
+ this.closed = true;
111
+ this.proc.stdin?.end();
112
+ await new Promise((resolve) => {
113
+ this.proc.once("exit", () => resolve());
114
+ this.proc.kill();
115
+ setTimeout(resolve, 500);
116
+ });
117
+ }
118
+ };
119
+ async function listTools(cmd) {
120
+ const conn = await McpConnection.connect(cmd);
121
+ let result;
122
+ try {
123
+ result = await conn.request("tools/list", {});
124
+ } finally {
125
+ await conn.shutdown();
126
+ }
127
+ const tools = result?.tools ?? [];
128
+ return tools.map((t) => {
129
+ const tool = t;
130
+ const name = tool.name;
131
+ if (typeof name !== "string") {
132
+ return null;
133
+ }
134
+ return {
135
+ name,
136
+ description: typeof tool.description === "string" ? tool.description : void 0,
137
+ inputSchema: tool.inputSchema
138
+ };
139
+ }).filter((t) => t !== null);
140
+ }
141
+ async function callTool(cmd, tool, args) {
142
+ const conn = await McpConnection.connect(cmd);
143
+ try {
144
+ return await conn.request("tools/call", { name: tool, arguments: args });
145
+ } finally {
146
+ await conn.shutdown();
147
+ }
148
+ }
149
+
150
+ export {
151
+ MCP_PROTOCOL_VERSION,
152
+ listTools,
153
+ callTool
154
+ };
@@ -1,231 +1,15 @@
1
+ import {
2
+ createPrimitives,
3
+ httpPrimitiveTransport
4
+ } from "./chunk-ZTJWBRUL.js";
5
+ import {
6
+ defineModel
7
+ } from "./chunk-HACAGK65.js";
1
8
  import {
2
9
  assertAllowedEgressUrl,
3
- defineModel,
4
10
  resolveGatewayToken,
5
11
  resolveGatewayUrl
6
- } from "./chunk-ODFEUVPW.js";
7
-
8
- // src/runnable/primitives.ts
9
- function dataUrlToBytes(dataUrl) {
10
- const match = /^data:([^;,]*)(;base64)?,([\s\S]*)$/.exec(dataUrl);
11
- if (!match) {
12
- throw new Error(
13
- "stt.transcribe expects an `audio` value that is a data: URL (data:<mime>;base64,<data>)"
14
- );
15
- }
16
- const mediaType = match[1] || "application/octet-stream";
17
- const isBase64 = Boolean(match[2]);
18
- const payload = match[3] ?? "";
19
- if (isBase64) {
20
- const binary = atob(payload);
21
- const bytes = new Uint8Array(binary.length);
22
- for (let i = 0; i < binary.length; i++) {
23
- bytes[i] = binary.charCodeAt(i);
24
- }
25
- return { bytes, mediaType };
26
- }
27
- return {
28
- bytes: new TextEncoder().encode(decodeURIComponent(payload)),
29
- mediaType
30
- };
31
- }
32
- function bytesToDataUrl(bytes, mediaType) {
33
- let binary = "";
34
- const chunk = 32768;
35
- for (let i = 0; i < bytes.length; i += chunk) {
36
- binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
37
- }
38
- return `data:${mediaType};base64,${btoa(binary)}`;
39
- }
40
- function httpPrimitiveTransport(options) {
41
- const base = options.nodeUrl.replace(/\/+$/, "");
42
- assertAllowedEgressUrl(base);
43
- const doFetch = options.fetchImpl ?? fetch;
44
- const authHeader = () => options.token ? { authorization: `Bearer ${options.token}` } : {};
45
- const failDetail = async (path, res) => {
46
- const detail = await res.text().catch(() => "");
47
- return new Error(
48
- `Ryu primitive call ${path} failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`
49
- );
50
- };
51
- const post = async (path, body) => {
52
- const res = await doFetch(`${base}${path}`, {
53
- method: "POST",
54
- headers: { "content-type": "application/json", ...authHeader() },
55
- body: JSON.stringify(body ?? {})
56
- });
57
- if (!res.ok) {
58
- throw await failDetail(path, res);
59
- }
60
- const text = await res.text();
61
- return text ? JSON.parse(text) : void 0;
62
- };
63
- const transcribeDirect = async (body) => {
64
- const input = body ?? {};
65
- if (!input.audio) {
66
- throw new Error(
67
- "stt.transcribe requires an `audio` data: URL (the recorded audio)"
68
- );
69
- }
70
- const { bytes, mediaType } = dataUrlToBytes(input.audio);
71
- const form = new FormData();
72
- form.append(
73
- "file",
74
- new Blob([bytes], { type: mediaType || "audio/wav" }),
75
- input.filename ?? "recording.wav"
76
- );
77
- const res = await doFetch(`${base}/api/voice/transcribe`, {
78
- method: "POST",
79
- headers: authHeader(),
80
- body: form
81
- });
82
- if (!res.ok) {
83
- throw await failDetail("/api/voice/transcribe", res);
84
- }
85
- const parsed = await res.json();
86
- return (parsed.text ?? "").trim();
87
- };
88
- const speakDirect = async (body) => {
89
- const res = await doFetch(`${base}/api/voice/speak`, {
90
- method: "POST",
91
- headers: { "content-type": "application/json", ...authHeader() },
92
- body: JSON.stringify(body ?? {})
93
- });
94
- if (!res.ok) {
95
- throw await failDetail("/api/voice/speak", res);
96
- }
97
- const bytes = new Uint8Array(await res.arrayBuffer());
98
- const mediaType = res.headers.get("content-type") || "audio/wav";
99
- return bytesToDataUrl(bytes, mediaType);
100
- };
101
- const generateImageDirect = async (body) => {
102
- const res = await doFetch(`${base}/api/images/generate`, {
103
- method: "POST",
104
- headers: { "content-type": "application/json", ...authHeader() },
105
- body: JSON.stringify(body ?? {})
106
- });
107
- if (!res.ok) {
108
- throw await failDetail("/api/images/generate", res);
109
- }
110
- const parsed = await res.json();
111
- return (parsed.data ?? []).map(
112
- (item) => item.url ? item.url : `data:image/png;base64,${item.b64_json ?? ""}`
113
- );
114
- };
115
- return {
116
- bridge(method, args) {
117
- if (!options.pluginId) {
118
- return Promise.reject(
119
- new Error(
120
- `bridge primitive "${method}" requires a pluginId (the /api/plugins/:id/host caller identity)`
121
- )
122
- );
123
- }
124
- return post(`/api/plugins/${options.pluginId}/host`, { method, args });
125
- },
126
- direct(path, body) {
127
- if (path === "/api/voice/transcribe") {
128
- return transcribeDirect(body);
129
- }
130
- if (path === "/api/voice/speak") {
131
- return speakDirect(body);
132
- }
133
- if (path === "/api/images/generate") {
134
- return generateImageDirect(body);
135
- }
136
- return post(path, body);
137
- },
138
- capability(cap, body) {
139
- return post(`/api/host/capability/${cap}`, body);
140
- }
141
- };
142
- }
143
- var PRIMITIVE_BINDINGS = {
144
- // Bridge families (existing `PluginHookBridge`).
145
- "engines.complete": {
146
- transport: "bridge",
147
- method: "model.complete",
148
- grant: "hook:side-model"
149
- },
150
- // Host-direct media data-path (the host holds the node token; returns data: URLs).
151
- "image.generate": {
152
- transport: "direct",
153
- path: "/api/images/generate",
154
- grant: "media:generate"
155
- },
156
- "tts.speak": {
157
- transport: "direct",
158
- path: "/api/voice/speak",
159
- grant: "media:generate"
160
- },
161
- "stt.transcribe": {
162
- transport: "direct",
163
- path: "/api/voice/transcribe",
164
- grant: "media:transcribe"
165
- },
166
- // Broker capabilities — no rpc family yet (@requires-grant).
167
- "rag.retrieve": { transport: "broker", capability: "rag" },
168
- "rag.embed": { transport: "broker", capability: "rag" },
169
- "rag.rerank": { transport: "broker", capability: "rag" },
170
- "memory.recall": { transport: "broker", capability: "memory" },
171
- "memory.store": { transport: "broker", capability: "memory" },
172
- "realtime.broadcast": { transport: "broker", capability: "realtime" },
173
- "realtime.subscribe": { transport: "broker", capability: "realtime" },
174
- "durable.checkpoint": { transport: "broker", capability: "durable" },
175
- "durable.resume": { transport: "broker", capability: "durable" },
176
- "engines.embed": { transport: "broker", capability: "engines" }
177
- };
178
- function brokerCall(transport, cap, op, input) {
179
- return transport.capability(cap, { op, input });
180
- }
181
- function createPrimitives(transport) {
182
- return {
183
- rag: {
184
- retrieve: (input) => brokerCall(transport, "rag", "retrieve", input),
185
- embed: (input) => brokerCall(transport, "rag", "embed", input),
186
- rerank: (input) => brokerCall(transport, "rag", "rerank", input)
187
- },
188
- memory: {
189
- recall: (input) => brokerCall(transport, "memory", "recall", input),
190
- store: (input) => brokerCall(transport, "memory", "store", input)
191
- },
192
- realtime: {
193
- broadcast: (input) => brokerCall(transport, "realtime", "broadcast", input).then(
194
- () => void 0
195
- ),
196
- subscribe: (input) => brokerCall(
197
- transport,
198
- "realtime",
199
- "subscribe",
200
- input
201
- )
202
- },
203
- durable: {
204
- checkpoint: (input) => brokerCall(transport, "durable", "checkpoint", input),
205
- resume: (input) => brokerCall(transport, "durable", "resume", input)
206
- },
207
- engines: {
208
- // Bridge family: model.complete → host.sideModel (wire keys are snake_case).
209
- complete: (input) => transport.bridge("model.complete", {
210
- prompt: input.prompt,
211
- system: input.system,
212
- model: input.model,
213
- model_pref_key: input.modelPrefKey,
214
- effort: input.effort
215
- }),
216
- embed: (input) => brokerCall(transport, "engines", "embed", input)
217
- },
218
- tts: {
219
- speak: (input) => transport.direct("/api/voice/speak", input)
220
- },
221
- stt: {
222
- transcribe: (input) => transport.direct("/api/voice/transcribe", input)
223
- },
224
- image: {
225
- generate: (input) => transport.direct("/api/images/generate", input)
226
- }
227
- };
228
- }
12
+ } from "./chunk-IKEDLLFY.js";
229
13
 
230
14
  // src/agent/model-call.ts
231
15
  var CHAT_COMPLETIONS_PATH = "/v1/chat/completions";
@@ -659,9 +443,6 @@ function query(input) {
659
443
  }
660
444
 
661
445
  export {
662
- httpPrimitiveTransport,
663
- PRIMITIVE_BINDINGS,
664
- createPrimitives,
665
446
  callModelWithTools,
666
447
  ryuTool,
667
448
  resolveToolDefs,
@@ -0,0 +1,282 @@
1
+ import {
2
+ assertAllowedEgressUrl
3
+ } from "./chunk-IKEDLLFY.js";
4
+
5
+ // src/runnable/primitives.ts
6
+ function dataUrlToBytes(dataUrl) {
7
+ const match = /^data:([^;,]*)(;base64)?,([\s\S]*)$/.exec(dataUrl);
8
+ if (!match) {
9
+ throw new Error(
10
+ "stt.transcribe expects an `audio` value that is a data: URL (data:<mime>;base64,<data>)"
11
+ );
12
+ }
13
+ const mediaType = match[1] || "application/octet-stream";
14
+ const isBase64 = Boolean(match[2]);
15
+ const payload = match[3] ?? "";
16
+ if (isBase64) {
17
+ const binary = atob(payload);
18
+ const bytes = new Uint8Array(binary.length);
19
+ for (let i = 0; i < binary.length; i++) {
20
+ bytes[i] = binary.charCodeAt(i);
21
+ }
22
+ return { bytes, mediaType };
23
+ }
24
+ return {
25
+ bytes: new TextEncoder().encode(decodeURIComponent(payload)),
26
+ mediaType
27
+ };
28
+ }
29
+ function bytesToDataUrl(bytes, mediaType) {
30
+ let binary = "";
31
+ const chunk = 32768;
32
+ for (let i = 0; i < bytes.length; i += chunk) {
33
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
34
+ }
35
+ return `data:${mediaType};base64,${btoa(binary)}`;
36
+ }
37
+ function httpPrimitiveTransport(options) {
38
+ const base = options.nodeUrl.replace(/\/+$/, "");
39
+ assertAllowedEgressUrl(base);
40
+ const doFetch = options.fetchImpl ?? fetch;
41
+ const authHeader = () => options.token ? { authorization: `Bearer ${options.token}` } : {};
42
+ const failDetail = async (path, res) => {
43
+ const detail = await res.text().catch(() => "");
44
+ return new Error(
45
+ `Ryu primitive call ${path} failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`
46
+ );
47
+ };
48
+ const post = async (path, body) => {
49
+ const res = await doFetch(`${base}${path}`, {
50
+ method: "POST",
51
+ headers: { "content-type": "application/json", ...authHeader() },
52
+ body: JSON.stringify(body ?? {})
53
+ });
54
+ if (!res.ok) {
55
+ throw await failDetail(path, res);
56
+ }
57
+ const text = await res.text();
58
+ return text ? JSON.parse(text) : void 0;
59
+ };
60
+ const transcribeDirect = async (body) => {
61
+ const input = body ?? {};
62
+ if (!input.audio) {
63
+ throw new Error(
64
+ "stt.transcribe requires an `audio` data: URL (the recorded audio)"
65
+ );
66
+ }
67
+ const { bytes, mediaType } = dataUrlToBytes(input.audio);
68
+ const form = new FormData();
69
+ form.append(
70
+ "file",
71
+ new Blob([bytes], { type: mediaType || "audio/wav" }),
72
+ input.filename ?? "recording.wav"
73
+ );
74
+ const res = await doFetch(`${base}/api/voice/transcribe`, {
75
+ method: "POST",
76
+ headers: authHeader(),
77
+ body: form
78
+ });
79
+ if (!res.ok) {
80
+ throw await failDetail("/api/voice/transcribe", res);
81
+ }
82
+ const parsed = await res.json();
83
+ return (parsed.text ?? "").trim();
84
+ };
85
+ const speakDirect = async (body) => {
86
+ const res = await doFetch(`${base}/api/voice/speak`, {
87
+ method: "POST",
88
+ headers: { "content-type": "application/json", ...authHeader() },
89
+ body: JSON.stringify(body ?? {})
90
+ });
91
+ if (!res.ok) {
92
+ throw await failDetail("/api/voice/speak", res);
93
+ }
94
+ const bytes = new Uint8Array(await res.arrayBuffer());
95
+ const mediaType = res.headers.get("content-type") || "audio/wav";
96
+ return bytesToDataUrl(bytes, mediaType);
97
+ };
98
+ const generateImageDirect = async (body) => {
99
+ const res = await doFetch(`${base}/api/images/generate`, {
100
+ method: "POST",
101
+ headers: { "content-type": "application/json", ...authHeader() },
102
+ body: JSON.stringify(body ?? {})
103
+ });
104
+ if (!res.ok) {
105
+ throw await failDetail("/api/images/generate", res);
106
+ }
107
+ const parsed = await res.json();
108
+ return (parsed.data ?? []).map(
109
+ (item) => item.url ? item.url : `data:image/png;base64,${item.b64_json ?? ""}`
110
+ );
111
+ };
112
+ return {
113
+ bridge(method, args) {
114
+ if (!options.pluginId) {
115
+ return Promise.reject(
116
+ new Error(
117
+ `bridge primitive "${method}" requires a pluginId (the /api/plugins/:id/host caller identity)`
118
+ )
119
+ );
120
+ }
121
+ return post(`/api/plugins/${options.pluginId}/host`, { method, args });
122
+ },
123
+ direct(path, body) {
124
+ if (path === "/api/voice/transcribe") {
125
+ return transcribeDirect(body);
126
+ }
127
+ if (path === "/api/voice/speak") {
128
+ return speakDirect(body);
129
+ }
130
+ if (path === "/api/images/generate") {
131
+ return generateImageDirect(body);
132
+ }
133
+ return post(path, body);
134
+ },
135
+ capability(cap, body) {
136
+ return post(`/api/host/capability/${cap}`, body);
137
+ }
138
+ };
139
+ }
140
+ var PRIMITIVE_BINDINGS = {
141
+ // Bridge families (existing `PluginHookBridge`).
142
+ "engines.complete": {
143
+ transport: "bridge",
144
+ method: "model.complete",
145
+ grant: "hook:side-model"
146
+ },
147
+ "background.list": {
148
+ transport: "bridge",
149
+ method: "background.list",
150
+ grant: "background:control"
151
+ },
152
+ "background.stop": {
153
+ transport: "bridge",
154
+ method: "background.stop",
155
+ grant: "background:control"
156
+ },
157
+ "storage.get": {
158
+ transport: "bridge",
159
+ method: "storage.get",
160
+ grant: "storage:kv"
161
+ },
162
+ "storage.set": {
163
+ transport: "bridge",
164
+ method: "storage.set",
165
+ grant: "storage:kv"
166
+ },
167
+ "storage.delete": {
168
+ transport: "bridge",
169
+ method: "storage.delete",
170
+ grant: "storage:kv"
171
+ },
172
+ "storage.keys": {
173
+ transport: "bridge",
174
+ method: "storage.keys",
175
+ grant: "storage:kv"
176
+ },
177
+ "storage.compareAndSet": {
178
+ transport: "bridge",
179
+ method: "storage.compareAndSet",
180
+ grant: "storage:kv"
181
+ },
182
+ // Host-direct media data-path (the host holds the node token; returns data: URLs).
183
+ "image.generate": {
184
+ transport: "direct",
185
+ path: "/api/images/generate",
186
+ grant: "media:generate"
187
+ },
188
+ "tts.speak": {
189
+ transport: "direct",
190
+ path: "/api/voice/speak",
191
+ grant: "media:generate"
192
+ },
193
+ "stt.transcribe": {
194
+ transport: "direct",
195
+ path: "/api/voice/transcribe",
196
+ grant: "media:transcribe"
197
+ },
198
+ // Broker capabilities — no rpc family yet (@requires-grant).
199
+ "rag.retrieve": { transport: "broker", capability: "rag" },
200
+ "rag.embed": { transport: "broker", capability: "rag" },
201
+ "rag.rerank": { transport: "broker", capability: "rag" },
202
+ "memory.recall": { transport: "broker", capability: "memory" },
203
+ "memory.store": { transport: "broker", capability: "memory" },
204
+ "realtime.broadcast": { transport: "broker", capability: "realtime" },
205
+ "realtime.subscribe": { transport: "broker", capability: "realtime" },
206
+ "durable.checkpoint": { transport: "broker", capability: "durable" },
207
+ "durable.resume": { transport: "broker", capability: "durable" },
208
+ "engines.embed": { transport: "broker", capability: "engines" }
209
+ };
210
+ function brokerCall(transport, cap, op, input) {
211
+ return transport.capability(cap, { op, input });
212
+ }
213
+ function createPrimitives(transport) {
214
+ return {
215
+ background: {
216
+ list: (input) => transport.bridge("background.list", {
217
+ producer: input?.producer,
218
+ running_only: input?.runningOnly
219
+ }),
220
+ stop: (input) => transport.bridge("background.stop", {
221
+ process_id: input.processId
222
+ })
223
+ },
224
+ storage: {
225
+ get: (input) => transport.bridge("storage.get", input),
226
+ set: (input) => transport.bridge("storage.set", input).then(() => void 0),
227
+ delete: (input) => transport.bridge("storage.delete", input).then(() => void 0),
228
+ keys: (input) => transport.bridge("storage.keys", input ?? {}).then((value) => Array.isArray(value) ? value : []),
229
+ compareAndSet: (input) => transport.bridge("storage.compareAndSet", input)
230
+ },
231
+ rag: {
232
+ retrieve: (input) => brokerCall(transport, "rag", "retrieve", input),
233
+ embed: (input) => brokerCall(transport, "rag", "embed", input),
234
+ rerank: (input) => brokerCall(transport, "rag", "rerank", input)
235
+ },
236
+ memory: {
237
+ recall: (input) => brokerCall(transport, "memory", "recall", input),
238
+ store: (input) => brokerCall(transport, "memory", "store", input)
239
+ },
240
+ realtime: {
241
+ broadcast: (input) => brokerCall(transport, "realtime", "broadcast", input).then(
242
+ () => void 0
243
+ ),
244
+ subscribe: (input) => brokerCall(
245
+ transport,
246
+ "realtime",
247
+ "subscribe",
248
+ input
249
+ )
250
+ },
251
+ durable: {
252
+ checkpoint: (input) => brokerCall(transport, "durable", "checkpoint", input),
253
+ resume: (input) => brokerCall(transport, "durable", "resume", input)
254
+ },
255
+ engines: {
256
+ // Bridge family: model.complete → host.sideModel (wire keys are snake_case).
257
+ complete: (input) => transport.bridge("model.complete", {
258
+ prompt: input.prompt,
259
+ system: input.system,
260
+ model: input.model,
261
+ model_pref_key: input.modelPrefKey,
262
+ effort: input.effort
263
+ }),
264
+ embed: (input) => brokerCall(transport, "engines", "embed", input)
265
+ },
266
+ tts: {
267
+ speak: (input) => transport.direct("/api/voice/speak", input)
268
+ },
269
+ stt: {
270
+ transcribe: (input) => transport.direct("/api/voice/transcribe", input)
271
+ },
272
+ image: {
273
+ generate: (input) => transport.direct("/api/images/generate", input)
274
+ }
275
+ };
276
+ }
277
+
278
+ export {
279
+ httpPrimitiveTransport,
280
+ PRIMITIVE_BINDINGS,
281
+ createPrimitives
282
+ };