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,883 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// acp sell init <name> — Scaffold a new offering
|
|
3
|
+
// acp sell create <name> — Validate + register offering on ACP
|
|
4
|
+
// acp sell delete <name> — Delist offering from ACP
|
|
5
|
+
// acp sell list — Show all offerings with status
|
|
6
|
+
// acp sell inspect <name> — Detailed view of single offering
|
|
7
|
+
//
|
|
8
|
+
// acp sell resource init <name> — Scaffold a new resource
|
|
9
|
+
// acp sell resource create <name> — Validate + register resource on ACP
|
|
10
|
+
// acp sell resource delete <name> — Delete resource from ACP
|
|
11
|
+
// acp sell resource list — Show all resources with status
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import * as output from "../lib/output.js";
|
|
18
|
+
import {
|
|
19
|
+
createJobOffering,
|
|
20
|
+
deleteJobOffering,
|
|
21
|
+
upsertResourceApi,
|
|
22
|
+
deleteResourceApi,
|
|
23
|
+
type JobOfferingData,
|
|
24
|
+
type PriceV2,
|
|
25
|
+
type Resource,
|
|
26
|
+
} from "../lib/api.js";
|
|
27
|
+
import { getMyAgentInfo } from "../lib/wallet.js";
|
|
28
|
+
import {
|
|
29
|
+
formatPrice,
|
|
30
|
+
getActiveAgent,
|
|
31
|
+
sanitizeAgentName,
|
|
32
|
+
} from "../lib/config.js";
|
|
33
|
+
|
|
34
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
35
|
+
const __dirname = path.dirname(__filename);
|
|
36
|
+
|
|
37
|
+
/** Offerings base: src/seller/offerings/ */
|
|
38
|
+
const OFFERINGS_BASE = path.resolve(__dirname, "..", "seller", "offerings");
|
|
39
|
+
|
|
40
|
+
/** Offerings root for the current agent: src/seller/offerings/<agent-name>/ */
|
|
41
|
+
function getOfferingsRoot(): string {
|
|
42
|
+
const agent = getActiveAgent();
|
|
43
|
+
if (!agent) {
|
|
44
|
+
console.error("Error: No active agent. Run `acp setup` first.");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
return path.resolve(OFFERINGS_BASE, sanitizeAgentName(agent.name));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detect offerings in the old flat structure (src/seller/offerings/<offering>/)
|
|
52
|
+
* instead of the new per-agent structure (src/seller/offerings/<agent>/<offering>/).
|
|
53
|
+
* Warns the user and shows the move command.
|
|
54
|
+
*/
|
|
55
|
+
export async function checkForLegacyOfferings(): Promise<void> {
|
|
56
|
+
if (!fs.existsSync(OFFERINGS_BASE)) return;
|
|
57
|
+
|
|
58
|
+
const agent = getActiveAgent();
|
|
59
|
+
if (!agent) return;
|
|
60
|
+
const agentDir = sanitizeAgentName(agent.name);
|
|
61
|
+
|
|
62
|
+
const entries = fs.readdirSync(OFFERINGS_BASE, { withFileTypes: true });
|
|
63
|
+
const legacyOfferings = entries.filter((e) => {
|
|
64
|
+
if (!e.isDirectory()) return false;
|
|
65
|
+
// Skip if it looks like an agent directory (contains subdirectories with offering.json)
|
|
66
|
+
const subPath = path.join(OFFERINGS_BASE, e.name);
|
|
67
|
+
const hasOfferingJson = fs.existsSync(path.join(subPath, "offering.json"));
|
|
68
|
+
const hasHandlers = fs.existsSync(path.join(subPath, "handlers.ts"));
|
|
69
|
+
// It's a legacy offering if it has offering.json + handlers.ts directly
|
|
70
|
+
return hasOfferingJson && hasHandlers;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (legacyOfferings.length === 0) return;
|
|
74
|
+
|
|
75
|
+
const agentInfo = await getMyAgentInfo();
|
|
76
|
+
const registeredOfferingNames = new Set(
|
|
77
|
+
agentInfo.jobs?.map((job) => job.name) || [],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const agentLegacyOfferings = legacyOfferings.filter((e) => {
|
|
81
|
+
if (registeredOfferingNames.has(e.name)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const offeringJsonPath = path.join(
|
|
86
|
+
OFFERINGS_BASE,
|
|
87
|
+
e.name,
|
|
88
|
+
"offering.json",
|
|
89
|
+
);
|
|
90
|
+
if (fs.existsSync(offeringJsonPath)) {
|
|
91
|
+
const offeringJson: OfferingJson = JSON.parse(
|
|
92
|
+
fs.readFileSync(offeringJsonPath, "utf-8"),
|
|
93
|
+
);
|
|
94
|
+
return registeredOfferingNames.has(offeringJson.name);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// If we can't read the file, skip this check
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Only show warning if there are legacy offerings that belong to the current agent
|
|
103
|
+
if (agentLegacyOfferings.length === 0) return;
|
|
104
|
+
|
|
105
|
+
const names = agentLegacyOfferings.map((e) => e.name);
|
|
106
|
+
output.warn(
|
|
107
|
+
`Found ${names.length} offering(s) in the legacy directory structure:\n` +
|
|
108
|
+
names.map((n) => ` - src/seller/offerings/${n}/`).join("\n") +
|
|
109
|
+
"\n\n" +
|
|
110
|
+
` Job offerings should be placed and classified by agent name in src/seller/offerings/${agentDir}/\n` +
|
|
111
|
+
` Move them with:\n\n` +
|
|
112
|
+
names
|
|
113
|
+
.map(
|
|
114
|
+
(n) =>
|
|
115
|
+
` mv src/seller/offerings/${n} src/seller/offerings/${agentDir}/${n}`,
|
|
116
|
+
)
|
|
117
|
+
.join("\n") +
|
|
118
|
+
"\n",
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Resources live at src/seller/resources/ */
|
|
123
|
+
const RESOURCES_ROOT = path.resolve(__dirname, "..", "seller", "resources");
|
|
124
|
+
|
|
125
|
+
interface OfferingJson {
|
|
126
|
+
name: string;
|
|
127
|
+
description: string;
|
|
128
|
+
jobFee: number;
|
|
129
|
+
jobFeeType: "fixed" | "percentage";
|
|
130
|
+
priceV2?: PriceV2;
|
|
131
|
+
slaMinutes?: number;
|
|
132
|
+
requiredFunds: boolean;
|
|
133
|
+
requirement?: Record<string, any>;
|
|
134
|
+
deliverable?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface ValidationResult {
|
|
138
|
+
valid: boolean;
|
|
139
|
+
errors: string[];
|
|
140
|
+
warnings: string[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveOfferingDir(offeringName: string): string {
|
|
144
|
+
return path.resolve(getOfferingsRoot(), offeringName);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateOfferingJson(filePath: string): ValidationResult {
|
|
148
|
+
const result: ValidationResult = { valid: true, errors: [], warnings: [] };
|
|
149
|
+
|
|
150
|
+
if (!fs.existsSync(filePath)) {
|
|
151
|
+
result.valid = false;
|
|
152
|
+
result.errors.push(`offering.json not found at ${filePath}`);
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let json: any;
|
|
157
|
+
try {
|
|
158
|
+
json = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
result.valid = false;
|
|
161
|
+
result.errors.push(`Invalid JSON in offering.json: ${err}`);
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!json.name || typeof json.name !== "string" || json.name.trim() === "") {
|
|
166
|
+
result.valid = false;
|
|
167
|
+
result.errors.push(
|
|
168
|
+
'offering.json: "name" is required — set to a non-empty string matching the directory name',
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
if (
|
|
172
|
+
!json.description ||
|
|
173
|
+
typeof json.description !== "string" ||
|
|
174
|
+
json.description.trim() === ""
|
|
175
|
+
) {
|
|
176
|
+
result.valid = false;
|
|
177
|
+
result.errors.push(
|
|
178
|
+
'offering.json: "description" is required — describe what this service does for buyers',
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (json.jobFee === undefined || json.jobFee === null) {
|
|
182
|
+
result.valid = false;
|
|
183
|
+
// Validate jobFee presence, type, and value based on jobFeeType
|
|
184
|
+
if (json.jobFee === undefined || json.jobFee === null) {
|
|
185
|
+
result.valid = false;
|
|
186
|
+
result.errors.push(
|
|
187
|
+
'offering.json: "jobFee" is required — set to a number (see "jobFeeType" docs)',
|
|
188
|
+
);
|
|
189
|
+
} else if (typeof json.jobFee !== "number") {
|
|
190
|
+
result.valid = false;
|
|
191
|
+
result.errors.push('offering.json: "jobFee" must be a number');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (json.jobFeeType === undefined || json.jobFeeType === null) {
|
|
195
|
+
result.valid = false;
|
|
196
|
+
result.errors.push(
|
|
197
|
+
'offering.json: "jobFeeType" is required ("fixed" or "percentage")',
|
|
198
|
+
);
|
|
199
|
+
} else if (
|
|
200
|
+
json.jobFeeType !== "fixed" &&
|
|
201
|
+
json.jobFeeType !== "percentage"
|
|
202
|
+
) {
|
|
203
|
+
result.valid = false;
|
|
204
|
+
result.errors.push(
|
|
205
|
+
'offering.json: "jobFeeType" must be either "fixed" or "percentage"',
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Additional validation if both jobFee is a number and jobFeeType is set
|
|
210
|
+
if (typeof json.jobFee === "number" && json.jobFeeType) {
|
|
211
|
+
if (json.jobFeeType === "fixed") {
|
|
212
|
+
if (json.jobFee < 0) {
|
|
213
|
+
result.valid = false;
|
|
214
|
+
result.errors.push(
|
|
215
|
+
'offering.json: "jobFee" must be a non-negative number (fee in USDC per job) for fixed fee type',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (json.jobFee === 0) {
|
|
219
|
+
result.warnings.push(
|
|
220
|
+
'offering.json: "jobFee" is 0; jobs will pay no fee to seller',
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
} else if (json.jobFeeType === "percentage") {
|
|
224
|
+
if (json.jobFee < 0.001 || json.jobFee > 0.99) {
|
|
225
|
+
result.valid = false;
|
|
226
|
+
result.errors.push(
|
|
227
|
+
'offering.json: "jobFee" must be >= 0.001 and <= 0.99 (value in decimals, eg. 50% = 0.5) for percentage fee type',
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (json.requiredFunds === undefined || json.requiredFunds === null) {
|
|
234
|
+
result.valid = false;
|
|
235
|
+
result.errors.push(
|
|
236
|
+
'offering.json: "requiredFunds" is required — set to true if the job needs additional token transfer beyond the fee, false otherwise',
|
|
237
|
+
);
|
|
238
|
+
} else if (typeof json.requiredFunds !== "boolean") {
|
|
239
|
+
result.valid = false;
|
|
240
|
+
result.errors.push('offering.json: "requiredFunds" must be true or false');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function validateHandlers(
|
|
247
|
+
filePath: string,
|
|
248
|
+
requiredFunds?: boolean,
|
|
249
|
+
): ValidationResult {
|
|
250
|
+
const result: ValidationResult = { valid: true, errors: [], warnings: [] };
|
|
251
|
+
|
|
252
|
+
if (!fs.existsSync(filePath)) {
|
|
253
|
+
result.valid = false;
|
|
254
|
+
result.errors.push(`handlers.ts not found at ${filePath}`);
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
259
|
+
|
|
260
|
+
const executeJobPatterns = [
|
|
261
|
+
/export\s+(async\s+)?function\s+executeJob\s*\(/,
|
|
262
|
+
/export\s+const\s+executeJob\s*=\s*(async\s*)?\(/,
|
|
263
|
+
/export\s+const\s+executeJob\s*=\s*(async\s*)?function/,
|
|
264
|
+
/export\s*\{\s*[^}]*executeJob[^}]*\}/,
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
if (!executeJobPatterns.some((p) => p.test(content))) {
|
|
268
|
+
result.valid = false;
|
|
269
|
+
result.errors.push(
|
|
270
|
+
'handlers.ts: must export an "executeJob" function — this is the required handler that runs your service logic',
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const hasValidate = [
|
|
275
|
+
/export\s+(async\s+)?function\s+validateRequirements\s*\(/,
|
|
276
|
+
/export\s+const\s+validateRequirements\s*=/,
|
|
277
|
+
/export\s*\{\s*[^}]*validateRequirements[^}]*\}/,
|
|
278
|
+
].some((p) => p.test(content));
|
|
279
|
+
|
|
280
|
+
const hasFunds = [
|
|
281
|
+
/export\s+(async\s+)?function\s+requestAdditionalFunds\s*\(/,
|
|
282
|
+
/export\s+const\s+requestAdditionalFunds\s*=/,
|
|
283
|
+
/export\s*\{\s*[^}]*requestAdditionalFunds[^}]*\}/,
|
|
284
|
+
].some((p) => p.test(content));
|
|
285
|
+
|
|
286
|
+
if (!hasValidate) {
|
|
287
|
+
result.warnings.push(
|
|
288
|
+
'handlers.ts: optional "validateRequirements" handler not found — requests will be accepted without validation',
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (requiredFunds === true && !hasFunds) {
|
|
292
|
+
result.valid = false;
|
|
293
|
+
result.errors.push(
|
|
294
|
+
'handlers.ts: "requiredFunds" is true in offering.json — must export "requestAdditionalFunds" to specify the token transfer details',
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
if (requiredFunds === false && hasFunds) {
|
|
298
|
+
result.valid = false;
|
|
299
|
+
result.errors.push(
|
|
300
|
+
'handlers.ts: "requiredFunds" is false in offering.json — must NOT export "requestAdditionalFunds" (remove it, or set requiredFunds to true)',
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function buildAcpPayload(json: OfferingJson): JobOfferingData {
|
|
308
|
+
return {
|
|
309
|
+
name: json.name,
|
|
310
|
+
description: json.description,
|
|
311
|
+
priceV2: json.priceV2 ?? { type: json.jobFeeType, value: json.jobFee },
|
|
312
|
+
slaMinutes: json.slaMinutes ?? 5,
|
|
313
|
+
requiredFunds: json.requiredFunds,
|
|
314
|
+
requirement: json.requirement ?? {},
|
|
315
|
+
deliverable: json.deliverable ?? "string",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// -- Init: scaffold a new offering --
|
|
320
|
+
|
|
321
|
+
export async function init(offeringName: string): Promise<void> {
|
|
322
|
+
await checkForLegacyOfferings();
|
|
323
|
+
if (!offeringName) {
|
|
324
|
+
output.fatal("Usage: acp sell init <offering_name>");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const dir = resolveOfferingDir(offeringName);
|
|
328
|
+
if (fs.existsSync(dir)) {
|
|
329
|
+
output.fatal(`Offering directory already exists: ${dir}`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
333
|
+
|
|
334
|
+
const offeringJson: Record<string, unknown> = {
|
|
335
|
+
name: offeringName,
|
|
336
|
+
description: "",
|
|
337
|
+
jobFee: null,
|
|
338
|
+
jobFeeType: null,
|
|
339
|
+
requiredFunds: null,
|
|
340
|
+
requirement: {},
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
fs.writeFileSync(
|
|
344
|
+
path.join(dir, "offering.json"),
|
|
345
|
+
JSON.stringify(offeringJson, null, 2) + "\n",
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const handlersTemplate = `import type { ExecuteJobResult, ValidationResult } from "../../../runtime/offeringTypes.js";
|
|
349
|
+
|
|
350
|
+
// Required: implement your service logic here
|
|
351
|
+
export async function executeJob(request: any): Promise<ExecuteJobResult> {
|
|
352
|
+
// TODO: Implement your service
|
|
353
|
+
return { deliverable: "TODO: Return your result" };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Optional: validate incoming requests
|
|
357
|
+
export function validateRequirements(request: any): ValidationResult {
|
|
358
|
+
// Return { valid: true } to accept, or { valid: false, reason: "explanation" } to reject
|
|
359
|
+
return { valid: true };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Optional: provide custom payment request message
|
|
363
|
+
export function requestPayment(request: any): string {
|
|
364
|
+
// Return a custom message/reason for the payment request
|
|
365
|
+
return "Request accepted";
|
|
366
|
+
}
|
|
367
|
+
`;
|
|
368
|
+
|
|
369
|
+
fs.writeFileSync(path.join(dir, "handlers.ts"), handlersTemplate);
|
|
370
|
+
|
|
371
|
+
const agent = getActiveAgent();
|
|
372
|
+
const agentDir = agent ? sanitizeAgentName(agent.name) : "unknown";
|
|
373
|
+
output.output({ created: dir }, () => {
|
|
374
|
+
output.heading("Offering Scaffolded");
|
|
375
|
+
output.log(` Created: src/seller/offerings/${agentDir}/${offeringName}/`);
|
|
376
|
+
output.log(
|
|
377
|
+
` - offering.json (edit name, description, fee, feeType, requirements)`,
|
|
378
|
+
);
|
|
379
|
+
output.log(` - handlers.ts (implement executeJob)`);
|
|
380
|
+
output.log(
|
|
381
|
+
`\n Next: edit the files, then run: acp sell create ${offeringName}\n`,
|
|
382
|
+
);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// -- Create: validate + register --
|
|
387
|
+
|
|
388
|
+
export async function create(offeringName: string): Promise<void> {
|
|
389
|
+
await checkForLegacyOfferings();
|
|
390
|
+
if (!offeringName) {
|
|
391
|
+
output.fatal("Usage: acp sell create <offering_name>");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const dir = resolveOfferingDir(offeringName);
|
|
395
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
396
|
+
output.fatal(
|
|
397
|
+
`Offering directory not found: ${dir}\n Create it with: acp sell init ${offeringName}`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
output.log(`\nValidating offering: "${offeringName}"\n`);
|
|
402
|
+
|
|
403
|
+
const allErrors: string[] = [];
|
|
404
|
+
const allWarnings: string[] = [];
|
|
405
|
+
|
|
406
|
+
// Validate offering.json
|
|
407
|
+
output.log(" Checking offering.json...");
|
|
408
|
+
const jsonPath = path.join(dir, "offering.json");
|
|
409
|
+
const jsonResult = validateOfferingJson(jsonPath);
|
|
410
|
+
allErrors.push(...jsonResult.errors);
|
|
411
|
+
allWarnings.push(...jsonResult.warnings);
|
|
412
|
+
|
|
413
|
+
let parsedOffering: OfferingJson | null = null;
|
|
414
|
+
if (jsonResult.valid) {
|
|
415
|
+
parsedOffering = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
416
|
+
output.log(` Valid — Name: "${parsedOffering!.name}"`);
|
|
417
|
+
output.log(` Fee: ${parsedOffering!.jobFee} USDC`);
|
|
418
|
+
output.log(` Funds required: ${parsedOffering!.requiredFunds}`);
|
|
419
|
+
} else {
|
|
420
|
+
output.log(" Invalid");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Validate handlers.ts
|
|
424
|
+
output.log("\n Checking handlers.ts...");
|
|
425
|
+
const handlersPath = path.join(dir, "handlers.ts");
|
|
426
|
+
const handlersResult = validateHandlers(
|
|
427
|
+
handlersPath,
|
|
428
|
+
parsedOffering?.requiredFunds,
|
|
429
|
+
);
|
|
430
|
+
allErrors.push(...handlersResult.errors);
|
|
431
|
+
allWarnings.push(...handlersResult.warnings);
|
|
432
|
+
|
|
433
|
+
if (handlersResult.valid) {
|
|
434
|
+
output.log(" Valid — executeJob handler found");
|
|
435
|
+
} else {
|
|
436
|
+
output.log(" Invalid");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
output.log("\n" + "-".repeat(50));
|
|
440
|
+
|
|
441
|
+
if (allWarnings.length > 0) {
|
|
442
|
+
output.log("\n Warnings:");
|
|
443
|
+
allWarnings.forEach((w) => output.log(` - ${w}`));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (allErrors.length > 0) {
|
|
447
|
+
output.log("\n Errors:");
|
|
448
|
+
allErrors.forEach((e) => output.log(` - ${e}`));
|
|
449
|
+
output.fatal("\n Validation failed. Fix the errors above.");
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
output.log("\n Validation passed!\n");
|
|
453
|
+
|
|
454
|
+
// Register with ACP
|
|
455
|
+
const json: OfferingJson = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
456
|
+
const acpPayload = buildAcpPayload(json);
|
|
457
|
+
|
|
458
|
+
output.log(" Registering offering with ACP...");
|
|
459
|
+
const result = await createJobOffering(acpPayload);
|
|
460
|
+
|
|
461
|
+
if (result.success) {
|
|
462
|
+
output.log(" Offering registered successfully.\n");
|
|
463
|
+
} else {
|
|
464
|
+
output.fatal(" Failed to register offering with ACP.");
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Start seller if not running
|
|
468
|
+
output.log(" Tip: Run `acp serve start` to begin accepting jobs.\n");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// -- Delete: delist offering --
|
|
472
|
+
|
|
473
|
+
export async function del(offeringName: string): Promise<void> {
|
|
474
|
+
if (!offeringName) {
|
|
475
|
+
output.fatal("Usage: acp sell delete <offering_name>");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
output.log(`\n Delisting offering: "${offeringName}"...\n`);
|
|
479
|
+
|
|
480
|
+
const result = await deleteJobOffering(offeringName);
|
|
481
|
+
|
|
482
|
+
if (result.success) {
|
|
483
|
+
output.log(" Offering delisted from ACP. Local files remain.\n");
|
|
484
|
+
} else {
|
|
485
|
+
output.fatal(" Failed to delist offering from ACP.");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// -- List: show all offerings with status --
|
|
490
|
+
|
|
491
|
+
interface LocalOffering {
|
|
492
|
+
dirName: string;
|
|
493
|
+
name: string;
|
|
494
|
+
description: string;
|
|
495
|
+
jobFee: number;
|
|
496
|
+
jobFeeType: "fixed" | "percentage";
|
|
497
|
+
requiredFunds: boolean;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function listLocalOfferings(): LocalOffering[] {
|
|
501
|
+
const offeringsRoot = getOfferingsRoot();
|
|
502
|
+
if (!fs.existsSync(offeringsRoot)) return [];
|
|
503
|
+
|
|
504
|
+
return fs
|
|
505
|
+
.readdirSync(offeringsRoot, { withFileTypes: true })
|
|
506
|
+
.filter((d) => d.isDirectory())
|
|
507
|
+
.map((d) => {
|
|
508
|
+
const configPath = path.join(offeringsRoot, d.name, "offering.json");
|
|
509
|
+
if (!fs.existsSync(configPath)) return null;
|
|
510
|
+
try {
|
|
511
|
+
const json = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
512
|
+
return {
|
|
513
|
+
dirName: d.name,
|
|
514
|
+
name: json.name ?? d.name,
|
|
515
|
+
description: json.description ?? "",
|
|
516
|
+
jobFee: json.jobFee ?? 0,
|
|
517
|
+
jobFeeType: json.jobFeeType ?? "fixed",
|
|
518
|
+
requiredFunds: json.requiredFunds ?? false,
|
|
519
|
+
};
|
|
520
|
+
} catch {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
.filter((o): o is LocalOffering => o !== null);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
interface AcpOffering {
|
|
528
|
+
name: string;
|
|
529
|
+
priceV2?: { type: string; value: number };
|
|
530
|
+
slaMinutes?: number;
|
|
531
|
+
requiredFunds?: boolean;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async function fetchAcpOfferings(): Promise<AcpOffering[]> {
|
|
535
|
+
try {
|
|
536
|
+
const agentInfo = await getMyAgentInfo();
|
|
537
|
+
return agentInfo.jobs ?? [];
|
|
538
|
+
} catch {
|
|
539
|
+
// API error — can't determine ACP status
|
|
540
|
+
return [];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function acpOfferingNames(acpOfferings: AcpOffering[]): Set<string> {
|
|
545
|
+
return new Set(acpOfferings.map((o) => o.name));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export async function list(): Promise<void> {
|
|
549
|
+
await checkForLegacyOfferings();
|
|
550
|
+
const acpOfferings = await fetchAcpOfferings();
|
|
551
|
+
const acpNames = acpOfferingNames(acpOfferings);
|
|
552
|
+
const localOfferings = listLocalOfferings();
|
|
553
|
+
const localNames = new Set(localOfferings.map((o) => o.name));
|
|
554
|
+
|
|
555
|
+
const localData = localOfferings.map((o) => ({
|
|
556
|
+
...o,
|
|
557
|
+
listed: acpNames.has(o.name),
|
|
558
|
+
acpOnly: false as const,
|
|
559
|
+
}));
|
|
560
|
+
|
|
561
|
+
// ACP-only offerings: listed on ACP but no local directory
|
|
562
|
+
const acpOnlyData = acpOfferings
|
|
563
|
+
.filter((o) => !localNames.has(o.name))
|
|
564
|
+
.map((o) => ({
|
|
565
|
+
dirName: "",
|
|
566
|
+
name: o.name,
|
|
567
|
+
description: "",
|
|
568
|
+
jobFee: o.priceV2?.value ?? 0,
|
|
569
|
+
jobFeeType: o.priceV2?.type ?? "fixed",
|
|
570
|
+
requiredFunds: o.requiredFunds ?? false,
|
|
571
|
+
listed: true,
|
|
572
|
+
acpOnly: true as const,
|
|
573
|
+
}));
|
|
574
|
+
|
|
575
|
+
const data = [...localData, ...acpOnlyData];
|
|
576
|
+
|
|
577
|
+
output.output(data, (offerings) => {
|
|
578
|
+
output.heading("Job Offerings");
|
|
579
|
+
|
|
580
|
+
if (offerings.length === 0) {
|
|
581
|
+
output.log(
|
|
582
|
+
" No offerings found. Run `acp sell init <name>` to create one.\n",
|
|
583
|
+
);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
for (const o of offerings) {
|
|
588
|
+
const status = o.acpOnly
|
|
589
|
+
? "Listed on ACP (no local files)"
|
|
590
|
+
: o.listed
|
|
591
|
+
? "Listed"
|
|
592
|
+
: "Local only";
|
|
593
|
+
output.log(`\n ${o.name}`);
|
|
594
|
+
if (!o.acpOnly) {
|
|
595
|
+
output.field(" Description", o.description);
|
|
596
|
+
}
|
|
597
|
+
output.field(" Fee", `${formatPrice(o.jobFee, o.jobFeeType)}`);
|
|
598
|
+
output.field(" Funds required", String(o.requiredFunds));
|
|
599
|
+
output.field(" Status", status);
|
|
600
|
+
if (o.acpOnly) {
|
|
601
|
+
output.log(
|
|
602
|
+
" Tip: Run `acp sell delete " + o.name + "` to delist from ACP",
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
output.log("");
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// -- Inspect: detailed view --
|
|
611
|
+
|
|
612
|
+
function detectHandlers(offeringDir: string): string[] {
|
|
613
|
+
const handlersPath = path.join(
|
|
614
|
+
getOfferingsRoot(),
|
|
615
|
+
offeringDir,
|
|
616
|
+
"handlers.ts",
|
|
617
|
+
);
|
|
618
|
+
if (!fs.existsSync(handlersPath)) return [];
|
|
619
|
+
|
|
620
|
+
const content = fs.readFileSync(handlersPath, "utf-8");
|
|
621
|
+
const found: string[] = [];
|
|
622
|
+
|
|
623
|
+
if (/export\s+(async\s+)?function\s+executeJob\s*\(/.test(content)) {
|
|
624
|
+
found.push("executeJob");
|
|
625
|
+
}
|
|
626
|
+
if (
|
|
627
|
+
/export\s+(async\s+)?function\s+validateRequirements\s*\(/.test(content)
|
|
628
|
+
) {
|
|
629
|
+
found.push("validateRequirements");
|
|
630
|
+
}
|
|
631
|
+
if (/export\s+(async\s+)?function\s+requestPayment\s*\(/.test(content)) {
|
|
632
|
+
found.push("requestPayment");
|
|
633
|
+
}
|
|
634
|
+
if (
|
|
635
|
+
/export\s+(async\s+)?function\s+requestAdditionalFunds\s*\(/.test(content)
|
|
636
|
+
) {
|
|
637
|
+
found.push("requestAdditionalFunds");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return found;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export async function inspect(offeringName: string): Promise<void> {
|
|
644
|
+
if (!offeringName) {
|
|
645
|
+
output.fatal("Usage: acp sell inspect <offering_name>");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const dir = resolveOfferingDir(offeringName);
|
|
649
|
+
const configPath = path.join(dir, "offering.json");
|
|
650
|
+
|
|
651
|
+
if (!fs.existsSync(configPath)) {
|
|
652
|
+
output.fatal(`Offering not found: ${offeringName}`);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const json = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
656
|
+
const acpOfferings = await fetchAcpOfferings();
|
|
657
|
+
const isListed = acpOfferingNames(acpOfferings).has(json.name);
|
|
658
|
+
const handlers = detectHandlers(offeringName);
|
|
659
|
+
|
|
660
|
+
const data = {
|
|
661
|
+
...json,
|
|
662
|
+
listed: isListed,
|
|
663
|
+
handlers,
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
output.output(data, (d) => {
|
|
667
|
+
output.heading(`Offering: ${d.name}`);
|
|
668
|
+
output.field("Description", d.description);
|
|
669
|
+
output.field("Fee", `${d.jobFee} USDC`);
|
|
670
|
+
output.field("Funds required", String(d.requiredFunds));
|
|
671
|
+
output.field("Status", d.listed ? "Listed on ACP" : "Local only");
|
|
672
|
+
output.field("Handlers", d.handlers.join(", ") || "(none)");
|
|
673
|
+
if (d.requirement) {
|
|
674
|
+
output.log("\n Requirement Schema:");
|
|
675
|
+
output.log(
|
|
676
|
+
JSON.stringify(d.requirement, null, 4)
|
|
677
|
+
.split("\n")
|
|
678
|
+
.map((line: string) => ` ${line}`)
|
|
679
|
+
.join("\n"),
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
output.log("");
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// =============================================================================
|
|
687
|
+
// Resource Management
|
|
688
|
+
// =============================================================================
|
|
689
|
+
|
|
690
|
+
function resolveResourceDir(resourceName: string): string {
|
|
691
|
+
return path.resolve(RESOURCES_ROOT, resourceName);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
interface LocalResource {
|
|
695
|
+
dirName: string;
|
|
696
|
+
name: string;
|
|
697
|
+
description: string;
|
|
698
|
+
url: string;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function fetchAcpResourceNames(): Promise<Resource[]> {
|
|
702
|
+
try {
|
|
703
|
+
const agentInfo = (await getMyAgentInfo()) as {
|
|
704
|
+
jobs?: unknown[];
|
|
705
|
+
resources?: Resource[];
|
|
706
|
+
};
|
|
707
|
+
return agentInfo.resources ?? [];
|
|
708
|
+
} catch {
|
|
709
|
+
return [];
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export async function resourceList(): Promise<void> {
|
|
714
|
+
const resources = await fetchAcpResourceNames();
|
|
715
|
+
|
|
716
|
+
output.output(resources, (resources) => {
|
|
717
|
+
output.heading("Resources");
|
|
718
|
+
|
|
719
|
+
if (resources.length === 0) {
|
|
720
|
+
output.log(
|
|
721
|
+
" No resources found. Run `acp sell resource init <name>` to create one.\n",
|
|
722
|
+
);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
for (const r of resources) {
|
|
727
|
+
output.log(`\n ${r.name}`);
|
|
728
|
+
if (!r.acpOnly) {
|
|
729
|
+
output.field(" Description", r.description);
|
|
730
|
+
output.field(" URL", r.url);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
output.log("");
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function validateResourceJson(filePath: string): ValidationResult {
|
|
738
|
+
const result: ValidationResult = { valid: true, errors: [], warnings: [] };
|
|
739
|
+
|
|
740
|
+
if (!fs.existsSync(filePath)) {
|
|
741
|
+
result.valid = false;
|
|
742
|
+
result.errors.push(`resources.json not found at ${filePath}`);
|
|
743
|
+
return result;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
let json: any;
|
|
747
|
+
try {
|
|
748
|
+
json = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
749
|
+
} catch (err) {
|
|
750
|
+
result.valid = false;
|
|
751
|
+
result.errors.push(`Invalid JSON in resources.json: ${err}`);
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (!json.name || typeof json.name !== "string" || json.name.trim() === "") {
|
|
756
|
+
result.valid = false;
|
|
757
|
+
result.errors.push('"name" field is required (non-empty string)');
|
|
758
|
+
}
|
|
759
|
+
if (
|
|
760
|
+
!json.description ||
|
|
761
|
+
typeof json.description !== "string" ||
|
|
762
|
+
json.description.trim() === ""
|
|
763
|
+
) {
|
|
764
|
+
result.valid = false;
|
|
765
|
+
result.errors.push('"description" field is required (non-empty string)');
|
|
766
|
+
}
|
|
767
|
+
if (!json.url || typeof json.url !== "string" || json.url.trim() === "") {
|
|
768
|
+
result.valid = false;
|
|
769
|
+
result.errors.push('"url" field is required (non-empty string)');
|
|
770
|
+
}
|
|
771
|
+
if (json.params !== undefined && json.params !== null) {
|
|
772
|
+
if (typeof json.params !== "object" || Array.isArray(json.params)) {
|
|
773
|
+
result.valid = false;
|
|
774
|
+
result.errors.push('"params" field must be an object if provided');
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// -- Resource Init: scaffold a new resource --
|
|
782
|
+
|
|
783
|
+
export async function resourceInit(resourceName: string): Promise<void> {
|
|
784
|
+
if (!resourceName) {
|
|
785
|
+
output.fatal("Usage: acp sell resource init <resource_name>");
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const dir = resolveResourceDir(resourceName);
|
|
789
|
+
if (fs.existsSync(dir)) {
|
|
790
|
+
output.fatal(`Resource directory already exists: ${dir}`);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
794
|
+
|
|
795
|
+
const resourceJson = {
|
|
796
|
+
name: resourceName,
|
|
797
|
+
description: "TODO: Describe what this resource provides",
|
|
798
|
+
url: "https://api.example.com/endpoint",
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
fs.writeFileSync(
|
|
802
|
+
path.join(dir, "resources.json"),
|
|
803
|
+
JSON.stringify(resourceJson, null, 2) + "\n",
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
output.output({ created: dir }, () => {
|
|
807
|
+
output.heading("Resource Scaffolded");
|
|
808
|
+
output.log(` Created: src/seller/resources/${resourceName}/`);
|
|
809
|
+
output.log(` - resources.json (edit name, description, url, params)`);
|
|
810
|
+
output.log(
|
|
811
|
+
`\n Next: edit the file, then run: acp sell resource create ${resourceName}\n`,
|
|
812
|
+
);
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// -- Resource Create: validate + register --
|
|
817
|
+
|
|
818
|
+
export async function resourceCreate(resourceName: string): Promise<void> {
|
|
819
|
+
if (!resourceName) {
|
|
820
|
+
output.fatal("Usage: acp sell resource create <resource_name>");
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const dir = resolveResourceDir(resourceName);
|
|
824
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
|
|
825
|
+
output.fatal(
|
|
826
|
+
`Resource directory not found: ${dir}\n Create it with: acp sell resource init ${resourceName}`,
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
output.log(`\nValidating resource: "${resourceName}"\n`);
|
|
831
|
+
|
|
832
|
+
const jsonPath = path.join(dir, "resources.json");
|
|
833
|
+
const validation = validateResourceJson(jsonPath);
|
|
834
|
+
|
|
835
|
+
if (!validation.valid) {
|
|
836
|
+
output.log(" Errors:");
|
|
837
|
+
validation.errors.forEach((e) => output.log(` - ${e}`));
|
|
838
|
+
output.fatal("\n Validation failed. Fix the errors above.");
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (validation.warnings.length > 0) {
|
|
842
|
+
output.log(" Warnings:");
|
|
843
|
+
validation.warnings.forEach((w) => output.log(` - ${w}`));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
output.log(" Validation passed!\n");
|
|
847
|
+
|
|
848
|
+
// Register with ACP
|
|
849
|
+
const json: any = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
|
|
850
|
+
const resource: Resource = {
|
|
851
|
+
name: json.name,
|
|
852
|
+
description: json.description,
|
|
853
|
+
url: json.url,
|
|
854
|
+
params: json.params,
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
output.log(" Registering resource with ACP...");
|
|
858
|
+
const result = await upsertResourceApi(resource);
|
|
859
|
+
|
|
860
|
+
if (result.success) {
|
|
861
|
+
output.log(" Resource registered successfully.\n");
|
|
862
|
+
} else {
|
|
863
|
+
output.fatal(" Failed to register resource with ACP.");
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// -- Resource Delete: delete resource --
|
|
868
|
+
|
|
869
|
+
export async function resourceDelete(resourceName: string): Promise<void> {
|
|
870
|
+
if (!resourceName) {
|
|
871
|
+
output.fatal("Usage: acp sell resource delete <resource_name>");
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
output.log(`\n Deleting resource: "${resourceName}"...\n`);
|
|
875
|
+
|
|
876
|
+
const result = await deleteResourceApi(resourceName);
|
|
877
|
+
|
|
878
|
+
if (result.success) {
|
|
879
|
+
output.log(" Resource deleted from ACP.\n");
|
|
880
|
+
} else {
|
|
881
|
+
output.fatal(" Failed to delete resource from ACP.");
|
|
882
|
+
}
|
|
883
|
+
}
|