agentspend 0.2.1 → 0.3.2
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 +68 -11
- package/SKILL.md +54 -101
- package/dist/cli.js +11 -32
- package/dist/commands/configure.js +3 -42
- package/dist/commands/pay.js +2 -22
- package/dist/commands/search.js +1 -1
- package/dist/commands/use.js +117 -0
- package/dist/lib/api.js +2 -8
- package/dist/lib/auth-flow.js +1 -4
- package/dist/lib/configure-flow.js +47 -0
- package/dist/lib/local-execution.js +74 -0
- package/dist/lib/use-cloud-result.js +54 -0
- package/dist/mcp/server.js +49 -0
- package/dist/mcp/shared.js +129 -0
- package/dist/mcp/tools/configure.js +12 -0
- package/dist/mcp/tools/pay.js +28 -0
- package/dist/mcp/tools/search.js +10 -0
- package/dist/mcp/tools/status.js +11 -0
- package/dist/mcp/tools/use.js +60 -0
- package/dist/mcp-server.js +6 -0
- package/dist/openclaw-plugin/hooks/prompt-routing.js +12 -0
- package/dist/openclaw-plugin/index.js +17 -0
- package/dist/openclaw-plugin/shared.js +132 -0
- package/dist/openclaw-plugin/tools/configure.js +23 -0
- package/dist/openclaw-plugin/tools/pay.js +60 -0
- package/dist/openclaw-plugin/tools/search.js +24 -0
- package/dist/openclaw-plugin/tools/status.js +22 -0
- package/dist/openclaw-plugin/tools/use.js +80 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +12 -5
- package/dist/commands/check.js +0 -40
- package/dist/commands/setup.js +0 -36
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { normalizeMethod } from "../../lib/request-options.js";
|
|
2
|
+
import { optionalStringRecord, requiredString, resolveApiKeyForTool, toJsonValue, toolSuccess, withToolErrorHandling, } from "../shared.js";
|
|
3
|
+
const MAX_BODY_CHARS = 6000;
|
|
4
|
+
const BODY_PREVIEW_CHARS = 1200;
|
|
5
|
+
function compactResponseBody(body) {
|
|
6
|
+
const jsonBody = toJsonValue(body);
|
|
7
|
+
const serialized = JSON.stringify(jsonBody);
|
|
8
|
+
if (!serialized || serialized.length <= MAX_BODY_CHARS) {
|
|
9
|
+
return { body: jsonBody, body_omitted: false };
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
body: {
|
|
13
|
+
note: "Response body omitted because it is large (common for base64 media payloads).",
|
|
14
|
+
size_chars: serialized.length,
|
|
15
|
+
preview: serialized.slice(0, BODY_PREVIEW_CHARS),
|
|
16
|
+
},
|
|
17
|
+
body_omitted: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function createPayTool(apiClient) {
|
|
21
|
+
return {
|
|
22
|
+
name: "agentspend_pay",
|
|
23
|
+
description: "Execute a paid API request through AgentSpend.",
|
|
24
|
+
parameters: {
|
|
25
|
+
type: "object",
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
properties: {
|
|
28
|
+
url: { type: "string", format: "uri" },
|
|
29
|
+
method: { type: "string", minLength: 1 },
|
|
30
|
+
headers: {
|
|
31
|
+
type: "object",
|
|
32
|
+
additionalProperties: { type: "string" },
|
|
33
|
+
},
|
|
34
|
+
body: {},
|
|
35
|
+
},
|
|
36
|
+
required: ["url", "method"],
|
|
37
|
+
},
|
|
38
|
+
execute: async (_toolCallId, args) => withToolErrorHandling(async () => {
|
|
39
|
+
const url = requiredString(args.url, "url");
|
|
40
|
+
const method = normalizeMethod(requiredString(args.method, "method"));
|
|
41
|
+
const headers = optionalStringRecord(args.headers, "headers");
|
|
42
|
+
const body = args.body;
|
|
43
|
+
const apiKey = await resolveApiKeyForTool(apiClient);
|
|
44
|
+
const response = await apiClient.pay(apiKey, {
|
|
45
|
+
url,
|
|
46
|
+
method,
|
|
47
|
+
headers,
|
|
48
|
+
body,
|
|
49
|
+
});
|
|
50
|
+
const compactBody = compactResponseBody(response.body);
|
|
51
|
+
return toolSuccess({
|
|
52
|
+
status: response.status,
|
|
53
|
+
body: compactBody.body,
|
|
54
|
+
body_omitted: compactBody.body_omitted,
|
|
55
|
+
charged_usd: response.payment?.charged_usd ?? null,
|
|
56
|
+
remaining_budget_usd: response.payment?.remaining_budget_usd ?? null,
|
|
57
|
+
});
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { requiredString, resolveApiKeyForTool, toolSuccess, withToolErrorHandling, } from "../shared.js";
|
|
2
|
+
export function createSearchTool(apiClient) {
|
|
3
|
+
return {
|
|
4
|
+
name: "agentspend_search",
|
|
5
|
+
description: "Search AgentSpend services catalog by keyword.",
|
|
6
|
+
parameters: {
|
|
7
|
+
type: "object",
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
properties: {
|
|
10
|
+
query: { type: "string", minLength: 1 },
|
|
11
|
+
},
|
|
12
|
+
required: ["query"],
|
|
13
|
+
},
|
|
14
|
+
execute: async (_toolCallId, args) => withToolErrorHandling(async () => {
|
|
15
|
+
const query = requiredString(args.query, "query");
|
|
16
|
+
const apiKey = await resolveApiKeyForTool(apiClient);
|
|
17
|
+
const response = await apiClient.search(apiKey, query);
|
|
18
|
+
return toolSuccess({
|
|
19
|
+
query: response.query,
|
|
20
|
+
services: response.services,
|
|
21
|
+
});
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolveApiKeyForTool, toJsonValue, toolSuccess, withToolErrorHandling, } from "../shared.js";
|
|
2
|
+
export function createStatusTool(apiClient) {
|
|
3
|
+
return {
|
|
4
|
+
name: "agentspend_status",
|
|
5
|
+
description: "Get weekly budget, current spend, remaining budget, and recent charges.",
|
|
6
|
+
parameters: {
|
|
7
|
+
type: "object",
|
|
8
|
+
additionalProperties: false,
|
|
9
|
+
properties: {},
|
|
10
|
+
},
|
|
11
|
+
execute: async () => withToolErrorHandling(async () => {
|
|
12
|
+
const apiKey = await resolveApiKeyForTool(apiClient);
|
|
13
|
+
const response = await apiClient.status(apiKey);
|
|
14
|
+
return toolSuccess({
|
|
15
|
+
weekly_budget_usd: response.weekly_budget_usd,
|
|
16
|
+
spent_this_week_usd: response.spent_this_week_usd,
|
|
17
|
+
remaining_budget_usd: response.remaining_budget_usd,
|
|
18
|
+
recent_charges: toJsonValue(response.recent_charges),
|
|
19
|
+
});
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ApiError } from "../../lib/api.js";
|
|
2
|
+
import { formatUseCloudResult } from "../../lib/use-cloud-result.js";
|
|
3
|
+
import { normalizeMethod } from "../../lib/request-options.js";
|
|
4
|
+
import { optionalStringRecord, requiredString, resolveApiKeyForTool, toolSuccess, withToolErrorHandling, } from "../shared.js";
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null;
|
|
7
|
+
}
|
|
8
|
+
function asPaymentMethodRequiredResult(error) {
|
|
9
|
+
if (error.status !== 403 || !isRecord(error.body)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (error.body.code !== "PAYMENT_METHOD_REQUIRED") {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
mode: "action_required",
|
|
17
|
+
code: "PAYMENT_METHOD_REQUIRED",
|
|
18
|
+
message: typeof error.body.message === "string"
|
|
19
|
+
? error.body.message
|
|
20
|
+
: "Payment method required. Run agentspend_configure and complete billing setup.",
|
|
21
|
+
configure_url: typeof error.body.configure_url === "string" ? error.body.configure_url : null,
|
|
22
|
+
details: isRecord(error.body.details) ? error.body.details : null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function createUseTool(apiClient) {
|
|
26
|
+
return {
|
|
27
|
+
name: "agentspend_use",
|
|
28
|
+
description: "Call a URL through AgentSpend.",
|
|
29
|
+
parameters: {
|
|
30
|
+
type: "object",
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
properties: {
|
|
33
|
+
url: { type: "string", minLength: 1 },
|
|
34
|
+
method: { type: "string", minLength: 1 },
|
|
35
|
+
headers: {
|
|
36
|
+
type: "object",
|
|
37
|
+
additionalProperties: { type: "string" },
|
|
38
|
+
},
|
|
39
|
+
body: {},
|
|
40
|
+
},
|
|
41
|
+
required: ["url"],
|
|
42
|
+
},
|
|
43
|
+
execute: async (_toolCallId, args) => withToolErrorHandling(async () => {
|
|
44
|
+
const url = requiredString(args.url, "url");
|
|
45
|
+
const methodRaw = args.method;
|
|
46
|
+
const method = methodRaw === undefined ? undefined : normalizeMethod(requiredString(methodRaw, "method"));
|
|
47
|
+
const headers = optionalStringRecord(args.headers, "headers");
|
|
48
|
+
const body = args.body;
|
|
49
|
+
const apiKey = await resolveApiKeyForTool(apiClient);
|
|
50
|
+
let response;
|
|
51
|
+
try {
|
|
52
|
+
response = await apiClient.use(apiKey, {
|
|
53
|
+
url,
|
|
54
|
+
method,
|
|
55
|
+
headers,
|
|
56
|
+
body,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof ApiError) {
|
|
61
|
+
const paymentRequiredResult = asPaymentMethodRequiredResult(error);
|
|
62
|
+
if (paymentRequiredResult) {
|
|
63
|
+
return toolSuccess(paymentRequiredResult);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
if (response.mode === "cloud_http_result") {
|
|
69
|
+
return toolSuccess(formatUseCloudResult(response));
|
|
70
|
+
}
|
|
71
|
+
return toolSuccess({
|
|
72
|
+
mode: response.mode,
|
|
73
|
+
code: response.code,
|
|
74
|
+
message: response.message,
|
|
75
|
+
configure_url: response.configure_url ?? null,
|
|
76
|
+
details: response.details ?? null,
|
|
77
|
+
});
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentspend",
|
|
3
|
-
"version": "0.2
|
|
4
|
-
"description": "AgentSpend CLI for
|
|
5
|
-
"files": ["dist", "SKILL.md"],
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "AgentSpend CLI for agent access to APIs",
|
|
5
|
+
"files": ["dist", "SKILL.md", "openclaw.plugin.json"],
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
|
-
"agentspend": "dist/index.js"
|
|
8
|
+
"agentspend": "dist/index.js",
|
|
9
|
+
"agentspend-mcp": "dist/mcp-server.js"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"build": "tsc -p tsconfig.json",
|
|
12
13
|
"dev": "tsx src/index.ts",
|
|
13
14
|
"dev:local": "tsx src/dev-index.ts",
|
|
14
|
-
"
|
|
15
|
+
"dev:mcp": "tsx src/mcp-server.ts",
|
|
16
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
17
|
+
"skills:validate": "node scripts/validate-skills-manifest.mjs"
|
|
18
|
+
},
|
|
19
|
+
"openclaw": {
|
|
20
|
+
"extensions": ["./dist/openclaw-plugin/index.js"]
|
|
15
21
|
},
|
|
16
22
|
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.18.0",
|
|
17
24
|
"bcryptjs": "^2.4.3",
|
|
18
25
|
"commander": "^12.1.0"
|
|
19
26
|
},
|
package/dist/commands/check.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { ApiError } from "../lib/api.js";
|
|
2
|
-
import { resolveApiKeyWithAutoClaim } from "../lib/auth-flow.js";
|
|
3
|
-
import { formatUsd, usd6ToUsd } from "../lib/output.js";
|
|
4
|
-
import { normalizeMethod, parseBody, parseHeaders } from "../lib/request-options.js";
|
|
5
|
-
export async function runCheck(apiClient, url, options) {
|
|
6
|
-
const apiKey = await resolveApiKeyWithAutoClaim(apiClient);
|
|
7
|
-
try {
|
|
8
|
-
const response = await apiClient.check(apiKey, {
|
|
9
|
-
url,
|
|
10
|
-
method: normalizeMethod(options.method),
|
|
11
|
-
headers: parseHeaders(options.header),
|
|
12
|
-
body: parseBody(options.body),
|
|
13
|
-
});
|
|
14
|
-
if (response.free) {
|
|
15
|
-
if ((response.status ?? 200) >= 400) {
|
|
16
|
-
console.log(`No payment required, but endpoint returned status ${response.status}.`);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
console.log("This endpoint is free.");
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
const policyUsd = response.price_usd ?? (typeof response.price_usd6 === "number" ? usd6ToUsd(response.price_usd6) : null);
|
|
23
|
-
const price = policyUsd !== null ? formatUsd(policyUsd) : "unavailable";
|
|
24
|
-
const description = response.description ?? "unavailable";
|
|
25
|
-
console.log(`Price: ${price}`);
|
|
26
|
-
console.log(`Description: ${description}`);
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
if (error instanceof ApiError) {
|
|
31
|
-
const body = error.body;
|
|
32
|
-
if (error.status === 502 && body?.code === "UNSUPPORTED_PAYMENT_REQUIRED_FORMAT") {
|
|
33
|
-
const headers = (body.details?.header_names ?? []).join(", ");
|
|
34
|
-
console.error(`Endpoint returned 402 with an unsupported payment format. Headers seen: ${headers || "none"}.`);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
}
|
package/dist/commands/setup.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import bcrypt from "bcryptjs";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
import { saveCredentials } from "../lib/credentials.js";
|
|
4
|
-
const POLL_INTERVAL_MS = 2_000;
|
|
5
|
-
const SETUP_TIMEOUT_MS = 10 * 60 * 1000;
|
|
6
|
-
function sleep(ms) {
|
|
7
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
-
}
|
|
9
|
-
function generateApiKey() {
|
|
10
|
-
return `sk_agent_${crypto.randomBytes(32).toString("hex")}`;
|
|
11
|
-
}
|
|
12
|
-
export async function runSetup(apiClient) {
|
|
13
|
-
const setup = await apiClient.createSetup();
|
|
14
|
-
console.log(`Open this URL to complete setup:\n${setup.setup_url}\n`);
|
|
15
|
-
console.log("Waiting for setup to complete...");
|
|
16
|
-
const started = Date.now();
|
|
17
|
-
while (Date.now() - started < SETUP_TIMEOUT_MS) {
|
|
18
|
-
const status = await apiClient.getSetupStatus(setup.setup_id);
|
|
19
|
-
if (status.status === "ready") {
|
|
20
|
-
const apiKey = generateApiKey();
|
|
21
|
-
const apiKeyHash = await bcrypt.hash(apiKey, 12);
|
|
22
|
-
await apiClient.claimSetup(setup.setup_id, apiKeyHash);
|
|
23
|
-
await saveCredentials(apiKey);
|
|
24
|
-
console.log("Ready! Your agent can now spend.");
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
if (status.status === "claimed") {
|
|
28
|
-
throw new Error("This setup session was already claimed. Run agentspend setup again.");
|
|
29
|
-
}
|
|
30
|
-
if (status.status === "expired") {
|
|
31
|
-
throw new Error("Setup timed out. Run agentspend setup again.");
|
|
32
|
-
}
|
|
33
|
-
await sleep(POLL_INTERVAL_MS);
|
|
34
|
-
}
|
|
35
|
-
throw new Error("Setup timed out. Run agentspend setup again.");
|
|
36
|
-
}
|