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 +114 -40
- package/dist/cli/index.js +210 -65
- package/dist/server/index.js +55 -20
- package/package.json +21 -21
- package/scripts/launch-electron.js +58 -0
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Agent World
|
|
2
2
|
|
|
3
|
+
[](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
|
-
- ✅
|
|
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 (
|
|
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
|
|
97
|
-
- Mid-text mentions (
|
|
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
|
|
170
|
+
## Development Scripts
|
|
137
171
|
|
|
138
|
-
Agent World
|
|
172
|
+
Agent World provides simple, consistent npm scripts for three main applications:
|
|
139
173
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
-
|
|
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
|
-
|
|
196
|
+
### Other Useful Scripts
|
|
154
197
|
```bash
|
|
155
|
-
#
|
|
156
|
-
npm run
|
|
157
|
-
npm
|
|
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
|
|
258
|
+
LOG_STORAGE_MIGRATION=info npm run web:dev
|
|
222
259
|
|
|
223
260
|
# MCP server problems
|
|
224
|
-
LOG_MCP=debug npm run
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (payload.type === '
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1026
|
-
worldState = await handleSubscribe(rootPath, options.world, streaming, globalState, activityMonitor,
|
|
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
|
-
|
|
1052
|
-
worldState = await handleSubscribe(effectiveRootPath, selectedWorld.worldName, streaming, globalState, activityMonitor,
|
|
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
|
-
|
|
1149
|
-
worldState = await handleSubscribe(effectiveRootPath, selectedWorld.worldName, streaming, globalState, activityMonitor,
|
|
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
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
204
|
+
// Direct execution handling
|
|
179
205
|
const currentFileUrl = import.meta.url;
|
|
180
|
-
const entryPointUrl =
|
|
206
|
+
const entryPointUrl = process.argv[1]
|
|
207
|
+
? pathToFileURL(path.resolve(process.argv[1])).href
|
|
208
|
+
: '';
|
|
181
209
|
const isDirectExecution = currentFileUrl === entryPointUrl;
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"dev": "npm-run-all --parallel server:watch web:
|
|
31
|
-
"cli": "
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
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:
|
|
40
|
-
"web:
|
|
41
|
-
"web:
|
|
42
|
-
"
|
|
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
|
+
});
|