repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | /** |
| #2 | * Solana Agent Kit — Complete TUI screen |
| #3 | * |
| #4 | * Views: Commands · Catalog · Characters · Templates · Stats |
| #5 | * Tab cycles views. [b/Esc] exits. |
| #6 | */ |
| #7 | |
| #8 | import chalk from 'chalk'; |
| #9 | import { readFile, readdir } from 'node:fs/promises'; |
| #10 | import { existsSync } from 'node:fs'; |
| #11 | import { dirname, join, resolve } from 'node:path'; |
| #12 | import { fileURLToPath } from 'node:url'; |
| #13 | |
| #14 | const TUI_SRC_DIR = dirname(fileURLToPath(import.meta.url)); |
| #15 | const REPO_ROOT = resolve(TUI_SRC_DIR, '../..'); |
| #16 | const AGENTS_DIR = join(REPO_ROOT, 'agents'); |
| #17 | |
| #18 | // ─── Types ──────────────────────────────────────────────────────────────────── |
| #19 | |
| #20 | interface KitCommand { |
| #21 | name: string; |
| #22 | cmd: string; |
| #23 | description: string; |
| #24 | risk: 'safe' | 'medium' | 'high'; |
| #25 | env?: string[]; |
| #26 | } |
| #27 | |
| #28 | interface KitCategory { |
| #29 | key: string; |
| #30 | icon: string; |
| #31 | label: string; |
| #32 | subtitle: string; |
| #33 | commands: KitCommand[]; |
| #34 | } |
| #35 | |
| #36 | interface CatalogAgent { |
| #37 | identifier: string; |
| #38 | title: string; |
| #39 | description: string; |
| #40 | avatar: string; |
| #41 | tags: string[]; |
| #42 | category: string; |
| #43 | oneShot: boolean; |
| #44 | featured: boolean; |
| #45 | } |
| #46 | |
| #47 | interface CharacterAgent { |
| #48 | file: string; |
| #49 | name: string; |
| #50 | bio: string; |
| #51 | adjectives: string[]; |
| #52 | topics: string[]; |
| #53 | } |
| #54 | |
| #55 | interface TemplateEntry { |
| #56 | templateId: string; |
| #57 | templateName: string; |
| #58 | templateDescription: string; |
| #59 | templateAvatar: string; |
| #60 | variables?: Record<string, unknown>[]; |
| #61 | } |
| #62 | |
| #63 | interface CatalogData { |
| #64 | agents: CatalogAgent[]; |
| #65 | stats: { |
| #66 | totalAgents: number; |
| #67 | totalOneShots: number; |
| #68 | totalFeatured: number; |
| #69 | totalTemplates: number; |
| #70 | byCategory: Record<string, number>; |
| #71 | metaplexEnabledAgents: number; |
| #72 | tradingCapableAgents: number; |
| #73 | }; |
| #74 | hub: { |
| #75 | gallery: string; |
| #76 | mint: string; |
| #77 | registry: string; |
| #78 | api: string; |
| #79 | }; |
| #80 | } |
| #81 | |
| #82 | type ViewMode = 'commands' | 'catalog' | 'characters' | 'templates' | 'stats'; |
| #83 | |
| #84 | // ─── Static command categories ──────────────────────────────────────────────── |
| #85 | |
| #86 | const CATEGORIES: KitCategory[] = [ |
| #87 | { |
| #88 | key: '1', |
| #89 | icon: '🚀', |
| #90 | label: 'Token Operations', |
| #91 | subtitle: 'Launch · SPL · Token-2022 · Metadata · Pump.fun · Raydium', |
| #92 | commands: [ |
| #93 | { |
| #94 | name: 'Pump.fun launch', |
| #95 | cmd: 'npx clawd-agent token launch --platform pumpfun --name "TOKEN" --symbol "TKN" --image logo.png', |
| #96 | description: 'Deploy token on pump.fun bonding curve with metadata', |
| #97 | risk: 'medium', |
| #98 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #99 | }, |
| #100 | { |
| #101 | name: 'Token-2022 create', |
| #102 | cmd: 'npx clawd-agent token create --program token-2022 --decimals 9 --transfer-fee 100', |
| #103 | description: 'Create Token-2022 with transfer-fee extension', |
| #104 | risk: 'safe', |
| #105 | env: ['SOLANA_RPC_URL'], |
| #106 | }, |
| #107 | { |
| #108 | name: 'Metaplex metadata', |
| #109 | cmd: 'npx clawd-agent token metadata --mint <MINT> --name "TOKEN" --symbol "TKN" --uri <URI>', |
| #110 | description: 'Set on-chain MPL metadata for any token mint', |
| #111 | risk: 'safe', |
| #112 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #113 | }, |
| #114 | { |
| #115 | name: 'Raydium CLMM pool', |
| #116 | cmd: 'npx clawd-agent pool create --dex raydium --token <MINT> --sol 5 --fee-tier 500', |
| #117 | description: 'Initialize Raydium CLMM liquidity pool', |
| #118 | risk: 'high', |
| #119 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET', 'HELIUS_API_KEY'], |
| #120 | }, |
| #121 | { |
| #122 | name: 'Pump.fun screener', |
| #123 | cmd: 'npx clawd-agent screen --source pumpfun --min-volume 10000 --min-holders 50 --limit 20', |
| #124 | description: 'Real-time screen of new pump.fun launches', |
| #125 | risk: 'safe', |
| #126 | env: ['HELIUS_API_KEY'], |
| #127 | }, |
| #128 | { |
| #129 | name: 'Mint auth check', |
| #130 | cmd: 'npx clawd-agent token info --mint <MINT> --check-auth --check-freeze', |
| #131 | description: 'Verify mint/freeze authority and rug risk signals', |
| #132 | risk: 'safe', |
| #133 | env: ['SOLANA_RPC_URL'], |
| #134 | }, |
| #135 | ], |
| #136 | }, |
| #137 | { |
| #138 | key: '2', |
| #139 | icon: '📈', |
| #140 | label: 'Trading / Perps', |
| #141 | subtitle: 'Vulcan/Phoenix · Jupiter spot · TWAP · DCA · Copy trading', |
| #142 | commands: [ |
| #143 | { |
| #144 | name: 'Perps status', |
| #145 | cmd: 'clawd-agents-perps status', |
| #146 | description: 'Show live Phoenix perps market status + risk config', |
| #147 | risk: 'safe', |
| #148 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_API_URL'], |
| #149 | }, |
| #150 | { |
| #151 | name: 'Paper long SOL', |
| #152 | cmd: 'clawd-agents-perps paper-long SOL --notional 100', |
| #153 | description: 'Simulated long SOL-PERP for $100 notional (paper mode)', |
| #154 | risk: 'safe', |
| #155 | env: ['PERPS_SIM_ONLY=true', 'LIVE_TRADING=false'], |
| #156 | }, |
| #157 | { |
| #158 | name: 'Paper short SOL', |
| #159 | cmd: 'clawd-agents-perps paper-short SOL --notional 100', |
| #160 | description: 'Simulated short SOL-PERP for $100 notional (paper mode)', |
| #161 | risk: 'safe', |
| #162 | env: ['PERPS_SIM_ONLY=true', 'LIVE_TRADING=false'], |
| #163 | }, |
| #164 | { |
| #165 | name: 'Live long (Vulcan)', |
| #166 | cmd: 'clawd-agents-perps live-long SOL --notional 100 --leverage 2', |
| #167 | description: '⚠ Real on-chain long via Vulcan CLI (requires LIVE_TRADING=true)', |
| #168 | risk: 'high', |
| #169 | env: ['LIVE_TRADING=true', 'OPERATOR_CONFIRMED=true', 'CLAWD_PERPS_WALLET'], |
| #170 | }, |
| #171 | { |
| #172 | name: 'TWAP execution', |
| #173 | cmd: 'clawd-agents-perps twap SOL --notional 500 --duration 2h --slices 12', |
| #174 | description: 'Time-weighted average price execution over 2 hours', |
| #175 | risk: 'high', |
| #176 | env: ['LIVE_TRADING=true', 'CLAWD_PERPS_WALLET'], |
| #177 | }, |
| #178 | { |
| #179 | name: 'Jupiter spot swap', |
| #180 | cmd: 'npx clawd-agent swap --in SOL --out USDC --amount 1 --slippage 0.5 --dex jupiter', |
| #181 | description: 'Best-route spot swap via Jupiter Aggregator', |
| #182 | risk: 'medium', |
| #183 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #184 | }, |
| #185 | { |
| #186 | name: 'Perps frontend', |
| #187 | cmd: 'clawd-agents-perps frontend', |
| #188 | description: 'Render perps dashboard JSON (useful as API payload)', |
| #189 | risk: 'safe', |
| #190 | env: ['SOLANA_RPC_URL'], |
| #191 | }, |
| #192 | { |
| #193 | name: 'Vulcan catalog', |
| #194 | cmd: 'clawd-agents-perps vulcan', |
| #195 | description: 'Show Vulcan CLI command catalog summary', |
| #196 | risk: 'safe', |
| #197 | env: [], |
| #198 | }, |
| #199 | ], |
| #200 | }, |
| #201 | { |
| #202 | key: '3', |
| #203 | icon: '💰', |
| #204 | label: 'DeFi / Yield', |
| #205 | subtitle: 'Kamino · Marginfi · Drift · Meteora DLMM · LP optimizer', |
| #206 | commands: [ |
| #207 | { |
| #208 | name: 'Scan best APY', |
| #209 | cmd: 'npx clawd-agent yield scan --tokens SOL,USDC,USDT --min-tvl 1000000 --limit 10', |
| #210 | description: 'Find highest yielding protocols across Solana DeFi', |
| #211 | risk: 'safe', |
| #212 | env: ['SOLANA_RPC_URL'], |
| #213 | }, |
| #214 | { |
| #215 | name: 'Kamino deposit', |
| #216 | cmd: 'npx clawd-agent kamino deposit --token USDC --amount 1000 --strategy auto', |
| #217 | description: 'Deposit into optimal Kamino vault (auto-selected)', |
| #218 | risk: 'medium', |
| #219 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET', 'HELIUS_API_KEY'], |
| #220 | }, |
| #221 | { |
| #222 | name: 'Marginfi lend', |
| #223 | cmd: 'npx clawd-agent marginfi lend --token SOL --amount 10 --group main', |
| #224 | description: 'Supply SOL to Marginfi lending pool', |
| #225 | risk: 'medium', |
| #226 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #227 | }, |
| #228 | { |
| #229 | name: 'Meteora DLMM LP', |
| #230 | cmd: 'npx clawd-agent lp add --dex meteora --pool SOL-USDC --amount-sol 1 --amount-usdc 100', |
| #231 | description: 'Add liquidity to Meteora DLMM pool with auto-range', |
| #232 | risk: 'high', |
| #233 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #234 | }, |
| #235 | { |
| #236 | name: 'IL calculator', |
| #237 | cmd: 'npx clawd-agent lp calc-il --pool <POOL_ADDR> --entry-price-a 160 --current-price-a 200', |
| #238 | description: 'Calculate impermanent loss for LP position', |
| #239 | risk: 'safe', |
| #240 | env: ['SOLANA_RPC_URL'], |
| #241 | }, |
| #242 | { |
| #243 | name: 'Yield optimizer', |
| #244 | cmd: 'npx clawd-agent yield optimize --capital 10000 --risk low --rebalance 24h --protocols kamino,marginfi', |
| #245 | description: 'Auto-rebalance capital across highest-yield protocols', |
| #246 | risk: 'medium', |
| #247 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET', 'HELIUS_API_KEY'], |
| #248 | }, |
| #249 | ], |
| #250 | }, |
| #251 | { |
| #252 | key: '4', |
| #253 | icon: '🎨', |
| #254 | label: 'NFT / Metaplex', |
| #255 | subtitle: 'MPL Core · Gasless agent mint · Staking · SAS · Collections', |
| #256 | commands: [ |
| #257 | { |
| #258 | name: 'Gasless agent mint', |
| #259 | cmd: `curl -X POST https://x402.wtf/api/mint/agent -H 'Content-Type: application/json' -d '{"agentId":1,"ownerPubkey":"<YOUR_PUBKEY>","network":"mainnet"}'`, |
| #260 | description: 'Mint MPL Core agent NFT — platform pays SOL fees', |
| #261 | risk: 'safe', |
| #262 | env: [], |
| #263 | }, |
| #264 | { |
| #265 | name: 'MPL Core collection', |
| #266 | cmd: 'npx clawd-agent nft collection create --name "Clawd Agents" --symbol "CLAWD" --royalty 500', |
| #267 | description: 'Create Metaplex Core collection with 5% royalty', |
| #268 | risk: 'medium', |
| #269 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #270 | }, |
| #271 | { |
| #272 | name: 'Stake agent NFT', |
| #273 | cmd: 'npx clawd-agent nft stake --mint <MINT> --duration 30d --reward-token CLAWD', |
| #274 | description: 'Stake MPL Core NFT for CLAWD yield rewards', |
| #275 | risk: 'medium', |
| #276 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #277 | }, |
| #278 | { |
| #279 | name: 'SAS attestation', |
| #280 | cmd: 'npx clawd-agent attest --mint <MINT> --schema agent-v1 --issuer x402.wtf', |
| #281 | description: 'Issue Solana Attestation Service cert for agent', |
| #282 | risk: 'medium', |
| #283 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #284 | }, |
| #285 | { |
| #286 | name: 'Browse registry', |
| #287 | cmd: 'curl https://x402.wtf/api/agents/catalog | jq ".agents | length"', |
| #288 | description: 'Count agents in the live on-chain registry', |
| #289 | risk: 'safe', |
| #290 | env: [], |
| #291 | }, |
| #292 | { |
| #293 | name: 'NFT floor snipe', |
| #294 | cmd: 'npx clawd-agent nft snipe --collection <SLUG> --max-price 5 --slippage 2 --dex tensor', |
| #295 | description: 'Auto-buy below floor price on Tensor marketplace', |
| #296 | risk: 'high', |
| #297 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #298 | }, |
| #299 | ], |
| #300 | }, |
| #301 | { |
| #302 | key: '5', |
| #303 | icon: '⚡', |
| #304 | label: 'x402 / Payments', |
| #305 | subtitle: 'USDC HTTP-402 rails · Pay-per-call · Provider catalog · CLAWD', |
| #306 | commands: [ |
| #307 | { |
| #308 | name: 'Check balance', |
| #309 | cmd: 'clawd balance', |
| #310 | description: 'Show USDC + CLAWD balance in the agent wallet', |
| #311 | risk: 'safe', |
| #312 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #313 | }, |
| #314 | { |
| #315 | name: 'Fund wallet', |
| #316 | cmd: 'clawd fund 10', |
| #317 | description: 'Fund agent wallet with 10 USDC via x402 rails', |
| #318 | risk: 'medium', |
| #319 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET'], |
| #320 | }, |
| #321 | { |
| #322 | name: 'Browse pay catalog', |
| #323 | cmd: 'curl https://x402.wtf/api/catalog | jq ".providers[:5]"', |
| #324 | description: 'List top 5 pay-per-call providers', |
| #325 | risk: 'safe', |
| #326 | env: [], |
| #327 | }, |
| #328 | { |
| #329 | name: 'x402 test call', |
| #330 | cmd: `curl -H 'X-Payment: <TOKEN>' https://x402.wtf/api/data/sol-price`, |
| #331 | description: 'Test a live x402 pay-per-call endpoint', |
| #332 | risk: 'safe', |
| #333 | env: [], |
| #334 | }, |
| #335 | { |
| #336 | name: 'Publish provider', |
| #337 | cmd: 'npx clawd-agent x402 publish --endpoint <URL> --price 0.001 --asset USDC --schema openapi', |
| #338 | description: 'Register a new x402 pay-per-call provider', |
| #339 | risk: 'medium', |
| #340 | env: ['CLAWD_PERPS_WALLET'], |
| #341 | }, |
| #342 | { |
| #343 | name: 'Payment history', |
| #344 | cmd: 'clawd payments --last 20 --format table', |
| #345 | description: 'Show recent x402 payment records + totals', |
| #346 | risk: 'safe', |
| #347 | env: ['SOLANA_RPC_URL'], |
| #348 | }, |
| #349 | ], |
| #350 | }, |
| #351 | { |
| #352 | key: '6', |
| #353 | icon: '🤖', |
| #354 | label: 'Automaton / Runtime', |
| #355 | subtitle: 'Leviathan · OODA loop · Quickstart · Three Laws · Backroom', |
| #356 | commands: [ |
| #357 | { |
| #358 | name: 'Quickstart wizard', |
| #359 | cmd: 'bash automaton-main/quickstart.sh', |
| #360 | description: 'Interactive OpenClawd quickstart — installs & configures stack', |
| #361 | risk: 'safe', |
| #362 | env: [], |
| #363 | }, |
| #364 | { |
| #365 | name: 'Leviathan boot', |
| #366 | cmd: 'bash automaton-main/leviathan.sh --full', |
| #367 | description: 'Full sovereign runtime bootstrap with Vulcan perps', |
| #368 | risk: 'safe', |
| #369 | env: [], |
| #370 | }, |
| #371 | { |
| #372 | name: 'Three Laws check', |
| #373 | cmd: 'bash automaton-main/three-laws-check.sh', |
| #374 | description: 'Verify constitution SHA-256 hash integrity', |
| #375 | risk: 'safe', |
| #376 | env: [], |
| #377 | }, |
| #378 | { |
| #379 | name: 'Global install', |
| #380 | cmd: 'curl -fsSL https://solanaclawd.com/leviathan.sh | sh', |
| #381 | description: 'One-line global leviathan runtime installer', |
| #382 | risk: 'medium', |
| #383 | env: [], |
| #384 | }, |
| #385 | { |
| #386 | name: 'Spawn automaton', |
| #387 | cmd: 'clawd spawn --ooda-interval 60 --mode paper --pulse-log', |
| #388 | description: 'Launch OODA pulse loop agent in paper mode', |
| #389 | risk: 'medium', |
| #390 | env: ['SOLANA_RPC_URL', 'HELIUS_API_KEY'], |
| #391 | }, |
| #392 | { |
| #393 | name: 'Backroom stream', |
| #394 | cmd: 'clawd backroom --stream --agents 2 --debate-topic "Solana DeFi alpha"', |
| #395 | description: 'Start infinite AI backroom debate stream', |
| #396 | risk: 'safe', |
| #397 | env: [], |
| #398 | }, |
| #399 | ], |
| #400 | }, |
| #401 | { |
| #402 | key: '7', |
| #403 | icon: '🧠', |
| #404 | label: 'Skills / Catalog', |
| #405 | subtitle: 'UltraThink · ClawdHub · Agent skills · On-chain routing · Catalog sync', |
| #406 | commands: [ |
| #407 | { |
| #408 | name: 'UltraThink reference', |
| #409 | cmd: 'clawd skill run ultrathink-blockchain', |
| #410 | description: 'Open the UltraThink Blockchain formula reference panel', |
| #411 | risk: 'safe', |
| #412 | env: [], |
| #413 | }, |
| #414 | { |
| #415 | name: 'Install UltraThink', |
| #416 | cmd: 'bash UltraThink-SKill/install.sh', |
| #417 | description: 'Install UltraThink Blockchain skill from local repo', |
| #418 | risk: 'safe', |
| #419 | env: [], |
| #420 | }, |
| #421 | { |
| #422 | name: 'UltraThink via ClawdHub', |
| #423 | cmd: 'clawdhub install ultrathink-blockchain', |
| #424 | description: 'Install UltraThink from ClawdHub registry', |
| #425 | risk: 'safe', |
| #426 | env: [], |
| #427 | }, |
| #428 | { |
| #429 | name: 'List Solana skills', |
| #430 | cmd: 'clawdhub list --category solana --format table', |
| #431 | description: 'Browse all installed Solana skills via ClawdHub', |
| #432 | risk: 'safe', |
| #433 | env: [], |
| #434 | }, |
| #435 | { |
| #436 | name: 'Meme trader skill', |
| #437 | cmd: 'clawd skill run meme-trader', |
| #438 | description: 'Run memecoin trading skill (pump.fun + Raydium)', |
| #439 | risk: 'high', |
| #440 | env: ['SOLANA_RPC_URL', 'CLAWD_PERPS_WALLET', 'HELIUS_API_KEY'], |
| #441 | }, |
| #442 | { |
| #443 | name: 'Token launcher skill', |
| #444 | cmd: 'clawd skill run meme-launcher', |
| #445 | description: 'Launch planning: tokenomics, bonding curve, community', |
| #446 | risk: 'medium', |
| #447 | env: [], |
| #448 | }, |
| #449 | { |
| #450 | name: 'Sync agent catalog', |
| #451 | cmd: 'cd agents && bun run build && echo "Catalog rebuilt."', |
| #452 | description: 'Rebuild the agent catalog from agents/src/*.json', |
| #453 | risk: 'safe', |
| #454 | env: [], |
| #455 | }, |
| #456 | ], |
| #457 | }, |
| #458 | ]; |
| #459 | |
| #460 | // ─── Layout helpers ─────────────────────────────────────────────────────────── |
| #461 | |
| #462 | const ANSI_RE = /\u001b\[[0-9;]*m/g; |
| #463 | |
| #464 | function visible(s: string): number { |
| #465 | return s.replace(ANSI_RE, '').length; |
| #466 | } |
| #467 | |
| #468 | function pad(s: string, width: number): string { |
| #469 | return `${s}${' '.repeat(Math.max(0, width - visible(s)))}`; |
| #470 | } |
| #471 | |
| #472 | function clip(s: string, width: number): string { |
| #473 | const raw = s.replace(ANSI_RE, ''); |
| #474 | if (raw.length <= width) return s; |
| #475 | return `${raw.slice(0, Math.max(0, width - 1))}…`; |
| #476 | } |
| #477 | |
| #478 | function riskBadge(r: KitCommand['risk']): string { |
| #479 | if (r === 'high') return chalk.red('[HIGH]'); |
| #480 | if (r === 'medium') return chalk.yellow('[MED] '); |
| #481 | return chalk.green('[SAFE]'); |
| #482 | } |
| #483 | |
| #484 | function cols(): number { |
| #485 | return Math.max(100, Math.min(process.stdout.columns || 130, 148)); |
| #486 | } |
| #487 | |
| #488 | function border(c: number): string { |
| #489 | return chalk.cyan('═'.repeat(c - 2)); |
| #490 | } |
| #491 | |
| #492 | function hdr(txt: string, c: number): string { |
| #493 | return `${chalk.cyan('║')}${pad(` ${txt}`, c - 2)}${chalk.cyan('║\n')}`; |
| #494 | } |
| #495 | |
| #496 | function row(left: string, lw: number, right: string, rw: number): string { |
| #497 | return `${chalk.cyan('║')} ${pad(clip(left, lw - 2), lw - 2)} ${chalk.cyan('│')} ${pad(clip(right, rw - 1), rw - 1)}${chalk.cyan('║\n')}`; |
| #498 | } |
| #499 | |
| #500 | function renderHeader(view: ViewMode, c: number): void { |
| #501 | const views: ViewMode[] = ['commands', 'catalog', 'characters', 'templates', 'stats']; |
| #502 | const tabLine = views.map(v => |
| #503 | v === view |
| #504 | ? chalk.bold.cyanBright(`[${v.toUpperCase()}]`) |
| #505 | : chalk.gray(`[${v}]`), |
| #506 | ).join(chalk.gray(' ')); |
| #507 | |
| #508 | process.stdout.write(`${chalk.cyan('╔')}${border(c)}${chalk.cyan('╗\n')}`); |
| #509 | process.stdout.write(hdr( |
| #510 | chalk.bold.cyanBright('🦞 SOLANA AGENT KIT') + |
| #511 | chalk.gray(' · token · perps · DeFi · NFT · x402 · automaton · 135 agents'), |
| #512 | c, |
| #513 | )); |
| #514 | process.stdout.write(hdr( |
| #515 | chalk.yellow('$CLAWD') + |
| #516 | chalk.gray(' CA: 8cHzQHUS2s2h8TzCmfqPKYiM4dSt4roa3n7MyRLApump · ') + |
| #517 | chalk.cyan('x402.wtf') + |
| #518 | chalk.gray(' · ') + |
| #519 | chalk.cyan('solanaclawd.com'), |
| #520 | c, |
| #521 | )); |
| #522 | process.stdout.write(hdr(`${chalk.gray('Tab: ')}${tabLine}`, c)); |
| #523 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #524 | } |
| #525 | |
| #526 | // ─── Commands view ──────────────────────────────────────────────────────────── |
| #527 | |
| #528 | function catRowStr(i: number, catIdx: number): string { |
| #529 | const c2 = CATEGORIES[i]; |
| #530 | if (!c2) return ''; |
| #531 | return i === catIdx |
| #532 | ? `${chalk.cyanBright('▶ ')}${chalk.bold.cyanBright(`${c2.key}. ${c2.icon} ${c2.label}`)}` |
| #533 | : `${chalk.gray(` ${c2.key}. `)}${chalk.white(`${c2.icon} ${c2.label}`)}`; |
| #534 | } |
| #535 | |
| #536 | function cmdRowStr(cmdItem: KitCommand | undefined, i: number, cmdIdx: number): string { |
| #537 | if (!cmdItem) return ''; |
| #538 | return i === cmdIdx |
| #539 | ? `${chalk.cyanBright('► ')}${chalk.bold.white(cmdItem.name)} ${riskBadge(cmdItem.risk)}` |
| #540 | : `${chalk.gray(' ')}${chalk.white(cmdItem.name)} ${riskBadge(cmdItem.risk)}`; |
| #541 | } |
| #542 | |
| #543 | function renderCommands(catIdx: number, cmdIdx: number, status: string, copied: boolean): void { |
| #544 | process.stdout.write('\x1b[2J\x1b[H'); |
| #545 | const c = cols(); |
| #546 | renderHeader('commands', c); |
| #547 | |
| #548 | const leftW = 28; |
| #549 | const rightW = c - leftW - 5; |
| #550 | const cat = CATEGORIES[catIdx]!; |
| #551 | const cmd = cat.commands[cmdIdx] ?? cat.commands[0]!; |
| #552 | |
| #553 | process.stdout.write(row(chalk.bold.white('CATEGORY'), leftW, chalk.bold.white(`COMMANDS — ${cat.icon} ${cat.label}`), rightW)); |
| #554 | process.stdout.write(row(chalk.gray('─'.repeat(leftW - 2)), leftW, chalk.gray(cat.subtitle), rightW)); |
| #555 | |
| #556 | const rowCount = Math.max(CATEGORIES.length, cat.commands.length); |
| #557 | for (let i = 0; i < rowCount; i++) { |
| #558 | process.stdout.write(row(catRowStr(i, catIdx), leftW, cmdRowStr(cat.commands[i], i, cmdIdx), rightW)); |
| #559 | } |
| #560 | |
| #561 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #562 | process.stdout.write(hdr(`${chalk.bold.white(` ${cmd.name}`)} ${riskBadge(cmd.risk)}`, c)); |
| #563 | process.stdout.write(hdr(chalk.gray(` ${cmd.description}`), c)); |
| #564 | |
| #565 | const maxCmdW = c - 6; |
| #566 | for (let i = 0; i < cmd.cmd.length; i += maxCmdW) { |
| #567 | process.stdout.write(hdr(` ${chalk.green(cmd.cmd.slice(i, i + maxCmdW))}`, c)); |
| #568 | } |
| #569 | if (cmd.env && cmd.env.length > 0) { |
| #570 | process.stdout.write(hdr(`${chalk.gray(' env: ')}${chalk.yellow(cmd.env.join(' '))}`, c)); |
| #571 | } |
| #572 | |
| #573 | const statusLine = copied ? chalk.green(' ✓ Command copied to clipboard!') : chalk.gray(` ${status}`); |
| #574 | process.stdout.write(hdr(statusLine, c)); |
| #575 | |
| #576 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #577 | process.stdout.write(hdr(chalk.gray('[Tab] next view [←→] category [↑↓] command [c] copy [e] env [1-7] jump [b] back'), c)); |
| #578 | process.stdout.write(`${chalk.cyan('╚')}${border(c)}${chalk.cyan('╝\n')}`); |
| #579 | } |
| #580 | |
| #581 | // ─── Catalog view ───────────────────────────────────────────────────────────── |
| #582 | |
| #583 | const CATALOG_CATEGORIES = [ |
| #584 | 'all', 'defi', 'payments', 'analytics', 'trading', 'security', |
| #585 | 'education', 'dev-tools', 'governance', 'nft', 'research', 'infrastructure', |
| #586 | ]; |
| #587 | |
| #588 | function renderCatalog( |
| #589 | catalog: CatalogData | null, |
| #590 | catFilter: number, |
| #591 | agentIdx: number, |
| #592 | status: string, |
| #593 | ): void { |
| #594 | process.stdout.write('\x1b[2J\x1b[H'); |
| #595 | const c = cols(); |
| #596 | renderHeader('catalog', c); |
| #597 | |
| #598 | if (!catalog) { |
| #599 | process.stdout.write(hdr(chalk.yellow(' Loading agent catalog…'), c)); |
| #600 | process.stdout.write(`${chalk.cyan('╚')}${border(c)}${chalk.cyan('╝\n')}`); |
| #601 | return; |
| #602 | } |
| #603 | |
| #604 | const filterKey = CATALOG_CATEGORIES[catFilter] ?? 'all'; |
| #605 | const agents = filterKey === 'all' |
| #606 | ? catalog.agents |
| #607 | : catalog.agents.filter(a => a.category === filterKey); |
| #608 | |
| #609 | const leftW = 22; |
| #610 | const rightW = c - leftW - 5; |
| #611 | |
| #612 | // Category filter list on left, agents on right |
| #613 | const visibleRows = Math.min(agents.length, 16); |
| #614 | const agentStart = Math.max(0, agentIdx - Math.floor(visibleRows / 2)); |
| #615 | const visibleAgents = agents.slice(agentStart, agentStart + visibleRows); |
| #616 | |
| #617 | // Header row |
| #618 | process.stdout.write(row( |
| #619 | chalk.bold.white('CATEGORY FILTER'), |
| #620 | leftW, |
| #621 | chalk.bold.white(`AGENTS (${agents.length}/${catalog.stats.totalAgents})`), |
| #622 | rightW, |
| #623 | )); |
| #624 | process.stdout.write(row( |
| #625 | chalk.gray('[f] to cycle filter'), |
| #626 | leftW, |
| #627 | chalk.gray(`filter: ${filterKey} · [↑↓] select [c] copy mint cmd`), |
| #628 | rightW, |
| #629 | )); |
| #630 | |
| #631 | const rowCount = Math.max(CATALOG_CATEGORIES.length, visibleRows); |
| #632 | for (let i = 0; i < rowCount; i++) { |
| #633 | const catKey = CATALOG_CATEGORIES[i]; |
| #634 | const count = catKey === 'all' |
| #635 | ? catalog.stats.totalAgents |
| #636 | : (catalog.stats.byCategory[catKey] ?? 0); |
| #637 | const leftStr = catKey !== undefined |
| #638 | ? (i === catFilter |
| #639 | ? chalk.cyanBright('▶ ') + chalk.bold.cyanBright(`${catKey}`) + chalk.gray(` (${count})`) |
| #640 | : chalk.gray(` ${catKey} (${count})`)) |
| #641 | : ''; |
| #642 | |
| #643 | const agent = visibleAgents[i]; |
| #644 | const globalIdx = agentStart + i; |
| #645 | const rightStr = agent |
| #646 | ? (globalIdx === agentIdx |
| #647 | ? chalk.cyanBright('► ') + |
| #648 | chalk.bold.white(`${agent.avatar} ${agent.title}`) + |
| #649 | (agent.featured ? chalk.yellow(' ★') : '') + |
| #650 | (agent.oneShot ? chalk.gray(' ·shot') : '') |
| #651 | : chalk.gray(' ') + |
| #652 | chalk.white(`${agent.avatar} ${agent.title}`) + |
| #653 | (agent.featured ? chalk.yellow(' ★') : '') + |
| #654 | (agent.oneShot ? chalk.gray(' ·shot') : '')) |
| #655 | : ''; |
| #656 | |
| #657 | process.stdout.write(row(leftStr, leftW, rightStr, rightW)); |
| #658 | } |
| #659 | |
| #660 | // Selected agent detail |
| #661 | const selected = agents[agentIdx]; |
| #662 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #663 | if (selected) { |
| #664 | process.stdout.write(hdr( |
| #665 | chalk.bold.white(` ${selected.avatar} ${selected.title}`) + |
| #666 | chalk.gray(` [${selected.category}]`) + |
| #667 | (selected.featured ? chalk.yellow(' ★ FEATURED') : '') + |
| #668 | (selected.oneShot ? chalk.gray(' one-shot') : ''), |
| #669 | c, |
| #670 | )); |
| #671 | process.stdout.write(hdr(chalk.gray(` ${selected.description}`), c)); |
| #672 | const tagLine = selected.tags.slice(0, 8).join(' '); |
| #673 | process.stdout.write(hdr(chalk.gray(' tags: ') + chalk.cyan(tagLine), c)); |
| #674 | const mintCmd = `curl -sX POST https://x402.wtf/api/mint/agent -H 'Content-Type: application/json' -d '{"identifier":"${selected.identifier}"}'`; |
| #675 | process.stdout.write(hdr(chalk.gray(' mint: ') + chalk.green(mintCmd.slice(0, c - 12)), c)); |
| #676 | } else { |
| #677 | process.stdout.write(hdr(chalk.gray(' Select an agent to view details'), c)); |
| #678 | } |
| #679 | process.stdout.write(hdr(chalk.gray(` ${status}`), c)); |
| #680 | |
| #681 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #682 | process.stdout.write(hdr( |
| #683 | chalk.gray('[Tab] next view [f] filter category [↑↓] agent [c] copy mint cmd [b] back'), |
| #684 | c, |
| #685 | )); |
| #686 | process.stdout.write(`${chalk.cyan('╚')}${border(c)}${chalk.cyan('╝\n')}`); |
| #687 | } |
| #688 | |
| #689 | // ─── Characters view ────────────────────────────────────────────────────────── |
| #690 | |
| #691 | function renderCharacters(characters: CharacterAgent[], charIdx: number, status: string): void { |
| #692 | process.stdout.write('\x1b[2J\x1b[H'); |
| #693 | const c = cols(); |
| #694 | renderHeader('characters', c); |
| #695 | |
| #696 | if (characters.length === 0) { |
| #697 | process.stdout.write(hdr(chalk.yellow(' Loading characters…'), c)); |
| #698 | process.stdout.write(`${chalk.cyan('╚')}${border(c)}${chalk.cyan('╝\n')}`); |
| #699 | return; |
| #700 | } |
| #701 | |
| #702 | const leftW = 26; |
| #703 | const rightW = c - leftW - 5; |
| #704 | const char = characters[charIdx]; |
| #705 | |
| #706 | process.stdout.write(row( |
| #707 | chalk.bold.white(`PERSONAS (${characters.length})`), |
| #708 | leftW, |
| #709 | chalk.bold.white(`PROFILE — ${char?.name ?? ''}`), |
| #710 | rightW, |
| #711 | )); |
| #712 | process.stdout.write(row( |
| #713 | chalk.gray('[↑↓] to browse'), |
| #714 | leftW, |
| #715 | chalk.gray('Eliza-compatible character JSON · deploy with x402.wtf'), |
| #716 | rightW, |
| #717 | )); |
| #718 | |
| #719 | const rowCount = Math.max(characters.length, 8); |
| #720 | const adjectives = char?.adjectives.slice(0, 4).join(' · ') ?? ''; |
| #721 | const topics = char?.topics.slice(0, 4) ?? []; |
| #722 | |
| #723 | for (let i = 0; i < rowCount; i++) { |
| #724 | const ch = characters[i]; |
| #725 | const leftStr = ch |
| #726 | ? (i === charIdx |
| #727 | ? chalk.cyanBright('▶ ') + chalk.bold.cyanBright(ch.name) |
| #728 | : chalk.gray(' ') + chalk.white(ch.name)) |
| #729 | : ''; |
| #730 | const rightStr = i === 0 |
| #731 | ? chalk.gray(adjectives) |
| #732 | : i < topics.length + 1 |
| #733 | ? chalk.gray(` 📌 ${topics[i - 1] ?? ''}`) |
| #734 | : ''; |
| #735 | process.stdout.write(row(leftStr, leftW, rightStr, rightW)); |
| #736 | } |
| #737 | |
| #738 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #739 | if (char) { |
| #740 | process.stdout.write(hdr(chalk.bold.white(` ${char.name}`) + chalk.gray(' · Eliza character'), c)); |
| #741 | process.stdout.write(hdr(chalk.gray(` ${char.bio}`), c)); |
| #742 | const deployCmd = `curl -sX POST https://x402.wtf/api/agents/deploy -d '{"character":"${char.file}"}'`; |
| #743 | process.stdout.write(hdr(chalk.gray(' deploy: ') + chalk.green(deployCmd.slice(0, c - 14)), c)); |
| #744 | } |
| #745 | process.stdout.write(hdr(chalk.gray(` ${status}`), c)); |
| #746 | |
| #747 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #748 | process.stdout.write(hdr( |
| #749 | chalk.gray('[Tab] next view [↑↓] character [c] copy deploy cmd [b] back'), |
| #750 | c, |
| #751 | )); |
| #752 | process.stdout.write(`${chalk.cyan('╚')}${border(c)}${chalk.cyan('╝\n')}`); |
| #753 | } |
| #754 | |
| #755 | // ─── Templates view ─────────────────────────────────────────────────────────── |
| #756 | |
| #757 | function renderTemplates(templates: TemplateEntry[], tmplIdx: number, status: string): void { |
| #758 | process.stdout.write('\x1b[2J\x1b[H'); |
| #759 | const c = cols(); |
| #760 | renderHeader('templates', c); |
| #761 | |
| #762 | if (templates.length === 0) { |
| #763 | process.stdout.write(hdr(chalk.yellow(' Loading templates…'), c)); |
| #764 | process.stdout.write(`${chalk.cyan('╚')}${border(c)}${chalk.cyan('╝\n')}`); |
| #765 | return; |
| #766 | } |
| #767 | |
| #768 | const leftW = 30; |
| #769 | const rightW = c - leftW - 5; |
| #770 | const tmpl = templates[tmplIdx]; |
| #771 | |
| #772 | process.stdout.write(row( |
| #773 | chalk.bold.white(`BLUEPRINTS (${templates.length})`), |
| #774 | leftW, |
| #775 | chalk.bold.white(`DETAIL — ${tmpl?.templateAvatar ?? ''} ${tmpl?.templateName ?? ''}`), |
| #776 | rightW, |
| #777 | )); |
| #778 | process.stdout.write(row( |
| #779 | chalk.gray('[↑↓] to browse'), |
| #780 | leftW, |
| #781 | chalk.gray('Ready-to-deploy agent blueprints for OpenClawd'), |
| #782 | rightW, |
| #783 | )); |
| #784 | |
| #785 | const rowCount = Math.max(templates.length, 8); |
| #786 | for (let i = 0; i < rowCount; i++) { |
| #787 | const t = templates[i]; |
| #788 | const leftStr = t |
| #789 | ? (i === tmplIdx |
| #790 | ? chalk.cyanBright('▶ ') + chalk.bold.cyanBright(`${t.templateAvatar} ${t.templateName}`) |
| #791 | : chalk.gray(' ') + chalk.white(`${t.templateAvatar} ${t.templateName}`)) |
| #792 | : ''; |
| #793 | const rightStr = t && i === tmplIdx ? chalk.gray(t.templateDescription.slice(0, rightW - 4)) : ''; |
| #794 | process.stdout.write(row(leftStr, leftW, rightStr, rightW)); |
| #795 | } |
| #796 | |
| #797 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #798 | if (tmpl) { |
| #799 | process.stdout.write(hdr(chalk.bold.white(` ${tmpl.templateAvatar} ${tmpl.templateName}`), c)); |
| #800 | process.stdout.write(hdr(chalk.gray(` ${tmpl.templateDescription}`), c)); |
| #801 | const useCmd = `npx clawd-agent template use ${tmpl.templateId} --name "MyAgent"`; |
| #802 | process.stdout.write(hdr(chalk.gray(' use: ') + chalk.green(useCmd), c)); |
| #803 | const mintCmd = `curl -sX POST https://x402.wtf/api/mint/template -d '{"templateId":"${tmpl.templateId}"}'`; |
| #804 | process.stdout.write(hdr(chalk.gray(' mint: ') + chalk.green(mintCmd.slice(0, c - 12)), c)); |
| #805 | } |
| #806 | process.stdout.write(hdr(chalk.gray(` ${status}`), c)); |
| #807 | |
| #808 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #809 | process.stdout.write(hdr( |
| #810 | chalk.gray('[Tab] next view [↑↓] template [c] copy use cmd [b] back'), |
| #811 | c, |
| #812 | )); |
| #813 | process.stdout.write(`${chalk.cyan('╚')}${border(c)}${chalk.cyan('╝\n')}`); |
| #814 | } |
| #815 | |
| #816 | // ─── Stats view ─────────────────────────────────────────────────────────────── |
| #817 | |
| #818 | function renderStats(catalog: CatalogData | null): void { |
| #819 | process.stdout.write('\x1b[2J\x1b[H'); |
| #820 | const c = cols(); |
| #821 | renderHeader('stats', c); |
| #822 | |
| #823 | if (!catalog) { |
| #824 | process.stdout.write(hdr(chalk.yellow(' Loading catalog stats…'), c)); |
| #825 | process.stdout.write(`${chalk.cyan('╚')}${border(c)}${chalk.cyan('╝\n')}`); |
| #826 | return; |
| #827 | } |
| #828 | |
| #829 | const s = catalog.stats; |
| #830 | const leftW = Math.floor((c - 5) / 2); |
| #831 | const rightW = c - leftW - 5; |
| #832 | |
| #833 | process.stdout.write(row(chalk.bold.white('HUB ENDPOINTS'), leftW, chalk.bold.white('AGENT STATS'), rightW)); |
| #834 | process.stdout.write(row(chalk.gray('─'.repeat(leftW - 2)), leftW, chalk.gray('─'.repeat(rightW - 2)), rightW)); |
| #835 | |
| #836 | const endpoints = [ |
| #837 | ['Gallery', catalog.hub.gallery], |
| #838 | ['Mint', catalog.hub.mint], |
| #839 | ['Registry',catalog.hub.registry], |
| #840 | ['API', catalog.hub.api], |
| #841 | ]; |
| #842 | const statRows: Array<[string, string]> = [ |
| #843 | ['Total agents', String(s.totalAgents)], |
| #844 | ['One-shots', String(s.totalOneShots)], |
| #845 | ['Featured', String(s.totalFeatured)], |
| #846 | ['Templates', String(s.totalTemplates)], |
| #847 | ['Metaplex-enabled', String(s.metaplexEnabledAgents)], |
| #848 | ['Trading-capable', String(s.tradingCapableAgents)], |
| #849 | ]; |
| #850 | |
| #851 | const rowCount = Math.max(endpoints.length, statRows.length); |
| #852 | for (let i = 0; i < rowCount; i++) { |
| #853 | const ep = endpoints[i]; |
| #854 | const sr = statRows[i]; |
| #855 | const leftStr = ep ? `${chalk.gray(` ${ep[0]}: `)}${chalk.cyan(ep[1]!)}` : ''; |
| #856 | const rightStr = sr ? `${chalk.gray(` ${sr[0]}: `)}${chalk.bold.white(sr[1]!)}` : ''; |
| #857 | process.stdout.write(row(leftStr, leftW, rightStr, rightW)); |
| #858 | } |
| #859 | |
| #860 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #861 | process.stdout.write(hdr(chalk.bold.white(' BY CATEGORY'), c)); |
| #862 | |
| #863 | const catEntries = Object.entries(s.byCategory).sort((a, b) => b[1] - a[1]); |
| #864 | const half = Math.ceil(catEntries.length / 2); |
| #865 | for (let i = 0; i < half; i++) { |
| #866 | const l = catEntries[i]; |
| #867 | const r = catEntries[i + half]; |
| #868 | const leftStr = l ? `${chalk.gray(` ${l[0]}: `)}${chalk.bold.white(String(l[1]))}` : ''; |
| #869 | const rightStr = r ? `${chalk.gray(` ${r[0]}: `)}${chalk.bold.white(String(r[1]))}` : ''; |
| #870 | process.stdout.write(row(leftStr, leftW, rightStr, rightW)); |
| #871 | } |
| #872 | |
| #873 | process.stdout.write(`${chalk.cyan('╠')}${border(c)}${chalk.cyan('╣\n')}`); |
| #874 | process.stdout.write(hdr( |
| #875 | chalk.gray('[Tab] next view [b] back'), |
| #876 | c, |
| #877 | )); |
| #878 | process.stdout.write(`${chalk.cyan('╚')}${border(c)}${chalk.cyan('╝\n')}`); |
| #879 | } |
| #880 | |
| #881 | // ─── Data loading ───────────────────────────────────────────────────────────── |
| #882 | |
| #883 | async function loadCatalog(): Promise<CatalogData | null> { |
| #884 | const p = join(AGENTS_DIR, 'agents-catalog.json'); |
| #885 | if (!existsSync(p)) return null; |
| #886 | try { |
| #887 | const raw = JSON.parse(await readFile(p, 'utf8')) as CatalogData; |
| #888 | return raw; |
| #889 | } catch { return null; } |
| #890 | } |
| #891 | |
| #892 | async function loadCharacters(): Promise<CharacterAgent[]> { |
| #893 | const dir = join(AGENTS_DIR, 'characters'); |
| #894 | if (!existsSync(dir)) return []; |
| #895 | try { |
| #896 | const files = (await readdir(dir)).filter(f => f.endsWith('.json')); |
| #897 | const chars: CharacterAgent[] = []; |
| #898 | for (const file of files) { |
| #899 | try { |
| #900 | const raw = JSON.parse(await readFile(join(dir, file), 'utf8')) as { |
| #901 | name?: string; |
| #902 | bio?: string | string[]; |
| #903 | adjectives?: string[]; |
| #904 | topics?: string[]; |
| #905 | }; |
| #906 | const bioText = Array.isArray(raw.bio) ? raw.bio[0] ?? '' : (raw.bio ?? ''); |
| #907 | chars.push({ |
| #908 | file: file.replace('.json', ''), |
| #909 | name: raw.name ?? file.replace('.json', ''), |
| #910 | bio: bioText.slice(0, 120), |
| #911 | adjectives: raw.adjectives ?? [], |
| #912 | topics: raw.topics ?? [], |
| #913 | }); |
| #914 | } catch { /* skip */ } |
| #915 | } |
| #916 | return chars.sort((a, b) => a.name.localeCompare(b.name)); |
| #917 | } catch { return []; } |
| #918 | } |
| #919 | |
| #920 | async function loadTemplates(): Promise<TemplateEntry[]> { |
| #921 | const indexPath = join(AGENTS_DIR, 'templates', 'index.json'); |
| #922 | if (!existsSync(indexPath)) return []; |
| #923 | try { |
| #924 | const raw = JSON.parse(await readFile(indexPath, 'utf8')) as { templates?: TemplateEntry[] }; |
| #925 | return raw.templates ?? []; |
| #926 | } catch { return []; } |
| #927 | } |
| #928 | |
| #929 | // ─── Clipboard helper ───────────────────────────────────────────────────────── |
| #930 | |
| #931 | async function copyToClipboard(text: string): Promise<boolean> { |
| #932 | const { execFile } = await import('node:child_process'); |
| #933 | try { |
| #934 | const tool = process.platform === 'darwin' ? 'pbcopy' : 'xclip'; |
| #935 | const args = process.platform === 'linux' ? ['-selection', 'clipboard'] : []; |
| #936 | await new Promise<void>((res, rej) => { |
| #937 | const p = execFile(tool, args); |
| #938 | if (p.stdin) { p.stdin.write(text); p.stdin.end(); } |
| #939 | p.on('close', code => (code === 0 ? res() : rej(new Error(`exit ${code}`)))); |
| #940 | }); |
| #941 | return true; |
| #942 | } catch { /* ignore */ } |
| #943 | return false; |
| #944 | } |
| #945 | |
| #946 | // ─── View cycling ───────────────────────────────────────────────────────────── |
| #947 | |
| #948 | const VIEW_ORDER: ViewMode[] = ['commands', 'catalog', 'characters', 'templates', 'stats']; |
| #949 | |
| #950 | function nextView(v: ViewMode): ViewMode { |
| #951 | const i = VIEW_ORDER.indexOf(v); |
| #952 | return VIEW_ORDER[(i + 1) % VIEW_ORDER.length] ?? 'commands'; |
| #953 | } |
| #954 | |
| #955 | const EXIT_KEYS = new Set<string>(['b', 'B', '\x1b', '\x03']); |
| #956 | |
| #957 | // ─── Main export ────────────────────────────────────────────────────────────── |
| #958 | |
| #959 | export async function runAgentKit(): Promise<void> { |
| #960 | // State |
| #961 | let view: ViewMode = 'commands'; |
| #962 | let catIdx = 0; |
| #963 | let cmdIdx = 0; |
| #964 | let catalogFilter = 0; |
| #965 | let agentIdx = 0; |
| #966 | let charIdx = 0; |
| #967 | let tmplIdx = 0; |
| #968 | let status = 'Ready.'; |
| #969 | let copied = false; |
| #970 | |
| #971 | // Async data (loaded in background) |
| #972 | let catalog: CatalogData | null = null; |
| #973 | let characters: CharacterAgent[] = []; |
| #974 | let templates: TemplateEntry[] = []; |
| #975 | |
| #976 | const redraw = (): void => { |
| #977 | if (view === 'commands') renderCommands(catIdx, cmdIdx, status, copied); |
| #978 | else if (view === 'catalog') renderCatalog(catalog, catalogFilter, agentIdx, status); |
| #979 | else if (view === 'characters') renderCharacters(characters, charIdx, status); |
| #980 | else if (view === 'templates') renderTemplates(templates, tmplIdx, status); |
| #981 | else renderStats(catalog); |
| #982 | }; |
| #983 | |
| #984 | redraw(); |
| #985 | |
| #986 | // Load data in background then redraw |
| #987 | void Promise.all([loadCatalog(), loadCharacters(), loadTemplates()]).then(([cat, chars, tmps]) => { |
| #988 | catalog = cat; |
| #989 | characters = chars; |
| #990 | templates = tmps; |
| #991 | redraw(); |
| #992 | }); |
| #993 | |
| #994 | // ── Tiny state mutators (defined before Promise to keep nesting level low) ── |
| #995 | |
| #996 | type SyncOrAsyncVoid = () => void | Promise<void>; |
| #997 | |
| #998 | const cmdUp = (): void => { |
| #999 | const cat = CATEGORIES[catIdx]!; |
| #1000 | cmdIdx = (cmdIdx - 1 + cat.commands.length) % cat.commands.length; |
| #1001 | status = `Command: ${cat.commands[cmdIdx]?.name ?? ''}`; |
| #1002 | }; |
| #1003 | const cmdDown = (): void => { |
| #1004 | const cat = CATEGORIES[catIdx]!; |
| #1005 | cmdIdx = (cmdIdx + 1) % cat.commands.length; |
| #1006 | status = `Command: ${cat.commands[cmdIdx]?.name ?? ''}`; |
| #1007 | }; |
| #1008 | const cmdPrevCat = (): void => { |
| #1009 | catIdx = (catIdx - 1 + CATEGORIES.length) % CATEGORIES.length; |
| #1010 | cmdIdx = 0; |
| #1011 | status = `Category: ${CATEGORIES[catIdx]?.label ?? ''}`; |
| #1012 | }; |
| #1013 | const cmdNextCat = (): void => { |
| #1014 | catIdx = (catIdx + 1) % CATEGORIES.length; |
| #1015 | cmdIdx = 0; |
| #1016 | status = `Category: ${CATEGORIES[catIdx]?.label ?? ''}`; |
| #1017 | }; |
| #1018 | const cmdCopy = async (): Promise<void> => { |
| #1019 | const cmd = CATEGORIES[catIdx]!.commands[cmdIdx]?.cmd ?? ''; |
| #1020 | const ok = await copyToClipboard(cmd); |
| #1021 | copied = ok; |
| #1022 | status = ok ? 'Copied!' : 'Copy failed — paste manually.'; |
| #1023 | }; |
| #1024 | const cmdEnv = (): void => { |
| #1025 | const env = CATEGORIES[catIdx]!.commands[cmdIdx]?.env ?? []; |
| #1026 | status = env.length ? `Required env: ${env.join(', ')}` : 'No env vars required.'; |
| #1027 | }; |
| #1028 | |
| #1029 | const CMD_DISPATCH: Record<string, SyncOrAsyncVoid> = { |
| #1030 | '\x1b[A': cmdUp, '\x1b[B': cmdDown, |
| #1031 | '\x1b[D': cmdPrevCat, '\x1b[Z': cmdPrevCat, '\x1b[C': cmdNextCat, |
| #1032 | c: cmdCopy, C: cmdCopy, e: cmdEnv, E: cmdEnv, |
| #1033 | }; |
| #1034 | |
| #1035 | const handleCommands = async (chunk: string): Promise<boolean> => { |
| #1036 | const numMatch = /^[1-7]$/.exec(chunk); |
| #1037 | if (numMatch) { |
| #1038 | catIdx = Number.parseInt(chunk, 10) - 1; |
| #1039 | cmdIdx = 0; |
| #1040 | status = `Category: ${CATEGORIES[catIdx]?.label ?? ''}`; |
| #1041 | return true; |
| #1042 | } |
| #1043 | const handler = CMD_DISPATCH[chunk]; |
| #1044 | if (!handler) return false; |
| #1045 | await handler(); |
| #1046 | return true; |
| #1047 | }; |
| #1048 | |
| #1049 | const filteredAgents = (): CatalogAgent[] => catalogFilter === 0 |
| #1050 | ? (catalog?.agents ?? []) |
| #1051 | : (catalog?.agents.filter(a => a.category === (CATALOG_CATEGORIES[catalogFilter] ?? '')) ?? []); |
| #1052 | |
| #1053 | const catAgentUp = (): void => { |
| #1054 | const agents = filteredAgents(); |
| #1055 | agentIdx = Math.max(0, agentIdx - 1); |
| #1056 | status = `Agent: ${agents[agentIdx]?.title ?? ''}`; |
| #1057 | }; |
| #1058 | const catAgentDown = (): void => { |
| #1059 | const agents = filteredAgents(); |
| #1060 | agentIdx = Math.min(agents.length - 1, agentIdx + 1); |
| #1061 | status = `Agent: ${agents[agentIdx]?.title ?? ''}`; |
| #1062 | }; |
| #1063 | const catFilter = (): void => { |
| #1064 | catalogFilter = (catalogFilter + 1) % CATALOG_CATEGORIES.length; |
| #1065 | agentIdx = 0; |
| #1066 | status = `Filter: ${CATALOG_CATEGORIES[catalogFilter] ?? 'all'}`; |
| #1067 | }; |
| #1068 | const catMint = async (): Promise<void> => { |
| #1069 | const agent = filteredAgents()[agentIdx]; |
| #1070 | if (!agent) return; |
| #1071 | const mintCmd = `curl -sX POST https://x402.wtf/api/mint/agent -H 'Content-Type: application/json' -d '{"identifier":"${agent.identifier}"}'`; |
| #1072 | const ok = await copyToClipboard(mintCmd); |
| #1073 | status = ok ? '✓ Mint command copied!' : 'Copy failed.'; |
| #1074 | }; |
| #1075 | |
| #1076 | const CAT_DISPATCH: Record<string, SyncOrAsyncVoid> = { |
| #1077 | '\x1b[A': catAgentUp, '\x1b[B': catAgentDown, |
| #1078 | f: catFilter, F: catFilter, c: catMint, C: catMint, |
| #1079 | }; |
| #1080 | |
| #1081 | const handleCatalog = async (chunk: string): Promise<boolean> => { |
| #1082 | const handler = CAT_DISPATCH[chunk]; |
| #1083 | if (!handler) return false; |
| #1084 | await handler(); |
| #1085 | return true; |
| #1086 | }; |
| #1087 | |
| #1088 | const charUp = (): void => { |
| #1089 | charIdx = Math.max(0, charIdx - 1); |
| #1090 | status = `Character: ${characters[charIdx]?.name ?? ''}`; |
| #1091 | }; |
| #1092 | const charDown = (): void => { |
| #1093 | charIdx = Math.min(characters.length - 1, charIdx + 1); |
| #1094 | status = `Character: ${characters[charIdx]?.name ?? ''}`; |
| #1095 | }; |
| #1096 | const charCopy = async (): Promise<void> => { |
| #1097 | const ch = characters[charIdx]; |
| #1098 | if (!ch) return; |
| #1099 | const cmd = `curl -sX POST https://x402.wtf/api/agents/deploy -d '{"character":"${ch.file}"}'`; |
| #1100 | const ok = await copyToClipboard(cmd); |
| #1101 | status = ok ? '✓ Deploy command copied!' : 'Copy failed.'; |
| #1102 | }; |
| #1103 | |
| #1104 | const CHAR_DISPATCH: Record<string, SyncOrAsyncVoid> = { |
| #1105 | '\x1b[A': charUp, '\x1b[B': charDown, c: charCopy, C: charCopy, |
| #1106 | }; |
| #1107 | |
| #1108 | const handleCharacters = async (chunk: string): Promise<boolean> => { |
| #1109 | const handler = CHAR_DISPATCH[chunk]; |
| #1110 | if (!handler) return false; |
| #1111 | await handler(); |
| #1112 | return true; |
| #1113 | }; |
| #1114 | |
| #1115 | const tmplUp = (): void => { |
| #1116 | tmplIdx = Math.max(0, tmplIdx - 1); |
| #1117 | status = `Template: ${templates[tmplIdx]?.templateName ?? ''}`; |
| #1118 | }; |
| #1119 | const tmplDown = (): void => { |
| #1120 | tmplIdx = Math.min(templates.length - 1, tmplIdx + 1); |
| #1121 | status = `Template: ${templates[tmplIdx]?.templateName ?? ''}`; |
| #1122 | }; |
| #1123 | const tmplCopy = async (): Promise<void> => { |
| #1124 | const tmpl = templates[tmplIdx]; |
| #1125 | if (!tmpl) return; |
| #1126 | const cmd = `npx clawd-agent template use ${tmpl.templateId} --name "MyAgent"`; |
| #1127 | const ok = await copyToClipboard(cmd); |
| #1128 | status = ok ? '✓ Command copied!' : 'Copy failed.'; |
| #1129 | }; |
| #1130 | |
| #1131 | const TMPL_DISPATCH: Record<string, SyncOrAsyncVoid> = { |
| #1132 | '\x1b[A': tmplUp, '\x1b[B': tmplDown, c: tmplCopy, C: tmplCopy, |
| #1133 | }; |
| #1134 | |
| #1135 | const handleTemplates = async (chunk: string): Promise<boolean> => { |
| #1136 | const handler = TMPL_DISPATCH[chunk]; |
| #1137 | if (!handler) return false; |
| #1138 | await handler(); |
| #1139 | return true; |
| #1140 | }; |
| #1141 | |
| #1142 | type ViewHandler = (chunk: string) => Promise<boolean>; |
| #1143 | const VIEW_HANDLERS: Partial<Record<ViewMode, ViewHandler>> = { |
| #1144 | commands: handleCommands, |
| #1145 | catalog: handleCatalog, |
| #1146 | characters: handleCharacters, |
| #1147 | templates: handleTemplates, |
| #1148 | }; |
| #1149 | |
| #1150 | // ── Raw stdin setup ── |
| #1151 | |
| #1152 | return new Promise<void>(resolve => { |
| #1153 | const enableRaw = (): void => { |
| #1154 | if (process.stdin.setRawMode) process.stdin.setRawMode(true); |
| #1155 | process.stdin.resume(); |
| #1156 | process.stdin.setEncoding('utf8'); |
| #1157 | }; |
| #1158 | const disableRaw = (): void => { |
| #1159 | if (process.stdin.setRawMode) process.stdin.setRawMode(false); |
| #1160 | }; |
| #1161 | |
| #1162 | enableRaw(); |
| #1163 | |
| #1164 | const onData = async (chunk: string): Promise<void> => { |
| #1165 | copied = false; |
| #1166 | if (EXIT_KEYS.has(chunk)) { |
| #1167 | if (chunk === '\x03') { disableRaw(); process.exit(0); } |
| #1168 | process.stdin.off('data', onData as (c: string) => void); |
| #1169 | disableRaw(); |
| #1170 | resolve(); |
| #1171 | return; |
| #1172 | } |
| #1173 | if (chunk === '\t') { view = nextView(view); status = `View: ${view}`; redraw(); return; } |
| #1174 | const viewHandler = VIEW_HANDLERS[view]; |
| #1175 | const handled = viewHandler ? await viewHandler(chunk) : false; |
| #1176 | if (handled || view === 'stats') redraw(); |
| #1177 | }; |
| #1178 | |
| #1179 | process.stdin.on('data', onData as (c: string) => void); |
| #1180 | }); |
| #1181 | } |
| #1182 |