spawn-skill 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.
Files changed (3) hide show
  1. package/bin/CLAUDE.md +7 -0
  2. package/bin/cli.js +619 -0
  3. package/package.json +24 -0
package/bin/CLAUDE.md ADDED
@@ -0,0 +1,7 @@
1
+ <claude-mem-context>
2
+ # Recent Activity
3
+
4
+ <!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
5
+
6
+ *No recent activity*
7
+ </claude-mem-context>
package/bin/cli.js ADDED
@@ -0,0 +1,619 @@
1
+ #!/usr/bin/env node
2
+
3
+ import chalk from 'chalk';
4
+ import inquirer from 'inquirer';
5
+ import ora from 'ora';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+
12
+ // Colors matching Spawn.wtf brand
13
+ const pink = chalk.hex('#ff2d92');
14
+ const dim = chalk.gray;
15
+
16
+ function printBanner() {
17
+ console.log('');
18
+ console.log(pink(' ╔═══════════════════════════════════════╗'));
19
+ console.log(pink(' ║') + chalk.white.bold(' SPAWN.WTF Skill Setup ') + pink('║'));
20
+ console.log(pink(' ╚═══════════════════════════════════════╝'));
21
+ console.log('');
22
+ }
23
+
24
+ function printSuccess(message) {
25
+ console.log(chalk.green('✓') + ' ' + message);
26
+ }
27
+
28
+ const SKILL_MD = `# Spawn Integration Skill
29
+
30
+ ## Overview
31
+
32
+ You are connected to **Spawn**, a mobile/desktop app that serves as your user interface. Instead of raw terminal output, your responses are rendered as rich UI components on the user's phone or computer.
33
+
34
+ This skill teaches you how to:
35
+
36
+ 1. Communicate through structured UI components
37
+ 2. Request approval for sensitive actions
38
+ 3. Spawn and manage sub-agents
39
+ 4. Respect the user's safety settings
40
+
41
+ **Important:** The user has configured safety settings in the Spawn app. You must respect these settings. They represent the user's explicit preferences about what you can do autonomously vs. what requires approval.
42
+
43
+ ---
44
+
45
+ ## Communication Protocol
46
+
47
+ ### Sending Messages
48
+
49
+ \`\`\`python
50
+ from spawn import ui
51
+
52
+ # Simple text
53
+ ui.send_text("I've analyzed the codebase. Found 3 issues.")
54
+
55
+ # Card with stats
56
+ ui.send_card(
57
+ title="Code Analysis",
58
+ value="3 issues",
59
+ style="warning",
60
+ fields=[
61
+ {"label": "Critical", "value": "1"},
62
+ {"label": "Warning", "value": "2"},
63
+ {"label": "Files scanned", "value": "47"},
64
+ ]
65
+ )
66
+ \`\`\`
67
+
68
+ ### Status Updates
69
+
70
+ Keep the user informed about what you're doing:
71
+
72
+ \`\`\`python
73
+ from spawn import status
74
+
75
+ status.set("thinking", "Analyzing codebase...")
76
+ status.set("working", "Refactoring auth.py (3/12)")
77
+ status.set("idle") # When done
78
+ \`\`\`
79
+
80
+ ---
81
+
82
+ ## Approval Flows
83
+
84
+ ### When to Ask for Approval
85
+
86
+ **Always ask** when:
87
+ - The action involves money or trades
88
+ - The action is irreversible (delete, deploy, send)
89
+ - You're unsure if the user would want this
90
+
91
+ **Never ask** when:
92
+ - The action is read-only
93
+ - You're just gathering information
94
+
95
+ ### How to Request Approval
96
+
97
+ \`\`\`python
98
+ from spawn import approval
99
+
100
+ approved = await approval.confirm(
101
+ title="Delete old logs?",
102
+ message="This will remove 47 log files older than 30 days.",
103
+ danger_level="medium" # low, medium, high, critical
104
+ )
105
+
106
+ if approved:
107
+ delete_logs()
108
+ else:
109
+ ui.send_text("Okay, I won't delete the logs.")
110
+ \`\`\`
111
+
112
+ ### Danger Levels
113
+
114
+ | Level | When to Use | UI Treatment |
115
+ |-------|-------------|--------------|
116
+ | \`low\` | Reversible, low impact | Tap to confirm |
117
+ | \`medium\` | Moderate impact, recoverable | Yellow card, tap to confirm |
118
+ | \`high\` | Significant impact, hard to reverse | Red card, slide to confirm |
119
+ | \`critical\` | Irreversible, financial, destructive | Red card, biometric required |
120
+
121
+ ---
122
+
123
+ ## Sub-Agent Spawning
124
+
125
+ ### Requesting a Sub-Agent
126
+
127
+ \`\`\`python
128
+ from spawn import agents
129
+
130
+ sub = await agents.request_spawn(
131
+ name="TestRunner",
132
+ role="QA Engineer",
133
+ description="Run tests in parallel while I refactor",
134
+ permissions=[
135
+ {"scope": "files.read", "path": "/projects/tests/**"},
136
+ {"scope": "process.execute", "command": "pytest"},
137
+ ],
138
+ reason="I need parallel test execution to catch breaking changes quickly."
139
+ )
140
+
141
+ if sub is None:
142
+ ui.send_text("Understood, I'll run tests sequentially instead.")
143
+ else:
144
+ await sub.start()
145
+ \`\`\`
146
+
147
+ ---
148
+
149
+ ## Path Restrictions
150
+
151
+ Before accessing any file, check if it's allowed:
152
+
153
+ \`\`\`python
154
+ from spawn import policy
155
+
156
+ if policy.is_path_forbidden(path):
157
+ ui.send_text("I can't access that path - it's protected.")
158
+ return
159
+ \`\`\`
160
+
161
+ ### Common Forbidden Paths
162
+
163
+ - \`~/.ssh/**\` - SSH keys
164
+ - \`~/.aws/**\` - AWS credentials
165
+ - \`**/.env\` - Environment files
166
+ - \`**/*.pem\`, \`**/*.key\` - Private keys
167
+
168
+ ---
169
+
170
+ ## Notifications
171
+
172
+ Send push notifications for important events:
173
+
174
+ \`\`\`python
175
+ from spawn import notify
176
+
177
+ notify.send(
178
+ title="Refactoring Complete",
179
+ body="All 12 files updated, tests passing.",
180
+ priority="normal" # silent, normal, high, critical
181
+ )
182
+ \`\`\`
183
+
184
+ ---
185
+
186
+ ## Best Practices
187
+
188
+ 1. **Always check settings first** before taking actions
189
+ 2. **Prefer asking over assuming** - users prefer being asked
190
+ 3. **Keep user informed** - update status during long operations
191
+ 4. **Graceful degradation** - if denied, explain alternatives
192
+ 5. **Respect the spirit** - don't exploit technical loopholes
193
+
194
+ The user trusts you enough to give you capabilities. Honor that trust by respecting their configured limits.
195
+ `;
196
+
197
+ function getTypeScriptConnector(token, agentName) {
198
+ return `/**
199
+ * Spawn.wtf Agent Connector
200
+ * Run with: node connect.js
201
+ */
202
+
203
+ const { SpawnAgent } = require('./node_modules/@spawn/agent-sdk/dist/index.js');
204
+
205
+ const agent = new SpawnAgent({
206
+ token: '${token}',
207
+ name: '${agentName}',
208
+
209
+ onConnect: () => {
210
+ console.log('Connected to Spawn.wtf!');
211
+ agent.sendText('${agentName} is online and ready.');
212
+ agent.updateStatus('idle', 'Ready for commands');
213
+ },
214
+
215
+ onMessage: (msg) => {
216
+ console.log('Message from app:', msg);
217
+
218
+ if (msg.type === 'message') {
219
+ const text = msg.payload.text || '';
220
+ agent.updateStatus('thinking', 'Processing...');
221
+
222
+ // Echo back
223
+ setTimeout(() => {
224
+ agent.sendText(\`You said: "\${text}"\`);
225
+ agent.updateStatus('idle', 'Ready');
226
+ }, 500);
227
+ }
228
+ },
229
+
230
+ onDisconnect: () => {
231
+ console.log('Disconnected from Spawn.wtf');
232
+ },
233
+
234
+ onError: (err) => {
235
+ console.error('Error:', err.message);
236
+ }
237
+ });
238
+
239
+ agent.connect();
240
+ console.log('Connecting to Spawn.wtf...');
241
+
242
+ process.on('SIGINT', () => {
243
+ agent.disconnect();
244
+ process.exit(0);
245
+ });
246
+ `;
247
+ }
248
+
249
+ function getPythonConnector(token, agentName) {
250
+ return `"""
251
+ Spawn.wtf Agent Connector
252
+ Run with: python connect.py
253
+ """
254
+
255
+ import asyncio
256
+ import json
257
+ import signal
258
+ import sys
259
+ from spawn_sdk import SpawnAgent
260
+
261
+ agent = SpawnAgent(
262
+ token='${token}',
263
+ name='${agentName}'
264
+ )
265
+
266
+ @agent.on('connect')
267
+ async def on_connect():
268
+ print('Connected to Spawn.wtf!')
269
+ await agent.send_text('${agentName} is online and ready.')
270
+ await agent.update_status('idle', 'Ready for commands')
271
+
272
+ @agent.on('message')
273
+ async def on_message(msg):
274
+ print(f'Message from app: {msg}')
275
+
276
+ if msg.get('type') == 'message':
277
+ text = msg.get('payload', {}).get('text', '')
278
+ await agent.update_status('thinking', 'Processing...')
279
+
280
+ # Echo back
281
+ await asyncio.sleep(0.5)
282
+ await agent.send_text(f'You said: "{text}"')
283
+ await agent.update_status('idle', 'Ready')
284
+
285
+ @agent.on('disconnect')
286
+ async def on_disconnect():
287
+ print('Disconnected from Spawn.wtf')
288
+
289
+ @agent.on('error')
290
+ async def on_error(err):
291
+ print(f'Error: {err}')
292
+
293
+ def signal_handler(sig, frame):
294
+ asyncio.create_task(agent.disconnect())
295
+ sys.exit(0)
296
+
297
+ signal.signal(signal.SIGINT, signal_handler)
298
+
299
+ if __name__ == '__main__':
300
+ print('Connecting to Spawn.wtf...')
301
+ asyncio.run(agent.connect())
302
+ `;
303
+ }
304
+
305
+ function getPythonSDK() {
306
+ return `"""
307
+ Spawn.wtf Python SDK
308
+ """
309
+
310
+ import asyncio
311
+ import json
312
+ import websockets
313
+ from typing import Callable, Optional, Dict, Any
314
+
315
+ class SpawnAgent:
316
+ def __init__(self, token: str, name: str, relay_url: str = 'wss://spawn-relay.ngvsqdjj5r.workers.dev/v1/agent'):
317
+ self.token = token
318
+ self.name = name
319
+ self.relay_url = relay_url
320
+ self.ws: Optional[websockets.WebSocketClientProtocol] = None
321
+ self._handlers: Dict[str, Callable] = {}
322
+ self._connected = False
323
+
324
+ def on(self, event: str):
325
+ """Decorator to register event handlers"""
326
+ def decorator(func: Callable):
327
+ self._handlers[event] = func
328
+ return func
329
+ return decorator
330
+
331
+ async def connect(self):
332
+ """Connect to the Spawn relay"""
333
+ try:
334
+ self.ws = await websockets.connect(
335
+ self.relay_url,
336
+ extra_headers={'Authorization': f'Bearer {self.token}'}
337
+ )
338
+ self._connected = True
339
+
340
+ # Send auth message
341
+ await self._send({
342
+ 'type': 'auth',
343
+ 'id': f'auth_{int(asyncio.get_event_loop().time() * 1000)}',
344
+ 'ts': int(asyncio.get_event_loop().time() * 1000),
345
+ 'payload': {
346
+ 'token': self.token,
347
+ 'name': self.name
348
+ }
349
+ })
350
+
351
+ # Start message loop
352
+ await self._message_loop()
353
+
354
+ except Exception as e:
355
+ if 'error' in self._handlers:
356
+ await self._handlers['error'](str(e))
357
+ raise
358
+
359
+ async def _message_loop(self):
360
+ """Main message receiving loop"""
361
+ try:
362
+ async for message in self.ws:
363
+ data = json.loads(message)
364
+ msg_type = data.get('type', '')
365
+
366
+ if msg_type == 'auth_success':
367
+ if 'connect' in self._handlers:
368
+ await self._handlers['connect']()
369
+ elif msg_type == 'message':
370
+ if 'message' in self._handlers:
371
+ await self._handlers['message'](data)
372
+ elif msg_type == 'pong':
373
+ pass # Keep-alive response
374
+
375
+ except websockets.ConnectionClosed:
376
+ self._connected = False
377
+ if 'disconnect' in self._handlers:
378
+ await self._handlers['disconnect']()
379
+
380
+ async def _send(self, data: Dict[str, Any]):
381
+ """Send a message to the relay"""
382
+ if self.ws and self._connected:
383
+ await self.ws.send(json.dumps(data))
384
+
385
+ async def send_text(self, text: str, format: str = 'plain'):
386
+ """Send a text message"""
387
+ await self._send({
388
+ 'type': 'message',
389
+ 'id': f'msg_{int(asyncio.get_event_loop().time() * 1000)}',
390
+ 'ts': int(asyncio.get_event_loop().time() * 1000),
391
+ 'payload': {
392
+ 'content_type': 'text',
393
+ 'text': text,
394
+ 'format': format
395
+ }
396
+ })
397
+
398
+ async def send_card(self, title: str, subtitle: str = None, style: str = 'default',
399
+ fields: list = None, footer: str = None):
400
+ """Send a card message"""
401
+ await self._send({
402
+ 'type': 'message',
403
+ 'id': f'msg_{int(asyncio.get_event_loop().time() * 1000)}',
404
+ 'ts': int(asyncio.get_event_loop().time() * 1000),
405
+ 'payload': {
406
+ 'content_type': 'card',
407
+ 'card': {
408
+ 'title': title,
409
+ 'subtitle': subtitle,
410
+ 'style': style,
411
+ 'fields': fields or [],
412
+ 'footer': footer
413
+ }
414
+ }
415
+ })
416
+
417
+ async def update_status(self, status: str, label: str = None):
418
+ """Update agent status"""
419
+ await self._send({
420
+ 'type': 'status_update',
421
+ 'id': f'status_{int(asyncio.get_event_loop().time() * 1000)}',
422
+ 'ts': int(asyncio.get_event_loop().time() * 1000),
423
+ 'payload': {
424
+ 'status': status,
425
+ 'label': label
426
+ }
427
+ })
428
+
429
+ async def notify(self, title: str, body: str, priority: str = 'normal'):
430
+ """Send a push notification"""
431
+ await self._send({
432
+ 'type': 'notification',
433
+ 'id': f'notif_{int(asyncio.get_event_loop().time() * 1000)}',
434
+ 'ts': int(asyncio.get_event_loop().time() * 1000),
435
+ 'payload': {
436
+ 'title': title,
437
+ 'body': body,
438
+ 'priority': priority
439
+ }
440
+ })
441
+
442
+ async def disconnect(self):
443
+ """Disconnect from the relay"""
444
+ if self.ws:
445
+ await self.ws.close()
446
+ self._connected = False
447
+ `;
448
+ }
449
+
450
+ async function createSkillFiles(token, agentName, language) {
451
+ const skillDir = path.join(process.cwd(), 'spawn');
452
+
453
+ // Create spawn directory
454
+ if (!fs.existsSync(skillDir)) {
455
+ fs.mkdirSync(skillDir, { recursive: true });
456
+ }
457
+
458
+ // Create SKILL.md
459
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), SKILL_MD);
460
+
461
+ // Create config file
462
+ const config = {
463
+ name: agentName,
464
+ token: token,
465
+ relay: 'wss://spawn-relay.ngvsqdjj5r.workers.dev/v1/agent',
466
+ language: language
467
+ };
468
+ fs.writeFileSync(path.join(skillDir, 'config.json'), JSON.stringify(config, null, 2));
469
+
470
+ if (language === 'typescript') {
471
+ // TypeScript/Node setup
472
+ fs.writeFileSync(path.join(skillDir, 'connect.js'), getTypeScriptConnector(token, agentName));
473
+
474
+ const packageJson = {
475
+ name: "spawn-agent",
476
+ version: "1.0.0",
477
+ type: "commonjs",
478
+ dependencies: {
479
+ "@spawn/agent-sdk": "github:SpawnWTF/spawn#main:sdk"
480
+ }
481
+ };
482
+ fs.writeFileSync(path.join(skillDir, 'package.json'), JSON.stringify(packageJson, null, 2));
483
+
484
+ } else {
485
+ // Python setup
486
+ fs.writeFileSync(path.join(skillDir, 'connect.py'), getPythonConnector(token, agentName));
487
+ fs.writeFileSync(path.join(skillDir, 'spawn_sdk.py'), getPythonSDK());
488
+ fs.writeFileSync(path.join(skillDir, 'requirements.txt'), 'websockets>=12.0\n');
489
+ }
490
+
491
+ return { skillDir, language };
492
+ }
493
+
494
+ async function main() {
495
+ printBanner();
496
+
497
+ // Parse arguments
498
+ const args = process.argv.slice(2);
499
+ let token = null;
500
+ let agentName = 'Claude';
501
+ let language = null;
502
+
503
+ for (let i = 0; i < args.length; i++) {
504
+ if (args[i] === '--token' || args[i] === '-t') {
505
+ token = args[i + 1];
506
+ }
507
+ if (args[i] === '--name' || args[i] === '-n') {
508
+ agentName = args[i + 1];
509
+ }
510
+ if (args[i] === '--python' || args[i] === '-py') {
511
+ language = 'python';
512
+ }
513
+ if (args[i] === '--node' || args[i] === '--typescript' || args[i] === '-ts') {
514
+ language = 'typescript';
515
+ }
516
+ }
517
+
518
+ // Interactive prompts
519
+ const questions = [];
520
+
521
+ if (!token) {
522
+ questions.push({
523
+ type: 'password',
524
+ name: 'token',
525
+ message: 'Enter your Spawn.wtf agent token:',
526
+ mask: '*',
527
+ validate: (input) => {
528
+ if (!input.startsWith('spwn_sk_')) {
529
+ return 'Token should start with spwn_sk_';
530
+ }
531
+ return true;
532
+ }
533
+ });
534
+ }
535
+
536
+ questions.push({
537
+ type: 'input',
538
+ name: 'agentName',
539
+ message: 'Agent name:',
540
+ default: agentName
541
+ });
542
+
543
+ if (!language) {
544
+ questions.push({
545
+ type: 'list',
546
+ name: 'language',
547
+ message: 'Choose your language:',
548
+ choices: [
549
+ { name: 'TypeScript / Node.js', value: 'typescript' },
550
+ { name: 'Python', value: 'python' }
551
+ ]
552
+ });
553
+ }
554
+
555
+ const answers = await inquirer.prompt(questions);
556
+
557
+ token = token || answers.token;
558
+ agentName = answers.agentName || agentName;
559
+ language = language || answers.language;
560
+
561
+ console.log('');
562
+
563
+ // Create files
564
+ const createSpinner = ora('Creating skill files...').start();
565
+ try {
566
+ const { skillDir } = await createSkillFiles(token, agentName, language);
567
+ createSpinner.succeed('Created skill files');
568
+ } catch (err) {
569
+ createSpinner.fail('Failed to create skill files');
570
+ console.error(err.message);
571
+ process.exit(1);
572
+ }
573
+
574
+ console.log('');
575
+ console.log(chalk.green.bold('✓ Spawn.wtf skill installed!'));
576
+ console.log('');
577
+
578
+ if (language === 'typescript') {
579
+ console.log(' Files created:');
580
+ console.log(dim(' spawn/SKILL.md'));
581
+ console.log(dim(' spawn/config.json'));
582
+ console.log(dim(' spawn/connect.js'));
583
+ console.log(dim(' spawn/package.json'));
584
+ console.log('');
585
+ console.log(' Next steps:');
586
+ console.log('');
587
+ console.log(' 1. ' + chalk.cyan('Install dependencies:'));
588
+ console.log(' ' + dim('cd spawn && npm install'));
589
+ console.log('');
590
+ console.log(' 2. ' + chalk.cyan('Run the connector:'));
591
+ console.log(' ' + dim('node spawn/connect.js'));
592
+ } else {
593
+ console.log(' Files created:');
594
+ console.log(dim(' spawn/SKILL.md'));
595
+ console.log(dim(' spawn/config.json'));
596
+ console.log(dim(' spawn/connect.py'));
597
+ console.log(dim(' spawn/spawn_sdk.py'));
598
+ console.log(dim(' spawn/requirements.txt'));
599
+ console.log('');
600
+ console.log(' Next steps:');
601
+ console.log('');
602
+ console.log(' 1. ' + chalk.cyan('Install dependencies:'));
603
+ console.log(' ' + dim('cd spawn && pip install -r requirements.txt'));
604
+ console.log('');
605
+ console.log(' 2. ' + chalk.cyan('Run the connector:'));
606
+ console.log(' ' + dim('python spawn/connect.py'));
607
+ }
608
+
609
+ console.log('');
610
+ console.log(' 3. ' + chalk.cyan('Open Spawn.wtf app') + ' - your agent should appear!');
611
+ console.log('');
612
+ console.log(' Docs: ' + pink('https://github.com/SpawnWTF/spawn'));
613
+ console.log('');
614
+ }
615
+
616
+ main().catch((err) => {
617
+ console.error('Error:', err.message);
618
+ process.exit(1);
619
+ });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "spawn-skill",
3
+ "version": "1.0.0",
4
+ "description": "Connect your AI agent to Spawn.wtf",
5
+ "bin": {
6
+ "spawn-skill": "./bin/cli.js"
7
+ },
8
+ "type": "module",
9
+ "files": [
10
+ "bin"
11
+ ],
12
+ "keywords": ["spawn", "ai", "agent", "claude", "monitoring"],
13
+ "author": "Spawn.wtf",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/SpawnWTF/spawn"
18
+ },
19
+ "dependencies": {
20
+ "chalk": "^5.3.0",
21
+ "inquirer": "^9.2.12",
22
+ "ora": "^8.0.1"
23
+ }
24
+ }