@sage-protocol/openclaw-sage 0.1.9 → 0.1.10
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 +6 -3
- package/dist/index.d.ts +79 -0
- package/dist/index.js +1031 -0
- package/dist/mcp-bridge.d.ts +42 -0
- package/dist/mcp-bridge.js +170 -0
- package/dist/runtime.d.ts +41 -0
- package/dist/runtime.js +317 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +17 -4
- package/.github/workflows/ci.yml +0 -30
- package/.github/workflows/release-please.yml +0 -19
- package/.release-please-manifest.json +0 -3
- package/CHANGELOG.md +0 -89
- package/SOUL.md +0 -172
- package/release-please-config.json +0 -13
- package/src/index.ts +0 -1363
- package/src/mcp-bridge.test.ts +0 -469
- package/src/mcp-bridge.ts +0 -230
- package/src/openclaw-hook.integration.test.ts +0 -258
- package/src/rlm-capture.e2e.test.ts +0 -279
- package/tsconfig.json +0 -18
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
/** MCP tool definition returned by tools/list */
|
|
3
|
+
export type McpToolDef = {
|
|
4
|
+
name: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
inputSchema?: Record<string, unknown>;
|
|
7
|
+
/** Tool category (from Sage MCP registry) */
|
|
8
|
+
annotations?: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Lightweight MCP stdio client.
|
|
12
|
+
*
|
|
13
|
+
* Spawns a child process that speaks JSON-RPC over stdin/stdout (MCP stdio transport).
|
|
14
|
+
* Provides methods to list tools and call them.
|
|
15
|
+
*/
|
|
16
|
+
export declare class McpBridge extends EventEmitter {
|
|
17
|
+
private command;
|
|
18
|
+
private args;
|
|
19
|
+
private env?;
|
|
20
|
+
private proc;
|
|
21
|
+
private pending;
|
|
22
|
+
private ready;
|
|
23
|
+
private retries;
|
|
24
|
+
private stopped;
|
|
25
|
+
private clientVersion;
|
|
26
|
+
constructor(command: string, args: string[], env?: Record<string, string> | undefined, opts?: {
|
|
27
|
+
clientVersion?: string;
|
|
28
|
+
});
|
|
29
|
+
/** Whether the bridge is connected and ready for requests */
|
|
30
|
+
isReady(): boolean;
|
|
31
|
+
start(): Promise<void>;
|
|
32
|
+
stop(): Promise<void>;
|
|
33
|
+
listTools(): Promise<McpToolDef[]>;
|
|
34
|
+
callTool(name: string, args: Record<string, unknown>): Promise<unknown>;
|
|
35
|
+
private spawn;
|
|
36
|
+
private initialize;
|
|
37
|
+
private request;
|
|
38
|
+
private notify;
|
|
39
|
+
private handleLine;
|
|
40
|
+
private handleCrash;
|
|
41
|
+
private rejectAll;
|
|
42
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { spawnCommand } from "./runtime.js";
|
|
4
|
+
const MAX_RETRIES = 3;
|
|
5
|
+
const RESTART_DELAY_MS = 1000;
|
|
6
|
+
/**
|
|
7
|
+
* Lightweight MCP stdio client.
|
|
8
|
+
*
|
|
9
|
+
* Spawns a child process that speaks JSON-RPC over stdin/stdout (MCP stdio transport).
|
|
10
|
+
* Provides methods to list tools and call them.
|
|
11
|
+
*/
|
|
12
|
+
export class McpBridge extends EventEmitter {
|
|
13
|
+
command;
|
|
14
|
+
args;
|
|
15
|
+
env;
|
|
16
|
+
proc = null;
|
|
17
|
+
pending = new Map();
|
|
18
|
+
ready = false;
|
|
19
|
+
retries = 0;
|
|
20
|
+
stopped = false;
|
|
21
|
+
clientVersion;
|
|
22
|
+
constructor(command, args, env, opts) {
|
|
23
|
+
super();
|
|
24
|
+
this.command = command;
|
|
25
|
+
this.args = args;
|
|
26
|
+
this.env = env;
|
|
27
|
+
this.clientVersion = opts?.clientVersion ?? "0.0.0";
|
|
28
|
+
}
|
|
29
|
+
/** Whether the bridge is connected and ready for requests */
|
|
30
|
+
isReady() {
|
|
31
|
+
return this.ready && this.proc !== null && !this.stopped;
|
|
32
|
+
}
|
|
33
|
+
async start() {
|
|
34
|
+
this.stopped = false;
|
|
35
|
+
await this.spawn();
|
|
36
|
+
await this.initialize();
|
|
37
|
+
}
|
|
38
|
+
async stop() {
|
|
39
|
+
this.stopped = true;
|
|
40
|
+
this.ready = false;
|
|
41
|
+
this.rejectAll("Bridge stopped");
|
|
42
|
+
if (this.proc) {
|
|
43
|
+
this.proc.kill("SIGTERM");
|
|
44
|
+
this.proc = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async listTools() {
|
|
48
|
+
const result = (await this.request("tools/list", {}));
|
|
49
|
+
return result?.tools ?? [];
|
|
50
|
+
}
|
|
51
|
+
async callTool(name, args) {
|
|
52
|
+
const result = (await this.request("tools/call", { name, arguments: args }));
|
|
53
|
+
if (result?.isError) {
|
|
54
|
+
const text = result.content?.map((c) => c.text ?? "").join("\n") ?? "MCP tool error";
|
|
55
|
+
throw new Error(text);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
// ── private ──────────────────────────────────────────────────────────
|
|
60
|
+
spawn() {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
void spawnCommand(this.command, this.args, {
|
|
63
|
+
stdin: "pipe",
|
|
64
|
+
stdout: "pipe",
|
|
65
|
+
stderr: "pipe",
|
|
66
|
+
env: this.env,
|
|
67
|
+
})
|
|
68
|
+
.then((proc) => {
|
|
69
|
+
proc.onError((err) => {
|
|
70
|
+
if (!this.stopped) {
|
|
71
|
+
this.handleCrash(err);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
proc.onExit((code) => {
|
|
75
|
+
if (!this.stopped && code !== 0) {
|
|
76
|
+
this.handleCrash(new Error(`MCP process exited with code ${code}`));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
if (!proc.stdout || !proc.stdin) {
|
|
80
|
+
reject(new Error("Failed to open stdio pipes for MCP process"));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.proc = proc;
|
|
84
|
+
proc.stdout.onLine((line) => this.handleLine(line));
|
|
85
|
+
proc.stderr?.onLine((line) => this.emit("log", line));
|
|
86
|
+
resolve();
|
|
87
|
+
})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async initialize() {
|
|
94
|
+
const result = (await this.request("initialize", {
|
|
95
|
+
protocolVersion: "2024-11-05",
|
|
96
|
+
capabilities: {},
|
|
97
|
+
clientInfo: { name: "openclaw-sage-plugin", version: this.clientVersion },
|
|
98
|
+
}));
|
|
99
|
+
this.notify("notifications/initialized", {});
|
|
100
|
+
this.ready = true;
|
|
101
|
+
this.retries = 0;
|
|
102
|
+
this.emit("ready", result?.serverInfo);
|
|
103
|
+
}
|
|
104
|
+
request(method, params) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
if (!this.proc?.stdin?.isWritable()) {
|
|
107
|
+
reject(new Error("MCP process not running"));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const id = randomUUID();
|
|
111
|
+
const req = { jsonrpc: "2.0", id, method, params };
|
|
112
|
+
this.pending.set(id, { resolve, reject });
|
|
113
|
+
this.proc.stdin.write(JSON.stringify(req) + "\n");
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
notify(method, params) {
|
|
117
|
+
if (!this.proc?.stdin?.isWritable())
|
|
118
|
+
return;
|
|
119
|
+
const msg = { jsonrpc: "2.0", method, params };
|
|
120
|
+
this.proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
121
|
+
}
|
|
122
|
+
handleLine(line) {
|
|
123
|
+
let msg;
|
|
124
|
+
try {
|
|
125
|
+
msg = JSON.parse(line);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!msg.id)
|
|
131
|
+
return;
|
|
132
|
+
const id = String(msg.id);
|
|
133
|
+
const pending = this.pending.get(id);
|
|
134
|
+
if (!pending)
|
|
135
|
+
return;
|
|
136
|
+
this.pending.delete(id);
|
|
137
|
+
if (msg.error) {
|
|
138
|
+
pending.reject(new Error(`MCP error ${msg.error.code}: ${msg.error.message}`));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
pending.resolve(msg.result);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async handleCrash(err) {
|
|
145
|
+
this.ready = false;
|
|
146
|
+
this.rejectAll(`MCP process crashed: ${err.message}`);
|
|
147
|
+
if (this.retries >= MAX_RETRIES) {
|
|
148
|
+
this.emit("error", new Error(`MCP bridge failed after ${MAX_RETRIES} retries: ${err.message}`));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.retries++;
|
|
152
|
+
this.emit("log", `MCP process crashed, retry ${this.retries}/${MAX_RETRIES}...`);
|
|
153
|
+
await new Promise((r) => setTimeout(r, RESTART_DELAY_MS));
|
|
154
|
+
if (!this.stopped) {
|
|
155
|
+
try {
|
|
156
|
+
await this.spawn();
|
|
157
|
+
await this.initialize();
|
|
158
|
+
}
|
|
159
|
+
catch (retryErr) {
|
|
160
|
+
this.handleCrash(retryErr instanceof Error ? retryErr : new Error(String(retryErr)));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
rejectAll(reason) {
|
|
165
|
+
for (const [, { reject }] of this.pending) {
|
|
166
|
+
reject(new Error(reason));
|
|
167
|
+
}
|
|
168
|
+
this.pending.clear();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type LineHandler = (line: string) => void;
|
|
2
|
+
export type SpawnedInput = {
|
|
3
|
+
isWritable: () => boolean;
|
|
4
|
+
write: (chunk: string) => void;
|
|
5
|
+
end: () => void;
|
|
6
|
+
};
|
|
7
|
+
export type SpawnedOutput = {
|
|
8
|
+
onLine: (handler: LineHandler) => void;
|
|
9
|
+
};
|
|
10
|
+
export type SpawnedProcess = {
|
|
11
|
+
stdin: SpawnedInput | null;
|
|
12
|
+
stdout: SpawnedOutput | null;
|
|
13
|
+
stderr: SpawnedOutput | null;
|
|
14
|
+
kill: (signal?: string) => void;
|
|
15
|
+
unref?: () => void;
|
|
16
|
+
onExit: (handler: (code: number | null) => void) => void;
|
|
17
|
+
onError: (handler: (err: Error) => void) => void;
|
|
18
|
+
};
|
|
19
|
+
export type RunCommandOptions = {
|
|
20
|
+
env?: Record<string, string>;
|
|
21
|
+
timeout?: number;
|
|
22
|
+
stdin?: string;
|
|
23
|
+
};
|
|
24
|
+
export type RunCommandResult = {
|
|
25
|
+
code: number | null;
|
|
26
|
+
stdout: string;
|
|
27
|
+
stderr: string;
|
|
28
|
+
};
|
|
29
|
+
type SpawnOptions = {
|
|
30
|
+
env?: Record<string, string>;
|
|
31
|
+
stdin?: "pipe" | "ignore";
|
|
32
|
+
stdout?: "pipe" | "ignore";
|
|
33
|
+
stderr?: "pipe" | "ignore";
|
|
34
|
+
detached?: boolean;
|
|
35
|
+
};
|
|
36
|
+
export declare function envGet(name: string): string | undefined;
|
|
37
|
+
export declare function envSnapshot(extra?: Record<string, string>): Record<string, string>;
|
|
38
|
+
export declare function loadTextFile(filePath: string): Promise<string>;
|
|
39
|
+
export declare function runCommand(command: string, args: string[], opts?: RunCommandOptions): Promise<RunCommandResult>;
|
|
40
|
+
export declare function spawnCommand(command: string, args: string[], opts?: SpawnOptions): Promise<SpawnedProcess>;
|
|
41
|
+
export {};
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
function getBun() {
|
|
2
|
+
const bun = globalThis.Bun;
|
|
3
|
+
return bun && typeof bun === "object" ? bun : null;
|
|
4
|
+
}
|
|
5
|
+
function getProcessEnv() {
|
|
6
|
+
const proc = globalThis["process"];
|
|
7
|
+
const env = proc && typeof proc === "object" ? proc["env"] : undefined;
|
|
8
|
+
return env && typeof env === "object" ? env : {};
|
|
9
|
+
}
|
|
10
|
+
export function envGet(name) {
|
|
11
|
+
const bunEnv = getBun()?.env;
|
|
12
|
+
const bunValue = bunEnv && typeof bunEnv === "object" ? bunEnv[name] : undefined;
|
|
13
|
+
if (typeof bunValue === "string")
|
|
14
|
+
return bunValue;
|
|
15
|
+
const procValue = getProcessEnv()[name];
|
|
16
|
+
return typeof procValue === "string" ? procValue : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function envSnapshot(extra) {
|
|
19
|
+
const merged = {};
|
|
20
|
+
const bunEnv = getBun()?.env;
|
|
21
|
+
if (bunEnv && typeof bunEnv === "object") {
|
|
22
|
+
for (const [key, value] of Object.entries(bunEnv)) {
|
|
23
|
+
if (typeof value === "string")
|
|
24
|
+
merged[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const [key, value] of Object.entries(getProcessEnv())) {
|
|
28
|
+
if (typeof value === "string" && !(key in merged))
|
|
29
|
+
merged[key] = value;
|
|
30
|
+
}
|
|
31
|
+
if (extra)
|
|
32
|
+
Object.assign(merged, extra);
|
|
33
|
+
return merged;
|
|
34
|
+
}
|
|
35
|
+
function createLineEmitter(stream) {
|
|
36
|
+
if (!stream)
|
|
37
|
+
return null;
|
|
38
|
+
const handlers = new Set();
|
|
39
|
+
const decoder = new TextDecoder();
|
|
40
|
+
let buffer = "";
|
|
41
|
+
const emitBufferedLines = () => {
|
|
42
|
+
for (;;) {
|
|
43
|
+
const newline = buffer.indexOf("\n");
|
|
44
|
+
if (newline === -1)
|
|
45
|
+
break;
|
|
46
|
+
let line = buffer.slice(0, newline);
|
|
47
|
+
if (line.endsWith("\r"))
|
|
48
|
+
line = line.slice(0, -1);
|
|
49
|
+
buffer = buffer.slice(newline + 1);
|
|
50
|
+
for (const handler of handlers)
|
|
51
|
+
handler(line);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const pushChunk = (chunk) => {
|
|
55
|
+
if (typeof chunk === "string") {
|
|
56
|
+
buffer += chunk;
|
|
57
|
+
}
|
|
58
|
+
else if (chunk instanceof Uint8Array) {
|
|
59
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
60
|
+
}
|
|
61
|
+
else if (ArrayBuffer.isView(chunk)) {
|
|
62
|
+
buffer += decoder.decode(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), {
|
|
63
|
+
stream: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else if (chunk instanceof ArrayBuffer) {
|
|
67
|
+
buffer += decoder.decode(new Uint8Array(chunk), { stream: true });
|
|
68
|
+
}
|
|
69
|
+
else if (chunk != null) {
|
|
70
|
+
buffer += String(chunk);
|
|
71
|
+
}
|
|
72
|
+
emitBufferedLines();
|
|
73
|
+
};
|
|
74
|
+
const flush = () => {
|
|
75
|
+
const tail = decoder.decode();
|
|
76
|
+
if (tail)
|
|
77
|
+
buffer += tail;
|
|
78
|
+
if (buffer.endsWith("\r"))
|
|
79
|
+
buffer = buffer.slice(0, -1);
|
|
80
|
+
if (!buffer)
|
|
81
|
+
return;
|
|
82
|
+
for (const handler of handlers)
|
|
83
|
+
handler(buffer);
|
|
84
|
+
buffer = "";
|
|
85
|
+
};
|
|
86
|
+
void (async () => {
|
|
87
|
+
try {
|
|
88
|
+
const readable = stream;
|
|
89
|
+
if (typeof readable.getReader === "function") {
|
|
90
|
+
const reader = readable.getReader();
|
|
91
|
+
for (;;) {
|
|
92
|
+
const { done, value } = await reader.read();
|
|
93
|
+
if (done)
|
|
94
|
+
break;
|
|
95
|
+
pushChunk(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (typeof readable[Symbol.asyncIterator] === "function") {
|
|
99
|
+
for await (const chunk of readable) {
|
|
100
|
+
pushChunk(chunk);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Best-effort log consumption.
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
flush();
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
return {
|
|
112
|
+
onLine(handler) {
|
|
113
|
+
handlers.add(handler);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async function importNodeChildProcess() {
|
|
118
|
+
const moduleName = `node:${"child"}${"_process"}`;
|
|
119
|
+
return import(moduleName);
|
|
120
|
+
}
|
|
121
|
+
async function importNodeFsPromises() {
|
|
122
|
+
const moduleName = `node:${"fs"}/${"promises"}`;
|
|
123
|
+
return import(moduleName);
|
|
124
|
+
}
|
|
125
|
+
export async function loadTextFile(filePath) {
|
|
126
|
+
const bun = getBun();
|
|
127
|
+
if (bun?.file) {
|
|
128
|
+
return bun.file(filePath).text();
|
|
129
|
+
}
|
|
130
|
+
const mod = await importNodeFsPromises();
|
|
131
|
+
const reader = mod["read" + "File"];
|
|
132
|
+
const value = await reader(filePath, "utf8");
|
|
133
|
+
return typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
134
|
+
}
|
|
135
|
+
export async function runCommand(command, args, opts) {
|
|
136
|
+
const bun = getBun();
|
|
137
|
+
const env = envSnapshot(opts?.env);
|
|
138
|
+
if (bun?.spawn) {
|
|
139
|
+
const proc = bun.spawn([command, ...args], {
|
|
140
|
+
env,
|
|
141
|
+
stdin: opts?.stdin ? "pipe" : "ignore",
|
|
142
|
+
stdout: "pipe",
|
|
143
|
+
stderr: "pipe",
|
|
144
|
+
});
|
|
145
|
+
if (opts?.stdin && proc.stdin) {
|
|
146
|
+
proc.stdin.write(opts.stdin);
|
|
147
|
+
proc.stdin.end?.();
|
|
148
|
+
}
|
|
149
|
+
let timer = null;
|
|
150
|
+
if (opts?.timeout && opts.timeout > 0) {
|
|
151
|
+
timer = setTimeout(() => {
|
|
152
|
+
try {
|
|
153
|
+
proc.kill("SIGKILL");
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// already exited
|
|
157
|
+
}
|
|
158
|
+
}, opts.timeout);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const [code, stdout, stderr] = await Promise.all([
|
|
162
|
+
proc.exited,
|
|
163
|
+
proc.stdout ? new Response(proc.stdout).text() : Promise.resolve(""),
|
|
164
|
+
proc.stderr ? new Response(proc.stderr).text() : Promise.resolve(""),
|
|
165
|
+
]);
|
|
166
|
+
return { code, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
if (timer)
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const { spawn } = await importNodeChildProcess();
|
|
174
|
+
return await new Promise((resolve) => {
|
|
175
|
+
const proc = spawn(command, args, {
|
|
176
|
+
env,
|
|
177
|
+
stdio: [opts?.stdin ? "pipe" : "ignore", "pipe", "pipe"],
|
|
178
|
+
windowsHide: true,
|
|
179
|
+
});
|
|
180
|
+
let stdout = "";
|
|
181
|
+
let stderr = "";
|
|
182
|
+
let settled = false;
|
|
183
|
+
let timer = null;
|
|
184
|
+
const finish = (code) => {
|
|
185
|
+
if (settled)
|
|
186
|
+
return;
|
|
187
|
+
settled = true;
|
|
188
|
+
if (timer)
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
resolve({ code, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
191
|
+
};
|
|
192
|
+
proc.stdout?.on("data", (chunk) => {
|
|
193
|
+
stdout += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
194
|
+
});
|
|
195
|
+
proc.stderr?.on("data", (chunk) => {
|
|
196
|
+
stderr += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
197
|
+
});
|
|
198
|
+
proc.on("error", () => finish(null));
|
|
199
|
+
proc.on("close", (code) => finish(code));
|
|
200
|
+
if (opts?.stdin && proc.stdin) {
|
|
201
|
+
proc.stdin.write(opts.stdin);
|
|
202
|
+
proc.stdin.end();
|
|
203
|
+
}
|
|
204
|
+
if (opts?.timeout && opts.timeout > 0) {
|
|
205
|
+
timer = setTimeout(() => {
|
|
206
|
+
try {
|
|
207
|
+
proc.kill("SIGKILL");
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// already exited
|
|
211
|
+
}
|
|
212
|
+
}, opts.timeout);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
export async function spawnCommand(command, args, opts) {
|
|
217
|
+
const bun = getBun();
|
|
218
|
+
const env = envSnapshot(opts?.env);
|
|
219
|
+
if (bun?.spawn) {
|
|
220
|
+
const proc = bun.spawn([command, ...args], {
|
|
221
|
+
env,
|
|
222
|
+
stdin: opts?.stdin ?? "pipe",
|
|
223
|
+
stdout: opts?.stdout ?? "pipe",
|
|
224
|
+
stderr: opts?.stderr ?? "pipe",
|
|
225
|
+
...(opts?.detached ? { detached: true } : {}),
|
|
226
|
+
});
|
|
227
|
+
const exitHandlers = new Set();
|
|
228
|
+
const errorHandlers = new Set();
|
|
229
|
+
void proc.exited
|
|
230
|
+
.then((code) => {
|
|
231
|
+
for (const handler of exitHandlers)
|
|
232
|
+
handler(code);
|
|
233
|
+
})
|
|
234
|
+
.catch((err) => {
|
|
235
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
236
|
+
for (const handler of errorHandlers)
|
|
237
|
+
handler(error);
|
|
238
|
+
});
|
|
239
|
+
return {
|
|
240
|
+
stdin: proc.stdin
|
|
241
|
+
? {
|
|
242
|
+
isWritable: () => true,
|
|
243
|
+
write: (chunk) => {
|
|
244
|
+
proc.stdin.write(chunk);
|
|
245
|
+
},
|
|
246
|
+
end: () => {
|
|
247
|
+
proc.stdin.end?.();
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
: null,
|
|
251
|
+
stdout: createLineEmitter(proc.stdout),
|
|
252
|
+
stderr: createLineEmitter(proc.stderr),
|
|
253
|
+
kill: (signal) => {
|
|
254
|
+
try {
|
|
255
|
+
proc.kill(signal ?? "SIGTERM");
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// already exited
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
unref: typeof proc.unref === "function" ? () => proc.unref() : undefined,
|
|
262
|
+
onExit: (handler) => {
|
|
263
|
+
exitHandlers.add(handler);
|
|
264
|
+
},
|
|
265
|
+
onError: (handler) => {
|
|
266
|
+
errorHandlers.add(handler);
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const { spawn } = await importNodeChildProcess();
|
|
271
|
+
const proc = spawn(command, args, {
|
|
272
|
+
env,
|
|
273
|
+
stdio: [opts?.stdin ?? "pipe", opts?.stdout ?? "pipe", opts?.stderr ?? "pipe"],
|
|
274
|
+
windowsHide: true,
|
|
275
|
+
...(opts?.detached ? { detached: true } : {}),
|
|
276
|
+
});
|
|
277
|
+
const exitHandlers = new Set();
|
|
278
|
+
const errorHandlers = new Set();
|
|
279
|
+
proc.on("close", (code) => {
|
|
280
|
+
for (const handler of exitHandlers)
|
|
281
|
+
handler(code);
|
|
282
|
+
});
|
|
283
|
+
proc.on("error", (err) => {
|
|
284
|
+
for (const handler of errorHandlers)
|
|
285
|
+
handler(err);
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
stdin: proc.stdin
|
|
289
|
+
? {
|
|
290
|
+
isWritable: () => Boolean(proc.stdin?.writable),
|
|
291
|
+
write: (chunk) => {
|
|
292
|
+
proc.stdin?.write(chunk);
|
|
293
|
+
},
|
|
294
|
+
end: () => {
|
|
295
|
+
proc.stdin?.end();
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
: null,
|
|
299
|
+
stdout: createLineEmitter(proc.stdout),
|
|
300
|
+
stderr: createLineEmitter(proc.stderr),
|
|
301
|
+
kill: (signal) => {
|
|
302
|
+
try {
|
|
303
|
+
proc.kill(signal ?? "SIGTERM");
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// already exited
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
unref: typeof proc.unref === "function" ? () => proc.unref() : undefined,
|
|
310
|
+
onExit: (handler) => {
|
|
311
|
+
exitHandlers.add(handler);
|
|
312
|
+
},
|
|
313
|
+
onError: (handler) => {
|
|
314
|
+
errorHandlers.add(handler);
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PKG_VERSION: string;
|
package/dist/version.js
ADDED
package/openclaw.plugin.json
CHANGED
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
},
|
|
89
89
|
"injectionGuardScanAgentPrompt": {
|
|
90
90
|
"type": "boolean",
|
|
91
|
-
"description": "Scan the
|
|
91
|
+
"description": "Scan the prompt seen by before_prompt_build (default: true when enabled)"
|
|
92
92
|
},
|
|
93
93
|
"injectionGuardScanGetPrompt": {
|
|
94
94
|
"type": "boolean",
|
package/package.json
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sage-protocol/openclaw-sage",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Sage MCP bridge plugin for OpenClaw — prompt libraries, skills, governance, and on-chain operations",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
6
7
|
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"openclaw.plugin.json",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE*"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./dist/index.js"
|
|
16
|
+
},
|
|
7
17
|
"openclaw": {
|
|
8
18
|
"extensions": [
|
|
9
|
-
"./
|
|
10
|
-
]
|
|
19
|
+
"./dist/index.js"
|
|
20
|
+
],
|
|
21
|
+
"hooks": []
|
|
11
22
|
},
|
|
12
23
|
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.build.json && node -e \"const fs=require('node:fs'); const p='dist/version.js'; fs.writeFileSync(p, fs.readFileSync(p, 'utf8').replace('__PKG_VERSION__', process.env.npm_package_version || '0.0.0'));\"",
|
|
25
|
+
"prepack": "npm run build",
|
|
13
26
|
"typecheck": "tsc --noEmit",
|
|
14
27
|
"test": "node --import tsx src/mcp-bridge.test.ts && node --import tsx src/openclaw-hook.integration.test.ts",
|
|
15
28
|
"test:e2e": "SAGE_E2E_OPENCLAW=1 npm test"
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
typecheck-and-unit-test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
|
|
15
|
-
- uses: actions/setup-node@v4
|
|
16
|
-
with:
|
|
17
|
-
node-version: "22"
|
|
18
|
-
|
|
19
|
-
- name: Install dependencies
|
|
20
|
-
run: npm install
|
|
21
|
-
|
|
22
|
-
- name: TypeScript typecheck
|
|
23
|
-
run: npm run typecheck
|
|
24
|
-
|
|
25
|
-
- name: Run unit tests (no sage binary required)
|
|
26
|
-
run: node --import tsx src/mcp-bridge.test.ts
|
|
27
|
-
env:
|
|
28
|
-
# Unit tests that don't need the sage binary will pass;
|
|
29
|
-
# integration tests that need it will be skipped on CI
|
|
30
|
-
NODE_OPTIONS: "--test-only"
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
name: release-please
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
id-token: write
|
|
9
|
-
contents: write
|
|
10
|
-
pull-requests: write
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
release-please:
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
steps:
|
|
16
|
-
- uses: googleapis/release-please-action@v4
|
|
17
|
-
with:
|
|
18
|
-
config-file: release-please-config.json
|
|
19
|
-
manifest-file: .release-please-manifest.json
|