settld 0.2.6 → 0.2.8

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.
@@ -182,7 +182,7 @@ Then activate host-side:
182
182
  - `codex`: restart Codex.
183
183
  - `claude`: restart Claude Desktop.
184
184
  - `cursor`: restart Cursor.
185
- - `openclaw`: run `openclaw doctor`, ensure OpenClaw onboarding is complete (`openclaw onboard --install-daemon`), then run `openclaw tui`.
185
+ - `openclaw`: run `openclaw doctor`, ensure OpenClaw onboarding is complete (`openclaw onboard --install-daemon`), install plugin (`openclaw plugins install settld@latest`), run local verification (`openclaw agent --local --agent main --session-id settld-smoke --message "Use the tool named settld_about with empty arguments. Return only JSON." --json`), then run `openclaw tui --session main`.
186
186
 
187
187
  ## 5) Fund and verify wallet state
188
188
 
@@ -32,7 +32,7 @@ Publish the folder `docs/integrations/openclaw/settld-mcp-skill/` as your skill
32
32
  If ClawHub UI requests install instructions, use:
33
33
 
34
34
  - command: `npx`
35
- - args: `-y settld-mcp`
35
+ - args: `-y --package settld@latest settld-mcp`
36
36
  - env: `SETTLD_BASE_URL`, `SETTLD_TENANT_ID`, `SETTLD_API_KEY`, optional `SETTLD_PAID_TOOLS_BASE_URL`
37
37
 
38
38
  ## 4) Post-Publish Smoke Test
@@ -72,19 +72,27 @@ Run:
72
72
 
73
73
  ```bash
74
74
  openclaw doctor
75
+ openclaw plugins install settld@latest
76
+ openclaw agent --local --agent main --session-id settld-smoke --message "Use the tool named settld_about with empty arguments. Return only JSON." --json
75
77
  ```
76
78
 
77
79
  Then from OpenClaw chat/test prompt:
78
80
 
79
- - `Call settld.about and return JSON.`
81
+ - `Use tool settld_about and return JSON only.`
80
82
 
81
83
  Expected result: success payload with Settld tool metadata.
82
84
 
85
+ If your TUI is in a channel-bound session (`whatsapp:*`, `telegram:*`), switch to `main` first:
86
+
87
+ ```bash
88
+ openclaw tui --session main
89
+ ```
90
+
83
91
  ## 4) Run first paid tool call
84
92
 
85
93
  From OpenClaw prompt:
86
94
 
87
- - `Run settld.weather_current_paid for city=Chicago unit=f and include x-settld-* headers in the response.`
95
+ - `Use tool settld_call with tool=settld.weather_current_paid and arguments={"city":"Chicago","unit":"f"}.`
88
96
 
89
97
  Expected result:
90
98
 
@@ -30,14 +30,16 @@ It is designed for the public `quick` onboarding flow:
30
30
  - Settld runtime env from setup (`SETTLD_API_KEY`, `SETTLD_BASE_URL`, `SETTLD_TENANT_ID`)
31
31
  - Optional paid tools base URL (`SETTLD_PAID_TOOLS_BASE_URL`)
32
32
 
33
- ## MCP Server Registration
33
+ ## OpenClaw Plugin Registration
34
34
 
35
- Use the server definition in `mcp-server.example.json`.
35
+ Install the Settld OpenClaw plugin from npm:
36
36
 
37
- Server command:
37
+ - `openclaw plugins install settld@latest`
38
38
 
39
- - command: `npx`
40
- - args: `["-y","settld-mcp"]`
39
+ This plugin wraps Settld MCP under OpenClaw-native tools:
40
+
41
+ - `settld_about`
42
+ - `settld_call`
41
43
 
42
44
  Required env vars:
43
45
 
@@ -52,10 +54,10 @@ Optional env vars:
52
54
 
53
55
  ## Agent Usage Pattern
54
56
 
55
- 1. Call `settld.about` to verify connectivity.
56
- 2. For paid search/data calls, use:
57
- - `settld.exa_search_paid`
58
- - `settld.weather_current_paid`
57
+ 1. Call `settld_about` to verify connectivity.
58
+ 2. For paid search/data calls, use `settld_call` with:
59
+ - `tool=settld.exa_search_paid`
60
+ - `tool=settld.weather_current_paid`
59
61
  3. For agreement lifecycle demo calls, use:
60
62
  - `settld.create_agreement`
61
63
  - `settld.submit_evidence`
@@ -64,8 +66,8 @@ Optional env vars:
64
66
 
65
67
  ## Smoke Prompts
66
68
 
67
- - "Call `settld.about` and return the result JSON."
68
- - "Run `settld.weather_current_paid` for Chicago in fahrenheit and include the `x-settld-*` headers."
69
+ - "Use tool `settld_about` and return JSON."
70
+ - "Use tool `settld_call` with `tool=settld.weather_current_paid` and arguments for Chicago/fahrenheit."
69
71
 
70
72
  ## Identity + Traceability
71
73
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "settld",
3
3
  "command": "npx",
4
- "args": ["-y", "settld-mcp"],
4
+ "args": ["-y", "--package", "settld@latest", "settld-mcp"],
5
5
  "env": {
6
6
  "SETTLD_BASE_URL": "https://api.settld.work",
7
7
  "SETTLD_TENANT_ID": "tenant_xxx",
@@ -0,0 +1,307 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+ import { spawn } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const REQUIRED_ENV_KEYS = ["SETTLD_BASE_URL", "SETTLD_TENANT_ID", "SETTLD_API_KEY"];
9
+ const OPTIONAL_ENV_KEYS = ["SETTLD_PAID_TOOLS_BASE_URL", "SETTLD_PAID_TOOLS_AGENT_PASSPORT"];
10
+ const MCP_SCRIPT_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "scripts", "mcp", "settld-mcp-server.mjs");
11
+
12
+ function isObject(value) {
13
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
14
+ }
15
+
16
+ function pickString(value) {
17
+ if (typeof value !== "string") return "";
18
+ return value.trim();
19
+ }
20
+
21
+ function parseSettldServerConfig(mcpConfig) {
22
+ if (!isObject(mcpConfig)) return null;
23
+ if (isObject(mcpConfig.mcpServers) && isObject(mcpConfig.mcpServers.settld)) {
24
+ return mcpConfig.mcpServers.settld;
25
+ }
26
+ if (pickString(mcpConfig.name) === "settld") return mcpConfig;
27
+ return null;
28
+ }
29
+
30
+ function parseSettldEnvFromServer(server) {
31
+ if (!isObject(server) || !isObject(server.env)) return {};
32
+ const env = {};
33
+ for (const key of [...REQUIRED_ENV_KEYS, ...OPTIONAL_ENV_KEYS]) {
34
+ const value = pickString(server.env[key]);
35
+ if (value) env[key] = value;
36
+ }
37
+ return env;
38
+ }
39
+
40
+ function defaultMcpConfigPathCandidates() {
41
+ const home = os.homedir();
42
+ const xdgConfigHome = pickString(process.env.XDG_CONFIG_HOME) || path.join(home, ".config");
43
+ return [
44
+ pickString(process.env.OPENCLAW_MCP_CONFIG_PATH),
45
+ pickString(process.env.OPENCLAW_HOME) ? path.join(pickString(process.env.OPENCLAW_HOME), "mcp.json") : "",
46
+ path.join(home, "Library", "Application Support", "OpenClaw", "mcp.json"),
47
+ path.join(home, ".openclaw", "mcp.json"),
48
+ path.join(xdgConfigHome, "OpenClaw", "mcp.json"),
49
+ path.join(xdgConfigHome, "openclaw", "mcp.json")
50
+ ].filter(Boolean);
51
+ }
52
+
53
+ async function readSettldEnvFromMcpConfig(mcpConfigPath) {
54
+ const raw = await fs.readFile(mcpConfigPath, "utf8");
55
+ const parsed = JSON.parse(raw);
56
+ const server = parseSettldServerConfig(parsed);
57
+ return parseSettldEnvFromServer(server);
58
+ }
59
+
60
+ async function resolveSettldEnv(pluginConfig = {}) {
61
+ const env = {};
62
+
63
+ const fromPluginConfig = {
64
+ SETTLD_BASE_URL: pickString(pluginConfig.baseUrl),
65
+ SETTLD_TENANT_ID: pickString(pluginConfig.tenantId),
66
+ SETTLD_API_KEY: pickString(pluginConfig.apiKey),
67
+ SETTLD_PAID_TOOLS_BASE_URL: pickString(pluginConfig.paidToolsBaseUrl),
68
+ SETTLD_PAID_TOOLS_AGENT_PASSPORT: pickString(pluginConfig.paidToolsAgentPassport)
69
+ };
70
+ for (const [key, value] of Object.entries(fromPluginConfig)) {
71
+ if (value) env[key] = value;
72
+ }
73
+
74
+ for (const key of [...REQUIRED_ENV_KEYS, ...OPTIONAL_ENV_KEYS]) {
75
+ const value = pickString(process.env[key]);
76
+ if (value && !env[key]) env[key] = value;
77
+ }
78
+
79
+ const missingRequired = REQUIRED_ENV_KEYS.filter((key) => !pickString(env[key]));
80
+ if (missingRequired.length === 0) return env;
81
+
82
+ const candidates = [];
83
+ const explicitPath = pickString(pluginConfig.mcpConfigPath);
84
+ if (explicitPath) candidates.push(explicitPath);
85
+ candidates.push(...defaultMcpConfigPathCandidates());
86
+
87
+ const seen = new Set();
88
+ for (const candidate of candidates) {
89
+ const resolved = path.resolve(candidate);
90
+ if (seen.has(resolved)) continue;
91
+ seen.add(resolved);
92
+ try {
93
+ const fromFile = await readSettldEnvFromMcpConfig(resolved);
94
+ for (const [key, value] of Object.entries(fromFile)) {
95
+ if (value && !env[key]) env[key] = value;
96
+ }
97
+ const nowMissing = REQUIRED_ENV_KEYS.filter((key) => !pickString(env[key]));
98
+ if (nowMissing.length === 0) return env;
99
+ } catch {
100
+ // Keep searching; users may have multiple OpenClaw profiles/config paths.
101
+ }
102
+ }
103
+
104
+ return env;
105
+ }
106
+
107
+ function parseLineJson(line) {
108
+ try {
109
+ return JSON.parse(line);
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ function waitForRpcResponse(pendingMap, id, timeoutMs) {
116
+ return new Promise((resolve, reject) => {
117
+ const timer = setTimeout(() => {
118
+ pendingMap.delete(id);
119
+ reject(new Error(`MCP request timed out (id=${id})`));
120
+ }, timeoutMs);
121
+ pendingMap.set(id, { resolve, reject, timer });
122
+ });
123
+ }
124
+
125
+ function routeRpcMessage(pendingMap, message) {
126
+ if (!isObject(message)) return;
127
+ if (!Object.prototype.hasOwnProperty.call(message, "id")) return;
128
+ const pending = pendingMap.get(message.id);
129
+ if (!pending) return;
130
+ clearTimeout(pending.timer);
131
+ pendingMap.delete(message.id);
132
+ pending.resolve(message);
133
+ }
134
+
135
+ function createStdoutRouter(stdout, pendingMap) {
136
+ let buffer = "";
137
+ stdout.setEncoding("utf8");
138
+ stdout.on("data", (chunk) => {
139
+ buffer += String(chunk ?? "");
140
+ while (buffer.includes("\n")) {
141
+ const idx = buffer.indexOf("\n");
142
+ const line = buffer.slice(0, idx).trim();
143
+ buffer = buffer.slice(idx + 1);
144
+ if (!line) continue;
145
+ const parsed = parseLineJson(line);
146
+ if (parsed) routeRpcMessage(pendingMap, parsed);
147
+ }
148
+ });
149
+ }
150
+
151
+ function rpcWrite(stdin, payload) {
152
+ stdin.write(`${JSON.stringify(payload)}\n`);
153
+ }
154
+
155
+ async function callSettldMcpTool({ toolName, toolArgs, env, timeoutMs = 30_000 }) {
156
+ const child = spawn(process.execPath, [MCP_SCRIPT_PATH], {
157
+ env: { ...process.env, ...env },
158
+ stdio: ["pipe", "pipe", "pipe"]
159
+ });
160
+ const pendingMap = new Map();
161
+ createStdoutRouter(child.stdout, pendingMap);
162
+
163
+ let stderr = "";
164
+ child.stderr.setEncoding("utf8");
165
+ child.stderr.on("data", (chunk) => {
166
+ stderr += String(chunk ?? "");
167
+ });
168
+
169
+ const waitInit = waitForRpcResponse(pendingMap, 1, timeoutMs);
170
+ rpcWrite(child.stdin, {
171
+ jsonrpc: "2.0",
172
+ id: 1,
173
+ method: "initialize",
174
+ params: {
175
+ protocolVersion: "2024-11-05",
176
+ clientInfo: { name: "openclaw-settld-plugin", version: "1" },
177
+ capabilities: {}
178
+ }
179
+ });
180
+ await waitInit;
181
+ rpcWrite(child.stdin, { jsonrpc: "2.0", method: "notifications/initialized", params: {} });
182
+
183
+ const waitCall = waitForRpcResponse(pendingMap, 2, timeoutMs);
184
+ rpcWrite(child.stdin, {
185
+ jsonrpc: "2.0",
186
+ id: 2,
187
+ method: "tools/call",
188
+ params: {
189
+ name: toolName,
190
+ arguments: isObject(toolArgs) ? toolArgs : {}
191
+ }
192
+ });
193
+ const callResponse = await waitCall;
194
+
195
+ child.kill("SIGTERM");
196
+ if (isObject(callResponse.error)) {
197
+ const message = pickString(callResponse.error.message) || "MCP tool call failed";
198
+ throw new Error(`${message}${stderr.trim() ? ` | stderr: ${stderr.trim()}` : ""}`);
199
+ }
200
+ return callResponse.result;
201
+ }
202
+
203
+ function buildMissingEnvMessage(env) {
204
+ const missing = REQUIRED_ENV_KEYS.filter((key) => !pickString(env[key]));
205
+ if (missing.length === 0) return "";
206
+ return [
207
+ `Missing Settld runtime env: ${missing.join(", ")}`,
208
+ "Run: npx -y settld@latest setup",
209
+ "Select host=openclaw in quick mode, then retry."
210
+ ].join(" ");
211
+ }
212
+
213
+ function normalizeToolArguments(params) {
214
+ if (isObject(params.arguments)) return params.arguments;
215
+ const raw = pickString(params.argumentsJson);
216
+ if (!raw) return {};
217
+ try {
218
+ const parsed = JSON.parse(raw);
219
+ if (!isObject(parsed)) {
220
+ throw new Error("argumentsJson must decode to a JSON object");
221
+ }
222
+ return parsed;
223
+ } catch (err) {
224
+ throw new Error(`Invalid argumentsJson: ${err?.message ?? "parse failed"}`);
225
+ }
226
+ }
227
+
228
+ function buildToolResult(payload, details = {}) {
229
+ return {
230
+ content: [{ type: "text", text: JSON.stringify(payload ?? {}, null, 2) }],
231
+ details
232
+ };
233
+ }
234
+
235
+ export default function register(api) {
236
+ api.registerTool({
237
+ name: "settld_about",
238
+ label: "Settld About",
239
+ description: "Check Settld runtime connectivity and capability info.",
240
+ parameters: {
241
+ type: "object",
242
+ additionalProperties: false,
243
+ properties: {}
244
+ },
245
+ async execute() {
246
+ const env = await resolveSettldEnv(api.pluginConfig ?? {});
247
+ const missingMessage = buildMissingEnvMessage(env);
248
+ if (missingMessage) throw new Error(missingMessage);
249
+ const result = await callSettldMcpTool({
250
+ toolName: "settld.about",
251
+ toolArgs: {},
252
+ env
253
+ });
254
+ return buildToolResult(result, { tool: "settld.about" });
255
+ }
256
+ });
257
+
258
+ api.registerTool({
259
+ name: "settld_call",
260
+ label: "Settld Tool Call",
261
+ description: "Call any Settld MCP tool by name with JSON arguments.",
262
+ parameters: {
263
+ type: "object",
264
+ additionalProperties: false,
265
+ required: ["tool"],
266
+ properties: {
267
+ tool: {
268
+ type: "string",
269
+ description: "Settld MCP tool name, for example settld.weather_current_paid."
270
+ },
271
+ arguments: {
272
+ type: "object",
273
+ additionalProperties: true,
274
+ description: "Tool arguments as an object."
275
+ },
276
+ argumentsJson: {
277
+ type: "string",
278
+ description: "JSON object string for arguments (use if your model cannot pass object args)."
279
+ }
280
+ }
281
+ },
282
+ async execute(_id, params = {}) {
283
+ const toolName = pickString(params.tool);
284
+ if (!toolName) throw new Error("tool is required");
285
+ if (!toolName.startsWith("settld.")) {
286
+ throw new Error("tool must start with settld.");
287
+ }
288
+ const toolArgs = normalizeToolArguments(params);
289
+ const env = await resolveSettldEnv(api.pluginConfig ?? {});
290
+ const missingMessage = buildMissingEnvMessage(env);
291
+ if (missingMessage) throw new Error(missingMessage);
292
+ const result = await callSettldMcpTool({
293
+ toolName,
294
+ toolArgs,
295
+ env
296
+ });
297
+ return buildToolResult(result, { tool: toolName });
298
+ }
299
+ });
300
+ }
301
+
302
+ export {
303
+ parseSettldServerConfig,
304
+ parseSettldEnvFromServer,
305
+ resolveSettldEnv,
306
+ normalizeToolArguments
307
+ };
@@ -0,0 +1,29 @@
1
+ {
2
+ "id": "settld",
3
+ "name": "Settld",
4
+ "description": "Settld tools for OpenClaw with runtime env auto-discovery from Settld setup.",
5
+ "configSchema": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "baseUrl": {
10
+ "type": "string"
11
+ },
12
+ "tenantId": {
13
+ "type": "string"
14
+ },
15
+ "apiKey": {
16
+ "type": "string"
17
+ },
18
+ "paidToolsBaseUrl": {
19
+ "type": "string"
20
+ },
21
+ "paidToolsAgentPassport": {
22
+ "type": "string"
23
+ },
24
+ "mcpConfigPath": {
25
+ "type": "string"
26
+ }
27
+ }
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "settld",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Settld kernel CLI and local control-plane tooling",
5
5
  "private": false,
6
6
  "type": "module",
@@ -19,6 +19,8 @@
19
19
  "README.md",
20
20
  "Dockerfile",
21
21
  "docker-compose.yml",
22
+ "openclaw.plugin.json",
23
+ "openclaw",
22
24
  "bin",
23
25
  "conformance",
24
26
  "packages/api-sdk/src",
@@ -35,6 +37,11 @@
35
37
  "settld": "bin/settld.js",
36
38
  "settld-mcp": "bin/settld-mcp"
37
39
  },
40
+ "openclaw": {
41
+ "extensions": [
42
+ "./openclaw/index.js"
43
+ ]
44
+ },
38
45
  "exports": {
39
46
  "./mcp": "./scripts/mcp/settld-mcp-server.mjs"
40
47
  },
@@ -299,7 +299,7 @@ function parsePaidToolsAgentPassportFromEnv(env) {
299
299
 
300
300
  function parseMcpArgsFromEnv(env) {
301
301
  const argsJson = typeof env.SETTLD_MCP_ARGS_JSON === "string" ? env.SETTLD_MCP_ARGS_JSON.trim() : "";
302
- if (!argsJson) return ["-y", "settld-mcp"];
302
+ if (!argsJson) return ["-y", "--package", "settld", "settld-mcp"];
303
303
  let parsed;
304
304
  try {
305
305
  parsed = JSON.parse(argsJson);
@@ -874,8 +874,10 @@ function buildHostNextSteps({ host, installedHosts }) {
874
874
  }
875
875
  if (host === "openclaw") {
876
876
  steps.push("Run `openclaw doctor` and ensure OpenClaw itself is onboarded (`openclaw onboard --install-daemon`).");
877
- steps.push("Run `openclaw tui`.");
878
- steps.push("In OpenClaw, ask for a Settld tool call (for example: `run settld.about`).");
877
+ steps.push("Install the Settld OpenClaw plugin: `openclaw plugins install settld@latest`.");
878
+ steps.push("Verify in local mode: `openclaw agent --local --agent main --session-id settld-smoke --message \"Use the tool named settld_about with empty arguments. Return only JSON.\" --json`.");
879
+ steps.push("Run `openclaw tui --session main`.");
880
+ steps.push("If you are in a channel-bound session (e.g. whatsapp:*), switch back to `main` to access Settld tools.");
879
881
  return steps;
880
882
  }
881
883
  if (host === "codex") {
@@ -1020,6 +1022,17 @@ async function runGuidedQuickFlow({
1020
1022
  }
1021
1023
  }
1022
1024
 
1025
+ const paidToolsBaseUrl = String(actionEnv.SETTLD_PAID_TOOLS_BASE_URL ?? "").trim();
1026
+ if (!paidToolsBaseUrl) {
1027
+ summary.firstPaidCall = {
1028
+ ok: false,
1029
+ skipped: true,
1030
+ reason: "SETTLD_PAID_TOOLS_BASE_URL not configured"
1031
+ };
1032
+ summary.warnings.push("first paid call probe skipped (SETTLD_PAID_TOOLS_BASE_URL not configured)");
1033
+ return summary;
1034
+ }
1035
+
1023
1036
  const paidProbe = runMcpPaidCallProbe({ env: actionEnv });
1024
1037
  if (paidProbe.ok) {
1025
1038
  summary.firstPaidCall = { ok: true };
@@ -1958,7 +1971,8 @@ export async function runOnboard({
1958
1971
  if (guided.ran) {
1959
1972
  lines.push(`- wallet fund: ${guided.walletFund?.ok ? "ok" : "not completed"}`);
1960
1973
  lines.push(`- wallet balance watch: ${guided.walletBalanceWatch?.ok ? "ok" : "not completed"}`);
1961
- lines.push(`- first paid call: ${guided.firstPaidCall?.ok ? "ok" : "failed"}`);
1974
+ const firstPaidCallState = guided.firstPaidCall?.skipped ? "skipped" : guided.firstPaidCall?.ok ? "ok" : "failed";
1975
+ lines.push(`- first paid call: ${firstPaidCallState}`);
1962
1976
  } else {
1963
1977
  lines.push("- skipped");
1964
1978
  }
@@ -5262,7 +5262,7 @@ async function handleTenantRuntimeBootstrap(req, res, tenantId) {
5262
5262
  const mcp = {
5263
5263
  schemaVersion: "SettldMcpServerConfig.v1",
5264
5264
  command: "npx",
5265
- args: ["-y", "settld-mcp"],
5265
+ args: ["-y", "--package", "settld", "settld-mcp"],
5266
5266
  env: mcpEnv
5267
5267
  };
5268
5268
  const mcpConfigJson = {
@@ -6212,7 +6212,7 @@ async function handleTenantRuntimeConformanceMatrix(req, res, tenantId) {
6212
6212
  const smokeOk = checkById.get("mcp_smoke")?.status === "pass";
6213
6213
  const paidOk = checkById.get("first_paid_call")?.status === "pass";
6214
6214
  const targetRows = targets.map((target) => {
6215
- const serverConfig = mcpEnv ? { command: "npx", args: ["-y", "settld-mcp"], env: mcpEnv } : null;
6215
+ const serverConfig = mcpEnv ? { command: "npx", args: ["-y", "--package", "settld", "settld-mcp"], env: mcpEnv } : null;
6216
6216
  let config;
6217
6217
  if (target === "openhands") {
6218
6218
  config = {