agentlaunch-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +359 -0
- package/dist/__tests__/config.test.d.ts +20 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +155 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/commands/config.d.ts +10 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +56 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/create.d.ts +22 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +493 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/deploy.d.ts +18 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +220 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/list.d.ts +10 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +131 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/scaffold.d.ts +13 -0
- package/dist/commands/scaffold.d.ts.map +1 -0
- package/dist/commands/scaffold.js +633 -0
- package/dist/commands/scaffold.js.map +1 -0
- package/dist/commands/status.d.ts +10 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +116 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/tokenize.d.ts +16 -0
- package/dist/commands/tokenize.d.ts.map +1 -0
- package/dist/commands/tokenize.js +139 -0
- package/dist/commands/tokenize.js.map +1 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +68 -0
- package/dist/config.js.map +1 -0
- package/dist/http.d.ts +20 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +69 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-002: scaffold command
|
|
3
|
+
*
|
|
4
|
+
* agentlaunch scaffold <name> [--type faucet|research|trading|data]
|
|
5
|
+
*
|
|
6
|
+
* Generates an agent project directory with:
|
|
7
|
+
* agent.py - Ready-to-edit agent code based on agent-business-template.py
|
|
8
|
+
* README.md - Quickstart instructions
|
|
9
|
+
* .env.example - Required environment variables
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
/** Per-type customisation slots injected into the template. */
|
|
14
|
+
const TYPE_META = {
|
|
15
|
+
faucet: {
|
|
16
|
+
domain: "faucet",
|
|
17
|
+
description: "Distributes testnet FET and BNB to new developers",
|
|
18
|
+
model: "mistralai/Mistral-7B-Instruct-v0.2",
|
|
19
|
+
},
|
|
20
|
+
research: {
|
|
21
|
+
domain: "research",
|
|
22
|
+
description: "Delivers on-demand research reports and analysis",
|
|
23
|
+
model: "mistralai/Mistral-7B-Instruct-v0.2",
|
|
24
|
+
},
|
|
25
|
+
trading: {
|
|
26
|
+
domain: "trading",
|
|
27
|
+
description: "Monitors token prices and sends trade alerts",
|
|
28
|
+
model: "mistralai/Mistral-7B-Instruct-v0.2",
|
|
29
|
+
},
|
|
30
|
+
data: {
|
|
31
|
+
domain: "data",
|
|
32
|
+
description: "Serves structured data feeds and query results",
|
|
33
|
+
model: "mistralai/Mistral-7B-Instruct-v0.2",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
/** Generate agent.py content based on type. */
|
|
37
|
+
function generateAgentPy(name, type) {
|
|
38
|
+
const meta = TYPE_META[type];
|
|
39
|
+
const className = toPascalCase(name);
|
|
40
|
+
return `#!/usr/bin/env python3
|
|
41
|
+
"""
|
|
42
|
+
${name} — AgentLaunch ${capitalize(type)} Agent
|
|
43
|
+
Generated by: agentlaunch scaffold ${name} --type ${type}
|
|
44
|
+
|
|
45
|
+
Platform constants (source of truth: deployed smart contracts):
|
|
46
|
+
- Deploy fee: 120 FET (read dynamically, can change via multi-sig)
|
|
47
|
+
- Graduation target: 30,000 FET -> auto DEX listing
|
|
48
|
+
- Trading fee: 2% -> 100% to protocol treasury (NO creator fee)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
from uagents import Agent, Context, Protocol
|
|
52
|
+
from uagents_core.contrib.protocols.chat import (
|
|
53
|
+
ChatAcknowledgement,
|
|
54
|
+
ChatMessage,
|
|
55
|
+
EndSessionContent,
|
|
56
|
+
TextContent,
|
|
57
|
+
chat_protocol_spec,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
import hashlib
|
|
61
|
+
import json
|
|
62
|
+
import os
|
|
63
|
+
import time
|
|
64
|
+
from collections import defaultdict
|
|
65
|
+
from datetime import datetime
|
|
66
|
+
from typing import Any, Dict, List, Optional
|
|
67
|
+
from uuid import uuid4
|
|
68
|
+
|
|
69
|
+
import requests
|
|
70
|
+
|
|
71
|
+
# ==============================================================================
|
|
72
|
+
# API CONFIG — Override via environment variables, never hardcode
|
|
73
|
+
# ==============================================================================
|
|
74
|
+
|
|
75
|
+
AGENTLAUNCH_API = os.environ.get("AGENTLAUNCH_API", "https://agent-launch.ai/api")
|
|
76
|
+
|
|
77
|
+
# ==============================================================================
|
|
78
|
+
# BUSINESS CONFIG
|
|
79
|
+
# ==============================================================================
|
|
80
|
+
|
|
81
|
+
OWNER_ADDRESS = os.environ.get("AGENT_OWNER_ADDRESS", "")
|
|
82
|
+
|
|
83
|
+
BUSINESS = {
|
|
84
|
+
"name": "${name}",
|
|
85
|
+
"description": "${meta.description}",
|
|
86
|
+
"version": "1.0.0",
|
|
87
|
+
"domain": "${meta.domain}",
|
|
88
|
+
"free_requests_per_day": 10,
|
|
89
|
+
"premium_token_threshold": 1000,
|
|
90
|
+
"ai_model": "${meta.model}",
|
|
91
|
+
"rate_limit_per_minute": 20,
|
|
92
|
+
"max_input_length": 5000,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ==============================================================================
|
|
97
|
+
# LAYER 1: FOUNDATION
|
|
98
|
+
# ==============================================================================
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Logger:
|
|
102
|
+
"""Structured logging with audit trail."""
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def info(ctx: Context, event: str, data: Optional[Dict] = None) -> None:
|
|
106
|
+
ctx.logger.info(f"[{event}] {json.dumps(data or {})}")
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def audit(ctx: Context, user: str, action: str) -> None:
|
|
110
|
+
ctx.logger.info(
|
|
111
|
+
f"[AUDIT] user={user[:20]} action={action} "
|
|
112
|
+
f"ts={datetime.utcnow().isoformat()}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def error(ctx: Context, event: str, error: str) -> None:
|
|
117
|
+
ctx.logger.error(f"[{event}] {error}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ==============================================================================
|
|
121
|
+
# LAYER 2: SECURITY
|
|
122
|
+
# ==============================================================================
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Security:
|
|
126
|
+
"""Rate limiting and input validation."""
|
|
127
|
+
|
|
128
|
+
def __init__(self) -> None:
|
|
129
|
+
self._requests: Dict[str, List[float]] = defaultdict(list)
|
|
130
|
+
self._check_count: int = 0
|
|
131
|
+
|
|
132
|
+
def check(self, ctx: Context, user_id: str, message: str) -> tuple:
|
|
133
|
+
now = time.time()
|
|
134
|
+
|
|
135
|
+
self._requests[user_id] = [
|
|
136
|
+
t for t in self._requests[user_id] if now - t < 60
|
|
137
|
+
]
|
|
138
|
+
if len(self._requests[user_id]) >= BUSINESS["rate_limit_per_minute"]:
|
|
139
|
+
return None, "Rate limit exceeded. Please wait a moment."
|
|
140
|
+
self._requests[user_id].append(now)
|
|
141
|
+
|
|
142
|
+
self._check_count += 1
|
|
143
|
+
if self._check_count % 100 == 0:
|
|
144
|
+
stale = [
|
|
145
|
+
k
|
|
146
|
+
for k, v in self._requests.items()
|
|
147
|
+
if not v or (now - max(v)) > 300
|
|
148
|
+
]
|
|
149
|
+
for k in stale:
|
|
150
|
+
del self._requests[k]
|
|
151
|
+
|
|
152
|
+
if not message or not message.strip():
|
|
153
|
+
return None, "Empty message."
|
|
154
|
+
if len(message) > BUSINESS["max_input_length"]:
|
|
155
|
+
return None, f"Message too long (max {BUSINESS['max_input_length']} chars)."
|
|
156
|
+
|
|
157
|
+
return message.strip(), None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ==============================================================================
|
|
161
|
+
# LAYER 3: STABILITY
|
|
162
|
+
# ==============================================================================
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Health:
|
|
166
|
+
"""Track uptime and error rate."""
|
|
167
|
+
|
|
168
|
+
def __init__(self) -> None:
|
|
169
|
+
self._start: datetime = datetime.utcnow()
|
|
170
|
+
self._requests: int = 0
|
|
171
|
+
self._errors: int = 0
|
|
172
|
+
|
|
173
|
+
def record(self, success: bool) -> None:
|
|
174
|
+
self._requests += 1
|
|
175
|
+
if not success:
|
|
176
|
+
self._errors += 1
|
|
177
|
+
|
|
178
|
+
def status(self) -> Dict[str, Any]:
|
|
179
|
+
uptime = (datetime.utcnow() - self._start).total_seconds()
|
|
180
|
+
error_rate = (self._errors / self._requests * 100) if self._requests else 0
|
|
181
|
+
return {
|
|
182
|
+
"status": "healthy" if error_rate < 10 else "degraded",
|
|
183
|
+
"uptime_seconds": int(uptime),
|
|
184
|
+
"requests": self._requests,
|
|
185
|
+
"error_rate": f"{error_rate:.1f}%",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ==============================================================================
|
|
190
|
+
# LAYER 4: SPEED
|
|
191
|
+
# ==============================================================================
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class Cache:
|
|
195
|
+
"""In-memory TTL cache with SHA256 keys."""
|
|
196
|
+
|
|
197
|
+
def __init__(self, max_size: int = 1000) -> None:
|
|
198
|
+
self._data: Dict[str, tuple] = {}
|
|
199
|
+
self._max_size: int = max_size
|
|
200
|
+
|
|
201
|
+
def get(self, key: str) -> Any:
|
|
202
|
+
if key in self._data:
|
|
203
|
+
value, expires = self._data[key]
|
|
204
|
+
if expires > time.time():
|
|
205
|
+
return value
|
|
206
|
+
del self._data[key]
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def set(self, key: str, value: Any, ttl: int = 300) -> None:
|
|
210
|
+
if len(self._data) >= self._max_size:
|
|
211
|
+
now = time.time()
|
|
212
|
+
expired = [k for k, (_, exp) in self._data.items() if exp <= now]
|
|
213
|
+
for k in expired:
|
|
214
|
+
del self._data[k]
|
|
215
|
+
if len(self._data) >= self._max_size:
|
|
216
|
+
to_drop = sorted(self._data.items(), key=lambda x: x[1][1])[
|
|
217
|
+
: self._max_size // 10
|
|
218
|
+
]
|
|
219
|
+
for k, _ in to_drop:
|
|
220
|
+
del self._data[k]
|
|
221
|
+
self._data[key] = (value, time.time() + ttl)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ==============================================================================
|
|
225
|
+
# LAYER 5: REVENUE
|
|
226
|
+
# ==============================================================================
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Revenue:
|
|
230
|
+
"""Token-gated access and daily usage quotas."""
|
|
231
|
+
|
|
232
|
+
def __init__(self, cache: Cache) -> None:
|
|
233
|
+
self._cache = cache
|
|
234
|
+
self._usage: Dict[str, List[str]] = defaultdict(list)
|
|
235
|
+
|
|
236
|
+
def get_tier(self, user_address: str) -> str:
|
|
237
|
+
cached = self._cache.get(f"tier:{user_address}")
|
|
238
|
+
if cached is not None:
|
|
239
|
+
return cached
|
|
240
|
+
try:
|
|
241
|
+
r = requests.get(
|
|
242
|
+
f"{AGENTLAUNCH_API}/agents/token/{user_address}", timeout=5
|
|
243
|
+
)
|
|
244
|
+
if r.status_code == 200:
|
|
245
|
+
data = r.json()
|
|
246
|
+
balance = data.get("balance", 0)
|
|
247
|
+
tier = (
|
|
248
|
+
"premium"
|
|
249
|
+
if balance >= BUSINESS["premium_token_threshold"]
|
|
250
|
+
else "free"
|
|
251
|
+
)
|
|
252
|
+
self._cache.set(f"tier:{user_address}", tier, ttl=300)
|
|
253
|
+
return tier
|
|
254
|
+
except Exception:
|
|
255
|
+
pass
|
|
256
|
+
return "free"
|
|
257
|
+
|
|
258
|
+
def check_quota(self, user_id: str, tier: str) -> tuple:
|
|
259
|
+
today = datetime.utcnow().date().isoformat()
|
|
260
|
+
self._usage[user_id] = [
|
|
261
|
+
t for t in self._usage[user_id] if t.startswith(today)
|
|
262
|
+
]
|
|
263
|
+
today_usage = len(self._usage[user_id])
|
|
264
|
+
limit = 1000 if tier == "premium" else BUSINESS["free_requests_per_day"]
|
|
265
|
+
if today_usage >= limit:
|
|
266
|
+
if tier == "free":
|
|
267
|
+
return False, (
|
|
268
|
+
f"Free limit reached ({limit}/day). "
|
|
269
|
+
f"Hold {BUSINESS['premium_token_threshold']} tokens for premium!"
|
|
270
|
+
)
|
|
271
|
+
return False, f"Daily limit reached ({limit}/day)."
|
|
272
|
+
self._usage[user_id].append(datetime.utcnow().isoformat())
|
|
273
|
+
return True, None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ==============================================================================
|
|
277
|
+
# AGENTLAUNCH INTEGRATION
|
|
278
|
+
# ==============================================================================
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class AgentLaunch:
|
|
282
|
+
"""Create and manage tokens on AgentLaunch."""
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def tokenize() -> Dict:
|
|
286
|
+
agent_address = os.environ.get("AGENT_ADDRESS")
|
|
287
|
+
if not agent_address:
|
|
288
|
+
return {"error": "AGENT_ADDRESS env var not set."}
|
|
289
|
+
try:
|
|
290
|
+
r = requests.post(
|
|
291
|
+
f"{AGENTLAUNCH_API}/agents/tokenize",
|
|
292
|
+
headers={
|
|
293
|
+
"X-API-Key": os.environ.get("AGENTLAUNCH_API_KEY", ""),
|
|
294
|
+
"Content-Type": "application/json",
|
|
295
|
+
},
|
|
296
|
+
json={
|
|
297
|
+
"agentAddress": agent_address,
|
|
298
|
+
"name": BUSINESS["name"],
|
|
299
|
+
"description": BUSINESS["description"],
|
|
300
|
+
},
|
|
301
|
+
timeout=30,
|
|
302
|
+
)
|
|
303
|
+
return r.json() if r.status_code in [200, 201] else {"error": r.text}
|
|
304
|
+
except Exception as e:
|
|
305
|
+
return {"error": str(e)}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ==============================================================================
|
|
309
|
+
# YOUR BUSINESS LOGIC — Customize this section
|
|
310
|
+
# ==============================================================================
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class ${className}Business:
|
|
314
|
+
"""
|
|
315
|
+
CUSTOMIZE THIS for your ${type} agent.
|
|
316
|
+
|
|
317
|
+
Pattern from FET Gifter: Agent + Wallet + Chat + Value Exchange = Economy
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
def __init__(self) -> None:
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
async def handle(self, ctx: Context, user_id: str, message: str, tier: str) -> str:
|
|
324
|
+
# Sanitize to prevent prompt injection
|
|
325
|
+
safe = message.replace("\\n", " ").replace("\\\\n", " ")[:500]
|
|
326
|
+
|
|
327
|
+
# TODO: Add your business logic here
|
|
328
|
+
return (
|
|
329
|
+
f"Hello from {BUSINESS['name']}! You said: {safe}\\n\\n"
|
|
330
|
+
f"Tier: {tier}. This is a scaffold — add your logic in ${className}Business.handle()."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
# ==============================================================================
|
|
335
|
+
# REPLY HELPER
|
|
336
|
+
# ==============================================================================
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
async def reply(ctx: Context, sender: str, text: str, end: bool = False) -> None:
|
|
340
|
+
content = [TextContent(type="text", text=text)]
|
|
341
|
+
if end:
|
|
342
|
+
content.append(EndSessionContent(type="end-session"))
|
|
343
|
+
try:
|
|
344
|
+
await ctx.send(
|
|
345
|
+
sender,
|
|
346
|
+
ChatMessage(timestamp=datetime.utcnow(), msg_id=uuid4(), content=content),
|
|
347
|
+
)
|
|
348
|
+
except Exception as e:
|
|
349
|
+
ctx.logger.error(f"Failed to send reply to {sender[:20]}: {e}")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# ==============================================================================
|
|
353
|
+
# MAIN AGENT
|
|
354
|
+
# ==============================================================================
|
|
355
|
+
|
|
356
|
+
cache = Cache(max_size=1000)
|
|
357
|
+
security = Security()
|
|
358
|
+
health = Health()
|
|
359
|
+
revenue = Revenue(cache)
|
|
360
|
+
business = ${className}Business()
|
|
361
|
+
|
|
362
|
+
agent = Agent()
|
|
363
|
+
chat_proto = Protocol(spec=chat_protocol_spec)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@chat_proto.on_message(ChatMessage)
|
|
367
|
+
async def handle_chat(ctx: Context, sender: str, msg: ChatMessage) -> None:
|
|
368
|
+
try:
|
|
369
|
+
await ctx.send(
|
|
370
|
+
sender,
|
|
371
|
+
ChatAcknowledgement(
|
|
372
|
+
timestamp=datetime.utcnow(), acknowledged_msg_id=msg.msg_id
|
|
373
|
+
),
|
|
374
|
+
)
|
|
375
|
+
except Exception as e:
|
|
376
|
+
ctx.logger.error(f"Failed to send ack to {sender[:20]}: {e}")
|
|
377
|
+
|
|
378
|
+
text = " ".join(
|
|
379
|
+
item.text for item in msg.content if isinstance(item, TextContent)
|
|
380
|
+
).strip()
|
|
381
|
+
text = text[: BUSINESS["max_input_length"]]
|
|
382
|
+
|
|
383
|
+
clean, error = security.check(ctx, sender, text)
|
|
384
|
+
if error:
|
|
385
|
+
health.record(False)
|
|
386
|
+
await reply(ctx, sender, error, end=True)
|
|
387
|
+
return
|
|
388
|
+
|
|
389
|
+
Logger.audit(ctx, sender, "request")
|
|
390
|
+
|
|
391
|
+
lower = clean.lower()
|
|
392
|
+
|
|
393
|
+
if lower in ("help", "?"):
|
|
394
|
+
tier = revenue.get_tier(sender)
|
|
395
|
+
await reply(
|
|
396
|
+
ctx,
|
|
397
|
+
sender,
|
|
398
|
+
f"**{BUSINESS['name']}** v{BUSINESS['version']}\\n\\n"
|
|
399
|
+
f"{BUSINESS['description']}\\n\\n"
|
|
400
|
+
f"Your tier: {tier.upper()}\\n\\n"
|
|
401
|
+
f"Commands: help, status, tokenize",
|
|
402
|
+
)
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
if lower == "status":
|
|
406
|
+
s = health.status()
|
|
407
|
+
await reply(
|
|
408
|
+
ctx,
|
|
409
|
+
sender,
|
|
410
|
+
f"Status: {s['status']} | Uptime: {s['uptime_seconds']}s | "
|
|
411
|
+
f"Requests: {s['requests']} | Error rate: {s['error_rate']}",
|
|
412
|
+
)
|
|
413
|
+
return
|
|
414
|
+
|
|
415
|
+
if "tokenize" in lower:
|
|
416
|
+
if OWNER_ADDRESS and sender != OWNER_ADDRESS:
|
|
417
|
+
await reply(ctx, sender, "Only the agent owner can trigger tokenization.", end=True)
|
|
418
|
+
return
|
|
419
|
+
result = AgentLaunch.tokenize()
|
|
420
|
+
link = result.get("data", {}).get("handoff_link") or result.get("handoff_link")
|
|
421
|
+
await reply(
|
|
422
|
+
ctx,
|
|
423
|
+
sender,
|
|
424
|
+
f"Token created! Deploy here: {link}" if link else f"Result: {json.dumps(result)}",
|
|
425
|
+
end=True,
|
|
426
|
+
)
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
tier = revenue.get_tier(sender)
|
|
430
|
+
allowed, quota_error = revenue.check_quota(sender, tier)
|
|
431
|
+
if not allowed:
|
|
432
|
+
health.record(False)
|
|
433
|
+
await reply(ctx, sender, quota_error, end=True)
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
response = await business.handle(ctx, sender, clean, tier)
|
|
438
|
+
health.record(True)
|
|
439
|
+
except Exception as e:
|
|
440
|
+
health.record(False)
|
|
441
|
+
Logger.error(ctx, "business_handle", str(e))
|
|
442
|
+
response = "Something went wrong. Please try again."
|
|
443
|
+
|
|
444
|
+
await reply(ctx, sender, response, end=True)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@chat_proto.on_message(ChatAcknowledgement)
|
|
448
|
+
async def handle_ack(ctx: Context, sender: str, msg: ChatAcknowledgement) -> None:
|
|
449
|
+
ctx.logger.debug(f"Ack from {sender[:20]} for msg {msg.acknowledged_msg_id}")
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
@agent.on_interval(period=3600)
|
|
453
|
+
async def periodic_health(ctx: Context) -> None:
|
|
454
|
+
ctx.logger.info(f"[HEALTH] {json.dumps(health.status())}")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
agent.include(chat_proto, publish_manifest=True)
|
|
458
|
+
|
|
459
|
+
if __name__ == "__main__":
|
|
460
|
+
agent.run()
|
|
461
|
+
`;
|
|
462
|
+
}
|
|
463
|
+
/** Generate README.md for the scaffolded project. */
|
|
464
|
+
function generateReadme(name, type) {
|
|
465
|
+
return `# ${name}
|
|
466
|
+
|
|
467
|
+
AgentLaunch ${capitalize(type)} Agent — generated by \`agentlaunch scaffold\`.
|
|
468
|
+
|
|
469
|
+
## Quickstart
|
|
470
|
+
|
|
471
|
+
### 1. Install dependencies
|
|
472
|
+
|
|
473
|
+
\`\`\`bash
|
|
474
|
+
pip install uagents uagents-core requests
|
|
475
|
+
\`\`\`
|
|
476
|
+
|
|
477
|
+
### 2. Configure environment
|
|
478
|
+
|
|
479
|
+
\`\`\`bash
|
|
480
|
+
cp .env.example .env
|
|
481
|
+
# Edit .env and fill in the required values
|
|
482
|
+
\`\`\`
|
|
483
|
+
|
|
484
|
+
### 3. Customize your agent
|
|
485
|
+
|
|
486
|
+
Open \`agent.py\` and edit the \`${toPascalCase(name)}Business.handle()\` method.
|
|
487
|
+
This is where your agent's value exchange lives.
|
|
488
|
+
|
|
489
|
+
### 4. Run locally
|
|
490
|
+
|
|
491
|
+
\`\`\`bash
|
|
492
|
+
source .env && python agent.py
|
|
493
|
+
\`\`\`
|
|
494
|
+
|
|
495
|
+
### 5. Deploy to Agentverse
|
|
496
|
+
|
|
497
|
+
\`\`\`bash
|
|
498
|
+
agentlaunch deploy
|
|
499
|
+
\`\`\`
|
|
500
|
+
|
|
501
|
+
This uploads \`agent.py\` to Agentverse, sets secrets, and starts the agent.
|
|
502
|
+
|
|
503
|
+
### 6. Tokenize your agent
|
|
504
|
+
|
|
505
|
+
\`\`\`bash
|
|
506
|
+
agentlaunch tokenize \\
|
|
507
|
+
--agent <address> \\
|
|
508
|
+
--name "${name}" \\
|
|
509
|
+
--symbol "${name.slice(0, 4).toUpperCase()}"
|
|
510
|
+
\`\`\`
|
|
511
|
+
|
|
512
|
+
You will receive a handoff link. Share it with a human to complete on-chain deployment.
|
|
513
|
+
|
|
514
|
+
## Platform Constants
|
|
515
|
+
|
|
516
|
+
- Deploy fee: **120 FET** (read dynamically from contract)
|
|
517
|
+
- Graduation target: **30,000 FET** — auto DEX listing
|
|
518
|
+
- Trading fee: **2%** — 100% to protocol treasury (no creator fee)
|
|
519
|
+
|
|
520
|
+
## Key Commands
|
|
521
|
+
|
|
522
|
+
| Command | Description |
|
|
523
|
+
|---------|-------------|
|
|
524
|
+
| \`agentlaunch config show\` | Show current config |
|
|
525
|
+
| \`agentlaunch deploy\` | Deploy agent.py to Agentverse |
|
|
526
|
+
| \`agentlaunch tokenize\` | Create a token record + handoff link |
|
|
527
|
+
|
|
528
|
+
## Resources
|
|
529
|
+
|
|
530
|
+
- [AgentLaunch Platform](https://agent-launch.ai)
|
|
531
|
+
- [Agentverse](https://agentverse.ai)
|
|
532
|
+
- [skill.md](https://agent-launch.ai/skill.md)
|
|
533
|
+
- [API docs](https://agent-launch.ai/docs/openapi)
|
|
534
|
+
`;
|
|
535
|
+
}
|
|
536
|
+
/** Generate .env.example */
|
|
537
|
+
function generateEnvExample(name) {
|
|
538
|
+
return `# ${name} — Environment Variables
|
|
539
|
+
# Copy to .env and fill in real values. Never commit .env.
|
|
540
|
+
|
|
541
|
+
# Your Agentverse API key (https://agentverse.ai/profile/api-keys)
|
|
542
|
+
AGENTVERSE_API_KEY=
|
|
543
|
+
|
|
544
|
+
# Your AgentLaunch API key (same as Agentverse key in most cases)
|
|
545
|
+
AGENTLAUNCH_API_KEY=
|
|
546
|
+
|
|
547
|
+
# The address of this agent on Agentverse (set after first deploy)
|
|
548
|
+
AGENT_ADDRESS=
|
|
549
|
+
|
|
550
|
+
# The wallet address that owns this agent (for owner-gated commands)
|
|
551
|
+
AGENT_OWNER_ADDRESS=
|
|
552
|
+
|
|
553
|
+
# Optional: override the API base URL (default: https://agent-launch.ai/api)
|
|
554
|
+
# AGENTLAUNCH_API=https://agent-launch.ai/api
|
|
555
|
+
|
|
556
|
+
# Optional: Hugging Face API key for AI features
|
|
557
|
+
# HUGGINGFACE_API_KEY=
|
|
558
|
+
`;
|
|
559
|
+
}
|
|
560
|
+
export function registerScaffoldCommand(program) {
|
|
561
|
+
program
|
|
562
|
+
.command("scaffold <name>")
|
|
563
|
+
.description("Generate an agent project from template in a new directory <name>")
|
|
564
|
+
.option("--type <type>", "Agent type: faucet, research, trading, data (default: research)", "research")
|
|
565
|
+
.option("--json", "Output only JSON (machine-readable)")
|
|
566
|
+
.action((name, options) => {
|
|
567
|
+
const isJson = options.json === true;
|
|
568
|
+
const type = options.type;
|
|
569
|
+
const validTypes = ["faucet", "research", "trading", "data"];
|
|
570
|
+
if (!validTypes.includes(type)) {
|
|
571
|
+
if (isJson) {
|
|
572
|
+
console.log(JSON.stringify({ error: `--type must be one of: ${validTypes.join(", ")}` }));
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
console.error(`Error: --type must be one of: ${validTypes.join(", ")}`);
|
|
576
|
+
}
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
// Sanitize directory name
|
|
580
|
+
const dirName = name.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
|
|
581
|
+
const targetDir = path.resolve(process.cwd(), dirName);
|
|
582
|
+
if (fs.existsSync(targetDir)) {
|
|
583
|
+
if (isJson) {
|
|
584
|
+
console.log(JSON.stringify({ error: `Directory "${dirName}" already exists.` }));
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
console.error(`Error: Directory "${dirName}" already exists.`);
|
|
588
|
+
}
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
if (!isJson) {
|
|
592
|
+
console.log(`Scaffolding ${capitalize(type)} agent: ${name}`);
|
|
593
|
+
console.log(`Directory: ${targetDir}`);
|
|
594
|
+
}
|
|
595
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
596
|
+
const files = {
|
|
597
|
+
"agent.py": generateAgentPy(name, type),
|
|
598
|
+
"README.md": generateReadme(name, type),
|
|
599
|
+
".env.example": generateEnvExample(name),
|
|
600
|
+
};
|
|
601
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
602
|
+
const filePath = path.join(targetDir, filename);
|
|
603
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
604
|
+
if (!isJson)
|
|
605
|
+
console.log(` Created: ${filename}`);
|
|
606
|
+
}
|
|
607
|
+
if (isJson) {
|
|
608
|
+
console.log(JSON.stringify({
|
|
609
|
+
name,
|
|
610
|
+
type,
|
|
611
|
+
directory: targetDir,
|
|
612
|
+
files: Object.keys(files),
|
|
613
|
+
}));
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
console.log(`\nDone! Next steps:\n`);
|
|
617
|
+
console.log(` cd ${dirName}`);
|
|
618
|
+
console.log(` cp .env.example .env`);
|
|
619
|
+
console.log(` # Edit .env and agent.py`);
|
|
620
|
+
console.log(` agentlaunch deploy`);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
// --- helpers ---
|
|
625
|
+
function capitalize(s) {
|
|
626
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
627
|
+
}
|
|
628
|
+
function toPascalCase(s) {
|
|
629
|
+
return s
|
|
630
|
+
.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
|
|
631
|
+
.replace(/^(.)/, (c) => c.toUpperCase());
|
|
632
|
+
}
|
|
633
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/commands/scaffold.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-004: status command
|
|
3
|
+
*
|
|
4
|
+
* agentlaunch status <address> [--json]
|
|
5
|
+
*
|
|
6
|
+
* Fetches a token from GET /api/agents/token/<address> and shows its details.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
export declare function registerStatusCommand(program: Command): void;
|
|
10
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8BpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA4E5D"}
|