@sage-protocol/openclaw-sage 0.1.1 → 0.1.3

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.
@@ -0,0 +1,18 @@
1
+ name: release-please
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ release-please:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: googleapis/release-please-action@v4
16
+ with:
17
+ config-file: release-please-config.json
18
+ manifest-file: .release-please-manifest.json
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.3"
3
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ ## [0.1.3](https://github.com/sage-protocol/openclaw-sage/compare/openclaw-sage-v0.1.2...openclaw-sage-v0.1.3) (2026-02-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * add Sage capture hooks ([f8fab39](https://github.com/sage-protocol/openclaw-sage/commit/f8fab399860de55d1949c9358a443372f0617eb6))
9
+ * adding ci, release please and improvements ([c32f79a](https://github.com/sage-protocol/openclaw-sage/commit/c32f79a05ee9212d4e382c9131ec684c91706add))
10
+ * adding readme ([4aea6c9](https://github.com/sage-protocol/openclaw-sage/commit/4aea6c950e2dcf276ebcb492cfe70b6ac4cd138e))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * fixing manifest naming ([28d6add](https://github.com/sage-protocol/openclaw-sage/commit/28d6add05e0b4e60b17993aaf73a23ef55ab1c94))
16
+ * fixing missing openclaw manifest ([5062ae7](https://github.com/sage-protocol/openclaw-sage/commit/5062ae789d2733a209ce2f0c63453779f73245fb))
17
+
18
+ ## Changelog
19
+
20
+ All notable changes to this package are documented here.
package/README.md CHANGED
@@ -1,6 +1,13 @@
1
- # @sage-protocol/openclaw-sage
1
+ # Sage Plugin (OpenClaw)
2
2
 
3
- Sage Protocol MCP bridge plugin for OpenClaw. Provides prompt libraries, skills, governance, and on-chain operations directly in OpenClaw sessions.
3
+ MCP bridge plugin that exposes all Sage Protocol tools inside OpenClaw. Spawns the sage MCP server as a child process and translates JSON-RPC calls into registered OpenClaw tools.
4
+
5
+ ## What It Does
6
+
7
+ - **MCP Tool Bridge** - Spawns `sage mcp start` and translates JSON-RPC tool calls into native OpenClaw tools
8
+ - **Dynamic Registration** - Discovers available tools at startup and registers them with typed schemas
9
+ - **RLM Capture** - Records prompt/response pairs for Sage's RLM feedback loop
10
+ - **Crash Recovery** - Automatically restarts the MCP subprocess on unexpected exits
4
11
 
5
12
  ## Install
6
13
 
@@ -8,11 +15,6 @@ Sage Protocol MCP bridge plugin for OpenClaw. Provides prompt libraries, skills,
8
15
  openclaw plugins install @sage-protocol/openclaw-sage
9
16
  ```
10
17
 
11
- ## Requirements
12
-
13
- - [Sage CLI](https://github.com/sage-protocol/sage-cli) installed and available on PATH
14
- - OpenClaw v0.1.0+
15
-
16
18
  ## Configuration
17
19
 
18
20
  The plugin auto-detects the `sage` binary from PATH. To override:
@@ -25,13 +27,18 @@ The plugin auto-detects the `sage` binary from PATH. To override:
25
27
 
26
28
  ## What It Provides
27
29
 
28
- The plugin exposes Sage Protocol MCP tools inside OpenClaw:
30
+ Once loaded, all Sage MCP tools are available in OpenClaw:
31
+
32
+ - **Prompts & Libraries** - Search, list, create, and manage prompt libraries
33
+ - **Skills** - Discover and activate skills from Sage Protocol, GitHub, or local sources
34
+ - **Builder** - AI-powered prompt recommendations and synthesis
35
+ - **Governance** - List DAOs, view proposals, check voting power
36
+ - **Hub** - Start/stop additional MCP servers (memory, brave-search, github, etc.)
29
37
 
30
- - **Prompts & Libraries** — search, list, create, and manage prompt libraries
31
- - **Skills** — discover and activate skills from Sage Protocol, GitHub, or local sources
32
- - **Builder** AI-powered prompt recommendations and synthesis
33
- - **Governance** — list DAOs, view proposals, check voting power
34
- - **Hub** — start/stop additional MCP servers (memory, brave-search, github, etc.)
38
+ ## Requirements
39
+
40
+ - Sage CLI on PATH
41
+ - OpenClaw v0.1.0+
35
42
 
36
43
  ## Development
37
44
 
package/SOUL.md ADDED
@@ -0,0 +1,62 @@
1
+ # OpenClaw Agent SOUL
2
+
3
+ This file defines the default behavior for an OpenClaw agent operating in the Sage Protocol ecosystem.
4
+
5
+ ## Core Objective
6
+
7
+ Help the user get productive with Sage as fast as possible by reliably steering onboarding toward:
8
+
9
+ ```bash
10
+ sage init
11
+ sage agent quickstart
12
+ ```
13
+
14
+ ## When To Prompt For Onboarding
15
+
16
+ Prompt the user to run `sage init` and/or `sage agent quickstart` when any of the following is true:
17
+
18
+ - The user asks about setup, installation, configuration, MCP tools, skills, libraries, or OpenClaw integration.
19
+ - The session appears to be a first run (no prior mention of `sage init` / `sage agent quickstart`).
20
+ - The user hits auth/credits/wallet friction (examples: wallet not configured, faucet needed, IPFS credits missing, library sync failing).
21
+
22
+ ## How To Prompt (Non-Spammy)
23
+
24
+ - Ask once, early, and only repeat if the user is blocked by missing setup.
25
+ - Prefer one short message with a single actionable next step.
26
+ - Don't derail the task: if the user is already mid-task, suggest onboarding as a quick fix only when it unblocks progress.
27
+
28
+ ## Recommended Message Templates
29
+
30
+ If Sage is not initialized:
31
+
32
+ ```text
33
+ Run `sage init` to set up the Sage skill + MCP integration for your environment.
34
+ After that, run `sage agent quickstart` to claim testnet tokens + IPFS credits.
35
+ ```
36
+
37
+ If Sage is initialized but the user needs tokens/credits:
38
+
39
+ ```text
40
+ Run `sage agent quickstart` to claim testnet tokens + IPFS credits (it will also check your current status).
41
+ ```
42
+
43
+ If the user is using OpenClaw and doesn't have MCP tools:
44
+
45
+ ```text
46
+ Install the OpenClaw MCP bridge: `openclaw plugins install @sage-protocol/openclaw-sage`.
47
+ Then run: `sage init` and `sage agent quickstart`.
48
+ ```
49
+
50
+ ## OpenClaw Skill Directory
51
+
52
+ OpenClaw loads skills from:
53
+
54
+ ```text
55
+ ~/.openclaw/workspace/skills/
56
+ ```
57
+
58
+ ## Guardrails
59
+
60
+ - Don't instruct `git push`, key export, or any destructive command unless the user explicitly asks.
61
+ - Don't ask the user to paste secrets (tokens, private keys). Use `sage wallet connect -w privy` (OAuth) or documented wallet flows.
62
+ - Prefer the unified `sage suggest ...` family for discovery, but onboarding comes first.
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "sage-mcp",
2
+ "id": "openclaw-sage",
3
3
  "uiHints": {
4
4
  "sageBinary": {
5
5
  "label": "Sage Binary Path",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sage-protocol/openclaw-sage",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Sage MCP bridge plugin for OpenClaw — prompt libraries, skills, governance, and on-chain operations",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "typecheck": "tsc --noEmit",
14
- "test": "node --import tsx src/mcp-bridge.test.ts"
14
+ "test": "npx --yes tsx src/mcp-bridge.test.ts"
15
15
  },
16
16
  "dependencies": {
17
17
  "@sinclair/typebox": "^0.34.0"
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "release-type": "node",
4
+ "include-v-in-tag": true,
5
+ "bump-minor-pre-major": true,
6
+ "bump-patch-for-minor-pre-major": true,
7
+ "packages": {
8
+ ".": {
9
+ "package-name": "@sage-protocol/openclaw-sage",
10
+ "changelog-path": "CHANGELOG.md"
11
+ }
12
+ }
13
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
 
3
+ import { spawn } from "node:child_process";
4
+
3
5
  import { McpBridge, type McpToolDef } from "./mcp-bridge.js";
4
6
 
5
7
  /**
@@ -100,10 +102,92 @@ function toToolResult(mcpResult: unknown) {
100
102
 
101
103
  let bridge: McpBridge | null = null;
102
104
 
105
+ function extractText(input: unknown): string {
106
+ if (typeof input === "string") return input;
107
+ if (!input || typeof input !== "object") return "";
108
+
109
+ const obj = input as any;
110
+ const direct = [obj.text, obj.content, obj.prompt, obj.message, obj.input];
111
+ for (const c of direct) {
112
+ if (typeof c === "string" && c.trim()) return c.trim();
113
+ }
114
+
115
+ if (obj.message && typeof obj.message === "object") {
116
+ const msg = obj.message as any;
117
+ if (typeof msg.text === "string" && msg.text.trim()) return msg.text.trim();
118
+ if (typeof msg.content === "string" && msg.content.trim()) return msg.content.trim();
119
+ }
120
+
121
+ // Transcript-style: [{role, content}]
122
+ if (Array.isArray(obj.messages)) {
123
+ for (let i = obj.messages.length - 1; i >= 0; i--) {
124
+ const m = obj.messages[i];
125
+ if (!m || typeof m !== "object") continue;
126
+ const mm = m as any;
127
+ if (typeof mm.content === "string" && mm.content.trim()) return mm.content.trim();
128
+ if (typeof mm.text === "string" && mm.text.trim()) return mm.text.trim();
129
+ if (Array.isArray(mm.content)) {
130
+ const text = mm.content
131
+ .map((b: any) => (b?.type === "text" ? b?.text : ""))
132
+ .filter(Boolean)
133
+ .join("\n");
134
+ if (text.trim()) return text.trim();
135
+ }
136
+ }
137
+ }
138
+
139
+ return "";
140
+ }
141
+
142
+ function lastAssistantText(input: unknown): string {
143
+ if (!input || typeof input !== "object") return "";
144
+ const obj = input as any;
145
+
146
+ const candidates = [obj.final, obj.output, obj.result, obj.response];
147
+ for (const c of candidates) {
148
+ const t = extractText(c);
149
+ if (t) return t;
150
+ }
151
+
152
+ if (Array.isArray(obj.messages)) {
153
+ for (let i = obj.messages.length - 1; i >= 0; i--) {
154
+ const m = obj.messages[i];
155
+ if (!m || typeof m !== "object") continue;
156
+ const mm = m as any;
157
+ if (mm.role === "assistant") {
158
+ const t = extractText(mm);
159
+ if (t) return t;
160
+ }
161
+ }
162
+ }
163
+
164
+ return "";
165
+ }
166
+
167
+ function toStringOrEmpty(v: unknown): string {
168
+ if (typeof v === "string") return v;
169
+ if (typeof v === "number") return String(v);
170
+ if (typeof v === "boolean") return v ? "1" : "0";
171
+ return "";
172
+ }
173
+
174
+ function fireAndForget(cmd: string, args: string[], env: Record<string, string>): void {
175
+ try {
176
+ const p = spawn(cmd, args, {
177
+ env: { ...process.env, ...env },
178
+ stdio: "ignore",
179
+ detached: true,
180
+ });
181
+ p.unref();
182
+ } catch {
183
+ // ignore
184
+ }
185
+ }
186
+
103
187
  const plugin = {
104
- id: "sage-mcp",
188
+ id: "openclaw-sage",
105
189
  name: "Sage Protocol",
106
- version: "0.1.0",
190
+ version: "0.1.2",
107
191
  description:
108
192
  "Sage MCP tools for prompt libraries, skills, governance, and on-chain operations",
109
193
 
@@ -113,6 +197,64 @@ const plugin = {
113
197
  bridge.on("log", (line: string) => api.logger.info(`[sage-mcp] ${line}`));
114
198
  bridge.on("error", (err: Error) => api.logger.error(`[sage-mcp] ${err.message}`));
115
199
 
200
+ // RLM capture hooks (derived data): attach prompt/response pairs with OpenClaw tags.
201
+ api.on("message_received", async (evt: unknown) => {
202
+ const prompt = extractText(evt);
203
+ if (!prompt) return;
204
+
205
+ const e = evt as any;
206
+ const sessionId =
207
+ toStringOrEmpty(e?.sessionId) ||
208
+ toStringOrEmpty(e?.sessionKey) ||
209
+ toStringOrEmpty(e?.context?.sessionId) ||
210
+ toStringOrEmpty(e?.context?.sessionKey);
211
+ const workspaceDir = toStringOrEmpty(e?.workspaceDir) || toStringOrEmpty(e?.context?.workspaceDir);
212
+
213
+ const attrs = {
214
+ openclaw: {
215
+ hook: "message_received",
216
+ sessionId: toStringOrEmpty(e?.sessionId) || toStringOrEmpty(e?.context?.sessionId),
217
+ sessionKey: toStringOrEmpty(e?.sessionKey) || toStringOrEmpty(e?.context?.sessionKey),
218
+ channel: toStringOrEmpty(e?.channel) || toStringOrEmpty(e?.context?.commandSource),
219
+ senderId: toStringOrEmpty(e?.senderId) || toStringOrEmpty(e?.context?.senderId),
220
+ },
221
+ };
222
+
223
+ fireAndForget("sage", ["capture", "hook", "prompt"], {
224
+ SAGE_SOURCE: "openclaw",
225
+ OPENCLAW: "1",
226
+ PROMPT: prompt,
227
+ SAGE_SESSION_ID: sessionId,
228
+ SAGE_WORKSPACE: workspaceDir,
229
+ SAGE_MODEL: toStringOrEmpty(e?.model) || toStringOrEmpty(e?.context?.model),
230
+ SAGE_PROVIDER: toStringOrEmpty(e?.provider) || toStringOrEmpty(e?.context?.provider),
231
+ SAGE_CAPTURE_ATTRIBUTES_JSON: JSON.stringify(attrs),
232
+ });
233
+ });
234
+
235
+ api.on("agent_end", async (evt: unknown) => {
236
+ const response = lastAssistantText(evt);
237
+ if (!response) return;
238
+
239
+ const e = evt as any;
240
+ const tokensIn =
241
+ toStringOrEmpty(e?.usage?.tokens_input) ||
242
+ toStringOrEmpty(e?.usage?.input_tokens) ||
243
+ toStringOrEmpty(e?.usage?.inputTokens);
244
+ const tokensOut =
245
+ toStringOrEmpty(e?.usage?.tokens_output) ||
246
+ toStringOrEmpty(e?.usage?.output_tokens) ||
247
+ toStringOrEmpty(e?.usage?.outputTokens);
248
+
249
+ fireAndForget("sage", ["capture", "hook", "response"], {
250
+ SAGE_SOURCE: "openclaw",
251
+ OPENCLAW: "1",
252
+ LAST_RESPONSE: response,
253
+ TOKENS_INPUT: tokensIn,
254
+ TOKENS_OUTPUT: tokensOut,
255
+ });
256
+ });
257
+
116
258
  api.registerService({
117
259
  id: "sage-mcp-bridge",
118
260
  start: async (ctx) => {
@@ -0,0 +1,81 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { resolve } from "node:path";
4
+
5
+ import { McpBridge } from "./mcp-bridge.js";
6
+ import plugin from "./index.js";
7
+
8
+ function addSageDebugBinToPath() {
9
+ // Ensure the `sage` binary used by the plugin resolves to this repo's build.
10
+ const binDir = resolve(new URL("..", import.meta.url).pathname, "..", "target", "debug");
11
+ const sep = process.platform === "win32" ? ";" : ":";
12
+ process.env.PATH = `${binDir}${sep}${process.env.PATH ?? ""}`;
13
+ return { binDir };
14
+ }
15
+
16
+ test("McpBridge can initialize, list tools, and call a native tool", async () => {
17
+ const sageBin = resolve(new URL("..", import.meta.url).pathname, "..", "target", "debug", "sage");
18
+ const bridge = new McpBridge(sageBin, ["mcp", "start"]);
19
+ await bridge.start();
20
+ try {
21
+ const tools = await bridge.listTools();
22
+ assert.ok(Array.isArray(tools));
23
+ assert.ok(tools.length > 0);
24
+
25
+ const hasProjectContext = tools.some((t) => t.name === "get_project_context");
26
+ assert.ok(hasProjectContext, "expected get_project_context tool to exist");
27
+
28
+ const result = await bridge.callTool("get_project_context", {});
29
+ assert.ok(result && typeof result === "object");
30
+ } finally {
31
+ await bridge.stop();
32
+ }
33
+ });
34
+
35
+ test("OpenClaw plugin registers MCP tools via sage mcp start", async () => {
36
+ addSageDebugBinToPath();
37
+
38
+ const registeredTools: string[] = [];
39
+ const services: Array<{ id: string; start: Function; stop?: Function }> = [];
40
+
41
+ const api = {
42
+ id: "t",
43
+ name: "t",
44
+ logger: {
45
+ info: (_: string) => {},
46
+ warn: (_: string) => {},
47
+ error: (_: string) => {},
48
+ },
49
+ registerTool: (tool: any) => {
50
+ if (tool?.name) registeredTools.push(tool.name);
51
+ },
52
+ registerService: (svc: any) => {
53
+ services.push(svc);
54
+ },
55
+ on: (_hook: string, _handler: any) => {},
56
+ };
57
+
58
+ plugin.register(api);
59
+ const svc = services.find((s) => s.id === "sage-mcp-bridge");
60
+ assert.ok(svc, "expected sage-mcp-bridge service to be registered");
61
+
62
+ await svc!.start({
63
+ config: {},
64
+ stateDir: "/tmp",
65
+ logger: api.logger,
66
+ });
67
+
68
+ // Tool names are prefixed with `sage_` in this plugin.
69
+ assert.ok(
70
+ registeredTools.some((n) => n.startsWith("sage_")),
71
+ "expected at least one sage_* tool",
72
+ );
73
+
74
+ if (svc!.stop) {
75
+ await svc!.stop({
76
+ config: {},
77
+ stateDir: "/tmp",
78
+ logger: api.logger,
79
+ });
80
+ }
81
+ });