@pinixai/core 0.7.0 → 0.8.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/package.json +7 -3
- package/src/http.ts +59 -11
- package/src/hub.ts +1 -3
- package/src/index.ts +1 -1
- package/src/manifest.ts +2 -3
- package/src/web.ts +339 -0
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pinixai/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Clip framework for Pinix
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "Clip framework for Pinix \u2014 define once, run as CLI / MCP / Pinix bridge",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
7
7
|
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./web": "./src/web.ts"
|
|
11
|
+
},
|
|
8
12
|
"files": [
|
|
9
13
|
"src"
|
|
10
14
|
],
|
|
@@ -33,4 +37,4 @@
|
|
|
33
37
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
34
38
|
"zod": "^4.3.6"
|
|
35
39
|
}
|
|
36
|
-
}
|
|
40
|
+
}
|
package/src/http.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, join, resolve, sep } from "node:path";
|
|
2
2
|
import { getClipName, type Clip } from "./clip";
|
|
3
|
+
import type { Stream } from "./handler";
|
|
3
4
|
import { zodToManifestType } from "./manifest";
|
|
4
5
|
|
|
5
6
|
const CORS_HEADERS = {
|
|
@@ -135,6 +136,14 @@ function listCommands(clip: Clip): Response {
|
|
|
135
136
|
return jsonResponse({ commands });
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
function wantsSSE(request: Request): boolean {
|
|
140
|
+
return (request.headers.get("accept") ?? "").includes("text/event-stream");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function sseEvent(data: unknown): string {
|
|
144
|
+
return `data: ${JSON.stringify(data)}\n\n`;
|
|
145
|
+
}
|
|
146
|
+
|
|
138
147
|
async function handleCommandRequest(clip: Clip, commandName: string, request: Request): Promise<Response> {
|
|
139
148
|
const commandHandler = clip.getCommands().get(commandName);
|
|
140
149
|
|
|
@@ -158,20 +167,59 @@ async function handleCommandRequest(clip: Clip, commandName: string, request: Re
|
|
|
158
167
|
return errorResponse(toErrorMessage(error), 400);
|
|
159
168
|
}
|
|
160
169
|
|
|
161
|
-
|
|
170
|
+
if (!wantsSSE(request)) {
|
|
171
|
+
let output: unknown;
|
|
162
172
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
try {
|
|
174
|
+
output = await commandHandler.fn(parsedInput as never);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return errorResponse(toErrorMessage(error), 500);
|
|
177
|
+
}
|
|
168
178
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
try {
|
|
180
|
+
const parsedOutput = await commandHandler.output.parseAsync(output);
|
|
181
|
+
return jsonResponse(parsedOutput);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return errorResponse(toErrorMessage(error), 500);
|
|
184
|
+
}
|
|
174
185
|
}
|
|
186
|
+
|
|
187
|
+
// SSE streaming: pass a Stream to the handler, emit chunks as SSE events
|
|
188
|
+
const { readable, writable } = new TransformStream();
|
|
189
|
+
const writer = writable.getWriter();
|
|
190
|
+
const encoder = new TextEncoder();
|
|
191
|
+
|
|
192
|
+
const stream: Stream = {
|
|
193
|
+
chunk(data: unknown) {
|
|
194
|
+
const raw = typeof data === "string" ? data : JSON.stringify(data);
|
|
195
|
+
// Each JSONL line becomes a separate SSE event (matches pinixd behavior)
|
|
196
|
+
for (const line of raw.split("\n")) {
|
|
197
|
+
const trimmed = line.trim();
|
|
198
|
+
if (trimmed) {
|
|
199
|
+
writer.write(encoder.encode(`data: ${trimmed}\n\n`)).catch(() => {});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
commandHandler.fn(parsedInput as never, stream).then(
|
|
206
|
+
() => {
|
|
207
|
+
writer.write(encoder.encode("event: done\ndata: {}\n\n")).catch(() => {});
|
|
208
|
+
writer.close().catch(() => {});
|
|
209
|
+
},
|
|
210
|
+
(error) => {
|
|
211
|
+
writer.write(encoder.encode(sseEvent({ error: toErrorMessage(error) }))).catch(() => {});
|
|
212
|
+
writer.close().catch(() => {});
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
return new Response(readable, {
|
|
217
|
+
headers: createHeaders({
|
|
218
|
+
"Content-Type": "text/event-stream",
|
|
219
|
+
"Cache-Control": "no-cache",
|
|
220
|
+
"Connection": "keep-alive",
|
|
221
|
+
}),
|
|
222
|
+
});
|
|
175
223
|
}
|
|
176
224
|
|
|
177
225
|
async function handleStaticRequest(pathname: string): Promise<Response> {
|
package/src/hub.ts
CHANGED
|
@@ -2,11 +2,9 @@ import { createClient } from "@connectrpc/connect";
|
|
|
2
2
|
import { createConnectTransport } from "@connectrpc/connect-web";
|
|
3
3
|
import { HubService, type ClipInfo } from "./gen/hub_pb";
|
|
4
4
|
|
|
5
|
-
const PINIX_URL = process.env.PINIX_URL ?? "http://127.0.0.1:9000";
|
|
6
|
-
|
|
7
5
|
function getTransport(hubUrl?: string) {
|
|
8
6
|
return createConnectTransport({
|
|
9
|
-
baseUrl: hubUrl ?? PINIX_URL,
|
|
7
|
+
baseUrl: hubUrl ?? process.env.PINIX_URL ?? "http://127.0.0.1:9000",
|
|
10
8
|
});
|
|
11
9
|
}
|
|
12
10
|
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { Binding, Bindings } from "./bindings";
|
|
|
3
3
|
export { command } from "./command";
|
|
4
4
|
export { handler, type HandlerDef, type Stream } from "./handler";
|
|
5
5
|
export { serveHTTP } from "./http";
|
|
6
|
-
export { hubListClips } from "./hub";
|
|
6
|
+
export { hubInvoke, hubListClips } from "./hub";
|
|
7
7
|
export { serveIPC, invoke, redirectConsoleToStderr } from "./ipc";
|
|
8
8
|
export type { IPCCommandInfo, IPCManifest } from "./manifest";
|
|
9
9
|
export { serveMCP } from "./mcp";
|
package/src/manifest.ts
CHANGED
|
@@ -176,12 +176,11 @@ function findJsonFile(filename: string): Record<string, unknown> | null {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
function resolvePackageInfo(): { package?: string; version?: string } {
|
|
179
|
-
const pinixJson = findJsonFile("pinix.json");
|
|
180
179
|
const packageJson = findJsonFile("package.json");
|
|
181
180
|
|
|
182
181
|
return {
|
|
183
|
-
package: asString(
|
|
184
|
-
version: asString(
|
|
182
|
+
package: asString(packageJson?.name),
|
|
183
|
+
version: asString(packageJson?.version),
|
|
185
184
|
};
|
|
186
185
|
}
|
|
187
186
|
|
package/src/web.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pinixai/core/web — Browser client for Clip Web UIs.
|
|
3
|
+
*
|
|
4
|
+
* Clip developers use invoke() and invokeStream() without caring
|
|
5
|
+
* about the underlying transport:
|
|
6
|
+
*
|
|
7
|
+
* import { invoke, invokeStream } from "@pinixai/core/web"
|
|
8
|
+
*
|
|
9
|
+
* const config = await invoke<Config>("config", { args: ["get"] })
|
|
10
|
+
*
|
|
11
|
+
* const cancel = invokeStream("send", { args: ["-p", "hello"] },
|
|
12
|
+
* (event) => console.log(event),
|
|
13
|
+
* (exitCode) => console.log("done", exitCode),
|
|
14
|
+
* )
|
|
15
|
+
*
|
|
16
|
+
* Transport is auto-detected:
|
|
17
|
+
* - Standalone (--web): HTTP POST /api/<command> + SSE streaming
|
|
18
|
+
* - Hub (pinixd / Cloud Hub): Connect-RPC Invoke RPC
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createClient } from "@connectrpc/connect";
|
|
22
|
+
import { createConnectTransport } from "@connectrpc/connect-web";
|
|
23
|
+
import { HubService } from "./gen/hub_pb";
|
|
24
|
+
|
|
25
|
+
// ── Public types ──
|
|
26
|
+
|
|
27
|
+
export interface InvokeOptions {
|
|
28
|
+
args?: string[];
|
|
29
|
+
stdin?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type StreamEvent =
|
|
33
|
+
| { type: "info"; message: string }
|
|
34
|
+
| { type: "text"; content: string }
|
|
35
|
+
| { type: "thinking"; content: string }
|
|
36
|
+
| { type: "tool_call"; name: string; arguments: string }
|
|
37
|
+
| { type: "tool_result"; content: string }
|
|
38
|
+
| { type: "inject"; content: string }
|
|
39
|
+
| { type: "result"; data: unknown }
|
|
40
|
+
| { type: "done" };
|
|
41
|
+
|
|
42
|
+
// ── Environment detection ──
|
|
43
|
+
|
|
44
|
+
interface StandaloneEnv {
|
|
45
|
+
mode: "standalone";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface HubEnv {
|
|
49
|
+
mode: "hub";
|
|
50
|
+
clipName: string;
|
|
51
|
+
hubUrl: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Env = StandaloneEnv | HubEnv;
|
|
55
|
+
|
|
56
|
+
const STREAM_EVENT_TYPES = new Set([
|
|
57
|
+
"info", "text", "thinking", "tool_call", "tool_result", "inject", "result", "done",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
function isStreamEvent(value: unknown): value is StreamEvent {
|
|
61
|
+
if (!value || typeof value !== "object") return false;
|
|
62
|
+
const type = (value as { type?: unknown }).type;
|
|
63
|
+
return typeof type === "string" && STREAM_EVENT_TYPES.has(type);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Detect whether we're running standalone (--web) or inside a Hub (pinixd / Cloud Hub).
|
|
68
|
+
*
|
|
69
|
+
* Hub mode: page URL matches /clips/<clipName>/...
|
|
70
|
+
* Standalone: everything else (typically root /)
|
|
71
|
+
*/
|
|
72
|
+
function detectEnv(): Env {
|
|
73
|
+
const pathname = globalThis.location?.pathname ?? "/";
|
|
74
|
+
const match = pathname.match(/^\/clips\/([^/]+)\//);
|
|
75
|
+
if (match) {
|
|
76
|
+
return {
|
|
77
|
+
mode: "hub",
|
|
78
|
+
clipName: match[1],
|
|
79
|
+
hubUrl: globalThis.location.origin,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return { mode: "standalone" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Standalone transport (HTTP + SSE) ──
|
|
86
|
+
|
|
87
|
+
async function httpInvoke(command: string, opts: InvokeOptions): Promise<unknown> {
|
|
88
|
+
const response = await fetch(`api/${command}`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { "Content-Type": "application/json" },
|
|
91
|
+
body: JSON.stringify(opts),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const text = await response.text();
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const parsed = text ? tryParse(text) : null;
|
|
98
|
+
const msg = extractError(parsed, `Command "${command}" failed (${response.status})`);
|
|
99
|
+
throw new Error(msg);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return text ? JSON.parse(text) : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function httpInvokeStream(
|
|
106
|
+
command: string,
|
|
107
|
+
opts: InvokeOptions,
|
|
108
|
+
onEvent: (event: StreamEvent) => void,
|
|
109
|
+
onDone: (exitCode: number) => void,
|
|
110
|
+
): () => void {
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
let cancelled = false;
|
|
113
|
+
|
|
114
|
+
(async () => {
|
|
115
|
+
let response: Response;
|
|
116
|
+
try {
|
|
117
|
+
response = await fetch(`api/${command}`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: {
|
|
120
|
+
"Content-Type": "application/json",
|
|
121
|
+
"Accept": "text/event-stream",
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify(opts),
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (!cancelled) onDone(-1);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!response.ok || !response.body) {
|
|
132
|
+
if (!cancelled) onDone(1);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const reader = response.body.getReader();
|
|
137
|
+
const decoder = new TextDecoder();
|
|
138
|
+
let buffer = "";
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
while (true) {
|
|
142
|
+
const { value, done } = await reader.read();
|
|
143
|
+
if (done) break;
|
|
144
|
+
buffer += decoder.decode(value, { stream: true });
|
|
145
|
+
|
|
146
|
+
const parts = buffer.split("\n\n");
|
|
147
|
+
buffer = parts.pop() ?? "";
|
|
148
|
+
|
|
149
|
+
for (const part of parts) {
|
|
150
|
+
if (!part.trim()) continue;
|
|
151
|
+
|
|
152
|
+
if (part.includes("event: done")) {
|
|
153
|
+
onEvent({ type: "done" });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const dataMatch = part.match(/^data:\s*(.+)$/m);
|
|
158
|
+
if (!dataMatch) continue;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const chunk = JSON.parse(dataMatch[1]);
|
|
162
|
+
if (isStreamEvent(chunk)) {
|
|
163
|
+
onEvent(chunk);
|
|
164
|
+
} else if (chunk?.error) {
|
|
165
|
+
onEvent({ type: "info", message: `Error: ${chunk.error}` });
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
// skip unparseable
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!cancelled) onDone(0);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (!cancelled) onDone(-1);
|
|
176
|
+
}
|
|
177
|
+
})();
|
|
178
|
+
|
|
179
|
+
return () => {
|
|
180
|
+
cancelled = true;
|
|
181
|
+
controller.abort();
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Hub transport (Connect-RPC) ──
|
|
186
|
+
|
|
187
|
+
function createHubClient(hubUrl: string) {
|
|
188
|
+
const transport = createConnectTransport({ baseUrl: hubUrl });
|
|
189
|
+
return createClient(HubService, transport);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const encoder = new TextEncoder();
|
|
193
|
+
const decoder = new TextDecoder();
|
|
194
|
+
|
|
195
|
+
async function rpcInvoke(env: HubEnv, command: string, opts: InvokeOptions): Promise<unknown> {
|
|
196
|
+
const client = createHubClient(env.hubUrl);
|
|
197
|
+
const input = encoder.encode(JSON.stringify(opts));
|
|
198
|
+
|
|
199
|
+
let outputBytes = new Uint8Array(0);
|
|
200
|
+
|
|
201
|
+
for await (const response of client.invoke({
|
|
202
|
+
clipName: env.clipName,
|
|
203
|
+
command,
|
|
204
|
+
input,
|
|
205
|
+
clipToken: "",
|
|
206
|
+
})) {
|
|
207
|
+
if (response.error) {
|
|
208
|
+
throw new Error(response.error.message || response.error.code || "Hub invoke error");
|
|
209
|
+
}
|
|
210
|
+
if (response.output.length > 0) {
|
|
211
|
+
const merged = new Uint8Array(outputBytes.length + response.output.length);
|
|
212
|
+
merged.set(outputBytes);
|
|
213
|
+
merged.set(response.output, outputBytes.length);
|
|
214
|
+
outputBytes = merged;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (outputBytes.length === 0) return undefined;
|
|
219
|
+
return JSON.parse(decoder.decode(outputBytes));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function rpcInvokeStream(
|
|
223
|
+
env: HubEnv,
|
|
224
|
+
command: string,
|
|
225
|
+
opts: InvokeOptions,
|
|
226
|
+
onEvent: (event: StreamEvent) => void,
|
|
227
|
+
onDone: (exitCode: number) => void,
|
|
228
|
+
): () => void {
|
|
229
|
+
const controller = new AbortController();
|
|
230
|
+
let cancelled = false;
|
|
231
|
+
|
|
232
|
+
(async () => {
|
|
233
|
+
const client = createHubClient(env.hubUrl);
|
|
234
|
+
const input = encoder.encode(JSON.stringify(opts));
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
for await (const response of client.invoke(
|
|
238
|
+
{ clipName: env.clipName, command, input, clipToken: "" },
|
|
239
|
+
{ signal: controller.signal },
|
|
240
|
+
)) {
|
|
241
|
+
if (cancelled) return;
|
|
242
|
+
|
|
243
|
+
if (response.error) {
|
|
244
|
+
onEvent({ type: "info", message: `Error: ${response.error.message}` });
|
|
245
|
+
if (!cancelled) onDone(1);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (response.output.length > 0) {
|
|
250
|
+
const text = decoder.decode(response.output);
|
|
251
|
+
// Each output chunk may contain JSONL lines
|
|
252
|
+
for (const line of text.split("\n")) {
|
|
253
|
+
const trimmed = line.trim();
|
|
254
|
+
if (!trimmed) continue;
|
|
255
|
+
try {
|
|
256
|
+
const event = JSON.parse(trimmed);
|
|
257
|
+
if (isStreamEvent(event)) {
|
|
258
|
+
onEvent(event);
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
// skip non-JSON lines
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!cancelled) onDone(0);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (!cancelled) onDone(-1);
|
|
270
|
+
}
|
|
271
|
+
})();
|
|
272
|
+
|
|
273
|
+
return () => {
|
|
274
|
+
cancelled = true;
|
|
275
|
+
controller.abort();
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Helpers ──
|
|
280
|
+
|
|
281
|
+
function tryParse(text: string): unknown {
|
|
282
|
+
try {
|
|
283
|
+
return JSON.parse(text);
|
|
284
|
+
} catch {
|
|
285
|
+
return text;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function extractError(payload: unknown, fallback: string): string {
|
|
290
|
+
if (typeof payload === "string" && payload.trim()) return payload;
|
|
291
|
+
if (payload && typeof payload === "object") {
|
|
292
|
+
const msg = (payload as { message?: string }).message;
|
|
293
|
+
if (typeof msg === "string" && msg.trim()) return msg;
|
|
294
|
+
const err = (payload as { error?: unknown }).error;
|
|
295
|
+
if (typeof err === "string" && err.trim()) return err;
|
|
296
|
+
if (err && typeof err === "object") {
|
|
297
|
+
const nestedMsg = (err as { message?: string }).message;
|
|
298
|
+
if (typeof nestedMsg === "string" && nestedMsg.trim()) return nestedMsg;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return fallback;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Public API ──
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Invoke a clip command and return the parsed result.
|
|
308
|
+
*
|
|
309
|
+
* Automatically uses HTTP (standalone --web) or Connect-RPC (Hub).
|
|
310
|
+
*/
|
|
311
|
+
export async function invoke<T = unknown>(
|
|
312
|
+
command: string,
|
|
313
|
+
opts: InvokeOptions = {},
|
|
314
|
+
): Promise<T> {
|
|
315
|
+
const env = detectEnv();
|
|
316
|
+
if (env.mode === "standalone") {
|
|
317
|
+
return httpInvoke(command, opts) as Promise<T>;
|
|
318
|
+
}
|
|
319
|
+
return rpcInvoke(env, command, opts) as Promise<T>;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Invoke a clip command with streaming output.
|
|
324
|
+
* Returns a cancel function.
|
|
325
|
+
*
|
|
326
|
+
* Automatically uses SSE (standalone --web) or Connect-RPC streaming (Hub).
|
|
327
|
+
*/
|
|
328
|
+
export function invokeStream(
|
|
329
|
+
command: string,
|
|
330
|
+
opts: InvokeOptions,
|
|
331
|
+
onEvent: (event: StreamEvent) => void,
|
|
332
|
+
onDone: (exitCode: number) => void,
|
|
333
|
+
): () => void {
|
|
334
|
+
const env = detectEnv();
|
|
335
|
+
if (env.mode === "standalone") {
|
|
336
|
+
return httpInvokeStream(command, opts, onEvent, onDone);
|
|
337
|
+
}
|
|
338
|
+
return rpcInvokeStream(env, command, opts, onEvent, onDone);
|
|
339
|
+
}
|