opentool 0.7.8 → 0.7.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 +58 -3
- package/dist/cli/index.d.ts +3 -2
- package/dist/cli/index.js +131 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +40 -4
- package/dist/index.js +74 -1
- package/dist/index.js.map +1 -1
- package/dist/{validate-BrOwtVYW.d.ts → validate-uetwG5jo.d.ts} +9 -1
- package/package.json +1 -1
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:** [`
|
|
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,62 @@ npx opentool validate
|
|
|
65
67
|
npx opentool dev
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
###
|
|
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: false },
|
|
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
|
+
### Cron schedules (`profile.schedule`)
|
|
119
|
+
|
|
120
|
+
- GET-only tools require `profile.schedule` with a standard 5–6 field cron expression (e.g., `0 12 * * *` or `0 0 ? * MON-FRI *`).
|
|
121
|
+
- Build validates the cron shape and emits `.well-known/opentool/cron.json` capturing each scheduled tool (`toolName`, `toolPath`, `scheduleExpression`). Enabled defaults to `false` even if authors set it to `true` in code. Deployment targets can translate these cron strings to their provider format (e.g., EventBridge) downstream.
|
|
122
|
+
|
|
123
|
+
### Public tools: Add x402 payments (optional)
|
|
69
124
|
|
|
70
|
-
Protect your tools with crypto payments using x402:
|
|
125
|
+
Protect your public tools with crypto payments using x402:
|
|
71
126
|
|
|
72
127
|
```typescript
|
|
73
128
|
// tools/premium-report.ts
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
export { G as GenerateMetadataOptions,
|
|
2
|
+
import { M as Metadata, I as InternalToolDefinition } from '../validate-uetwG5jo.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-uetwG5jo.js';
|
|
4
4
|
import 'zod';
|
|
5
5
|
import '../x402/index.js';
|
|
6
6
|
import 'viem';
|
|
@@ -17,6 +17,7 @@ interface BuildArtifacts {
|
|
|
17
17
|
tools: InternalToolDefinition[];
|
|
18
18
|
compiledTools: CompiledToolArtifact[];
|
|
19
19
|
workflowBundles: WorkflowBundleArtifact | null;
|
|
20
|
+
cronManifestPath: string | null;
|
|
20
21
|
}
|
|
21
22
|
interface CompiledToolArtifact {
|
|
22
23
|
name: string;
|
package/dist/cli/index.js
CHANGED
|
@@ -932,6 +932,40 @@ var HTTP_METHODS = [
|
|
|
932
932
|
"OPTIONS"
|
|
933
933
|
];
|
|
934
934
|
|
|
935
|
+
// src/utils/schedule.ts
|
|
936
|
+
var CRON_WRAPPED_REGEX = /^cron\((.*)\)$/i;
|
|
937
|
+
var CRON_TOKEN_REGEX = /^[A-Za-z0-9*?/,\-#L]+$/;
|
|
938
|
+
function normalizeScheduleExpression(raw, context) {
|
|
939
|
+
const value = raw?.trim();
|
|
940
|
+
if (!value) {
|
|
941
|
+
throw new Error(`${context}: profile.schedule.cron must be a non-empty string`);
|
|
942
|
+
}
|
|
943
|
+
const cronBody = extractCronBody(value);
|
|
944
|
+
const cronFields = cronBody.trim().split(/\s+/).filter(Boolean);
|
|
945
|
+
if (cronFields.length !== 5 && cronFields.length !== 6) {
|
|
946
|
+
throw new Error(`${context}: cron expression must have 5 or 6 fields (got ${cronFields.length})`);
|
|
947
|
+
}
|
|
948
|
+
validateCronTokens(cronFields, context);
|
|
949
|
+
return {
|
|
950
|
+
type: "cron",
|
|
951
|
+
expression: cronFields.join(" ")
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
function extractCronBody(value) {
|
|
955
|
+
const cronMatch = CRON_WRAPPED_REGEX.exec(value);
|
|
956
|
+
if (cronMatch) {
|
|
957
|
+
return (cronMatch[1] ?? "").trim();
|
|
958
|
+
}
|
|
959
|
+
return value;
|
|
960
|
+
}
|
|
961
|
+
function validateCronTokens(fields, context) {
|
|
962
|
+
fields.forEach((token, idx) => {
|
|
963
|
+
if (!CRON_TOKEN_REGEX.test(token)) {
|
|
964
|
+
throw new Error(`${context}: invalid cron token "${token}" at position ${idx + 1}`);
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
935
969
|
// src/cli/validate.ts
|
|
936
970
|
var SUPPORTED_EXTENSIONS = [
|
|
937
971
|
".ts",
|
|
@@ -1008,6 +1042,12 @@ async function loadAndValidateTools(toolsDir, options = {}) {
|
|
|
1008
1042
|
if (fs4.existsSync(tempDir)) {
|
|
1009
1043
|
fs4.rmSync(tempDir, { recursive: true, force: true });
|
|
1010
1044
|
}
|
|
1045
|
+
const kebabCase = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[a-z]+$/;
|
|
1046
|
+
for (const f of files) {
|
|
1047
|
+
if (!kebabCase.test(f)) {
|
|
1048
|
+
throw new Error(`Tool filename must be kebab-case: ${f}`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1011
1051
|
const entryPoints = files.map((file) => path5.join(toolsDir, file));
|
|
1012
1052
|
const { outDir, cleanup } = await transpileWithEsbuild({
|
|
1013
1053
|
entryPoints,
|
|
@@ -1032,6 +1072,35 @@ async function loadAndValidateTools(toolsDir, options = {}) {
|
|
|
1032
1072
|
const inputSchemaRaw = schema ? toJsonSchema(toolName, schema) : void 0;
|
|
1033
1073
|
const inputSchema = normalizeInputSchema(inputSchemaRaw);
|
|
1034
1074
|
const httpHandlersRaw = collectHttpHandlers(toolModule, file);
|
|
1075
|
+
const hasGET = typeof toolModule.GET === "function";
|
|
1076
|
+
const hasPOST = typeof toolModule.POST === "function";
|
|
1077
|
+
const otherMethods = HTTP_METHODS.filter((m) => m !== "GET" && m !== "POST").filter(
|
|
1078
|
+
(m) => typeof toolModule[m] === "function"
|
|
1079
|
+
);
|
|
1080
|
+
if (otherMethods.length > 0) {
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
`${file} must not export ${otherMethods.join(", ")}. Only one of GET or POST is allowed.`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
if (hasGET === hasPOST) {
|
|
1086
|
+
throw new Error(`${file}: export exactly one of GET or POST`);
|
|
1087
|
+
}
|
|
1088
|
+
let normalizedSchedule = null;
|
|
1089
|
+
if (hasGET) {
|
|
1090
|
+
const schedule = toolModule?.profile?.schedule;
|
|
1091
|
+
if (!schedule || typeof schedule?.cron !== "string" || schedule.cron.trim().length === 0) {
|
|
1092
|
+
throw new Error(`${file}: GET tools require profile.schedule { cron }`);
|
|
1093
|
+
}
|
|
1094
|
+
normalizedSchedule = normalizeScheduleExpression(schedule.cron, file);
|
|
1095
|
+
if (typeof schedule.enabled === "boolean") {
|
|
1096
|
+
normalizedSchedule.authoredEnabled = schedule.enabled;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (hasPOST) {
|
|
1100
|
+
if (!schema) {
|
|
1101
|
+
throw new Error(`${file}: POST tools must export a Zod schema as 'schema'`);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1035
1104
|
const httpHandlers = [...httpHandlersRaw];
|
|
1036
1105
|
if (httpHandlers.length === 0) {
|
|
1037
1106
|
throw new Error(
|
|
@@ -1082,7 +1151,9 @@ async function loadAndValidateTools(toolsDir, options = {}) {
|
|
|
1082
1151
|
filename: toBaseName(file),
|
|
1083
1152
|
sourcePath: path5.join(toolsDir, file),
|
|
1084
1153
|
handler: async (params) => adapter(params),
|
|
1085
|
-
payment: paymentExport ?? null
|
|
1154
|
+
payment: paymentExport ?? null,
|
|
1155
|
+
schedule: normalizedSchedule,
|
|
1156
|
+
profileDescription: typeof toolModule?.profile?.description === "string" ? toolModule.profile?.description ?? null : null
|
|
1086
1157
|
};
|
|
1087
1158
|
tools.push(tool);
|
|
1088
1159
|
}
|
|
@@ -1304,6 +1375,11 @@ async function buildProject(options) {
|
|
|
1304
1375
|
projectRoot,
|
|
1305
1376
|
outputDir
|
|
1306
1377
|
});
|
|
1378
|
+
const cronManifestPath = await writeCronManifest({
|
|
1379
|
+
tools,
|
|
1380
|
+
compiledTools,
|
|
1381
|
+
outputDir
|
|
1382
|
+
});
|
|
1307
1383
|
const workflowBundles = await buildWorkflowsIfPresent({
|
|
1308
1384
|
projectRoot,
|
|
1309
1385
|
outputDir
|
|
@@ -1326,7 +1402,8 @@ async function buildProject(options) {
|
|
|
1326
1402
|
defaultsApplied,
|
|
1327
1403
|
tools,
|
|
1328
1404
|
compiledTools,
|
|
1329
|
-
workflowBundles
|
|
1405
|
+
workflowBundles,
|
|
1406
|
+
cronManifestPath
|
|
1330
1407
|
};
|
|
1331
1408
|
}
|
|
1332
1409
|
async function emitTools(tools, config) {
|
|
@@ -1469,6 +1546,55 @@ async function writeMcpServer(options) {
|
|
|
1469
1546
|
fs4.writeFileSync(serverPath, serverCode);
|
|
1470
1547
|
fs4.chmodSync(serverPath, 493);
|
|
1471
1548
|
}
|
|
1549
|
+
function writeCronManifest(options) {
|
|
1550
|
+
const scheduledTools = options.tools.filter((tool) => tool.schedule?.expression);
|
|
1551
|
+
const manifestDir = path5.join(options.outputDir, ".well-known", "opentool");
|
|
1552
|
+
const manifestPath = path5.join(manifestDir, "cron.json");
|
|
1553
|
+
if (scheduledTools.length === 0) {
|
|
1554
|
+
if (fs4.existsSync(manifestPath)) {
|
|
1555
|
+
fs4.rmSync(manifestPath);
|
|
1556
|
+
}
|
|
1557
|
+
return null;
|
|
1558
|
+
}
|
|
1559
|
+
const entries = scheduledTools.map((tool) => {
|
|
1560
|
+
const schedule = tool.schedule;
|
|
1561
|
+
if (!schedule) {
|
|
1562
|
+
throw new Error(`Internal error: missing schedule for tool ${tool.filename}`);
|
|
1563
|
+
}
|
|
1564
|
+
const compiled = options.compiledTools.find(
|
|
1565
|
+
(artifact) => artifact.filename === tool.filename
|
|
1566
|
+
);
|
|
1567
|
+
if (!compiled) {
|
|
1568
|
+
throw new Error(`Internal error: missing compiled artifact for ${tool.filename}`);
|
|
1569
|
+
}
|
|
1570
|
+
const toolName = tool.metadata?.name ?? tool.filename;
|
|
1571
|
+
const description = tool.metadata?.description ?? tool.profileDescription ?? void 0;
|
|
1572
|
+
const payloadPath = compiled.modulePath.replace(/\\/g, "/");
|
|
1573
|
+
const entry = {
|
|
1574
|
+
toolName,
|
|
1575
|
+
scheduleType: schedule.type,
|
|
1576
|
+
scheduleExpression: schedule.expression,
|
|
1577
|
+
enabledDefault: false,
|
|
1578
|
+
...schedule.authoredEnabled !== void 0 ? { authoredEnabled: schedule.authoredEnabled } : {},
|
|
1579
|
+
payload: {
|
|
1580
|
+
toolPath: payloadPath,
|
|
1581
|
+
httpMethod: "GET"
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
if (description !== void 0) {
|
|
1585
|
+
entry.description = description;
|
|
1586
|
+
}
|
|
1587
|
+
return entry;
|
|
1588
|
+
});
|
|
1589
|
+
const manifest = {
|
|
1590
|
+
version: 1,
|
|
1591
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1592
|
+
entries
|
|
1593
|
+
};
|
|
1594
|
+
fs4.mkdirSync(manifestDir, { recursive: true });
|
|
1595
|
+
fs4.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1596
|
+
return manifestPath;
|
|
1597
|
+
}
|
|
1472
1598
|
function logBuildSummary(artifacts, options) {
|
|
1473
1599
|
const end = timestamp();
|
|
1474
1600
|
console.log(`[${end}] Build completed successfully!`);
|
|
@@ -1485,6 +1611,9 @@ function logBuildSummary(artifacts, options) {
|
|
|
1485
1611
|
console.log(` - ${tool.name} [${methods}]${walletBadge}`);
|
|
1486
1612
|
});
|
|
1487
1613
|
console.log(" \u2022 metadata.json (registry artifact)");
|
|
1614
|
+
if (artifacts.cronManifestPath) {
|
|
1615
|
+
console.log(" \u2022 .well-known/opentool/cron.json (cron manifest)");
|
|
1616
|
+
}
|
|
1488
1617
|
if (artifacts.workflowBundles) {
|
|
1489
1618
|
console.log(" \u2022 .well-known/workflow/v1/ (workflow bundles)");
|
|
1490
1619
|
console.log(" - flow.js");
|