opentool 0.7.7 → 0.7.9

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 CHANGED
@@ -5,12 +5,14 @@
5
5
 
6
6
  Build serverless TypeScript tools that work with AI assistants, handle crypto payments, and deploy to AWS Lambda automatically.
7
7
 
8
- **For LLMs/AI Code Generation:** [`dist/opentool-context.ts`](./scripts/build-context.ts)
8
+ **For LLMs/AI Code Generation:** [`context/opentool-context.ts`](./context/opentool-context.ts)
9
9
 
10
10
  ## What is it?
11
11
 
12
12
  OpenTool lets you write simple TypeScript functions that can be called by other agents, monetized with crypto payments, and deployed as serverless functions. It handles the boring stuff like:
13
13
 
14
+ Tools are either Public or Private. Public tools are accessible to the public and are monetized with crypto payments using x402. Private tools are accessible only to the app developer and are mainly for trading and onchain interaction use cases.
15
+
14
16
  ## Installation
15
17
 
16
18
  ```bash
@@ -65,9 +67,57 @@ npx opentool validate
65
67
  npx opentool dev
66
68
  ```
67
69
 
68
- ### 4. Add x402 payments (optional)
70
+ ### Private tools: GET-only and POST-only
71
+
72
+ For private tools, say for internal trading apps:
73
+
74
+ - GET-only (scheduled default profile)
75
+ - POST-only (one-off, parameterized with Zod)
76
+
77
+ GET-only (scheduled default)
78
+
79
+ ```typescript
80
+ // tools/aave-stake.ts
81
+ export const profile = {
82
+ description: "Stake 100 USDC daily at 12:00 UTC",
83
+ fixedAmount: "100",
84
+ tokenSymbol: "USDC",
85
+ schedule: { cron: "0 12 * * *", enabled: true },
86
+ limits: { concurrency: 1, dailyCap: 1 },
87
+ };
88
+
89
+ export async function GET(_req: Request) {
90
+ const amount = profile.fixedAmount;
91
+ return Response.json({
92
+ ok: true,
93
+ action: "stake",
94
+ amount,
95
+ token: profile.tokenSymbol,
96
+ });
97
+ }
98
+ ```
99
+
100
+ POST-only (one-off)
101
+
102
+ ```typescript
103
+ // tools/aave-unstake.ts
104
+ import { z } from "zod";
105
+
106
+ export const schema = z.object({
107
+ amount: z.string(),
108
+ token: z.string().default("USDC"),
109
+ });
110
+
111
+ export async function POST(req: Request) {
112
+ const body = await req.json();
113
+ const { amount, token } = schema.parse(body);
114
+ return Response.json({ ok: true, action: "unstake", amount, token });
115
+ }
116
+ ```
117
+
118
+ ### Public tools: Add x402 payments (optional)
69
119
 
70
- Protect your tools with crypto payments using x402:
120
+ Protect your public tools with crypto payments using x402:
71
121
 
72
122
  ```typescript
73
123
  // tools/premium-report.ts
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { h as Metadata, I as InternalToolDefinition } from '../validate-BrOwtVYW.js';
3
- export { G as GenerateMetadataOptions, j as GenerateMetadataResult, V as ValidateOptions, g as generateMetadata, a as generateMetadataCommand, l as loadAndValidateTools, v as validateCommand, k as validateFullCommand } from '../validate-BrOwtVYW.js';
2
+ import { M as Metadata, I as InternalToolDefinition } from '../validate-BBjyq5nS.js';
3
+ export { G as GenerateMetadataOptions, a as GenerateMetadataResult, V as ValidateOptions, b as generateMetadata, g as generateMetadataCommand, l as loadAndValidateTools, v as validateCommand, c as validateFullCommand } from '../validate-BBjyq5nS.js';
4
4
  import 'zod';
5
5
  import '../x402/index.js';
6
6
  import 'viem';
package/dist/cli/index.js CHANGED
@@ -535,6 +535,7 @@ function extractX402Attempt(request) {
535
535
  }
536
536
  async function verifyX402Payment(attempt, definition, options = {}) {
537
537
  const fetchImpl = options.fetchImpl ?? fetch;
538
+ const timeout = options.timeout ?? 25e3;
538
539
  const facilitator = definition.facilitator;
539
540
  const verifierUrl = new URL(
540
541
  facilitator.verifyPath ?? "/verify",
@@ -550,13 +551,18 @@ async function verifyX402Payment(attempt, definition, options = {}) {
550
551
  };
551
552
  console.log("[x402] Calling facilitator /verify", {
552
553
  url: verifierUrl,
553
- bodyPreview: JSON.stringify(verifyBody).substring(0, 200)
554
- });
555
- const verifyResponse = await fetchImpl(verifierUrl, {
556
- method: "POST",
557
- headers,
558
- body: JSON.stringify(verifyBody)
554
+ fullBody: JSON.stringify(verifyBody, null, 2)
559
555
  });
556
+ const verifyResponse = await Promise.race([
557
+ fetchImpl(verifierUrl, {
558
+ method: "POST",
559
+ headers,
560
+ body: JSON.stringify(verifyBody)
561
+ }),
562
+ new Promise(
563
+ (_, reject) => setTimeout(() => reject(new Error(`Verification timeout after ${timeout}ms`)), timeout)
564
+ )
565
+ ]);
560
566
  console.log("[x402] Facilitator /verify response", { status: verifyResponse.status });
561
567
  if (!verifyResponse.ok) {
562
568
  const errorText = await verifyResponse.text().catch(() => "");
@@ -586,27 +592,39 @@ async function verifyX402Payment(attempt, definition, options = {}) {
586
592
  ensureTrailingSlash(facilitator.url)
587
593
  ).toString();
588
594
  try {
589
- console.log("[x402] Calling facilitator /settle", { url: settleUrl });
590
- const settleResponse = await fetchImpl(settleUrl, {
591
- method: "POST",
592
- headers,
593
- body: JSON.stringify({
594
- x402Version: attempt.payload.x402Version,
595
- paymentPayload: attempt.payload,
596
- paymentRequirements: requirement
597
- })
595
+ const settleBody = {
596
+ x402Version: attempt.payload.x402Version,
597
+ paymentPayload: attempt.payload,
598
+ paymentRequirements: requirement
599
+ };
600
+ console.log("[x402] Calling facilitator /settle", {
601
+ url: settleUrl,
602
+ bodyPreview: JSON.stringify(settleBody).substring(0, 300)
598
603
  });
604
+ const settleResponse = await Promise.race([
605
+ fetchImpl(settleUrl, {
606
+ method: "POST",
607
+ headers,
608
+ body: JSON.stringify(settleBody)
609
+ }),
610
+ new Promise(
611
+ (_, reject) => setTimeout(() => reject(new Error(`Settlement timeout after ${timeout}ms`)), timeout)
612
+ )
613
+ ]);
599
614
  console.log("[x402] Facilitator /settle response", { status: settleResponse.status });
600
615
  if (!settleResponse.ok) {
616
+ const errorText = await settleResponse.text().catch(() => "");
617
+ console.error("[x402] Facilitator /settle error", { status: settleResponse.status, body: errorText });
601
618
  return {
602
619
  success: false,
603
620
  failure: {
604
- reason: `Facilitator settlement failed: ${settleResponse.status}`,
621
+ reason: `Facilitator settlement failed: ${settleResponse.status}${errorText ? ` - ${errorText}` : ""}`,
605
622
  code: "settlement_failed"
606
623
  }
607
624
  };
608
625
  }
609
626
  const settlePayload = await settleResponse.json();
627
+ console.log("[x402] Facilitator /settle success", { txHash: settlePayload.txHash });
610
628
  if (settlePayload.txHash) {
611
629
  responseHeaders[HEADER_PAYMENT_RESPONSE] = JSON.stringify({
612
630
  settled: true,
@@ -614,6 +632,7 @@ async function verifyX402Payment(attempt, definition, options = {}) {
614
632
  });
615
633
  }
616
634
  } catch (error) {
635
+ console.error("[x402] Settlement exception", { error: error instanceof Error ? error.message : String(error) });
617
636
  return {
618
637
  success: false,
619
638
  failure: {
@@ -989,6 +1008,12 @@ async function loadAndValidateTools(toolsDir, options = {}) {
989
1008
  if (fs4.existsSync(tempDir)) {
990
1009
  fs4.rmSync(tempDir, { recursive: true, force: true });
991
1010
  }
1011
+ const kebabCase = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z]+$/;
1012
+ for (const f of files) {
1013
+ if (!kebabCase.test(f)) {
1014
+ throw new Error(`Tool filename must be kebab-case: ${f}`);
1015
+ }
1016
+ }
992
1017
  const entryPoints = files.map((file) => path5.join(toolsDir, file));
993
1018
  const { outDir, cleanup } = await transpileWithEsbuild({
994
1019
  entryPoints,
@@ -1013,6 +1038,30 @@ async function loadAndValidateTools(toolsDir, options = {}) {
1013
1038
  const inputSchemaRaw = schema ? toJsonSchema(toolName, schema) : void 0;
1014
1039
  const inputSchema = normalizeInputSchema(inputSchemaRaw);
1015
1040
  const httpHandlersRaw = collectHttpHandlers(toolModule, file);
1041
+ const hasGET = typeof toolModule.GET === "function";
1042
+ const hasPOST = typeof toolModule.POST === "function";
1043
+ const otherMethods = HTTP_METHODS.filter((m) => m !== "GET" && m !== "POST").filter(
1044
+ (m) => typeof toolModule[m] === "function"
1045
+ );
1046
+ if (otherMethods.length > 0) {
1047
+ throw new Error(
1048
+ `${file} must not export ${otherMethods.join(", ")}. Only one of GET or POST is allowed.`
1049
+ );
1050
+ }
1051
+ if (hasGET === hasPOST) {
1052
+ throw new Error(`${file}: export exactly one of GET or POST`);
1053
+ }
1054
+ if (hasGET) {
1055
+ const schedule = toolModule?.profile?.schedule;
1056
+ if (!schedule || typeof schedule?.cron !== "string" || schedule.cron.trim().length === 0) {
1057
+ throw new Error(`${file}: GET tools require profile.schedule { cron }`);
1058
+ }
1059
+ }
1060
+ if (hasPOST) {
1061
+ if (!schema) {
1062
+ throw new Error(`${file}: POST tools must export a Zod schema as 'schema'`);
1063
+ }
1064
+ }
1016
1065
  const httpHandlers = [...httpHandlersRaw];
1017
1066
  if (httpHandlers.length === 0) {
1018
1067
  throw new Error(