@superhq/shuru 0.1.2 → 0.3.1
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/README.md +101 -3
- package/package.json +1 -1
- package/src/index.ts +11 -1
- package/src/process-handle.ts +54 -0
- package/src/process.ts +62 -6
- package/src/sandbox.ts +61 -5
- package/src/types.ts +36 -0
package/README.md
CHANGED
|
@@ -15,14 +15,27 @@ import { Sandbox } from "@superhq/shuru";
|
|
|
15
15
|
|
|
16
16
|
const sb = await Sandbox.start();
|
|
17
17
|
|
|
18
|
+
// Buffered exec — run a command and get the full result
|
|
18
19
|
const result = await sb.exec("echo hello");
|
|
19
20
|
console.log(result.stdout); // "hello\n"
|
|
20
21
|
|
|
22
|
+
// Streaming spawn — real-time stdout/stderr
|
|
23
|
+
const proc = await sb.spawn("npm run dev");
|
|
24
|
+
proc.on("stdout", (data) => process.stdout.write(data));
|
|
25
|
+
proc.on("stderr", (data) => process.stderr.write(data));
|
|
26
|
+
proc.on("exit", (code) => console.log("exited:", code));
|
|
27
|
+
|
|
28
|
+
// File watching — guest-side inotify events
|
|
29
|
+
await sb.watch("/workspace", (event) => {
|
|
30
|
+
console.log(event.event, event.path); // "modify" "/workspace/src/main.ts"
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// File I/O
|
|
21
34
|
await sb.writeFile("/tmp/app.ts", "console.log('hi')");
|
|
22
35
|
const data = await sb.readFile("/tmp/app.ts"); // Uint8Array
|
|
23
|
-
const text = new TextDecoder().decode(data);
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
// Checkpoint — save disk state and stop
|
|
38
|
+
await sb.checkpoint("my-env");
|
|
26
39
|
```
|
|
27
40
|
|
|
28
41
|
### Start from a checkpoint
|
|
@@ -42,6 +55,10 @@ const sb = await Sandbox.start({
|
|
|
42
55
|
allowNet: true,
|
|
43
56
|
ports: ["8080:80"],
|
|
44
57
|
mounts: { "./src": "/workspace" },
|
|
58
|
+
secrets: {
|
|
59
|
+
API_KEY: { from: "OPENAI_API_KEY", hosts: ["api.openai.com"] },
|
|
60
|
+
},
|
|
61
|
+
network: { allow: ["api.openai.com", "registry.npmjs.org"] },
|
|
45
62
|
});
|
|
46
63
|
```
|
|
47
64
|
|
|
@@ -54,6 +71,8 @@ const sb = await Sandbox.start({
|
|
|
54
71
|
| `allowNet` | `boolean` | Enable network access |
|
|
55
72
|
| `ports` | `string[]` | Port forwards (`"host:guest"`) |
|
|
56
73
|
| `mounts` | `Record<string, string>` | Directory mounts (`{ hostPath: guestPath }`) |
|
|
74
|
+
| `secrets` | `Record<string, SecretConfig>` | Secrets to inject via proxy |
|
|
75
|
+
| `network` | `NetworkConfig` | Network access policy |
|
|
57
76
|
| `shuruBin` | `string` | Path to shuru binary (default: `"shuru"`) |
|
|
58
77
|
|
|
59
78
|
## API
|
|
@@ -64,7 +83,47 @@ Boot a new microVM. Returns when the VM is ready.
|
|
|
64
83
|
|
|
65
84
|
### `sandbox.exec(command): Promise<ExecResult>`
|
|
66
85
|
|
|
67
|
-
Run a shell command in the VM. Returns `{ stdout, stderr, exitCode }`.
|
|
86
|
+
Run a shell command in the VM. Returns `{ stdout, stderr, exitCode }`. Stdout and stderr are buffered — the promise resolves when the command finishes.
|
|
87
|
+
|
|
88
|
+
### `sandbox.spawn(command, opts?): Promise<SandboxProcess>`
|
|
89
|
+
|
|
90
|
+
Spawn a long-running command in the VM. Returns a `SandboxProcess` handle immediately, streaming output in real-time.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const proc = await sb.spawn("npm run dev", { cwd: "/workspace" });
|
|
94
|
+
|
|
95
|
+
proc.on("stdout", (data: Buffer) => { /* real-time chunks */ });
|
|
96
|
+
proc.on("stderr", (data: Buffer) => { /* real-time chunks */ });
|
|
97
|
+
proc.on("exit", (code: number) => { /* process exited */ });
|
|
98
|
+
|
|
99
|
+
proc.write("input to stdin\n"); // write to stdin
|
|
100
|
+
await proc.kill(); // send SIGTERM
|
|
101
|
+
const exitCode = await proc.exited; // await completion
|
|
102
|
+
console.log(proc.pid); // process ID
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**`SpawnOptions`:**
|
|
106
|
+
| Option | Type | Description |
|
|
107
|
+
|--------|------|-------------|
|
|
108
|
+
| `cwd` | `string` | Working directory for the command |
|
|
109
|
+
| `env` | `Record<string, string>` | Environment variables |
|
|
110
|
+
|
|
111
|
+
### `sandbox.watch(path, handler, opts?): Promise<void>`
|
|
112
|
+
|
|
113
|
+
Watch a directory for file changes inside the guest VM. Uses guest-side inotify, so it detects writes to tmpfs overlays that host-side watchers cannot see.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
await sb.watch("/workspace", (event) => {
|
|
117
|
+
console.log(event.event, event.path);
|
|
118
|
+
// event.event: "create" | "modify" | "delete" | "rename"
|
|
119
|
+
// event.path: full path of the changed file
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**`WatchOptions`:**
|
|
124
|
+
| Option | Type | Default | Description |
|
|
125
|
+
|--------|------|---------|-------------|
|
|
126
|
+
| `recursive` | `boolean` | `true` | Watch subdirectories recursively |
|
|
68
127
|
|
|
69
128
|
### `sandbox.readFile(path): Promise<Uint8Array>`
|
|
70
129
|
|
|
@@ -82,6 +141,45 @@ Save the VM's disk state and stop the VM. To continue working, call `Sandbox.sta
|
|
|
82
141
|
|
|
83
142
|
Stop the VM without saving. All changes are discarded.
|
|
84
143
|
|
|
144
|
+
### Secrets
|
|
145
|
+
|
|
146
|
+
Secrets keep API keys on the host. The guest receives a random placeholder token; the proxy substitutes the real value only on HTTPS requests to the specified hosts.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const sb = await Sandbox.start({
|
|
150
|
+
allowNet: true,
|
|
151
|
+
secrets: {
|
|
152
|
+
API_KEY: { from: "OPENAI_API_KEY", hosts: ["api.openai.com"] },
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
// Inside the VM, $API_KEY is a placeholder token.
|
|
156
|
+
// Requests to api.openai.com get the real key injected by the proxy.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Network policy
|
|
160
|
+
|
|
161
|
+
Restrict which domains the guest can reach:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
const sb = await Sandbox.start({
|
|
165
|
+
allowNet: true,
|
|
166
|
+
network: { allow: ["api.openai.com", "*.npmjs.org"] },
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Omit `network.allow` to allow all domains.
|
|
171
|
+
|
|
172
|
+
## Concurrency
|
|
173
|
+
|
|
174
|
+
Multiple `spawn()` calls run concurrently in the same VM. Each gets a unique pid and independent stdout/stderr streams. You can mix `spawn()`, `exec()`, and `watch()` freely:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// Start a dev server, run tests, and watch for changes — all at once
|
|
178
|
+
const server = await sb.spawn("npm run dev", { cwd: "/workspace" });
|
|
179
|
+
const watcher = sb.watch("/workspace", (e) => console.log(e));
|
|
180
|
+
const tests = await sb.exec("npm test");
|
|
181
|
+
```
|
|
182
|
+
|
|
85
183
|
## Requirements
|
|
86
184
|
|
|
87
185
|
- macOS 14+ on Apple Silicon
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
+
export { SandboxProcess } from "./process-handle";
|
|
1
2
|
export { Sandbox } from "./sandbox";
|
|
2
|
-
export type {
|
|
3
|
+
export type {
|
|
4
|
+
ExecOptions,
|
|
5
|
+
ExecResult,
|
|
6
|
+
FileChangeEvent,
|
|
7
|
+
NetworkConfig,
|
|
8
|
+
SecretConfig,
|
|
9
|
+
SpawnOptions,
|
|
10
|
+
StartOptions,
|
|
11
|
+
WatchOptions,
|
|
12
|
+
} from "./types";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ShuruProcess } from "./process";
|
|
2
|
+
|
|
3
|
+
type EventMap = {
|
|
4
|
+
stdout: (data: Buffer) => void;
|
|
5
|
+
stderr: (data: Buffer) => void;
|
|
6
|
+
exit: (code: number) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export class SandboxProcess {
|
|
10
|
+
readonly pid: string;
|
|
11
|
+
readonly exited: Promise<number>;
|
|
12
|
+
private proc: ShuruProcess;
|
|
13
|
+
private listeners: {
|
|
14
|
+
stdout: ((data: Buffer) => void)[];
|
|
15
|
+
stderr: ((data: Buffer) => void)[];
|
|
16
|
+
exit: ((code: number) => void)[];
|
|
17
|
+
} = { stdout: [], stderr: [], exit: [] };
|
|
18
|
+
|
|
19
|
+
constructor(proc: ShuruProcess, pid: string) {
|
|
20
|
+
this.proc = proc;
|
|
21
|
+
this.pid = pid;
|
|
22
|
+
|
|
23
|
+
this.exited = new Promise<number>((resolve) => {
|
|
24
|
+
proc.processHandlers.set(pid, {
|
|
25
|
+
onStdout: (data) => {
|
|
26
|
+
for (const h of this.listeners.stdout) h(data);
|
|
27
|
+
},
|
|
28
|
+
onStderr: (data) => {
|
|
29
|
+
for (const h of this.listeners.stderr) h(data);
|
|
30
|
+
},
|
|
31
|
+
onExit: (code) => {
|
|
32
|
+
for (const h of this.listeners.exit) h(code);
|
|
33
|
+
resolve(code);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
on<K extends keyof EventMap>(event: K, handler: EventMap[K]): this {
|
|
40
|
+
this.listeners[event].push(handler);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
write(data: Buffer | string): void {
|
|
45
|
+
this.proc.sendNotification("input", {
|
|
46
|
+
pid: this.pid,
|
|
47
|
+
data: Buffer.from(data).toString("base64"),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async kill(): Promise<void> {
|
|
52
|
+
await this.proc.send("kill", { pid: this.pid });
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/process.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { Subprocess } from "bun";
|
|
2
|
-
import type { JsonRpcResponse, JsonRpcResult } from "./types";
|
|
2
|
+
import type { FileChangeEvent, JsonRpcResponse, JsonRpcResult } from "./types";
|
|
3
3
|
|
|
4
4
|
interface PendingRequest {
|
|
5
5
|
resolve: (value: JsonRpcResult) => void;
|
|
6
6
|
reject: (reason: Error) => void;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface ProcessEventHandlers {
|
|
10
|
+
onStdout: (data: Buffer) => void;
|
|
11
|
+
onStderr: (data: Buffer) => void;
|
|
12
|
+
onExit: (code: number) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
export class ShuruProcess {
|
|
10
16
|
private proc: Subprocess<"pipe", "pipe", "inherit"> | null = null;
|
|
11
17
|
private pending = new Map<number, PendingRequest>();
|
|
@@ -13,6 +19,12 @@ export class ShuruProcess {
|
|
|
13
19
|
private onReady: (() => void) | null = null;
|
|
14
20
|
private onReadyError: ((err: Error) => void) | null = null;
|
|
15
21
|
|
|
22
|
+
/** Handlers for spawned process output/exit, keyed by pid. */
|
|
23
|
+
readonly processHandlers = new Map<string, ProcessEventHandlers>();
|
|
24
|
+
|
|
25
|
+
/** Handler for file change events from the guest watcher. */
|
|
26
|
+
fileChangeHandler: ((event: FileChangeEvent) => void) | null = null;
|
|
27
|
+
|
|
16
28
|
async start(args: string[]): Promise<void> {
|
|
17
29
|
this.proc = Bun.spawn(args, {
|
|
18
30
|
stdin: "pipe",
|
|
@@ -55,6 +67,14 @@ export class ShuruProcess {
|
|
|
55
67
|
});
|
|
56
68
|
}
|
|
57
69
|
|
|
70
|
+
/** Send a fire-and-forget notification (no id, no response expected). */
|
|
71
|
+
sendNotification(method: string, params: Record<string, unknown>): void {
|
|
72
|
+
if (!this.proc) throw new Error("shuru process not started");
|
|
73
|
+
const line = `${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`;
|
|
74
|
+
this.proc.stdin.write(line);
|
|
75
|
+
this.proc.stdin.flush();
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
async stop(): Promise<void> {
|
|
59
79
|
if (!this.proc) return;
|
|
60
80
|
|
|
@@ -132,11 +152,47 @@ export class ShuruProcess {
|
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
private dispatch(msg: JsonRpcResponse): void {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
// Handle notifications (no id)
|
|
156
|
+
if ("method" in msg && !("id" in msg)) {
|
|
157
|
+
const params = msg.params as Record<string, unknown> | undefined;
|
|
158
|
+
|
|
159
|
+
switch (msg.method) {
|
|
160
|
+
case "ready": {
|
|
161
|
+
if (this.onReady) {
|
|
162
|
+
this.onReady();
|
|
163
|
+
this.onReady = null;
|
|
164
|
+
this.onReadyError = null;
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
case "output": {
|
|
169
|
+
if (!params) return;
|
|
170
|
+
const pid = params.pid as string;
|
|
171
|
+
const h = this.processHandlers.get(pid);
|
|
172
|
+
if (!h) return;
|
|
173
|
+
const buf = Buffer.from(params.data as string, "base64");
|
|
174
|
+
if (params.stream === "stdout") h.onStdout(buf);
|
|
175
|
+
else if (params.stream === "stderr") h.onStderr(buf);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
case "exit": {
|
|
179
|
+
if (!params) return;
|
|
180
|
+
const pid = params.pid as string;
|
|
181
|
+
const h = this.processHandlers.get(pid);
|
|
182
|
+
if (h) {
|
|
183
|
+
h.onExit(params.code as number);
|
|
184
|
+
this.processHandlers.delete(pid);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
case "file_change": {
|
|
189
|
+
if (!params) return;
|
|
190
|
+
this.fileChangeHandler?.({
|
|
191
|
+
path: params.path as string,
|
|
192
|
+
event: params.event as FileChangeEvent["event"],
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
140
196
|
}
|
|
141
197
|
return;
|
|
142
198
|
}
|
package/src/sandbox.ts
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { ShuruProcess } from "./process";
|
|
2
|
-
import
|
|
2
|
+
import { SandboxProcess } from "./process-handle";
|
|
3
|
+
import type {
|
|
4
|
+
ExecOptions,
|
|
5
|
+
ExecResult,
|
|
6
|
+
FileChangeEvent,
|
|
7
|
+
SpawnOptions,
|
|
8
|
+
StartOptions,
|
|
9
|
+
WatchOptions,
|
|
10
|
+
} from "./types";
|
|
3
11
|
|
|
4
12
|
const Method = {
|
|
5
13
|
EXEC: "exec",
|
|
14
|
+
SPAWN: "spawn",
|
|
6
15
|
READ_FILE: "read_file",
|
|
7
16
|
WRITE_FILE: "write_file",
|
|
8
17
|
CHECKPOINT: "checkpoint",
|
|
18
|
+
WATCH: "watch",
|
|
9
19
|
} as const;
|
|
10
20
|
|
|
11
21
|
export class Sandbox {
|
|
@@ -26,10 +36,15 @@ export class Sandbox {
|
|
|
26
36
|
return new Sandbox(proc);
|
|
27
37
|
}
|
|
28
38
|
|
|
29
|
-
async exec(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
async exec(
|
|
40
|
+
command: string | string[],
|
|
41
|
+
opts?: ExecOptions,
|
|
42
|
+
): Promise<ExecResult> {
|
|
43
|
+
const argv =
|
|
44
|
+
typeof command === "string"
|
|
45
|
+
? [opts?.shell ?? "sh", "-c", command]
|
|
46
|
+
: command;
|
|
47
|
+
const resp = await this.proc.send(Method.EXEC, { argv });
|
|
33
48
|
const r = resp.result as {
|
|
34
49
|
stdout: string;
|
|
35
50
|
stderr: string;
|
|
@@ -42,6 +57,35 @@ export class Sandbox {
|
|
|
42
57
|
};
|
|
43
58
|
}
|
|
44
59
|
|
|
60
|
+
async spawn(
|
|
61
|
+
command: string | string[],
|
|
62
|
+
opts?: SpawnOptions,
|
|
63
|
+
): Promise<SandboxProcess> {
|
|
64
|
+
const argv =
|
|
65
|
+
typeof command === "string"
|
|
66
|
+
? [opts?.shell ?? "sh", "-c", command]
|
|
67
|
+
: command;
|
|
68
|
+
const resp = await this.proc.send(Method.SPAWN, {
|
|
69
|
+
argv,
|
|
70
|
+
cwd: opts?.cwd,
|
|
71
|
+
env: opts?.env,
|
|
72
|
+
});
|
|
73
|
+
const { pid } = resp.result as { pid: string };
|
|
74
|
+
return new SandboxProcess(this.proc, pid);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async watch(
|
|
78
|
+
path: string,
|
|
79
|
+
handler: (event: FileChangeEvent) => void,
|
|
80
|
+
opts?: WatchOptions,
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
this.proc.fileChangeHandler = handler;
|
|
83
|
+
await this.proc.send(Method.WATCH, {
|
|
84
|
+
path,
|
|
85
|
+
recursive: opts?.recursive ?? true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
45
89
|
async readFile(path: string): Promise<Uint8Array> {
|
|
46
90
|
const resp = await this.proc.send(Method.READ_FILE, { path });
|
|
47
91
|
const r = resp.result as { content: string };
|
|
@@ -76,6 +120,18 @@ export function buildArgs(bin: string, opts: StartOptions): string[] {
|
|
|
76
120
|
if (opts.diskSize) args.push("--disk-size", String(opts.diskSize));
|
|
77
121
|
if (opts.allowNet) args.push("--allow-net");
|
|
78
122
|
|
|
123
|
+
if (opts.secrets) {
|
|
124
|
+
for (const [name, secret] of Object.entries(opts.secrets)) {
|
|
125
|
+
args.push("--secret", `${name}=${secret.from}@${secret.hosts.join(",")}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (opts.network?.allow) {
|
|
130
|
+
for (const host of opts.network.allow) {
|
|
131
|
+
args.push("--allow-host", host);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
79
135
|
if (opts.ports) {
|
|
80
136
|
for (const p of opts.ports) {
|
|
81
137
|
args.push("-p", p);
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
export interface SecretConfig {
|
|
2
|
+
/** Host environment variable containing the real value. */
|
|
3
|
+
from: string;
|
|
4
|
+
/** Domains where this secret may be sent (e.g. "api.openai.com"). */
|
|
5
|
+
hosts: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface NetworkConfig {
|
|
9
|
+
/** Allowed domain patterns. Omit to allow all. */
|
|
10
|
+
allow?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
export interface StartOptions {
|
|
2
14
|
from?: string;
|
|
3
15
|
cpus?: number;
|
|
@@ -6,6 +18,8 @@ export interface StartOptions {
|
|
|
6
18
|
allowNet?: boolean;
|
|
7
19
|
ports?: string[];
|
|
8
20
|
mounts?: Record<string, string>;
|
|
21
|
+
secrets?: Record<string, SecretConfig>;
|
|
22
|
+
network?: NetworkConfig;
|
|
9
23
|
shuruBin?: string;
|
|
10
24
|
}
|
|
11
25
|
|
|
@@ -15,6 +29,27 @@ export interface ExecResult {
|
|
|
15
29
|
exitCode: number;
|
|
16
30
|
}
|
|
17
31
|
|
|
32
|
+
export interface ExecOptions {
|
|
33
|
+
/** Shell to use when command is a string. Defaults to "sh". Ignored when command is an array. */
|
|
34
|
+
shell?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SpawnOptions {
|
|
38
|
+
cwd?: string;
|
|
39
|
+
env?: Record<string, string>;
|
|
40
|
+
/** Shell to use when command is a string. Defaults to "sh". Ignored when command is an array. */
|
|
41
|
+
shell?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface WatchOptions {
|
|
45
|
+
recursive?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FileChangeEvent {
|
|
49
|
+
path: string;
|
|
50
|
+
event: "create" | "modify" | "delete" | "rename";
|
|
51
|
+
}
|
|
52
|
+
|
|
18
53
|
// --- JSON-RPC 2.0 wire types (internal) ---
|
|
19
54
|
|
|
20
55
|
export interface JsonRpcResult {
|
|
@@ -32,6 +67,7 @@ export interface JsonRpcError {
|
|
|
32
67
|
export interface JsonRpcNotification {
|
|
33
68
|
jsonrpc: "2.0";
|
|
34
69
|
method: string;
|
|
70
|
+
params?: Record<string, unknown>;
|
|
35
71
|
}
|
|
36
72
|
|
|
37
73
|
export type JsonRpcResponse =
|