325 lines
13 KiB
JavaScript
325 lines
13 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* P2P Swarm V2 CLI Commands
|
|
*
|
|
* Production-grade P2P swarm coordination with:
|
|
* - Ed25519/X25519 cryptography
|
|
* - GunDB relay coordination
|
|
* - Task execution with claim resolution
|
|
* - Heartbeat-based liveness
|
|
* - Verified member registry
|
|
*/
|
|
import { Command } from 'commander';
|
|
import { createP2PSwarmV2 } from '../../swarm/p2p-swarm-v2.js';
|
|
// Global swarm instance
|
|
let swarmInstance = null;
|
|
export function createSwarmCommand() {
|
|
const swarm = new Command('swarm')
|
|
.description('P2P Swarm V2 - Production-grade decentralized coordination');
|
|
// Initialize and connect to swarm
|
|
swarm
|
|
.command('connect')
|
|
.description('Connect to a P2P swarm')
|
|
.option('-a, --agent-id <id>', 'Agent ID (default: auto-generated)')
|
|
.option('-k, --swarm-key <key>', 'Swarm key for joining existing swarm')
|
|
.option('-e, --executor', 'Enable task executor capability')
|
|
.option('-j, --json', 'Output as JSON')
|
|
.action(async (options) => {
|
|
try {
|
|
const agentId = options.agentId || `agent-${Date.now().toString(36)}`;
|
|
console.log(`Connecting to P2P Swarm...`);
|
|
swarmInstance = await createP2PSwarmV2(agentId, options.swarmKey);
|
|
if (options.executor) {
|
|
swarmInstance.startTaskExecutor();
|
|
}
|
|
const status = swarmInstance.getStatus();
|
|
if (options.json) {
|
|
console.log(JSON.stringify({
|
|
connected: status.connected,
|
|
agentId: status.agentId,
|
|
swarmId: status.swarmId,
|
|
swarmKey: swarmInstance.getSwarmKey(),
|
|
relays: status.relays,
|
|
executor: options.executor || false,
|
|
}, null, 2));
|
|
}
|
|
else {
|
|
console.log(`\n✅ Connected to P2P Swarm V2`);
|
|
console.log(` Agent ID: ${status.agentId}`);
|
|
console.log(` Swarm ID: ${status.swarmId}`);
|
|
console.log(` Swarm Key: ${swarmInstance.getSwarmKey().slice(0, 20)}...`);
|
|
console.log(` Relays: ${status.relays.healthy}/${status.relays.total} healthy`);
|
|
console.log(` Executor: ${options.executor ? 'Enabled' : 'Disabled'}`);
|
|
console.log(`\nShare the swarm key to invite others:`);
|
|
console.log(` npx agentic-flow@alpha swarm connect -k "${swarmInstance.getSwarmKey()}"`);
|
|
}
|
|
// Keep process alive for background operations
|
|
if (options.executor) {
|
|
console.log(`\nTask executor running. Press Ctrl+C to stop.`);
|
|
process.on('SIGINT', () => {
|
|
swarmInstance?.disconnect();
|
|
process.exit(0);
|
|
});
|
|
// Keep alive
|
|
await new Promise(() => { });
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
// Get swarm status
|
|
swarm
|
|
.command('status')
|
|
.description('Get current swarm status')
|
|
.option('-j, --json', 'Output as JSON')
|
|
.action(async (options) => {
|
|
if (!swarmInstance) {
|
|
console.error('Not connected to swarm. Run: swarm connect');
|
|
process.exit(1);
|
|
}
|
|
const status = swarmInstance.getStatus();
|
|
const liveMembers = swarmInstance.getLiveMembers();
|
|
if (options.json) {
|
|
console.log(JSON.stringify({
|
|
...status,
|
|
liveMembers,
|
|
liveMemberCount: swarmInstance.getLiveMemberCount(),
|
|
}, null, 2));
|
|
}
|
|
else {
|
|
console.log(`\n📊 P2P Swarm Status`);
|
|
console.log(` Connected: ${status.connected}`);
|
|
console.log(` Agent ID: ${status.agentId}`);
|
|
console.log(` Swarm ID: ${status.swarmId}`);
|
|
console.log(` Relays: ${status.relays.healthy}/${status.relays.total} healthy`);
|
|
console.log(` Avg Latency: ${status.relays.avgLatency.toFixed(0)}ms`);
|
|
console.log(`\n👥 Live Members: ${swarmInstance.getLiveMemberCount()}`);
|
|
for (const member of liveMembers) {
|
|
const status = member.isAlive ? '🟢' : '🔴';
|
|
const age = Math.round((Date.now() - member.lastSeen) / 1000);
|
|
console.log(` ${status} ${member.agentId} (${member.capabilities.join(', ')}) - ${age}s ago`);
|
|
}
|
|
}
|
|
});
|
|
// List members
|
|
swarm
|
|
.command('members')
|
|
.description('List swarm members')
|
|
.option('-a, --all', 'Include offline members')
|
|
.option('-j, --json', 'Output as JSON')
|
|
.action(async (options) => {
|
|
if (!swarmInstance) {
|
|
console.error('Not connected to swarm. Run: swarm connect');
|
|
process.exit(1);
|
|
}
|
|
const members = swarmInstance.getLiveMembers();
|
|
const filtered = options.all ? members : members.filter(m => m.isAlive);
|
|
if (options.json) {
|
|
console.log(JSON.stringify(filtered, null, 2));
|
|
}
|
|
else {
|
|
console.log(`\n👥 Swarm Members (${filtered.length})`);
|
|
for (const member of filtered) {
|
|
const status = member.isAlive ? '🟢 Online' : '🔴 Offline';
|
|
const age = Math.round((Date.now() - member.lastSeen) / 1000);
|
|
console.log(`\n ${member.agentId}`);
|
|
console.log(` Status: ${status}`);
|
|
console.log(` Capabilities: ${member.capabilities.join(', ')}`);
|
|
console.log(` Last Seen: ${age}s ago`);
|
|
}
|
|
}
|
|
});
|
|
// Publish message to topic
|
|
swarm
|
|
.command('publish <topic> <message>')
|
|
.description('Publish a message to a topic')
|
|
.option('-j, --json', 'Parse message as JSON')
|
|
.action(async (topic, message, options) => {
|
|
if (!swarmInstance) {
|
|
console.error('Not connected to swarm. Run: swarm connect');
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
const payload = options.json ? JSON.parse(message) : { text: message };
|
|
const messageId = await swarmInstance.publish(topic, payload);
|
|
console.log(`✅ Published to ${topic}`);
|
|
console.log(` Message ID: ${messageId}`);
|
|
}
|
|
catch (error) {
|
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
// Subscribe to topic
|
|
swarm
|
|
.command('subscribe <topic>')
|
|
.description('Subscribe to a topic and print messages')
|
|
.action(async (topic) => {
|
|
if (!swarmInstance) {
|
|
console.error('Not connected to swarm. Run: swarm connect');
|
|
process.exit(1);
|
|
}
|
|
console.log(`📡 Subscribed to ${topic}. Waiting for messages...`);
|
|
swarmInstance.subscribe(topic, (data, from) => {
|
|
console.log(`\n[${new Date().toISOString()}] From: ${from}`);
|
|
console.log(JSON.stringify(data, null, 2));
|
|
});
|
|
// Keep alive
|
|
process.on('SIGINT', () => {
|
|
swarmInstance?.disconnect();
|
|
process.exit(0);
|
|
});
|
|
await new Promise(() => { });
|
|
});
|
|
// Sync Q-table
|
|
swarm
|
|
.command('sync-qtable')
|
|
.description('Sync a Q-table to the swarm')
|
|
.requiredOption('-f, --file <path>', 'Path to Q-table JSON file')
|
|
.action(async (options) => {
|
|
if (!swarmInstance) {
|
|
console.error('Not connected to swarm. Run: swarm connect');
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
const fs = await import('fs');
|
|
const data = fs.readFileSync(options.file, 'utf-8');
|
|
const qTable = JSON.parse(data);
|
|
if (!Array.isArray(qTable) || !Array.isArray(qTable[0])) {
|
|
throw new Error('Q-table must be a 2D array');
|
|
}
|
|
const pointer = await swarmInstance.syncQTable(qTable);
|
|
console.log(`✅ Q-table synced`);
|
|
console.log(` CID: ${pointer.cid}`);
|
|
console.log(` Dimensions: ${pointer.dimensions}`);
|
|
console.log(` Checksum: ${pointer.checksum}`);
|
|
}
|
|
catch (error) {
|
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
// Sync memory vectors
|
|
swarm
|
|
.command('sync-memory')
|
|
.description('Sync memory vectors to the swarm')
|
|
.requiredOption('-f, --file <path>', 'Path to vectors JSON file')
|
|
.option('-n, --namespace <ns>', 'Memory namespace', 'default')
|
|
.action(async (options) => {
|
|
if (!swarmInstance) {
|
|
console.error('Not connected to swarm. Run: swarm connect');
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
const fs = await import('fs');
|
|
const data = fs.readFileSync(options.file, 'utf-8');
|
|
const vectors = JSON.parse(data);
|
|
if (!Array.isArray(vectors) || !Array.isArray(vectors[0])) {
|
|
throw new Error('Vectors must be a 2D array');
|
|
}
|
|
const pointer = await swarmInstance.syncMemory(vectors, options.namespace);
|
|
console.log(`✅ Memory vectors synced`);
|
|
console.log(` CID: ${pointer.cid}`);
|
|
console.log(` Namespace: ${options.namespace}`);
|
|
console.log(` Dimensions: ${pointer.dimensions}`);
|
|
}
|
|
catch (error) {
|
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
// Submit task
|
|
swarm
|
|
.command('submit-task')
|
|
.description('Submit a task for execution')
|
|
.requiredOption('-m, --module <cid>', 'WASM module CID')
|
|
.requiredOption('-i, --input <cid>', 'Input data CID')
|
|
.option('-e, --entrypoint <fn>', 'Entrypoint function', 'main')
|
|
.option('-f, --fuel <n>', 'Fuel limit', '1000000')
|
|
.option('-M, --memory <mb>', 'Memory limit in MB', '64')
|
|
.option('-t, --timeout <ms>', 'Timeout in ms', '30000')
|
|
.action(async (options) => {
|
|
if (!swarmInstance) {
|
|
console.error('Not connected to swarm. Run: swarm connect');
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
const taskId = await swarmInstance.submitTask({
|
|
taskId: `task-${Date.now().toString(36)}`,
|
|
moduleCID: options.module,
|
|
inputCID: options.input,
|
|
entrypoint: options.entrypoint,
|
|
outputSchemaHash: '',
|
|
budgets: {
|
|
fuelLimit: parseInt(options.fuel),
|
|
memoryMB: parseInt(options.memory),
|
|
timeoutMs: parseInt(options.timeout),
|
|
},
|
|
});
|
|
console.log(`✅ Task submitted`);
|
|
console.log(` Message ID: ${taskId}`);
|
|
console.log(` Module: ${options.module}`);
|
|
console.log(` Input: ${options.input}`);
|
|
}
|
|
catch (error) {
|
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
// Start executor
|
|
swarm
|
|
.command('executor')
|
|
.description('Start task executor daemon')
|
|
.action(async () => {
|
|
if (!swarmInstance) {
|
|
console.error('Not connected to swarm. Run: swarm connect');
|
|
process.exit(1);
|
|
}
|
|
swarmInstance.startTaskExecutor();
|
|
console.log(`✅ Task executor started`);
|
|
console.log(` Listening for tasks...`);
|
|
process.on('SIGINT', () => {
|
|
swarmInstance?.stopTaskExecutor();
|
|
swarmInstance?.disconnect();
|
|
process.exit(0);
|
|
});
|
|
await new Promise(() => { });
|
|
});
|
|
// Disconnect
|
|
swarm
|
|
.command('disconnect')
|
|
.description('Disconnect from the swarm')
|
|
.action(async () => {
|
|
if (swarmInstance) {
|
|
swarmInstance.disconnect();
|
|
swarmInstance = null;
|
|
console.log('✅ Disconnected from swarm');
|
|
}
|
|
else {
|
|
console.log('Not connected to swarm');
|
|
}
|
|
});
|
|
// Generate new swarm key
|
|
swarm
|
|
.command('keygen')
|
|
.description('Generate a new swarm key')
|
|
.action(async () => {
|
|
const crypto = await import('crypto');
|
|
const key = crypto.randomBytes(32).toString('base64');
|
|
console.log(`🔑 New Swarm Key:`);
|
|
console.log(` ${key}`);
|
|
console.log(`\nUse with:`);
|
|
console.log(` npx agentic-flow@alpha swarm connect -k "${key}"`);
|
|
});
|
|
return swarm;
|
|
}
|
|
// Export singleton getter for hooks/MCP integration
|
|
export function getSwarmInstance() {
|
|
return swarmInstance;
|
|
}
|
|
export function setSwarmInstance(instance) {
|
|
swarmInstance = instance;
|
|
}
|
|
//# sourceMappingURL=swarm.js.map
|