agent-world 0.10.0 → 0.11.1

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
@@ -1,5 +1,11 @@
1
1
  # Agent World
2
2
 
3
+ [![Latest Release](https://img.shields.io/github/v/release/yysun/agent-world?label=release)](https://github.com/yysun/agent-world/releases)
4
+
5
+ <p align="center">
6
+ <img src="electron/assets/icons/agent-world-icon.svg" alt="Agent World Logo" width="120" />
7
+ </p>
8
+
3
9
  *Build AI agent teams with just words—no coding required.*
4
10
 
5
11
  ## Why Agent World?
@@ -29,8 +35,27 @@ Paste that prompt. Agents come alive instantly.
29
35
  - ✅ No Code Required - Agents are defined entirely in natural language
30
36
  - ✅ Natural Communication - Agents understand context and conversations
31
37
  - ✅ Built-in Rules for Messages - Turn limits to prevent loops
38
+ - ✅ Concurrent Chat Sessions - Isolated `chatId` routing enables parallel conversations
39
+ - ✅ Progressive Agent Skills - Skills are discovered and loaded on demand via `load_skill`
40
+ - ✅ Cross-Client HITL Approval - Option-based approvals in CLI, Web, and Electron
41
+ - ✅ Runtime Controls - Session-scoped send/stop flows and tool lifecycle visibility
42
+ - ✅ Safer Tool Execution - Trusted-CWD and argument-scope guards for `shell_cmd`
32
43
  - ✅ Multiple AI Providers - Use different models for different agents
33
- - ✅ Modern Web Interface - React + Next.js frontend with real-time chat
44
+ - ✅ Web + CLI + Electron - Modern interfaces with real-time streaming and status feedback
45
+
46
+ ## Latest Highlights (v0.11.0)
47
+
48
+ - Electron desktop app with workspace-folder world loading, recents, and improved world info
49
+ - Concurrent chat session isolation with chat-scoped event routing and stop controls
50
+ - World-level `mainAgent` routing and agent-level `autoReply` configuration
51
+ - Core-owned edit/resubmit and chat-title flows for consistent behavior across clients
52
+ - World variables as `.env` text with runtime interpolation support
53
+ - Progressive skills (`load_skill`) with skill registry sync and HITL-gated activation
54
+
55
+ ## Release Notes
56
+
57
+ - **v0.11.0** - Electron desktop workflow, concurrent chat sessions, main-agent routing, progressive skills + HITL, and runtime safety hardening
58
+ - Full history: [CHANGELOG.md](CHANGELOG.md)
34
59
 
35
60
  ## What You Can Build
36
61
 
@@ -81,7 +106,7 @@ Each Agent World has a collection of agents that can communicate through a share
81
106
  | **Human message** | `Hello everyone!` | All active agents |
82
107
  | **Direct mention** | `@alice Can you help?` | Only @alice |
83
108
  | **Paragraph mention** | `Please review this:\n@alice` | Only @alice |
84
- | **Mid-text mention** | `I think @alice should help` | Nobody (saved to memory) |
109
+ | **Mid-text mention** | `I think @alice should help` | Nobody (event is persisted; no agent-memory save) |
85
110
  | **Stop World** | `<world>pass</world>` | No agents |
86
111
 
87
112
  ### Agent Behavior
@@ -93,8 +118,12 @@ Each Agent World has a collection of agents that can communicate through a share
93
118
 
94
119
  **Agents never respond to:**
95
120
  - Their own messages
96
- - Other agents (unless @mentioned), but will save message to memory
97
- - Mid-text mentions (will save message to memory)
121
+ - Other agents (unless @mentioned at paragraph start)
122
+ - Mid-text mentions (not at paragraph start)
123
+
124
+ **When messages are saved to agent memory:**
125
+ - Incoming messages are saved only for agents that will respond
126
+ - Non-responding agents skip agent-memory save (message events are still persisted)
98
127
 
99
128
  **Turn limits prevent loops:**
100
129
  - Default: 5 responses per conversation thread
@@ -129,46 +158,47 @@ npx agent-world -w default-world "hi"
129
158
  echo "hi" | npx agent-world -w default-world
130
159
  ```
131
160
 
161
+ **Option 3: Electron Desktop App (repo)**
162
+ ```bash
163
+ npm run electron:dev
164
+ ```
165
+
132
166
  ## Project Structure
133
167
 
134
168
  See [Project Structure Documentation](project.md)
135
169
 
136
- ## Development Scripts Convention
170
+ ## Development Scripts
137
171
 
138
- Agent World follows a consistent naming convention for all npm scripts:
172
+ Agent World provides simple, consistent npm scripts for three main applications:
139
173
 
140
- | Script Pattern | Description | Example |
141
- |---------------|-------------|---------|
142
- | `<module>` | Shorthand for `<module>:start` | `npm run server` |
143
- | `<module>:start` | Run compiled code from `dist/` | `npm run server:start` |
144
- | `<module>:dev` | Run with tsx (no build needed) | `npm run server:dev` |
145
- | `<module>:watch` | Run with tsx in watch mode | `npm run server:watch` |
174
+ ### Development (hot reload)
175
+ ```bash
176
+ npm run dev # Web app with server (default)
177
+ npm run web:dev # Web app with server (explicit)
178
+ npm run cli:dev # CLI with watch mode
179
+ npm run electron:dev # Electron app
180
+ ```
146
181
 
147
- **Available modules:** `server`, `cli`, `ws`, `tui`
182
+ ### Production
183
+ ```bash
184
+ npm start # Web server (default)
185
+ npm run web:start # Web server (explicit)
186
+ npm run cli:start # CLI (built)
187
+ npm run electron:start # Electron app
188
+ ```
148
189
 
149
- **Module Dependencies:**
150
- - `web:dev` / `web:watch` → Depends on `server` (waits for server to be ready)
151
- - `tui:dev` / `tui:watch` Depends on `ws` (waits for WebSocket server)
190
+ ### Behind the Scenes
191
+ The scripts handle dependencies automatically:
192
+ - **Web**: Builds core, starts server in watch mode, launches Vite dev server
193
+ - **CLI**: Runs with tsx watch mode for instant feedback
194
+ - **Electron**: Builds core, launches Electron with Vite HMR
152
195
 
153
- **Examples:**
196
+ ### Other Useful Scripts
154
197
  ```bash
155
- # Production execution (requires build)
156
- npm run server # Runs: node dist/server/index.js
157
- npm run cli # Runs: node dist/cli/index.js
158
-
159
- # Development (no build needed)
160
- npm run server:dev # Runs: npx tsx server/index.ts
161
- npm run ws:dev # Runs: npx tsx ws/index.ts
162
-
163
- # Watch mode (auto-restart on changes)
164
- npm run server:watch # Runs: npx tsx --watch server/index.ts
165
- npm run cli:watch # Runs: npx tsx --watch cli/index.ts
166
-
167
- # With dependencies (auto-start required services)
168
- npm run web:dev # Waits for server, then starts web
169
- npm run web:watch # Runs server:watch + web in parallel
170
- npm run tui:dev # Waits for ws, then starts tui
171
- npm run tui:watch # Runs ws:watch + tui in parallel
198
+ npm run build # Build all (core + root + web)
199
+ npm run check # TypeScript type checking
200
+ npm test # Run unit tests
201
+ npm run test:watch # Watch mode
172
202
  ```
173
203
 
174
204
  ### Environment Setup
@@ -187,6 +217,13 @@ export OLLAMA_BASE_URL="http://localhost:11434"
187
217
 
188
218
  Or create a `.env` file in your working directory with:
189
219
 
220
+ ```bash
221
+ OPENAI_API_KEY=your-key-here
222
+ ANTHROPIC_API_KEY=your-key-here
223
+ GOOGLE_API_KEY=your-key-here
224
+ OLLAMA_BASE_URL=http://localhost:11434
225
+ ```
226
+
190
227
  ## Testing
191
228
 
192
229
  **Run all tests:**
@@ -218,18 +255,18 @@ Agent World uses **scenario-based logging** to help you debug specific issues wi
218
255
 
219
256
  ```bash
220
257
  # Database migration issues
221
- LOG_STORAGE_MIGRATION=info npm run server
258
+ LOG_STORAGE_MIGRATION=info npm run web:dev
222
259
 
223
260
  # MCP server problems
224
- LOG_MCP=debug npm run server
261
+ LOG_MCP=debug npm run web:dev
225
262
 
226
263
  # Agent response debugging
227
- LOG_EVENTS_AGENT=debug LOG_LLM=debug npm run server
264
+ LOG_EVENTS_AGENT=debug LOG_LLM=debug npm run web:dev
228
265
  ```
229
266
 
230
267
  **For complete logging documentation**, see [Logging Guide](docs/logging-guide.md).
231
268
 
232
- ## Learn More
269
+ ## Storage Configuration
233
270
 
234
271
  ### World Database Setup
235
272
 
@@ -245,9 +282,12 @@ export AGENT_WORLD_DATA_PATH=./data/worlds
245
282
 
246
283
  ## Learn More
247
284
 
285
+ - **[Docs Home](docs/docs-home.md)** - Central navigation page for all major documentation
248
286
  - **[Building Agents with Just Words](docs/Building%20Agents%20with%20Just%20Words.md)** - Complete guide with examples
249
287
  - **[Shell Command Tool (shell_cmd)](docs/shell-cmd-tool.md)** - Built-in tool for executing shell commands
288
+ - **[HITL Approval Flow](docs/hitl-approval-flow.md)** - Option-based approval flow across Core/Electron/Web/CLI
250
289
  - **[Using Core from npm](docs/core-npm-usage.md)** - Integration guide for server and browser apps
290
+ - **[Electron Desktop App](docs/electron-desktop.md)** - Open-folder workflow and local world creation
251
291
 
252
292
 
253
293
  ## Built-in Tools
@@ -257,16 +297,51 @@ Agent World includes built-in tools that are automatically available to all agen
257
297
  ### shell_cmd
258
298
  Execute shell commands with full output capture and execution history. Perfect for file operations, system information, and automation tasks.
259
299
 
300
+ - Enforces trusted working-directory scope from world/tool context
301
+ - Validates command/path arguments to prevent out-of-scope traversal patterns
302
+ - Supports lifecycle tracking and session-scoped cancellation in active runtimes
303
+
260
304
  ```typescript
261
305
  // Available to LLMs as 'shell_cmd' tool
262
306
  {
263
307
  "command": "ls",
264
- "parameters": ["-la", "/tmp"]
308
+ "parameters": ["-la", "./"]
265
309
  }
266
310
  ```
267
311
 
268
312
  See [Shell Command Tool Documentation](docs/shell-cmd-tool.md) for complete details.
269
313
 
314
+ ### load_skill (Agent Skills)
315
+
316
+ Agent World includes progressive skill loading through the `load_skill` built-in tool.
317
+
318
+ - Skills are discovered from `SKILL.md` files in:
319
+ - Project roots: `.agents/skills`, `skills`
320
+ - User roots: `~/.agents/skills`, `~/.codex/skills`
321
+ - The model receives compact skill summaries first, then calls `load_skill` only when full instructions are needed.
322
+ - Skill activation in interactive runtimes is HITL-gated.
323
+
324
+ Minimal `SKILL.md` example:
325
+
326
+ ```md
327
+ ---
328
+ name: sql-review
329
+ description: Review SQL migrations for safety and rollback compatibility.
330
+ ---
331
+
332
+ # SQL Review Skill
333
+
334
+ 1. Check for destructive DDL.
335
+ 2. Verify index and lock impact.
336
+ 3. Validate rollback path.
337
+ ```
338
+
339
+ HITL options for skill activation:
340
+
341
+ - `yes_once`: approve this call only
342
+ - `yes_in_session`: approve this `skill_id` in the current world/chat session
343
+ - `no`: decline
344
+
270
345
  ## Experimental Features
271
346
 
272
347
  - **MCP Support** - *Currently in experiment* - Model Context Protocol integration for tools like search and code execution. e.g.,
@@ -308,4 +383,3 @@ Agent World thrives on community examples and improvements:
308
383
  MIT License - Build amazing things and share them with the world!
309
384
 
310
385
  Copyright © 2025 Yiyi Sun
311
-
package/dist/cli/index.js CHANGED
@@ -13,6 +13,9 @@
13
13
  * - Work with loaded world without importing (uses external storage path)
14
14
  *
15
15
  * Changes:
16
+ * - 2026-02-14: Added interactive + pipeline HITL option response handling for generic approval requests.
17
+ * - 2026-02-11: Fixed tool-stream timeout: extendTimeout() resets idle timeout on streaming data
18
+ * - 2026-02-11: Pipeline mode now listens for tool-stream SSE events to extend timeout
16
19
  * - 2026-01-09: Added --streaming flag for explicit streaming control (overrides TTY auto-detection)
17
20
  * - 2025-02-06: Prevented duplicate MESSAGE output when streaming already rendered agent responses
18
21
  * - 2025-11-01: Added multi-world selection in loadWorldFromFile
@@ -73,17 +76,20 @@ import path from 'path';
73
76
  import { fileURLToPath } from 'url';
74
77
  import { program } from 'commander';
75
78
  import readline from 'readline';
76
- import { listWorlds, subscribeWorld, createCategoryLogger, LLMProvider, enableStreaming, disableStreaming } from '../core/index.js';
79
+ import { listWorlds, subscribeWorld, submitWorldOptionResponse, createCategoryLogger, LLMProvider, enableStreaming, disableStreaming } from '../core/index.js';
77
80
  import { EventType } from '../core/types.js';
78
81
  import { getDefaultRootPath } from '../core/storage/storage-factory.js';
79
82
  import { processCLIInput, displayChatMessages } from './commands.js';
80
- import { createStreamingState, handleWorldEventWithStreaming, handleToolEvents, handleActivityEvents, } from './stream.js';
83
+ import { createStreamingState, handleWorldEventWithStreaming, handleToolEvents, } from './stream.js';
81
84
  import { configureLLMProvider } from '../core/llm-config.js';
85
+ import { createStatusLineManager, log as statusLog, } from './display.js';
86
+ import { parseHitlOptionRequest, resolveHitlOptionSelectionInput, } from './hitl.js';
82
87
  // Create CLI category logger after logger auto-initialization
83
88
  const logger = createCategoryLogger('cli');
84
89
  function createGlobalState() {
85
90
  return {
86
- awaitingResponse: false
91
+ awaitingResponse: false,
92
+ hitlPromptActive: false
87
93
  };
88
94
  }
89
95
  // Color helpers - consolidated styling API
@@ -163,7 +169,8 @@ class WorldActivityMonitor {
163
169
  cleanup();
164
170
  reject(error);
165
171
  },
166
- seenProcessing: false
172
+ seenProcessing: false,
173
+ timeoutMs
167
174
  };
168
175
  const cleanup = () => {
169
176
  if (waiter.timeoutId) {
@@ -208,6 +215,26 @@ class WorldActivityMonitor {
208
215
  }
209
216
  this.lastEvent = null;
210
217
  }
218
+ /**
219
+ * Extend timeout for all active waiters.
220
+ * Called when streaming data arrives to prevent premature timeout.
221
+ */
222
+ extendTimeout() {
223
+ for (const waiter of this.waiters) {
224
+ // Reset the main timeout
225
+ if (waiter.timeoutId) {
226
+ clearTimeout(waiter.timeoutId);
227
+ waiter.timeoutId = setTimeout(() => {
228
+ this.finishWaiter(waiter, false, new Error('Timed out waiting for world to become idle'));
229
+ }, waiter.timeoutMs);
230
+ }
231
+ // Clear noActivity timeout since we have activity
232
+ if (waiter.noActivityTimeoutId) {
233
+ clearTimeout(waiter.noActivityTimeoutId);
234
+ waiter.noActivityTimeoutId = undefined;
235
+ }
236
+ }
237
+ }
211
238
  getActiveSources() {
212
239
  return this.lastEvent?.activeSources ?? [];
213
240
  }
@@ -243,34 +270,37 @@ function parseActivitySource(source) {
243
270
  }
244
271
  return null;
245
272
  }
246
- class ActivityProgressRenderer {
247
- activeAgents = new Set();
248
- handle(event) {
249
- if (!event)
250
- return;
251
- // Reset on idle
252
- if (event.type === 'idle') {
253
- this.reset();
254
- return;
255
- }
256
- const details = parseActivitySource(event.source);
257
- if (!details || details.type !== 'agent') {
258
- return;
259
- }
260
- // Track agent start on response-start
261
- if (event.type === 'response-start' && !this.activeAgents.has(details.name)) {
262
- this.activeAgents.add(details.name);
263
- }
264
- // Track agent end on response-end
265
- if (event.type === 'response-end' && this.activeAgents.has(details.name)) {
266
- this.activeAgents.delete(details.name);
273
+ async function promptHitlOptionSelection(request, statusLine, rl) {
274
+ const fallbackOptionId = request.defaultOptionId;
275
+ statusLine.pause();
276
+ try {
277
+ while (true) {
278
+ console.log(`\n${boldMagenta('Approval Required')}`);
279
+ console.log(`${boldCyan(request.title)}`);
280
+ if (request.message) {
281
+ console.log(gray(request.message));
282
+ }
283
+ console.log(gray('Select an option:'));
284
+ request.options.forEach((option, index) => {
285
+ const isDefault = option.id === fallbackOptionId;
286
+ const defaultSuffix = isDefault ? gray(' (default)') : '';
287
+ console.log(` ${yellow(String(index + 1) + '.')} ${option.label}${defaultSuffix}`);
288
+ if (option.description) {
289
+ console.log(` ${gray(option.description)}`);
290
+ }
291
+ });
292
+ const answer = await new Promise((resolve) => {
293
+ rl.question(`${boldMagenta('Choice (number or option id):')} `, (input) => resolve(input.trim()));
294
+ });
295
+ const resolvedOptionId = resolveHitlOptionSelectionInput(request.options, answer, fallbackOptionId);
296
+ if (resolvedOptionId) {
297
+ return resolvedOptionId;
298
+ }
299
+ console.log(boldRed('Invalid selection. Please choose a listed option.'));
267
300
  }
268
301
  }
269
- reset() {
270
- if (this.activeAgents.size > 0) {
271
- this.activeAgents.clear();
272
- // console.log(gray('All agents finished.'));
273
- }
302
+ finally {
303
+ statusLine.resume();
274
304
  }
275
305
  }
276
306
  // LLM Provider configuration from environment variables
@@ -360,19 +390,19 @@ function printCLIResult(result) {
360
390
  * @param streaming - Streaming state for interactive mode (null for pipeline mode)
361
391
  * @param globalState - Global state for interactive mode (null for pipeline mode)
362
392
  * @param activityMonitor - Activity monitor for tracking world events
363
- * @param progressRenderer - Progress renderer for displaying activity
393
+ * @param statusLine - Status line manager for interactive display (null for pipeline mode)
364
394
  * @param rl - Readline interface for interactive mode (undefined for pipeline mode)
365
395
  * @returns Map of event types to listener functions for cleanup
366
396
  */
367
- function attachCLIListeners(world, streaming, globalState, activityMonitor, progressRenderer, rl) {
397
+ function attachCLIListeners(world, streaming, globalState, activityMonitor, statusLine, rl) {
368
398
  const listeners = new Map();
399
+ let hitlPromptChain = Promise.resolve();
369
400
  // World activity events
370
401
  const worldListener = (eventData) => {
371
402
  activityMonitor.handle(eventData);
372
403
  // Only render activity progress in interactive mode
373
- if (streaming && globalState && rl) {
374
- progressRenderer.handle(eventData);
375
- handleWorldEvent(EventType.WORLD, eventData, streaming, globalState, activityMonitor, progressRenderer, rl)
404
+ if (streaming && globalState && rl && statusLine) {
405
+ handleWorldEvent(EventType.WORLD, eventData, streaming, globalState, activityMonitor, statusLine, rl)
376
406
  .catch(err => console.error('Error handling world event:', err));
377
407
  }
378
408
  // Pipeline mode: silently track events for completion detection
@@ -385,8 +415,8 @@ function attachCLIListeners(world, streaming, globalState, activityMonitor, prog
385
415
  typeof eventData.content === 'string' &&
386
416
  eventData.content.includes('Success message sent'))
387
417
  return;
388
- if (streaming && globalState && rl) {
389
- handleWorldEvent(EventType.MESSAGE, eventData, streaming, globalState, activityMonitor, progressRenderer, rl)
418
+ if (streaming && globalState && rl && statusLine) {
419
+ handleWorldEvent(EventType.MESSAGE, eventData, streaming, globalState, activityMonitor, statusLine, rl)
390
420
  .catch(err => console.error('Error handling message event:', err));
391
421
  }
392
422
  else {
@@ -402,22 +432,81 @@ function attachCLIListeners(world, streaming, globalState, activityMonitor, prog
402
432
  world.eventEmitter.on(EventType.MESSAGE, messageListener);
403
433
  listeners.set(EventType.MESSAGE, messageListener);
404
434
  // SSE events (interactive mode only - pipeline mode uses non-streaming LLM calls)
405
- if (streaming && globalState && rl) {
435
+ if (streaming && globalState && rl && statusLine) {
406
436
  const sseListener = (eventData) => {
407
- handleWorldEvent(EventType.SSE, eventData, streaming, globalState, activityMonitor, progressRenderer, rl)
437
+ // Extend timeout when tool-stream data arrives (keeps long-running tools alive)
438
+ if (eventData.type === 'tool-stream') {
439
+ activityMonitor.extendTimeout();
440
+ }
441
+ handleWorldEvent(EventType.SSE, eventData, streaming, globalState, activityMonitor, statusLine, rl)
408
442
  .catch(err => console.error('Error handling SSE event:', err));
409
443
  };
410
444
  world.eventEmitter.on(EventType.SSE, sseListener);
411
445
  listeners.set(EventType.SSE, sseListener);
412
446
  }
447
+ else {
448
+ // Pipeline mode: listen for tool-stream events to extend timeout on long-running commands
449
+ const sseTimeoutListener = (eventData) => {
450
+ if (eventData.type === 'tool-stream') {
451
+ activityMonitor.extendTimeout();
452
+ }
453
+ };
454
+ world.eventEmitter.on(EventType.SSE, sseTimeoutListener);
455
+ listeners.set(EventType.SSE, sseTimeoutListener);
456
+ }
413
457
  // System events
414
458
  const systemListener = (eventData) => {
459
+ const hitlRequest = parseHitlOptionRequest(eventData);
460
+ if (hitlRequest) {
461
+ hitlPromptChain = hitlPromptChain
462
+ .then(async () => {
463
+ if (streaming && globalState && rl && statusLine) {
464
+ globalState.hitlPromptActive = true;
465
+ try {
466
+ const selectedOptionId = await promptHitlOptionSelection(hitlRequest, statusLine, rl);
467
+ const result = submitWorldOptionResponse({
468
+ worldId: world.id,
469
+ requestId: hitlRequest.requestId,
470
+ optionId: selectedOptionId
471
+ });
472
+ if (!result.accepted) {
473
+ statusLine.pause();
474
+ console.log(boldRed(`Failed to submit approval response: ${result.reason || 'unknown error'}`));
475
+ statusLine.resume();
476
+ return;
477
+ }
478
+ statusLine.pause();
479
+ console.log(green(`Approved option: ${selectedOptionId}`));
480
+ statusLine.resume();
481
+ return;
482
+ }
483
+ finally {
484
+ globalState.hitlPromptActive = false;
485
+ }
486
+ }
487
+ // Pipeline/non-interactive mode: auto-respond with default option to avoid blocking.
488
+ const result = submitWorldOptionResponse({
489
+ worldId: world.id,
490
+ requestId: hitlRequest.requestId,
491
+ optionId: hitlRequest.defaultOptionId
492
+ });
493
+ if (!result.accepted) {
494
+ console.error(boldRed(`Failed to auto-respond HITL request: ${result.reason || 'unknown error'}`));
495
+ return;
496
+ }
497
+ console.log(`${gray('● system:')} Auto-selected HITL option "${hitlRequest.defaultOptionId}"`);
498
+ })
499
+ .catch((error) => {
500
+ console.error(boldRed(`Error handling HITL request: ${error instanceof Error ? error.message : String(error)}`));
501
+ });
502
+ return;
503
+ }
415
504
  if (eventData.content &&
416
505
  typeof eventData.content === 'string' &&
417
506
  eventData.content.includes('Success message sent'))
418
507
  return;
419
- if (streaming && globalState && rl) {
420
- handleWorldEvent(EventType.SYSTEM, eventData, streaming, globalState, activityMonitor, progressRenderer, rl)
508
+ if (streaming && globalState && rl && statusLine) {
509
+ handleWorldEvent(EventType.SYSTEM, eventData, streaming, globalState, activityMonitor, statusLine, rl)
421
510
  .catch(err => console.error('Error handling system event:', err));
422
511
  }
423
512
  else if (eventData.message || eventData.content) {
@@ -446,7 +535,6 @@ async function runPipelineMode(options, messageFromArgs) {
446
535
  let worldSubscription = null;
447
536
  let cliListeners = null;
448
537
  const activityMonitor = new WorldActivityMonitor();
449
- const progressRenderer = new ActivityProgressRenderer();
450
538
  try {
451
539
  if (options.world) {
452
540
  // Subscribe to world lifecycle but do not request forwarding callbacks
@@ -458,7 +546,7 @@ async function runPipelineMode(options, messageFromArgs) {
458
546
  world = worldSubscription.world;
459
547
  // Attach direct listeners to the world.eventEmitter for pipeline handling
460
548
  // Note: Pipeline mode uses non-streaming LLM calls, so SSE events are not needed
461
- cliListeners = attachCLIListeners(world, null, null, activityMonitor, progressRenderer);
549
+ cliListeners = attachCLIListeners(world, null, null, activityMonitor, null);
462
550
  }
463
551
  // Execute command from --command option
464
552
  if (options.command) {
@@ -587,11 +675,11 @@ function cleanupWorldSubscription(worldState) {
587
675
  * @param streaming - Streaming state for real-time response display
588
676
  * @param globalState - Global state for timer management
589
677
  * @param activityMonitor - Activity monitor for tracking world events
590
- * @param progressRenderer - Progress renderer for displaying activity
678
+ * @param statusLine - Status line manager for interactive display
591
679
  * @param rl - Readline interface for interactive input
592
680
  * @returns WorldState with subscription and world instance
593
681
  */
594
- async function handleSubscribe(rootPath, worldName, streaming, globalState, activityMonitor, progressRenderer, rl) {
682
+ async function handleSubscribe(rootPath, worldName, streaming, globalState, activityMonitor, statusLine, rl) {
595
683
  // Subscribe to world lifecycle but do not request forwarding callbacks
596
684
  const subscription = await subscribeWorld(worldName, { isOpen: true });
597
685
  if (!subscription)
@@ -603,36 +691,87 @@ async function handleSubscribe(rootPath, worldName, streaming, globalState, acti
603
691
  }
604
692
  // Attach direct listeners to the world.eventEmitter for CLI handling
605
693
  // Interactive mode needs all event types including SSE for streaming responses
606
- attachCLIListeners(world, streaming, globalState, activityMonitor, progressRenderer, rl);
694
+ attachCLIListeners(world, streaming, globalState, activityMonitor, statusLine, rl);
607
695
  return { subscription, world };
608
696
  }
609
697
  // Handle world events with streaming support
610
- async function handleWorldEvent(eventType, eventData, streaming, globalState, activityMonitor, progressRenderer, rl) {
698
+ async function handleWorldEvent(eventType, eventData, streaming, globalState, activityMonitor, statusLine, rl) {
611
699
  if (eventType === 'world') {
612
700
  const payload = eventData;
613
701
  // Handle activity events (new format: type = 'response-start' | 'response-end' | 'idle')
614
702
  if (payload.type === 'response-start' || payload.type === 'response-end' || payload.type === 'idle') {
615
703
  activityMonitor.handle(payload);
616
- progressRenderer.handle(payload);
617
- // Call handleActivityEvents for verbose activity logging
618
- handleActivityEvents(payload);
619
- if (payload.type === 'idle' && rl && globalState.awaitingResponse) {
620
- globalState.awaitingResponse = false;
621
- console.log(); // Empty line before prompt
622
- rl.prompt();
704
+ // Update status line based on activity events
705
+ const details = parseActivitySource(payload.source);
706
+ const agentName = details?.type === 'agent' ? details.name : payload.source || '';
707
+ if (payload.type === 'response-start' && agentName) {
708
+ statusLine.setSpinner(`${agentName} is thinking...`);
709
+ statusLine.startElapsedTimer();
710
+ // Update agent list from activeSources
711
+ if (payload.activeSources) {
712
+ const agentEntries = payload.activeSources
713
+ .map((s) => parseActivitySource(s))
714
+ .filter((d) => d?.type === 'agent')
715
+ .map((d) => ({ name: d.name, active: true }));
716
+ statusLine.setAgents(agentEntries);
717
+ }
718
+ }
719
+ if (payload.type === 'response-end') {
720
+ // Update agent list — remove finished agent
721
+ if (payload.activeSources) {
722
+ const agentEntries = payload.activeSources
723
+ .map((s) => parseActivitySource(s))
724
+ .filter((d) => d?.type === 'agent')
725
+ .map((d) => ({ name: d.name, active: true }));
726
+ statusLine.setAgents(agentEntries);
727
+ // Update spinner label to next active agent if any
728
+ if (agentEntries.length > 0) {
729
+ statusLine.setSpinner(`${agentEntries[0].name} is thinking...`);
730
+ }
731
+ else {
732
+ statusLine.setSpinner(null);
733
+ statusLine.stopElapsedTimer();
734
+ }
735
+ }
736
+ }
737
+ if (payload.type === 'idle') {
738
+ statusLine.reset();
739
+ if (rl && globalState.awaitingResponse) {
740
+ globalState.awaitingResponse = false;
741
+ statusLine.clear();
742
+ console.log(); // Empty line before prompt
743
+ rl.prompt();
744
+ }
623
745
  }
624
746
  }
625
747
  // Handle informational world messages.
626
748
  else if (payload.type === 'info' && payload.message) {
627
- console.log(`${gray('[World]')} ${payload.message}`);
749
+ statusLog(statusLine, `${gray('[World]')} ${payload.message}`);
628
750
  }
629
751
  // Handle tool events (migrated from sse channel)
630
752
  else if (payload.type === 'tool-start' || payload.type === 'tool-result' || payload.type === 'tool-error' || payload.type === 'tool-progress') {
753
+ // Update status line with tool info
754
+ if (payload.type === 'tool-start' && payload.toolExecution) {
755
+ statusLine.addTool(payload.toolExecution.toolName);
756
+ }
757
+ else if (payload.type === 'tool-result' && payload.toolExecution) {
758
+ statusLine.removeTool(payload.toolExecution.toolName, 'done');
759
+ }
760
+ else if (payload.type === 'tool-error' && payload.toolExecution) {
761
+ statusLine.removeTool(payload.toolExecution.toolName, 'error');
762
+ }
763
+ // Print permanent tool event output
764
+ statusLine.pause();
631
765
  handleToolEvents(payload);
766
+ // Only resume status line if not actively streaming
767
+ // (streaming code manages its own pause/resume lifecycle)
768
+ if (!streaming.isActive) {
769
+ statusLine.resume();
770
+ }
632
771
  }
633
772
  return;
634
773
  }
635
- if (handleWorldEventWithStreaming(eventType, eventData, streaming)) {
774
+ if (handleWorldEventWithStreaming(eventType, eventData, streaming, statusLine)) {
636
775
  return;
637
776
  }
638
777
  if (eventData.content &&
@@ -662,12 +801,12 @@ async function handleWorldEvent(eventType, eventData, streaming, globalState, ac
662
801
  }
663
802
  // Display system messages
664
803
  if (eventData.sender === 'system') {
665
- console.log(`${boldRed('● system:')} ${eventData.content}`);
804
+ statusLog(statusLine, `${boldRed('● system:')} ${eventData.content}`);
666
805
  return;
667
806
  }
668
807
  // Display agent messages (fallback for non-streaming or missed messages)
669
808
  if (eventData.content) {
670
- console.log(`\n${boldGreen(`● ${eventData.sender}:`)} ${eventData.content}\n`);
809
+ statusLog(statusLine, `\n${boldGreen(`● ${eventData.sender}:`)} ${eventData.content}\n`);
671
810
  }
672
811
  return;
673
812
  }
@@ -1005,7 +1144,7 @@ async function runInteractiveMode(options) {
1005
1144
  const globalState = createGlobalState();
1006
1145
  const streaming = createStreamingState();
1007
1146
  const activityMonitor = new WorldActivityMonitor();
1008
- const progressRenderer = new ActivityProgressRenderer();
1147
+ const statusLine = createStatusLineManager();
1009
1148
  const rl = readline.createInterface({
1010
1149
  input: process.stdin,
1011
1150
  output: process.stdout,
@@ -1022,8 +1161,8 @@ async function runInteractiveMode(options) {
1022
1161
  logger.debug(`Loading world: ${options.world}`);
1023
1162
  try {
1024
1163
  activityMonitor.reset();
1025
- progressRenderer.reset();
1026
- worldState = await handleSubscribe(rootPath, options.world, streaming, globalState, activityMonitor, progressRenderer, rl);
1164
+ statusLine.reset();
1165
+ worldState = await handleSubscribe(rootPath, options.world, streaming, globalState, activityMonitor, statusLine, rl);
1027
1166
  currentWorldName = options.world;
1028
1167
  console.log(success(`Connected to world: ${currentWorldName}`));
1029
1168
  if (worldState?.world) {
@@ -1048,8 +1187,8 @@ async function runInteractiveMode(options) {
1048
1187
  logger.debug(`Loading world: ${selectedWorld.worldName} from ${effectiveRootPath}`);
1049
1188
  try {
1050
1189
  activityMonitor.reset();
1051
- progressRenderer.reset();
1052
- worldState = await handleSubscribe(effectiveRootPath, selectedWorld.worldName, streaming, globalState, activityMonitor, progressRenderer, rl);
1190
+ statusLine.reset();
1191
+ worldState = await handleSubscribe(effectiveRootPath, selectedWorld.worldName, streaming, globalState, activityMonitor, statusLine, rl);
1053
1192
  currentWorldName = selectedWorld.worldName;
1054
1193
  console.log(success(`Connected to world: ${currentWorldName}`));
1055
1194
  if (selectedWorld.externalPath) {
@@ -1082,6 +1221,9 @@ async function runInteractiveMode(options) {
1082
1221
  console.log(); // Empty line before prompt
1083
1222
  rl.prompt();
1084
1223
  rl.on('line', async (input) => {
1224
+ if (globalState.hitlPromptActive) {
1225
+ return;
1226
+ }
1085
1227
  const trimmedInput = input.trim();
1086
1228
  if (!trimmedInput) {
1087
1229
  console.log(); // Empty line before prompt
@@ -1145,8 +1287,8 @@ async function runInteractiveMode(options) {
1145
1287
  // Subscribe to the new world
1146
1288
  logger.debug(`Subscribing to world: ${selectedWorld.worldName}...`);
1147
1289
  activityMonitor.reset();
1148
- progressRenderer.reset();
1149
- worldState = await handleSubscribe(effectiveRootPath, selectedWorld.worldName, streaming, globalState, activityMonitor, progressRenderer, rl);
1290
+ statusLine.reset();
1291
+ worldState = await handleSubscribe(effectiveRootPath, selectedWorld.worldName, streaming, globalState, activityMonitor, statusLine, rl);
1150
1292
  currentWorldName = selectedWorld.worldName;
1151
1293
  console.log(success(`Connected to world: ${currentWorldName}`));
1152
1294
  if (selectedWorld.externalPath) {
@@ -1354,6 +1496,7 @@ async function runInteractiveMode(options) {
1354
1496
  if (isExiting)
1355
1497
  return; // Prevent duplicate cleanup
1356
1498
  isExiting = true;
1499
+ statusLine.cleanup();
1357
1500
  console.log(`\n${boldCyan('Goodbye!')}`);
1358
1501
  if (worldState)
1359
1502
  cleanupWorldSubscription(worldState);
@@ -1363,6 +1506,7 @@ async function runInteractiveMode(options) {
1363
1506
  if (isExiting)
1364
1507
  return; // Prevent duplicate cleanup
1365
1508
  isExiting = true;
1509
+ statusLine.cleanup();
1366
1510
  console.log(`\n${boldCyan('Shutting down...')}`);
1367
1511
  console.log(`\n${boldCyan('Goodbye!')}`);
1368
1512
  if (worldState)
@@ -1373,6 +1517,7 @@ async function runInteractiveMode(options) {
1373
1517
  }
1374
1518
  catch (err) {
1375
1519
  console.error(boldRed('Error starting interactive mode:'), err instanceof Error ? err.message : err);
1520
+ statusLine.cleanup();
1376
1521
  rl.close();
1377
1522
  process.exit(1);
1378
1523
  }
@@ -8,9 +8,17 @@
8
8
  * - Category-based logging with configurable levels
9
9
  * - Health check endpoint and proper error handling
10
10
  * - Environment Variables: Automatically loads .env file for API keys and configuration
11
+ * - Controlled startup behavior for direct execution vs imported usage
12
+ * - Optional browser auto-open and process signal handler registration
11
13
  *
12
14
  * Configuration: AGENT_WORLD_DATA_PATH, LOG_LEVEL, LLM provider keys
13
15
  * Endpoints: /health + API routes from ./api.ts
16
+ *
17
+ * Recent Changes:
18
+ * - 2026-02-08: Fixed auto-run detection to only trigger on direct execution
19
+ * - 2026-02-08: Made browser auto-open configurable via AGENT_WORLD_AUTO_OPEN env var
20
+ * - 2026-02-08: Prevented duplicate process signal handler registration using WeakSet
21
+ * - 2026-02-08: Added shutdown guard to prevent race conditions during graceful shutdown
14
22
  */
15
23
  // Load environment variables from .env file
16
24
  import dotenv from 'dotenv';
@@ -32,6 +40,8 @@ const PORT = Number(process.env.PORT) || 0;
32
40
  const HOST = process.env.HOST || '127.0.0.1';
33
41
  // Create server logger after logger auto-initialization
34
42
  const serverLogger = createCategoryLogger('server');
43
+ // Track servers that have registered process handlers (prevents duplicate registration)
44
+ const serversWithHandlers = new WeakSet();
35
45
  // LLM provider configuration
36
46
  function configureLLMProvidersFromEnv() {
37
47
  const providers = [
@@ -114,7 +124,9 @@ app.use((req, res) => {
114
124
  res.status(404).json({ error: 'Endpoint not found', code: 'NOT_FOUND' });
115
125
  });
116
126
  // Server startup function
117
- export function startWebServer(port = PORT, host = HOST) {
127
+ export function startWebServer(port = PORT, host = HOST, options = {}) {
128
+ const openBrowser = options.openBrowser ?? false;
129
+ const registerProcessHandlers = options.registerProcessHandlers ?? false;
118
130
  return new Promise((resolve, reject) => {
119
131
  configureLLMProvidersFromEnv();
120
132
  // Initialize MCP registry
@@ -129,12 +141,23 @@ export function startWebServer(port = PORT, host = HOST) {
129
141
  // console.log(`📁 Serving static files from: ${path.join(__dirname, '../public')}`);
130
142
  // console.log(`🚀 HTTP server running with REST API and SSE chat`);
131
143
  resolve(server);
132
- open(url);
144
+ if (openBrowser) {
145
+ open(url).catch((error) => {
146
+ serverLogger.warn('Failed to open browser automatically', {
147
+ error: error instanceof Error ? error.message : String(error),
148
+ url
149
+ });
150
+ });
151
+ }
133
152
  }
134
153
  });
135
154
  server.on('error', reject);
136
155
  // Setup graceful shutdown handlers
156
+ let shuttingDown = false;
137
157
  const gracefulShutdown = async (signal) => {
158
+ if (shuttingDown)
159
+ return;
160
+ shuttingDown = true;
138
161
  serverLogger.info(`Received ${signal}, initiating graceful shutdown`);
139
162
  try {
140
163
  // Shutdown MCP servers first
@@ -159,29 +182,41 @@ export function startWebServer(port = PORT, host = HOST) {
159
182
  process.exit(1);
160
183
  }
161
184
  };
162
- // Register shutdown handlers
163
- process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
164
- process.on('SIGINT', () => gracefulShutdown('SIGINT'));
165
- // Handle uncaught exceptions and unhandled rejections
166
- process.on('uncaughtException', (error) => {
167
- serverLogger.error('Uncaught exception', { error: error.message });
168
- gracefulShutdown('uncaughtException');
169
- });
170
- process.on('unhandledRejection', (reason, promise) => {
171
- serverLogger.error('Unhandled rejection', {
172
- reason: reason instanceof Error ? reason.message : reason,
173
- promise: promise.toString()
185
+ if (registerProcessHandlers && !serversWithHandlers.has(server)) {
186
+ serversWithHandlers.add(server);
187
+ // Register shutdown handlers once
188
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
189
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
190
+ // Handle uncaught exceptions and unhandled rejections
191
+ process.on('uncaughtException', (error) => {
192
+ serverLogger.error('Uncaught exception', { error: error.message });
193
+ gracefulShutdown('uncaughtException');
174
194
  });
175
- });
195
+ process.on('unhandledRejection', (reason, promise) => {
196
+ serverLogger.error('Unhandled rejection', {
197
+ reason: reason instanceof Error ? reason.message : reason,
198
+ promise: promise.toString()
199
+ });
200
+ });
201
+ }
176
202
  });
177
203
  }
178
- // Direct execution handling - check both direct execution and npm bin execution
204
+ // Direct execution handling
179
205
  const currentFileUrl = import.meta.url;
180
- const entryPointUrl = pathToFileURL(path.resolve(process.argv[1])).href;
206
+ const entryPointUrl = process.argv[1]
207
+ ? pathToFileURL(path.resolve(process.argv[1])).href
208
+ : '';
181
209
  const isDirectExecution = currentFileUrl === entryPointUrl;
182
- const isServerBinCommand = process.argv[1].includes('agent-world-server') || currentFileUrl.includes('server/index.js');
183
- if (isDirectExecution || isServerBinCommand) {
184
- startWebServer()
210
+ const isBinExecution = process.argv[1]?.includes('agent-world-server') || false;
211
+ // Auto-open browser by default when launched via npx/bin, unless explicitly disabled
212
+ const shouldOpenBrowser = isBinExecution
213
+ ? process.env.AGENT_WORLD_AUTO_OPEN !== 'false'
214
+ : process.env.AGENT_WORLD_AUTO_OPEN === 'true';
215
+ if (isDirectExecution || isBinExecution) {
216
+ startWebServer(PORT, HOST, {
217
+ openBrowser: shouldOpenBrowser,
218
+ registerProcessHandlers: true
219
+ })
185
220
  .then(() => console.log('Server started successfully'))
186
221
  .catch((error) => {
187
222
  console.error('Failed to start server:', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-world",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "exports": {
@@ -17,32 +17,34 @@
17
17
  "type": "module",
18
18
  "workspaces": [
19
19
  "core",
20
- "react",
21
20
  "web"
22
21
  ],
23
22
  "bin": {
24
- "agent-world": "dist/cli/index.js",
23
+ "agent-world": "scripts/launch-electron.js",
24
+ "agent-world-cli": "dist/cli/index.js",
25
25
  "agent-world-server": "dist/server/index.js"
26
26
  },
27
27
  "scripts": {
28
- "prestart": "npm run build",
29
- "start": "node dist/server/index.js",
30
- "dev": "npm-run-all --parallel server:watch web:dev react:dev",
31
- "cli": "npm run cli:start",
32
- "cli:start": "node dist/cli/index.js",
33
- "cli:dev": "npx tsx cli/index.ts",
34
- "cli:watch": "npx tsx --watch cli/index.ts",
35
- "server": "npm run server:start",
36
- "server:start": "node dist/server/index.js",
28
+ "dev": "npm run web:dev",
29
+ "dev:reset": "for p in 3000 8080; do pids=$(lsof -tiTCP:$p -sTCP:LISTEN 2>/dev/null || true); if [ -n \"$pids\" ]; then kill $pids; fi; done",
30
+ "web:dev": "npm-run-all --parallel server:watch web:vite:wait",
31
+ "cli:dev": "npx tsx --watch cli/index.ts",
32
+ "electron:dev": "npm-run-all --sequential build:core electron:dev:watch",
33
+ "electron:dev:watch": "npm-run-all --parallel build:core:watch electron:vite",
34
+ "start": "npm run web:start",
35
+ "web:start": "npm run build && npm run server:start",
36
+ "cli:start": "npm run build && node dist/cli/index.js",
37
+ "electron:start": "npm run build:core && npm run start --prefix electron",
38
+ "build": "npm run build --workspace=core && tsc --project tsconfig.build.json && npm run build --workspace=web",
39
+ "build:core": "npm run build --workspace=core",
40
+ "build:core:watch": "npm run build --workspace=core -- --watch --preserveWatchOutput",
41
+ "check": "tsc --noEmit --project tsconfig.build.json && npm run check --workspace=core && npm run check --workspace=web",
37
42
  "server:dev": "npx tsx server/index.ts",
38
43
  "server:watch": "npx tsx --watch server/index.ts",
39
- "server:wait": "wait-on http://127.0.0.1:3000/health",
40
- "web:dev": "npm-run-all --sequential server:wait web:dev:direct",
41
- "web:dev:direct": "npm run dev --workspace=web",
42
- "web:watch": "npm-run-all --parallel server:watch web:dev:direct",
43
- "react:dev": "npm-run-all --parallel server:wait react:dev:direct",
44
- "react:dev:direct": "npm run dev --workspace=react",
45
- "react:watch": "npm-run-all --parallel server:watch react:dev:direct",
44
+ "server:start": "node dist/server/index.js",
45
+ "web:vite": "npm run dev --workspace=web",
46
+ "web:vite:wait": "wait-on tcp:127.0.0.1:3000 && npm run web:vite",
47
+ "electron:vite": "npm run dev --prefix electron",
46
48
  "test": "vitest run",
47
49
  "test:watch": "vitest",
48
50
  "test:ui": "vitest --ui",
@@ -51,8 +53,6 @@
51
53
  "test:e2e": "npx tsx tests/e2e/test-agent-response-rules.ts",
52
54
  "test:e2e:interactive": "npx tsx tests/e2e/test-agent-response-rules.ts -i",
53
55
  "integration": "vitest run --config vitest.integration.config.ts",
54
- "check": "tsc --noEmit --project tsconfig.build.json && npm run check --workspace=core && npm run check --workspace=web && npm run check --workspace=react",
55
- "build": "npm run build --workspace=core && tsc --project tsconfig.build.json && npm run build --workspace=web && npm run build --workspace=react",
56
56
  "pkill": "pkill -f tsx"
57
57
  },
58
58
  "description": "World-mediated agent management system with clean API surface",
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Launch Electron App - Simple launcher for agent-world desktop app
5
+ *
6
+ * Purpose:
7
+ * - Launch the Electron desktop app when running `agent-world` command
8
+ * - Ensures necessary builds exist before launching
9
+ *
10
+ * Features:
11
+ * - Checks for core build
12
+ * - Launches Electron app
13
+ * - Provides helpful error messages if builds are missing
14
+ *
15
+ * Implementation Notes:
16
+ * - Used as bin entry point in package.json
17
+ * - Replaces CLI as default when running `agent-world` command
18
+ */
19
+
20
+ import { spawn } from 'node:child_process';
21
+ import { fileURLToPath } from 'node:url';
22
+ import path from 'node:path';
23
+ import fs from 'node:fs';
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = path.dirname(__filename);
27
+ const projectRoot = path.resolve(__dirname, '..');
28
+
29
+ // Check if core build exists
30
+ const coreIndexPath = path.join(projectRoot, 'dist', 'core', 'index.js');
31
+ if (!fs.existsSync(coreIndexPath)) {
32
+ console.error('Error: Core build not found. Please run: npm run build');
33
+ process.exit(1);
34
+ }
35
+
36
+ // Check if electron directory exists
37
+ const electronPath = path.join(projectRoot, 'electron');
38
+ if (!fs.existsSync(electronPath)) {
39
+ console.error('Error: Electron app not found.');
40
+ process.exit(1);
41
+ }
42
+
43
+ // Launch Electron app
44
+ const electronMainPath = path.join(electronPath, 'main.js');
45
+ const electronProcess = spawn('npx', ['electron', electronMainPath], {
46
+ cwd: projectRoot,
47
+ stdio: 'inherit',
48
+ env: { ...process.env }
49
+ });
50
+
51
+ electronProcess.on('error', (error) => {
52
+ console.error('Failed to launch Electron app:', error);
53
+ process.exit(1);
54
+ });
55
+
56
+ electronProcess.on('exit', (code) => {
57
+ process.exit(code || 0);
58
+ });