@clankxyz/agent 0.1.0 → 0.1.2
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 +17 -17
- package/dist/cli.js +994 -29
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +81 -15
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/dist/cli.js
CHANGED
|
@@ -140,6 +140,10 @@ import chalk from "chalk";
|
|
|
140
140
|
init_state();
|
|
141
141
|
import { ClankClient } from "@clankxyz/sdk";
|
|
142
142
|
import { STATUS, VERIFICATION } from "@clankxyz/shared";
|
|
143
|
+
import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
|
|
144
|
+
import { SuiClient } from "@mysten/sui/client";
|
|
145
|
+
import { Transaction } from "@mysten/sui/transactions";
|
|
146
|
+
import { decodeSuiPrivateKey } from "@mysten/sui/cryptography";
|
|
143
147
|
|
|
144
148
|
// src/skills/types.ts
|
|
145
149
|
var SkillRegistry = class {
|
|
@@ -221,6 +225,902 @@ var echoSkillHandler = {
|
|
|
221
225
|
}
|
|
222
226
|
};
|
|
223
227
|
|
|
228
|
+
// src/skills/translation.ts
|
|
229
|
+
var SUPPORTED_LANGUAGES = [
|
|
230
|
+
"en",
|
|
231
|
+
// English
|
|
232
|
+
"es",
|
|
233
|
+
// Spanish
|
|
234
|
+
"fr",
|
|
235
|
+
// French
|
|
236
|
+
"de",
|
|
237
|
+
// German
|
|
238
|
+
"it",
|
|
239
|
+
// Italian
|
|
240
|
+
"pt",
|
|
241
|
+
// Portuguese
|
|
242
|
+
"ja",
|
|
243
|
+
// Japanese
|
|
244
|
+
"zh",
|
|
245
|
+
// Chinese
|
|
246
|
+
"ko",
|
|
247
|
+
// Korean
|
|
248
|
+
"ar"
|
|
249
|
+
// Arabic
|
|
250
|
+
];
|
|
251
|
+
function mockTranslate(text, targetLang) {
|
|
252
|
+
const prefixes = {
|
|
253
|
+
en: "[EN]",
|
|
254
|
+
es: "[ES]",
|
|
255
|
+
fr: "[FR]",
|
|
256
|
+
de: "[DE]",
|
|
257
|
+
it: "[IT]",
|
|
258
|
+
pt: "[PT]",
|
|
259
|
+
ja: "[JA]",
|
|
260
|
+
zh: "[ZH]",
|
|
261
|
+
ko: "[KO]",
|
|
262
|
+
ar: "[AR]"
|
|
263
|
+
};
|
|
264
|
+
return `${prefixes[targetLang]} ${text}`;
|
|
265
|
+
}
|
|
266
|
+
async function llmTranslate(text, sourceLang, targetLang) {
|
|
267
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
268
|
+
if (!apiKey) {
|
|
269
|
+
throw new Error("OPENAI_API_KEY not set");
|
|
270
|
+
}
|
|
271
|
+
const languageNames = {
|
|
272
|
+
en: "English",
|
|
273
|
+
es: "Spanish",
|
|
274
|
+
fr: "French",
|
|
275
|
+
de: "German",
|
|
276
|
+
it: "Italian",
|
|
277
|
+
pt: "Portuguese",
|
|
278
|
+
ja: "Japanese",
|
|
279
|
+
zh: "Chinese",
|
|
280
|
+
ko: "Korean",
|
|
281
|
+
ar: "Arabic"
|
|
282
|
+
};
|
|
283
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
284
|
+
method: "POST",
|
|
285
|
+
headers: {
|
|
286
|
+
"Content-Type": "application/json",
|
|
287
|
+
Authorization: `Bearer ${apiKey}`
|
|
288
|
+
},
|
|
289
|
+
body: JSON.stringify({
|
|
290
|
+
model: "gpt-4o-mini",
|
|
291
|
+
messages: [
|
|
292
|
+
{
|
|
293
|
+
role: "system",
|
|
294
|
+
content: `You are a professional translator. Translate the following text from ${sourceLang} to ${languageNames[targetLang]}. Only output the translated text, nothing else.`
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
role: "user",
|
|
298
|
+
content: text
|
|
299
|
+
}
|
|
300
|
+
],
|
|
301
|
+
temperature: 0.3,
|
|
302
|
+
max_tokens: 2048
|
|
303
|
+
})
|
|
304
|
+
});
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
const error = await response.text();
|
|
307
|
+
throw new Error(`OpenAI API error: ${error}`);
|
|
308
|
+
}
|
|
309
|
+
const data = await response.json();
|
|
310
|
+
return data.choices[0].message.content.trim();
|
|
311
|
+
}
|
|
312
|
+
var translationSkillHandler = {
|
|
313
|
+
name: "translation",
|
|
314
|
+
version: "1.0.0",
|
|
315
|
+
canHandle(skillName, skillVersion) {
|
|
316
|
+
const name = skillName.toLowerCase();
|
|
317
|
+
return name.includes("translat") || name.includes("translate") || name === "translation";
|
|
318
|
+
},
|
|
319
|
+
async execute(input, context) {
|
|
320
|
+
const startTime = Date.now();
|
|
321
|
+
try {
|
|
322
|
+
const { text, source_language, target_language } = input;
|
|
323
|
+
if (!text || typeof text !== "string") {
|
|
324
|
+
return {
|
|
325
|
+
success: false,
|
|
326
|
+
error: "Missing or invalid 'text' field"
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (!target_language || !SUPPORTED_LANGUAGES.includes(target_language)) {
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
error: `Invalid target_language. Supported: ${SUPPORTED_LANGUAGES.join(", ")}`
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const sourceLang = source_language || "en";
|
|
336
|
+
const useLLM = !!process.env.OPENAI_API_KEY;
|
|
337
|
+
let translatedText;
|
|
338
|
+
if (useLLM) {
|
|
339
|
+
translatedText = await llmTranslate(text, sourceLang, target_language);
|
|
340
|
+
} else {
|
|
341
|
+
translatedText = mockTranslate(text, target_language);
|
|
342
|
+
}
|
|
343
|
+
const output = {
|
|
344
|
+
translated_text: translatedText,
|
|
345
|
+
source_language: sourceLang,
|
|
346
|
+
target_language,
|
|
347
|
+
confidence: useLLM ? 0.95 : 0.5,
|
|
348
|
+
// Mock has low confidence
|
|
349
|
+
word_count: text.split(/\s+/).length,
|
|
350
|
+
processing_time_ms: Date.now() - startTime,
|
|
351
|
+
mode: useLLM ? "llm" : "mock"
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
success: true,
|
|
355
|
+
output
|
|
356
|
+
};
|
|
357
|
+
} catch (error) {
|
|
358
|
+
return {
|
|
359
|
+
success: false,
|
|
360
|
+
error: error instanceof Error ? error.message : "Translation failed"
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
validateInput(input) {
|
|
365
|
+
const { text, target_language } = input;
|
|
366
|
+
if (!text || typeof text !== "string") {
|
|
367
|
+
return { valid: false, error: "Missing or invalid 'text' field" };
|
|
368
|
+
}
|
|
369
|
+
if (text.length > 1e4) {
|
|
370
|
+
return { valid: false, error: "Text too long (max 10000 characters)" };
|
|
371
|
+
}
|
|
372
|
+
if (!target_language) {
|
|
373
|
+
return { valid: false, error: "Missing 'target_language' field" };
|
|
374
|
+
}
|
|
375
|
+
if (!SUPPORTED_LANGUAGES.includes(target_language)) {
|
|
376
|
+
return {
|
|
377
|
+
valid: false,
|
|
378
|
+
error: `Unsupported target_language. Use: ${SUPPORTED_LANGUAGES.join(", ")}`
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return { valid: true };
|
|
382
|
+
},
|
|
383
|
+
estimateExecutionTime(input) {
|
|
384
|
+
const { text } = input;
|
|
385
|
+
const wordCount = text?.split(/\s+/).length || 0;
|
|
386
|
+
const useLLM = !!process.env.OPENAI_API_KEY;
|
|
387
|
+
return useLLM ? wordCount * 50 + 1e3 : wordCount * 10 + 100;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// src/skills/summarization.ts
|
|
392
|
+
function mockSummarize(text, maxSentences) {
|
|
393
|
+
const sentences = text.split(/(?<=[.!?])\s+/).filter((s) => s.trim().length > 0);
|
|
394
|
+
const selectedSentences = sentences.slice(0, maxSentences);
|
|
395
|
+
return selectedSentences.join(" ");
|
|
396
|
+
}
|
|
397
|
+
async function llmSummarize(text, length, style) {
|
|
398
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
399
|
+
if (!apiKey) {
|
|
400
|
+
throw new Error("OPENAI_API_KEY not set");
|
|
401
|
+
}
|
|
402
|
+
const lengthInstructions = {
|
|
403
|
+
short: "Provide a very brief summary in 1-2 sentences.",
|
|
404
|
+
medium: "Provide a concise summary in 3-5 sentences.",
|
|
405
|
+
long: "Provide a comprehensive summary covering all main points."
|
|
406
|
+
};
|
|
407
|
+
const styleInstructions = {
|
|
408
|
+
bullet: "Format as bullet points.",
|
|
409
|
+
paragraph: "Format as flowing prose."
|
|
410
|
+
};
|
|
411
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
412
|
+
method: "POST",
|
|
413
|
+
headers: {
|
|
414
|
+
"Content-Type": "application/json",
|
|
415
|
+
Authorization: `Bearer ${apiKey}`
|
|
416
|
+
},
|
|
417
|
+
body: JSON.stringify({
|
|
418
|
+
model: "gpt-4o-mini",
|
|
419
|
+
messages: [
|
|
420
|
+
{
|
|
421
|
+
role: "system",
|
|
422
|
+
content: `You are a professional summarizer. ${lengthInstructions[length]} ${styleInstructions[style]} Only output the summary, nothing else.`
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
role: "user",
|
|
426
|
+
content: `Summarize this text:
|
|
427
|
+
|
|
428
|
+
${text}`
|
|
429
|
+
}
|
|
430
|
+
],
|
|
431
|
+
temperature: 0.3,
|
|
432
|
+
max_tokens: 1024
|
|
433
|
+
})
|
|
434
|
+
});
|
|
435
|
+
if (!response.ok) {
|
|
436
|
+
const error = await response.text();
|
|
437
|
+
throw new Error(`OpenAI API error: ${error}`);
|
|
438
|
+
}
|
|
439
|
+
const data = await response.json();
|
|
440
|
+
return data.choices[0].message.content.trim();
|
|
441
|
+
}
|
|
442
|
+
var summarizationSkillHandler = {
|
|
443
|
+
name: "summarization",
|
|
444
|
+
version: "1.0.0",
|
|
445
|
+
canHandle(skillName, skillVersion) {
|
|
446
|
+
const name = skillName.toLowerCase();
|
|
447
|
+
return name.includes("summar") || name.includes("digest") || name.includes("tldr") || name === "summarize";
|
|
448
|
+
},
|
|
449
|
+
async execute(input, context) {
|
|
450
|
+
const startTime = Date.now();
|
|
451
|
+
try {
|
|
452
|
+
const {
|
|
453
|
+
text,
|
|
454
|
+
length = "medium",
|
|
455
|
+
max_sentences,
|
|
456
|
+
style = "paragraph"
|
|
457
|
+
} = input;
|
|
458
|
+
if (!text || typeof text !== "string") {
|
|
459
|
+
return {
|
|
460
|
+
success: false,
|
|
461
|
+
error: "Missing or invalid 'text' field"
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const useLLM = !!process.env.OPENAI_API_KEY;
|
|
465
|
+
let summary;
|
|
466
|
+
if (useLLM) {
|
|
467
|
+
summary = await llmSummarize(text, length, style);
|
|
468
|
+
} else {
|
|
469
|
+
const sentenceCounts = {
|
|
470
|
+
short: 2,
|
|
471
|
+
medium: 4,
|
|
472
|
+
long: 8
|
|
473
|
+
};
|
|
474
|
+
const numSentences = max_sentences || sentenceCounts[length];
|
|
475
|
+
summary = mockSummarize(text, numSentences);
|
|
476
|
+
}
|
|
477
|
+
const output = {
|
|
478
|
+
summary,
|
|
479
|
+
original_length: text.length,
|
|
480
|
+
summary_length: summary.length,
|
|
481
|
+
compression_ratio: Math.round((1 - summary.length / text.length) * 100) / 100,
|
|
482
|
+
sentence_count: summary.split(/(?<=[.!?])\s+/).length,
|
|
483
|
+
processing_time_ms: Date.now() - startTime,
|
|
484
|
+
mode: useLLM ? "llm" : "mock"
|
|
485
|
+
};
|
|
486
|
+
return {
|
|
487
|
+
success: true,
|
|
488
|
+
output
|
|
489
|
+
};
|
|
490
|
+
} catch (error) {
|
|
491
|
+
return {
|
|
492
|
+
success: false,
|
|
493
|
+
error: error instanceof Error ? error.message : "Summarization failed"
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
validateInput(input) {
|
|
498
|
+
const { text, length, style } = input;
|
|
499
|
+
if (!text || typeof text !== "string") {
|
|
500
|
+
return { valid: false, error: "Missing or invalid 'text' field" };
|
|
501
|
+
}
|
|
502
|
+
if (text.length < 50) {
|
|
503
|
+
return { valid: false, error: "Text too short (min 50 characters)" };
|
|
504
|
+
}
|
|
505
|
+
if (text.length > 5e4) {
|
|
506
|
+
return { valid: false, error: "Text too long (max 50000 characters)" };
|
|
507
|
+
}
|
|
508
|
+
if (length && !["short", "medium", "long"].includes(length)) {
|
|
509
|
+
return { valid: false, error: "Invalid length. Use: short, medium, long" };
|
|
510
|
+
}
|
|
511
|
+
if (style && !["bullet", "paragraph"].includes(style)) {
|
|
512
|
+
return { valid: false, error: "Invalid style. Use: bullet, paragraph" };
|
|
513
|
+
}
|
|
514
|
+
return { valid: true };
|
|
515
|
+
},
|
|
516
|
+
estimateExecutionTime(input) {
|
|
517
|
+
const { text } = input;
|
|
518
|
+
const charCount = text?.length || 0;
|
|
519
|
+
const useLLM = !!process.env.OPENAI_API_KEY;
|
|
520
|
+
return useLLM ? charCount / 10 + 2e3 : charCount / 100 + 100;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/skills/sentiment.ts
|
|
525
|
+
var POSITIVE_WORDS = /* @__PURE__ */ new Set([
|
|
526
|
+
"good",
|
|
527
|
+
"great",
|
|
528
|
+
"excellent",
|
|
529
|
+
"amazing",
|
|
530
|
+
"wonderful",
|
|
531
|
+
"fantastic",
|
|
532
|
+
"love",
|
|
533
|
+
"happy",
|
|
534
|
+
"best",
|
|
535
|
+
"awesome",
|
|
536
|
+
"beautiful",
|
|
537
|
+
"perfect",
|
|
538
|
+
"brilliant",
|
|
539
|
+
"superb",
|
|
540
|
+
"outstanding",
|
|
541
|
+
"pleased",
|
|
542
|
+
"delighted",
|
|
543
|
+
"enjoy",
|
|
544
|
+
"like",
|
|
545
|
+
"nice",
|
|
546
|
+
"thanks",
|
|
547
|
+
"thank",
|
|
548
|
+
"helpful",
|
|
549
|
+
"recommend",
|
|
550
|
+
"satisfied"
|
|
551
|
+
]);
|
|
552
|
+
var NEGATIVE_WORDS = /* @__PURE__ */ new Set([
|
|
553
|
+
"bad",
|
|
554
|
+
"terrible",
|
|
555
|
+
"awful",
|
|
556
|
+
"horrible",
|
|
557
|
+
"poor",
|
|
558
|
+
"worst",
|
|
559
|
+
"hate",
|
|
560
|
+
"sad",
|
|
561
|
+
"angry",
|
|
562
|
+
"disappointing",
|
|
563
|
+
"disappointed",
|
|
564
|
+
"ugly",
|
|
565
|
+
"fail",
|
|
566
|
+
"failed",
|
|
567
|
+
"broken",
|
|
568
|
+
"useless",
|
|
569
|
+
"waste",
|
|
570
|
+
"problem",
|
|
571
|
+
"issue",
|
|
572
|
+
"bug",
|
|
573
|
+
"error",
|
|
574
|
+
"wrong",
|
|
575
|
+
"sucks",
|
|
576
|
+
"annoying",
|
|
577
|
+
"frustrated"
|
|
578
|
+
]);
|
|
579
|
+
function mockSentimentAnalysis(text) {
|
|
580
|
+
const words = text.toLowerCase().split(/\W+/);
|
|
581
|
+
const positiveMatches = [];
|
|
582
|
+
const negativeMatches = [];
|
|
583
|
+
for (const word of words) {
|
|
584
|
+
if (POSITIVE_WORDS.has(word)) positiveMatches.push(word);
|
|
585
|
+
if (NEGATIVE_WORDS.has(word)) negativeMatches.push(word);
|
|
586
|
+
}
|
|
587
|
+
const total = words.length || 1;
|
|
588
|
+
const positiveScore = positiveMatches.length / total;
|
|
589
|
+
const negativeScore = negativeMatches.length / total;
|
|
590
|
+
const neutralScore = 1 - positiveScore - negativeScore;
|
|
591
|
+
let sentiment;
|
|
592
|
+
if (positiveScore > negativeScore * 2) {
|
|
593
|
+
sentiment = "positive";
|
|
594
|
+
} else if (negativeScore > positiveScore * 2) {
|
|
595
|
+
sentiment = "negative";
|
|
596
|
+
} else if (positiveScore > 0 && negativeScore > 0) {
|
|
597
|
+
sentiment = "mixed";
|
|
598
|
+
} else {
|
|
599
|
+
sentiment = "neutral";
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
sentiment,
|
|
603
|
+
scores: {
|
|
604
|
+
positive: Math.round(positiveScore * 100) / 100,
|
|
605
|
+
negative: Math.round(negativeScore * 100) / 100,
|
|
606
|
+
neutral: Math.round(neutralScore * 100) / 100
|
|
607
|
+
},
|
|
608
|
+
keywords: [...positiveMatches, ...negativeMatches]
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
async function llmSentimentAnalysis(text, detailed) {
|
|
612
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
613
|
+
if (!apiKey) {
|
|
614
|
+
throw new Error("OPENAI_API_KEY not set");
|
|
615
|
+
}
|
|
616
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
617
|
+
method: "POST",
|
|
618
|
+
headers: {
|
|
619
|
+
"Content-Type": "application/json",
|
|
620
|
+
Authorization: `Bearer ${apiKey}`
|
|
621
|
+
},
|
|
622
|
+
body: JSON.stringify({
|
|
623
|
+
model: "gpt-4o-mini",
|
|
624
|
+
messages: [
|
|
625
|
+
{
|
|
626
|
+
role: "system",
|
|
627
|
+
content: `You are a sentiment analysis expert. Analyze the sentiment of the given text and respond in JSON format with the following structure:
|
|
628
|
+
{
|
|
629
|
+
"sentiment": "positive" | "negative" | "neutral" | "mixed",
|
|
630
|
+
"scores": { "positive": 0.0-1.0, "negative": 0.0-1.0, "neutral": 0.0-1.0 },
|
|
631
|
+
"keywords": ["list", "of", "sentiment", "keywords"],
|
|
632
|
+
"explanation": "Brief explanation of the sentiment"
|
|
633
|
+
}
|
|
634
|
+
Only output valid JSON, nothing else.`
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
role: "user",
|
|
638
|
+
content: text
|
|
639
|
+
}
|
|
640
|
+
],
|
|
641
|
+
temperature: 0.1,
|
|
642
|
+
max_tokens: 512
|
|
643
|
+
})
|
|
644
|
+
});
|
|
645
|
+
if (!response.ok) {
|
|
646
|
+
const error = await response.text();
|
|
647
|
+
throw new Error(`OpenAI API error: ${error}`);
|
|
648
|
+
}
|
|
649
|
+
const data = await response.json();
|
|
650
|
+
const content = data.choices[0].message.content.trim();
|
|
651
|
+
try {
|
|
652
|
+
return JSON.parse(content);
|
|
653
|
+
} catch {
|
|
654
|
+
return {
|
|
655
|
+
sentiment: "neutral",
|
|
656
|
+
scores: { positive: 0.33, negative: 0.33, neutral: 0.34 },
|
|
657
|
+
keywords: [],
|
|
658
|
+
explanation: "Failed to parse LLM response"
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
var sentimentSkillHandler = {
|
|
663
|
+
name: "sentiment",
|
|
664
|
+
version: "1.0.0",
|
|
665
|
+
canHandle(skillName, skillVersion) {
|
|
666
|
+
const name = skillName.toLowerCase();
|
|
667
|
+
return name.includes("sentiment") || name.includes("emotion") || name.includes("mood") || name.includes("opinion");
|
|
668
|
+
},
|
|
669
|
+
async execute(input, context) {
|
|
670
|
+
const startTime = Date.now();
|
|
671
|
+
try {
|
|
672
|
+
const { text, detailed = false } = input;
|
|
673
|
+
if (!text || typeof text !== "string") {
|
|
674
|
+
return {
|
|
675
|
+
success: false,
|
|
676
|
+
error: "Missing or invalid 'text' field"
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
const useLLM = !!process.env.OPENAI_API_KEY;
|
|
680
|
+
let result;
|
|
681
|
+
if (useLLM) {
|
|
682
|
+
result = await llmSentimentAnalysis(text, detailed);
|
|
683
|
+
} else {
|
|
684
|
+
result = mockSentimentAnalysis(text);
|
|
685
|
+
}
|
|
686
|
+
const maxScore = Math.max(
|
|
687
|
+
result.scores.positive,
|
|
688
|
+
result.scores.negative,
|
|
689
|
+
result.scores.neutral
|
|
690
|
+
);
|
|
691
|
+
const confidence = useLLM ? Math.min(0.95, maxScore + 0.3) : maxScore;
|
|
692
|
+
const output = {
|
|
693
|
+
sentiment: result.sentiment,
|
|
694
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
695
|
+
scores: result.scores,
|
|
696
|
+
keywords: detailed ? result.keywords : void 0,
|
|
697
|
+
explanation: detailed ? result.explanation : void 0,
|
|
698
|
+
processing_time_ms: Date.now() - startTime,
|
|
699
|
+
mode: useLLM ? "llm" : "mock"
|
|
700
|
+
};
|
|
701
|
+
return {
|
|
702
|
+
success: true,
|
|
703
|
+
output
|
|
704
|
+
};
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return {
|
|
707
|
+
success: false,
|
|
708
|
+
error: error instanceof Error ? error.message : "Sentiment analysis failed"
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
},
|
|
712
|
+
validateInput(input) {
|
|
713
|
+
const { text } = input;
|
|
714
|
+
if (!text || typeof text !== "string") {
|
|
715
|
+
return { valid: false, error: "Missing or invalid 'text' field" };
|
|
716
|
+
}
|
|
717
|
+
if (text.length < 5) {
|
|
718
|
+
return { valid: false, error: "Text too short (min 5 characters)" };
|
|
719
|
+
}
|
|
720
|
+
if (text.length > 1e4) {
|
|
721
|
+
return { valid: false, error: "Text too long (max 10000 characters)" };
|
|
722
|
+
}
|
|
723
|
+
return { valid: true };
|
|
724
|
+
},
|
|
725
|
+
estimateExecutionTime(input) {
|
|
726
|
+
const { text } = input;
|
|
727
|
+
const charCount = text?.length || 0;
|
|
728
|
+
const useLLM = !!process.env.OPENAI_API_KEY;
|
|
729
|
+
return useLLM ? charCount / 20 + 1e3 : charCount / 100 + 50;
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// src/skills/image-generation.ts
|
|
734
|
+
var SUPPORTED_STYLES = [
|
|
735
|
+
"photorealistic",
|
|
736
|
+
"digital-art",
|
|
737
|
+
"anime",
|
|
738
|
+
"oil-painting",
|
|
739
|
+
"watercolor",
|
|
740
|
+
"sketch"
|
|
741
|
+
];
|
|
742
|
+
function mockGenerateImages(prompt, width, height, numImages, seed) {
|
|
743
|
+
return Array.from({ length: numImages }, (_, i) => ({
|
|
744
|
+
url: `https://picsum.photos/seed/${seed ?? Date.now() + i}/${width}/${height}`,
|
|
745
|
+
width,
|
|
746
|
+
height
|
|
747
|
+
}));
|
|
748
|
+
}
|
|
749
|
+
async function dalleGenerateImages(prompt, numImages, size) {
|
|
750
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
751
|
+
if (!apiKey) {
|
|
752
|
+
throw new Error("OPENAI_API_KEY not set");
|
|
753
|
+
}
|
|
754
|
+
const response = await fetch("https://api.openai.com/v1/images/generations", {
|
|
755
|
+
method: "POST",
|
|
756
|
+
headers: {
|
|
757
|
+
"Content-Type": "application/json",
|
|
758
|
+
Authorization: `Bearer ${apiKey}`
|
|
759
|
+
},
|
|
760
|
+
body: JSON.stringify({
|
|
761
|
+
model: "dall-e-3",
|
|
762
|
+
prompt,
|
|
763
|
+
n: numImages,
|
|
764
|
+
size,
|
|
765
|
+
quality: "standard",
|
|
766
|
+
response_format: "url"
|
|
767
|
+
})
|
|
768
|
+
});
|
|
769
|
+
if (!response.ok) {
|
|
770
|
+
const error = await response.text();
|
|
771
|
+
throw new Error(`DALL-E API error: ${error}`);
|
|
772
|
+
}
|
|
773
|
+
const data = await response.json();
|
|
774
|
+
const [width, height] = size.split("x").map(Number);
|
|
775
|
+
return {
|
|
776
|
+
images: data.data.map((item) => ({
|
|
777
|
+
url: item.url,
|
|
778
|
+
width,
|
|
779
|
+
height
|
|
780
|
+
})),
|
|
781
|
+
revised_prompt: data.data[0]?.revised_prompt
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
var imageGenerationSkillHandler = {
|
|
785
|
+
name: "image-generation",
|
|
786
|
+
version: "1.0.0",
|
|
787
|
+
canHandle(skillName, _skillVersion) {
|
|
788
|
+
const normalized = skillName.toLowerCase().replace(/[-_\s]/g, "");
|
|
789
|
+
return normalized === "imagegeneration" || normalized === "imagegen" || normalized === "texttoimage" || normalized === "dalle" || normalized === "stablediffusion";
|
|
790
|
+
},
|
|
791
|
+
async execute(input, context) {
|
|
792
|
+
const startTime = Date.now();
|
|
793
|
+
try {
|
|
794
|
+
const {
|
|
795
|
+
prompt,
|
|
796
|
+
style,
|
|
797
|
+
width = 1024,
|
|
798
|
+
height = 1024,
|
|
799
|
+
num_images = 1,
|
|
800
|
+
seed
|
|
801
|
+
} = input;
|
|
802
|
+
if (!prompt || typeof prompt !== "string") {
|
|
803
|
+
return { success: false, error: "Missing or invalid 'prompt' field" };
|
|
804
|
+
}
|
|
805
|
+
if (prompt.length > 4e3) {
|
|
806
|
+
return { success: false, error: "Prompt too long (max 4000 characters)" };
|
|
807
|
+
}
|
|
808
|
+
if (width < 256 || width > 2048) {
|
|
809
|
+
return { success: false, error: "Width must be between 256 and 2048" };
|
|
810
|
+
}
|
|
811
|
+
if (height < 256 || height > 2048) {
|
|
812
|
+
return { success: false, error: "Height must be between 256 and 2048" };
|
|
813
|
+
}
|
|
814
|
+
if (num_images < 1 || num_images > 4) {
|
|
815
|
+
return { success: false, error: "num_images must be between 1 and 4" };
|
|
816
|
+
}
|
|
817
|
+
const enhancedPrompt = style ? `${prompt}, ${style} style, high quality, detailed` : `${prompt}, high quality, detailed`;
|
|
818
|
+
const useDalle = !!process.env.OPENAI_API_KEY;
|
|
819
|
+
let images;
|
|
820
|
+
let revisedPrompt;
|
|
821
|
+
if (useDalle) {
|
|
822
|
+
const dalleSize = width > height ? "1792x1024" : height > width ? "1024x1792" : "1024x1024";
|
|
823
|
+
const result = await dalleGenerateImages(enhancedPrompt, num_images, dalleSize);
|
|
824
|
+
images = result.images;
|
|
825
|
+
revisedPrompt = result.revised_prompt;
|
|
826
|
+
} else {
|
|
827
|
+
images = mockGenerateImages(prompt, width, height, num_images, seed);
|
|
828
|
+
revisedPrompt = enhancedPrompt;
|
|
829
|
+
}
|
|
830
|
+
const output = {
|
|
831
|
+
images,
|
|
832
|
+
prompt,
|
|
833
|
+
revised_prompt: revisedPrompt,
|
|
834
|
+
processing_time_ms: Date.now() - startTime,
|
|
835
|
+
mode: useDalle ? "dalle" : "mock",
|
|
836
|
+
model: useDalle ? "dall-e-3" : "mock-diffusion-v1",
|
|
837
|
+
seed: seed ?? Math.floor(Math.random() * 1e6)
|
|
838
|
+
};
|
|
839
|
+
return { success: true, output };
|
|
840
|
+
} catch (error) {
|
|
841
|
+
return {
|
|
842
|
+
success: false,
|
|
843
|
+
error: error instanceof Error ? error.message : "Image generation failed"
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
validateInput(input) {
|
|
848
|
+
const { prompt, width, height, num_images, style } = input;
|
|
849
|
+
if (!prompt || typeof prompt !== "string") {
|
|
850
|
+
return { valid: false, error: "Missing or invalid 'prompt' field" };
|
|
851
|
+
}
|
|
852
|
+
if (prompt.length === 0) {
|
|
853
|
+
return { valid: false, error: "Prompt cannot be empty" };
|
|
854
|
+
}
|
|
855
|
+
if (prompt.length > 4e3) {
|
|
856
|
+
return { valid: false, error: "Prompt too long (max 4000 characters)" };
|
|
857
|
+
}
|
|
858
|
+
if (width !== void 0 && (width < 256 || width > 2048)) {
|
|
859
|
+
return { valid: false, error: "Width must be between 256 and 2048" };
|
|
860
|
+
}
|
|
861
|
+
if (height !== void 0 && (height < 256 || height > 2048)) {
|
|
862
|
+
return { valid: false, error: "Height must be between 256 and 2048" };
|
|
863
|
+
}
|
|
864
|
+
if (num_images !== void 0 && (num_images < 1 || num_images > 4)) {
|
|
865
|
+
return { valid: false, error: "num_images must be between 1 and 4" };
|
|
866
|
+
}
|
|
867
|
+
if (style !== void 0 && !SUPPORTED_STYLES.includes(style)) {
|
|
868
|
+
return {
|
|
869
|
+
valid: false,
|
|
870
|
+
error: `Invalid style. Supported: ${SUPPORTED_STYLES.join(", ")}`
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
return { valid: true };
|
|
874
|
+
},
|
|
875
|
+
estimateExecutionTime(input) {
|
|
876
|
+
const { width = 1024, height = 1024, num_images = 1 } = input;
|
|
877
|
+
const useDalle = !!process.env.OPENAI_API_KEY;
|
|
878
|
+
if (useDalle) {
|
|
879
|
+
return num_images * 15e3;
|
|
880
|
+
}
|
|
881
|
+
const resolutionFactor = width * height / (1024 * 1024);
|
|
882
|
+
return Math.ceil(500 * resolutionFactor * num_images);
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
// src/skills/code-review.ts
|
|
887
|
+
var FOCUS_AREAS = [
|
|
888
|
+
"security",
|
|
889
|
+
"performance",
|
|
890
|
+
"readability",
|
|
891
|
+
"bugs",
|
|
892
|
+
"best-practices",
|
|
893
|
+
"typing"
|
|
894
|
+
];
|
|
895
|
+
var ISSUE_PATTERNS = [
|
|
896
|
+
{ pattern: /console\.log/g, message: "Remove console.log in production", severity: "warning", category: "best-practices" },
|
|
897
|
+
{ pattern: /TODO|FIXME|HACK/g, message: "Unresolved TODO/FIXME comment", severity: "info", category: "maintainability" },
|
|
898
|
+
{ pattern: /==\s/g, message: "Use strict equality (===) instead of loose equality (==)", severity: "warning", category: "best-practices" },
|
|
899
|
+
{ pattern: /var\s+\w+/g, message: "Use const/let instead of var", severity: "suggestion", category: "best-practices" },
|
|
900
|
+
{ pattern: /:\s*any\s*[;,)>]/g, message: "Avoid using 'any' type - be more specific", severity: "warning", category: "typing" },
|
|
901
|
+
{ pattern: /eval\s*\(/g, message: "Avoid using eval() - potential security risk", severity: "critical", category: "security" },
|
|
902
|
+
{ pattern: /innerHTML\s*=/g, message: "innerHTML can lead to XSS - use textContent or sanitize", severity: "critical", category: "security" },
|
|
903
|
+
{ pattern: /password.*=.*["']/gi, message: "Hardcoded password detected", severity: "critical", category: "security" },
|
|
904
|
+
{ pattern: /function\s+\w+\s*\([^)]{80,}\)/g, message: "Function has too many parameters - consider an options object", severity: "suggestion", category: "readability" }
|
|
905
|
+
];
|
|
906
|
+
function detectLanguage(code) {
|
|
907
|
+
if (code.includes("import React") || code.includes("jsx")) return "typescript/react";
|
|
908
|
+
if (code.includes("fn ") && code.includes("->")) return "rust";
|
|
909
|
+
if (code.includes("func ") && code.includes(":=")) return "go";
|
|
910
|
+
if (code.includes("def ") && code.includes(":")) return "python";
|
|
911
|
+
if (code.includes("public class") || code.includes("private void")) return "java";
|
|
912
|
+
if (code.includes("module ") && code.includes("public entry fun")) return "move";
|
|
913
|
+
if (code.includes("function") || code.includes("const ") || code.includes("=>")) return "typescript";
|
|
914
|
+
return "unknown";
|
|
915
|
+
}
|
|
916
|
+
function analyzeCode(code, focusAreas) {
|
|
917
|
+
const issues = [];
|
|
918
|
+
const lines = code.split("\n");
|
|
919
|
+
for (const pattern of ISSUE_PATTERNS) {
|
|
920
|
+
if (focusAreas && focusAreas.length > 0) {
|
|
921
|
+
const categoryMatch = focusAreas.some(
|
|
922
|
+
(area) => pattern.category.toLowerCase().includes(area.toLowerCase())
|
|
923
|
+
);
|
|
924
|
+
if (!categoryMatch) continue;
|
|
925
|
+
}
|
|
926
|
+
const matches = code.matchAll(pattern.pattern);
|
|
927
|
+
for (const match of matches) {
|
|
928
|
+
if (match.index === void 0) continue;
|
|
929
|
+
const beforeMatch = code.slice(0, match.index);
|
|
930
|
+
const lineNumber = beforeMatch.split("\n").length;
|
|
931
|
+
issues.push({
|
|
932
|
+
severity: pattern.severity,
|
|
933
|
+
line: lineNumber,
|
|
934
|
+
message: pattern.message,
|
|
935
|
+
category: pattern.category,
|
|
936
|
+
code_snippet: lines[lineNumber - 1]?.trim().slice(0, 80)
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return issues;
|
|
941
|
+
}
|
|
942
|
+
async function llmCodeReview(code, language, context) {
|
|
943
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
944
|
+
if (!apiKey) {
|
|
945
|
+
throw new Error("OPENAI_API_KEY not set");
|
|
946
|
+
}
|
|
947
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
948
|
+
method: "POST",
|
|
949
|
+
headers: {
|
|
950
|
+
"Content-Type": "application/json",
|
|
951
|
+
Authorization: `Bearer ${apiKey}`
|
|
952
|
+
},
|
|
953
|
+
body: JSON.stringify({
|
|
954
|
+
model: "gpt-4o-mini",
|
|
955
|
+
messages: [
|
|
956
|
+
{
|
|
957
|
+
role: "system",
|
|
958
|
+
content: `You are an expert code reviewer. Analyze the following ${language} code and provide:
|
|
959
|
+
1. A brief summary (1-2 sentences)
|
|
960
|
+
2. A list of issues with severity (critical/warning/info/suggestion), line number if possible, message, and category
|
|
961
|
+
3. General improvement suggestions
|
|
962
|
+
|
|
963
|
+
Respond in JSON format:
|
|
964
|
+
{
|
|
965
|
+
"summary": "string",
|
|
966
|
+
"issues": [{ "severity": "critical|warning|info|suggestion", "line": number|null, "message": "string", "category": "string" }],
|
|
967
|
+
"suggestions": ["string"]
|
|
968
|
+
}`
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
role: "user",
|
|
972
|
+
content: context ? `Context: ${context}
|
|
973
|
+
|
|
974
|
+
Code:
|
|
975
|
+
${code}` : code
|
|
976
|
+
}
|
|
977
|
+
],
|
|
978
|
+
temperature: 0.3,
|
|
979
|
+
max_tokens: 2048,
|
|
980
|
+
response_format: { type: "json_object" }
|
|
981
|
+
})
|
|
982
|
+
});
|
|
983
|
+
if (!response.ok) {
|
|
984
|
+
const error = await response.text();
|
|
985
|
+
throw new Error(`OpenAI API error: ${error}`);
|
|
986
|
+
}
|
|
987
|
+
const data = await response.json();
|
|
988
|
+
return JSON.parse(data.choices[0].message.content);
|
|
989
|
+
}
|
|
990
|
+
var codeReviewSkillHandler = {
|
|
991
|
+
name: "code-review",
|
|
992
|
+
version: "1.0.0",
|
|
993
|
+
canHandle(skillName, _skillVersion) {
|
|
994
|
+
const normalized = skillName.toLowerCase().replace(/[-_\s]/g, "");
|
|
995
|
+
return normalized === "codereview" || normalized === "codeanalysis" || normalized === "linting" || normalized === "staticanalysis";
|
|
996
|
+
},
|
|
997
|
+
async execute(input, context) {
|
|
998
|
+
const startTime = Date.now();
|
|
999
|
+
try {
|
|
1000
|
+
const {
|
|
1001
|
+
code,
|
|
1002
|
+
language,
|
|
1003
|
+
context: codeContext,
|
|
1004
|
+
focus_areas
|
|
1005
|
+
} = input;
|
|
1006
|
+
if (!code || typeof code !== "string") {
|
|
1007
|
+
return { success: false, error: "Missing or invalid 'code' field" };
|
|
1008
|
+
}
|
|
1009
|
+
if (code.length === 0) {
|
|
1010
|
+
return { success: false, error: "Code cannot be empty" };
|
|
1011
|
+
}
|
|
1012
|
+
if (code.length > 1e5) {
|
|
1013
|
+
return { success: false, error: "Code too long (max 100000 characters)" };
|
|
1014
|
+
}
|
|
1015
|
+
const detectedLanguage = language || detectLanguage(code);
|
|
1016
|
+
const useLLM = !!process.env.OPENAI_API_KEY;
|
|
1017
|
+
let issues;
|
|
1018
|
+
let summary;
|
|
1019
|
+
let suggestions = [];
|
|
1020
|
+
if (useLLM) {
|
|
1021
|
+
const llmResult = await llmCodeReview(code, detectedLanguage, codeContext);
|
|
1022
|
+
summary = llmResult.summary;
|
|
1023
|
+
issues = llmResult.issues;
|
|
1024
|
+
suggestions = llmResult.suggestions;
|
|
1025
|
+
} else {
|
|
1026
|
+
issues = analyzeCode(code, focus_areas);
|
|
1027
|
+
if (issues.filter((i) => i.severity === "critical").length > 0) {
|
|
1028
|
+
summary = `Found ${issues.filter((i) => i.severity === "critical").length} critical issue(s) that should be addressed immediately.`;
|
|
1029
|
+
} else if (issues.filter((i) => i.severity === "warning").length > 0) {
|
|
1030
|
+
summary = `Found ${issues.filter((i) => i.severity === "warning").length} warning(s) that need attention.`;
|
|
1031
|
+
} else if (issues.length === 0) {
|
|
1032
|
+
summary = "No significant issues found. Code looks good!";
|
|
1033
|
+
} else {
|
|
1034
|
+
summary = `Found ${issues.length} minor issue(s) and suggestions.`;
|
|
1035
|
+
}
|
|
1036
|
+
if (code.length > 500 && !code.includes("//") && !code.includes("/*")) {
|
|
1037
|
+
suggestions.push("Add comments to explain complex logic");
|
|
1038
|
+
}
|
|
1039
|
+
if (code.split("\n").length > 100) {
|
|
1040
|
+
suggestions.push("Consider splitting large files into smaller modules");
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const issueCount = {
|
|
1044
|
+
critical: issues.filter((i) => i.severity === "critical").length,
|
|
1045
|
+
warning: issues.filter((i) => i.severity === "warning").length,
|
|
1046
|
+
info: issues.filter((i) => i.severity === "info").length,
|
|
1047
|
+
suggestion: issues.filter((i) => i.severity === "suggestion").length
|
|
1048
|
+
};
|
|
1049
|
+
const deductions = issueCount.critical * 20 + issueCount.warning * 5 + issueCount.info * 1 + issueCount.suggestion * 0.5;
|
|
1050
|
+
const overallScore = Math.max(0, Math.min(100, Math.round(100 - deductions)));
|
|
1051
|
+
const linesOfCode = code.split("\n").filter((l) => l.trim().length > 0).length;
|
|
1052
|
+
const complexity = linesOfCode < 50 ? "low" : linesOfCode < 200 ? "medium" : "high";
|
|
1053
|
+
const maintainability = overallScore > 80 ? "excellent" : overallScore > 60 ? "good" : overallScore > 40 ? "fair" : "needs improvement";
|
|
1054
|
+
const output = {
|
|
1055
|
+
summary,
|
|
1056
|
+
overall_score: overallScore,
|
|
1057
|
+
issues,
|
|
1058
|
+
suggestions,
|
|
1059
|
+
metrics: {
|
|
1060
|
+
lines_of_code: linesOfCode,
|
|
1061
|
+
complexity,
|
|
1062
|
+
maintainability
|
|
1063
|
+
},
|
|
1064
|
+
processing_time_ms: Date.now() - startTime,
|
|
1065
|
+
mode: useLLM ? "llm" : "mock",
|
|
1066
|
+
language_detected: detectedLanguage,
|
|
1067
|
+
issue_count: issueCount
|
|
1068
|
+
};
|
|
1069
|
+
return { success: true, output };
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
return {
|
|
1072
|
+
success: false,
|
|
1073
|
+
error: error instanceof Error ? error.message : "Code review failed"
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
validateInput(input) {
|
|
1078
|
+
const { code, focus_areas } = input;
|
|
1079
|
+
if (!code || typeof code !== "string") {
|
|
1080
|
+
return { valid: false, error: "Missing or invalid 'code' field" };
|
|
1081
|
+
}
|
|
1082
|
+
if (code.length === 0) {
|
|
1083
|
+
return { valid: false, error: "Code cannot be empty" };
|
|
1084
|
+
}
|
|
1085
|
+
if (code.length > 1e5) {
|
|
1086
|
+
return { valid: false, error: "Code too long (max 100000 characters)" };
|
|
1087
|
+
}
|
|
1088
|
+
if (focus_areas !== void 0) {
|
|
1089
|
+
if (!Array.isArray(focus_areas)) {
|
|
1090
|
+
return { valid: false, error: "focus_areas must be an array" };
|
|
1091
|
+
}
|
|
1092
|
+
for (const area of focus_areas) {
|
|
1093
|
+
if (!FOCUS_AREAS.includes(area)) {
|
|
1094
|
+
return {
|
|
1095
|
+
valid: false,
|
|
1096
|
+
error: `Invalid focus area: ${area}. Supported: ${FOCUS_AREAS.join(", ")}`
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return { valid: true };
|
|
1102
|
+
},
|
|
1103
|
+
estimateExecutionTime(input) {
|
|
1104
|
+
const { code } = input;
|
|
1105
|
+
const useLLM = !!process.env.OPENAI_API_KEY;
|
|
1106
|
+
if (!code) return 2e3;
|
|
1107
|
+
if (useLLM) {
|
|
1108
|
+
return Math.min(3e4, 2e3 + code.length / 20);
|
|
1109
|
+
}
|
|
1110
|
+
return Math.min(5e3, 500 + code.length / 100);
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
|
|
1114
|
+
// src/skills/index.ts
|
|
1115
|
+
var allSkillHandlers = [
|
|
1116
|
+
echoSkillHandler,
|
|
1117
|
+
translationSkillHandler,
|
|
1118
|
+
summarizationSkillHandler,
|
|
1119
|
+
sentimentSkillHandler,
|
|
1120
|
+
imageGenerationSkillHandler,
|
|
1121
|
+
codeReviewSkillHandler
|
|
1122
|
+
];
|
|
1123
|
+
|
|
224
1124
|
// src/agent.ts
|
|
225
1125
|
var ClankAgent = class {
|
|
226
1126
|
client;
|
|
@@ -229,6 +1129,9 @@ var ClankAgent = class {
|
|
|
229
1129
|
skillRegistry;
|
|
230
1130
|
running = false;
|
|
231
1131
|
heartbeatTimer;
|
|
1132
|
+
// On-chain transaction capabilities
|
|
1133
|
+
keypair;
|
|
1134
|
+
suiClient;
|
|
232
1135
|
// Queue for tasks to create in requester mode
|
|
233
1136
|
taskCreationQueue = [];
|
|
234
1137
|
constructor(config) {
|
|
@@ -245,6 +1148,17 @@ var ClankAgent = class {
|
|
|
245
1148
|
});
|
|
246
1149
|
this.skillRegistry = new SkillRegistry();
|
|
247
1150
|
this.skillRegistry.register(echoSkillHandler);
|
|
1151
|
+
if (config.privateKey) {
|
|
1152
|
+
try {
|
|
1153
|
+
const { secretKey } = decodeSuiPrivateKey(config.privateKey);
|
|
1154
|
+
this.keypair = Ed25519Keypair.fromSecretKey(secretKey);
|
|
1155
|
+
const rpcUrl = config.rpcUrl || (config.network === "mainnet" ? "https://fullnode.mainnet.sui.io:443" : "https://fullnode.testnet.sui.io:443");
|
|
1156
|
+
this.suiClient = new SuiClient({ url: rpcUrl });
|
|
1157
|
+
console.log(` \u{1F511} On-chain signing enabled: ${this.keypair.toSuiAddress().slice(0, 15)}...`);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
console.error(` \u26A0\uFE0F Failed to load private key: ${error}`);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
248
1162
|
}
|
|
249
1163
|
/**
|
|
250
1164
|
* Register a custom skill handler
|
|
@@ -349,13 +1263,21 @@ var ClankAgent = class {
|
|
|
349
1263
|
}
|
|
350
1264
|
console.log(` \u{1F4CB} Polling for tasks...`);
|
|
351
1265
|
try {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
1266
|
+
let allTasks = [];
|
|
1267
|
+
for (const skillId of skillIds) {
|
|
1268
|
+
const result = await this.client.api.listTasks({
|
|
1269
|
+
status: STATUS.POSTED,
|
|
1270
|
+
skillId,
|
|
1271
|
+
limit: 10
|
|
1272
|
+
});
|
|
1273
|
+
allTasks = [...allTasks, ...result.data];
|
|
1274
|
+
}
|
|
1275
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1276
|
+
const tasks = allTasks.filter((t) => {
|
|
1277
|
+
if (seen.has(t.id)) return false;
|
|
1278
|
+
seen.add(t.id);
|
|
1279
|
+
return true;
|
|
357
1280
|
});
|
|
358
|
-
const tasks = result.data;
|
|
359
1281
|
console.log(` \u{1F4CB} Found ${tasks.length} available tasks`);
|
|
360
1282
|
for (const task of tasks) {
|
|
361
1283
|
if (!this.running) break;
|
|
@@ -476,13 +1398,52 @@ var ClankAgent = class {
|
|
|
476
1398
|
console.log(` Handler executed successfully`);
|
|
477
1399
|
const storedOutput = await this.client.walrus.storeJson(result.output);
|
|
478
1400
|
console.log(` Output stored: ${storedOutput.blobId.slice(0, 20)}...`);
|
|
479
|
-
|
|
1401
|
+
if (this.keypair && this.suiClient && this.config.packageId) {
|
|
1402
|
+
try {
|
|
1403
|
+
console.log(` Submitting on-chain...`);
|
|
1404
|
+
await this.submitTaskOnChain(task.taskId, storedOutput.blobId);
|
|
1405
|
+
console.log(` \u2705 Task submitted on-chain: ${task.taskId.slice(0, 16)}`);
|
|
1406
|
+
} catch (error) {
|
|
1407
|
+
console.error(` \u274C On-chain submission failed: ${error}`);
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
console.log(` \u2705 Task completed (no on-chain key): ${task.taskId.slice(0, 16)}`);
|
|
1411
|
+
}
|
|
480
1412
|
markTaskCompleted(this.state, task.taskId, task.paymentAmountMist);
|
|
481
1413
|
} catch (error) {
|
|
482
1414
|
console.error(` \u274C Execution error: ${error}`);
|
|
483
1415
|
markTaskFailed(this.state, task.taskId);
|
|
484
1416
|
}
|
|
485
1417
|
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Submit task output on-chain
|
|
1420
|
+
*/
|
|
1421
|
+
async submitTaskOnChain(taskId, outputRef) {
|
|
1422
|
+
if (!this.keypair || !this.suiClient || !this.config.packageId) {
|
|
1423
|
+
throw new Error("On-chain signing not configured");
|
|
1424
|
+
}
|
|
1425
|
+
const tx = new Transaction();
|
|
1426
|
+
tx.setGasBudget(1e7);
|
|
1427
|
+
tx.moveCall({
|
|
1428
|
+
target: `${this.config.packageId}::task::submit`,
|
|
1429
|
+
arguments: [
|
|
1430
|
+
tx.object(taskId),
|
|
1431
|
+
tx.object(this.config.agentId),
|
|
1432
|
+
tx.pure.string(outputRef),
|
|
1433
|
+
tx.object("0x6")
|
|
1434
|
+
// Clock
|
|
1435
|
+
]
|
|
1436
|
+
});
|
|
1437
|
+
const result = await this.suiClient.signAndExecuteTransaction({
|
|
1438
|
+
transaction: tx,
|
|
1439
|
+
signer: this.keypair,
|
|
1440
|
+
options: { showEffects: true }
|
|
1441
|
+
});
|
|
1442
|
+
if (result.effects?.status?.status !== "success") {
|
|
1443
|
+
throw new Error(`Transaction failed: ${result.effects?.status?.error || "unknown"}`);
|
|
1444
|
+
}
|
|
1445
|
+
console.log(` Tx: ${result.digest.slice(0, 15)}...`);
|
|
1446
|
+
}
|
|
486
1447
|
/**
|
|
487
1448
|
* Refresh skills list from API
|
|
488
1449
|
*/
|
|
@@ -758,17 +1719,17 @@ var ClankAgent = class {
|
|
|
758
1719
|
// src/config.ts
|
|
759
1720
|
import "dotenv/config";
|
|
760
1721
|
function loadConfig() {
|
|
761
|
-
const apiUrl = process.env.
|
|
762
|
-
const apiKey = process.env.
|
|
763
|
-
const agentId = process.env.
|
|
1722
|
+
const apiUrl = process.env.CLANK_API_URL;
|
|
1723
|
+
const apiKey = process.env.CLANK_API_KEY;
|
|
1724
|
+
const agentId = process.env.CLANK_AGENT_ID;
|
|
764
1725
|
if (!apiUrl) {
|
|
765
|
-
throw new Error("
|
|
1726
|
+
throw new Error("CLANK_API_URL is required");
|
|
766
1727
|
}
|
|
767
1728
|
if (!apiKey) {
|
|
768
|
-
throw new Error("
|
|
1729
|
+
throw new Error("CLANK_API_KEY is required");
|
|
769
1730
|
}
|
|
770
1731
|
if (!agentId) {
|
|
771
|
-
throw new Error("
|
|
1732
|
+
throw new Error("CLANK_AGENT_ID is required");
|
|
772
1733
|
}
|
|
773
1734
|
return {
|
|
774
1735
|
// API configuration
|
|
@@ -778,9 +1739,10 @@ function loadConfig() {
|
|
|
778
1739
|
// Agent mode
|
|
779
1740
|
mode: process.env.AGENT_MODE ?? "worker",
|
|
780
1741
|
// Network configuration
|
|
781
|
-
network: process.env.
|
|
1742
|
+
network: process.env.CLANK_NETWORK ?? "testnet",
|
|
782
1743
|
rpcUrl: process.env.SUI_RPC_URL,
|
|
783
|
-
packageId: process.env.
|
|
1744
|
+
packageId: process.env.CLANK_PACKAGE_ID,
|
|
1745
|
+
privateKey: process.env.CLANK_PRIVATE_KEY,
|
|
784
1746
|
// Walrus configuration
|
|
785
1747
|
walrusAggregator: process.env.WALRUS_AGGREGATOR_URL,
|
|
786
1748
|
walrusPublisher: process.env.WALRUS_PUBLISHER_URL,
|
|
@@ -848,6 +1810,9 @@ program.command("start").description("Start the agent").action(async () => {
|
|
|
848
1810
|
const config = loadConfig();
|
|
849
1811
|
validateConfig(config);
|
|
850
1812
|
const agent = new ClankAgent(config);
|
|
1813
|
+
for (const handler of allSkillHandlers) {
|
|
1814
|
+
agent.registerSkillHandler(handler);
|
|
1815
|
+
}
|
|
851
1816
|
const shutdown = async () => {
|
|
852
1817
|
await agent.stop();
|
|
853
1818
|
process.exit(0);
|
|
@@ -939,14 +1904,14 @@ program.command("status").description("Show agent status (requires running agent
|
|
|
939
1904
|
program.command("config").description("Show required environment variables").action(() => {
|
|
940
1905
|
console.log(banner);
|
|
941
1906
|
console.log(chalk.bold("\nRequired Environment Variables:\n"));
|
|
942
|
-
console.log(`
|
|
943
|
-
console.log(`
|
|
944
|
-
console.log(`
|
|
1907
|
+
console.log(` CLANK_API_URL API server URL`);
|
|
1908
|
+
console.log(` CLANK_API_KEY API authentication key`);
|
|
1909
|
+
console.log(` CLANK_AGENT_ID Your agent's on-chain ID`);
|
|
945
1910
|
console.log(chalk.bold("\nAgent Mode:\n"));
|
|
946
1911
|
console.log(` AGENT_MODE Agent mode: worker, requester, or hybrid [default: worker]`);
|
|
947
1912
|
console.log(chalk.bold("\nNetwork Configuration:\n"));
|
|
948
|
-
console.log(`
|
|
949
|
-
console.log(`
|
|
1913
|
+
console.log(` CLANK_NETWORK Network (testnet, mainnet) [default: testnet]`);
|
|
1914
|
+
console.log(` CLANK_PACKAGE_ID Clank contract package ID`);
|
|
950
1915
|
console.log(` SUI_RPC_URL Sui RPC endpoint`);
|
|
951
1916
|
console.log(` WALRUS_AGGREGATOR_URL Walrus aggregator URL`);
|
|
952
1917
|
console.log(` WALRUS_PUBLISHER_URL Walrus publisher URL`);
|
|
@@ -963,23 +1928,23 @@ program.command("config").description("Show required environment variables").act
|
|
|
963
1928
|
console.log(` HEARTBEAT_INTERVAL_MS Heartbeat interval [default: 60000]`);
|
|
964
1929
|
console.log(` STATE_FILE_PATH State persistence file [default: .agent-state.json]`);
|
|
965
1930
|
console.log(chalk.bold("\nExample .env file (Worker Mode):\n"));
|
|
966
|
-
console.log(chalk.gray(`
|
|
967
|
-
|
|
968
|
-
|
|
1931
|
+
console.log(chalk.gray(`CLANK_API_URL=http://localhost:3000
|
|
1932
|
+
CLANK_API_KEY=ck_your_api_key
|
|
1933
|
+
CLANK_AGENT_ID=0x1234...
|
|
969
1934
|
AGENT_MODE=worker
|
|
970
1935
|
SKILL_IDS=echo-skill
|
|
971
1936
|
`));
|
|
972
1937
|
console.log(chalk.bold("Example .env file (Requester Mode):\n"));
|
|
973
|
-
console.log(chalk.gray(`
|
|
974
|
-
|
|
975
|
-
|
|
1938
|
+
console.log(chalk.gray(`CLANK_API_URL=http://localhost:3000
|
|
1939
|
+
CLANK_API_KEY=ck_your_api_key
|
|
1940
|
+
CLANK_AGENT_ID=0x1234...
|
|
976
1941
|
AGENT_MODE=requester
|
|
977
1942
|
AUTO_CONFIRM_DETERMINISTIC=true
|
|
978
1943
|
`));
|
|
979
1944
|
console.log(chalk.bold("Example .env file (Hybrid Mode):\n"));
|
|
980
|
-
console.log(chalk.gray(`
|
|
981
|
-
|
|
982
|
-
|
|
1945
|
+
console.log(chalk.gray(`CLANK_API_URL=http://localhost:3000
|
|
1946
|
+
CLANK_API_KEY=ck_your_api_key
|
|
1947
|
+
CLANK_AGENT_ID=0x1234...
|
|
983
1948
|
AGENT_MODE=hybrid
|
|
984
1949
|
SKILL_IDS=echo-skill
|
|
985
1950
|
MAX_PENDING_TASKS=10
|