@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/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
- 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
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
- console.log(` \u2705 Task completed: ${task.taskId.slice(0, 16)}`);
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.TASKNET_API_URL;
762
- const apiKey = process.env.TASKNET_API_KEY;
763
- const agentId = process.env.TASKNET_AGENT_ID;
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("TASKNET_API_URL is required");
1726
+ throw new Error("CLANK_API_URL is required");
766
1727
  }
767
1728
  if (!apiKey) {
768
- throw new Error("TASKNET_API_KEY is required");
1729
+ throw new Error("CLANK_API_KEY is required");
769
1730
  }
770
1731
  if (!agentId) {
771
- throw new Error("TASKNET_AGENT_ID is required");
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.TASKNET_NETWORK ?? "testnet",
1742
+ network: process.env.CLANK_NETWORK ?? "testnet",
782
1743
  rpcUrl: process.env.SUI_RPC_URL,
783
- packageId: process.env.TASKNET_PACKAGE_ID,
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(` 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`);
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(` TASKNET_NETWORK Network (testnet, mainnet) [default: testnet]`);
949
- console.log(` TASKNET_PACKAGE_ID Clank contract package ID`);
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(`TASKNET_API_URL=http://localhost:3000
967
- TASKNET_API_KEY=ck_your_api_key
968
- TASKNET_AGENT_ID=0x1234...
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(`TASKNET_API_URL=http://localhost:3000
974
- TASKNET_API_KEY=ck_your_api_key
975
- TASKNET_AGENT_ID=0x1234...
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(`TASKNET_API_URL=http://localhost:3000
981
- TASKNET_API_KEY=ck_your_api_key
982
- TASKNET_AGENT_ID=0x1234...
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