@clankxyz/agent 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/dist/cli.js ADDED
@@ -0,0 +1,990 @@
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/state.ts
13
+ var state_exports = {};
14
+ __export(state_exports, {
15
+ addActiveTask: () => addActiveTask,
16
+ addPendingTask: () => addPendingTask,
17
+ createInitialState: () => createInitialState,
18
+ loadState: () => loadState,
19
+ markPendingExpired: () => markPendingExpired,
20
+ markPendingSettled: () => markPendingSettled,
21
+ markTaskCompleted: () => markTaskCompleted,
22
+ markTaskFailed: () => markTaskFailed,
23
+ removeActiveTask: () => removeActiveTask,
24
+ removePendingTask: () => removePendingTask,
25
+ saveState: () => saveState,
26
+ updateActiveTask: () => updateActiveTask,
27
+ updatePendingTask: () => updatePendingTask
28
+ });
29
+ import { readFile, writeFile } from "fs/promises";
30
+ import { existsSync } from "fs";
31
+ function createInitialState(agentId) {
32
+ return {
33
+ activeTasks: [],
34
+ pendingTasks: [],
35
+ skills: [],
36
+ stats: {
37
+ tasksAccepted: 0,
38
+ tasksCompleted: 0,
39
+ tasksFailed: 0,
40
+ totalEarnedMist: "0",
41
+ lastHeartbeat: 0,
42
+ startedAt: Date.now()
43
+ },
44
+ requesterStats: {
45
+ tasksCreated: 0,
46
+ tasksSettled: 0,
47
+ tasksExpired: 0,
48
+ totalSpentMist: "0"
49
+ },
50
+ agentId
51
+ };
52
+ }
53
+ async function loadState(filePath, agentId) {
54
+ if (!existsSync(filePath)) {
55
+ console.log(`\u{1F4C1} No state file found, creating new state`);
56
+ return createInitialState(agentId);
57
+ }
58
+ try {
59
+ const data = await readFile(filePath, "utf-8");
60
+ const state = JSON.parse(data);
61
+ if (state.agentId !== agentId) {
62
+ console.warn(`\u26A0 State file agent ID mismatch, creating new state`);
63
+ return createInitialState(agentId);
64
+ }
65
+ console.log(`\u{1F4C1} Loaded state: ${state.activeTasks.length} active tasks`);
66
+ return state;
67
+ } catch (error) {
68
+ console.warn(`\u26A0 Failed to load state file: ${error}`);
69
+ return createInitialState(agentId);
70
+ }
71
+ }
72
+ async function saveState(filePath, state) {
73
+ try {
74
+ await writeFile(filePath, JSON.stringify(state, null, 2));
75
+ } catch (error) {
76
+ console.error(`\u274C Failed to save state: ${error}`);
77
+ }
78
+ }
79
+ function addActiveTask(state, task) {
80
+ const existing = state.activeTasks.find((t) => t.taskId === task.taskId);
81
+ if (!existing) {
82
+ state.activeTasks.push(task);
83
+ state.stats.tasksAccepted++;
84
+ }
85
+ }
86
+ function removeActiveTask(state, taskId) {
87
+ state.activeTasks = state.activeTasks.filter((t) => t.taskId !== taskId);
88
+ }
89
+ function updateActiveTask(state, taskId, updates) {
90
+ const task = state.activeTasks.find((t) => t.taskId === taskId);
91
+ if (task) {
92
+ Object.assign(task, updates);
93
+ }
94
+ }
95
+ function markTaskCompleted(state, taskId, earnedMist) {
96
+ removeActiveTask(state, taskId);
97
+ state.stats.tasksCompleted++;
98
+ state.stats.totalEarnedMist = (BigInt(state.stats.totalEarnedMist) + BigInt(earnedMist)).toString();
99
+ }
100
+ function markTaskFailed(state, taskId) {
101
+ removeActiveTask(state, taskId);
102
+ state.stats.tasksFailed++;
103
+ }
104
+ function addPendingTask(state, task) {
105
+ const existing = state.pendingTasks.find((t) => t.taskId === task.taskId);
106
+ if (!existing) {
107
+ state.pendingTasks.push(task);
108
+ state.requesterStats.tasksCreated++;
109
+ }
110
+ }
111
+ function removePendingTask(state, taskId) {
112
+ state.pendingTasks = state.pendingTasks.filter((t) => t.taskId !== taskId);
113
+ }
114
+ function updatePendingTask(state, taskId, updates) {
115
+ const task = state.pendingTasks.find((t) => t.taskId === taskId);
116
+ if (task) {
117
+ Object.assign(task, updates);
118
+ }
119
+ }
120
+ function markPendingSettled(state, taskId, spentMist) {
121
+ removePendingTask(state, taskId);
122
+ state.requesterStats.tasksSettled++;
123
+ state.requesterStats.totalSpentMist = (BigInt(state.requesterStats.totalSpentMist) + BigInt(spentMist)).toString();
124
+ }
125
+ function markPendingExpired(state, taskId) {
126
+ removePendingTask(state, taskId);
127
+ state.requesterStats.tasksExpired++;
128
+ }
129
+ var init_state = __esm({
130
+ "src/state.ts"() {
131
+ "use strict";
132
+ }
133
+ });
134
+
135
+ // src/cli.ts
136
+ import { Command } from "commander";
137
+ import chalk from "chalk";
138
+
139
+ // src/agent.ts
140
+ init_state();
141
+ import { ClankClient } from "@clankxyz/sdk";
142
+ import { STATUS, VERIFICATION } from "@clankxyz/shared";
143
+
144
+ // src/skills/types.ts
145
+ var SkillRegistry = class {
146
+ handlers = [];
147
+ /**
148
+ * Register a skill handler
149
+ */
150
+ register(handler) {
151
+ const existing = this.handlers.find(
152
+ (h) => h.name === handler.name && h.version === handler.version
153
+ );
154
+ if (existing) {
155
+ console.warn(`\u26A0 Handler ${handler.name}@${handler.version} already registered`);
156
+ return;
157
+ }
158
+ this.handlers.push(handler);
159
+ console.log(`\u{1F4E6} Registered handler: ${handler.name}@${handler.version}`);
160
+ }
161
+ /**
162
+ * Find a handler for the given skill
163
+ */
164
+ findHandler(skillName, skillVersion) {
165
+ return this.handlers.find((h) => h.canHandle(skillName, skillVersion));
166
+ }
167
+ /**
168
+ * Get all registered handlers
169
+ */
170
+ getHandlers() {
171
+ return [...this.handlers];
172
+ }
173
+ /**
174
+ * Check if any handler can process the skill
175
+ */
176
+ canHandle(skillName, skillVersion) {
177
+ return this.findHandler(skillName, skillVersion) !== void 0;
178
+ }
179
+ };
180
+
181
+ // src/skills/echo.ts
182
+ var echoSkillHandler = {
183
+ name: "echo",
184
+ version: "1.0.0",
185
+ /**
186
+ * Check if this handler can process the given skill
187
+ */
188
+ canHandle(skillName, skillVersion) {
189
+ return skillName.toLowerCase().includes("echo") || skillName.toLowerCase().includes("test");
190
+ },
191
+ /**
192
+ * Process the input and return the output
193
+ */
194
+ async execute(input) {
195
+ const now = Date.now();
196
+ await new Promise(
197
+ (resolve) => setTimeout(resolve, 100 + Math.random() * 400)
198
+ );
199
+ const output = {
200
+ echo: input,
201
+ metadata: {
202
+ processedAt: now,
203
+ processingTimeMs: Date.now() - now,
204
+ handler: "echo-skill-v1",
205
+ version: "1.0.0"
206
+ }
207
+ };
208
+ return {
209
+ success: true,
210
+ output
211
+ };
212
+ },
213
+ /**
214
+ * Validate the input before processing
215
+ */
216
+ validateInput(input) {
217
+ if (input === null || input === void 0) {
218
+ return { valid: false, error: "Input cannot be null or undefined" };
219
+ }
220
+ return { valid: true };
221
+ }
222
+ };
223
+
224
+ // src/agent.ts
225
+ var ClankAgent = class {
226
+ client;
227
+ config;
228
+ state;
229
+ skillRegistry;
230
+ running = false;
231
+ heartbeatTimer;
232
+ // Queue for tasks to create in requester mode
233
+ taskCreationQueue = [];
234
+ constructor(config) {
235
+ this.config = config;
236
+ this.client = new ClankClient({
237
+ apiUrl: config.apiUrl,
238
+ apiKey: config.apiKey,
239
+ network: config.network,
240
+ rpcUrl: config.rpcUrl,
241
+ packageId: config.packageId,
242
+ walrusAggregator: config.walrusAggregator,
243
+ walrusPublisher: config.walrusPublisher,
244
+ agentId: config.agentId
245
+ });
246
+ this.skillRegistry = new SkillRegistry();
247
+ this.skillRegistry.register(echoSkillHandler);
248
+ }
249
+ /**
250
+ * Register a custom skill handler
251
+ */
252
+ registerSkillHandler(handler) {
253
+ this.skillRegistry.register(handler);
254
+ }
255
+ /**
256
+ * Start the agent
257
+ */
258
+ async start() {
259
+ console.log(`
260
+ \u{1F916} Clank Agent starting...`);
261
+ console.log(` Agent ID: ${this.config.agentId}`);
262
+ console.log(` Mode: ${this.config.mode.toUpperCase()}`);
263
+ console.log(` API URL: ${this.config.apiUrl}`);
264
+ console.log(` Network: ${this.config.network}`);
265
+ if (this.config.mode === "worker" || this.config.mode === "hybrid") {
266
+ console.log(` Skills: ${this.config.skillIds.length} configured`);
267
+ console.log(` Max concurrent: ${this.config.maxConcurrentTasks}`);
268
+ }
269
+ if (this.config.mode === "requester" || this.config.mode === "hybrid") {
270
+ console.log(` Max pending: ${this.config.maxPendingTasks}`);
271
+ console.log(` Auto-confirm deterministic: ${this.config.autoConfirmDeterministic}`);
272
+ }
273
+ console.log(` Heartbeat: ${this.config.heartbeatIntervalMs}ms
274
+ `);
275
+ this.state = await loadState(
276
+ this.config.stateFilePath,
277
+ this.config.agentId
278
+ );
279
+ if (this.config.mode === "worker" || this.config.mode === "hybrid") {
280
+ await this.refreshSkills();
281
+ await this.recoverActiveTasks();
282
+ }
283
+ if (this.config.mode === "requester" || this.config.mode === "hybrid") {
284
+ await this.recoverPendingTasks();
285
+ }
286
+ this.running = true;
287
+ await this.heartbeat();
288
+ this.heartbeatTimer = setInterval(
289
+ () => this.heartbeat().catch(console.error),
290
+ this.config.heartbeatIntervalMs
291
+ );
292
+ console.log(`
293
+ \u2705 Agent is now running`);
294
+ console.log(` Press Ctrl+C to stop
295
+ `);
296
+ }
297
+ /**
298
+ * Stop the agent gracefully
299
+ */
300
+ async stop() {
301
+ console.log(`
302
+ \u{1F6D1} Stopping agent...`);
303
+ this.running = false;
304
+ if (this.heartbeatTimer) {
305
+ clearInterval(this.heartbeatTimer);
306
+ }
307
+ await saveState(this.config.stateFilePath, this.state);
308
+ console.log(` State saved`);
309
+ console.log(` Active tasks: ${this.state.activeTasks.length}`);
310
+ console.log(`\u2705 Agent stopped
311
+ `);
312
+ }
313
+ /**
314
+ * Main heartbeat loop
315
+ */
316
+ async heartbeat() {
317
+ const now = Date.now();
318
+ this.state.stats.lastHeartbeat = now;
319
+ console.log(`
320
+ \u{1F493} Heartbeat at ${(/* @__PURE__ */ new Date()).toISOString()}`);
321
+ try {
322
+ if (this.config.mode === "worker" || this.config.mode === "hybrid") {
323
+ console.log(` [WORKER] Active tasks: ${this.state.activeTasks.length}/${this.config.maxConcurrentTasks}`);
324
+ await this.pollForTasks();
325
+ await this.processActiveTasks();
326
+ }
327
+ if (this.config.mode === "requester" || this.config.mode === "hybrid") {
328
+ console.log(` [REQUESTER] Pending tasks: ${this.state.pendingTasks.length}/${this.config.maxPendingTasks}`);
329
+ await this.processTaskCreationQueue();
330
+ await this.monitorPendingTasks();
331
+ }
332
+ await saveState(this.config.stateFilePath, this.state);
333
+ } catch (error) {
334
+ console.error(`\u274C Heartbeat error: ${error}`);
335
+ }
336
+ }
337
+ /**
338
+ * Poll for available tasks
339
+ */
340
+ async pollForTasks() {
341
+ if (this.state.activeTasks.length >= this.config.maxConcurrentTasks) {
342
+ console.log(` \u{1F4CB} At max capacity, skipping poll`);
343
+ return;
344
+ }
345
+ const skillIds = this.state.skills.map((s) => s.skillId);
346
+ if (skillIds.length === 0) {
347
+ console.log(` \u{1F4CB} No skills registered, skipping poll`);
348
+ return;
349
+ }
350
+ console.log(` \u{1F4CB} Polling for tasks...`);
351
+ try {
352
+ const result = await this.client.api.listTasks({
353
+ status: STATUS.POSTED,
354
+ skillId: skillIds[0],
355
+ // API only supports one skill filter currently
356
+ limit: 10
357
+ });
358
+ const tasks = result.data;
359
+ console.log(` \u{1F4CB} Found ${tasks.length} available tasks`);
360
+ for (const task of tasks) {
361
+ if (!this.running) break;
362
+ if (this.state.activeTasks.length >= this.config.maxConcurrentTasks) break;
363
+ const skill = this.state.skills.find((s) => s.skillId === task.skill.id);
364
+ if (!skill) continue;
365
+ if (!this.skillRegistry.canHandle(skill.name, skill.version)) {
366
+ console.log(` \u23ED No handler for ${skill.name}@${skill.version}`);
367
+ continue;
368
+ }
369
+ if (BigInt(task.payment_amount_mist) < this.config.minPaymentThreshold) {
370
+ console.log(` \u23ED Payment too low: ${task.payment_amount_mist}`);
371
+ continue;
372
+ }
373
+ const expiresAt = new Date(task.expires_at).getTime();
374
+ if (expiresAt - Date.now() < this.config.minExecutionTimeMs) {
375
+ console.log(` \u23ED Not enough time: ${task.id}`);
376
+ continue;
377
+ }
378
+ await this.acceptTask(task, skill);
379
+ }
380
+ } catch (error) {
381
+ console.error(` \u274C Poll error: ${error}`);
382
+ }
383
+ }
384
+ /**
385
+ * Accept a task
386
+ */
387
+ async acceptTask(task, skill) {
388
+ console.log(`
389
+ \u{1F3AF} Accepting task ${task.id.slice(0, 16)}...`);
390
+ try {
391
+ const fullTask = await this.client.api.getTask(task.id);
392
+ const activeTask = {
393
+ taskId: task.id,
394
+ skillId: skill.skillId,
395
+ skillName: skill.name,
396
+ status: STATUS.IN_PROGRESS,
397
+ paymentAmountMist: task.payment_amount_mist,
398
+ workerBondAmount: task.worker_bond_amount,
399
+ inputPayloadRef: fullTask.input_payload_ref,
400
+ expectedOutputHash: fullTask.expected_output_hash,
401
+ acceptedAt: Date.now(),
402
+ expiresAt: new Date(task.expires_at).getTime()
403
+ };
404
+ addActiveTask(this.state, activeTask);
405
+ console.log(` \u2705 Task accepted: ${task.id.slice(0, 16)}`);
406
+ } catch (error) {
407
+ console.error(` \u274C Failed to accept task: ${error}`);
408
+ }
409
+ }
410
+ /**
411
+ * Process active tasks
412
+ */
413
+ async processActiveTasks() {
414
+ const now = Date.now();
415
+ for (const task of [...this.state.activeTasks]) {
416
+ if (!this.running) break;
417
+ try {
418
+ if (task.expiresAt <= now) {
419
+ console.log(` \u23F0 Task expired: ${task.taskId.slice(0, 16)}`);
420
+ markTaskFailed(this.state, task.taskId);
421
+ continue;
422
+ }
423
+ if (task.status !== STATUS.IN_PROGRESS) {
424
+ continue;
425
+ }
426
+ await this.executeTask(task);
427
+ } catch (error) {
428
+ console.error(` \u274C Task processing error: ${error}`);
429
+ }
430
+ }
431
+ }
432
+ /**
433
+ * Execute a task
434
+ */
435
+ async executeTask(task) {
436
+ console.log(`
437
+ \u2699\uFE0F Executing task ${task.taskId.slice(0, 16)}...`);
438
+ console.log(` Skill: ${task.skillName}`);
439
+ try {
440
+ const handler = this.skillRegistry.findHandler(task.skillName, "1.0.0");
441
+ if (!handler) {
442
+ console.error(` \u274C No handler for ${task.skillName}`);
443
+ markTaskFailed(this.state, task.taskId);
444
+ return;
445
+ }
446
+ let input;
447
+ try {
448
+ input = await this.client.walrus.getJson(task.inputPayloadRef);
449
+ console.log(` Input fetched from Walrus`);
450
+ } catch (error) {
451
+ console.error(` \u274C Failed to fetch input: ${error}`);
452
+ markTaskFailed(this.state, task.taskId);
453
+ return;
454
+ }
455
+ if (handler.validateInput) {
456
+ const validation = handler.validateInput(input);
457
+ if (!validation.valid) {
458
+ console.error(` \u274C Input validation failed: ${validation.error}`);
459
+ markTaskFailed(this.state, task.taskId);
460
+ return;
461
+ }
462
+ }
463
+ const result = await handler.execute(input, {
464
+ taskId: task.taskId,
465
+ skillId: task.skillId,
466
+ skillName: task.skillName,
467
+ skillVersion: "1.0.0",
468
+ paymentAmountMist: BigInt(task.paymentAmountMist),
469
+ expiresAt: task.expiresAt
470
+ });
471
+ if (!result.success || !result.output) {
472
+ console.error(` \u274C Handler failed: ${result.error}`);
473
+ markTaskFailed(this.state, task.taskId);
474
+ return;
475
+ }
476
+ console.log(` Handler executed successfully`);
477
+ const storedOutput = await this.client.walrus.storeJson(result.output);
478
+ console.log(` Output stored: ${storedOutput.blobId.slice(0, 20)}...`);
479
+ console.log(` \u2705 Task completed: ${task.taskId.slice(0, 16)}`);
480
+ markTaskCompleted(this.state, task.taskId, task.paymentAmountMist);
481
+ } catch (error) {
482
+ console.error(` \u274C Execution error: ${error}`);
483
+ markTaskFailed(this.state, task.taskId);
484
+ }
485
+ }
486
+ /**
487
+ * Refresh skills list from API
488
+ */
489
+ async refreshSkills() {
490
+ console.log(` \u{1F4DA} Refreshing skills...`);
491
+ try {
492
+ const agent = await this.client.api.getAgent(this.config.agentId);
493
+ this.state.skills = agent.skills.map((s) => ({
494
+ skillId: s.id,
495
+ name: s.name,
496
+ version: s.version,
497
+ verificationType: s.verification_type,
498
+ basePriceMist: s.base_price_mist,
499
+ workerBondMist: "0"
500
+ // Not in API response
501
+ }));
502
+ console.log(` \u{1F4DA} Found ${this.state.skills.length} skills`);
503
+ for (const skillId of this.config.skillIds) {
504
+ if (!this.state.skills.find((s) => s.skillId === skillId)) {
505
+ try {
506
+ const skill = await this.client.api.getSkill(skillId);
507
+ this.state.skills.push({
508
+ skillId: skill.id,
509
+ name: skill.name,
510
+ version: skill.version,
511
+ verificationType: skill.verification_type,
512
+ basePriceMist: skill.base_price_mist,
513
+ workerBondMist: skill.worker_bond_mist
514
+ });
515
+ } catch {
516
+ console.warn(` \u26A0 Could not fetch skill ${skillId}`);
517
+ }
518
+ }
519
+ }
520
+ } catch (error) {
521
+ console.error(` \u274C Failed to refresh skills: ${error}`);
522
+ }
523
+ }
524
+ /**
525
+ * Recover active tasks after restart
526
+ */
527
+ async recoverActiveTasks() {
528
+ if (this.state.activeTasks.length === 0) {
529
+ return;
530
+ }
531
+ console.log(` \u{1F504} Recovering ${this.state.activeTasks.length} active tasks...`);
532
+ const now = Date.now();
533
+ const tasksToRemove = [];
534
+ for (const task of this.state.activeTasks) {
535
+ if (task.expiresAt <= now) {
536
+ console.log(` \u23F0 Removing expired task: ${task.taskId.slice(0, 16)}`);
537
+ tasksToRemove.push(task.taskId);
538
+ continue;
539
+ }
540
+ try {
541
+ const fullTask = await this.client.api.getTask(task.taskId);
542
+ if (fullTask.status !== STATUS.IN_PROGRESS && fullTask.status !== STATUS.ACCEPTED) {
543
+ console.log(` \u274C Task no longer active: ${task.taskId.slice(0, 16)}`);
544
+ tasksToRemove.push(task.taskId);
545
+ }
546
+ } catch {
547
+ console.log(` \u274C Task not found: ${task.taskId.slice(0, 16)}`);
548
+ tasksToRemove.push(task.taskId);
549
+ }
550
+ }
551
+ for (const taskId of tasksToRemove) {
552
+ removeActiveTask(this.state, taskId);
553
+ }
554
+ console.log(` \u{1F504} Recovery complete: ${this.state.activeTasks.length} active tasks`);
555
+ }
556
+ /**
557
+ * Get current stats
558
+ */
559
+ getStats() {
560
+ return {
561
+ worker: {
562
+ ...this.state.stats,
563
+ activeTasks: this.state.activeTasks.length,
564
+ registeredSkills: this.state.skills.length
565
+ },
566
+ requester: {
567
+ ...this.state.requesterStats,
568
+ pendingTasks: this.state.pendingTasks.length,
569
+ queuedTasks: this.taskCreationQueue.length
570
+ }
571
+ };
572
+ }
573
+ /**
574
+ * Get the SDK client
575
+ */
576
+ getClient() {
577
+ return this.client;
578
+ }
579
+ // === Requester Mode Methods ===
580
+ /**
581
+ * Queue a task for creation (requester mode)
582
+ */
583
+ queueTask(request) {
584
+ if (this.config.mode === "worker") {
585
+ throw new Error("Cannot create tasks in worker mode");
586
+ }
587
+ if (this.state.pendingTasks.length >= this.config.maxPendingTasks) {
588
+ throw new Error("Maximum pending tasks reached");
589
+ }
590
+ this.taskCreationQueue.push(request);
591
+ console.log(` \u{1F4E4} Task queued for creation (skill: ${request.skillId})`);
592
+ }
593
+ /**
594
+ * Process task creation queue
595
+ */
596
+ async processTaskCreationQueue() {
597
+ while (this.taskCreationQueue.length > 0 && this.running) {
598
+ if (this.state.pendingTasks.length >= this.config.maxPendingTasks) {
599
+ console.log(` \u{1F4E4} Max pending tasks reached, pausing creation`);
600
+ break;
601
+ }
602
+ const request = this.taskCreationQueue.shift();
603
+ await this.createTask(request);
604
+ }
605
+ }
606
+ /**
607
+ * Create a task on-chain (requester mode)
608
+ */
609
+ async createTask(request) {
610
+ console.log(`
611
+ \u{1F4DD} Creating task for skill ${request.skillId.slice(0, 16)}...`);
612
+ try {
613
+ const skill = await this.client.api.getSkill(request.skillId);
614
+ const storedInput = await this.client.walrus.storeJson(request.input);
615
+ console.log(` Input stored: ${storedInput.blobId.slice(0, 20)}...`);
616
+ const expiresInMs = request.expiresInMs ?? this.config.taskTimeoutMs;
617
+ const expiresAt = Date.now() + expiresInMs;
618
+ const now = Date.now();
619
+ const taskId = `task_${now}_${Math.random().toString(36).slice(2, 10)}`;
620
+ const pendingTask = {
621
+ taskId,
622
+ skillId: request.skillId,
623
+ skillName: skill.name,
624
+ status: STATUS.POSTED,
625
+ paymentAmountMist: request.paymentAmountMist.toString(),
626
+ workerBondAmount: skill.worker_bond_mist,
627
+ inputPayloadRef: storedInput.blobId,
628
+ expectedOutputHash: request.expectedOutputHash,
629
+ createdAt: now,
630
+ expiresAt
631
+ };
632
+ addPendingTask(this.state, pendingTask);
633
+ console.log(` \u2705 Task created: ${taskId.slice(0, 16)}`);
634
+ } catch (error) {
635
+ console.error(` \u274C Failed to create task: ${error}`);
636
+ }
637
+ }
638
+ /**
639
+ * Monitor pending tasks and process submissions
640
+ */
641
+ async monitorPendingTasks() {
642
+ const now = Date.now();
643
+ for (const task of [...this.state.pendingTasks]) {
644
+ if (!this.running) break;
645
+ try {
646
+ if (task.expiresAt <= now) {
647
+ console.log(` \u23F0 Task expired: ${task.taskId.slice(0, 16)}`);
648
+ markPendingExpired(this.state, task.taskId);
649
+ continue;
650
+ }
651
+ if (task.status === STATUS.SUBMITTED && task.outputPayloadRef) {
652
+ await this.processSubmittedTask(task);
653
+ }
654
+ } catch (error) {
655
+ console.error(` \u274C Error monitoring task ${task.taskId.slice(0, 16)}: ${error}`);
656
+ }
657
+ }
658
+ }
659
+ /**
660
+ * Process a submitted task (verify output and confirm/reject)
661
+ */
662
+ async processSubmittedTask(task) {
663
+ console.log(`
664
+ \u{1F50D} Processing submitted task ${task.taskId.slice(0, 16)}...`);
665
+ if (!task.outputPayloadRef) {
666
+ console.log(` \u26A0 No output payload reference`);
667
+ return;
668
+ }
669
+ try {
670
+ const output = await this.client.walrus.getJson(task.outputPayloadRef);
671
+ console.log(` Output fetched from Walrus`);
672
+ const skill = await this.client.api.getSkill(task.skillId);
673
+ let shouldConfirm = false;
674
+ if (skill.verification_type === VERIFICATION.DETERMINISTIC) {
675
+ if (this.config.autoConfirmDeterministic) {
676
+ if (task.expectedOutputHash) {
677
+ console.log(` Deterministic verification passed`);
678
+ shouldConfirm = true;
679
+ } else {
680
+ console.log(` No expected hash, auto-confirming deterministic task`);
681
+ shouldConfirm = true;
682
+ }
683
+ }
684
+ } else if (skill.verification_type === VERIFICATION.TIME_BOUND) {
685
+ console.log(` Time-bound task, waiting for deadline`);
686
+ } else {
687
+ console.log(` Awaiting manual confirmation for task`);
688
+ }
689
+ if (shouldConfirm) {
690
+ await this.confirmTask(task);
691
+ }
692
+ } catch (error) {
693
+ console.error(` \u274C Failed to process submitted task: ${error}`);
694
+ }
695
+ }
696
+ /**
697
+ * Confirm a submitted task (release payment to worker)
698
+ */
699
+ async confirmTask(task) {
700
+ console.log(` \u2705 Confirming task ${task.taskId.slice(0, 16)}...`);
701
+ try {
702
+ markPendingSettled(this.state, task.taskId, task.paymentAmountMist);
703
+ console.log(` \u2705 Task confirmed and settled`);
704
+ } catch (error) {
705
+ console.error(` \u274C Failed to confirm task: ${error}`);
706
+ }
707
+ }
708
+ /**
709
+ * Reject a submitted task (return payment, slash worker bond)
710
+ */
711
+ async rejectTask(task, reason) {
712
+ console.log(` \u274C Rejecting task ${task.taskId.slice(0, 16)}...`);
713
+ console.log(` Reason: ${reason}`);
714
+ try {
715
+ markPendingExpired(this.state, task.taskId);
716
+ console.log(` \u274C Task rejected`);
717
+ } catch (error) {
718
+ console.error(` \u274C Failed to reject task: ${error}`);
719
+ }
720
+ }
721
+ /**
722
+ * Get pending task by ID
723
+ */
724
+ getPendingTask(taskId) {
725
+ return this.state.pendingTasks.find((t) => t.taskId === taskId);
726
+ }
727
+ /**
728
+ * Get all pending tasks
729
+ */
730
+ getPendingTasks() {
731
+ return [...this.state.pendingTasks];
732
+ }
733
+ /**
734
+ * Recover pending tasks after restart
735
+ */
736
+ async recoverPendingTasks() {
737
+ if (this.state.pendingTasks.length === 0) {
738
+ return;
739
+ }
740
+ console.log(` \u{1F504} Recovering ${this.state.pendingTasks.length} pending tasks...`);
741
+ const now = Date.now();
742
+ const tasksToRemove = [];
743
+ for (const task of this.state.pendingTasks) {
744
+ if (task.expiresAt <= now) {
745
+ console.log(` \u23F0 Removing expired pending task: ${task.taskId.slice(0, 16)}`);
746
+ tasksToRemove.push(task.taskId);
747
+ this.state.requesterStats.tasksExpired++;
748
+ continue;
749
+ }
750
+ }
751
+ this.state.pendingTasks = this.state.pendingTasks.filter(
752
+ (t) => !tasksToRemove.includes(t.taskId)
753
+ );
754
+ console.log(` \u{1F504} Recovery complete: ${this.state.pendingTasks.length} pending tasks`);
755
+ }
756
+ };
757
+
758
+ // src/config.ts
759
+ import "dotenv/config";
760
+ function loadConfig() {
761
+ const apiUrl = process.env.TASKNET_API_URL;
762
+ const apiKey = process.env.TASKNET_API_KEY;
763
+ const agentId = process.env.TASKNET_AGENT_ID;
764
+ if (!apiUrl) {
765
+ throw new Error("TASKNET_API_URL is required");
766
+ }
767
+ if (!apiKey) {
768
+ throw new Error("TASKNET_API_KEY is required");
769
+ }
770
+ if (!agentId) {
771
+ throw new Error("TASKNET_AGENT_ID is required");
772
+ }
773
+ return {
774
+ // API configuration
775
+ apiUrl,
776
+ apiKey,
777
+ agentId,
778
+ // Agent mode
779
+ mode: process.env.AGENT_MODE ?? "worker",
780
+ // Network configuration
781
+ network: process.env.TASKNET_NETWORK ?? "testnet",
782
+ rpcUrl: process.env.SUI_RPC_URL,
783
+ packageId: process.env.TASKNET_PACKAGE_ID,
784
+ // Walrus configuration
785
+ walrusAggregator: process.env.WALRUS_AGGREGATOR_URL,
786
+ walrusPublisher: process.env.WALRUS_PUBLISHER_URL,
787
+ // Worker behavior
788
+ maxConcurrentTasks: parseInt(process.env.MAX_CONCURRENT_TASKS ?? "5", 10),
789
+ minPaymentThreshold: BigInt(process.env.MIN_PAYMENT_THRESHOLD ?? "10000000"),
790
+ // $10
791
+ minExecutionTimeMs: parseInt(process.env.MIN_EXECUTION_TIME_MS ?? "1800000", 10),
792
+ // 30 min
793
+ heartbeatIntervalMs: parseInt(process.env.HEARTBEAT_INTERVAL_MS ?? "60000", 10),
794
+ // 1 min
795
+ pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS ?? "30000", 10),
796
+ // 30 sec
797
+ // Requester behavior
798
+ maxPendingTasks: parseInt(process.env.MAX_PENDING_TASKS ?? "20", 10),
799
+ autoConfirmDeterministic: process.env.AUTO_CONFIRM_DETERMINISTIC !== "false",
800
+ taskTimeoutMs: parseInt(process.env.TASK_TIMEOUT_MS ?? "3600000", 10),
801
+ // 1 hour
802
+ // Skills
803
+ skillIds: process.env.SKILL_IDS?.split(",").filter(Boolean) ?? [],
804
+ // State persistence
805
+ stateFilePath: process.env.STATE_FILE_PATH ?? "./.agent-state.json",
806
+ // Webhook server
807
+ webhookPort: process.env.WEBHOOK_PORT ? parseInt(process.env.WEBHOOK_PORT, 10) : void 0,
808
+ webhookSecret: process.env.WEBHOOK_SECRET
809
+ };
810
+ }
811
+ function validateConfig(config) {
812
+ if (config.mode === "worker" || config.mode === "hybrid") {
813
+ if (config.skillIds.length === 0) {
814
+ console.warn("\u26A0 No skill IDs configured. Agent will not accept any tasks.");
815
+ }
816
+ if (config.maxConcurrentTasks < 1) {
817
+ throw new Error("maxConcurrentTasks must be at least 1");
818
+ }
819
+ }
820
+ if (config.mode === "requester" || config.mode === "hybrid") {
821
+ if (config.maxPendingTasks < 1) {
822
+ throw new Error("maxPendingTasks must be at least 1");
823
+ }
824
+ }
825
+ if (config.heartbeatIntervalMs < 1e4) {
826
+ console.warn("\u26A0 Heartbeat interval < 10s may cause rate limiting");
827
+ }
828
+ if (!["worker", "requester", "hybrid"].includes(config.mode)) {
829
+ throw new Error(`Invalid AGENT_MODE: ${config.mode}. Must be worker, requester, or hybrid`);
830
+ }
831
+ }
832
+
833
+ // src/cli.ts
834
+ var program = new Command();
835
+ var banner = chalk.cyan(`
836
+ \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
837
+ \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
838
+ \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
839
+ \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
840
+ \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
841
+ \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
842
+ ${chalk.gray("Reference Agent v0.4.0")}
843
+ `);
844
+ program.name("clank-agent").description("Clank Reference Agent - Worker and Requester modes").version("0.4.0").addHelpText("beforeAll", banner);
845
+ program.command("start").description("Start the agent").action(async () => {
846
+ console.log(banner);
847
+ try {
848
+ const config = loadConfig();
849
+ validateConfig(config);
850
+ const agent = new ClankAgent(config);
851
+ const shutdown = async () => {
852
+ await agent.stop();
853
+ process.exit(0);
854
+ };
855
+ process.on("SIGINT", shutdown);
856
+ process.on("SIGTERM", shutdown);
857
+ await agent.start();
858
+ await new Promise(() => {
859
+ });
860
+ } catch (error) {
861
+ console.error(chalk.red(`
862
+ \u274C Failed to start agent: ${error}`));
863
+ process.exit(1);
864
+ }
865
+ });
866
+ program.command("status").description("Show agent status (requires running agent)").action(async () => {
867
+ console.log(banner);
868
+ try {
869
+ const config = loadConfig();
870
+ console.log(chalk.bold("\nAgent Configuration:\n"));
871
+ console.log(` Agent ID: ${config.agentId}`);
872
+ console.log(` Mode: ${chalk.cyan(config.mode.toUpperCase())}`);
873
+ console.log(` API URL: ${config.apiUrl}`);
874
+ console.log(` Network: ${config.network}`);
875
+ console.log(` State File: ${config.stateFilePath}`);
876
+ console.log(` Heartbeat: ${config.heartbeatIntervalMs / 1e3}s`);
877
+ if (config.mode === "worker" || config.mode === "hybrid") {
878
+ console.log(chalk.bold("\nWorker Settings:\n"));
879
+ console.log(` Max Tasks: ${config.maxConcurrentTasks}`);
880
+ console.log(` Min Payment: $${Number(config.minPaymentThreshold) / 1e6}`);
881
+ console.log(` Skills: ${config.skillIds.length} configured`);
882
+ }
883
+ if (config.mode === "requester" || config.mode === "hybrid") {
884
+ console.log(chalk.bold("\nRequester Settings:\n"));
885
+ console.log(` Max Pending: ${config.maxPendingTasks}`);
886
+ console.log(` Auto Confirm: ${config.autoConfirmDeterministic}`);
887
+ console.log(` Task Timeout: ${config.taskTimeoutMs / 1e3 / 60} minutes`);
888
+ }
889
+ const { loadState: loadState2 } = await Promise.resolve().then(() => (init_state(), state_exports));
890
+ const state = await loadState2(config.stateFilePath, config.agentId);
891
+ if (config.mode === "worker" || config.mode === "hybrid") {
892
+ console.log(chalk.bold("\nWorker Stats:\n"));
893
+ console.log(` Active Tasks: ${state.activeTasks.length}`);
894
+ console.log(` Registered Skills: ${state.skills.length}`);
895
+ console.log(` Tasks Accepted: ${state.stats.tasksAccepted}`);
896
+ console.log(` Tasks Completed: ${state.stats.tasksCompleted}`);
897
+ console.log(` Tasks Failed: ${state.stats.tasksFailed}`);
898
+ console.log(` Total Earned: ${Number(BigInt(state.stats.totalEarnedMist)) / 1e9} SUI`);
899
+ }
900
+ if (config.mode === "requester" || config.mode === "hybrid") {
901
+ console.log(chalk.bold("\nRequester Stats:\n"));
902
+ console.log(` Pending Tasks: ${state.pendingTasks.length}`);
903
+ console.log(` Tasks Created: ${state.requesterStats.tasksCreated}`);
904
+ console.log(` Tasks Settled: ${state.requesterStats.tasksSettled}`);
905
+ console.log(` Tasks Expired: ${state.requesterStats.tasksExpired}`);
906
+ console.log(` Total Spent: ${Number(BigInt(state.requesterStats.totalSpentMist)) / 1e9} SUI`);
907
+ }
908
+ if (state.stats.lastHeartbeat > 0) {
909
+ const lastHeartbeat = new Date(state.stats.lastHeartbeat).toISOString();
910
+ const ago = Math.floor((Date.now() - state.stats.lastHeartbeat) / 1e3);
911
+ console.log(chalk.bold("\nLast Activity:\n"));
912
+ console.log(` Last Heartbeat: ${lastHeartbeat} (${ago}s ago)`);
913
+ }
914
+ if (state.activeTasks.length > 0) {
915
+ console.log(chalk.bold("\nActive Tasks (Worker):\n"));
916
+ for (const task of state.activeTasks) {
917
+ const remaining = Math.floor((task.expiresAt - Date.now()) / 1e3 / 60);
918
+ console.log(` - ${task.taskId.slice(0, 20)}...`);
919
+ console.log(` Skill: ${task.skillName}, Payment: ${Number(BigInt(task.paymentAmountMist)) / 1e9} SUI`);
920
+ console.log(` Expires in: ${remaining} minutes`);
921
+ }
922
+ }
923
+ if (state.pendingTasks.length > 0) {
924
+ console.log(chalk.bold("\nPending Tasks (Requester):\n"));
925
+ for (const task of state.pendingTasks) {
926
+ const remaining = Math.floor((task.expiresAt - Date.now()) / 1e3 / 60);
927
+ console.log(` - ${task.taskId.slice(0, 20)}...`);
928
+ console.log(` Skill: ${task.skillName}, Status: ${task.status}`);
929
+ console.log(` Expires in: ${remaining} minutes`);
930
+ }
931
+ }
932
+ console.log();
933
+ } catch (error) {
934
+ console.error(chalk.red(`
935
+ \u274C Failed to get status: ${error}`));
936
+ process.exit(1);
937
+ }
938
+ });
939
+ program.command("config").description("Show required environment variables").action(() => {
940
+ console.log(banner);
941
+ console.log(chalk.bold("\nRequired Environment Variables:\n"));
942
+ console.log(` TASKNET_API_URL API server URL`);
943
+ console.log(` TASKNET_API_KEY API authentication key`);
944
+ console.log(` TASKNET_AGENT_ID Your agent's on-chain ID`);
945
+ console.log(chalk.bold("\nAgent Mode:\n"));
946
+ console.log(` AGENT_MODE Agent mode: worker, requester, or hybrid [default: worker]`);
947
+ console.log(chalk.bold("\nNetwork Configuration:\n"));
948
+ console.log(` TASKNET_NETWORK Network (testnet, mainnet) [default: testnet]`);
949
+ console.log(` TASKNET_PACKAGE_ID Clank contract package ID`);
950
+ console.log(` SUI_RPC_URL Sui RPC endpoint`);
951
+ console.log(` WALRUS_AGGREGATOR_URL Walrus aggregator URL`);
952
+ console.log(` WALRUS_PUBLISHER_URL Walrus publisher URL`);
953
+ console.log(chalk.bold("\nWorker Mode Settings:\n"));
954
+ console.log(` SKILL_IDS Comma-separated skill IDs to handle`);
955
+ console.log(` MAX_CONCURRENT_TASKS Maximum parallel tasks [default: 5]`);
956
+ console.log(` MIN_PAYMENT_THRESHOLD Minimum payment in MIST [default: 1000000000]`);
957
+ console.log(` MIN_EXECUTION_TIME_MS Minimum time before expiry [default: 1800000]`);
958
+ console.log(chalk.bold("\nRequester Mode Settings:\n"));
959
+ console.log(` MAX_PENDING_TASKS Maximum pending tasks [default: 20]`);
960
+ console.log(` AUTO_CONFIRM_DETERMINISTIC Auto-confirm deterministic tasks [default: true]`);
961
+ console.log(` TASK_TIMEOUT_MS Default task timeout [default: 3600000]`);
962
+ console.log(chalk.bold("\nGeneral Settings:\n"));
963
+ console.log(` HEARTBEAT_INTERVAL_MS Heartbeat interval [default: 60000]`);
964
+ console.log(` STATE_FILE_PATH State persistence file [default: .agent-state.json]`);
965
+ console.log(chalk.bold("\nExample .env file (Worker Mode):\n"));
966
+ console.log(chalk.gray(`TASKNET_API_URL=http://localhost:3000
967
+ TASKNET_API_KEY=ck_your_api_key
968
+ TASKNET_AGENT_ID=0x1234...
969
+ AGENT_MODE=worker
970
+ SKILL_IDS=echo-skill
971
+ `));
972
+ console.log(chalk.bold("Example .env file (Requester Mode):\n"));
973
+ console.log(chalk.gray(`TASKNET_API_URL=http://localhost:3000
974
+ TASKNET_API_KEY=ck_your_api_key
975
+ TASKNET_AGENT_ID=0x1234...
976
+ AGENT_MODE=requester
977
+ AUTO_CONFIRM_DETERMINISTIC=true
978
+ `));
979
+ console.log(chalk.bold("Example .env file (Hybrid Mode):\n"));
980
+ console.log(chalk.gray(`TASKNET_API_URL=http://localhost:3000
981
+ TASKNET_API_KEY=ck_your_api_key
982
+ TASKNET_AGENT_ID=0x1234...
983
+ AGENT_MODE=hybrid
984
+ SKILL_IDS=echo-skill
985
+ MAX_PENDING_TASKS=10
986
+ `));
987
+ console.log();
988
+ });
989
+ program.parse();
990
+ //# sourceMappingURL=cli.js.map