caddie-mcp 0.1.0
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 +152 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +134 -0
- package/dist/guide.d.ts +2 -0
- package/dist/guide.js +351 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +107 -0
- package/dist/setup.d.ts +9 -0
- package/dist/setup.js +429 -0
- package/dist/sse-client.d.ts +65 -0
- package/dist/sse-client.js +101 -0
- package/dist/tools/ask-widget-types.generated.d.ts +2 -0
- package/dist/tools/ask-widget-types.generated.js +34 -0
- package/dist/tools/blockchain.d.ts +2 -0
- package/dist/tools/blockchain.js +52 -0
- package/dist/tools/caddie.d.ts +2 -0
- package/dist/tools/caddie.js +148 -0
- package/dist/tools/catalog.d.ts +2 -0
- package/dist/tools/catalog.js +151 -0
- package/dist/tools/connectors.d.ts +2 -0
- package/dist/tools/connectors.js +200 -0
- package/dist/tools/database.d.ts +2 -0
- package/dist/tools/database.js +66 -0
- package/dist/tools/lookups.d.ts +2 -0
- package/dist/tools/lookups.js +282 -0
- package/dist/tools/org.d.ts +2 -0
- package/dist/tools/org.js +29 -0
- package/dist/tools/parse-caddie-blocks.d.ts +20 -0
- package/dist/tools/parse-caddie-blocks.js +305 -0
- package/dist/tools/runs.d.ts +2 -0
- package/dist/tools/runs.js +248 -0
- package/dist/tools/shared.d.ts +13 -0
- package/dist/tools/shared.js +27 -0
- package/dist/tools/workflows.d.ts +2 -0
- package/dist/tools/workflows.js +226 -0
- package/dist/types.d.ts +157 -0
- package/dist/types.js +2 -0
- package/package.json +37 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { request, truncateResponse } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Named lookup tools — convenience wrappers over the action proxy.
|
|
5
|
+
* Each calls POST /v1/action-proxy/{type}/query — no CU cost, read-only.
|
|
6
|
+
*
|
|
7
|
+
* ## Adding a new lookup tool
|
|
8
|
+
* 1. Identify the B3OS action type (e.g. "coingecko-get-token-data")
|
|
9
|
+
* 2. Add a registerTool call below
|
|
10
|
+
* 3. Use queryAction(actionType, payload) helper
|
|
11
|
+
*/
|
|
12
|
+
async function queryAction(actionType, payload) {
|
|
13
|
+
const result = await request(`/v1/action-proxy/${actionType}/query`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
body: { payload },
|
|
16
|
+
});
|
|
17
|
+
return truncateResponse(JSON.stringify(result, null, 2));
|
|
18
|
+
}
|
|
19
|
+
export function registerLookupTools(s) {
|
|
20
|
+
// ── 1. Token Lookup ─────────────────────────────────────────────────
|
|
21
|
+
s.registerTool("b3os_token_lookup", {
|
|
22
|
+
description: `Look up token information by on-chain address or CoinGecko ID. Read-only, no CU cost.
|
|
23
|
+
|
|
24
|
+
USE CASES:
|
|
25
|
+
- Get token metadata (name, symbol, market cap, image) by contract address on a chain
|
|
26
|
+
- Get current USD price by CoinGecko coin ID (e.g. "bitcoin", "ethereum")
|
|
27
|
+
|
|
28
|
+
INPUTS (provide ONE of these combinations):
|
|
29
|
+
- coinId → fetches price via CoinGecko ID (e.g. "bitcoin", "uniswap", "aave")
|
|
30
|
+
- network + address → fetches full token data by on-chain contract address
|
|
31
|
+
network values: "ethereum", "base", "arbitrum-one", "polygon-pos", "optimistic-ethereum", "avalanche", "bsc"
|
|
32
|
+
|
|
33
|
+
EXAMPLES:
|
|
34
|
+
- { coinId: "bitcoin" } → current BTC/USD price
|
|
35
|
+
- { network: "base", address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } → USDC on Base`,
|
|
36
|
+
inputSchema: {
|
|
37
|
+
network: z.string().optional().describe('Chain network slug (e.g. "ethereum", "base", "arbitrum-one")'),
|
|
38
|
+
address: z.string().optional().describe("Token contract address (hex, checksummed or lowercase)"),
|
|
39
|
+
coinId: z.string().optional().describe('CoinGecko coin ID (e.g. "bitcoin", "ethereum", "uniswap")'),
|
|
40
|
+
},
|
|
41
|
+
}, async ({ network, address, coinId }) => {
|
|
42
|
+
try {
|
|
43
|
+
if (coinId) {
|
|
44
|
+
const text = await queryAction("coingecko-get-token-price", {
|
|
45
|
+
coinIds: [coinId],
|
|
46
|
+
vsCurrencies: ["usd"],
|
|
47
|
+
});
|
|
48
|
+
return { content: [{ type: "text", text }] };
|
|
49
|
+
}
|
|
50
|
+
if (network && address) {
|
|
51
|
+
const text = await queryAction("coingecko-get-token-data", { network, address });
|
|
52
|
+
return { content: [{ type: "text", text }] };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: "Provide either `coinId` for a price lookup, or both `network` and `address` for full token data.",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return {
|
|
65
|
+
content: [
|
|
66
|
+
{
|
|
67
|
+
type: "text",
|
|
68
|
+
text: `Token lookup failed: ${err instanceof Error ? err.message : err}`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// ── 2. Price Lookup ─────────────────────────────────────────────────
|
|
75
|
+
s.registerTool("b3os_price_lookup", {
|
|
76
|
+
description: `Get current prices for one or more tokens by CoinGecko ID. Read-only, no CU cost.
|
|
77
|
+
|
|
78
|
+
Batch-friendly: pass multiple coin IDs in a single call.
|
|
79
|
+
|
|
80
|
+
EXAMPLES:
|
|
81
|
+
- { coinIds: ["bitcoin", "ethereum"] } → BTC and ETH prices in USD
|
|
82
|
+
- { coinIds: ["aave"], vsCurrencies: ["usd", "eur"] } → AAVE in USD and EUR`,
|
|
83
|
+
inputSchema: {
|
|
84
|
+
coinIds: z.array(z.string()).describe('Array of CoinGecko coin IDs (e.g. ["bitcoin", "ethereum"])'),
|
|
85
|
+
vsCurrencies: z
|
|
86
|
+
.array(z.string())
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Quote currencies (default: ["usd"]). e.g. ["usd", "eur", "btc"]'),
|
|
89
|
+
},
|
|
90
|
+
}, async ({ coinIds, vsCurrencies }) => {
|
|
91
|
+
try {
|
|
92
|
+
const text = await queryAction("coingecko-get-token-price", {
|
|
93
|
+
coinIds,
|
|
94
|
+
vsCurrencies: vsCurrencies ?? ["usd"],
|
|
95
|
+
});
|
|
96
|
+
return { content: [{ type: "text", text }] };
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: `Price lookup failed: ${err instanceof Error ? err.message : err}`,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
// ── 3. Balance Lookup ───────────────────────────────────────────────
|
|
110
|
+
s.registerTool("b3os_balance_lookup", {
|
|
111
|
+
description: `Get wallet token balances across all supported chains. Read-only, no CU cost.
|
|
112
|
+
|
|
113
|
+
Returns token holdings with USD values, sorted by value descending.
|
|
114
|
+
|
|
115
|
+
INPUTS:
|
|
116
|
+
- address (required) — wallet address to check
|
|
117
|
+
- chainIds (optional) — filter to specific chain IDs (e.g. [1, 8453] for Ethereum + Base)
|
|
118
|
+
- limit (optional, default 20) — max tokens to return
|
|
119
|
+
- offset (optional) — pagination offset
|
|
120
|
+
|
|
121
|
+
CHAIN IDs: 1=Ethereum, 8453=Base, 42161=Arbitrum, 137=Polygon, 10=Optimism, 43114=Avalanche, 56=BSC
|
|
122
|
+
|
|
123
|
+
EXAMPLES:
|
|
124
|
+
- { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" } → vitalik.eth balances
|
|
125
|
+
- { address: "0x...", chainIds: [8453], limit: 50 } → top 50 tokens on Base`,
|
|
126
|
+
inputSchema: {
|
|
127
|
+
address: z.string().describe("Wallet address (0x...)"),
|
|
128
|
+
chainIds: z.array(z.number()).optional().describe("Filter by chain IDs (e.g. [1, 8453])"),
|
|
129
|
+
limit: z.number().optional().describe("Max tokens to return (default: 20)"),
|
|
130
|
+
offset: z.number().optional().describe("Pagination offset"),
|
|
131
|
+
},
|
|
132
|
+
}, async ({ address, chainIds, limit, offset, }) => {
|
|
133
|
+
try {
|
|
134
|
+
const payload = { address, limit: limit ?? 20 };
|
|
135
|
+
if (chainIds)
|
|
136
|
+
payload.chainIds = chainIds;
|
|
137
|
+
if (offset !== undefined)
|
|
138
|
+
payload.offset = offset;
|
|
139
|
+
const text = await queryAction("sim-dune-get-wallet-balances", payload);
|
|
140
|
+
return { content: [{ type: "text", text }] };
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: `Balance lookup failed: ${err instanceof Error ? err.message : err}`,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// ── 4. DeFi Lookup ──────────────────────────────────────────────────
|
|
154
|
+
s.registerTool("b3os_defi_lookup", {
|
|
155
|
+
description: `Get DeFi positions (lending, staking, LPs, vaults) for a wallet. Read-only, no CU cost.
|
|
156
|
+
|
|
157
|
+
Returns protocol positions with USD values across supported chains.
|
|
158
|
+
|
|
159
|
+
INPUTS:
|
|
160
|
+
- address (required) — wallet address to check
|
|
161
|
+
- chainId (optional) — filter to a single chain ID
|
|
162
|
+
|
|
163
|
+
EXAMPLES:
|
|
164
|
+
- { address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" } → all DeFi positions
|
|
165
|
+
- { address: "0x...", chainId: 1 } → Ethereum-only DeFi positions`,
|
|
166
|
+
inputSchema: {
|
|
167
|
+
address: z.string().describe("Wallet address (0x...)"),
|
|
168
|
+
chainId: z.number().optional().describe("Filter to a single chain ID (e.g. 1 for Ethereum)"),
|
|
169
|
+
},
|
|
170
|
+
}, async ({ address, chainId }) => {
|
|
171
|
+
try {
|
|
172
|
+
const payload = { address };
|
|
173
|
+
if (chainId !== undefined)
|
|
174
|
+
payload.chainId = chainId;
|
|
175
|
+
const text = await queryAction("sim-dune-get-defi-positions", payload);
|
|
176
|
+
return { content: [{ type: "text", text }] };
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
return {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: "text",
|
|
183
|
+
text: `DeFi lookup failed: ${err instanceof Error ? err.message : err}`,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// ── 5. Debug Transaction ────────────────────────────────────────────
|
|
190
|
+
s.registerTool("b3os_debug_transaction", {
|
|
191
|
+
description: `Trace and debug an EVM transaction. Read-only, no CU cost.
|
|
192
|
+
|
|
193
|
+
Returns decoded call trace, logs, balance changes, and revert reasons (if failed).
|
|
194
|
+
Much richer than a block explorer — shows internal calls, state diffs, and decoded parameters.
|
|
195
|
+
|
|
196
|
+
INPUTS:
|
|
197
|
+
- txHash (required) — the transaction hash
|
|
198
|
+
- chainId (required) — numeric chain ID
|
|
199
|
+
|
|
200
|
+
CHAIN IDs: 1=Ethereum, 8453=Base, 42161=Arbitrum, 137=Polygon, 10=Optimism, 43114=Avalanche, 56=BSC
|
|
201
|
+
|
|
202
|
+
EXAMPLES:
|
|
203
|
+
- { txHash: "0xabc...123", chainId: 8453 } → debug a Base transaction
|
|
204
|
+
- { txHash: "0xdef...456", chainId: 1 } → debug an Ethereum transaction`,
|
|
205
|
+
inputSchema: {
|
|
206
|
+
txHash: z.string().describe("Transaction hash (0x...)"),
|
|
207
|
+
chainId: z.number().describe("Numeric chain ID (e.g. 1, 8453, 42161)"),
|
|
208
|
+
},
|
|
209
|
+
}, async ({ txHash, chainId }) => {
|
|
210
|
+
try {
|
|
211
|
+
const text = await queryAction("debug-transaction", { txHash, chainId });
|
|
212
|
+
return { content: [{ type: "text", text }] };
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
return {
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: "text",
|
|
219
|
+
text: `Debug transaction failed: ${err instanceof Error ? err.message : err}`,
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
// ── 6. Polymarket Lookup ────────────────────────────────────────────
|
|
226
|
+
s.registerTool("b3os_polymarket_lookup", {
|
|
227
|
+
description: `Search or fetch Polymarket prediction markets. Read-only, no CU cost.
|
|
228
|
+
|
|
229
|
+
USE CASES:
|
|
230
|
+
- Search markets by keyword (e.g. "bitcoin", "election", "fed rate")
|
|
231
|
+
- Fetch a specific market by slug or URL
|
|
232
|
+
|
|
233
|
+
INPUTS (provide ONE):
|
|
234
|
+
- query → search markets by keyword
|
|
235
|
+
- slug → fetch a specific market by its slug (the URL path segment)
|
|
236
|
+
- marketUrl → fetch a specific market by its full Polymarket URL
|
|
237
|
+
|
|
238
|
+
EXAMPLES:
|
|
239
|
+
- { query: "bitcoin 100k" } → search for Bitcoin prediction markets
|
|
240
|
+
- { slug: "will-bitcoin-hit-100k-2025" } → get specific market details
|
|
241
|
+
- { marketUrl: "https://polymarket.com/event/will-bitcoin-hit-100k-2025" } → same, by URL`,
|
|
242
|
+
inputSchema: {
|
|
243
|
+
query: z.string().optional().describe("Search keyword(s) for finding markets"),
|
|
244
|
+
slug: z.string().optional().describe("Market slug (URL path segment)"),
|
|
245
|
+
marketUrl: z.string().optional().describe("Full Polymarket URL"),
|
|
246
|
+
},
|
|
247
|
+
}, async ({ query, slug, marketUrl }) => {
|
|
248
|
+
try {
|
|
249
|
+
if (slug || marketUrl) {
|
|
250
|
+
const payload = {};
|
|
251
|
+
if (slug)
|
|
252
|
+
payload.slug = slug;
|
|
253
|
+
if (marketUrl)
|
|
254
|
+
payload.marketUrl = marketUrl;
|
|
255
|
+
const text = await queryAction("polymarket-get-market", payload);
|
|
256
|
+
return { content: [{ type: "text", text }] };
|
|
257
|
+
}
|
|
258
|
+
if (query) {
|
|
259
|
+
const text = await queryAction("polymarket-search-markets", { query });
|
|
260
|
+
return { content: [{ type: "text", text }] };
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
content: [
|
|
264
|
+
{
|
|
265
|
+
type: "text",
|
|
266
|
+
text: "Provide `query` to search markets, or `slug`/`marketUrl` to fetch a specific market.",
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
return {
|
|
273
|
+
content: [
|
|
274
|
+
{
|
|
275
|
+
type: "text",
|
|
276
|
+
text: `Polymarket lookup failed: ${err instanceof Error ? err.message : err}`,
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { request } from "../client.js";
|
|
2
|
+
export function registerOrgTools(s) {
|
|
3
|
+
s.registerTool("b3os_whoami", {
|
|
4
|
+
description: `Show the organization associated with your B3OS API key. Use this to:
|
|
5
|
+
- Verify the API key is valid and see which org it belongs to
|
|
6
|
+
- Get the orgId needed for b3os_list_wallets
|
|
7
|
+
- Confirm identity before performing operations`,
|
|
8
|
+
}, async () => {
|
|
9
|
+
const data = await request("/v1/organizations");
|
|
10
|
+
const orgs = data?.items || [];
|
|
11
|
+
if (orgs.length === 0) {
|
|
12
|
+
return { content: [{ type: "text", text: "No organization found for this API key." }] };
|
|
13
|
+
}
|
|
14
|
+
const org = orgs[0];
|
|
15
|
+
return {
|
|
16
|
+
content: [
|
|
17
|
+
{
|
|
18
|
+
type: "text",
|
|
19
|
+
text: JSON.stringify({
|
|
20
|
+
id: org.id,
|
|
21
|
+
name: org.name,
|
|
22
|
+
slug: org.slug,
|
|
23
|
+
description: org.description,
|
|
24
|
+
}, null, 2),
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse [[ASK]] and [[PLAN]] blocks from Caddie messages and format them
|
|
3
|
+
* as CLI-friendly instructions.
|
|
4
|
+
*
|
|
5
|
+
* [[ASK]] blocks become tool instructions (which MCP tools to call).
|
|
6
|
+
* [[PLAN]] blocks become reviewable proposals (present to user before approving).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Parse [[ASK]] and [[PLAN]] blocks from a Caddie message and format them
|
|
10
|
+
* as CLI-friendly instructions.
|
|
11
|
+
*
|
|
12
|
+
* `callerTool` is the MCP tool name the agent should call when re-submitting
|
|
13
|
+
* answers or approving the plan (e.g. "b3os_build_workflow" or "b3os_debug_run").
|
|
14
|
+
*
|
|
15
|
+
* If no blocks are found, returns the message unchanged.
|
|
16
|
+
*
|
|
17
|
+
* ASK and PLAN are mutually exclusive in Caddie's output (the agent must
|
|
18
|
+
* choose one per response), so ASK takes precedence if both are present.
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatCaddieBlocksForCLI(message: string, callerTool?: string): string;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse [[ASK]] and [[PLAN]] blocks from Caddie messages and format them
|
|
3
|
+
* as CLI-friendly instructions.
|
|
4
|
+
*
|
|
5
|
+
* [[ASK]] blocks become tool instructions (which MCP tools to call).
|
|
6
|
+
* [[PLAN]] blocks become reviewable proposals (present to user before approving).
|
|
7
|
+
*/
|
|
8
|
+
const CHAIN_IDS_HINT = "Common chain IDs: 1=Ethereum, 8453=Base, 42161=Arbitrum, 137=Polygon, 10=Optimism, 56=BSC, 7565164=Solana";
|
|
9
|
+
/**
|
|
10
|
+
* Maps widget types to CLI-friendly tool instructions.
|
|
11
|
+
* Types not in this map get no tool suggestion (ask user directly).
|
|
12
|
+
* Update here when adding new widget types that have associated MCP tools.
|
|
13
|
+
*/
|
|
14
|
+
const TOOL_INSTRUCTIONS = {
|
|
15
|
+
"slack-channel": 'Use `b3os_list_connectors` to find your Slack connector ID (type: "slack"), then `b3os_list_slack_channels` with that connectorId to see available channels.',
|
|
16
|
+
"telegram-chat": 'Use `b3os_list_connectors` to find your Telegram bot connector ID (type: "telegram-bot"), then `b3os_list_telegram_chats` with that connectorId to see available chats.',
|
|
17
|
+
"turnkey-wallet": "Use `b3os_whoami` to get your orgId, then `b3os_list_wallets` with that orgId to see available wallets.",
|
|
18
|
+
"connector-account": (q) => {
|
|
19
|
+
const connType = q.properties?.connector_type;
|
|
20
|
+
if (connType) {
|
|
21
|
+
return `Use \`b3os_list_connectors\` and find a connector with type: "${connType}".`;
|
|
22
|
+
}
|
|
23
|
+
return "Use `b3os_list_connectors` to see available connectors.";
|
|
24
|
+
},
|
|
25
|
+
"quickbooks-account": 'Use `b3os_list_connectors` to find your QuickBooks connector (type: "quickbooks").',
|
|
26
|
+
"token-address": "Use `b3os_token_lookup` to resolve a token by name/symbol, or provide the contract address directly.",
|
|
27
|
+
"token-addresses": "Use `b3os_token_lookup` to resolve tokens by name/symbol, or provide contract addresses directly.",
|
|
28
|
+
"asset-selector": "Use `b3os_token_lookup` to find the token, or provide the CoinGecko coin ID directly.",
|
|
29
|
+
"chain-id": CHAIN_IDS_HINT,
|
|
30
|
+
"chain-ids": CHAIN_IDS_HINT,
|
|
31
|
+
network: CHAIN_IDS_HINT,
|
|
32
|
+
networks: CHAIN_IDS_HINT,
|
|
33
|
+
"polymarket-outcome": 'Use `b3os_polymarket_lookup` to find the market, then provide the answer as JSON: { "market": "<url>", "outcome": "Yes" }',
|
|
34
|
+
"database-table": (q) => {
|
|
35
|
+
const tableName = q.properties?.tableName;
|
|
36
|
+
const columns = q.properties?.columns;
|
|
37
|
+
const schemaInfo = tableName ? ` Proposed table name: "${tableName}".` : "";
|
|
38
|
+
const columnInfo = Array.isArray(columns) ? ` Proposed columns: ${JSON.stringify(columns)}.` : "";
|
|
39
|
+
return (`The table must be created BEFORE the workflow runs — workflow nodes cannot CREATE/ALTER tables.${schemaInfo}${columnInfo} ` +
|
|
40
|
+
`REVIEW THE SCHEMA: tables should be named by FUNCTION (e.g. "price_alerts", "budgets", "positions"), NOT by workflow (e.g. "eth_price_alert_state"). ` +
|
|
41
|
+
`Always include a workflow_id TEXT column so multiple workflows can share the table. ` +
|
|
42
|
+
`If Caddie's proposed name/schema is workflow-specific, suggest a more reusable design to the user before proceeding. ` +
|
|
43
|
+
`Once the user approves a schema, create the table via \`b3os_query_database\` with CREATE TABLE IF NOT EXISTS, then provide the table name as the answer.`);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
// JSON repair logic (replicated from apps/b3os-web/utils/parseLLMBlock.ts)
|
|
47
|
+
function repairLLMJSON(json) {
|
|
48
|
+
let repaired = json.replace(/\r?\n/g, " ");
|
|
49
|
+
repaired = repaired.replace(/,\s*([}\]])/g, "$1");
|
|
50
|
+
return repaired;
|
|
51
|
+
}
|
|
52
|
+
function findBracePositions(json, brace) {
|
|
53
|
+
const positions = [];
|
|
54
|
+
let inString = false;
|
|
55
|
+
let escaped = false;
|
|
56
|
+
for (let i = 0; i < json.length; i++) {
|
|
57
|
+
const ch = json[i];
|
|
58
|
+
if (escaped) {
|
|
59
|
+
escaped = false;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (ch === "\\") {
|
|
63
|
+
escaped = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (ch === '"') {
|
|
67
|
+
inString = !inString;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!inString && ch === brace)
|
|
71
|
+
positions.push(i);
|
|
72
|
+
}
|
|
73
|
+
return positions;
|
|
74
|
+
}
|
|
75
|
+
function rebalanceBraces(json) {
|
|
76
|
+
const openPositions = findBracePositions(json, "{");
|
|
77
|
+
const closePositions = findBracePositions(json, "}");
|
|
78
|
+
let targets;
|
|
79
|
+
if (closePositions.length > openPositions.length) {
|
|
80
|
+
targets = closePositions.slice(0, -1).reverse();
|
|
81
|
+
}
|
|
82
|
+
else if (openPositions.length > closePositions.length) {
|
|
83
|
+
targets = openPositions.slice(1);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
for (const pos of targets) {
|
|
89
|
+
const candidate = json.slice(0, pos) + json.slice(pos + 1);
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(candidate);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Try next position
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
// Matches only the first [[ASK]] block — multiple blocks per message are not expected.
|
|
100
|
+
const ASK_BLOCK_REGEX = /\[\[ASK\]\]\s*([\s\S]*?)\s*\[\[\/ASK\]\]/;
|
|
101
|
+
function parseAskBlock(message) {
|
|
102
|
+
const match = message.match(ASK_BLOCK_REGEX);
|
|
103
|
+
if (!match) {
|
|
104
|
+
return { before: message, askBlock: null, after: "" };
|
|
105
|
+
}
|
|
106
|
+
const [fullMatch, jsonContent] = match;
|
|
107
|
+
const matchIndex = match.index;
|
|
108
|
+
const before = message.slice(0, matchIndex).trim();
|
|
109
|
+
const after = message.slice(matchIndex + fullMatch.length).trim();
|
|
110
|
+
let parsed = null;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(jsonContent);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
const repaired = repairLLMJSON(jsonContent);
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(repaired);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
parsed = rebalanceBraces(repaired);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { before, askBlock: parsed, after };
|
|
124
|
+
}
|
|
125
|
+
function getToolInstruction(question) {
|
|
126
|
+
const entry = TOOL_INSTRUCTIONS[question.type];
|
|
127
|
+
if (!entry)
|
|
128
|
+
return null;
|
|
129
|
+
if (typeof entry === "function")
|
|
130
|
+
return entry(question);
|
|
131
|
+
return entry;
|
|
132
|
+
}
|
|
133
|
+
function formatDefault(value) {
|
|
134
|
+
if (Array.isArray(value))
|
|
135
|
+
return value.map(String).join(", ");
|
|
136
|
+
if (typeof value === "object" && value !== null)
|
|
137
|
+
return JSON.stringify(value);
|
|
138
|
+
return String(value);
|
|
139
|
+
}
|
|
140
|
+
function formatQuestionBlock(question, index) {
|
|
141
|
+
const lines = [];
|
|
142
|
+
const reqMarker = question.required ? " (required)" : "";
|
|
143
|
+
lines.push(`${index + 1}. **${question.label}**${reqMarker}`);
|
|
144
|
+
if (question.description) {
|
|
145
|
+
lines.push(` ${question.description}`);
|
|
146
|
+
}
|
|
147
|
+
const formattedDefault = question.default != null ? formatDefault(question.default) : "";
|
|
148
|
+
if (formattedDefault) {
|
|
149
|
+
lines.push(` Default: ${formattedDefault}`);
|
|
150
|
+
}
|
|
151
|
+
if (question.options?.length) {
|
|
152
|
+
const optLabels = question.options.map(o => o.label || o.value);
|
|
153
|
+
lines.push(` Options: ${optLabels.join(", ")}`);
|
|
154
|
+
}
|
|
155
|
+
const instruction = getToolInstruction(question);
|
|
156
|
+
if (instruction) {
|
|
157
|
+
lines.push(` -> ${instruction}`);
|
|
158
|
+
}
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
161
|
+
function getAnswerPlaceholder(type) {
|
|
162
|
+
switch (type) {
|
|
163
|
+
case "chain-id":
|
|
164
|
+
case "network":
|
|
165
|
+
return "<chain ID>";
|
|
166
|
+
case "chain-ids":
|
|
167
|
+
case "networks":
|
|
168
|
+
return "<chain ID>, <chain ID>";
|
|
169
|
+
case "token-address":
|
|
170
|
+
case "contract-address":
|
|
171
|
+
case "address":
|
|
172
|
+
case "recipient-address":
|
|
173
|
+
return "<0x...>";
|
|
174
|
+
case "token-addresses":
|
|
175
|
+
return "<0x...>, <0x...>";
|
|
176
|
+
case "turnkey-wallet":
|
|
177
|
+
return "<wallet address or ID>";
|
|
178
|
+
case "slack-channel":
|
|
179
|
+
return "<channel ID>";
|
|
180
|
+
case "telegram-chat":
|
|
181
|
+
return "<chat ID>";
|
|
182
|
+
case "polymarket-outcome":
|
|
183
|
+
return '<{ "market": "...", "outcome": "Yes" }>';
|
|
184
|
+
case "boolean":
|
|
185
|
+
return "<Yes/No>";
|
|
186
|
+
case "date-time":
|
|
187
|
+
case "date":
|
|
188
|
+
return "<ISO 8601>";
|
|
189
|
+
default:
|
|
190
|
+
return "<value>";
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function formatAskBlockForCLI(message, callerTool) {
|
|
194
|
+
const { before, askBlock, after } = parseAskBlock(message);
|
|
195
|
+
if (!askBlock?.questions?.length) {
|
|
196
|
+
return message;
|
|
197
|
+
}
|
|
198
|
+
const parts = [];
|
|
199
|
+
if (before) {
|
|
200
|
+
parts.push(before);
|
|
201
|
+
parts.push("");
|
|
202
|
+
}
|
|
203
|
+
parts.push("Caddie needs the following information to continue:");
|
|
204
|
+
parts.push("");
|
|
205
|
+
const answerLines = [];
|
|
206
|
+
for (const [i, q] of askBlock.questions.entries()) {
|
|
207
|
+
if (i > 0)
|
|
208
|
+
parts.push("");
|
|
209
|
+
parts.push(formatQuestionBlock(q, i));
|
|
210
|
+
answerLines.push(` ${q.label}: ${getAnswerPlaceholder(q.type)}`);
|
|
211
|
+
}
|
|
212
|
+
parts.push("");
|
|
213
|
+
parts.push("IMPORTANT: Present these questions to the user. Do NOT auto-answer them.");
|
|
214
|
+
parts.push("Only fill in values the user has already provided in this conversation. For any missing value, ask the user directly.");
|
|
215
|
+
parts.push("If a question has an MCP tool suggestion (-> line), you may run that tool to gather options to present, but still let the user pick.");
|
|
216
|
+
parts.push("");
|
|
217
|
+
parts.push(`Once you have the user's answers, call \`${callerTool}\` again with them formatted as:`);
|
|
218
|
+
parts.push(...answerLines);
|
|
219
|
+
if (after) {
|
|
220
|
+
parts.push("");
|
|
221
|
+
parts.push(after);
|
|
222
|
+
}
|
|
223
|
+
return parts.join("\n");
|
|
224
|
+
}
|
|
225
|
+
// Matches only the first [[PLAN]] block.
|
|
226
|
+
const PLAN_BLOCK_REGEX = /\[\[PLAN\]\]\s*([\s\S]*?)\s*\[\[\/PLAN\]\]/;
|
|
227
|
+
function parsePlanBlock(message) {
|
|
228
|
+
const match = message.match(PLAN_BLOCK_REGEX);
|
|
229
|
+
if (!match) {
|
|
230
|
+
return { before: message, planBlock: null, after: "" };
|
|
231
|
+
}
|
|
232
|
+
const [fullMatch, jsonContent] = match;
|
|
233
|
+
const matchIndex = match.index;
|
|
234
|
+
const before = message.slice(0, matchIndex).trim();
|
|
235
|
+
const after = message.slice(matchIndex + fullMatch.length).trim();
|
|
236
|
+
let parsed = null;
|
|
237
|
+
try {
|
|
238
|
+
parsed = JSON.parse(jsonContent);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
const repaired = repairLLMJSON(jsonContent);
|
|
242
|
+
try {
|
|
243
|
+
parsed = JSON.parse(repaired);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
parsed = rebalanceBraces(repaired);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return { before, planBlock: parsed, after };
|
|
250
|
+
}
|
|
251
|
+
function formatPlanBlockForCLI(message, callerTool) {
|
|
252
|
+
const { before, planBlock, after } = parsePlanBlock(message);
|
|
253
|
+
if (!planBlock?.changes?.length) {
|
|
254
|
+
return message;
|
|
255
|
+
}
|
|
256
|
+
const parts = [];
|
|
257
|
+
if (before) {
|
|
258
|
+
parts.push(before);
|
|
259
|
+
parts.push("");
|
|
260
|
+
}
|
|
261
|
+
parts.push("Caddie proposes the following workflow plan:");
|
|
262
|
+
parts.push("");
|
|
263
|
+
if (planBlock.note) {
|
|
264
|
+
parts.push(planBlock.note);
|
|
265
|
+
parts.push("");
|
|
266
|
+
}
|
|
267
|
+
for (const [i, change] of planBlock.changes.entries()) {
|
|
268
|
+
const action = change.action.toUpperCase();
|
|
269
|
+
const nodeType = change.type ? ` (${change.type})` : "";
|
|
270
|
+
parts.push(`${i + 1}. [${action}] ${change.description}${nodeType}`);
|
|
271
|
+
}
|
|
272
|
+
parts.push("");
|
|
273
|
+
parts.push("IMPORTANT: Present this plan to the user for review. Do NOT auto-approve it.");
|
|
274
|
+
parts.push(`If the user approves, call \`${callerTool}\` again with "Looks good, build it." to generate the full workflow definition.`);
|
|
275
|
+
parts.push(`If the user wants changes, call \`${callerTool}\` with their feedback.`);
|
|
276
|
+
if (after) {
|
|
277
|
+
parts.push("");
|
|
278
|
+
parts.push(after);
|
|
279
|
+
}
|
|
280
|
+
return parts.join("\n");
|
|
281
|
+
}
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Unified formatter — handles both [[ASK]] and [[PLAN]] blocks
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
/**
|
|
286
|
+
* Parse [[ASK]] and [[PLAN]] blocks from a Caddie message and format them
|
|
287
|
+
* as CLI-friendly instructions.
|
|
288
|
+
*
|
|
289
|
+
* `callerTool` is the MCP tool name the agent should call when re-submitting
|
|
290
|
+
* answers or approving the plan (e.g. "b3os_build_workflow" or "b3os_debug_run").
|
|
291
|
+
*
|
|
292
|
+
* If no blocks are found, returns the message unchanged.
|
|
293
|
+
*
|
|
294
|
+
* ASK and PLAN are mutually exclusive in Caddie's output (the agent must
|
|
295
|
+
* choose one per response), so ASK takes precedence if both are present.
|
|
296
|
+
*/
|
|
297
|
+
export function formatCaddieBlocksForCLI(message, callerTool = "b3os_build_workflow") {
|
|
298
|
+
if (message.includes("[[ASK]]")) {
|
|
299
|
+
return formatAskBlockForCLI(message, callerTool);
|
|
300
|
+
}
|
|
301
|
+
if (message.includes("[[PLAN]]")) {
|
|
302
|
+
return formatPlanBlockForCLI(message, callerTool);
|
|
303
|
+
}
|
|
304
|
+
return message;
|
|
305
|
+
}
|