sam-coder-cli 1.0.10 → 1.0.12

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/MULTIPLAYER.md ADDED
@@ -0,0 +1,112 @@
1
+ # Multiplayer Mode for SAM-CODER
2
+
3
+ This guide explains how to use the Multiplayer Mode to collaborate with multiple AI agents in real-time.
4
+
5
+ ## Features
6
+
7
+ - Host or join multiplayer sessions
8
+ - Connect 2-4 AI agents in a single session
9
+ - Each agent can have a unique name and role
10
+ - Real-time chat and task coordination
11
+ - Shared work history and task assignment
12
+ - Support for different models/endpoints per agent
13
+
14
+ ## Quick Start
15
+
16
+ 1. First, install the required dependencies:
17
+ ```bash
18
+ npm install
19
+ ```
20
+
21
+ 2. Start the multiplayer server in a separate terminal:
22
+ ```bash
23
+ node bin/multiplayer-server.js
24
+ ```
25
+
26
+ 3. In a new terminal, start the first agent (host):
27
+ ```bash
28
+ node bin/agi-cli.js --multiplayer
29
+ ```
30
+ - Press Enter to create a new session
31
+ - Note the session ID shown in the console
32
+
33
+ 4. In another terminal, start additional agents (up to 3 more):
34
+ ```bash
35
+ node bin/agi-cli.js --multiplayer
36
+ ```
37
+ - Enter the session ID from step 3 to join
38
+
39
+ ## Multiplayer Commands
40
+
41
+ Once in a multiplayer session, you can use these commands:
42
+
43
+ - `/name [new_name]` - Set or show your agent's name
44
+ - `/role [role]` - Set or show your agent's role
45
+ - `/work [description]` - Update your work status
46
+ - `/task [agent_name] [task]` - Assign a task to another agent
47
+ - `/list` - List all agents in the session
48
+ - `/help` - Show available commands
49
+ - `/exit` - Leave the session
50
+
51
+ ## Agent Coordination
52
+
53
+ ### Work History
54
+ - All agents can see updates to the shared work history
55
+ - Use `/work` to update what you're working on
56
+ - The work history helps agents coordinate and avoid duplicate work
57
+
58
+ ### Task Assignment
59
+ - Use `/task` to assign specific tasks to other agents
60
+ - Example: `/task Coder1 Implement the login function`
61
+ - Assigned tasks appear in the target agent's console
62
+
63
+ ### Chat
64
+ - Type any message to send it to all agents in the session
65
+ - Use this to coordinate, ask questions, or share information
66
+
67
+ ## Advanced Usage
68
+
69
+ ### Custom Server URL
70
+ By default, the client connects to `ws://localhost:8080`. To use a different server:
71
+
72
+ ```bash
73
+ MP_SERVER_URL=ws://your-server-address:port node bin/agi-cli.js --multiplayer
74
+ ```
75
+
76
+ ### Different Models per Agent
77
+ Each agent can use a different model by setting the `MODEL` environment variable:
78
+
79
+ ```bash
80
+ MODEL=anthropic/claude-2 node bin/agi-cli.js --multiplayer
81
+ ```
82
+
83
+ ## Troubleshooting
84
+
85
+ ### Connection Issues
86
+ - Ensure the multiplayer server is running
87
+ - Check that the server URL is correct
88
+ - Verify that your firewall allows WebSocket connections on the specified port
89
+
90
+ ### Session Full
91
+ - Each session supports up to 4 agents
92
+ - Create a new session if you need more agents
93
+
94
+ ### Agent Names
95
+ - Agent names must be unique within a session
96
+ - Use `/name` to change your agent's name if needed
97
+
98
+ ## Example Workflow
99
+
100
+ 1. Agent 1 creates a session and becomes the host
101
+ 2. Agent 2 joins the session with `/join <session-id>`
102
+ 3. Agents assign roles:
103
+ - Agent 1: `/role Backend Developer`
104
+ - Agent 2: `/role Frontend Developer`
105
+ 4. They coordinate work:
106
+ - Agent 1: `/work Implementing user authentication API`
107
+ - Agent 2: `/work Creating login form UI`
108
+ 5. They can assign tasks to each other:
109
+ - Agent 1: `/task Frontend Add form validation`
110
+ - Agent 2: `/task Backend Add rate limiting to auth endpoint`
111
+
112
+ Enjoy collaborating with multiple AI agents in real-time!
package/bin/agi-cli.js CHANGED
@@ -8,6 +8,7 @@ const fs = require('fs').promises;
8
8
  const { exec } = require('child_process');
9
9
  const util = require('util');
10
10
  const execAsync = util.promisify(exec);
11
+ const MultiplayerMode = require('./multiplayer-mode');
11
12
 
12
13
  // Configuration
13
14
  const CONFIG_PATH = path.join(os.homedir(), '.sam-coder-config.json');
@@ -382,6 +383,22 @@ function extractJsonFromMarkdown(text) {
382
383
  // Call OpenRouter API with tool calling
383
384
  async function callOpenRouter(messages, currentModel, useJson = false) {
384
385
  const apiKey = OPENROUTER_API_KEY;
386
+ const isCustomEndpoint = API_BASE_URL !== 'https://openrouter.ai/api/v1';
387
+
388
+ let body = {
389
+ model: currentModel,
390
+ messages: messages,
391
+ };
392
+
393
+ // For standard OpenRouter calls that are not legacy, add tool parameters.
394
+ if (!isCustomEndpoint && !useJson) {
395
+ body.tools = tools;
396
+ body.tool_choice = 'auto';
397
+ }
398
+ // For custom endpoints (like vllm), ensure no tool-related parameters are sent.
399
+ else if (isCustomEndpoint) {
400
+ // The body is already clean for vLLM, containing only model and messages.
401
+ }
385
402
 
386
403
  try {
387
404
  const response = await fetch(`${API_BASE_URL}/chat/completions`, {
@@ -390,12 +407,7 @@ async function callOpenRouter(messages, currentModel, useJson = false) {
390
407
  'Authorization': `Bearer ${apiKey}`,
391
408
  'Content-Type': 'application/json'
392
409
  },
393
- body: JSON.stringify({
394
- model: currentModel,
395
- messages: messages,
396
- tools: useJson ? undefined : tools,
397
- tool_choice: useJson ? undefined : 'auto'
398
- })
410
+ body: JSON.stringify(body)
399
411
  });
400
412
 
401
413
  if (!response.ok) {
@@ -661,10 +673,20 @@ async function chat(rl, useToolCalling, initialModel) {
661
673
  });
662
674
  }
663
675
 
664
- function askForMode(rl) {
676
+ function askForMode(rl, includeMultiplayer = false) {
665
677
  return new Promise((resolve) => {
666
- rl.question('Select mode (1 for tool calling, 2 for function calling): ', (answer) => {
667
- resolve(answer.trim() === '1');
678
+ const prompt = includeMultiplayer
679
+ ? 'Select mode (1 for tool calling, 2 for function calling, 3 for multiplayer): '
680
+ : 'Select mode (1 for tool calling, 2 for function calling): ';
681
+
682
+ rl.question(prompt, (answer) => {
683
+ const choice = answer.trim();
684
+ if (includeMultiplayer) {
685
+ if (choice === '3') resolve('multiplayer');
686
+ else resolve(choice === '1' ? 'tool' : 'function');
687
+ } else {
688
+ resolve(choice === '1');
689
+ }
668
690
  });
669
691
  });
670
692
  }
@@ -737,20 +759,45 @@ async function start() {
737
759
  MODEL = config.MODEL || 'deepseek/deepseek-chat-v3-0324:free';
738
760
 
739
761
  OPENROUTER_API_KEY = config.OPENROUTER_API_KEY;
762
+
740
763
  if (config.isPro && config.customApiBase) {
741
764
  API_BASE_URL = config.customApiBase;
742
765
  console.log(`🚀 Using Pro Plan custom endpoint: ${API_BASE_URL}`);
743
766
  }
744
767
 
745
768
  ui.showHeader();
769
+
770
+ // Check for multiplayer mode flag
771
+ if (process.argv.includes('--multiplayer')) {
772
+ const multiplayerMode = new MultiplayerMode({
773
+ rl,
774
+ model: MODEL,
775
+ serverUrl: process.env.MP_SERVER_URL || 'ws://localhost:8080'
776
+ });
777
+ await multiplayerMode.start();
778
+ return;
779
+ }
780
+
781
+ // Standard single-agent mode
746
782
  console.log('Select Mode:');
747
783
  console.log('1. Tool Calling (for models that support it)');
748
784
  console.log('2. Function Calling (legacy)');
785
+ console.log('3. Multiplayer Mode (collaborate with other agents)');
749
786
 
750
- const useToolCalling = await askForMode(rl);
751
- ui.showResponse(`\nStarting in ${useToolCalling ? 'Tool Calling' : 'Function Calling'} mode...\n`);
787
+ const mode = await askForMode(rl, true);
752
788
 
753
- await chat(rl, useToolCalling, MODEL);
789
+ if (mode === 'multiplayer') {
790
+ const multiplayerMode = new MultiplayerMode({
791
+ rl,
792
+ model: MODEL,
793
+ serverUrl: process.env.MP_SERVER_URL || 'ws://localhost:8080'
794
+ });
795
+ await multiplayerMode.start();
796
+ } else {
797
+ const useToolCalling = mode === 'tool';
798
+ ui.showResponse(`\nStarting in ${useToolCalling ? 'Tool Calling' : 'Function Calling'} mode...\n`);
799
+ await chat(rl, useToolCalling, MODEL);
800
+ }
754
801
  } catch (error) {
755
802
  ui.showError(error);
756
803
  rl.close();
@@ -0,0 +1,222 @@
1
+ const WebSocket = require('ws');
2
+ const EventEmitter = require('events');
3
+ const readline = require('readline');
4
+ const chalk = require('chalk');
5
+ const { v4: uuidv4 } = require('uuid');
6
+
7
+ class MultiplayerClient extends EventEmitter {
8
+ constructor(options = {}) {
9
+ super();
10
+ this.serverUrl = options.serverUrl || 'ws://localhost:8080';
11
+ this.clientId = uuidv4();
12
+ this.sessionId = null;
13
+ this.agentName = options.agentName || `Agent-${Math.floor(Math.random() * 1000)}`;
14
+ this.model = options.model || 'default';
15
+ this.isHost = false;
16
+ this.clients = new Map();
17
+ this.workHistory = [];
18
+ this.ws = null;
19
+ this.rl = options.rl || readline.createInterface({
20
+ input: process.stdin,
21
+ output: process.stdout
22
+ });
23
+ }
24
+
25
+ connect() {
26
+ this.ws = new WebSocket(this.serverUrl);
27
+
28
+ this.ws.on('open', () => {
29
+ this.emit('connected');
30
+ });
31
+
32
+ this.ws.on('message', (data) => {
33
+ try {
34
+ const message = JSON.parse(data);
35
+ this.handleMessage(message);
36
+ } catch (error) {
37
+ console.error('Error parsing message:', error);
38
+ }
39
+ });
40
+
41
+ this.ws.on('close', () => {
42
+ this.emit('disconnected');
43
+ });
44
+
45
+ this.ws.on('error', (error) => {
46
+ this.emit('error', error);
47
+ });
48
+ }
49
+
50
+ handleMessage(message) {
51
+ switch (message.type) {
52
+ case 'session_joined':
53
+ this.handleSessionJoined(message);
54
+ break;
55
+ case 'agent_joined':
56
+ this.handleAgentJoined(message);
57
+ break;
58
+ case 'agent_left':
59
+ this.handleAgentLeft(message);
60
+ break;
61
+ case 'chat':
62
+ this.handleChatMessage(message);
63
+ break;
64
+ case 'work_update':
65
+ this.handleWorkUpdate(message);
66
+ break;
67
+ case 'task_assigned':
68
+ this.handleTaskAssigned(message);
69
+ break;
70
+ case 'error':
71
+ this.handleError(message);
72
+ break;
73
+ default:
74
+ this.emit('message', message);
75
+ }
76
+ }
77
+
78
+ handleSessionJoined(message) {
79
+ this.sessionId = message.sessionId;
80
+ this.isHost = message.clients.find(c => c.clientId === this.clientId)?.isHost || false;
81
+
82
+ // Update local client list
83
+ this.clients.clear();
84
+ message.clients.forEach(client => {
85
+ this.clients.set(client.clientId, client);
86
+ });
87
+
88
+ this.workHistory = message.workHistory || [];
89
+ this.emit('session_joined', message);
90
+ }
91
+
92
+ handleAgentJoined(message) {
93
+ this.clients.set(message.clientId, {
94
+ ...message.clientInfo,
95
+ clientId: message.clientId
96
+ });
97
+
98
+ if (message.clientId !== this.clientId) {
99
+ console.log(chalk.blue(`\n${message.clientInfo.name || 'Agent'} has joined the session`));
100
+ }
101
+ this.emit('agent_joined', message);
102
+ }
103
+
104
+ handleAgentLeft(message) {
105
+ const client = this.clients.get(message.clientId);
106
+ if (client) {
107
+ console.log(chalk.yellow(`\n${client.name || 'Agent'} has left the session`));
108
+ this.clients.delete(message.clientId);
109
+ }
110
+ this.emit('agent_left', message);
111
+ }
112
+
113
+ handleChatMessage(message) {
114
+ if (message.clientId === this.clientId) return;
115
+
116
+ const sender = this.clients.get(message.clientId) || { name: 'Unknown' };
117
+ console.log(chalk.cyan(`\n[${sender.name}]: ${message.content}`));
118
+ this.emit('chat', message);
119
+ }
120
+
121
+ handleWorkUpdate(message) {
122
+ this.workHistory.push({
123
+ ...message.work,
124
+ timestamp: message.timestamp,
125
+ clientId: message.clientId
126
+ });
127
+
128
+ const client = this.clients.get(message.clientId) || { name: 'Unknown' };
129
+ console.log(chalk.green(`\n[Work Update] ${client.name}: ${message.work.description}`));
130
+ this.emit('work_updated', message);
131
+ }
132
+
133
+ handleTaskAssigned(message) {
134
+ if (message.assignedTo === this.clientId) {
135
+ console.log(chalk.magenta(`\n[Task Assigned] ${message.task.description}`));
136
+ this.emit('task_assigned', message.task);
137
+ }
138
+ }
139
+
140
+ handleError(message) {
141
+ console.error(chalk.red(`\n[Error] ${message.message}`));
142
+ this.emit('error', new Error(message.message));
143
+ }
144
+
145
+ createSession() {
146
+ this.send({
147
+ type: 'create_session',
148
+ clientInfo: {
149
+ name: this.agentName,
150
+ model: this.model,
151
+ isHost: true
152
+ }
153
+ });
154
+ }
155
+
156
+ joinSession(sessionId) {
157
+ this.sessionId = sessionId;
158
+ this.send({
159
+ type: 'join_session',
160
+ sessionId,
161
+ clientInfo: {
162
+ name: this.agentName,
163
+ model: this.model
164
+ }
165
+ });
166
+ }
167
+
168
+ sendChatMessage(content) {
169
+ this.send({
170
+ type: 'chat',
171
+ sessionId: this.sessionId,
172
+ content,
173
+ timestamp: new Date().toISOString()
174
+ });
175
+ }
176
+
177
+ updateWork(work) {
178
+ const workUpdate = {
179
+ id: uuidv4(),
180
+ description: work.description,
181
+ status: work.status || 'in_progress',
182
+ timestamp: new Date().toISOString()
183
+ };
184
+
185
+ this.send({
186
+ type: 'work_update',
187
+ sessionId: this.sessionId,
188
+ work: workUpdate
189
+ });
190
+ }
191
+
192
+ assignTask(task, assigneeClientId) {
193
+ this.send({
194
+ type: 'task_assigned',
195
+ sessionId: this.sessionId,
196
+ task: {
197
+ ...task,
198
+ assignedBy: this.clientId,
199
+ assignedAt: new Date().toISOString()
200
+ },
201
+ assignedTo: assigneeClientId
202
+ });
203
+ }
204
+
205
+ send(message) {
206
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
207
+ this.ws.send(JSON.stringify({
208
+ ...message,
209
+ clientId: this.clientId,
210
+ timestamp: new Date().toISOString()
211
+ }));
212
+ }
213
+ }
214
+
215
+ disconnect() {
216
+ if (this.ws) {
217
+ this.ws.close();
218
+ }
219
+ }
220
+ }
221
+
222
+ module.exports = MultiplayerClient;
@@ -0,0 +1,228 @@
1
+ const MultiplayerClient = require('./multiplayer-client');
2
+ const chalk = require('chalk');
3
+ const readline = require('readline');
4
+ const { v4: uuidv4 } = require('uuid');
5
+
6
+ class MultiplayerMode {
7
+ constructor(options = {}) {
8
+ this.rl = options.rl || readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout
11
+ });
12
+
13
+ this.client = new MultiplayerClient({
14
+ rl: this.rl,
15
+ agentName: options.agentName || this.generateAgentName(),
16
+ model: options.model || 'default',
17
+ serverUrl: options.serverUrl
18
+ });
19
+
20
+ this.setupEventListeners();
21
+ this.agentRole = '';
22
+ this.isWorking = false;
23
+ }
24
+
25
+ generateAgentName() {
26
+ const adjectives = ['Swift', 'Clever', 'Wise', 'Agile', 'Smart', 'Quick', 'Bright', 'Sharp'];
27
+ const nouns = ['Coder', 'Thinker', 'Solver', 'Creator', 'Builder', 'Planner', 'Strategist'];
28
+ const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
29
+ const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
30
+ return `${randomAdjective}${randomNoun}${Math.floor(100 + Math.random() * 900)}`;
31
+ }
32
+
33
+ setupEventListeners() {
34
+ this.client.on('connected', () => {
35
+ console.log(chalk.green('Connected to multiplayer server'));
36
+ });
37
+
38
+ this.client.on('disconnected', () => {
39
+ console.log(chalk.yellow('Disconnected from multiplayer server'));
40
+ });
41
+
42
+ this.client.on('session_joined', (data) => {
43
+ console.log(chalk.green(`\nJoined session: ${data.sessionId}`));
44
+ console.log(chalk.blue(`You are: ${this.client.agentName}`));
45
+ console.log(chalk.blue(`Model: ${this.client.model}`));
46
+ console.log(chalk.blue(`Clients in session: ${Array.from(this.client.clients.values()).map(c => c.name).join(', ')}`));
47
+
48
+ if (this.client.isHost) {
49
+ console.log(chalk.yellow('\nYou are the host. Type /help for available commands.'));
50
+ }
51
+
52
+ this.prompt();
53
+ });
54
+
55
+ this.client.on('agent_joined', (data) => {
56
+ console.log(chalk.blue(`\n${data.clientInfo.name} joined the session`));
57
+ this.prompt();
58
+ });
59
+
60
+ this.client.on('agent_left', (data) => {
61
+ const client = this.client.clients.get(data.clientId);
62
+ if (client) {
63
+ console.log(chalk.yellow(`\n${client.name} left the session`));
64
+ this.prompt();
65
+ }
66
+ });
67
+
68
+ this.client.on('chat', (message) => {
69
+ // Chat messages are already handled by the client
70
+ this.prompt();
71
+ });
72
+
73
+ this.client.on('work_updated', (data) => {
74
+ this.prompt();
75
+ });
76
+
77
+ this.client.on('task_assigned', (task) => {
78
+ console.log(chalk.magenta(`\n[New Task] ${task.description}`));
79
+ this.prompt();
80
+ });
81
+
82
+ this.client.on('error', (error) => {
83
+ console.error(chalk.red(`\nError: ${error.message}`));
84
+ this.prompt();
85
+ });
86
+ }
87
+
88
+ async start() {
89
+ this.client.connect();
90
+
91
+ this.rl.question(chalk.blue('Enter session ID to join or press Enter to create a new session: '), (sessionId) => {
92
+ if (sessionId && sessionId.trim()) {
93
+ this.client.joinSession(sessionId.trim());
94
+ } else {
95
+ console.log(chalk.yellow('Creating a new session...'));
96
+ this.client.createSession();
97
+ }
98
+ });
99
+ }
100
+
101
+ prompt() {
102
+ if (this.isWorking) return;
103
+
104
+ this.rl.question(chalk.blue('> '), async (input) => {
105
+ if (!input.trim()) {
106
+ this.prompt();
107
+ return;
108
+ }
109
+
110
+ // Handle commands
111
+ if (input.startsWith('/')) {
112
+ await this.handleCommand(input);
113
+ } else {
114
+ // Regular chat message
115
+ this.client.sendChatMessage(input);
116
+ this.prompt();
117
+ }
118
+ });
119
+ }
120
+
121
+ async handleCommand(command) {
122
+ const [cmd, ...args] = command.slice(1).split(' ');
123
+
124
+ switch (cmd.toLowerCase()) {
125
+ case 'help':
126
+ this.showHelp();
127
+ break;
128
+
129
+ case 'name':
130
+ if (args.length > 0) {
131
+ const newName = args.join(' ');
132
+ this.client.agentName = newName;
133
+ console.log(chalk.green(`Name changed to: ${newName}`));
134
+ // Notify others about the name change
135
+ this.client.sendChatMessage(`I am now known as ${newName}`);
136
+ } else {
137
+ console.log(chalk.yellow('Current name:'), this.client.agentName);
138
+ }
139
+ break;
140
+
141
+ case 'role':
142
+ if (args.length > 0) {
143
+ this.agentRole = args.join(' ');
144
+ console.log(chalk.green(`Role set to: ${this.agentRole}`));
145
+ this.client.sendChatMessage(`My role is: ${this.agentRole}`);
146
+ } else {
147
+ console.log(chalk.yellow('Current role:'), this.agentRole || 'Not set');
148
+ }
149
+ break;
150
+
151
+ case 'work':
152
+ if (args.length > 0) {
153
+ const workDescription = args.join(' ');
154
+ this.client.updateWork({
155
+ description: workDescription,
156
+ status: 'in_progress'
157
+ });
158
+ console.log(chalk.green('Work update sent'));
159
+ } else {
160
+ console.log(chalk.yellow('Please provide a work description'));
161
+ }
162
+ break;
163
+
164
+ case 'task':
165
+ if (args.length > 1) {
166
+ const [targetName, ...taskParts] = args;
167
+ const taskDescription = taskParts.join(' ');
168
+
169
+ // Find client by name
170
+ const targetClient = Array.from(this.client.clients.values())
171
+ .find(c => c.name.toLowerCase() === targetName.toLowerCase());
172
+
173
+ if (targetClient) {
174
+ this.client.assignTask({
175
+ description: taskDescription,
176
+ status: 'pending'
177
+ }, targetClient.clientId);
178
+ console.log(chalk.green(`Task assigned to ${targetClient.name}`));
179
+ } else {
180
+ console.log(chalk.red(`No agent found with name: ${targetName}`));
181
+ }
182
+ } else {
183
+ console.log(chalk.yellow('Usage: /task <agent_name> <task_description>'));
184
+ }
185
+ break;
186
+
187
+ case 'list':
188
+ this.listAgents();
189
+ break;
190
+
191
+ case 'exit':
192
+ console.log(chalk.yellow('Disconnecting...'));
193
+ this.client.disconnect();
194
+ process.exit(0);
195
+ break;
196
+
197
+ default:
198
+ console.log(chalk.red('Unknown command. Type /help for available commands.'));
199
+ }
200
+
201
+ this.prompt();
202
+ }
203
+
204
+ showHelp() {
205
+ console.log(chalk.cyan('\nMultiplayer Mode Commands:'));
206
+ console.log(' /help - Show this help message');
207
+ console.log(' /name [new_name] - Set or show your agent\'s name');
208
+ console.log(' /role [role] - Set or show your agent\'s role');
209
+ console.log(' /work [description] - Update your work status');
210
+ console.log(' /task [name] [task] - Assign a task to another agent');
211
+ console.log(' /list - List all agents in the session');
212
+ console.log(' /exit - Leave the session and exit\n');
213
+ }
214
+
215
+ listAgents() {
216
+ console.log(chalk.cyan('\nAgents in session:'));
217
+ let index = 1;
218
+ for (const [id, client] of this.client.clients) {
219
+ const roleInfo = client.role ? ` (${client.role})` : '';
220
+ const hostInfo = client.isHost ? ' [HOST]' : '';
221
+ console.log(` ${index}. ${chalk.green(client.name)}${roleInfo}${hostInfo}`);
222
+ index++;
223
+ }
224
+ console.log('');
225
+ }
226
+ }
227
+
228
+ module.exports = MultiplayerMode;
@@ -0,0 +1,160 @@
1
+ const WebSocket = require('ws');
2
+ const http = require('http');
3
+ const uuid = require('uuid');
4
+
5
+ class MultiplayerServer {
6
+ constructor(port = 8080) {
7
+ this.port = port;
8
+ this.sessions = new Map();
9
+ this.server = http.createServer();
10
+ this.wss = new WebSocket.Server({ server: this.server });
11
+ this.setupEventHandlers();
12
+ }
13
+
14
+ setupEventHandlers() {
15
+ this.wss.on('connection', (ws) => {
16
+ let clientId = uuid.v4();
17
+ let sessionId = null;
18
+ let clientInfo = {};
19
+
20
+ ws.on('message', (message) => {
21
+ try {
22
+ const data = JSON.parse(message);
23
+ this.handleMessage(ws, clientId, data);
24
+ } catch (error) {
25
+ console.error('Error parsing message:', error);
26
+ }
27
+ });
28
+
29
+ ws.on('close', () => {
30
+ if (sessionId && this.sessions.has(sessionId)) {
31
+ const session = this.sessions.get(sessionId);
32
+ delete session.clients[clientId];
33
+ this.broadcastToSession(sessionId, {
34
+ type: 'agent_left',
35
+ clientId,
36
+ clientInfo
37
+ });
38
+
39
+ if (Object.keys(session.clients).length === 0) {
40
+ this.sessions.delete(sessionId);
41
+ }
42
+ }
43
+ });
44
+ });
45
+ }
46
+
47
+ handleMessage(ws, clientId, data) {
48
+ switch (data.type) {
49
+ case 'create_session':
50
+ this.handleCreateSession(ws, clientId, data);
51
+ break;
52
+ case 'join_session':
53
+ this.handleJoinSession(ws, clientId, data);
54
+ break;
55
+ case 'update_work':
56
+ case 'chat':
57
+ case 'task_update':
58
+ this.broadcastToSession(data.sessionId, {
59
+ ...data,
60
+ clientId,
61
+ timestamp: new Date().toISOString()
62
+ });
63
+ break;
64
+ default:
65
+ console.log('Unknown message type:', data.type);
66
+ }
67
+ }
68
+
69
+ handleCreateSession(ws, clientId, data) {
70
+ const sessionId = uuid.v4();
71
+ this.sessions.set(sessionId, {
72
+ id: sessionId,
73
+ clients: {},
74
+ workHistory: [],
75
+ createdAt: new Date().toISOString()
76
+ });
77
+
78
+ this.handleJoinSession(ws, clientId, {
79
+ ...data,
80
+ sessionId,
81
+ isHost: true
82
+ });
83
+ }
84
+
85
+ handleJoinSession(ws, clientId, data) {
86
+ const { sessionId, clientInfo } = data;
87
+
88
+ if (!this.sessions.has(sessionId)) {
89
+ ws.send(JSON.stringify({
90
+ type: 'error',
91
+ message: 'Session not found'
92
+ }));
93
+ return;
94
+ }
95
+
96
+ const session = this.sessions.get(sessionId);
97
+
98
+ if (Object.keys(session.clients).length >= 4) {
99
+ ws.send(JSON.stringify({
100
+ type: 'error',
101
+ message: 'Session is full (max 4 agents)'
102
+ }));
103
+ return;
104
+ }
105
+
106
+ // Add client to session
107
+ session.clients[clientId] = {
108
+ ws,
109
+ clientId,
110
+ clientInfo: {
111
+ ...clientInfo,
112
+ joinedAt: new Date().toISOString(),
113
+ isHost: data.isHost || false
114
+ }
115
+ };
116
+
117
+ // Send welcome message with session info
118
+ ws.send(JSON.stringify({
119
+ type: 'session_joined',
120
+ sessionId,
121
+ clientId,
122
+ clients: Object.values(session.clients).map(c => c.clientInfo),
123
+ workHistory: session.workHistory
124
+ }));
125
+
126
+ // Notify other clients
127
+ this.broadcastToSession(sessionId, {
128
+ type: 'agent_joined',
129
+ clientId,
130
+ clientInfo: session.clients[clientId].clientInfo
131
+ }, clientId);
132
+ }
133
+
134
+ broadcastToSession(sessionId, message, excludeClientId = null) {
135
+ if (!this.sessions.has(sessionId)) return;
136
+
137
+ const session = this.sessions.get(sessionId);
138
+ const messageStr = JSON.stringify(message);
139
+
140
+ Object.entries(session.clients).forEach(([id, client]) => {
141
+ if (id !== excludeClientId && client.ws.readyState === WebSocket.OPEN) {
142
+ client.ws.send(messageStr);
143
+ }
144
+ });
145
+ }
146
+
147
+ start() {
148
+ this.server.listen(this.port, () => {
149
+ console.log(`Multiplayer server running on ws://localhost:${this.port}`);
150
+ });
151
+ }
152
+ }
153
+
154
+ // Start server if run directly
155
+ if (require.main === module) {
156
+ const server = new MultiplayerServer();
157
+ server.start();
158
+ }
159
+
160
+ module.exports = MultiplayerServer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sam-coder-cli",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "SAM-CODER: An animated command-line AI assistant with agency capabilities.",
5
5
  "main": "bin/agi-cli.js",
6
6
  "bin": {
@@ -20,7 +20,10 @@
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
22
  "chalk": "^4.1.2",
23
- "ora": "^5.4.1"
23
+ "node-fetch": "^2.6.7",
24
+ "ora": "^5.4.1",
25
+ "uuid": "^9.0.0",
26
+ "ws": "^8.18.3"
24
27
  },
25
28
  "devDependencies": {
26
29
  "eslint": "^7.27.0"