@clankxyz/cli 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 +295 -0
- package/dist/index.js +2389 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2389 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
var config_exports = {};
|
|
14
|
+
__export(config_exports, {
|
|
15
|
+
clearConfig: () => clearConfig,
|
|
16
|
+
config: () => config,
|
|
17
|
+
fetchRemoteConfig: () => fetchRemoteConfig,
|
|
18
|
+
getConfig: () => getConfig,
|
|
19
|
+
getConfigPath: () => getConfigPath,
|
|
20
|
+
getEffectiveConfig: () => getEffectiveConfig,
|
|
21
|
+
setConfig: () => setConfig
|
|
22
|
+
});
|
|
23
|
+
import Conf from "conf";
|
|
24
|
+
function getConfig() {
|
|
25
|
+
return {
|
|
26
|
+
apiUrl: config.get("apiUrl"),
|
|
27
|
+
apiKey: config.get("apiKey"),
|
|
28
|
+
network: config.get("network"),
|
|
29
|
+
rpcUrl: config.get("rpcUrl"),
|
|
30
|
+
packageId: config.get("packageId"),
|
|
31
|
+
walrusAggregator: config.get("walrusAggregator"),
|
|
32
|
+
walrusPublisher: config.get("walrusPublisher"),
|
|
33
|
+
agentId: config.get("agentId"),
|
|
34
|
+
agentName: config.get("agentName"),
|
|
35
|
+
outputFormat: config.get("outputFormat")
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function setConfig(key, value) {
|
|
39
|
+
config.set(key, value);
|
|
40
|
+
}
|
|
41
|
+
function clearConfig() {
|
|
42
|
+
config.clear();
|
|
43
|
+
}
|
|
44
|
+
function getConfigPath() {
|
|
45
|
+
return config.path;
|
|
46
|
+
}
|
|
47
|
+
async function fetchRemoteConfig() {
|
|
48
|
+
if (cachedRemoteConfig) {
|
|
49
|
+
return cachedRemoteConfig;
|
|
50
|
+
}
|
|
51
|
+
const apiUrl = config.get("apiUrl");
|
|
52
|
+
if (!apiUrl || apiUrl === "http://localhost:3000") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetch(`${apiUrl}/api/config`);
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
cachedRemoteConfig = await response.json();
|
|
61
|
+
return cachedRemoteConfig;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function getEffectiveConfig() {
|
|
67
|
+
const local = getConfig();
|
|
68
|
+
const remote = await fetchRemoteConfig();
|
|
69
|
+
return {
|
|
70
|
+
...local,
|
|
71
|
+
// Use local values if set, otherwise fall back to remote
|
|
72
|
+
packageId: local.packageId || remote?.package_id || "",
|
|
73
|
+
walrusAggregator: local.walrusAggregator || remote?.walrus.aggregator,
|
|
74
|
+
walrusPublisher: local.walrusPublisher || remote?.walrus.publisher
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
var cachedRemoteConfig, defaults, config;
|
|
78
|
+
var init_config = __esm({
|
|
79
|
+
"src/config.ts"() {
|
|
80
|
+
"use strict";
|
|
81
|
+
cachedRemoteConfig = null;
|
|
82
|
+
defaults = {
|
|
83
|
+
apiUrl: "http://localhost:3000",
|
|
84
|
+
network: "testnet",
|
|
85
|
+
outputFormat: "table"
|
|
86
|
+
};
|
|
87
|
+
config = new Conf({
|
|
88
|
+
projectName: "clank-cli",
|
|
89
|
+
projectVersion: "0.1.0",
|
|
90
|
+
defaults,
|
|
91
|
+
schema: {
|
|
92
|
+
apiUrl: { type: "string" },
|
|
93
|
+
apiKey: { type: "string" },
|
|
94
|
+
network: { type: "string", enum: ["mainnet", "testnet", "devnet", "localnet"] },
|
|
95
|
+
rpcUrl: { type: "string" },
|
|
96
|
+
packageId: { type: "string" },
|
|
97
|
+
walrusAggregator: { type: "string" },
|
|
98
|
+
walrusPublisher: { type: "string" },
|
|
99
|
+
agentId: { type: "string" },
|
|
100
|
+
agentName: { type: "string" },
|
|
101
|
+
outputFormat: { type: "string", enum: ["table", "json"] }
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// src/index.ts
|
|
108
|
+
import { Command } from "commander";
|
|
109
|
+
import chalk3 from "chalk";
|
|
110
|
+
|
|
111
|
+
// src/commands/config.ts
|
|
112
|
+
init_config();
|
|
113
|
+
|
|
114
|
+
// src/utils.ts
|
|
115
|
+
init_config();
|
|
116
|
+
import chalk from "chalk";
|
|
117
|
+
import ora from "ora";
|
|
118
|
+
import { SuiClient } from "@mysten/sui/client";
|
|
119
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
120
|
+
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
|
|
121
|
+
import { FULLNODE_URLS } from "@clankxyz/shared";
|
|
122
|
+
var colors = {
|
|
123
|
+
primary: chalk.cyan,
|
|
124
|
+
success: chalk.green,
|
|
125
|
+
warning: chalk.yellow,
|
|
126
|
+
error: chalk.red,
|
|
127
|
+
muted: chalk.gray,
|
|
128
|
+
highlight: chalk.bold.white
|
|
129
|
+
};
|
|
130
|
+
function log(message) {
|
|
131
|
+
console.log(message);
|
|
132
|
+
}
|
|
133
|
+
function success(message) {
|
|
134
|
+
console.log(colors.success("\u2713"), message);
|
|
135
|
+
}
|
|
136
|
+
function error(message) {
|
|
137
|
+
console.error(colors.error("\u2717"), message);
|
|
138
|
+
}
|
|
139
|
+
function spinner(text) {
|
|
140
|
+
return ora({ text, color: "cyan" });
|
|
141
|
+
}
|
|
142
|
+
function formatAddress(address, length = 8) {
|
|
143
|
+
if (address.length <= length * 2 + 2) return address;
|
|
144
|
+
return `${address.slice(0, length + 2)}...${address.slice(-length)}`;
|
|
145
|
+
}
|
|
146
|
+
var MIST_PER_SUI = 1e9;
|
|
147
|
+
function formatSui(amount) {
|
|
148
|
+
const value = typeof amount === "string" ? BigInt(amount) : amount;
|
|
149
|
+
const sui = Number(value) / MIST_PER_SUI;
|
|
150
|
+
const formatted = sui.toFixed(4).replace(/\.?0+$/, "");
|
|
151
|
+
return `${formatted} SUI`;
|
|
152
|
+
}
|
|
153
|
+
function formatTimestamp(timestamp) {
|
|
154
|
+
const ms = typeof timestamp === "bigint" ? Number(timestamp) : Number(timestamp);
|
|
155
|
+
return new Date(ms).toLocaleString();
|
|
156
|
+
}
|
|
157
|
+
function formatDuration(seconds) {
|
|
158
|
+
if (seconds < 60) return `${seconds}s`;
|
|
159
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
|
160
|
+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
|
|
161
|
+
return `${Math.floor(seconds / 86400)}d`;
|
|
162
|
+
}
|
|
163
|
+
function formatPercent(value) {
|
|
164
|
+
if (value === null || value === void 0) return "-";
|
|
165
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
166
|
+
}
|
|
167
|
+
function table(headers, rows, options = {}) {
|
|
168
|
+
const maxWidth = options.maxWidth ?? 20;
|
|
169
|
+
const widths = headers.map(
|
|
170
|
+
(h, i) => Math.min(
|
|
171
|
+
maxWidth,
|
|
172
|
+
Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
|
|
176
|
+
const separator = widths.map((w) => "-".repeat(w)).join(" ");
|
|
177
|
+
const rowLines = rows.map(
|
|
178
|
+
(row) => row.map((cell, i) => (cell ?? "").padEnd(widths[i])).join(" ")
|
|
179
|
+
);
|
|
180
|
+
return [
|
|
181
|
+
colors.muted(headerLine),
|
|
182
|
+
colors.muted(separator),
|
|
183
|
+
...rowLines
|
|
184
|
+
].join("\n");
|
|
185
|
+
}
|
|
186
|
+
function jsonOutput(data) {
|
|
187
|
+
console.log(JSON.stringify(data, null, 2));
|
|
188
|
+
}
|
|
189
|
+
function output(data, tableFormatter) {
|
|
190
|
+
const config2 = getConfig();
|
|
191
|
+
if (config2.outputFormat === "json") {
|
|
192
|
+
jsonOutput(data);
|
|
193
|
+
} else if (tableFormatter) {
|
|
194
|
+
log(tableFormatter());
|
|
195
|
+
} else {
|
|
196
|
+
jsonOutput(data);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function requireConfig(key) {
|
|
200
|
+
const config2 = getConfig();
|
|
201
|
+
const value = config2[key];
|
|
202
|
+
if (!value) {
|
|
203
|
+
error(`Missing configuration: ${key}`);
|
|
204
|
+
error(`Run: clank config set ${key} <value>`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
function requireApiKey() {
|
|
210
|
+
return requireConfig("apiKey");
|
|
211
|
+
}
|
|
212
|
+
function getSuiClient() {
|
|
213
|
+
const config2 = getConfig();
|
|
214
|
+
const rpcUrl = config2.rpcUrl ?? FULLNODE_URLS[config2.network] ?? FULLNODE_URLS.mainnet;
|
|
215
|
+
return new SuiClient({ url: rpcUrl });
|
|
216
|
+
}
|
|
217
|
+
async function loadKeypair(keystorePath, ownerAddress) {
|
|
218
|
+
const fs = await import("fs");
|
|
219
|
+
const os = await import("os");
|
|
220
|
+
const path = await import("path");
|
|
221
|
+
const defaultPath = path.join(os.homedir(), ".sui", "sui_config", "sui.keystore");
|
|
222
|
+
const keyPath = keystorePath ?? defaultPath;
|
|
223
|
+
if (!fs.existsSync(keyPath)) {
|
|
224
|
+
throw new Error(`Sui keystore not found at: ${keyPath}`);
|
|
225
|
+
}
|
|
226
|
+
const keystoreData = fs.readFileSync(keyPath, "utf-8");
|
|
227
|
+
const keys = JSON.parse(keystoreData);
|
|
228
|
+
if (keys.length === 0) {
|
|
229
|
+
throw new Error("No keys found in Sui keystore");
|
|
230
|
+
}
|
|
231
|
+
if (ownerAddress) {
|
|
232
|
+
const normalizedOwner = ownerAddress.toLowerCase();
|
|
233
|
+
for (const encodedKey of keys) {
|
|
234
|
+
try {
|
|
235
|
+
const { secretKey: secretKey2 } = decodeSuiPrivateKey(encodedKey);
|
|
236
|
+
const keypair = Ed25519Keypair.fromSecretKey(secretKey2);
|
|
237
|
+
const address = keypair.getPublicKey().toSuiAddress().toLowerCase();
|
|
238
|
+
if (address === normalizedOwner) {
|
|
239
|
+
return keypair;
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
throw new Error(`No key found for address: ${ownerAddress}`);
|
|
246
|
+
}
|
|
247
|
+
const { secretKey } = decodeSuiPrivateKey(keys[0]);
|
|
248
|
+
return Ed25519Keypair.fromSecretKey(secretKey);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/commands/config.ts
|
|
252
|
+
function registerConfigCommands(program2) {
|
|
253
|
+
const configCmd = program2.command("config").description("Manage CLI configuration");
|
|
254
|
+
configCmd.command("show").description("Show current configuration").option("--json", "Output as JSON").action((options) => {
|
|
255
|
+
const config2 = getConfig();
|
|
256
|
+
if (options.json) {
|
|
257
|
+
console.log(JSON.stringify(config2, null, 2));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
log(colors.primary("\nClank CLI Configuration\n"));
|
|
261
|
+
log(colors.muted(`Config file: ${getConfigPath()}
|
|
262
|
+
`));
|
|
263
|
+
const rows = Object.entries(config2).map(([key, value]) => [
|
|
264
|
+
colors.highlight(key),
|
|
265
|
+
value !== void 0 ? String(value) : colors.muted("(not set)")
|
|
266
|
+
]);
|
|
267
|
+
log(table(["Key", "Value"], rows));
|
|
268
|
+
log("");
|
|
269
|
+
});
|
|
270
|
+
configCmd.command("get <key>").description("Get a configuration value").action((key) => {
|
|
271
|
+
const config2 = getConfig();
|
|
272
|
+
const value = config2[key];
|
|
273
|
+
if (value === void 0) {
|
|
274
|
+
error(`Configuration key "${key}" is not set`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
log(String(value));
|
|
278
|
+
});
|
|
279
|
+
configCmd.command("set <key> <value>").description("Set a configuration value").action((key, value) => {
|
|
280
|
+
const validKeys = [
|
|
281
|
+
"apiUrl",
|
|
282
|
+
"apiKey",
|
|
283
|
+
"network",
|
|
284
|
+
"rpcUrl",
|
|
285
|
+
"packageId",
|
|
286
|
+
"walrusAggregator",
|
|
287
|
+
"walrusPublisher",
|
|
288
|
+
"agentId",
|
|
289
|
+
"agentName",
|
|
290
|
+
"outputFormat"
|
|
291
|
+
];
|
|
292
|
+
if (!validKeys.includes(key)) {
|
|
293
|
+
error(`Invalid configuration key: ${key}`);
|
|
294
|
+
log(`
|
|
295
|
+
Valid keys: ${validKeys.join(", ")}`);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
if (key === "network") {
|
|
299
|
+
const validNetworks = ["mainnet", "testnet", "devnet", "localnet"];
|
|
300
|
+
if (!validNetworks.includes(value)) {
|
|
301
|
+
error(`Invalid network: ${value}`);
|
|
302
|
+
log(`Valid networks: ${validNetworks.join(", ")}`);
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (key === "outputFormat") {
|
|
307
|
+
const validFormats = ["table", "json"];
|
|
308
|
+
if (!validFormats.includes(value)) {
|
|
309
|
+
error(`Invalid output format: ${value}`);
|
|
310
|
+
log(`Valid formats: ${validFormats.join(", ")}`);
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
setConfig(key, value);
|
|
315
|
+
success(`Set ${key} = ${value}`);
|
|
316
|
+
});
|
|
317
|
+
configCmd.command("unset <key>").description("Unset a configuration value").action((key) => {
|
|
318
|
+
setConfig(key, void 0);
|
|
319
|
+
success(`Unset ${key}`);
|
|
320
|
+
});
|
|
321
|
+
configCmd.command("reset").description("Reset all configuration to defaults").option("-y, --yes", "Skip confirmation").action(async (options) => {
|
|
322
|
+
if (!options.yes) {
|
|
323
|
+
const prompts = await import("prompts");
|
|
324
|
+
const { confirm } = await prompts.default({
|
|
325
|
+
type: "confirm",
|
|
326
|
+
name: "confirm",
|
|
327
|
+
message: "Reset all configuration to defaults?",
|
|
328
|
+
initial: false
|
|
329
|
+
});
|
|
330
|
+
if (!confirm) {
|
|
331
|
+
log("Cancelled");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
clearConfig();
|
|
336
|
+
success("Configuration reset to defaults");
|
|
337
|
+
});
|
|
338
|
+
configCmd.command("path").description("Show configuration file path").action(() => {
|
|
339
|
+
log(getConfigPath());
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/client.ts
|
|
344
|
+
init_config();
|
|
345
|
+
import { ClankClient } from "@clankxyz/sdk";
|
|
346
|
+
var clientInstance = null;
|
|
347
|
+
function getClient() {
|
|
348
|
+
if (clientInstance) return clientInstance;
|
|
349
|
+
const config2 = getConfig();
|
|
350
|
+
clientInstance = new ClankClient({
|
|
351
|
+
apiUrl: config2.apiUrl,
|
|
352
|
+
apiKey: config2.apiKey,
|
|
353
|
+
network: config2.network,
|
|
354
|
+
rpcUrl: config2.rpcUrl,
|
|
355
|
+
packageId: config2.packageId,
|
|
356
|
+
walrusAggregator: config2.walrusAggregator,
|
|
357
|
+
walrusPublisher: config2.walrusPublisher,
|
|
358
|
+
agentId: config2.agentId
|
|
359
|
+
});
|
|
360
|
+
return clientInstance;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/commands/agent.ts
|
|
364
|
+
init_config();
|
|
365
|
+
function generateSignatureChallenge(agentId, action, timestamp) {
|
|
366
|
+
return `Clank Verification
|
|
367
|
+
|
|
368
|
+
Agent: ${agentId}
|
|
369
|
+
Action: ${action}
|
|
370
|
+
Timestamp: ${timestamp}
|
|
371
|
+
|
|
372
|
+
Sign this message to verify ownership.`;
|
|
373
|
+
}
|
|
374
|
+
function registerAgentCommands(program2) {
|
|
375
|
+
const agentCmd = program2.command("agent").description("Manage agents");
|
|
376
|
+
agentCmd.command("list").description("List all agents").option("-p, --page <number>", "Page number", "1").option("-l, --limit <number>", "Items per page", "20").action(async (options) => {
|
|
377
|
+
const spin = spinner("Fetching agents...").start();
|
|
378
|
+
try {
|
|
379
|
+
const client = getClient();
|
|
380
|
+
const result = await client.api.listAgents({
|
|
381
|
+
page: parseInt(options.page),
|
|
382
|
+
limit: parseInt(options.limit)
|
|
383
|
+
});
|
|
384
|
+
spin.stop();
|
|
385
|
+
output(result, () => {
|
|
386
|
+
const rows = result.data.map((agent) => [
|
|
387
|
+
formatAddress(agent.id),
|
|
388
|
+
formatAddress(agent.owner),
|
|
389
|
+
String(agent.skills_count),
|
|
390
|
+
String(agent.tasks_as_worker),
|
|
391
|
+
formatPercent(agent.stats?.completion_rate)
|
|
392
|
+
]);
|
|
393
|
+
return `
|
|
394
|
+
${colors.primary("Agents")} (${result.pagination.total} total)
|
|
395
|
+
|
|
396
|
+
` + table(["ID", "Owner", "Skills", "Tasks Done", "Success"], rows) + `
|
|
397
|
+
|
|
398
|
+
Page ${result.pagination.page}/${result.pagination.pages}`;
|
|
399
|
+
});
|
|
400
|
+
} catch (err) {
|
|
401
|
+
spin.stop();
|
|
402
|
+
error(`Failed to list agents: ${err.message}`);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
agentCmd.command("info [id]").description("Get agent details (uses configured agent if no ID)").action(async (id) => {
|
|
407
|
+
const agentId = id ?? getConfig().agentId;
|
|
408
|
+
if (!agentId) {
|
|
409
|
+
error("No agent ID provided. Set one with: clank config set agentId <id>");
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
const spin = spinner("Fetching agent...").start();
|
|
413
|
+
try {
|
|
414
|
+
const client = getClient();
|
|
415
|
+
const agent = await client.api.getAgent(agentId);
|
|
416
|
+
spin.stop();
|
|
417
|
+
output(agent, () => {
|
|
418
|
+
let output2 = `
|
|
419
|
+
${colors.primary("Agent Details")}
|
|
420
|
+
|
|
421
|
+
`;
|
|
422
|
+
output2 += `${colors.muted("ID:")} ${agent.id}
|
|
423
|
+
`;
|
|
424
|
+
output2 += `${colors.muted("Owner:")} ${agent.owner}
|
|
425
|
+
`;
|
|
426
|
+
output2 += `${colors.muted("Created:")} ${formatTimestamp(agent.created_at)}
|
|
427
|
+
`;
|
|
428
|
+
output2 += `${colors.muted("Metadata:")} ${agent.metadata_uri ?? "(none)"}
|
|
429
|
+
`;
|
|
430
|
+
if (agent.skills.length > 0) {
|
|
431
|
+
output2 += `
|
|
432
|
+
${colors.primary("Skills")} (${agent.skills.length})
|
|
433
|
+
|
|
434
|
+
`;
|
|
435
|
+
const skillRows = agent.skills.map((s) => [
|
|
436
|
+
s.name,
|
|
437
|
+
s.version,
|
|
438
|
+
formatSui(s.base_price_mist)
|
|
439
|
+
]);
|
|
440
|
+
output2 += table(["Name", "Version", "Price"], skillRows);
|
|
441
|
+
}
|
|
442
|
+
if (agent.badges.length > 0) {
|
|
443
|
+
output2 += `
|
|
444
|
+
|
|
445
|
+
${colors.primary("Badges")} (${agent.badges.length})
|
|
446
|
+
|
|
447
|
+
`;
|
|
448
|
+
const badgeRows = agent.badges.map((b) => [
|
|
449
|
+
b.badge_type,
|
|
450
|
+
`Tier ${b.tier}`,
|
|
451
|
+
formatTimestamp(b.earned_at)
|
|
452
|
+
]);
|
|
453
|
+
output2 += table(["Type", "Tier", "Earned"], badgeRows);
|
|
454
|
+
}
|
|
455
|
+
return output2;
|
|
456
|
+
});
|
|
457
|
+
} catch (err) {
|
|
458
|
+
spin.stop();
|
|
459
|
+
error(`Failed to get agent: ${err.message}`);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
agentCmd.command("stats [id]").description("Get agent statistics").action(async (id) => {
|
|
464
|
+
const agentId = id ?? getConfig().agentId;
|
|
465
|
+
if (!agentId) {
|
|
466
|
+
error("No agent ID provided");
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
const spin = spinner("Fetching stats...").start();
|
|
470
|
+
try {
|
|
471
|
+
const client = getClient();
|
|
472
|
+
const stats = await client.api.getAgentStats(agentId);
|
|
473
|
+
spin.stop();
|
|
474
|
+
output(stats, () => {
|
|
475
|
+
let output2 = `
|
|
476
|
+
${colors.primary("Agent Statistics")}
|
|
477
|
+
|
|
478
|
+
`;
|
|
479
|
+
output2 += `${colors.highlight("Worker Stats")}
|
|
480
|
+
`;
|
|
481
|
+
output2 += ` Tasks Completed: ${stats.workerTasksCompleted}
|
|
482
|
+
`;
|
|
483
|
+
output2 += ` Tasks Failed: ${stats.workerTasksFailed}
|
|
484
|
+
`;
|
|
485
|
+
output2 += ` Tasks Expired: ${stats.workerTasksExpired}
|
|
486
|
+
`;
|
|
487
|
+
output2 += ` Total Earned: ${formatSui(stats.workerTotalEarnedMist)}
|
|
488
|
+
`;
|
|
489
|
+
output2 += ` Completion Rate: ${formatPercent(stats.completionRate)}
|
|
490
|
+
`;
|
|
491
|
+
output2 += `
|
|
492
|
+
${colors.highlight("Requester Stats")}
|
|
493
|
+
`;
|
|
494
|
+
output2 += ` Tasks Posted: ${stats.requesterTasksPosted}
|
|
495
|
+
`;
|
|
496
|
+
output2 += ` Tasks Settled: ${stats.requesterTasksSettled}
|
|
497
|
+
`;
|
|
498
|
+
output2 += ` Tasks Cancelled: ${stats.requesterTasksCancelled}
|
|
499
|
+
`;
|
|
500
|
+
output2 += ` Tasks Rejected: ${stats.requesterTasksRejected}
|
|
501
|
+
`;
|
|
502
|
+
output2 += ` Total Spent: ${formatSui(stats.requesterTotalSpentMist)}
|
|
503
|
+
`;
|
|
504
|
+
output2 += ` Rejection Rate: ${formatPercent(stats.rejectionRate)}
|
|
505
|
+
`;
|
|
506
|
+
output2 += `
|
|
507
|
+
${colors.highlight("Metrics")}
|
|
508
|
+
`;
|
|
509
|
+
output2 += ` Unique Partners: ${stats.uniqueCounterparties}
|
|
510
|
+
`;
|
|
511
|
+
output2 += ` Flagged: ${stats.flaggedForAbuse ? colors.error("Yes") : colors.success("No")}
|
|
512
|
+
`;
|
|
513
|
+
return output2;
|
|
514
|
+
});
|
|
515
|
+
} catch (err) {
|
|
516
|
+
spin.stop();
|
|
517
|
+
error(`Failed to get stats: ${err.message}`);
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
agentCmd.command("use <id>").description("Set the current agent for commands").option("-n, --name <name>", "Agent name for display").action((id, options) => {
|
|
522
|
+
setConfig("agentId", id);
|
|
523
|
+
if (options.name) {
|
|
524
|
+
setConfig("agentName", options.name);
|
|
525
|
+
}
|
|
526
|
+
success(`Now using agent: ${formatAddress(id)}`);
|
|
527
|
+
});
|
|
528
|
+
agentCmd.command("current").description("Show current agent").action(() => {
|
|
529
|
+
const config2 = getConfig();
|
|
530
|
+
if (!config2.agentId) {
|
|
531
|
+
log("No agent configured. Use: clank agent use <id>");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
log(`
|
|
535
|
+
Current Agent:`);
|
|
536
|
+
log(` ID: ${config2.agentId}`);
|
|
537
|
+
if (config2.agentName) {
|
|
538
|
+
log(` Name: ${config2.agentName}`);
|
|
539
|
+
}
|
|
540
|
+
log("");
|
|
541
|
+
});
|
|
542
|
+
agentCmd.command("link <agent-id>").description("Link an on-chain agent to your API key (requires wallet signature)").option("-k, --api-key <key>", "API key to link (uses configured key if not provided)").option("--keystore <path>", "Path to sui.keystore file").action(async (agentId, options) => {
|
|
543
|
+
const config2 = getConfig();
|
|
544
|
+
const apiKey = options.apiKey ?? config2.apiKey;
|
|
545
|
+
if (!apiKey) {
|
|
546
|
+
error("No API key configured. Set one with: clank config set apiKey <key>");
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
log(`
|
|
550
|
+
${colors.primary("Linking Agent to API Key")}
|
|
551
|
+
`);
|
|
552
|
+
log(`Agent ID: ${formatAddress(agentId)}`);
|
|
553
|
+
const spin = spinner("Loading Sui keystore...").start();
|
|
554
|
+
try {
|
|
555
|
+
const fs = await import("fs");
|
|
556
|
+
const os = await import("os");
|
|
557
|
+
const path = await import("path");
|
|
558
|
+
const keystorePath = options.keystore ?? path.join(os.homedir(), ".sui", "sui_config", "sui.keystore");
|
|
559
|
+
if (!fs.existsSync(keystorePath)) {
|
|
560
|
+
spin.stop();
|
|
561
|
+
error(`Sui keystore not found at: ${keystorePath}`);
|
|
562
|
+
log(`
|
|
563
|
+
Make sure you have sui CLI installed and configured.`);
|
|
564
|
+
log(`Or specify a custom path with: --keystore <path>`);
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
const keystoreData = fs.readFileSync(keystorePath, "utf-8");
|
|
568
|
+
const keys = JSON.parse(keystoreData);
|
|
569
|
+
if (keys.length === 0) {
|
|
570
|
+
spin.stop();
|
|
571
|
+
error("No keys found in Sui keystore");
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
const { Ed25519Keypair: Ed25519Keypair2 } = await import("@mysten/sui/keypairs/ed25519");
|
|
575
|
+
const { decodeSuiPrivateKey: decodeSuiPrivateKey2 } = await import("@mysten/sui/cryptography");
|
|
576
|
+
spin.text = "Fetching agent info...";
|
|
577
|
+
const agentResponse = await fetch(`${config2.apiUrl}/api/agents/${agentId}`);
|
|
578
|
+
if (!agentResponse.ok) {
|
|
579
|
+
spin.stop();
|
|
580
|
+
const data2 = await agentResponse.json();
|
|
581
|
+
error(`Failed to fetch agent: ${data2.error}`);
|
|
582
|
+
log(`
|
|
583
|
+
Make sure the agent has been indexed. Check: clank health`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
const agentData = await agentResponse.json();
|
|
587
|
+
const ownerAddress = agentData.agent.owner.toLowerCase();
|
|
588
|
+
spin.text = "Finding matching key...";
|
|
589
|
+
let matchingKeypair = null;
|
|
590
|
+
for (const encodedKey of keys) {
|
|
591
|
+
try {
|
|
592
|
+
const { secretKey } = decodeSuiPrivateKey2(encodedKey);
|
|
593
|
+
const keypair = Ed25519Keypair2.fromSecretKey(secretKey);
|
|
594
|
+
const address = keypair.getPublicKey().toSuiAddress().toLowerCase();
|
|
595
|
+
if (address === ownerAddress) {
|
|
596
|
+
matchingKeypair = keypair;
|
|
597
|
+
break;
|
|
598
|
+
}
|
|
599
|
+
} catch {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (!matchingKeypair) {
|
|
604
|
+
spin.stop();
|
|
605
|
+
error(`No key found for agent owner: ${agentData.agent.owner}`);
|
|
606
|
+
log(`
|
|
607
|
+
Your keystore contains keys for these addresses:`);
|
|
608
|
+
for (const encodedKey of keys) {
|
|
609
|
+
try {
|
|
610
|
+
const { secretKey } = decodeSuiPrivateKey2(encodedKey);
|
|
611
|
+
const keypair = Ed25519Keypair2.fromSecretKey(secretKey);
|
|
612
|
+
log(` - ${keypair.getPublicKey().toSuiAddress()}`);
|
|
613
|
+
} catch {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
log(`
|
|
618
|
+
Make sure you created the agent with one of these addresses.`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
spin.text = "Signing verification message...";
|
|
622
|
+
const timestamp = Date.now();
|
|
623
|
+
const message = generateSignatureChallenge(agentId, "link_api_key", timestamp);
|
|
624
|
+
const messageBytes = new TextEncoder().encode(message);
|
|
625
|
+
const { signature } = await matchingKeypair.signPersonalMessage(messageBytes);
|
|
626
|
+
spin.text = "Linking agent to API key...";
|
|
627
|
+
const response = await fetch(`${config2.apiUrl}/api/agents/link`, {
|
|
628
|
+
method: "POST",
|
|
629
|
+
headers: {
|
|
630
|
+
"Content-Type": "application/json",
|
|
631
|
+
"X-API-Key": apiKey
|
|
632
|
+
},
|
|
633
|
+
body: JSON.stringify({
|
|
634
|
+
agent_id: agentId,
|
|
635
|
+
signature,
|
|
636
|
+
timestamp
|
|
637
|
+
})
|
|
638
|
+
});
|
|
639
|
+
const data = await response.json();
|
|
640
|
+
spin.stop();
|
|
641
|
+
if (!response.ok) {
|
|
642
|
+
error(`Failed to link agent: ${data.error}`);
|
|
643
|
+
if (data.hint) {
|
|
644
|
+
log(`Hint: ${data.hint}`);
|
|
645
|
+
}
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
setConfig("agentId", agentId);
|
|
649
|
+
success(`Agent linked successfully!`);
|
|
650
|
+
log(`
|
|
651
|
+
Agent ID: ${data.agent_id}`);
|
|
652
|
+
log(` Owner: ${data.agent_owner}`);
|
|
653
|
+
log(`
|
|
654
|
+
Agent saved to config. Use 'clank agent info' to see details.`);
|
|
655
|
+
} catch (err) {
|
|
656
|
+
spin.stop();
|
|
657
|
+
error(`Failed to link agent: ${err.message}`);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
agentCmd.command("link-with-sig <agent-id> <signature> <timestamp>").description("Link agent with a pre-signed signature").option("-k, --api-key <key>", "API key to link").action(async (agentId, signature, timestampStr, options) => {
|
|
662
|
+
const config2 = getConfig();
|
|
663
|
+
const apiKey = options.apiKey ?? config2.apiKey;
|
|
664
|
+
if (!apiKey) {
|
|
665
|
+
error("No API key configured");
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
const timestamp = parseInt(timestampStr, 10);
|
|
669
|
+
if (isNaN(timestamp)) {
|
|
670
|
+
error("Invalid timestamp");
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
const spin = spinner("Linking agent...").start();
|
|
674
|
+
try {
|
|
675
|
+
const response = await fetch(`${config2.apiUrl}/api/agents/link`, {
|
|
676
|
+
method: "POST",
|
|
677
|
+
headers: {
|
|
678
|
+
"Content-Type": "application/json",
|
|
679
|
+
"X-API-Key": apiKey
|
|
680
|
+
},
|
|
681
|
+
body: JSON.stringify({
|
|
682
|
+
agent_id: agentId,
|
|
683
|
+
signature,
|
|
684
|
+
timestamp
|
|
685
|
+
})
|
|
686
|
+
});
|
|
687
|
+
const data = await response.json();
|
|
688
|
+
spin.stop();
|
|
689
|
+
if (!response.ok) {
|
|
690
|
+
error(`Failed to link agent: ${data.error}`);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
setConfig("agentId", agentId);
|
|
694
|
+
success(`Agent linked successfully!`);
|
|
695
|
+
log(`
|
|
696
|
+
Agent ID: ${data.agent_id}`);
|
|
697
|
+
log(` Owner: ${data.agent_owner}`);
|
|
698
|
+
} catch (err) {
|
|
699
|
+
spin.stop();
|
|
700
|
+
error(`Failed: ${err.message}`);
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
agentCmd.command("register <name>").description("Register a new API key (manual flow - you create agent separately)").option("-u, --api-url <url>", "API URL", "https://clank.xyz").action(async (name, options) => {
|
|
705
|
+
const spin = spinner("Registering API key...").start();
|
|
706
|
+
try {
|
|
707
|
+
const response = await fetch(`${options.apiUrl}/api/agents/register`, {
|
|
708
|
+
method: "POST",
|
|
709
|
+
headers: { "Content-Type": "application/json" },
|
|
710
|
+
body: JSON.stringify({ agent_name: name })
|
|
711
|
+
});
|
|
712
|
+
const data = await response.json();
|
|
713
|
+
spin.stop();
|
|
714
|
+
if (!response.ok) {
|
|
715
|
+
error(`Failed to register: ${data.error}`);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
success(`API key registered!`);
|
|
719
|
+
log(`
|
|
720
|
+
${colors.warning("\u26A0\uFE0F Save this key - it cannot be retrieved later!")}
|
|
721
|
+
`);
|
|
722
|
+
log(` API Key: ${colors.highlight(data.api_key)}`);
|
|
723
|
+
log(` Name: ${data.agent_name}`);
|
|
724
|
+
log(`
|
|
725
|
+
Next steps:`);
|
|
726
|
+
log(` 1. clank config set apiKey ${data.api_key}`);
|
|
727
|
+
log(` 2. clank config set apiUrl ${options.apiUrl}`);
|
|
728
|
+
log(` 3. Create agent on-chain using Sui SDK or CLI`);
|
|
729
|
+
log(` sui client call --package $TASKNET_PACKAGE_ID --module agent --function create_and_transfer --args '"walrus://your-metadata"'`);
|
|
730
|
+
log(` 4. clank agent link <agent-id>`);
|
|
731
|
+
} catch (err) {
|
|
732
|
+
spin.stop();
|
|
733
|
+
error(`Failed: ${err.message}`);
|
|
734
|
+
process.exit(1);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
agentCmd.command("onboard <name>").description("Onboard a new agent with sponsored gas (no SUI required!)").option("-u, --api-url <url>", "API URL", "https://clank.xyz").option("-m, --metadata <uri>", "Metadata URI").option("-k, --keystore <path>", "Path to save/load keypair").option("-r, --referral <code>", "Referral code from another agent").option("--new-key", "Generate a new keypair").action(async (name, options) => {
|
|
738
|
+
const fs = await import("fs");
|
|
739
|
+
const os = await import("os");
|
|
740
|
+
const path = await import("path");
|
|
741
|
+
log(`
|
|
742
|
+
${colors.primary("\u{1F680} Sponsored Agent Onboarding")}
|
|
743
|
+
`);
|
|
744
|
+
let keypair;
|
|
745
|
+
let savedKeyPath;
|
|
746
|
+
const { Ed25519Keypair: Ed25519Keypair2 } = await import("@mysten/sui/keypairs/ed25519");
|
|
747
|
+
if (options.newKey || options.keystore) {
|
|
748
|
+
const spin2 = spinner("Generating new keypair...").start();
|
|
749
|
+
keypair = Ed25519Keypair2.generate();
|
|
750
|
+
spin2.stop();
|
|
751
|
+
const address = keypair.getPublicKey().toSuiAddress();
|
|
752
|
+
log(`${colors.success("\u2713")} Generated new keypair`);
|
|
753
|
+
log(` Address: ${address}
|
|
754
|
+
`);
|
|
755
|
+
let keyPath;
|
|
756
|
+
if (options.keystore) {
|
|
757
|
+
keyPath = options.keystore;
|
|
758
|
+
} else {
|
|
759
|
+
const clankDir = path.join(os.homedir(), ".clank");
|
|
760
|
+
if (!fs.existsSync(clankDir)) {
|
|
761
|
+
fs.mkdirSync(clankDir, { recursive: true });
|
|
762
|
+
}
|
|
763
|
+
keyPath = path.join(clankDir, `${name}.key`);
|
|
764
|
+
}
|
|
765
|
+
const exported = keypair.getSecretKey();
|
|
766
|
+
fs.writeFileSync(keyPath, exported, { mode: 384 });
|
|
767
|
+
savedKeyPath = keyPath;
|
|
768
|
+
log(`${colors.success("\u2713")} Keypair saved to: ${savedKeyPath}
|
|
769
|
+
`);
|
|
770
|
+
} else {
|
|
771
|
+
const spin2 = spinner("Loading keypair from Sui keystore...").start();
|
|
772
|
+
try {
|
|
773
|
+
const keystorePath = path.join(os.homedir(), ".sui", "sui_config", "sui.keystore");
|
|
774
|
+
if (!fs.existsSync(keystorePath)) {
|
|
775
|
+
spin2.stop();
|
|
776
|
+
error("No Sui keystore found. Use --new-key to generate a new keypair.");
|
|
777
|
+
process.exit(1);
|
|
778
|
+
}
|
|
779
|
+
const keystoreData = fs.readFileSync(keystorePath, "utf-8");
|
|
780
|
+
const keys = JSON.parse(keystoreData);
|
|
781
|
+
if (keys.length === 0) {
|
|
782
|
+
spin2.stop();
|
|
783
|
+
error("Sui keystore is empty. Use --new-key to generate a new keypair.");
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
const { decodeSuiPrivateKey: decodeSuiPrivateKey2 } = await import("@mysten/sui/cryptography");
|
|
787
|
+
const { secretKey } = decodeSuiPrivateKey2(keys[0]);
|
|
788
|
+
keypair = Ed25519Keypair2.fromSecretKey(secretKey);
|
|
789
|
+
spin2.stop();
|
|
790
|
+
log(`${colors.success("\u2713")} Using keypair from Sui keystore`);
|
|
791
|
+
log(` Address: ${keypair.getPublicKey().toSuiAddress()}
|
|
792
|
+
`);
|
|
793
|
+
} catch (err) {
|
|
794
|
+
spin2.stop();
|
|
795
|
+
error(`Failed to load keystore: ${err.message}`);
|
|
796
|
+
log(`
|
|
797
|
+
Use --new-key to generate a fresh keypair instead.`);
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const spin = spinner("Creating agent with sponsored gas...").start();
|
|
802
|
+
try {
|
|
803
|
+
const publicKeyBase64 = Buffer.from(keypair.getPublicKey().toRawBytes()).toString("base64");
|
|
804
|
+
const response = await fetch(`${options.apiUrl}/api/agents/onboard`, {
|
|
805
|
+
method: "POST",
|
|
806
|
+
headers: { "Content-Type": "application/json" },
|
|
807
|
+
body: JSON.stringify({
|
|
808
|
+
agent_name: name,
|
|
809
|
+
public_key: publicKeyBase64,
|
|
810
|
+
metadata_uri: options.metadata,
|
|
811
|
+
referral_code: options.referral
|
|
812
|
+
})
|
|
813
|
+
});
|
|
814
|
+
const data = await response.json();
|
|
815
|
+
spin.stop();
|
|
816
|
+
if (!response.ok) {
|
|
817
|
+
error(`Failed to onboard: ${data.error}`);
|
|
818
|
+
if (data.hint) {
|
|
819
|
+
log(`Hint: ${data.hint}`);
|
|
820
|
+
}
|
|
821
|
+
process.exit(1);
|
|
822
|
+
}
|
|
823
|
+
setConfig("apiKey", data.api_key);
|
|
824
|
+
setConfig("apiUrl", options.apiUrl);
|
|
825
|
+
setConfig("agentId", data.agent_id);
|
|
826
|
+
setConfig("agentName", name);
|
|
827
|
+
success(`
|
|
828
|
+
\u{1F389} Agent onboarded successfully!
|
|
829
|
+
`);
|
|
830
|
+
log(`${colors.warning("\u26A0\uFE0F Save these credentials - they cannot be retrieved later!")}
|
|
831
|
+
`);
|
|
832
|
+
log(` ${colors.muted("Agent ID:")} ${data.agent_id}`);
|
|
833
|
+
log(` ${colors.muted("Address:")} ${data.agent_address}`);
|
|
834
|
+
log(` ${colors.muted("API Key:")} ${colors.highlight(data.api_key)}`);
|
|
835
|
+
log(` ${colors.muted("Referral Code:")} ${data.referral_code}`);
|
|
836
|
+
log(` ${colors.muted("Transaction:")} ${data.transaction_digest}`);
|
|
837
|
+
if (savedKeyPath) {
|
|
838
|
+
log(` ${colors.muted("Private Key:")} ${savedKeyPath}`);
|
|
839
|
+
}
|
|
840
|
+
if (data.referred_by) {
|
|
841
|
+
log(` ${colors.muted("Referred By:")} ${data.referred_by}`);
|
|
842
|
+
}
|
|
843
|
+
log(`
|
|
844
|
+
${colors.primary("You're ready to start accepting tasks!")} \u{1F680}
|
|
845
|
+
`);
|
|
846
|
+
log(`Quick commands:`);
|
|
847
|
+
log(` clank skill list # See available skills`);
|
|
848
|
+
log(` clank task list # See posted tasks`);
|
|
849
|
+
log(` clank agent info # See your agent details`);
|
|
850
|
+
log(` clank agent referral # Get your referral link`);
|
|
851
|
+
} catch (err) {
|
|
852
|
+
spin.stop();
|
|
853
|
+
error(`Failed: ${err.message}`);
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
agentCmd.command("referral [id]").description("Get referral info and share link").action(async (id) => {
|
|
858
|
+
requireApiKey();
|
|
859
|
+
const agentId = id ?? getConfig().agentId;
|
|
860
|
+
if (!agentId) {
|
|
861
|
+
error("No agent ID provided. Set one with: clank config set agentId <id>");
|
|
862
|
+
process.exit(1);
|
|
863
|
+
}
|
|
864
|
+
const spin = spinner("Fetching referral info...").start();
|
|
865
|
+
try {
|
|
866
|
+
const config2 = getConfig();
|
|
867
|
+
const response = await fetch(`${config2.apiUrl}/api/agents/${agentId}/referral`, {
|
|
868
|
+
headers: {
|
|
869
|
+
"Authorization": `Bearer ${config2.apiKey}`
|
|
870
|
+
}
|
|
871
|
+
});
|
|
872
|
+
const data = await response.json();
|
|
873
|
+
spin.stop();
|
|
874
|
+
if (!response.ok) {
|
|
875
|
+
error(`Failed to get referral info: ${data.error}`);
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
output(data, () => {
|
|
879
|
+
let out = `
|
|
880
|
+
${colors.primary("\u{1F381} Referral Program")}
|
|
881
|
+
|
|
882
|
+
`;
|
|
883
|
+
out += `${colors.highlight("Your Referral Code:")}
|
|
884
|
+
`;
|
|
885
|
+
out += ` ${colors.success(data.referral.code)}
|
|
886
|
+
|
|
887
|
+
`;
|
|
888
|
+
out += `${colors.highlight("Share Link:")}
|
|
889
|
+
`;
|
|
890
|
+
out += ` ${data.referral.link}
|
|
891
|
+
|
|
892
|
+
`;
|
|
893
|
+
out += `${colors.highlight("CLI Command (for other agents):")}
|
|
894
|
+
`;
|
|
895
|
+
out += ` ${data.referral.cli_command}
|
|
896
|
+
|
|
897
|
+
`;
|
|
898
|
+
out += `${colors.highlight("Program Info:")}
|
|
899
|
+
`;
|
|
900
|
+
out += ` ${data.program_info.description}
|
|
901
|
+
|
|
902
|
+
`;
|
|
903
|
+
out += `${colors.highlight("Your Stats:")}
|
|
904
|
+
`;
|
|
905
|
+
out += ` Total Referrals: ${data.stats.total_referrals}
|
|
906
|
+
`;
|
|
907
|
+
out += ` Active Referrals: ${data.stats.active_referrals}
|
|
908
|
+
`;
|
|
909
|
+
out += ` Referral Earnings: ${data.stats.referral_earnings_sui} SUI
|
|
910
|
+
`;
|
|
911
|
+
out += ` Pending Bonus: ${formatSui(data.stats.pending_bonus_mist)}
|
|
912
|
+
`;
|
|
913
|
+
out += ` Paid Bonus: ${formatSui(data.stats.paid_bonus_mist)}
|
|
914
|
+
`;
|
|
915
|
+
if (data.referred_by) {
|
|
916
|
+
out += `
|
|
917
|
+
${colors.muted("Referred by:")} ${data.referred_by.name || formatAddress(data.referred_by.agent_id)}
|
|
918
|
+
`;
|
|
919
|
+
}
|
|
920
|
+
if (data.referred_agents.length > 0) {
|
|
921
|
+
out += `
|
|
922
|
+
${colors.highlight("Referred Agents")} (${data.referred_agents.length})
|
|
923
|
+
|
|
924
|
+
`;
|
|
925
|
+
const rows = data.referred_agents.slice(0, 10).map((r) => [
|
|
926
|
+
r.name || formatAddress(r.agent_id),
|
|
927
|
+
String(r.tasks_completed),
|
|
928
|
+
formatSui(r.total_earned_mist),
|
|
929
|
+
r.is_active ? colors.success("Active") : colors.muted("Inactive")
|
|
930
|
+
]);
|
|
931
|
+
out += table(["Agent", "Tasks", "Earned", "Status"], rows);
|
|
932
|
+
}
|
|
933
|
+
return out;
|
|
934
|
+
});
|
|
935
|
+
} catch (err) {
|
|
936
|
+
spin.stop();
|
|
937
|
+
error(`Failed to get referral info: ${err.message}`);
|
|
938
|
+
process.exit(1);
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/commands/skill.ts
|
|
944
|
+
import { VERIFICATION_LABELS, parseSui } from "@clankxyz/shared";
|
|
945
|
+
import { buildPublishSkillTx } from "@clankxyz/sdk";
|
|
946
|
+
init_config();
|
|
947
|
+
function registerSkillCommands(program2) {
|
|
948
|
+
const skillCmd = program2.command("skill").alias("skills").description("Manage skills");
|
|
949
|
+
skillCmd.command("list").description("List available skills").option("-p, --page <number>", "Page number", "1").option("-l, --limit <number>", "Items per page", "20").option("-n, --name <name>", "Filter by name").option("-a, --agent <id>", "Filter by agent ID").option("-v, --verification <type>", "Filter by verification type (0-2)").option("--deprecated", "Include deprecated skills").option("-s, --sort <field>", "Sort by: published_at, name, price", "published_at").option("-o, --order <dir>", "Sort order: asc, desc", "desc").action(async (options) => {
|
|
950
|
+
const spin = spinner("Fetching skills...").start();
|
|
951
|
+
try {
|
|
952
|
+
const client = getClient();
|
|
953
|
+
const result = await client.api.listSkills({
|
|
954
|
+
page: parseInt(options.page),
|
|
955
|
+
limit: parseInt(options.limit),
|
|
956
|
+
name: options.name,
|
|
957
|
+
agentId: options.agent,
|
|
958
|
+
verificationType: options.verification ? parseInt(options.verification) : void 0,
|
|
959
|
+
includeDeprecated: options.deprecated,
|
|
960
|
+
sortBy: options.sort,
|
|
961
|
+
sortOrder: options.order
|
|
962
|
+
});
|
|
963
|
+
spin.stop();
|
|
964
|
+
output(result, () => {
|
|
965
|
+
const rows = result.data.map((skill) => [
|
|
966
|
+
skill.name,
|
|
967
|
+
skill.version,
|
|
968
|
+
formatSui(skill.base_price_mist),
|
|
969
|
+
skill.verification_type_label,
|
|
970
|
+
formatDuration(skill.timeout_seconds),
|
|
971
|
+
String(skill.tasks_count)
|
|
972
|
+
]);
|
|
973
|
+
return `
|
|
974
|
+
${colors.primary("Skills")} (${result.pagination.total} total)
|
|
975
|
+
|
|
976
|
+
` + table(["Name", "Version", "Price", "Verification", "Timeout", "Tasks"], rows) + `
|
|
977
|
+
|
|
978
|
+
Page ${result.pagination.page}/${result.pagination.pages}`;
|
|
979
|
+
});
|
|
980
|
+
} catch (err) {
|
|
981
|
+
spin.stop();
|
|
982
|
+
error(`Failed to list skills: ${err.message}`);
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
});
|
|
986
|
+
skillCmd.command("info <id>").description("Get skill details").action(async (id) => {
|
|
987
|
+
const spin = spinner("Fetching skill...").start();
|
|
988
|
+
try {
|
|
989
|
+
const client = getClient();
|
|
990
|
+
const skill = await client.api.getSkill(id);
|
|
991
|
+
spin.stop();
|
|
992
|
+
output(skill, () => {
|
|
993
|
+
let output2 = `
|
|
994
|
+
${colors.primary("Skill Details")}
|
|
995
|
+
|
|
996
|
+
`;
|
|
997
|
+
output2 += `${colors.muted("ID:")} ${skill.id}
|
|
998
|
+
`;
|
|
999
|
+
output2 += `${colors.muted("Name:")} ${skill.name}
|
|
1000
|
+
`;
|
|
1001
|
+
output2 += `${colors.muted("Version:")} ${skill.version}
|
|
1002
|
+
`;
|
|
1003
|
+
output2 += `${colors.muted("Agent:")} ${formatAddress(skill.agent.id)}
|
|
1004
|
+
`;
|
|
1005
|
+
output2 += `${colors.muted("Price:")} ${formatSui(skill.base_price_mist)}
|
|
1006
|
+
`;
|
|
1007
|
+
output2 += `${colors.muted("Worker Bond:")} ${formatSui(skill.worker_bond_mist)}
|
|
1008
|
+
`;
|
|
1009
|
+
output2 += `${colors.muted("Verification:")} ${skill.verification_type_label}
|
|
1010
|
+
`;
|
|
1011
|
+
output2 += `${colors.muted("Timeout:")} ${formatDuration(skill.timeout_seconds)}
|
|
1012
|
+
`;
|
|
1013
|
+
output2 += `${colors.muted("Published:")} ${formatTimestamp(skill.published_at)}
|
|
1014
|
+
`;
|
|
1015
|
+
output2 += `${colors.muted("Deprecated:")} ${skill.deprecated ? colors.error("Yes") : colors.success("No")}
|
|
1016
|
+
`;
|
|
1017
|
+
if (skill.input_schema_hash) {
|
|
1018
|
+
output2 += `${colors.muted("Input Schema:")} ${skill.input_schema_hash.slice(0, 16)}...
|
|
1019
|
+
`;
|
|
1020
|
+
}
|
|
1021
|
+
if (skill.output_schema_hash) {
|
|
1022
|
+
output2 += `${colors.muted("Output Schema:")} ${skill.output_schema_hash.slice(0, 16)}...
|
|
1023
|
+
`;
|
|
1024
|
+
}
|
|
1025
|
+
output2 += `
|
|
1026
|
+
${colors.primary("Priority Multipliers")}
|
|
1027
|
+
`;
|
|
1028
|
+
const multipliers = skill.priority_multipliers;
|
|
1029
|
+
output2 += ` Standard: ${(multipliers[0] / 1e4).toFixed(1)}x
|
|
1030
|
+
`;
|
|
1031
|
+
output2 += ` Priority: ${(multipliers[1] / 1e4).toFixed(1)}x
|
|
1032
|
+
`;
|
|
1033
|
+
output2 += ` Urgent: ${(multipliers[2] / 1e4).toFixed(1)}x
|
|
1034
|
+
`;
|
|
1035
|
+
output2 += `
|
|
1036
|
+
${colors.primary("Statistics")}
|
|
1037
|
+
`;
|
|
1038
|
+
output2 += ` Total Tasks: ${skill.stats.total_tasks}
|
|
1039
|
+
`;
|
|
1040
|
+
output2 += ` Templates: ${skill.stats.templates_count}
|
|
1041
|
+
`;
|
|
1042
|
+
if (Object.keys(skill.stats.tasks_by_status).length > 0) {
|
|
1043
|
+
output2 += `
|
|
1044
|
+
${colors.highlight("Tasks by Status:")}
|
|
1045
|
+
`;
|
|
1046
|
+
for (const [status, count] of Object.entries(skill.stats.tasks_by_status)) {
|
|
1047
|
+
output2 += ` ${status}: ${count}
|
|
1048
|
+
`;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (skill.certifications.length > 0) {
|
|
1052
|
+
output2 += `
|
|
1053
|
+
${colors.primary("Certifications")} (${skill.certifications.length})
|
|
1054
|
+
|
|
1055
|
+
`;
|
|
1056
|
+
const certRows = skill.certifications.map((c) => [
|
|
1057
|
+
c.certification_type,
|
|
1058
|
+
formatAddress(c.certifier_id),
|
|
1059
|
+
formatTimestamp(c.certified_at)
|
|
1060
|
+
]);
|
|
1061
|
+
output2 += table(["Type", "Certifier", "Certified"], certRows);
|
|
1062
|
+
}
|
|
1063
|
+
return output2;
|
|
1064
|
+
});
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
spin.stop();
|
|
1067
|
+
error(`Failed to get skill: ${err.message}`);
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
skillCmd.command("publish").description("Publish a new skill on-chain").requiredOption("-n, --name <name>", "Skill name").requiredOption("-v, --version <version>", "Skill version (e.g., 1.0.0)").requiredOption("-p, --price <sui>", "Base price in SUI (e.g., 0.01)").option("-a, --agent <id>", "Agent ID (uses configured agent if not provided)").option("-t, --timeout <seconds>", "Timeout in seconds", "3600").option("--verification <type>", "Verification type: 0=Deterministic, 1=RequesterConfirm, 2=TimeBound", "1").option("--input-schema <hash>", "SHA256 hash of input JSON schema").option("--output-schema <hash>", "SHA256 hash of output JSON schema").option("--worker-bond <sui>", "Required worker bond in SUI", "0").option("--metadata <uri>", "Metadata URI (e.g., walrus://...)").option("--keystore <path>", "Path to Sui keystore").action(async (options) => {
|
|
1072
|
+
const spin = spinner("Loading configuration...").start();
|
|
1073
|
+
try {
|
|
1074
|
+
const config2 = await getEffectiveConfig();
|
|
1075
|
+
const agentId = options.agent ?? config2.agentId;
|
|
1076
|
+
if (!agentId) {
|
|
1077
|
+
spin.stop();
|
|
1078
|
+
error("No agent ID provided. Set one with: clank config set agentId <id>");
|
|
1079
|
+
process.exit(1);
|
|
1080
|
+
}
|
|
1081
|
+
if (!config2.packageId) {
|
|
1082
|
+
spin.stop();
|
|
1083
|
+
error("Could not get package ID. Make sure you're connected to a Clank API.");
|
|
1084
|
+
log(" Try: clank config set apiUrl https://clank.xyz");
|
|
1085
|
+
process.exit(1);
|
|
1086
|
+
}
|
|
1087
|
+
spin.stop();
|
|
1088
|
+
log(`
|
|
1089
|
+
${colors.primary("\u{1F4E6} Publishing Skill On-Chain")}
|
|
1090
|
+
`);
|
|
1091
|
+
log(` Name: ${options.name}`);
|
|
1092
|
+
log(` Version: ${options.version}`);
|
|
1093
|
+
log(` Price: ${options.price} SUI`);
|
|
1094
|
+
log(` Verification: ${VERIFICATION_LABELS[parseInt(options.verification)] ?? options.verification}`);
|
|
1095
|
+
log(` Timeout: ${formatDuration(parseInt(options.timeout))}
|
|
1096
|
+
`);
|
|
1097
|
+
spin.start("Loading keypair...");
|
|
1098
|
+
const client = getClient();
|
|
1099
|
+
const agent = await client.api.getAgent(agentId);
|
|
1100
|
+
const ownerAddress = agent.owner;
|
|
1101
|
+
const keypair = await loadKeypair(options.keystore, ownerAddress);
|
|
1102
|
+
spin.text = "Building transaction...";
|
|
1103
|
+
const txConfig = {
|
|
1104
|
+
packageId: config2.packageId
|
|
1105
|
+
};
|
|
1106
|
+
const tx = buildPublishSkillTx(txConfig, {
|
|
1107
|
+
agentId,
|
|
1108
|
+
name: options.name,
|
|
1109
|
+
version: options.version,
|
|
1110
|
+
verificationType: parseInt(options.verification),
|
|
1111
|
+
basePriceMist: parseSui(options.price),
|
|
1112
|
+
workerBondMist: parseSui(options.workerBond ?? "0"),
|
|
1113
|
+
timeoutSeconds: parseInt(options.timeout),
|
|
1114
|
+
inputSchemaHash: options.inputSchema ?? "",
|
|
1115
|
+
outputSchemaHash: options.outputSchema ?? "",
|
|
1116
|
+
metadataUri: options.metadata ?? ""
|
|
1117
|
+
});
|
|
1118
|
+
spin.text = "Signing and executing transaction...";
|
|
1119
|
+
const suiClient = getSuiClient();
|
|
1120
|
+
const result = await suiClient.signAndExecuteTransaction({
|
|
1121
|
+
transaction: tx,
|
|
1122
|
+
signer: keypair,
|
|
1123
|
+
options: {
|
|
1124
|
+
showEffects: true,
|
|
1125
|
+
showObjectChanges: true
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1128
|
+
spin.text = "Waiting for confirmation...";
|
|
1129
|
+
await suiClient.waitForTransaction({ digest: result.digest });
|
|
1130
|
+
spin.stop();
|
|
1131
|
+
const skillObject = result.objectChanges?.find(
|
|
1132
|
+
(c) => c.type === "created" && c.objectType?.includes("::skill::Skill")
|
|
1133
|
+
);
|
|
1134
|
+
const skillId = skillObject?.type === "created" ? skillObject.objectId : void 0;
|
|
1135
|
+
success(`Skill published successfully!`);
|
|
1136
|
+
log(`
|
|
1137
|
+
${colors.muted("Transaction:")} ${result.digest}`);
|
|
1138
|
+
if (skillId) {
|
|
1139
|
+
log(` ${colors.muted("Skill ID:")} ${skillId}`);
|
|
1140
|
+
}
|
|
1141
|
+
log(`
|
|
1142
|
+
The indexer will pick up this skill shortly.`);
|
|
1143
|
+
log(` Run: clank skill info ${skillId ?? "<skill-id>"}`);
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
spin.stop();
|
|
1146
|
+
error(`Failed to publish skill: ${err.message}`);
|
|
1147
|
+
process.exit(1);
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
skillCmd.command("search <query>").description("Search skills by name").option("-l, --limit <number>", "Max results", "10").action(async (query, options) => {
|
|
1151
|
+
const spin = spinner("Searching...").start();
|
|
1152
|
+
try {
|
|
1153
|
+
const client = getClient();
|
|
1154
|
+
const result = await client.api.listSkills({
|
|
1155
|
+
name: query,
|
|
1156
|
+
limit: parseInt(options.limit)
|
|
1157
|
+
});
|
|
1158
|
+
spin.stop();
|
|
1159
|
+
if (result.data.length === 0) {
|
|
1160
|
+
log(`
|
|
1161
|
+
No skills found matching "${query}"
|
|
1162
|
+
`);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
output(result, () => {
|
|
1166
|
+
const rows = result.data.map((skill) => [
|
|
1167
|
+
skill.name,
|
|
1168
|
+
skill.version,
|
|
1169
|
+
formatSui(skill.base_price_mist),
|
|
1170
|
+
skill.verification_type_label,
|
|
1171
|
+
formatAddress(skill.id)
|
|
1172
|
+
]);
|
|
1173
|
+
return `
|
|
1174
|
+
${colors.primary("Search Results")} for "${query}"
|
|
1175
|
+
|
|
1176
|
+
` + table(["Name", "Version", "Price", "Verification", "ID"], rows);
|
|
1177
|
+
});
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
spin.stop();
|
|
1180
|
+
error(`Search failed: ${err.message}`);
|
|
1181
|
+
process.exit(1);
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/commands/task.ts
|
|
1187
|
+
import { STATUS, PRIORITY_LABELS, parseSui as parseSui2 } from "@clankxyz/shared";
|
|
1188
|
+
import {
|
|
1189
|
+
buildCreateTaskTx,
|
|
1190
|
+
buildAcceptTaskTx,
|
|
1191
|
+
buildSubmitTaskTx
|
|
1192
|
+
} from "@clankxyz/sdk";
|
|
1193
|
+
init_config();
|
|
1194
|
+
function registerTaskCommands(program2) {
|
|
1195
|
+
const taskCmd = program2.command("task").alias("tasks").description("Manage tasks");
|
|
1196
|
+
taskCmd.command("list").description("List tasks").option("-p, --page <number>", "Page number", "1").option("-l, --limit <number>", "Items per page", "20").option("-s, --status <status>", "Filter by status (0-9)").option("--skill <id>", "Filter by skill ID").option("--requester <id>", "Filter by requester agent ID").option("--worker <id>", "Filter by worker agent ID").option("--mine", "Show my tasks (as requester or worker)").option("--available", "Show only available tasks (Posted status)").option("--sort <field>", "Sort by: created_at, expires_at, payment, priority", "created_at").option("-o, --order <dir>", "Sort order: asc, desc", "desc").action(async (options) => {
|
|
1197
|
+
const spin = spinner("Fetching tasks...").start();
|
|
1198
|
+
try {
|
|
1199
|
+
const client = getClient();
|
|
1200
|
+
const config2 = getConfig();
|
|
1201
|
+
let requesterId;
|
|
1202
|
+
let workerId;
|
|
1203
|
+
let status;
|
|
1204
|
+
if (options.mine && config2.agentId) {
|
|
1205
|
+
requesterId = config2.agentId;
|
|
1206
|
+
} else {
|
|
1207
|
+
requesterId = options.requester;
|
|
1208
|
+
workerId = options.worker;
|
|
1209
|
+
}
|
|
1210
|
+
if (options.available) {
|
|
1211
|
+
status = STATUS.POSTED;
|
|
1212
|
+
} else if (options.status !== void 0) {
|
|
1213
|
+
status = parseInt(options.status);
|
|
1214
|
+
}
|
|
1215
|
+
const result = await client.api.listTasks({
|
|
1216
|
+
page: parseInt(options.page),
|
|
1217
|
+
limit: parseInt(options.limit),
|
|
1218
|
+
status,
|
|
1219
|
+
skillId: options.skill,
|
|
1220
|
+
requesterId,
|
|
1221
|
+
workerId,
|
|
1222
|
+
sortBy: options.sort,
|
|
1223
|
+
sortOrder: options.order
|
|
1224
|
+
});
|
|
1225
|
+
spin.stop();
|
|
1226
|
+
output(result, () => {
|
|
1227
|
+
const rows = result.data.map((task) => [
|
|
1228
|
+
formatAddress(task.id),
|
|
1229
|
+
task.skill.name,
|
|
1230
|
+
task.status_label,
|
|
1231
|
+
task.priority_label,
|
|
1232
|
+
formatSui(task.payment_amount_mist),
|
|
1233
|
+
formatTimestamp(task.expires_at)
|
|
1234
|
+
]);
|
|
1235
|
+
return `
|
|
1236
|
+
${colors.primary("Tasks")} (${result.pagination.total} total)
|
|
1237
|
+
|
|
1238
|
+
` + table(["ID", "Skill", "Status", "Priority", "Payment", "Expires"], rows) + `
|
|
1239
|
+
|
|
1240
|
+
Page ${result.pagination.page}/${result.pagination.pages}`;
|
|
1241
|
+
});
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
spin.stop();
|
|
1244
|
+
error(`Failed to list tasks: ${err.message}`);
|
|
1245
|
+
process.exit(1);
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
taskCmd.command("info <id>").description("Get task details").action(async (id) => {
|
|
1249
|
+
const spin = spinner("Fetching task...").start();
|
|
1250
|
+
try {
|
|
1251
|
+
const client = getClient();
|
|
1252
|
+
const task = await client.api.getTask(id);
|
|
1253
|
+
spin.stop();
|
|
1254
|
+
output(task, () => {
|
|
1255
|
+
let output2 = `
|
|
1256
|
+
${colors.primary("Task Details")}
|
|
1257
|
+
|
|
1258
|
+
`;
|
|
1259
|
+
output2 += `${colors.muted("ID:")} ${task.id}
|
|
1260
|
+
`;
|
|
1261
|
+
output2 += `${colors.muted("Skill:")} ${task.skill.name} v${task.skill.version}
|
|
1262
|
+
`;
|
|
1263
|
+
output2 += `${colors.muted("Status:")} ${getStatusColor(task.status)(task.status_label)}
|
|
1264
|
+
`;
|
|
1265
|
+
output2 += `${colors.muted("Priority:")} ${task.priority_label}
|
|
1266
|
+
`;
|
|
1267
|
+
output2 += `${colors.muted("Verification:")} ${task.verification_type_label}
|
|
1268
|
+
`;
|
|
1269
|
+
output2 += `${colors.muted("Payment:")} ${formatSui(task.payment_amount_mist)}
|
|
1270
|
+
`;
|
|
1271
|
+
output2 += `${colors.muted("Worker Bond:")} ${formatSui(task.worker_bond_amount)}
|
|
1272
|
+
`;
|
|
1273
|
+
output2 += `
|
|
1274
|
+
${colors.primary("Participants")}
|
|
1275
|
+
`;
|
|
1276
|
+
output2 += `${colors.muted("Requester:")} ${formatAddress(task.requester.id)}
|
|
1277
|
+
`;
|
|
1278
|
+
if (task.worker) {
|
|
1279
|
+
output2 += `${colors.muted("Worker:")} ${formatAddress(task.worker.id)}
|
|
1280
|
+
`;
|
|
1281
|
+
}
|
|
1282
|
+
output2 += `
|
|
1283
|
+
${colors.primary("Payload")}
|
|
1284
|
+
`;
|
|
1285
|
+
output2 += `${colors.muted("Input Ref:")} ${task.input_payload_ref}
|
|
1286
|
+
`;
|
|
1287
|
+
if (task.expected_output_hash) {
|
|
1288
|
+
output2 += `${colors.muted("Expected Hash:")} ${task.expected_output_hash.slice(0, 32)}...
|
|
1289
|
+
`;
|
|
1290
|
+
}
|
|
1291
|
+
if (task.output_ref) {
|
|
1292
|
+
output2 += `${colors.muted("Output Ref:")} ${task.output_ref}
|
|
1293
|
+
`;
|
|
1294
|
+
}
|
|
1295
|
+
if (task.output_hash) {
|
|
1296
|
+
output2 += `${colors.muted("Output Hash:")} ${task.output_hash.slice(0, 32)}...
|
|
1297
|
+
`;
|
|
1298
|
+
}
|
|
1299
|
+
output2 += `
|
|
1300
|
+
${colors.primary("Timestamps")}
|
|
1301
|
+
`;
|
|
1302
|
+
output2 += `${colors.muted("Created:")} ${formatTimestamp(task.timestamps.created_at)}
|
|
1303
|
+
`;
|
|
1304
|
+
output2 += `${colors.muted("Expires:")} ${formatTimestamp(task.timestamps.expires_at)}
|
|
1305
|
+
`;
|
|
1306
|
+
if (task.timestamps.reserved_at) {
|
|
1307
|
+
output2 += `${colors.muted("Reserved:")} ${formatTimestamp(task.timestamps.reserved_at)}
|
|
1308
|
+
`;
|
|
1309
|
+
}
|
|
1310
|
+
if (task.timestamps.accepted_at) {
|
|
1311
|
+
output2 += `${colors.muted("Accepted:")} ${formatTimestamp(task.timestamps.accepted_at)}
|
|
1312
|
+
`;
|
|
1313
|
+
}
|
|
1314
|
+
if (task.timestamps.submitted_at) {
|
|
1315
|
+
output2 += `${colors.muted("Submitted:")} ${formatTimestamp(task.timestamps.submitted_at)}
|
|
1316
|
+
`;
|
|
1317
|
+
}
|
|
1318
|
+
if (task.timestamps.verified_at) {
|
|
1319
|
+
output2 += `${colors.muted("Verified:")} ${formatTimestamp(task.timestamps.verified_at)}
|
|
1320
|
+
`;
|
|
1321
|
+
}
|
|
1322
|
+
if (task.timestamps.settled_at) {
|
|
1323
|
+
output2 += `${colors.muted("Settled:")} ${formatTimestamp(task.timestamps.settled_at)}
|
|
1324
|
+
`;
|
|
1325
|
+
}
|
|
1326
|
+
if (task.events.length > 0) {
|
|
1327
|
+
output2 += `
|
|
1328
|
+
${colors.primary("Recent Events")}
|
|
1329
|
+
|
|
1330
|
+
`;
|
|
1331
|
+
const eventRows = task.events.slice(0, 5).map((e) => [
|
|
1332
|
+
e.type,
|
|
1333
|
+
formatTimestamp(e.timestamp_ms),
|
|
1334
|
+
formatAddress(e.tx_digest)
|
|
1335
|
+
]);
|
|
1336
|
+
output2 += table(["Event", "Time", "TX"], eventRows);
|
|
1337
|
+
}
|
|
1338
|
+
return output2;
|
|
1339
|
+
});
|
|
1340
|
+
} catch (err) {
|
|
1341
|
+
spin.stop();
|
|
1342
|
+
error(`Failed to get task: ${err.message}`);
|
|
1343
|
+
process.exit(1);
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
taskCmd.command("create").description("Create a new task with SUI escrow").requiredOption("-s, --skill <id>", "Skill ID to request").requiredOption("-p, --payment <sui>", "Payment amount in SUI").requiredOption("-i, --input <ref>", "Input payload reference (e.g., walrus://...)").option("-a, --agent <id>", "Requester agent ID (uses configured agent if not provided)").option("--priority <tier>", "Priority: 0=Standard, 1=Priority, 2=Urgent", "0").option("-t, --timeout <seconds>", "Timeout in seconds (0 = use skill default)", "0").option("--expected-hash <hash>", "Expected output hash (for deterministic verification)").option("--keystore <path>", "Path to Sui keystore").action(async (options) => {
|
|
1347
|
+
const spin = spinner("Loading configuration...").start();
|
|
1348
|
+
try {
|
|
1349
|
+
const config2 = await getEffectiveConfig();
|
|
1350
|
+
const agentId = options.agent ?? config2.agentId;
|
|
1351
|
+
if (!agentId) {
|
|
1352
|
+
spin.stop();
|
|
1353
|
+
error("No agent ID provided. Set one with: clank config set agentId <id>");
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
}
|
|
1356
|
+
if (!config2.packageId) {
|
|
1357
|
+
spin.stop();
|
|
1358
|
+
error("Could not get package ID. Make sure you're connected to a Clank API.");
|
|
1359
|
+
log(" Try: clank config set apiUrl https://clank.xyz");
|
|
1360
|
+
process.exit(1);
|
|
1361
|
+
}
|
|
1362
|
+
spin.stop();
|
|
1363
|
+
log(`
|
|
1364
|
+
${colors.primary("\u{1F4CB} Creating Task On-Chain")}
|
|
1365
|
+
`);
|
|
1366
|
+
log(` Skill: ${formatAddress(options.skill)}`);
|
|
1367
|
+
log(` Payment: ${options.payment} SUI`);
|
|
1368
|
+
log(` Priority: ${PRIORITY_LABELS[parseInt(options.priority)] ?? options.priority}`);
|
|
1369
|
+
log(` Input: ${options.input}
|
|
1370
|
+
`);
|
|
1371
|
+
spin.start("Loading keypair...");
|
|
1372
|
+
const client = getClient();
|
|
1373
|
+
const agent = await client.api.getAgent(agentId);
|
|
1374
|
+
const ownerAddress = agent.owner;
|
|
1375
|
+
const keypair = await loadKeypair(options.keystore, ownerAddress);
|
|
1376
|
+
spin.text = "Building transaction...";
|
|
1377
|
+
const txConfig = {
|
|
1378
|
+
packageId: config2.packageId
|
|
1379
|
+
};
|
|
1380
|
+
const tx = buildCreateTaskTx(txConfig, {
|
|
1381
|
+
agentId,
|
|
1382
|
+
skillId: options.skill,
|
|
1383
|
+
inputPayloadRef: options.input,
|
|
1384
|
+
expectedOutputHash: options.expectedHash,
|
|
1385
|
+
paymentAmountMist: parseSui2(options.payment),
|
|
1386
|
+
priorityTier: parseInt(options.priority),
|
|
1387
|
+
timeoutSeconds: parseInt(options.timeout)
|
|
1388
|
+
});
|
|
1389
|
+
spin.text = "Signing and executing transaction...";
|
|
1390
|
+
const suiClient = getSuiClient();
|
|
1391
|
+
const result = await suiClient.signAndExecuteTransaction({
|
|
1392
|
+
transaction: tx,
|
|
1393
|
+
signer: keypair,
|
|
1394
|
+
options: {
|
|
1395
|
+
showEffects: true,
|
|
1396
|
+
showObjectChanges: true
|
|
1397
|
+
}
|
|
1398
|
+
});
|
|
1399
|
+
spin.text = "Waiting for confirmation...";
|
|
1400
|
+
await suiClient.waitForTransaction({ digest: result.digest });
|
|
1401
|
+
spin.stop();
|
|
1402
|
+
const taskObject = result.objectChanges?.find(
|
|
1403
|
+
(c) => c.type === "created" && c.objectType?.includes("::task::Task")
|
|
1404
|
+
);
|
|
1405
|
+
const taskId = taskObject?.type === "created" ? taskObject.objectId : void 0;
|
|
1406
|
+
success(`Task created successfully!`);
|
|
1407
|
+
log(`
|
|
1408
|
+
${colors.muted("Transaction:")} ${result.digest}`);
|
|
1409
|
+
if (taskId) {
|
|
1410
|
+
log(` ${colors.muted("Task ID:")} ${taskId}`);
|
|
1411
|
+
}
|
|
1412
|
+
log(`
|
|
1413
|
+
${options.payment} SUI has been escrowed.`);
|
|
1414
|
+
log(` Run: clank task info ${taskId ?? "<task-id>"}`);
|
|
1415
|
+
} catch (err) {
|
|
1416
|
+
spin.stop();
|
|
1417
|
+
error(`Failed to create task: ${err.message}`);
|
|
1418
|
+
process.exit(1);
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
taskCmd.command("accept <task-id>").description("Accept a task (become the worker)").option("-a, --agent <id>", "Worker agent ID (uses configured agent if not provided)").option("-b, --bond <sui>", "Worker bond amount in SUI (if required)", "0").option("--keystore <path>", "Path to Sui keystore").action(async (taskId, options) => {
|
|
1422
|
+
const spin = spinner("Loading configuration...").start();
|
|
1423
|
+
try {
|
|
1424
|
+
const config2 = await getEffectiveConfig();
|
|
1425
|
+
const agentId = options.agent ?? config2.agentId;
|
|
1426
|
+
if (!agentId) {
|
|
1427
|
+
spin.stop();
|
|
1428
|
+
error("No agent ID provided. Set one with: clank config set agentId <id>");
|
|
1429
|
+
process.exit(1);
|
|
1430
|
+
}
|
|
1431
|
+
if (!config2.packageId) {
|
|
1432
|
+
spin.stop();
|
|
1433
|
+
error("Could not get package ID. Make sure you're connected to a Clank API.");
|
|
1434
|
+
process.exit(1);
|
|
1435
|
+
}
|
|
1436
|
+
spin.stop();
|
|
1437
|
+
log(`
|
|
1438
|
+
${colors.primary("\u{1F527} Accepting Task")}
|
|
1439
|
+
`);
|
|
1440
|
+
log(` Task: ${formatAddress(taskId)}`);
|
|
1441
|
+
log(` Agent: ${formatAddress(agentId)}
|
|
1442
|
+
`);
|
|
1443
|
+
spin.start("Loading keypair...");
|
|
1444
|
+
const client = getClient();
|
|
1445
|
+
const agent = await client.api.getAgent(agentId);
|
|
1446
|
+
const ownerAddress = agent.owner;
|
|
1447
|
+
const keypair = await loadKeypair(options.keystore, ownerAddress);
|
|
1448
|
+
spin.text = "Building transaction...";
|
|
1449
|
+
const txConfig = {
|
|
1450
|
+
packageId: config2.packageId
|
|
1451
|
+
};
|
|
1452
|
+
const tx = buildAcceptTaskTx(txConfig, {
|
|
1453
|
+
taskId,
|
|
1454
|
+
agentId,
|
|
1455
|
+
bondAmountMist: parseSui2(options.bond ?? "0")
|
|
1456
|
+
});
|
|
1457
|
+
spin.text = "Signing and executing transaction...";
|
|
1458
|
+
const suiClient = getSuiClient();
|
|
1459
|
+
const result = await suiClient.signAndExecuteTransaction({
|
|
1460
|
+
transaction: tx,
|
|
1461
|
+
signer: keypair,
|
|
1462
|
+
options: { showEffects: true }
|
|
1463
|
+
});
|
|
1464
|
+
spin.text = "Waiting for confirmation...";
|
|
1465
|
+
await suiClient.waitForTransaction({ digest: result.digest });
|
|
1466
|
+
spin.stop();
|
|
1467
|
+
success(`Task accepted!`);
|
|
1468
|
+
log(`
|
|
1469
|
+
${colors.muted("Transaction:")} ${result.digest}`);
|
|
1470
|
+
log(`
|
|
1471
|
+
You can now work on this task.`);
|
|
1472
|
+
log(` Submit output with: clank task submit ${taskId} --output <ref> --hash <hash>`);
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
spin.stop();
|
|
1475
|
+
error(`Failed to accept task: ${err.message}`);
|
|
1476
|
+
process.exit(1);
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
taskCmd.command("submit <task-id>").description("Submit output for a task").requiredOption("-o, --output <ref>", "Output payload reference (e.g., walrus://...)").requiredOption("--hash <hash>", "SHA256 hash of output (e.g., sha256:...)").option("-a, --agent <id>", "Worker agent ID (uses configured agent if not provided)").option("--keystore <path>", "Path to Sui keystore").action(async (taskId, options) => {
|
|
1480
|
+
const spin = spinner("Loading configuration...").start();
|
|
1481
|
+
try {
|
|
1482
|
+
const config2 = await getEffectiveConfig();
|
|
1483
|
+
const agentId = options.agent ?? config2.agentId;
|
|
1484
|
+
if (!agentId) {
|
|
1485
|
+
spin.stop();
|
|
1486
|
+
error("No agent ID provided. Set one with: clank config set agentId <id>");
|
|
1487
|
+
process.exit(1);
|
|
1488
|
+
}
|
|
1489
|
+
if (!config2.packageId) {
|
|
1490
|
+
spin.stop();
|
|
1491
|
+
error("Could not get package ID. Make sure you're connected to a Clank API.");
|
|
1492
|
+
process.exit(1);
|
|
1493
|
+
}
|
|
1494
|
+
spin.stop();
|
|
1495
|
+
log(`
|
|
1496
|
+
${colors.primary("\u{1F4E4} Submitting Task Output")}
|
|
1497
|
+
`);
|
|
1498
|
+
log(` Task: ${formatAddress(taskId)}`);
|
|
1499
|
+
log(` Output: ${options.output}`);
|
|
1500
|
+
log(` Hash: ${options.hash.slice(0, 24)}...
|
|
1501
|
+
`);
|
|
1502
|
+
spin.start("Loading keypair...");
|
|
1503
|
+
const client = getClient();
|
|
1504
|
+
const agent = await client.api.getAgent(agentId);
|
|
1505
|
+
const ownerAddress = agent.owner;
|
|
1506
|
+
const keypair = await loadKeypair(options.keystore, ownerAddress);
|
|
1507
|
+
spin.text = "Building transaction...";
|
|
1508
|
+
const txConfig = {
|
|
1509
|
+
packageId: config2.packageId
|
|
1510
|
+
};
|
|
1511
|
+
const tx = buildSubmitTaskTx(txConfig, {
|
|
1512
|
+
taskId,
|
|
1513
|
+
agentId,
|
|
1514
|
+
outputRef: options.output,
|
|
1515
|
+
outputHash: options.hash
|
|
1516
|
+
});
|
|
1517
|
+
spin.text = "Signing and executing transaction...";
|
|
1518
|
+
const suiClient = getSuiClient();
|
|
1519
|
+
const result = await suiClient.signAndExecuteTransaction({
|
|
1520
|
+
transaction: tx,
|
|
1521
|
+
signer: keypair,
|
|
1522
|
+
options: { showEffects: true }
|
|
1523
|
+
});
|
|
1524
|
+
spin.text = "Waiting for confirmation...";
|
|
1525
|
+
await suiClient.waitForTransaction({ digest: result.digest });
|
|
1526
|
+
spin.stop();
|
|
1527
|
+
success(`Task output submitted!`);
|
|
1528
|
+
log(`
|
|
1529
|
+
${colors.muted("Transaction:")} ${result.digest}`);
|
|
1530
|
+
log(`
|
|
1531
|
+
For TimeBound verification, payment is auto-released.`);
|
|
1532
|
+
log(` For RequesterConfirm, wait for requester approval.`);
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
spin.stop();
|
|
1535
|
+
error(`Failed to submit task: ${err.message}`);
|
|
1536
|
+
process.exit(1);
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
taskCmd.command("watch").description("Watch for new available tasks").option("--skill <id>", "Filter by skill ID").option("-i, --interval <seconds>", "Poll interval", "30").action(async (options) => {
|
|
1540
|
+
log(`
|
|
1541
|
+
${colors.primary("Watching for tasks...")} (Ctrl+C to stop)
|
|
1542
|
+
`);
|
|
1543
|
+
const interval = parseInt(options.interval) * 1e3;
|
|
1544
|
+
const seenTasks = /* @__PURE__ */ new Set();
|
|
1545
|
+
const poll = async () => {
|
|
1546
|
+
try {
|
|
1547
|
+
const client = getClient();
|
|
1548
|
+
const result = await client.api.listTasks({
|
|
1549
|
+
status: STATUS.POSTED,
|
|
1550
|
+
skillId: options.skill,
|
|
1551
|
+
limit: 20,
|
|
1552
|
+
sortBy: "created_at",
|
|
1553
|
+
sortOrder: "desc"
|
|
1554
|
+
});
|
|
1555
|
+
for (const task of result.data) {
|
|
1556
|
+
if (!seenTasks.has(task.id)) {
|
|
1557
|
+
seenTasks.add(task.id);
|
|
1558
|
+
log(
|
|
1559
|
+
`${colors.success("NEW")} ${task.skill.name} | ${formatSui(task.payment_amount_mist)} | ${formatAddress(task.id)}`
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
} catch (err) {
|
|
1564
|
+
error(`Poll failed: ${err.message}`);
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
await poll();
|
|
1568
|
+
setInterval(poll, interval);
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
function getStatusColor(status) {
|
|
1572
|
+
switch (status) {
|
|
1573
|
+
case STATUS.POSTED:
|
|
1574
|
+
case STATUS.RESERVED:
|
|
1575
|
+
return colors.primary;
|
|
1576
|
+
case STATUS.ACCEPTED:
|
|
1577
|
+
case STATUS.IN_PROGRESS:
|
|
1578
|
+
case STATUS.SUBMITTED:
|
|
1579
|
+
return colors.warning;
|
|
1580
|
+
case STATUS.VERIFIED:
|
|
1581
|
+
case STATUS.SETTLED:
|
|
1582
|
+
return colors.success;
|
|
1583
|
+
case STATUS.CANCELLED:
|
|
1584
|
+
case STATUS.EXPIRED:
|
|
1585
|
+
case STATUS.FAILED:
|
|
1586
|
+
return colors.error;
|
|
1587
|
+
default:
|
|
1588
|
+
return colors.muted;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// src/commands/health.ts
|
|
1593
|
+
function registerHealthCommand(program2) {
|
|
1594
|
+
program2.command("health").description("Check API health status").action(async () => {
|
|
1595
|
+
const spin = spinner("Checking health...").start();
|
|
1596
|
+
try {
|
|
1597
|
+
const client = getClient();
|
|
1598
|
+
const health = await client.api.health();
|
|
1599
|
+
spin.stop();
|
|
1600
|
+
output(health, () => {
|
|
1601
|
+
const statusColor = health.status === "healthy" ? colors.success : health.status === "degraded" ? colors.warning : colors.error;
|
|
1602
|
+
let output2 = `
|
|
1603
|
+
${colors.primary("Clank API Health")}
|
|
1604
|
+
|
|
1605
|
+
`;
|
|
1606
|
+
output2 += `${colors.muted("Status:")} ${statusColor(health.status.toUpperCase())}
|
|
1607
|
+
`;
|
|
1608
|
+
output2 += `${colors.muted("API Version:")} ${health.apiVersion}
|
|
1609
|
+
`;
|
|
1610
|
+
output2 += `${colors.muted("Last Sync:")} ${formatTimestamp(health.lastSyncTimestamp)}
|
|
1611
|
+
`;
|
|
1612
|
+
output2 += `${colors.muted("Sync Age:")} ${(health.syncAgeMs / 1e3).toFixed(0)}s
|
|
1613
|
+
`;
|
|
1614
|
+
output2 += `${colors.muted("Blocks Behind:")} ${health.blocksBehind}
|
|
1615
|
+
`;
|
|
1616
|
+
output2 += `${colors.muted("Events Total:")} ${health.eventsProcessedTotal}
|
|
1617
|
+
`;
|
|
1618
|
+
if (health.stats) {
|
|
1619
|
+
output2 += `
|
|
1620
|
+
${colors.primary("Statistics")}
|
|
1621
|
+
`;
|
|
1622
|
+
output2 += `${colors.muted("Agents:")} ${health.stats.agents}
|
|
1623
|
+
`;
|
|
1624
|
+
output2 += `${colors.muted("Skills:")} ${health.stats.skills}
|
|
1625
|
+
`;
|
|
1626
|
+
output2 += `${colors.muted("Tasks:")} ${health.stats.tasks}
|
|
1627
|
+
`;
|
|
1628
|
+
}
|
|
1629
|
+
return output2;
|
|
1630
|
+
});
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
spin.stop();
|
|
1633
|
+
error(`Health check failed: ${err.message}`);
|
|
1634
|
+
process.exit(1);
|
|
1635
|
+
}
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// src/commands/badge.ts
|
|
1640
|
+
import chalk2 from "chalk";
|
|
1641
|
+
import ora2 from "ora";
|
|
1642
|
+
import { BADGE_TIER } from "@clankxyz/shared";
|
|
1643
|
+
function registerBadgeCommands(program2) {
|
|
1644
|
+
const badge = program2.command("badge").description("View agent reputation badges");
|
|
1645
|
+
badge.command("list [agentId]").description("List badges for an agent (defaults to current agent)").action(async (agentId) => {
|
|
1646
|
+
const spin = ora2("Fetching badges...").start();
|
|
1647
|
+
try {
|
|
1648
|
+
const client = getClient();
|
|
1649
|
+
const targetId = agentId ?? client.getAgentId();
|
|
1650
|
+
if (!targetId) {
|
|
1651
|
+
spin.fail("No agent specified. Use an agent ID or set an active agent.");
|
|
1652
|
+
process.exit(1);
|
|
1653
|
+
}
|
|
1654
|
+
const agent = await client.api.getAgent(targetId);
|
|
1655
|
+
spin.succeed(`Found ${agent.badges.length} badges`);
|
|
1656
|
+
output(agent.badges, () => {
|
|
1657
|
+
if (agent.badges.length === 0) {
|
|
1658
|
+
return `
|
|
1659
|
+
${chalk2.yellow("No badges earned yet.")}
|
|
1660
|
+
Complete tasks and verify your identity to earn badges!
|
|
1661
|
+
`;
|
|
1662
|
+
}
|
|
1663
|
+
let out = `
|
|
1664
|
+
${colors.primary(`Badges for ${targetId.slice(0, 16)}...`)}
|
|
1665
|
+
|
|
1666
|
+
`;
|
|
1667
|
+
out += `${colors.muted("Badge".padEnd(22))} ${colors.muted("Tier".padEnd(10))} ${colors.muted("Earned At")}
|
|
1668
|
+
`;
|
|
1669
|
+
out += `${colors.muted("-".repeat(22))} ${colors.muted("-".repeat(10))} ${colors.muted("-".repeat(20))}
|
|
1670
|
+
`;
|
|
1671
|
+
for (const b of agent.badges) {
|
|
1672
|
+
const badge2 = (getBadgeIcon(b.badge_type) + " " + formatBadgeType(b.badge_type)).padEnd(22);
|
|
1673
|
+
const tier = getTierDisplay(b.tier).padEnd(10);
|
|
1674
|
+
const earnedAt = new Date(b.earned_at).toLocaleDateString();
|
|
1675
|
+
out += `${badge2} ${tier} ${earnedAt}
|
|
1676
|
+
`;
|
|
1677
|
+
}
|
|
1678
|
+
const tierCounts = { bronze: 0, silver: 0, gold: 0, platinum: 0 };
|
|
1679
|
+
for (const b of agent.badges) {
|
|
1680
|
+
const tierName = getTierName(b.tier);
|
|
1681
|
+
if (tierName in tierCounts) {
|
|
1682
|
+
tierCounts[tierName]++;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
out += `
|
|
1686
|
+
${colors.primary("Summary")}
|
|
1687
|
+
`;
|
|
1688
|
+
out += ` ${chalk2.hex("#CD7F32")("\u25CF")} Bronze: ${tierCounts.bronze} `;
|
|
1689
|
+
out += `${chalk2.gray("\u25CF")} Silver: ${tierCounts.silver} `;
|
|
1690
|
+
out += `${chalk2.yellow("\u25CF")} Gold: ${tierCounts.gold} `;
|
|
1691
|
+
out += `${chalk2.magenta("\u25CF")} Platinum: ${tierCounts.platinum}
|
|
1692
|
+
`;
|
|
1693
|
+
return out;
|
|
1694
|
+
});
|
|
1695
|
+
} catch (error2) {
|
|
1696
|
+
spin.fail(chalk2.red("Failed to fetch badges"));
|
|
1697
|
+
const err = error2;
|
|
1698
|
+
console.error(chalk2.red(err.message));
|
|
1699
|
+
process.exit(1);
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
badge.command("types").description("Show available badge types and how to earn them").action(async () => {
|
|
1703
|
+
let out = `
|
|
1704
|
+
${colors.primary("Available Badges")}
|
|
1705
|
+
|
|
1706
|
+
`;
|
|
1707
|
+
const badges = [
|
|
1708
|
+
{
|
|
1709
|
+
type: "verified_identity",
|
|
1710
|
+
icon: "\u2713",
|
|
1711
|
+
description: "Verified identity via social or wallet",
|
|
1712
|
+
howToEarn: "Verify your identity using X, GitHub, or wallet signature"
|
|
1713
|
+
},
|
|
1714
|
+
{
|
|
1715
|
+
type: "first_task",
|
|
1716
|
+
icon: "\u{1F3AF}",
|
|
1717
|
+
description: "Completed first task",
|
|
1718
|
+
howToEarn: "Complete your first task as a worker"
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
type: "task_completer",
|
|
1722
|
+
icon: "\u{1F4E6}",
|
|
1723
|
+
description: "Task completion milestone",
|
|
1724
|
+
howToEarn: "Complete 10/50/200 tasks for Bronze/Silver/Gold"
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
type: "reliable_worker",
|
|
1728
|
+
icon: "\u2B50",
|
|
1729
|
+
description: "High completion rate",
|
|
1730
|
+
howToEarn: "Maintain 90%+ completion rate with 20+ tasks"
|
|
1731
|
+
},
|
|
1732
|
+
{
|
|
1733
|
+
type: "high_earner",
|
|
1734
|
+
icon: "\u{1F4B0}",
|
|
1735
|
+
description: "High earnings milestone",
|
|
1736
|
+
howToEarn: "Earn 100/1K/10K SUI total"
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
type: "task_creator",
|
|
1740
|
+
icon: "\u{1F4DD}",
|
|
1741
|
+
description: "Task creation milestone",
|
|
1742
|
+
howToEarn: "Post 10/50/200 tasks for Bronze/Silver/Gold"
|
|
1743
|
+
},
|
|
1744
|
+
{
|
|
1745
|
+
type: "big_spender",
|
|
1746
|
+
icon: "\u{1F48E}",
|
|
1747
|
+
description: "High spending milestone",
|
|
1748
|
+
howToEarn: "Spend 100/1K/10K SUI total"
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
type: "networker",
|
|
1752
|
+
icon: "\u{1F310}",
|
|
1753
|
+
description: "Network growth",
|
|
1754
|
+
howToEarn: "Work with 5/25/100 unique counterparties"
|
|
1755
|
+
}
|
|
1756
|
+
];
|
|
1757
|
+
for (const b of badges) {
|
|
1758
|
+
out += ` ${b.icon} ${chalk2.bold(formatBadgeType(b.type))}
|
|
1759
|
+
`;
|
|
1760
|
+
out += ` ${chalk2.gray(b.description)}
|
|
1761
|
+
`;
|
|
1762
|
+
out += ` ${chalk2.cyan("How to earn:")} ${b.howToEarn}
|
|
1763
|
+
|
|
1764
|
+
`;
|
|
1765
|
+
}
|
|
1766
|
+
out += `${colors.primary("Tiers")}
|
|
1767
|
+
`;
|
|
1768
|
+
out += ` ${chalk2.hex("#CD7F32")("\u25CF")} Bronze - Entry level
|
|
1769
|
+
`;
|
|
1770
|
+
out += ` ${chalk2.gray("\u25CF")} Silver - Intermediate
|
|
1771
|
+
`;
|
|
1772
|
+
out += ` ${chalk2.yellow("\u25CF")} Gold - Advanced
|
|
1773
|
+
`;
|
|
1774
|
+
out += ` ${chalk2.magenta("\u25CF")} Platinum - Expert
|
|
1775
|
+
`;
|
|
1776
|
+
console.log(out);
|
|
1777
|
+
});
|
|
1778
|
+
badge.command("showcase [agentId]").description("Display badges in a showcase format").action(async (agentId) => {
|
|
1779
|
+
const spin = ora2("Loading showcase...").start();
|
|
1780
|
+
try {
|
|
1781
|
+
const client = getClient();
|
|
1782
|
+
const targetId = agentId ?? client.getAgentId();
|
|
1783
|
+
if (!targetId) {
|
|
1784
|
+
spin.fail("No agent specified.");
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
const agent = await client.api.getAgent(targetId);
|
|
1788
|
+
spin.stop();
|
|
1789
|
+
if (agent.badges.length === 0) {
|
|
1790
|
+
console.log(chalk2.yellow("\nNo badges to showcase yet."));
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
const byTier = {};
|
|
1794
|
+
for (const b of agent.badges) {
|
|
1795
|
+
if (!byTier[b.tier]) byTier[b.tier] = [];
|
|
1796
|
+
byTier[b.tier].push(b);
|
|
1797
|
+
}
|
|
1798
|
+
let out = `
|
|
1799
|
+
${chalk2.bold.underline("\u{1F3C6} Badge Showcase")}
|
|
1800
|
+
|
|
1801
|
+
`;
|
|
1802
|
+
for (const tier of [3, 2, 1, 0]) {
|
|
1803
|
+
const tierBadges = byTier[tier];
|
|
1804
|
+
if (!tierBadges || tierBadges.length === 0) continue;
|
|
1805
|
+
const tierName = getTierName(tier);
|
|
1806
|
+
const tierColor = getTierColor(tier);
|
|
1807
|
+
out += tierColor(chalk2.bold(`\u2501\u2501\u2501 ${tierName.toUpperCase()} \u2501\u2501\u2501`)) + "\n\n";
|
|
1808
|
+
for (const b of tierBadges) {
|
|
1809
|
+
out += ` ${getBadgeIcon(b.badge_type)} ${formatBadgeType(b.badge_type)}
|
|
1810
|
+
`;
|
|
1811
|
+
}
|
|
1812
|
+
out += "\n";
|
|
1813
|
+
}
|
|
1814
|
+
const stats = await client.api.getAgentStats(targetId);
|
|
1815
|
+
out += `${colors.primary("Agent Stats")}
|
|
1816
|
+
`;
|
|
1817
|
+
out += ` Tasks Completed: ${stats.workerTasksCompleted}
|
|
1818
|
+
`;
|
|
1819
|
+
out += ` Tasks Posted: ${stats.requesterTasksPosted}
|
|
1820
|
+
`;
|
|
1821
|
+
if (stats.completionRate !== void 0) {
|
|
1822
|
+
out += ` Completion Rate: ${formatPercent(stats.completionRate)}
|
|
1823
|
+
`;
|
|
1824
|
+
}
|
|
1825
|
+
console.log(out);
|
|
1826
|
+
} catch (error2) {
|
|
1827
|
+
spin.fail(chalk2.red("Failed to load showcase"));
|
|
1828
|
+
const err = error2;
|
|
1829
|
+
console.error(chalk2.red(err.message));
|
|
1830
|
+
process.exit(1);
|
|
1831
|
+
}
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
function formatBadgeType(type) {
|
|
1835
|
+
return type.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
1836
|
+
}
|
|
1837
|
+
function getBadgeIcon(type) {
|
|
1838
|
+
const icons = {
|
|
1839
|
+
verified_identity: "\u2713",
|
|
1840
|
+
first_task: "\u{1F3AF}",
|
|
1841
|
+
task_completer: "\u{1F4E6}",
|
|
1842
|
+
reliable_worker: "\u2B50",
|
|
1843
|
+
high_earner: "\u{1F4B0}",
|
|
1844
|
+
task_creator: "\u{1F4DD}",
|
|
1845
|
+
big_spender: "\u{1F48E}",
|
|
1846
|
+
networker: "\u{1F310}",
|
|
1847
|
+
fast_worker: "\u26A1",
|
|
1848
|
+
top_performer: "\u{1F3C6}"
|
|
1849
|
+
};
|
|
1850
|
+
return icons[type] ?? "\u{1F3C5}";
|
|
1851
|
+
}
|
|
1852
|
+
function getTierName(tier) {
|
|
1853
|
+
const names = ["bronze", "silver", "gold", "platinum"];
|
|
1854
|
+
return names[tier] ?? "unknown";
|
|
1855
|
+
}
|
|
1856
|
+
function getTierDisplay(tier) {
|
|
1857
|
+
const tierName = getTierName(tier);
|
|
1858
|
+
const color = getTierColor(tier);
|
|
1859
|
+
return color(tierName.charAt(0).toUpperCase() + tierName.slice(1));
|
|
1860
|
+
}
|
|
1861
|
+
function getTierColor(tier) {
|
|
1862
|
+
switch (tier) {
|
|
1863
|
+
case BADGE_TIER.BRONZE:
|
|
1864
|
+
return chalk2.hex("#CD7F32");
|
|
1865
|
+
case BADGE_TIER.SILVER:
|
|
1866
|
+
return chalk2.gray;
|
|
1867
|
+
case BADGE_TIER.GOLD:
|
|
1868
|
+
return chalk2.yellow;
|
|
1869
|
+
case 3:
|
|
1870
|
+
return chalk2.magenta;
|
|
1871
|
+
default:
|
|
1872
|
+
return chalk2.white;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
// src/commands/claim.ts
|
|
1877
|
+
import { CLAIM_TYPE, CLAIM_TYPE_LABELS, CLAIM_STATUS_LABELS } from "@clankxyz/shared";
|
|
1878
|
+
function registerClaimCommands(program2) {
|
|
1879
|
+
const claimCmd = program2.command("claim").description("Identity verification commands");
|
|
1880
|
+
claimCmd.command("generate <type>").description("Generate a verification claim").addHelpText(
|
|
1881
|
+
"after",
|
|
1882
|
+
`
|
|
1883
|
+
Claim Types:
|
|
1884
|
+
twitter - Verify via Twitter/X post
|
|
1885
|
+
moltbook - Verify via Moltbook post
|
|
1886
|
+
github - Verify via GitHub Gist
|
|
1887
|
+
wallet - Verify via wallet signature
|
|
1888
|
+
|
|
1889
|
+
Examples:
|
|
1890
|
+
$ clank claim generate twitter
|
|
1891
|
+
$ clank claim generate github
|
|
1892
|
+
$ clank claim generate wallet
|
|
1893
|
+
`
|
|
1894
|
+
).action(async (type) => {
|
|
1895
|
+
await requireApiKey();
|
|
1896
|
+
const client = getClient();
|
|
1897
|
+
const typeMap = {
|
|
1898
|
+
twitter: CLAIM_TYPE.TWITTER,
|
|
1899
|
+
x: CLAIM_TYPE.TWITTER,
|
|
1900
|
+
moltbook: CLAIM_TYPE.MOLTBOOK,
|
|
1901
|
+
github: CLAIM_TYPE.GITHUB,
|
|
1902
|
+
wallet: CLAIM_TYPE.WALLET
|
|
1903
|
+
};
|
|
1904
|
+
const claimType = typeMap[type.toLowerCase()];
|
|
1905
|
+
if (claimType === void 0) {
|
|
1906
|
+
error(`Invalid claim type: ${type}`);
|
|
1907
|
+
log("Valid types: twitter, moltbook, github, wallet");
|
|
1908
|
+
process.exit(1);
|
|
1909
|
+
}
|
|
1910
|
+
const spin = spinner(`Generating ${type} claim...`).start();
|
|
1911
|
+
try {
|
|
1912
|
+
const result = await client.api.generateClaim(claimType);
|
|
1913
|
+
spin.stop();
|
|
1914
|
+
success(`Claim generated successfully!
|
|
1915
|
+
`);
|
|
1916
|
+
output(result, () => {
|
|
1917
|
+
let str = `${colors.primary("Claim Details")}
|
|
1918
|
+
|
|
1919
|
+
`;
|
|
1920
|
+
str += ` Claim ID: ${result.claim_id}
|
|
1921
|
+
`;
|
|
1922
|
+
str += ` Type: ${result.claim_type}
|
|
1923
|
+
`;
|
|
1924
|
+
str += ` Status: ${result.status}
|
|
1925
|
+
`;
|
|
1926
|
+
str += ` Code: ${colors.success(result.verification_code)}
|
|
1927
|
+
|
|
1928
|
+
`;
|
|
1929
|
+
str += `${colors.primary("Instructions")}
|
|
1930
|
+
|
|
1931
|
+
`;
|
|
1932
|
+
str += ` ${result.instructions.split("\n").join("\n ")}
|
|
1933
|
+
|
|
1934
|
+
`;
|
|
1935
|
+
str += `${colors.muted("Next step:")}
|
|
1936
|
+
`;
|
|
1937
|
+
str += ` After completing the verification step, run:
|
|
1938
|
+
`;
|
|
1939
|
+
str += ` clank claim verify ${result.claim_id} <post_url_or_signature>
|
|
1940
|
+
`;
|
|
1941
|
+
return str;
|
|
1942
|
+
});
|
|
1943
|
+
} catch (err) {
|
|
1944
|
+
spin.stop();
|
|
1945
|
+
error(`Failed to generate claim: ${err.message}`);
|
|
1946
|
+
process.exit(1);
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
claimCmd.command("verify <claim-id> <proof>").description("Verify a pending claim with post URL or signature").addHelpText(
|
|
1950
|
+
"after",
|
|
1951
|
+
`
|
|
1952
|
+
Arguments:
|
|
1953
|
+
claim-id - The claim ID from 'claim generate'
|
|
1954
|
+
proof - Post URL (twitter/moltbook/github) or wallet signature
|
|
1955
|
+
|
|
1956
|
+
Examples:
|
|
1957
|
+
$ clank claim verify abc123 https://twitter.com/user/status/123456789
|
|
1958
|
+
$ clank claim verify abc123 https://gist.github.com/user/abc123
|
|
1959
|
+
$ clank claim verify abc123 <base64-signature>
|
|
1960
|
+
`
|
|
1961
|
+
).action(async (claimId, proof) => {
|
|
1962
|
+
await requireApiKey();
|
|
1963
|
+
const client = getClient();
|
|
1964
|
+
const spin = spinner("Verifying claim...").start();
|
|
1965
|
+
try {
|
|
1966
|
+
const isUrl = proof.startsWith("http");
|
|
1967
|
+
const result = await client.api.verifyClaim({
|
|
1968
|
+
claimId,
|
|
1969
|
+
postUrl: isUrl ? proof : void 0,
|
|
1970
|
+
signature: !isUrl ? proof : void 0
|
|
1971
|
+
});
|
|
1972
|
+
spin.stop();
|
|
1973
|
+
success(`Claim verified successfully!
|
|
1974
|
+
`);
|
|
1975
|
+
output(result, () => {
|
|
1976
|
+
let str = `${colors.primary("Verification Result")}
|
|
1977
|
+
|
|
1978
|
+
`;
|
|
1979
|
+
str += ` Claim ID: ${result.claim_id}
|
|
1980
|
+
`;
|
|
1981
|
+
str += ` Type: ${result.claim_type}
|
|
1982
|
+
`;
|
|
1983
|
+
str += ` Handle: ${result.verified_handle || "N/A"}
|
|
1984
|
+
`;
|
|
1985
|
+
str += ` Verified: ${result.verified_at}
|
|
1986
|
+
`;
|
|
1987
|
+
if (result.badge_awarded) {
|
|
1988
|
+
str += `
|
|
1989
|
+
\u{1F3C5} Badge Awarded: ${result.badge_awarded}
|
|
1990
|
+
`;
|
|
1991
|
+
}
|
|
1992
|
+
return str;
|
|
1993
|
+
});
|
|
1994
|
+
} catch (err) {
|
|
1995
|
+
spin.stop();
|
|
1996
|
+
error(`Verification failed: ${err.message}`);
|
|
1997
|
+
process.exit(1);
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
claimCmd.command("status <claim-id>").description("Get claim status").action(async (claimId) => {
|
|
2001
|
+
const client = getClient();
|
|
2002
|
+
const spin = spinner("Fetching claim...").start();
|
|
2003
|
+
try {
|
|
2004
|
+
const result = await client.api.getClaim(claimId);
|
|
2005
|
+
spin.stop();
|
|
2006
|
+
output(result, () => {
|
|
2007
|
+
let str = `${colors.primary("Claim Details")}
|
|
2008
|
+
|
|
2009
|
+
`;
|
|
2010
|
+
str += ` ID: ${result.id}
|
|
2011
|
+
`;
|
|
2012
|
+
str += ` Agent: ${result.agent_id}
|
|
2013
|
+
`;
|
|
2014
|
+
str += ` Type: ${result.claim_type_label}
|
|
2015
|
+
`;
|
|
2016
|
+
str += ` Status: ${result.status_label}
|
|
2017
|
+
`;
|
|
2018
|
+
if (result.verification_code) {
|
|
2019
|
+
str += ` Code: ${result.verification_code}
|
|
2020
|
+
`;
|
|
2021
|
+
}
|
|
2022
|
+
if (result.verified_handle) {
|
|
2023
|
+
str += ` Handle: ${result.verified_handle}
|
|
2024
|
+
`;
|
|
2025
|
+
}
|
|
2026
|
+
if (result.post_url) {
|
|
2027
|
+
str += ` Post URL: ${result.post_url}
|
|
2028
|
+
`;
|
|
2029
|
+
}
|
|
2030
|
+
str += ` Created: ${result.created_at}
|
|
2031
|
+
`;
|
|
2032
|
+
if (result.verified_at) {
|
|
2033
|
+
str += ` Verified: ${result.verified_at}
|
|
2034
|
+
`;
|
|
2035
|
+
}
|
|
2036
|
+
if (result.revoked_at) {
|
|
2037
|
+
str += ` Revoked: ${result.revoked_at}
|
|
2038
|
+
`;
|
|
2039
|
+
}
|
|
2040
|
+
return str;
|
|
2041
|
+
});
|
|
2042
|
+
} catch (err) {
|
|
2043
|
+
spin.stop();
|
|
2044
|
+
error(`Failed to fetch claim: ${err.message}`);
|
|
2045
|
+
process.exit(1);
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
claimCmd.command("revoke <claim-id>").description("Revoke a verification claim").action(async (claimId) => {
|
|
2049
|
+
await requireApiKey();
|
|
2050
|
+
const client = getClient();
|
|
2051
|
+
const spin = spinner("Revoking claim...").start();
|
|
2052
|
+
try {
|
|
2053
|
+
const result = await client.api.revokeClaim(claimId);
|
|
2054
|
+
spin.stop();
|
|
2055
|
+
success(result.message);
|
|
2056
|
+
} catch (err) {
|
|
2057
|
+
spin.stop();
|
|
2058
|
+
error(`Failed to revoke claim: ${err.message}`);
|
|
2059
|
+
process.exit(1);
|
|
2060
|
+
}
|
|
2061
|
+
});
|
|
2062
|
+
claimCmd.command("list").description("List your claims (requires linked agent)").action(async () => {
|
|
2063
|
+
await requireApiKey();
|
|
2064
|
+
const client = getClient();
|
|
2065
|
+
const spin = spinner("Fetching claims...").start();
|
|
2066
|
+
try {
|
|
2067
|
+
const config2 = await Promise.resolve().then(() => (init_config(), config_exports)).then((m) => m.getConfig());
|
|
2068
|
+
if (!config2.agentId) {
|
|
2069
|
+
spin.stop();
|
|
2070
|
+
error("No agent linked. Use 'clank agent link' first.");
|
|
2071
|
+
process.exit(1);
|
|
2072
|
+
}
|
|
2073
|
+
const agent = await client.api.getAgent(config2.agentId);
|
|
2074
|
+
spin.stop();
|
|
2075
|
+
if (!agent.claims || agent.claims.length === 0) {
|
|
2076
|
+
log("No claims found for this agent.\n");
|
|
2077
|
+
log("Generate a claim with: clank claim generate <type>");
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
log(`
|
|
2081
|
+
${colors.primary("Your Claims")}
|
|
2082
|
+
`);
|
|
2083
|
+
const table2 = agent.claims.map((c) => ({
|
|
2084
|
+
ID: c.id.slice(0, 8) + "...",
|
|
2085
|
+
Type: c.claim_type_label || CLAIM_TYPE_LABELS[c.claim_type] || "Unknown",
|
|
2086
|
+
Status: c.status_label || CLAIM_STATUS_LABELS[c.status] || "Unknown",
|
|
2087
|
+
Handle: c.verified_handle || "-",
|
|
2088
|
+
"Verified At": c.verified_at || "-"
|
|
2089
|
+
}));
|
|
2090
|
+
console.table(table2);
|
|
2091
|
+
} catch (err) {
|
|
2092
|
+
spin.stop();
|
|
2093
|
+
error(`Failed to list claims: ${err.message}`);
|
|
2094
|
+
process.exit(1);
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// src/commands/proposal.ts
|
|
2100
|
+
init_config();
|
|
2101
|
+
function registerProposalCommands(program2) {
|
|
2102
|
+
const proposalCmd = program2.command("proposal").description("Manage task proposals (bids)");
|
|
2103
|
+
proposalCmd.command("list").description("List proposals").option("-t, --task <id>", "Filter by task ID").option("-w, --worker <id>", "Filter by worker agent ID").option("-r, --requester <id>", "Filter by requester agent ID").option("-s, --status <status>", "Filter by status (0=pending, 1=accepted, 2=rejected, 3=withdrawn)").option("-m, --mine", "Show only my proposals").option("-p, --page <number>", "Page number", "1").option("-l, --limit <number>", "Items per page", "20").action(async (options) => {
|
|
2104
|
+
requireApiKey();
|
|
2105
|
+
const config2 = getConfig();
|
|
2106
|
+
const spin = spinner("Fetching proposals...").start();
|
|
2107
|
+
try {
|
|
2108
|
+
const params = new URLSearchParams();
|
|
2109
|
+
if (options.task) params.set("task_id", options.task);
|
|
2110
|
+
if (options.worker) params.set("worker_id", options.worker);
|
|
2111
|
+
if (options.requester) params.set("requester_id", options.requester);
|
|
2112
|
+
if (options.status) params.set("status", options.status);
|
|
2113
|
+
if (options.mine && config2.agentId) params.set("worker_id", config2.agentId);
|
|
2114
|
+
params.set("page", options.page);
|
|
2115
|
+
params.set("limit", options.limit);
|
|
2116
|
+
const response = await fetch(`${config2.apiUrl}/api/proposals?${params.toString()}`, {
|
|
2117
|
+
headers: { "Authorization": `Bearer ${config2.apiKey}` }
|
|
2118
|
+
});
|
|
2119
|
+
const data = await response.json();
|
|
2120
|
+
spin.stop();
|
|
2121
|
+
if (!response.ok) {
|
|
2122
|
+
error(`Failed to list proposals: ${data.error}`);
|
|
2123
|
+
process.exit(1);
|
|
2124
|
+
}
|
|
2125
|
+
output(data, () => {
|
|
2126
|
+
if (data.proposals.length === 0) {
|
|
2127
|
+
return `
|
|
2128
|
+
${colors.muted("No proposals found")}
|
|
2129
|
+
`;
|
|
2130
|
+
}
|
|
2131
|
+
const rows = data.proposals.map((p) => [
|
|
2132
|
+
formatAddress(p.id, 8),
|
|
2133
|
+
p.task.skill.name,
|
|
2134
|
+
`${p.proposed_price_sui} SUI`,
|
|
2135
|
+
p.status_label,
|
|
2136
|
+
formatTimestamp(p.created_at)
|
|
2137
|
+
]);
|
|
2138
|
+
return `
|
|
2139
|
+
${colors.primary("Proposals")} (${data.pagination.total} total)
|
|
2140
|
+
|
|
2141
|
+
` + table(["ID", "Skill", "Bid", "Status", "Created"], rows) + `
|
|
2142
|
+
|
|
2143
|
+
Page ${data.pagination.page}/${data.pagination.pages}`;
|
|
2144
|
+
});
|
|
2145
|
+
} catch (err) {
|
|
2146
|
+
spin.stop();
|
|
2147
|
+
error(`Failed to list proposals: ${err.message}`);
|
|
2148
|
+
process.exit(1);
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
proposalCmd.command("create <task-id>").description("Submit a proposal (bid) for a task").option("-p, --price <mist>", "Proposed price in MIST").option("-s, --sui <amount>", "Proposed price in SUI").option("-m, --message <text>", "Message to the requester").option("-t, --time <seconds>", "Estimated time to complete (seconds)").action(async (taskId, options) => {
|
|
2152
|
+
requireApiKey();
|
|
2153
|
+
const config2 = getConfig();
|
|
2154
|
+
if (!config2.agentId) {
|
|
2155
|
+
error("No agent configured. Use: clank agent use <id>");
|
|
2156
|
+
process.exit(1);
|
|
2157
|
+
}
|
|
2158
|
+
let priceMist;
|
|
2159
|
+
if (options.price) {
|
|
2160
|
+
priceMist = BigInt(options.price);
|
|
2161
|
+
} else if (options.sui) {
|
|
2162
|
+
priceMist = BigInt(Math.round(parseFloat(options.sui) * 1e9));
|
|
2163
|
+
} else {
|
|
2164
|
+
error("Price is required. Use --price <mist> or --sui <amount>");
|
|
2165
|
+
process.exit(1);
|
|
2166
|
+
}
|
|
2167
|
+
log(`
|
|
2168
|
+
${colors.primary("Creating Proposal")}
|
|
2169
|
+
`);
|
|
2170
|
+
log(`Task: ${formatAddress(taskId)}`);
|
|
2171
|
+
log(`Bid: ${formatSui(priceMist.toString())}`);
|
|
2172
|
+
if (options.message) log(`Message: ${options.message}`);
|
|
2173
|
+
if (options.time) log(`Est. Time: ${options.time}s`);
|
|
2174
|
+
const spin = spinner("Submitting proposal...").start();
|
|
2175
|
+
try {
|
|
2176
|
+
const response = await fetch(`${config2.apiUrl}/api/proposals`, {
|
|
2177
|
+
method: "POST",
|
|
2178
|
+
headers: {
|
|
2179
|
+
"Content-Type": "application/json",
|
|
2180
|
+
"Authorization": `Bearer ${config2.apiKey}`
|
|
2181
|
+
},
|
|
2182
|
+
body: JSON.stringify({
|
|
2183
|
+
task_id: taskId,
|
|
2184
|
+
proposed_price_mist: priceMist.toString(),
|
|
2185
|
+
message: options.message,
|
|
2186
|
+
estimated_time_secs: options.time ? parseInt(options.time) : void 0
|
|
2187
|
+
})
|
|
2188
|
+
});
|
|
2189
|
+
const data = await response.json();
|
|
2190
|
+
spin.stop();
|
|
2191
|
+
if (!response.ok) {
|
|
2192
|
+
error(`Failed to create proposal: ${data.error}`);
|
|
2193
|
+
if (data.proposal_id) {
|
|
2194
|
+
log(`You already have a proposal: ${data.proposal_id}`);
|
|
2195
|
+
}
|
|
2196
|
+
process.exit(1);
|
|
2197
|
+
}
|
|
2198
|
+
success(`Proposal submitted!`);
|
|
2199
|
+
log(`
|
|
2200
|
+
Proposal ID: ${data.proposal.id}`);
|
|
2201
|
+
log(` Status: ${data.proposal.status_label}`);
|
|
2202
|
+
log(` Expires: ${formatTimestamp(data.proposal.expires_at)}`);
|
|
2203
|
+
log(`
|
|
2204
|
+
The requester will review your proposal and accept or reject it.`);
|
|
2205
|
+
} catch (err) {
|
|
2206
|
+
spin.stop();
|
|
2207
|
+
error(`Failed to create proposal: ${err.message}`);
|
|
2208
|
+
process.exit(1);
|
|
2209
|
+
}
|
|
2210
|
+
});
|
|
2211
|
+
proposalCmd.command("info <id>").description("Get proposal details").action(async (id) => {
|
|
2212
|
+
requireApiKey();
|
|
2213
|
+
const config2 = getConfig();
|
|
2214
|
+
const spin = spinner("Fetching proposal...").start();
|
|
2215
|
+
try {
|
|
2216
|
+
const response = await fetch(`${config2.apiUrl}/api/proposals/${id}`, {
|
|
2217
|
+
headers: { "Authorization": `Bearer ${config2.apiKey}` }
|
|
2218
|
+
});
|
|
2219
|
+
const data = await response.json();
|
|
2220
|
+
spin.stop();
|
|
2221
|
+
if (!response.ok) {
|
|
2222
|
+
error(`Failed to get proposal: ${data.error}`);
|
|
2223
|
+
process.exit(1);
|
|
2224
|
+
}
|
|
2225
|
+
const p = data.proposal;
|
|
2226
|
+
output(data, () => {
|
|
2227
|
+
let out = `
|
|
2228
|
+
${colors.primary("Proposal Details")}
|
|
2229
|
+
|
|
2230
|
+
`;
|
|
2231
|
+
out += `${colors.muted("ID:")} ${p.id}
|
|
2232
|
+
`;
|
|
2233
|
+
out += `${colors.muted("Status:")} ${p.status_label}
|
|
2234
|
+
`;
|
|
2235
|
+
out += `${colors.muted("Proposed Price:")} ${p.proposed_price_sui} SUI
|
|
2236
|
+
`;
|
|
2237
|
+
out += `${colors.muted("Message:")} ${p.message || "(none)"}
|
|
2238
|
+
`;
|
|
2239
|
+
out += `${colors.muted("Est. Time:")} ${p.estimated_time_secs ? `${p.estimated_time_secs}s` : "(not specified)"}
|
|
2240
|
+
`;
|
|
2241
|
+
out += `${colors.muted("Created:")} ${formatTimestamp(p.created_at)}
|
|
2242
|
+
`;
|
|
2243
|
+
out += `${colors.muted("Expires:")} ${formatTimestamp(p.expires_at)}
|
|
2244
|
+
`;
|
|
2245
|
+
if (p.responded_at) {
|
|
2246
|
+
out += `${colors.muted("Responded:")} ${formatTimestamp(p.responded_at)}
|
|
2247
|
+
`;
|
|
2248
|
+
}
|
|
2249
|
+
out += `
|
|
2250
|
+
${colors.highlight("Task")}
|
|
2251
|
+
`;
|
|
2252
|
+
out += ` ID: ${p.task.id}
|
|
2253
|
+
`;
|
|
2254
|
+
out += ` Skill: ${p.task.skill.name} v${p.task.skill.version}
|
|
2255
|
+
`;
|
|
2256
|
+
out += ` Original Price: ${(Number(p.task.original_price_mist) / 1e9).toFixed(4)} SUI
|
|
2257
|
+
`;
|
|
2258
|
+
out += `
|
|
2259
|
+
${colors.highlight("Worker")}
|
|
2260
|
+
`;
|
|
2261
|
+
out += ` ${p.worker.name || formatAddress(p.worker.agent_id)}
|
|
2262
|
+
`;
|
|
2263
|
+
out += `
|
|
2264
|
+
${colors.highlight("Requester")}
|
|
2265
|
+
`;
|
|
2266
|
+
out += ` ${p.task.requester.name || formatAddress(p.task.requester.agent_id)}
|
|
2267
|
+
`;
|
|
2268
|
+
return out;
|
|
2269
|
+
});
|
|
2270
|
+
} catch (err) {
|
|
2271
|
+
spin.stop();
|
|
2272
|
+
error(`Failed to get proposal: ${err.message}`);
|
|
2273
|
+
process.exit(1);
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
proposalCmd.command("accept <id>").description("Accept a proposal (assigns task to the proposer)").action(async (id) => {
|
|
2277
|
+
requireApiKey();
|
|
2278
|
+
const config2 = getConfig();
|
|
2279
|
+
log(`
|
|
2280
|
+
${colors.primary("Accepting Proposal")}
|
|
2281
|
+
`);
|
|
2282
|
+
log(`Proposal ID: ${formatAddress(id)}`);
|
|
2283
|
+
const spin = spinner("Accepting proposal...").start();
|
|
2284
|
+
try {
|
|
2285
|
+
const response = await fetch(`${config2.apiUrl}/api/proposals/${id}`, {
|
|
2286
|
+
method: "PUT",
|
|
2287
|
+
headers: {
|
|
2288
|
+
"Content-Type": "application/json",
|
|
2289
|
+
"Authorization": `Bearer ${config2.apiKey}`
|
|
2290
|
+
},
|
|
2291
|
+
body: JSON.stringify({ action: "accept" })
|
|
2292
|
+
});
|
|
2293
|
+
const data = await response.json();
|
|
2294
|
+
spin.stop();
|
|
2295
|
+
if (!response.ok) {
|
|
2296
|
+
error(`Failed to accept proposal: ${data.error}`);
|
|
2297
|
+
process.exit(1);
|
|
2298
|
+
}
|
|
2299
|
+
success(`Proposal accepted!`);
|
|
2300
|
+
log(`
|
|
2301
|
+
Task: ${data.task_id}`);
|
|
2302
|
+
log(` Worker: ${formatAddress(data.worker_agent_id)}`);
|
|
2303
|
+
log(` Price: ${formatSui(data.accepted_price_mist)}`);
|
|
2304
|
+
log(`
|
|
2305
|
+
The task is now reserved for the worker.`);
|
|
2306
|
+
} catch (err) {
|
|
2307
|
+
spin.stop();
|
|
2308
|
+
error(`Failed to accept proposal: ${err.message}`);
|
|
2309
|
+
process.exit(1);
|
|
2310
|
+
}
|
|
2311
|
+
});
|
|
2312
|
+
proposalCmd.command("reject <id>").description("Reject a proposal").action(async (id) => {
|
|
2313
|
+
requireApiKey();
|
|
2314
|
+
const config2 = getConfig();
|
|
2315
|
+
const spin = spinner("Rejecting proposal...").start();
|
|
2316
|
+
try {
|
|
2317
|
+
const response = await fetch(`${config2.apiUrl}/api/proposals/${id}`, {
|
|
2318
|
+
method: "PUT",
|
|
2319
|
+
headers: {
|
|
2320
|
+
"Content-Type": "application/json",
|
|
2321
|
+
"Authorization": `Bearer ${config2.apiKey}`
|
|
2322
|
+
},
|
|
2323
|
+
body: JSON.stringify({ action: "reject" })
|
|
2324
|
+
});
|
|
2325
|
+
const data = await response.json();
|
|
2326
|
+
spin.stop();
|
|
2327
|
+
if (!response.ok) {
|
|
2328
|
+
error(`Failed to reject proposal: ${data.error}`);
|
|
2329
|
+
process.exit(1);
|
|
2330
|
+
}
|
|
2331
|
+
success(`Proposal rejected.`);
|
|
2332
|
+
} catch (err) {
|
|
2333
|
+
spin.stop();
|
|
2334
|
+
error(`Failed to reject proposal: ${err.message}`);
|
|
2335
|
+
process.exit(1);
|
|
2336
|
+
}
|
|
2337
|
+
});
|
|
2338
|
+
proposalCmd.command("withdraw <id>").description("Withdraw your proposal").action(async (id) => {
|
|
2339
|
+
requireApiKey();
|
|
2340
|
+
const config2 = getConfig();
|
|
2341
|
+
const spin = spinner("Withdrawing proposal...").start();
|
|
2342
|
+
try {
|
|
2343
|
+
const response = await fetch(`${config2.apiUrl}/api/proposals/${id}`, {
|
|
2344
|
+
method: "DELETE",
|
|
2345
|
+
headers: { "Authorization": `Bearer ${config2.apiKey}` }
|
|
2346
|
+
});
|
|
2347
|
+
const data = await response.json();
|
|
2348
|
+
spin.stop();
|
|
2349
|
+
if (!response.ok) {
|
|
2350
|
+
error(`Failed to withdraw proposal: ${data.error}`);
|
|
2351
|
+
process.exit(1);
|
|
2352
|
+
}
|
|
2353
|
+
success(`Proposal withdrawn.`);
|
|
2354
|
+
} catch (err) {
|
|
2355
|
+
spin.stop();
|
|
2356
|
+
error(`Failed to withdraw proposal: ${err.message}`);
|
|
2357
|
+
process.exit(1);
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// src/index.ts
|
|
2363
|
+
init_config();
|
|
2364
|
+
var program = new Command();
|
|
2365
|
+
var banner = chalk3.cyan(`
|
|
2366
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
2367
|
+
\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D
|
|
2368
|
+
\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551
|
|
2369
|
+
\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2588\u2588\u2557 \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551
|
|
2370
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551
|
|
2371
|
+
\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D
|
|
2372
|
+
`);
|
|
2373
|
+
program.name("clank").description("CLI for Clank Protocol - Agent-to-Agent Skills Marketplace").version("0.7.0").addHelpText("beforeAll", banner).option("--json", "Output as JSON").hook("preAction", (thisCommand) => {
|
|
2374
|
+
const opts = thisCommand.opts();
|
|
2375
|
+
if (opts.json) {
|
|
2376
|
+
const config2 = getConfig();
|
|
2377
|
+
process.env.TASKNET_OUTPUT_FORMAT = "json";
|
|
2378
|
+
}
|
|
2379
|
+
});
|
|
2380
|
+
registerConfigCommands(program);
|
|
2381
|
+
registerAgentCommands(program);
|
|
2382
|
+
registerSkillCommands(program);
|
|
2383
|
+
registerTaskCommands(program);
|
|
2384
|
+
registerBadgeCommands(program);
|
|
2385
|
+
registerClaimCommands(program);
|
|
2386
|
+
registerProposalCommands(program);
|
|
2387
|
+
registerHealthCommand(program);
|
|
2388
|
+
program.parse();
|
|
2389
|
+
//# sourceMappingURL=index.js.map
|