agent-world 0.4.0 → 0.4.6

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 CHANGED
@@ -107,14 +107,23 @@ Each Agent World has a collection of agents that can communicate through a share
107
107
 
108
108
  ### Quick Start
109
109
 
110
- **Option 1: CLI Interface**
110
+ **Option 1: Web Interface**
111
111
  ```bash
112
- npx agent-world
112
+ npx agent-world-server
113
113
  ```
114
114
 
115
- **Option 2: Web Interface**
115
+ **Option 2: CLI Interface**
116
+ 1. Interactive Mode
117
+ ```bash
118
+ npx agent-world
119
+ ```
120
+ 2. Command Mode
121
+ ```bash
122
+ npx agent-world -w default-world "hi"
123
+ ```
124
+ 3. Pipeline Mode
116
125
  ```bash
117
- npx agent-world --server
126
+ echo "hi" | npx agent-world -w default-world
118
127
  ```
119
128
 
120
129
  ## Project Structure (npm workspaces)
package/dist/cli/index.js CHANGED
@@ -38,21 +38,12 @@ import path from 'path';
38
38
  import { fileURLToPath } from 'url';
39
39
  import { program } from 'commander';
40
40
  import readline from 'readline';
41
- import { listWorlds, subscribeWorld, createCategoryLogger, LLMProvider, initializeLogger, enableStreaming, disableStreaming } from '../core/index.js';
41
+ import { listWorlds, subscribeWorld, createCategoryLogger, LLMProvider, enableStreaming, disableStreaming } from '../core/index.js';
42
+ import { getDefaultRootPath } from '../core/storage-factory.js';
42
43
  import { processCLIInput } from './commands.js';
43
44
  import { createStreamingState, handleWorldEventWithStreaming } from './stream.js';
44
45
  import { configureLLMProvider } from '../core/llm-config.js';
45
- // Initialize logger system with default configuration: all categories at 'error' level
46
- initializeLogger({
47
- globalLevel: 'error',
48
- categoryLevels: {
49
- cli: 'error',
50
- core: 'error',
51
- events: 'error',
52
- llm: 'error'
53
- }
54
- });
55
- // Create CLI category logger after initialization
46
+ // Create CLI category logger after logger auto-initialization
56
47
  const logger = createCategoryLogger('cli');
57
48
  function setupPromptTimer(globalState, rl, callback, delay = 2000) {
58
49
  clearPromptTimer(globalState);
@@ -85,25 +76,6 @@ const boldCyan = (text) => `\x1b[1m\x1b[36m${text}\x1b[0m`;
85
76
  const success = (text) => `${boldGreen('✓')} ${text}`;
86
77
  const error = (text) => `${boldRed('✗')} ${text}`;
87
78
  const bullet = (text) => `${gray('•')} ${text}`;
88
- // Logger configuration
89
- async function configureLogger(logLevel) {
90
- // Use the centralized logger configuration from core
91
- const level = (logLevel || 'error');
92
- // Reinitialize logger with new configuration
93
- initializeLogger({
94
- globalLevel: level,
95
- categoryLevels: {
96
- cli: 'error', // Always keep CLI at error level
97
- core: level, // Core modules use global level
98
- events: 'error', // Keep events at error level (too verbose)
99
- llm: level, // LLM module uses global level
100
- }
101
- });
102
- // Only log the debug message if we're actually at debug level for global
103
- if (level === 'debug' || level === 'trace') {
104
- logger.debug(`Global log level set to: ${level}, CLI log level: error`);
105
- }
106
- }
107
79
  // LLM Provider configuration from environment variables
108
80
  function configureLLMProvidersFromEnv() {
109
81
  // OpenAI
@@ -167,8 +139,23 @@ function configureLLMProvidersFromEnv() {
167
139
  logger.debug('Configured Ollama provider with default URL');
168
140
  }
169
141
  }
170
- const AGENT_WORLD_DATA_PATH = process.env.AGENT_WORLD_DATA_PATH || './data/worlds';
171
- const DEFAULT_ROOT_PATH = path.join(process.cwd(), AGENT_WORLD_DATA_PATH);
142
+ // Get default root path from storage-factory (no local defaults)
143
+ const DEFAULT_ROOT_PATH = getDefaultRootPath();
144
+ // Helper to print CLI results in a user-friendly way
145
+ function printCLIResult(result) {
146
+ if (result.success) {
147
+ if (result.message)
148
+ console.log(success(result.message));
149
+ if (result.data && typeof result.data === 'string')
150
+ console.log(result.data);
151
+ }
152
+ else {
153
+ if (result.message)
154
+ console.log(error(result.message));
155
+ if (result.error && result.error !== result.message)
156
+ console.log(error(result.error));
157
+ }
158
+ }
172
159
  // Pipeline mode execution with timer-based cleanup
173
160
  async function runPipelineMode(options, messageFromArgs) {
174
161
  const rootPath = options.root || DEFAULT_ROOT_PATH;
@@ -182,8 +169,12 @@ async function runPipelineMode(options, messageFromArgs) {
182
169
  onWorldEvent: (eventType, eventData) => {
183
170
  if (eventData.content && eventData.content.includes('Success message sent'))
184
171
  return;
185
- if ((eventType === 'system' || eventType === 'world') && eventData.message) {
186
- console.log(`${boldRed('● system:')} ${eventData.message}`);
172
+ if ((eventType === 'system' || eventType === 'world') && (eventData.message || eventData.content)) {
173
+ // existing logic
174
+ }
175
+ else if (eventType === 'message' && eventData.sender === 'system') {
176
+ const msg = eventData.content;
177
+ console.log(`${boldRed('● system:')} ${msg}`);
187
178
  }
188
179
  if (eventType === 'sse' && eventData.content) {
189
180
  setupExitTimer(5000);
@@ -221,7 +212,7 @@ async function runPipelineMode(options, messageFromArgs) {
221
212
  process.exit(1);
222
213
  }
223
214
  const result = await processCLIInput(options.command, world, rootPath, 'HUMAN');
224
- console.log(JSON.stringify(result, null, 2));
215
+ printCLIResult(result);
225
216
  // Only set timer if sending message to world (not for commands)
226
217
  if (!options.command.startsWith('/') && world) {
227
218
  setupExitTimer();
@@ -244,7 +235,7 @@ async function runPipelineMode(options, messageFromArgs) {
244
235
  process.exit(1);
245
236
  }
246
237
  const result = await processCLIInput(messageFromArgs, world, rootPath, 'HUMAN');
247
- console.log(JSON.stringify(result, null, 2));
238
+ printCLIResult(result);
248
239
  // Set timer with longer delay for message processing (always needed for messages)
249
240
  setupExitTimer(8000);
250
241
  if (!result.success) {
@@ -264,7 +255,7 @@ async function runPipelineMode(options, messageFromArgs) {
264
255
  process.exit(1);
265
256
  }
266
257
  const result = await processCLIInput(input.trim(), world, rootPath, 'HUMAN');
267
- console.log(JSON.stringify(result, null, 2));
258
+ printCLIResult(result);
268
259
  // Set timer with longer delay for message processing (always needed for stdin messages)
269
260
  setupExitTimer(8000);
270
261
  if (!result.success) {
@@ -311,8 +302,12 @@ function handleWorldEvent(eventType, eventData, streaming, globalState, rl) {
311
302
  }
312
303
  if (eventData.content && eventData.content.includes('Success message sent'))
313
304
  return;
314
- if ((eventType === 'system' || eventType === 'world') && eventData.message) {
315
- console.log(`\n${boldRed('● system:')} ${eventData.message}`);
305
+ if ((eventType === 'system' || eventType === 'world') && (eventData.message || eventData.content)) {
306
+ // existing logic
307
+ }
308
+ else if (eventType === 'message' && eventData.sender === 'system') {
309
+ const msg = eventData.content;
310
+ console.log(`${boldRed('● system:')} ${msg}`);
316
311
  }
317
312
  }
318
313
  // World discovery and selection
@@ -444,7 +439,9 @@ async function runInteractiveMode(options) {
444
439
  }
445
440
  // Show usage tips
446
441
  console.log(`\n${gray('Tips:')}`);
447
- console.log(` ${bullet(gray('Type commands like:'))} ${cyan('/clear agent1')}, ${cyan('/clear all')}, ${cyan('/add MyAgent')}`);
442
+ console.log(` ${bullet(gray('Short commands:'))} ${cyan('/list')}, ${cyan('/show agent1')}, ${cyan('/edit agent1')}, ${cyan('/del agent1')}`);
443
+ console.log(` ${bullet(gray('Context-sensitive:'))} ${cyan('/list')} ${gray('shows agents if world selected, worlds otherwise')}`);
444
+ console.log(` ${bullet(gray('Legacy commands:'))} ${cyan('/clear agent1')}, ${cyan('/clear all')}, ${cyan('/add MyAgent')}`);
448
445
  console.log(` ${bullet(gray('Use'))} ${cyan('/select')} ${gray('to choose a different world')}`);
449
446
  console.log(` ${bullet(gray('Type messages to send to agents'))}`);
450
447
  console.log(` ${bullet(gray('Use'))} ${cyan('/quit')} ${gray('or')} ${cyan('/exit')} ${gray('to exit, or press')} ${boldYellow('Ctrl+C')}`);
@@ -457,23 +454,39 @@ async function runInteractiveMode(options) {
457
454
  rl.prompt();
458
455
  return;
459
456
  }
457
+ // Check for exit commands before anything else
458
+ const isExitCommand = trimmedInput.toLowerCase() === '/exit' || trimmedInput.toLowerCase() === '/quit';
459
+ if (isExitCommand) {
460
+ if (isExiting)
461
+ return;
462
+ isExiting = true;
463
+ // Clear any existing timers immediately
464
+ clearPromptTimer(globalState);
465
+ if (streaming.stopWait)
466
+ streaming.stopWait();
467
+ console.log(`\n${boldCyan('Goodbye!')}`);
468
+ if (worldState)
469
+ cleanupWorldSubscription(worldState);
470
+ rl.close();
471
+ process.exit(0);
472
+ return;
473
+ }
460
474
  console.log(`\n${boldYellow('● you:')} ${trimmedInput}`);
461
475
  try {
462
476
  const result = await processCLIInput(trimmedInput, worldState?.world || null, rootPath, 'HUMAN');
463
- // Handle exit commands
477
+ // Handle exit commands from result (redundant, but keep for safety)
464
478
  if (result.data?.exit) {
465
479
  if (isExiting)
466
480
  return; // Prevent duplicate exit handling
467
481
  isExiting = true;
468
- // Clear any existing timers immediately
469
- if (streaming.stopWait) {
482
+ clearPromptTimer(globalState);
483
+ if (streaming.stopWait)
470
484
  streaming.stopWait();
471
- }
472
485
  console.log(`\n${boldCyan('Goodbye!')}`);
473
- if (worldState) {
486
+ if (worldState)
474
487
  cleanupWorldSubscription(worldState);
475
- }
476
488
  rl.close();
489
+ process.exit(0);
477
490
  return;
478
491
  }
479
492
  // Handle world selection command
@@ -520,7 +533,23 @@ async function runInteractiveMode(options) {
520
533
  console.log(success(result.message));
521
534
  }
522
535
  if (result.data && !(result.data.sender === 'HUMAN')) {
523
- console.log(`${boldMagenta('Data:')} ${JSON.stringify(result.data, null, 2)}`);
536
+ // Print a concise summary of result.data if present and not already in message
537
+ if (result.data) {
538
+ if (typeof result.data === 'string') {
539
+ console.log(`${boldMagenta('Data:')} ${result.data}`);
540
+ }
541
+ else if (result.data.name) {
542
+ // If it's an agent or world object
543
+ console.log(`${boldMagenta('Data:')} ${result.data.name}`);
544
+ }
545
+ else if (Array.isArray(result.data)) {
546
+ console.log(`${boldMagenta('Data:')} ${result.data.length} items`);
547
+ }
548
+ else {
549
+ // Fallback: print keys
550
+ console.log(`${boldMagenta('Data:')} ${Object.keys(result.data).join(', ')}`);
551
+ }
552
+ }
524
553
  }
525
554
  // Refresh world if needed
526
555
  if (result.refreshWorld && currentWorldName && worldState) {
@@ -541,13 +570,8 @@ async function runInteractiveMode(options) {
541
570
  }
542
571
  // Set timer based on input type: commands get short delay, messages get longer delay
543
572
  const isCommand = trimmedInput.startsWith('/');
544
- const isExitCommand = trimmedInput.toLowerCase() === '/exit' || trimmedInput.toLowerCase() === '/quit';
545
573
  const isSelectCommand = trimmedInput.toLowerCase() === '/select';
546
- if (isExitCommand) {
547
- // For exit commands, don't set any timer - exit should be immediate
548
- return;
549
- }
550
- else if (isSelectCommand) {
574
+ if (isSelectCommand) {
551
575
  // For select command, prompt is already shown in the handler
552
576
  return;
553
577
  }
@@ -564,27 +588,26 @@ async function runInteractiveMode(options) {
564
588
  if (isExiting)
565
589
  return; // Prevent duplicate cleanup
566
590
  isExiting = true;
591
+ clearPromptTimer(globalState);
592
+ if (streaming.stopWait)
593
+ streaming.stopWait();
567
594
  console.log(`\n${boldCyan('Goodbye!')}`);
568
- if (worldState) {
569
- if (streaming.stopWait) {
570
- streaming.stopWait();
571
- }
595
+ if (worldState)
572
596
  cleanupWorldSubscription(worldState);
573
- }
574
597
  process.exit(0);
575
598
  });
576
599
  rl.on('SIGINT', () => {
577
600
  if (isExiting)
578
601
  return; // Prevent duplicate cleanup
579
602
  isExiting = true;
603
+ clearPromptTimer(globalState);
604
+ if (streaming.stopWait)
605
+ streaming.stopWait();
580
606
  console.log(`\n${boldCyan('Goodbye!')}`);
581
- if (worldState) {
582
- if (streaming.stopWait) {
583
- streaming.stopWait();
584
- }
607
+ if (worldState)
585
608
  cleanupWorldSubscription(worldState);
586
- }
587
609
  rl.close();
610
+ process.exit(0);
588
611
  });
589
612
  }
590
613
  catch (err) {
@@ -597,6 +620,9 @@ async function runInteractiveMode(options) {
597
620
  async function main() {
598
621
  // Configure LLM providers from environment variables at startup
599
622
  configureLLMProvidersFromEnv();
623
+ // Import help generator from commands.ts
624
+ // (import at top: import { generateHelpMessage } from './commands.js';)
625
+ const { generateHelpMessage } = await import('./commands.js');
600
626
  program
601
627
  .name('cli')
602
628
  .description('Agent World CLI')
@@ -608,6 +634,8 @@ async function main() {
608
634
  .option('-s, --server', 'Launch the server before running CLI')
609
635
  .allowUnknownOption()
610
636
  .allowExcessArguments()
637
+ .helpOption('-h, --help', 'Display help for command')
638
+ .addHelpText('beforeAll', () => generateHelpMessage())
611
639
  .parse();
612
640
  const options = program.opts();
613
641
  // If --server is specified, launch the server first
@@ -628,8 +656,6 @@ async function main() {
628
656
  // If server exits, exit CLI as well
629
657
  process.exit(serverProcess.status || 0);
630
658
  }
631
- // Configure logger - set global level first, then CLI-specific level
632
- await configureLogger(options.logLevel);
633
659
  const args = program.args;
634
660
  const messageFromArgs = args.length > 0 ? args.join(' ') : null;
635
661
  const isPipelineMode = !!(options.command ||
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Web Server - Express.js HTTP Server with REST API and SSE
4
+ *
5
+ * Features:
6
+ * - Express server with CORS, static files, and modular API routing
7
+ * - Environment-based LLM provider configuration
8
+ * - Category-based logging with configurable levels
9
+ * - Health check endpoint and proper error handling
10
+ * - Environment Variables: Automatically loads .env file for API keys and configuration
11
+ *
12
+ * Configuration: AGENT_WORLD_DATA_PATH, LOG_LEVEL, LLM provider keys
13
+ * Endpoints: /health + API routes from ./api.ts
14
+ */
15
+ // Load environment variables from .env file
16
+ import dotenv from 'dotenv';
17
+ dotenv.config();
18
+ import open from 'open';
19
+ import { createCategoryLogger, LLMProvider } from '../core/index.js';
20
+ import { configureLLMProvider } from '../core/llm-config.js';
21
+ import express from 'express';
22
+ import cors from 'cors';
23
+ import path from 'path';
24
+ import { fileURLToPath, pathToFileURL } from 'url';
25
+ import apiRouter from './api.js';
26
+ // ES modules setup
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = path.dirname(__filename);
29
+ // Configuration
30
+ const PORT = Number(process.env.PORT) || 0;
31
+ const HOST = process.env.HOST || '127.0.0.1';
32
+ // Create server logger after logger auto-initialization
33
+ const serverLogger = createCategoryLogger('server');
34
+ // LLM provider configuration
35
+ function configureLLMProvidersFromEnv() {
36
+ const providers = [
37
+ { env: 'OPENAI_API_KEY', provider: LLMProvider.OPENAI, config: (key) => ({ apiKey: key }) },
38
+ { env: 'ANTHROPIC_API_KEY', provider: LLMProvider.ANTHROPIC, config: (key) => ({ apiKey: key }) },
39
+ { env: 'GOOGLE_API_KEY', provider: LLMProvider.GOOGLE, config: (key) => ({ apiKey: key }) },
40
+ { env: 'XAI_API_KEY', provider: LLMProvider.XAI, config: (key) => ({ apiKey: key }) }
41
+ ];
42
+ providers.forEach(({ env, provider, config }) => {
43
+ const key = process.env[env];
44
+ if (key) {
45
+ configureLLMProvider(provider, config(key));
46
+ serverLogger.debug(`Configured ${provider} provider from environment`);
47
+ }
48
+ });
49
+ // Azure (requires multiple env vars)
50
+ if (process.env.AZURE_OPENAI_API_KEY && process.env.AZURE_ENDPOINT && process.env.AZURE_DEPLOYMENT) {
51
+ configureLLMProvider(LLMProvider.AZURE, {
52
+ apiKey: process.env.AZURE_OPENAI_API_KEY,
53
+ endpoint: process.env.AZURE_ENDPOINT,
54
+ deployment: process.env.AZURE_DEPLOYMENT,
55
+ apiVersion: process.env.AZURE_API_VERSION || '2023-12-01-preview'
56
+ });
57
+ serverLogger.debug('Configured Azure provider from environment');
58
+ }
59
+ // OpenAI Compatible
60
+ if (process.env.OPENAI_COMPATIBLE_API_KEY && process.env.OPENAI_COMPATIBLE_BASE_URL) {
61
+ configureLLMProvider(LLMProvider.OPENAI_COMPATIBLE, {
62
+ apiKey: process.env.OPENAI_COMPATIBLE_API_KEY,
63
+ baseUrl: process.env.OPENAI_COMPATIBLE_BASE_URL
64
+ });
65
+ serverLogger.debug('Configured OpenAI-Compatible provider from environment');
66
+ }
67
+ // Ollama
68
+ const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434/api';
69
+ configureLLMProvider(LLMProvider.OLLAMA, { baseUrl: ollamaUrl });
70
+ serverLogger.debug('Configured Ollama provider', { baseUrl: ollamaUrl });
71
+ }
72
+ // Express app setup
73
+ const app = express();
74
+ app.use(cors());
75
+ app.use(express.json());
76
+ // Request logging
77
+ app.use((req, res, next) => {
78
+ console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
79
+ next();
80
+ });
81
+ // Static files and API routes
82
+ app.use(express.static(path.join(__dirname, '../public')));
83
+ app.use('/api', apiRouter);
84
+ // Health check endpoint
85
+ app.get('/health', (req, res) => {
86
+ try {
87
+ res.json({
88
+ status: 'healthy',
89
+ timestamp: new Date().toISOString(),
90
+ services: { express: 'running' }
91
+ });
92
+ }
93
+ catch (error) {
94
+ console.error('Error getting server health:', error);
95
+ res.status(500).json({
96
+ status: 'error',
97
+ timestamp: new Date().toISOString(),
98
+ error: 'Failed to get server health'
99
+ });
100
+ }
101
+ });
102
+ // SPA fallback - serve index.html for all non-API routes
103
+ app.get('*', (req, res) => {
104
+ res.sendFile(path.join(__dirname, '../public/index.html'));
105
+ });
106
+ // Error handling middleware
107
+ app.use((error, req, res, next) => {
108
+ console.error('Unhandled error:', error);
109
+ res.status(500).json({ error: 'Internal server error', code: 'INTERNAL_ERROR' });
110
+ });
111
+ // 404 handler
112
+ app.use((req, res) => {
113
+ res.status(404).json({ error: 'Endpoint not found', code: 'NOT_FOUND' });
114
+ });
115
+ // Server startup function
116
+ export function startWebServer(port = PORT, host = HOST) {
117
+ return new Promise((resolve, reject) => {
118
+ configureLLMProvidersFromEnv();
119
+ const server = app.listen(port, host, () => {
120
+ const serverAddress = server.address();
121
+ if (serverAddress && typeof serverAddress === 'object') {
122
+ const actualPort = serverAddress.port;
123
+ const url = `http://${host}:${actualPort}`;
124
+ console.log(`🌐 Web server running at ${url}`);
125
+ // console.log(`📁 Serving static files from: ${path.join(__dirname, '../public')}`);
126
+ // console.log(`🚀 HTTP server running with REST API and SSE chat`);
127
+ resolve(server);
128
+ open(url);
129
+ }
130
+ });
131
+ server.on('error', reject);
132
+ });
133
+ }
134
+ // Direct execution handling - check both direct execution and npm bin execution
135
+ const currentFileUrl = import.meta.url;
136
+ const entryPointUrl = pathToFileURL(path.resolve(process.argv[1])).href;
137
+ const isDirectExecution = currentFileUrl === entryPointUrl;
138
+ const isServerBinCommand = process.argv[1].includes('agent-world-server') || currentFileUrl.includes('server/index.js');
139
+ if (isDirectExecution || isServerBinCommand) {
140
+ startWebServer()
141
+ .then(() => console.log('Server started successfully'))
142
+ .catch((error) => {
143
+ console.error('Failed to start server:', error);
144
+ process.exit(1);
145
+ });
146
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-world",
3
- "version": "0.4.0",
3
+ "version": "0.4.6",
4
4
  "main": "index.ts",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -8,7 +8,8 @@
8
8
  "next"
9
9
  ],
10
10
  "bin": {
11
- "agent-world": "dist/cli/index.js"
11
+ "agent-world": "dist/cli/index.js",
12
+ "agent-world-server": "dist/server/index.js"
12
13
  },
13
14
  "scripts": {
14
15
  "prestart": "npm run build",
@@ -49,6 +50,7 @@
49
50
  "chalk": "^4.1.2",
50
51
  "cors": "^2.8.5",
51
52
  "dotenv": "^16.5.0",
53
+ "enquirer": "^2.4.1",
52
54
  "events": "^3.3.0",
53
55
  "express": "^4.21.2",
54
56
  "ollama-ai-provider": "^1.2.0",