@liy/mote-pi-runtime 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +63 -0
- package/dist/assets/skills/agent-browser/SKILL.md +15 -0
- package/dist/assets/skills/gen-app/SKILL.md +76 -0
- package/dist/assets/skills/inline-widget/SKILL.md +38 -0
- package/dist/assets/skills/orchestrator/SKILL.md +160 -0
- package/dist/cli.js +14892 -0
- package/dist/cli.js.map +7 -0
- package/dist/extensions/mote-mcp.js +32 -0
- package/dist/extensions/mote-prompt-composer.js +44 -0
- package/dist/extensions/mote-provider.js +291 -0
- package/dist/extensions/node_modules/pi-mcp-adapter/index.js +19261 -0
- package/dist/extensions/node_modules/pi-mcp-adapter/package.json +5 -0
- package/dist/extensions/package.json +4 -0
- package/dist/extensions/web-access.js +24568 -0
- package/dist/pi-rpc.js +348 -0
- package/dist/pi-rpc.js.map +7 -0
- package/package.json +56 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// assets/mote-mcp.ts
|
|
2
|
+
var adapterModuleName = "pi-mcp-adapter";
|
|
3
|
+
var { createMcpAdapter } = await import(adapterModuleName);
|
|
4
|
+
var registerMoteMcpAdapter = createMcpAdapter({
|
|
5
|
+
// Mote owns this fixed local transport. Keeping it programmatic prevents a
|
|
6
|
+
// writable Pi session from becoming a source of MCP routing configuration.
|
|
7
|
+
config: {
|
|
8
|
+
// Widget data may exceed the adapter's 50 KiB model-text limit. Preserve
|
|
9
|
+
// the full structured result for timeline replay while retaining that text
|
|
10
|
+
// guard. A 192 KiB widget fits even with MCP's escaped text duplicate.
|
|
11
|
+
settings: {
|
|
12
|
+
directToolResultDetails: "bounded",
|
|
13
|
+
outputGuard: { detailsMaxBytes: 1024 * 1024 }
|
|
14
|
+
},
|
|
15
|
+
mcpServers: {
|
|
16
|
+
mote: {
|
|
17
|
+
command: "orchestrator",
|
|
18
|
+
args: ["mcp", "mote"],
|
|
19
|
+
protocolVersion: "2026-07-28",
|
|
20
|
+
// Cards require the native structured tool result. A scripting wrapper
|
|
21
|
+
// can omit it or print it as prose before Pi emits the message.
|
|
22
|
+
directTools: ["propose_action", "render_inline_widget"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
function registerMoteMcp(pi) {
|
|
28
|
+
registerMoteMcpAdapter(pi);
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
registerMoteMcp as default
|
|
32
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// assets/mote-prompt-composer.ts
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
var INSTRUCTIONS = {
|
|
5
|
+
en: {
|
|
6
|
+
language: "Reply concisely in English, even when history or tools use another language. Preserve code, identifiers and necessary quotations.",
|
|
7
|
+
sources: "Connected sources first: look for relevant data in the user's authorized MCP tools, files, devices, and services before searching the web. Use the web to fill gaps or when explicitly requested.",
|
|
8
|
+
assessment: "Assess ticket outcomes using the orchestrator skill. Start with orchestrator ticket brief; use ticket worklog for authored context and targeted ticket timeline pages for evidence. Decide the next assignment, gatekeeper review, parking or completion from the evidence. Delegate implementation and validation; do not repeat that work yourself. Assign a different agent to gatekeep completed work. Finish after recording your decisions; do not wait or poll for agents.",
|
|
9
|
+
ticket: "Use the orchestrator skill and orchestrator ticket brief to read the assigned ticket and its current context. Complete only the assigned work and report the outcome, evidence and any blocker concisely. Do not assign agents or change ticket lifecycle state. When assigned as gatekeeper, review against the requirements and integrate app changes only after they pass. Never gatekeep your own work. Before ending the assignment, submit orchestrator ticket handoff with results, validation, remainingWork, and optional evidence references. Report incomplete work honestly; a final response does not replace the handoff command. Finish your turn when the assignment is complete or blocked."
|
|
10
|
+
},
|
|
11
|
+
"zh-CN": {
|
|
12
|
+
language: "\u4F7F\u7528\u7B80\u4F53\u4E2D\u6587\u7B80\u6D01\u56DE\u590D\uFF0C\u5373\u4F7F\u5386\u53F2\u6D88\u606F\u6216\u5DE5\u5177\u5185\u5BB9\u4F7F\u7528\u5176\u4ED6\u8BED\u8A00\u3002\u4FDD\u7559\u4EE3\u7801\u3001\u6807\u8BC6\u7B26\u548C\u5FC5\u8981\u7684\u539F\u6587\u5F15\u7528\u3002",
|
|
13
|
+
sources: "\u5DF2\u8FDE\u63A5\u7684\u6570\u636E\u6E90\u4F18\u5148\uFF1A\u5148\u4ECE\u7528\u6237\u5DF2\u6388\u6743\u7684 MCP \u5DE5\u5177\u3001\u6587\u4EF6\u3001\u8BBE\u5907\u548C\u670D\u52A1\u4E2D\u67E5\u627E\u76F8\u5173\u6570\u636E\uFF0C\u518D\u641C\u7D22\u7F51\u9875\u3002\u9700\u8981\u8865\u5145\u4FE1\u606F\u6216\u7528\u6237\u660E\u786E\u8981\u6C42\u65F6\uFF0C\u4F7F\u7528\u7F51\u9875\u641C\u7D22\u3002",
|
|
14
|
+
assessment: "\u4F7F\u7528 orchestrator \u6280\u80FD\uFF0C\u5148\u8BFB\u53D6 orchestrator ticket brief\uFF0C\u901A\u8FC7 ticket worklog \u83B7\u53D6\u8865\u5145\u4E0A\u4E0B\u6587\uFF0C\u4EC5\u5728\u9700\u8981\u8BC1\u636E\u65F6\u8BFB\u53D6\u76F8\u5E94\u7684 ticket timeline \u9875\u9762\u3002\u4F9D\u636E\u8BC1\u636E\u8BC4\u4F30\u5DE5\u5355\u7ED3\u679C\uFF0C\u51B3\u5B9A\u4E0B\u4E00\u6B21\u5206\u914D\u3001\u9A8C\u6536\u3001\u6401\u7F6E\u6216\u5B8C\u6210\u3002\u5C06\u5B9E\u73B0\u548C\u9A8C\u8BC1\u4EA4\u7ED9\u76F8\u5E94\u4E13\u5BB6\uFF0C\u4E0D\u8981\u4EB2\u81EA\u91CD\u590D\u6267\u884C\u3002\u5DF2\u5B8C\u6210\u7684\u5DE5\u4F5C\u5E94\u4EA4\u7ED9\u53E6\u4E00\u4F4D\u4EE3\u7406\u9A8C\u6536\u3002\u8BB0\u5F55\u51B3\u7B56\u540E\u7ED3\u675F\uFF0C\u4E0D\u8981\u7B49\u5F85\u6216\u8F6E\u8BE2\u5176\u4ED6\u4EE3\u7406\u3002",
|
|
15
|
+
ticket: "\u4F7F\u7528 orchestrator \u6280\u80FD\u548C orchestrator ticket brief \u8BFB\u53D6\u6240\u5206\u914D\u7684\u5DE5\u5355\u53CA\u5176\u5F53\u524D\u4E0A\u4E0B\u6587\u3002\u4EC5\u5B8C\u6210\u6240\u5206\u914D\u7684\u5DE5\u4F5C\uFF0C\u7B80\u8981\u62A5\u544A\u7ED3\u679C\u3001\u8BC1\u636E\u53CA\u963B\u788D\u3002\u4E0D\u8981\u5206\u914D\u4EE3\u7406\u6216\u66F4\u6539\u5DE5\u5355\u751F\u547D\u5468\u671F\u72B6\u6001\u3002\u88AB\u5206\u914D\u4E3A\u9A8C\u6536\u8005\u65F6\uFF0C\u4F9D\u636E\u9700\u6C42\u5BA1\u67E5\uFF0C\u901A\u8FC7\u540E\u518D\u96C6\u6210\u5E94\u7528\u66F4\u6539\u3002\u4E0D\u5F97\u9A8C\u6536\u81EA\u5DF1\u7684\u5DE5\u4F5C\u3002\u7ED3\u675F\u4EFB\u52A1\u524D\uFF0C\u63D0\u4EA4 orchestrator ticket handoff\uFF0C\u5305\u542B results\u3001validation\u3001remainingWork \u548C\u53EF\u9009\u7684\u8BC1\u636E\u5F15\u7528\u3002\u5982\u5B9E\u62A5\u544A\u672A\u5B8C\u6210\u7684\u5DE5\u4F5C\uFF1B\u6700\u7EC8\u56DE\u590D\u4E0D\u80FD\u4EE3\u66FF\u4EA4\u63A5\u547D\u4EE4\u3002\u4EFB\u52A1\u5B8C\u6210\u6216\u53D7\u963B\u540E\u7ED3\u675F\u672C\u8F6E\u3002"
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function composeMotePrompt(basePrompt, document, contextKind) {
|
|
19
|
+
const match = /^---\n([^\n]+)\n---\n([\s\S]*)$/.exec(document);
|
|
20
|
+
if (!match?.[1] || !match[2]?.trim()) throw new Error("Invalid Mote persona snapshot");
|
|
21
|
+
const metadata = JSON.parse(match[1]);
|
|
22
|
+
if (metadata.locale !== "en" && metadata.locale !== "zh-CN") throw new Error("Invalid Mote persona locale");
|
|
23
|
+
const instructions = INSTRUCTIONS[metadata.locale];
|
|
24
|
+
if (!["channel", "assessment", "ticket"].includes(contextKind)) throw new Error("Invalid Mote session context");
|
|
25
|
+
const execution = contextKind === "assessment" ? instructions.assessment : contextKind === "ticket" ? instructions.ticket : "";
|
|
26
|
+
const boundary = basePrompt.indexOf("\n\nAvailable tools:\n");
|
|
27
|
+
if (boundary < 0) throw new Error("Pi prompt is missing its tools boundary");
|
|
28
|
+
return [match[2].trim(), instructions.language, instructions.sources, execution].filter(Boolean).join("\n\n") + basePrompt.slice(boundary);
|
|
29
|
+
}
|
|
30
|
+
function mote_prompt_composer_default(pi) {
|
|
31
|
+
pi.on("before_agent_start", async (event) => {
|
|
32
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR;
|
|
33
|
+
if (!agentDir) throw new Error("Mote agent profile directory is missing");
|
|
34
|
+
return { systemPrompt: composeMotePrompt(
|
|
35
|
+
event.systemPrompt,
|
|
36
|
+
await readFile(join(agentDir, "system_prompt.md"), "utf8"),
|
|
37
|
+
process.env.MOTE_SESSION_CONTEXT_KIND ?? ""
|
|
38
|
+
) };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export {
|
|
42
|
+
composeMotePrompt,
|
|
43
|
+
mote_prompt_composer_default as default
|
|
44
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// assets/mote-provider.ts
|
|
2
|
+
import { connect } from "node:net";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import {
|
|
8
|
+
getModels,
|
|
9
|
+
streamSimpleAnthropic as streamAnthropic,
|
|
10
|
+
streamSimpleOpenAIResponses as streamResponses,
|
|
11
|
+
streamSimpleOpenAICompletions as streamCompletions,
|
|
12
|
+
streamSimpleGoogle as streamGoogle
|
|
13
|
+
} from "@earendil-works/pi-ai/compat";
|
|
14
|
+
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
15
|
+
var MAX_FRAME_BYTES = 8 * 1024 * 1024;
|
|
16
|
+
var MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
17
|
+
var PROVIDER_STREAM_TIMEOUT_MS = 10 * 6e4;
|
|
18
|
+
function registerMoteProvider(pi) {
|
|
19
|
+
registerMoteProviderWithTransport(pi, openRunConnectorStream);
|
|
20
|
+
}
|
|
21
|
+
function registerMoteProviderWithTransport(pi, transport) {
|
|
22
|
+
register("deepseek", "openai-responses", streamResponses, "/responses");
|
|
23
|
+
register("moonshotai", "openai-completions", streamCompletions, "/chat/completions");
|
|
24
|
+
register("openai", "openai-responses", streamResponses, "/responses");
|
|
25
|
+
register("anthropic", "anthropic-messages", streamAnthropic, "/messages");
|
|
26
|
+
register("zai", "openai-completions", streamCompletions, "/chat/completions");
|
|
27
|
+
pi.registerProvider("google", {
|
|
28
|
+
api: "google-generative-ai",
|
|
29
|
+
apiKey: "run-bound",
|
|
30
|
+
streamSimple: (model, context, options) => streamGoogleWithRelay(model, context, options, transport)
|
|
31
|
+
});
|
|
32
|
+
function register(provider, api, stream, path) {
|
|
33
|
+
pi.registerProvider(provider, {
|
|
34
|
+
api,
|
|
35
|
+
apiKey: "run-bound",
|
|
36
|
+
// Pi selects the override only when the model API matches. In particular,
|
|
37
|
+
// its DeepSeek catalog defaults to Completions; setting provider.api alone
|
|
38
|
+
// leaves those models on Pi's direct-network adapter. Keep all metadata.
|
|
39
|
+
models: getModels(provider).map((model) => ({ ...model, api })),
|
|
40
|
+
streamSimple(model, context, options) {
|
|
41
|
+
return stream({ ...model, api }, context, {
|
|
42
|
+
...options,
|
|
43
|
+
apiKey: "run-bound",
|
|
44
|
+
transport: "sse",
|
|
45
|
+
fetch: createRunBoundFetch(provider, model.baseUrl, path, transport)
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function createRunBoundFetch(provider, baseUrl, path, transport) {
|
|
52
|
+
return async (input, init) => {
|
|
53
|
+
const request = new Request(input, init);
|
|
54
|
+
const url = new URL(request.url);
|
|
55
|
+
const base = new URL(baseUrl);
|
|
56
|
+
const prefix = base.pathname.replace(/\/$/u, "");
|
|
57
|
+
if (request.method !== "POST" || url.origin !== base.origin || !url.pathname.startsWith(`${prefix}/`) || !url.pathname.endsWith(path) || url.search || url.hash) throw new Error("provider adapter attempted an unsupported route");
|
|
58
|
+
const response = await transport({
|
|
59
|
+
version: 1,
|
|
60
|
+
operation: "connector.stream",
|
|
61
|
+
alias: provider,
|
|
62
|
+
...process.env.MOTE_AGENT_LAUNCH_ID?.trim() ? { launchId: process.env.MOTE_AGENT_LAUNCH_ID.trim() } : {},
|
|
63
|
+
method: "POST",
|
|
64
|
+
path,
|
|
65
|
+
body: JSON.parse(await request.text())
|
|
66
|
+
}, request.signal);
|
|
67
|
+
return new Response(response.body, {
|
|
68
|
+
status: response.status,
|
|
69
|
+
...response.contentType ? { headers: { "content-type": response.contentType } } : {}
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function streamGoogleWithRelay(model, context, options, transport) {
|
|
74
|
+
const result = createAssistantMessageEventStream();
|
|
75
|
+
void (async () => {
|
|
76
|
+
let relay;
|
|
77
|
+
let setupError;
|
|
78
|
+
try {
|
|
79
|
+
relay = await openGoogleRelay(model.id, transport);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
setupError = error;
|
|
82
|
+
}
|
|
83
|
+
const native = streamGoogle({ ...model, baseUrl: relay?.url ?? model.baseUrl }, context, {
|
|
84
|
+
...options,
|
|
85
|
+
apiKey: relay?.key ?? "run-bound",
|
|
86
|
+
...relay ? {} : { onPayload: () => {
|
|
87
|
+
throw setupError;
|
|
88
|
+
} }
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
for await (const event of native) result.push(event);
|
|
92
|
+
result.end(await native.result());
|
|
93
|
+
} finally {
|
|
94
|
+
await relay?.close();
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
async function openGoogleRelay(modelId, transport) {
|
|
100
|
+
const key = randomBytes(32).toString("hex");
|
|
101
|
+
const keyBytes = Buffer.from(key);
|
|
102
|
+
const path = `/models/${encodeURIComponent(modelId)}:streamGenerateContent?alt=sse`;
|
|
103
|
+
const server = createServer((request, response) => {
|
|
104
|
+
void (async () => {
|
|
105
|
+
const supplied = Buffer.from(String(request.headers["x-goog-api-key"] ?? ""));
|
|
106
|
+
if (supplied.length !== keyBytes.length || !timingSafeEqual(supplied, keyBytes)) {
|
|
107
|
+
response.writeHead(403).end();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (request.method !== "POST" || request.url !== path) {
|
|
111
|
+
response.writeHead(404).end();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const abort = new AbortController();
|
|
115
|
+
response.once("close", () => abort.abort());
|
|
116
|
+
const chunks = [];
|
|
117
|
+
let bytes = 0;
|
|
118
|
+
for await (const chunk of request) {
|
|
119
|
+
bytes += chunk.length;
|
|
120
|
+
if (bytes > 1024 * 1024) {
|
|
121
|
+
response.writeHead(413).end();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
chunks.push(chunk);
|
|
125
|
+
}
|
|
126
|
+
const upstream = await transport({
|
|
127
|
+
version: 1,
|
|
128
|
+
operation: "connector.stream",
|
|
129
|
+
alias: "google",
|
|
130
|
+
...process.env.MOTE_AGENT_LAUNCH_ID?.trim() ? { launchId: process.env.MOTE_AGENT_LAUNCH_ID.trim() } : {},
|
|
131
|
+
method: "POST",
|
|
132
|
+
path,
|
|
133
|
+
body: JSON.parse(Buffer.concat(chunks).toString("utf8"))
|
|
134
|
+
}, abort.signal);
|
|
135
|
+
response.writeHead(upstream.status, { "content-type": upstream.contentType ?? "application/json" });
|
|
136
|
+
await pipeline(Readable.fromWeb(upstream.body), response);
|
|
137
|
+
})().catch(() => {
|
|
138
|
+
if (!response.headersSent) response.writeHead(502, { "content-type": "application/json" }).end('{"error":{"message":"provider transport failed"}}');
|
|
139
|
+
else response.destroy();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
await new Promise((resolve, reject) => {
|
|
143
|
+
server.once("error", reject);
|
|
144
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
145
|
+
});
|
|
146
|
+
const address = server.address();
|
|
147
|
+
if (!address || typeof address === "string") throw new Error("Google relay did not bind");
|
|
148
|
+
return {
|
|
149
|
+
key,
|
|
150
|
+
url: `http://127.0.0.1:${address.port}`,
|
|
151
|
+
close: () => new Promise((resolve) => {
|
|
152
|
+
server.close(() => resolve());
|
|
153
|
+
server.closeAllConnections();
|
|
154
|
+
})
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function openRunConnectorStream(value, signal) {
|
|
158
|
+
const socketPath = process.env.MOTE_AGENT_COMMAND_SOCKET?.trim();
|
|
159
|
+
if (!socketPath?.startsWith("/users/") || !socketPath.endsWith("/agent.sock")) {
|
|
160
|
+
return Promise.reject(new Error("agent command socket is unavailable"));
|
|
161
|
+
}
|
|
162
|
+
const requestBody = Buffer.from(JSON.stringify(value));
|
|
163
|
+
if (requestBody.byteLength === 0 || requestBody.byteLength > 1024 * 1024) {
|
|
164
|
+
return Promise.reject(new Error("provider request is too large"));
|
|
165
|
+
}
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const socket = connect(socketPath);
|
|
168
|
+
let responseController;
|
|
169
|
+
const responseBody = new ReadableStream({
|
|
170
|
+
start(controller) {
|
|
171
|
+
responseController = controller;
|
|
172
|
+
},
|
|
173
|
+
cancel() {
|
|
174
|
+
socket.destroy();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
let buffered = Buffer.alloc(0);
|
|
178
|
+
let frameLength;
|
|
179
|
+
let responseStarted = false;
|
|
180
|
+
let terminal = false;
|
|
181
|
+
let bytesRead = 0;
|
|
182
|
+
const cleanup = () => signal?.removeEventListener("abort", onAbort);
|
|
183
|
+
const fail = (message) => {
|
|
184
|
+
if (terminal) return;
|
|
185
|
+
terminal = true;
|
|
186
|
+
cleanup();
|
|
187
|
+
socket.destroy();
|
|
188
|
+
const error = new Error(message);
|
|
189
|
+
if (responseStarted) responseController?.error(error);
|
|
190
|
+
else reject(error);
|
|
191
|
+
};
|
|
192
|
+
const onAbort = () => fail("provider request was aborted");
|
|
193
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
194
|
+
socket.setTimeout(PROVIDER_STREAM_TIMEOUT_MS, () => fail("provider request timed out"));
|
|
195
|
+
socket.once("connect", () => {
|
|
196
|
+
const header = Buffer.allocUnsafe(4);
|
|
197
|
+
header.writeUInt32BE(requestBody.byteLength);
|
|
198
|
+
socket.write(Buffer.concat([header, requestBody]));
|
|
199
|
+
});
|
|
200
|
+
socket.on("data", (chunk) => {
|
|
201
|
+
buffered = Buffer.concat([buffered, chunk]);
|
|
202
|
+
while (!terminal) {
|
|
203
|
+
if (frameLength === void 0) {
|
|
204
|
+
if (buffered.byteLength < 4) return;
|
|
205
|
+
frameLength = buffered.readUInt32BE(0);
|
|
206
|
+
buffered = buffered.subarray(4);
|
|
207
|
+
if (frameLength < 1 || frameLength > MAX_FRAME_BYTES) {
|
|
208
|
+
fail("provider response frame is invalid");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (buffered.byteLength < frameLength) return;
|
|
213
|
+
const frameBody = buffered.subarray(0, frameLength);
|
|
214
|
+
buffered = buffered.subarray(frameLength);
|
|
215
|
+
frameLength = void 0;
|
|
216
|
+
let frame;
|
|
217
|
+
try {
|
|
218
|
+
frame = JSON.parse(frameBody.toString("utf8"));
|
|
219
|
+
} catch {
|
|
220
|
+
fail("provider response frame is invalid");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const record = requireRecord(frame, "provider response frame");
|
|
224
|
+
if (record.type === "connector.response.start") {
|
|
225
|
+
if (responseStarted || typeof record.ok !== "boolean" || !Number.isInteger(record.status) || record.contentType !== void 0 && record.contentType !== "application/json" && record.contentType !== "text/plain" && record.contentType !== "text/event-stream") {
|
|
226
|
+
fail("provider response start frame is invalid");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
responseStarted = true;
|
|
230
|
+
resolve({
|
|
231
|
+
ok: record.ok,
|
|
232
|
+
status: Number(record.status),
|
|
233
|
+
...record.contentType ? { contentType: record.contentType } : {},
|
|
234
|
+
body: responseBody
|
|
235
|
+
});
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (record.type === "connector.response.chunk") {
|
|
239
|
+
if (!responseStarted || typeof record.data !== "string") {
|
|
240
|
+
fail("provider response chunk frame is invalid");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const bytes = Buffer.from(record.data, "base64");
|
|
244
|
+
bytesRead += bytes.byteLength;
|
|
245
|
+
if (bytesRead > MAX_RESPONSE_BYTES) {
|
|
246
|
+
fail("provider response is too large");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
responseController?.enqueue(bytes);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (record.type === "connector.response.end") {
|
|
253
|
+
if (!responseStarted) {
|
|
254
|
+
fail("provider response ended before it started");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
terminal = true;
|
|
258
|
+
cleanup();
|
|
259
|
+
responseController?.close();
|
|
260
|
+
socket.end();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (record.type === "connector.response.error") {
|
|
264
|
+
const error = record.error && typeof record.error === "object" ? record.error : void 0;
|
|
265
|
+
fail(typeof error?.message === "string" ? error.message : "provider response stream failed");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (record.ok === false && record.error && typeof record.error === "object") {
|
|
269
|
+
const error = record.error;
|
|
270
|
+
fail(typeof error.message === "string" ? error.message : "provider request failed");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
fail("provider response frame is invalid");
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
socket.once("error", () => fail("provider channel is unavailable"));
|
|
277
|
+
socket.once("end", () => {
|
|
278
|
+
if (!terminal) fail("provider response ended early");
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function requireRecord(value, label) {
|
|
283
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
284
|
+
throw new Error(`${label} is invalid`);
|
|
285
|
+
}
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
export {
|
|
289
|
+
registerMoteProvider as default,
|
|
290
|
+
registerMoteProviderWithTransport
|
|
291
|
+
};
|