spawn-skill 1.0.1 → 1.1.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 +2 -0
  2. package/bin/cli.js +279 -30
  3. package/package.json +1 -1
package/bin/CLAUDE.md CHANGED
@@ -7,5 +7,7 @@
7
7
 
8
8
  | ID | Time | T | Title | Read |
9
9
  |----|------|---|-------|------|
10
+ | #905 | 9:07 PM | 🔵 | Spawn-Skill CLI Tool Complete Implementation | ~576 |
11
+ | #894 | 8:55 PM | 🔵 | spawn-skill CLI contains embedded skill documentation | ~435 |
10
12
  | #794 | 11:11 AM | ✅ | Simplified spawn-skill CLI package.json generation to use direct ws dependency | ~353 |
11
13
  </claude-mem-context>
package/bin/cli.js CHANGED
@@ -233,18 +233,15 @@ function connect() {
233
233
  });
234
234
 
235
235
  ws.on('open', () => {
236
- console.log('Connected to relay, authenticating...');
237
- send('auth', { token: TOKEN, name: NAME });
236
+ console.log('Connected to Spawn.wtf!');
237
+ // Auth happens via token in header - just start working
238
+ sendText(\`\${NAME} is online and ready.\`);
239
+ updateStatus('idle', 'Ready for commands');
238
240
  });
239
241
 
240
242
  ws.on('message', (data) => {
241
243
  const msg = JSON.parse(data.toString());
242
-
243
- if (msg.type === 'auth_success') {
244
- console.log('Authenticated! Agent is online.');
245
- sendText(\`\${NAME} is online and ready.\`);
246
- updateStatus('idle', 'Ready for commands');
247
- }
244
+ console.log('Received:', msg.type);
248
245
 
249
246
  if (msg.type === 'message') {
250
247
  console.log('Message from app:', msg.payload);
@@ -341,6 +338,172 @@ if __name__ == '__main__':
341
338
  `;
342
339
  }
343
340
 
341
+ function getMCPServer(token) {
342
+ return `#!/usr/bin/env node
343
+ /**
344
+ * Spawn.wtf MCP Server
345
+ * Bridges Spawn.wtf with Claude Code
346
+ */
347
+
348
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
349
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
350
+ import {
351
+ CallToolRequestSchema,
352
+ ListToolsRequestSchema,
353
+ } from '@modelcontextprotocol/sdk/types.js';
354
+ import WebSocket from 'ws';
355
+
356
+ const RELAY_URL = 'wss://spawn-relay.ngvsqdjj5r.workers.dev/v1/agent';
357
+ const TOKEN = process.env.SPAWN_TOKEN || '${token}';
358
+
359
+ const messageQueue = [];
360
+ let ws = null;
361
+ let connected = false;
362
+ let connectionError = null;
363
+
364
+ function connectToRelay() {
365
+ if (!TOKEN) {
366
+ connectionError = 'SPAWN_TOKEN not set';
367
+ return;
368
+ }
369
+
370
+ ws = new WebSocket(RELAY_URL, {
371
+ headers: { 'Authorization': \\\`Bearer \\\${TOKEN}\\\` }
372
+ });
373
+
374
+ ws.on('open', () => {
375
+ connected = true;
376
+ connectionError = null;
377
+ sendMessage('status_update', { status: 'idle', label: 'Ready' });
378
+ });
379
+
380
+ ws.on('message', (data) => {
381
+ try {
382
+ const msg = JSON.parse(data.toString());
383
+ if (msg.type === 'message') {
384
+ messageQueue.push({
385
+ id: msg.id,
386
+ timestamp: msg.ts || Date.now(),
387
+ text: msg.payload?.text || '',
388
+ from: msg.payload?.from || 'user'
389
+ });
390
+ }
391
+ } catch (e) {}
392
+ });
393
+
394
+ ws.on('close', () => {
395
+ connected = false;
396
+ setTimeout(connectToRelay, 5000);
397
+ });
398
+
399
+ ws.on('error', (err) => {
400
+ connectionError = err.message;
401
+ });
402
+ }
403
+
404
+ function sendMessage(type, payload) {
405
+ if (ws?.readyState === WebSocket.OPEN) {
406
+ ws.send(JSON.stringify({
407
+ type,
408
+ id: \\\`msg_\\\${Date.now()}\\\`,
409
+ ts: Date.now(),
410
+ payload
411
+ }));
412
+ }
413
+ }
414
+
415
+ setInterval(() => {
416
+ if (ws?.readyState === WebSocket.OPEN) {
417
+ sendMessage('ping', {});
418
+ }
419
+ }, 30000);
420
+
421
+ connectToRelay();
422
+
423
+ const server = new Server(
424
+ { name: 'spawn', version: '1.0.0' },
425
+ { capabilities: { tools: {} } }
426
+ );
427
+
428
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
429
+ tools: [
430
+ {
431
+ name: 'spawn_check_messages',
432
+ description: 'Check for new messages from the Spawn.wtf iOS app',
433
+ inputSchema: { type: 'object', properties: {}, required: [] }
434
+ },
435
+ {
436
+ name: 'spawn_respond',
437
+ description: 'Send a response back to the user through Spawn.wtf',
438
+ inputSchema: {
439
+ type: 'object',
440
+ properties: {
441
+ text: { type: 'string', description: 'Message to send' },
442
+ format: { type: 'string', enum: ['plain', 'markdown'], default: 'plain' }
443
+ },
444
+ required: ['text']
445
+ }
446
+ },
447
+ {
448
+ name: 'spawn_status',
449
+ description: 'Update your status in Spawn.wtf (idle/thinking/working)',
450
+ inputSchema: {
451
+ type: 'object',
452
+ properties: {
453
+ status: { type: 'string', enum: ['idle', 'thinking', 'working'] },
454
+ label: { type: 'string', description: 'Status label' }
455
+ },
456
+ required: ['status']
457
+ }
458
+ },
459
+ {
460
+ name: 'spawn_connection_info',
461
+ description: 'Get Spawn.wtf connection status',
462
+ inputSchema: { type: 'object', properties: {}, required: [] }
463
+ }
464
+ ]
465
+ }));
466
+
467
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
468
+ const { name, arguments: args } = request.params;
469
+
470
+ switch (name) {
471
+ case 'spawn_check_messages': {
472
+ const messages = [...messageQueue];
473
+ messageQueue.length = 0;
474
+ if (messages.length > 0) {
475
+ sendMessage('status_update', { status: 'thinking', label: 'Reading messages' });
476
+ }
477
+ return { content: [{ type: 'text', text: JSON.stringify({ messages, count: messages.length }) }] };
478
+ }
479
+ case 'spawn_respond': {
480
+ if (!connected) {
481
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Not connected' }) }] };
482
+ }
483
+ sendMessage('message', { content_type: 'text', text: args.text, format: args.format || 'plain' });
484
+ sendMessage('status_update', { status: 'idle', label: 'Ready' });
485
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true }) }] };
486
+ }
487
+ case 'spawn_status': {
488
+ if (!connected) {
489
+ return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: 'Not connected' }) }] };
490
+ }
491
+ sendMessage('status_update', { status: args.status, label: args.label });
492
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true }) }] };
493
+ }
494
+ case 'spawn_connection_info': {
495
+ return { content: [{ type: 'text', text: JSON.stringify({ connected, error: connectionError, pendingMessages: messageQueue.length }) }] };
496
+ }
497
+ default:
498
+ throw new Error(\\\`Unknown tool: \\\${name}\\\`);
499
+ }
500
+ });
501
+
502
+ const transport = new StdioServerTransport();
503
+ server.connect(transport);
504
+ `;
505
+ }
506
+
344
507
  function getPythonSDK() {
345
508
  return `"""
346
509
  Spawn.wtf Python SDK
@@ -376,16 +539,9 @@ class SpawnAgent:
376
539
  )
377
540
  self._connected = True
378
541
 
379
- # Send auth message
380
- await self._send({
381
- 'type': 'auth',
382
- 'id': f'auth_{int(asyncio.get_event_loop().time() * 1000)}',
383
- 'ts': int(asyncio.get_event_loop().time() * 1000),
384
- 'payload': {
385
- 'token': self.token,
386
- 'name': self.name
387
- }
388
- })
542
+ # Auth happens via token header - trigger connect immediately
543
+ if 'connect' in self._handlers:
544
+ await self._handlers['connect']()
389
545
 
390
546
  # Start message loop
391
547
  await self._message_loop()
@@ -402,10 +558,7 @@ class SpawnAgent:
402
558
  data = json.loads(message)
403
559
  msg_type = data.get('type', '')
404
560
 
405
- if msg_type == 'auth_success':
406
- if 'connect' in self._handlers:
407
- await self._handlers['connect']()
408
- elif msg_type == 'message':
561
+ if msg_type == 'message':
409
562
  if 'message' in self._handlers:
410
563
  await self._handlers['message'](data)
411
564
  elif msg_type == 'pong':
@@ -486,7 +639,7 @@ class SpawnAgent:
486
639
  `;
487
640
  }
488
641
 
489
- async function createSkillFiles(token, agentName, language) {
642
+ async function createSkillFiles(token, agentName, language, claudeCode = false) {
490
643
  const skillDir = path.join(process.cwd(), 'spawn');
491
644
 
492
645
  // Create spawn directory
@@ -502,7 +655,8 @@ async function createSkillFiles(token, agentName, language) {
502
655
  name: agentName,
503
656
  token: token,
504
657
  relay: 'wss://spawn-relay.ngvsqdjj5r.workers.dev/v1/agent',
505
- language: language
658
+ language: language,
659
+ claudeCode: claudeCode
506
660
  };
507
661
  fs.writeFileSync(path.join(skillDir, 'config.json'), JSON.stringify(config, null, 2));
508
662
 
@@ -527,7 +681,45 @@ async function createSkillFiles(token, agentName, language) {
527
681
  fs.writeFileSync(path.join(skillDir, 'requirements.txt'), 'websockets>=12.0\n');
528
682
  }
529
683
 
530
- return { skillDir, language };
684
+ // Claude Code MCP integration
685
+ if (claudeCode) {
686
+ const mcpDir = path.join(skillDir, 'mcp');
687
+ if (!fs.existsSync(mcpDir)) {
688
+ fs.mkdirSync(mcpDir, { recursive: true });
689
+ }
690
+
691
+ // Create MCP server
692
+ fs.writeFileSync(path.join(mcpDir, 'server.js'), getMCPServer(token));
693
+
694
+ // Create MCP package.json
695
+ const mcpPackageJson = {
696
+ name: "spawn-mcp-server",
697
+ version: "1.0.0",
698
+ type: "module",
699
+ dependencies: {
700
+ "@modelcontextprotocol/sdk": "^1.0.0",
701
+ "ws": "^8.16.0"
702
+ }
703
+ };
704
+ fs.writeFileSync(path.join(mcpDir, 'package.json'), JSON.stringify(mcpPackageJson, null, 2));
705
+
706
+ // Create Claude Code settings snippet
707
+ const mcpFullPath = path.resolve(mcpDir, 'server.js');
708
+ const settingsSnippet = {
709
+ mcpServers: {
710
+ spawn: {
711
+ command: "node",
712
+ args: [mcpFullPath],
713
+ env: {
714
+ SPAWN_TOKEN: token
715
+ }
716
+ }
717
+ }
718
+ };
719
+ fs.writeFileSync(path.join(mcpDir, 'claude-settings.json'), JSON.stringify(settingsSnippet, null, 2));
720
+ }
721
+
722
+ return { skillDir, language, claudeCode };
531
723
  }
532
724
 
533
725
  async function main() {
@@ -591,18 +783,26 @@ async function main() {
591
783
  });
592
784
  }
593
785
 
786
+ questions.push({
787
+ type: 'confirm',
788
+ name: 'claudeCode',
789
+ message: 'Add Claude Code integration? (MCP server for chatting via phone)',
790
+ default: true
791
+ });
792
+
594
793
  const answers = await inquirer.prompt(questions);
595
794
 
596
795
  token = token || answers.token;
597
796
  agentName = answers.agentName || agentName;
598
797
  language = language || answers.language;
798
+ const claudeCode = answers.claudeCode;
599
799
 
600
800
  console.log('');
601
801
 
602
802
  // Create files
603
803
  const createSpinner = ora('Creating skill files...').start();
604
804
  try {
605
- const { skillDir } = await createSkillFiles(token, agentName, language);
805
+ const { skillDir } = await createSkillFiles(token, agentName, language, claudeCode);
606
806
  createSpinner.succeed('Created skill files');
607
807
  } catch (err) {
608
808
  createSpinner.fail('Failed to create skill files');
@@ -610,6 +810,18 @@ async function main() {
610
810
  process.exit(1);
611
811
  }
612
812
 
813
+ // Install MCP dependencies if needed
814
+ if (claudeCode) {
815
+ const mcpSpinner = ora('Installing MCP dependencies...').start();
816
+ try {
817
+ const { execSync } = await import('child_process');
818
+ execSync('npm install', { cwd: path.join(process.cwd(), 'spawn', 'mcp'), stdio: 'pipe' });
819
+ mcpSpinner.succeed('Installed MCP dependencies');
820
+ } catch (err) {
821
+ mcpSpinner.warn('Run "cd spawn/mcp && npm install" to finish MCP setup');
822
+ }
823
+ }
824
+
613
825
  console.log('');
614
826
  console.log(chalk.green.bold('✓ Spawn.wtf skill installed!'));
615
827
  console.log('');
@@ -620,14 +832,20 @@ async function main() {
620
832
  console.log(dim(' spawn/config.json'));
621
833
  console.log(dim(' spawn/connect.js'));
622
834
  console.log(dim(' spawn/package.json'));
835
+ if (claudeCode) {
836
+ console.log(dim(' spawn/mcp/server.js'));
837
+ console.log(dim(' spawn/mcp/package.json'));
838
+ console.log(dim(' spawn/mcp/claude-settings.json'));
839
+ }
623
840
  console.log('');
624
841
  console.log(' Next steps:');
625
842
  console.log('');
626
843
  console.log(' 1. ' + chalk.cyan('Install dependencies:'));
627
- console.log(' ' + dim('cd spawn && npm install'));
844
+ console.log(' ' + dim('cd spawn'));
845
+ console.log(' ' + dim('npm install'));
628
846
  console.log('');
629
847
  console.log(' 2. ' + chalk.cyan('Run the connector:'));
630
- console.log(' ' + dim('node spawn/connect.js'));
848
+ console.log(' ' + dim('node connect.js'));
631
849
  } else {
632
850
  console.log(' Files created:');
633
851
  console.log(dim(' spawn/SKILL.md'));
@@ -635,18 +853,49 @@ async function main() {
635
853
  console.log(dim(' spawn/connect.py'));
636
854
  console.log(dim(' spawn/spawn_sdk.py'));
637
855
  console.log(dim(' spawn/requirements.txt'));
856
+ if (claudeCode) {
857
+ console.log(dim(' spawn/mcp/server.js'));
858
+ console.log(dim(' spawn/mcp/package.json'));
859
+ console.log(dim(' spawn/mcp/claude-settings.json'));
860
+ }
638
861
  console.log('');
639
862
  console.log(' Next steps:');
640
863
  console.log('');
641
864
  console.log(' 1. ' + chalk.cyan('Install dependencies:'));
642
- console.log(' ' + dim('cd spawn && pip install -r requirements.txt'));
865
+ console.log(' ' + dim('cd spawn'));
866
+ console.log(' ' + dim('pip install -r requirements.txt'));
643
867
  console.log('');
644
868
  console.log(' 2. ' + chalk.cyan('Run the connector:'));
645
- console.log(' ' + dim('python spawn/connect.py'));
869
+ console.log(' ' + dim('python connect.py'));
646
870
  }
647
871
 
648
872
  console.log('');
649
873
  console.log(' 3. ' + chalk.cyan('Open Spawn.wtf app') + ' - your agent should appear!');
874
+
875
+ if (claudeCode) {
876
+ console.log('');
877
+ console.log(pink(' ═══ Claude Code Integration ═══'));
878
+ console.log('');
879
+ console.log(' Add to your ' + chalk.cyan('.claude/settings.json') + ':');
880
+ console.log('');
881
+ const mcpPath = path.resolve(process.cwd(), 'spawn', 'mcp', 'server.js');
882
+ console.log(dim(' {'));
883
+ console.log(dim(' "mcpServers": {'));
884
+ console.log(dim(' "spawn": {'));
885
+ console.log(dim(' "command": "node",'));
886
+ console.log(dim(` "args": ["${mcpPath}"],`));
887
+ console.log(dim(' "env": { "SPAWN_TOKEN": "' + token.slice(0, 12) + '..." }'));
888
+ console.log(dim(' }'));
889
+ console.log(dim(' }'));
890
+ console.log(dim(' }'));
891
+ console.log('');
892
+ console.log(' Or copy from: ' + dim('spawn/mcp/claude-settings.json'));
893
+ console.log('');
894
+ console.log(' Then restart Claude Code and ask:');
895
+ console.log(' ' + chalk.white('"Check Spawn messages"'));
896
+ console.log(' ' + chalk.white('"Respond: Hello from Claude!"'));
897
+ }
898
+
650
899
  console.log('');
651
900
  console.log(' Docs: ' + pink('https://github.com/SpawnWTF/spawn'));
652
901
  console.log('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spawn-skill",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Connect your AI agent to Spawn.wtf",
5
5
  "bin": {
6
6
  "spawn-skill": "./bin/cli.js"