spendos 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/.dockerignore +4 -0
- package/.env.example +30 -0
- package/AGENTS.md +212 -0
- package/BOOTSTRAP.md +55 -0
- package/Dockerfile +52 -0
- package/HEARTBEAT.md +7 -0
- package/IDENTITY.md +23 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/SOUL.md +202 -0
- package/SUBMISSION.md +128 -0
- package/TOOLS.md +40 -0
- package/USER.md +17 -0
- package/acp-seller/bin/acp.ts +807 -0
- package/acp-seller/config.json +34 -0
- package/acp-seller/package.json +55 -0
- package/acp-seller/src/commands/agent.ts +328 -0
- package/acp-seller/src/commands/bounty.ts +1189 -0
- package/acp-seller/src/commands/deploy.ts +414 -0
- package/acp-seller/src/commands/job.ts +217 -0
- package/acp-seller/src/commands/profile.ts +71 -0
- package/acp-seller/src/commands/resource.ts +91 -0
- package/acp-seller/src/commands/search.ts +327 -0
- package/acp-seller/src/commands/sell.ts +883 -0
- package/acp-seller/src/commands/serve.ts +258 -0
- package/acp-seller/src/commands/setup.ts +399 -0
- package/acp-seller/src/commands/token.ts +88 -0
- package/acp-seller/src/commands/wallet.ts +123 -0
- package/acp-seller/src/lib/api.ts +118 -0
- package/acp-seller/src/lib/auth.ts +291 -0
- package/acp-seller/src/lib/bounty.ts +257 -0
- package/acp-seller/src/lib/client.ts +42 -0
- package/acp-seller/src/lib/config.ts +240 -0
- package/acp-seller/src/lib/open.ts +41 -0
- package/acp-seller/src/lib/openclawCron.ts +138 -0
- package/acp-seller/src/lib/output.ts +104 -0
- package/acp-seller/src/lib/wallet.ts +81 -0
- package/acp-seller/src/seller/offerings/_shared/preTransactionScan.ts +127 -0
- package/acp-seller/src/seller/offerings/canonical-catalog.ts +221 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/handlers.ts +20 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_summarize_url/offering.json +18 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_translate/handlers.ts +21 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_translate/offering.json +22 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/handlers.ts +20 -0
- package/acp-seller/src/seller/offerings/spendos/spendos_tweet_gen/offering.json +18 -0
- package/acp-seller/src/seller/runtime/acpSocket.ts +413 -0
- package/acp-seller/src/seller/runtime/logger.ts +36 -0
- package/acp-seller/src/seller/runtime/offeringTypes.ts +52 -0
- package/acp-seller/src/seller/runtime/offerings.ts +277 -0
- package/acp-seller/src/seller/runtime/paymentVerification.test.ts +207 -0
- package/acp-seller/src/seller/runtime/paymentVerification.ts +363 -0
- package/acp-seller/src/seller/runtime/seller.onchain.test.ts +220 -0
- package/acp-seller/src/seller/runtime/seller.test.ts +823 -0
- package/acp-seller/src/seller/runtime/seller.ts +1041 -0
- package/acp-seller/src/seller/runtime/sellerApi.ts +71 -0
- package/acp-seller/src/seller/runtime/startup.ts +270 -0
- package/acp-seller/src/seller/runtime/types.ts +62 -0
- package/acp-seller/tsconfig.json +20 -0
- package/bin/spendos.js +23 -0
- package/contracts/SpendOSAudit.sol +29 -0
- package/dist/mcp-server.mjs +153 -0
- package/jobs/translate.json +7 -0
- package/jobs/tweet-gen.json +7 -0
- package/openclaw.json +41 -0
- package/package.json +49 -0
- package/plugins/spendos-events/index.ts +78 -0
- package/plugins/spendos-events/package.json +14 -0
- package/policies/enforce-bounds.mjs +71 -0
- package/public/index.html +509 -0
- package/public/landing.html +241 -0
- package/railway.json +12 -0
- package/railway.toml +12 -0
- package/scripts/deploy.ts +48 -0
- package/scripts/test-x402-mainnet.ts +30 -0
- package/scripts/xmtp-listener.ts +61 -0
- package/setup.sh +278 -0
- package/skills/spendos/skill.md +26 -0
- package/src/agent.ts +152 -0
- package/src/audit.ts +166 -0
- package/src/governance.ts +367 -0
- package/src/job-registry.ts +306 -0
- package/src/mcp-public.ts +145 -0
- package/src/mcp-server.ts +171 -0
- package/src/opportunity-scanner.ts +138 -0
- package/src/server.ts +870 -0
- package/src/venice-x402.ts +234 -0
- package/src/xmtp.ts +109 -0
- package/src/zerion.ts +58 -0
- package/start.sh +168 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// acp resource query <url> [--params '<json>'] — Query a resource by URL
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import * as output from "../lib/output.js";
|
|
7
|
+
|
|
8
|
+
export async function query(
|
|
9
|
+
url: string,
|
|
10
|
+
params?: Record<string, any>,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
if (!url) {
|
|
13
|
+
output.fatal("Usage: acp resource query <url> [--params '<json>']");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Validate URL format
|
|
17
|
+
try {
|
|
18
|
+
new URL(url);
|
|
19
|
+
} catch {
|
|
20
|
+
output.fatal(`Invalid URL: ${url}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
// Make HTTP request to resource URL
|
|
25
|
+
output.log(`\nQuerying resource at: ${url}`);
|
|
26
|
+
if (params && Object.keys(params).length > 0) {
|
|
27
|
+
output.log(` With params: ${JSON.stringify(params, null, 2)}\n`);
|
|
28
|
+
} else {
|
|
29
|
+
output.log("");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let response;
|
|
33
|
+
try {
|
|
34
|
+
// Always use GET request, params as query string
|
|
35
|
+
if (params && Object.keys(params).length > 0) {
|
|
36
|
+
// Build query string from params
|
|
37
|
+
const queryString = new URLSearchParams();
|
|
38
|
+
for (const [key, value] of Object.entries(params)) {
|
|
39
|
+
if (value !== null && value !== undefined) {
|
|
40
|
+
queryString.append(key, String(value));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const urlWithParams = url.includes("?")
|
|
44
|
+
? `${url}&${queryString.toString()}`
|
|
45
|
+
: `${url}?${queryString.toString()}`;
|
|
46
|
+
response = await axios.get(urlWithParams);
|
|
47
|
+
} else {
|
|
48
|
+
response = await axios.get(url);
|
|
49
|
+
}
|
|
50
|
+
} catch (httpError: any) {
|
|
51
|
+
if (httpError.response) {
|
|
52
|
+
// Server responded with error status
|
|
53
|
+
const errorMsg = httpError.response.data
|
|
54
|
+
? JSON.stringify(httpError.response.data, null, 2)
|
|
55
|
+
: httpError.response.statusText;
|
|
56
|
+
output.fatal(
|
|
57
|
+
`Resource query failed: ${httpError.response.status} ${httpError.response.statusText}\n${errorMsg}`,
|
|
58
|
+
);
|
|
59
|
+
} else {
|
|
60
|
+
output.fatal(
|
|
61
|
+
`Resource query failed: ${
|
|
62
|
+
httpError instanceof Error ? httpError.message : String(httpError)
|
|
63
|
+
}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const responseData = response.data;
|
|
69
|
+
|
|
70
|
+
output.output(responseData, (data) => {
|
|
71
|
+
output.heading(`Resource Query Result`);
|
|
72
|
+
output.log(`\n URL: ${url}`);
|
|
73
|
+
output.log(`\n Response:`);
|
|
74
|
+
if (typeof data === "string") {
|
|
75
|
+
output.log(` ${data}`);
|
|
76
|
+
} else {
|
|
77
|
+
output.log(
|
|
78
|
+
` ${JSON.stringify(data, null, 2)
|
|
79
|
+
.split("\n")
|
|
80
|
+
.map((line, i) => (i === 0 ? line : ` ${line}`))
|
|
81
|
+
.join("\n")}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
output.log("");
|
|
85
|
+
});
|
|
86
|
+
} catch (e) {
|
|
87
|
+
output.fatal(
|
|
88
|
+
`Resource query failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// acp browse <query> — Search agents with filters and configurations
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import * as output from "../lib/output.js";
|
|
7
|
+
|
|
8
|
+
const SEARCH_URL =
|
|
9
|
+
process.env.SEARCH_URL || "https://acpx.virtuals.io/api/agents/v5/search";
|
|
10
|
+
|
|
11
|
+
// -- Types --
|
|
12
|
+
|
|
13
|
+
export interface SearchOptions {
|
|
14
|
+
mode?: "hybrid" | "vector" | "keyword";
|
|
15
|
+
contains?: string;
|
|
16
|
+
match?: "all" | "any";
|
|
17
|
+
similarityCutoff?: number;
|
|
18
|
+
sparseCutoff?: number;
|
|
19
|
+
topK?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface AgentMetrics {
|
|
23
|
+
successfulJobCount: number | null;
|
|
24
|
+
successRate: number | null;
|
|
25
|
+
uniqueBuyerCount: number | null;
|
|
26
|
+
minsFromLastOnlineTime: number | null;
|
|
27
|
+
isOnline: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface AgentJob {
|
|
31
|
+
id: number;
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
type: string;
|
|
35
|
+
price: number;
|
|
36
|
+
priceV2: { type: string; value: number };
|
|
37
|
+
requiredFunds: boolean;
|
|
38
|
+
slaMinutes: number;
|
|
39
|
+
requirement: Record<string, unknown>;
|
|
40
|
+
deliverable: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface AgentResource {
|
|
44
|
+
name: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
url?: string;
|
|
47
|
+
params?: Record<string, unknown>;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Agent {
|
|
52
|
+
id: number;
|
|
53
|
+
name: string;
|
|
54
|
+
description: string;
|
|
55
|
+
contractAddress: string;
|
|
56
|
+
walletAddress: string;
|
|
57
|
+
twitterHandle: string;
|
|
58
|
+
profilePic: string;
|
|
59
|
+
tokenAddress: string | null;
|
|
60
|
+
cluster: string | null;
|
|
61
|
+
category: string | null;
|
|
62
|
+
symbol: string | null;
|
|
63
|
+
virtualAgentId: number | null;
|
|
64
|
+
isVirtualAgent: boolean;
|
|
65
|
+
metrics: AgentMetrics;
|
|
66
|
+
jobs: AgentJob[];
|
|
67
|
+
resources: AgentResource[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// -- Defaults (server-side, documented here for help text & summary) --
|
|
71
|
+
|
|
72
|
+
export const SEARCH_DEFAULTS = {
|
|
73
|
+
mode: "hybrid" as const,
|
|
74
|
+
similarityCutoff: 0.5,
|
|
75
|
+
sparseCutoff: 0.0,
|
|
76
|
+
match: "all" as const,
|
|
77
|
+
topK: 5,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// -- Friendly mode → API searchMode mapping --
|
|
81
|
+
|
|
82
|
+
const MODE_MAP: Record<string, string> = {
|
|
83
|
+
hybrid: "hybrid",
|
|
84
|
+
vector: "dense",
|
|
85
|
+
keyword: "sparse",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// -- Build query params --
|
|
89
|
+
|
|
90
|
+
function buildParams(
|
|
91
|
+
query: string,
|
|
92
|
+
opts: SearchOptions,
|
|
93
|
+
): Record<string, string> {
|
|
94
|
+
const params: Record<string, string> = { query };
|
|
95
|
+
params.claw = "true";
|
|
96
|
+
|
|
97
|
+
// Search mode
|
|
98
|
+
if (opts.mode) {
|
|
99
|
+
const apiMode = MODE_MAP[opts.mode];
|
|
100
|
+
if (!apiMode) {
|
|
101
|
+
output.fatal(
|
|
102
|
+
`Invalid search mode "${opts.mode}". Use: hybrid, vector, keyword`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
params.searchMode = apiMode;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// String filters
|
|
109
|
+
if (opts.contains) params.fullTextFilter = opts.contains;
|
|
110
|
+
if (opts.match) params.fullTextMatch = opts.match;
|
|
111
|
+
|
|
112
|
+
// Cutoffs
|
|
113
|
+
params.similarityCutoff = String(
|
|
114
|
+
opts.similarityCutoff ?? SEARCH_DEFAULTS.similarityCutoff,
|
|
115
|
+
);
|
|
116
|
+
if (opts.sparseCutoff !== undefined)
|
|
117
|
+
params.sparseCutoff = String(opts.sparseCutoff);
|
|
118
|
+
|
|
119
|
+
// Result count
|
|
120
|
+
params.topK = String(opts.topK ?? SEARCH_DEFAULTS.topK);
|
|
121
|
+
|
|
122
|
+
return params;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// -- Table formatting --
|
|
126
|
+
|
|
127
|
+
function truncate(s: string, max: number): string {
|
|
128
|
+
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatTable(agents: Agent[]): void {
|
|
132
|
+
const header = {
|
|
133
|
+
rank: "#",
|
|
134
|
+
name: "Name",
|
|
135
|
+
id: "ID",
|
|
136
|
+
category: "Category",
|
|
137
|
+
rate: "Success",
|
|
138
|
+
jobs: "Jobs",
|
|
139
|
+
buyers: "Buyers",
|
|
140
|
+
online: "Online",
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Column widths
|
|
144
|
+
const w = {
|
|
145
|
+
rank: 4,
|
|
146
|
+
name: 20,
|
|
147
|
+
id: 6,
|
|
148
|
+
category: 16,
|
|
149
|
+
rate: 9,
|
|
150
|
+
jobs: 6,
|
|
151
|
+
buyers: 8,
|
|
152
|
+
online: 6,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const row = (r: typeof header) =>
|
|
156
|
+
` ${r.rank.toString().padStart(w.rank)} ` +
|
|
157
|
+
`${truncate(r.name, w.name).padEnd(w.name)} ` +
|
|
158
|
+
`${r.id.toString().padEnd(w.id)} ` +
|
|
159
|
+
`${truncate(r.category, w.category).padEnd(w.category)} ` +
|
|
160
|
+
`${r.rate.toString().padStart(w.rate)} ` +
|
|
161
|
+
`${r.jobs.toString().padStart(w.jobs)} ` +
|
|
162
|
+
`${r.buyers.toString().padStart(w.buyers)} ` +
|
|
163
|
+
`${r.online.toString().padEnd(w.online)}`;
|
|
164
|
+
|
|
165
|
+
// Header
|
|
166
|
+
output.log(output.colors.dim(row(header)));
|
|
167
|
+
|
|
168
|
+
// Rows
|
|
169
|
+
for (let i = 0; i < agents.length; i++) {
|
|
170
|
+
const a = agents[i];
|
|
171
|
+
output.log(
|
|
172
|
+
row({
|
|
173
|
+
rank: String(i + 1),
|
|
174
|
+
name: a.name,
|
|
175
|
+
id: String(a.id),
|
|
176
|
+
category: a.category ?? "-",
|
|
177
|
+
rate:
|
|
178
|
+
a.metrics.successRate != null
|
|
179
|
+
? `${a.metrics.successRate.toFixed(1)}%`
|
|
180
|
+
: "-",
|
|
181
|
+
jobs:
|
|
182
|
+
a.metrics.successfulJobCount != null
|
|
183
|
+
? String(a.metrics.successfulJobCount)
|
|
184
|
+
: "-",
|
|
185
|
+
buyers:
|
|
186
|
+
a.metrics.uniqueBuyerCount != null
|
|
187
|
+
? String(a.metrics.uniqueBuyerCount)
|
|
188
|
+
: "-",
|
|
189
|
+
online: a.metrics.isOnline ? "Yes" : "No",
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// -- Detailed per-agent output (offerings + resources) --
|
|
196
|
+
|
|
197
|
+
function formatPrice(price: number, priceType?: string): string {
|
|
198
|
+
if (priceType === "percentage") return `${(price * 100).toFixed(1)}%`;
|
|
199
|
+
return `$${price} USDC`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function formatDetails(agents: Agent[]): void {
|
|
203
|
+
for (const a of agents) {
|
|
204
|
+
output.log(`\n ${output.colors.bold(a.name)}`);
|
|
205
|
+
output.log(` Wallet: ${a.walletAddress}`);
|
|
206
|
+
if (a.description) {
|
|
207
|
+
output.log(` ${output.colors.dim(a.description)}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const jobs = a.jobs ?? [];
|
|
211
|
+
if (jobs.length > 0) {
|
|
212
|
+
output.log(" Offerings:");
|
|
213
|
+
for (const j of jobs) {
|
|
214
|
+
const fee = formatPrice(j.price, j.priceV2?.type);
|
|
215
|
+
const funds = j.requiredFunds ? " [requires funds]" : "";
|
|
216
|
+
output.log(` - ${j.name} (${fee}${funds})`);
|
|
217
|
+
if (j.description) {
|
|
218
|
+
output.log(` ${j.description}`);
|
|
219
|
+
}
|
|
220
|
+
if (j.requirement && Object.keys(j.requirement).length > 0) {
|
|
221
|
+
const req = JSON.stringify(j.requirement, null, 2)
|
|
222
|
+
.split("\n")
|
|
223
|
+
.join("\n ");
|
|
224
|
+
output.log(` Requirement: ${req}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const resources = a.resources ?? [];
|
|
230
|
+
if (resources.length > 0) {
|
|
231
|
+
output.log(" Resources:");
|
|
232
|
+
for (const r of resources) {
|
|
233
|
+
output.log(` - ${r.name}`);
|
|
234
|
+
if (r.description) {
|
|
235
|
+
output.log(` ${r.description}`);
|
|
236
|
+
}
|
|
237
|
+
if (r.url) {
|
|
238
|
+
output.log(` URL: ${r.url}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// -- Settings summary --
|
|
246
|
+
|
|
247
|
+
function formatSummary(opts: SearchOptions): string {
|
|
248
|
+
const parts: string[] = [];
|
|
249
|
+
|
|
250
|
+
// Mode
|
|
251
|
+
parts.push(`mode=${opts.mode ?? SEARCH_DEFAULTS.mode}`);
|
|
252
|
+
|
|
253
|
+
// Active filters
|
|
254
|
+
const filters: string[] = [];
|
|
255
|
+
if (opts.contains) {
|
|
256
|
+
const m = opts.match ?? SEARCH_DEFAULTS.match;
|
|
257
|
+
filters.push(`contains="${opts.contains}" (match=${m})`);
|
|
258
|
+
}
|
|
259
|
+
parts.push(filters.join(", "));
|
|
260
|
+
|
|
261
|
+
return parts.join(" · ");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// -- Main search function --
|
|
265
|
+
|
|
266
|
+
export async function search(
|
|
267
|
+
query: string,
|
|
268
|
+
opts: SearchOptions,
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
if (!query.trim()) {
|
|
271
|
+
output.fatal(
|
|
272
|
+
"Usage: acp browse <query>\n Run `acp browse --help` for all options.",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Validate: --match requires --contains
|
|
277
|
+
if (opts.match && !opts.contains) {
|
|
278
|
+
output.fatal("--match requires --contains");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const params = buildParams(query, opts);
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const response = await axios.get<{ data: Agent[] }>(SEARCH_URL, { params });
|
|
285
|
+
const data = response.data?.data;
|
|
286
|
+
|
|
287
|
+
// Handle the known SQL-error quirk for empty results
|
|
288
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
|
289
|
+
output.output([], () => {
|
|
290
|
+
output.log(`\n No agents found for "${query}".`);
|
|
291
|
+
output.log(
|
|
292
|
+
` Try tweaking search parameters (\`acp browse --help\`) or run \`acp bounty create "${query}"\` to post a bounty.`,
|
|
293
|
+
);
|
|
294
|
+
output.log("");
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
output.output(data, (agents: Agent[]) => {
|
|
300
|
+
output.heading(`Search results for "${query}"`);
|
|
301
|
+
output.log(output.colors.dim(` ${formatSummary(opts)}`));
|
|
302
|
+
output.log("");
|
|
303
|
+
formatTable(agents);
|
|
304
|
+
formatDetails(agents);
|
|
305
|
+
output.log(
|
|
306
|
+
output.colors.dim(
|
|
307
|
+
`\n ${agents.length} result${agents.length === 1 ? "" : "s"}`,
|
|
308
|
+
),
|
|
309
|
+
);
|
|
310
|
+
output.log("");
|
|
311
|
+
});
|
|
312
|
+
} catch (e: unknown) {
|
|
313
|
+
// Handle the SQL-error quirk (empty WHERE IN ())
|
|
314
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
315
|
+
if (msg.includes("syntax") || msg.includes("SQL")) {
|
|
316
|
+
output.output([], () => {
|
|
317
|
+
output.log(`\n No agents found for "${query}".`);
|
|
318
|
+
output.log(
|
|
319
|
+
` Try tweaking search parameters (\`acp browse --help\`) or run \`acp bounty create "${query}"\` to post a bounty.`,
|
|
320
|
+
);
|
|
321
|
+
output.log("");
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
output.fatal(`Search failed: ${msg}`);
|
|
326
|
+
}
|
|
327
|
+
}
|