@myrialabs/clopen 0.1.3 → 0.1.5

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.
Files changed (49) hide show
  1. package/CONTRIBUTING.md +40 -355
  2. package/README.md +46 -113
  3. package/backend/lib/chat/stream-manager.ts +8 -0
  4. package/backend/lib/database/migrations/022_add_snapshot_changes_column.ts +35 -0
  5. package/backend/lib/database/migrations/index.ts +7 -0
  6. package/backend/lib/database/queries/snapshot-queries.ts +7 -4
  7. package/backend/lib/files/file-watcher.ts +34 -0
  8. package/backend/lib/mcp/config.ts +7 -3
  9. package/backend/lib/mcp/servers/helper.ts +25 -14
  10. package/backend/lib/mcp/servers/index.ts +7 -2
  11. package/backend/lib/project/status-manager.ts +6 -4
  12. package/backend/lib/snapshot/snapshot-service.ts +471 -316
  13. package/backend/lib/terminal/pty-session-manager.ts +1 -32
  14. package/backend/ws/chat/stream.ts +45 -2
  15. package/backend/ws/snapshot/restore.ts +77 -67
  16. package/frontend/lib/components/chat/ChatInterface.svelte +21 -14
  17. package/frontend/lib/components/chat/input/ChatInput.svelte +2 -2
  18. package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +1 -1
  19. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +24 -12
  20. package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +12 -2
  21. package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +3 -8
  22. package/frontend/lib/components/checkpoint/TimelineModal.svelte +222 -30
  23. package/frontend/lib/components/common/MonacoEditor.svelte +14 -0
  24. package/frontend/lib/components/common/xterm/XTerm.svelte +9 -0
  25. package/frontend/lib/components/common/xterm/xterm-service.ts +9 -0
  26. package/frontend/lib/components/git/DiffViewer.svelte +16 -2
  27. package/frontend/lib/components/history/HistoryModal.svelte +3 -4
  28. package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +59 -0
  29. package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +1 -1
  30. package/frontend/lib/components/terminal/Terminal.svelte +1 -7
  31. package/frontend/lib/components/workspace/DesktopNavigator.svelte +11 -19
  32. package/frontend/lib/components/workspace/MobileNavigator.svelte +4 -15
  33. package/frontend/lib/components/workspace/PanelHeader.svelte +623 -616
  34. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
  35. package/frontend/lib/components/workspace/panels/FilesPanel.svelte +3 -2
  36. package/frontend/lib/components/workspace/panels/GitPanel.svelte +3 -2
  37. package/frontend/lib/services/notification/global-stream-monitor.ts +56 -16
  38. package/frontend/lib/services/snapshot/snapshot.service.ts +71 -32
  39. package/frontend/lib/stores/core/presence.svelte.ts +63 -1
  40. package/frontend/lib/stores/features/settings.svelte.ts +9 -1
  41. package/frontend/lib/stores/features/terminal.svelte.ts +6 -0
  42. package/frontend/lib/stores/ui/workspace.svelte.ts +4 -3
  43. package/package.json +1 -1
  44. package/shared/types/database/schema.ts +18 -0
  45. package/shared/types/stores/settings.ts +2 -0
  46. package/scripts/pre-publish-check.sh +0 -142
  47. package/scripts/setup-hooks.sh +0 -134
  48. package/scripts/validate-branch-name.sh +0 -47
  49. package/scripts/validate-commit-msg.sh +0 -42
package/README.md CHANGED
@@ -1,6 +1,4 @@
1
- # 🎯 Clopen
2
-
3
- > All-in-one web workspace for Claude Code & OpenCode
1
+ # Clopen
4
2
 
5
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
4
  [![Built with Bun](https://img.shields.io/badge/Built%20with-Bun-black)](https://bun.sh)
@@ -9,27 +7,28 @@
9
7
 
10
8
  ---
11
9
 
12
- ## Features
13
-
14
- - 👤 **Multi-Account Claude Code** - Manage multiple accounts (personal, work, team) and switch instantly per session, isolated under `~/.clopen/claude/user/` without touching system-level Claude config
15
- - 🔀 **Multi-Engine Support** - Switch between Claude Code and OpenCode
16
- - 💬 **AI Chat Interface** - Streaming responses with tool use visualization
17
- - 🔄 **Background Processing** - Chat, terminal, and other processes continue running even when you close the browser — come back later and pick up where you left off
18
- - 🌿 **Git-like Checkpoints** - Multi-branch undo/redo system with file and folder snapshots
19
- - 🌐 **Real Browser Preview** - Puppeteer-based Chromium rendering with WebCodecs streaming (80-90% bandwidth reduction), full click/type/scroll/drag interaction
20
- - 💻 **Integrated Terminal** - Multi-tab terminal with full PTY control
21
- - 📁 **File Management** - Directory browsing, live editing, and real-time file watching
22
- - 🗂️ **Git Management** - Full source control: staging, commits, branches, push/pull, stash, log, conflict resolution
23
- - 👥 **Real-time Collaboration** - Multiple users can work on the same project simultaneously
24
- - 🚇 **Built-in Cloudflare Tunnel** - Expose local projects publicly for testing and sharing
10
+ ## Features
11
+
12
+ - **Multi-Account Claude Code** - Manage multiple accounts (personal, work, team) and switch instantly per session, isolated under `~/.clopen/claude/user/` without touching system-level Claude config
13
+ - **Multi-Engine Support** - Switch between Claude Code and OpenCode
14
+ - **AI Chat Interface** - Streaming responses with tool use visualization
15
+ - **Background Processing** - Chat, terminal, and other processes continue running even when you close the browser — come back later and pick up where you left off
16
+ - **Git-like Checkpoints** - Multi-branch undo/redo system with file and folder snapshots
17
+ - **Real Browser Preview** - Puppeteer-based Chromium rendering with WebCodecs streaming (80-90% bandwidth reduction), full click/type/scroll/drag interaction
18
+ - **Integrated Terminal** - Multi-tab terminal with full PTY control
19
+ - **File Management** - Directory browsing, live editing, and real-time file watching
20
+ - **Git Management** - Full source control: staging, commits, branches, push/pull, stash, log, conflict resolution
21
+ - **Real-time Collaboration** - Multiple users can work on the same project simultaneously
22
+ - **Built-in Cloudflare Tunnel** - Expose local projects publicly for testing and sharing
23
+
25
24
  ---
26
25
 
27
- ## 🚀 Quick Start
26
+ ## Quick Start
28
27
 
29
28
  ### Prerequisites
30
29
 
31
- - [Bun](https://bun.sh/) v1.2.12 or higher
32
- - [Claude Code](https://github.com/anthropics/claude-code) and/or [OpenCode](https://opencode.ai) — required for AI chat functionality
30
+ - [Bun](https://bun.sh/) v1.2.12+
31
+ - [Claude Code](https://github.com/anthropics/claude-code) and/or [OpenCode](https://github.com/anomalyco/opencode) — required for AI chat functionality
33
32
 
34
33
  ### Installation
35
34
 
@@ -37,25 +36,25 @@
37
36
  bun add -g @myrialabs/clopen
38
37
  ```
39
38
 
40
- This installs dependencies, builds the frontend, and makes the `clopen` command available globally.
41
-
42
- ### Usage
39
+ ### Update
43
40
 
44
41
  ```bash
45
- clopen
42
+ bun add -g @myrialabs/clopen
46
43
  ```
47
44
 
48
- On first run, Clopen creates `.env` from `.env.example`, verifies the build, and starts the server on `http://localhost:9141`.
45
+ Same command as installation Bun will update to the latest version.
46
+
47
+ ### Usage
49
48
 
50
- **Configuration** — edit `.env` to customize:
51
49
  ```bash
52
- PORT=9141 # Server port
53
- NODE_ENV=production # Environment mode
50
+ clopen
54
51
  ```
55
52
 
53
+ Starts the server on `http://localhost:9141`.
54
+
56
55
  ---
57
56
 
58
- ## 🛠️ Development
57
+ ## Development
59
58
 
60
59
  ```bash
61
60
  git clone https://github.com/myrialabs/clopen.git
@@ -67,7 +66,7 @@ bun run check # Type checking
67
66
 
68
67
  ---
69
68
 
70
- ## 📚 Architecture
69
+ ## Architecture
71
70
 
72
71
  | Layer | Technology |
73
72
  |-------|-----------|
@@ -79,58 +78,22 @@ bun run check # Type checking
79
78
  | Terminal | bun-pty |
80
79
  | AI Engines | Claude Code + OpenCode |
81
80
 
82
- ### Engine Architecture
83
-
84
- Clopen uses an engine-agnostic adapter pattern — the frontend and stream manager are not tied to any specific AI tool:
85
-
86
- ```
87
- ┌─────────────────────────────────────────┐
88
- │ Stream Manager │
89
- └──────────────┬──────────────────────────┘
90
-
91
- ┌───────┴───────┐
92
- ▼ ▼
93
- ClaudeCodeEngine OpenCodeEngine
94
- ```
95
-
96
- Both engines normalize output to Claude SDK message format, ensuring a consistent experience regardless of which engine is selected.
81
+ Clopen uses an engine-agnostic adapter pattern — both engines normalize output to Claude SDK message format, ensuring a consistent experience regardless of which engine is selected.
97
82
 
98
83
  ---
99
84
 
100
- ## 🛣️ Planned Features
101
-
102
- - [ ] **Configurable MCP Servers** - Add, remove, enable, and disable Model Context Protocol servers through the UI
103
- - [ ] **Built-in Database Management** - Adminer/TablePlus-like interface
104
- - [ ] **Additional Preview Platforms** - Android, iOS, and Desktop app preview
105
- - [ ] **Enhanced Collaboration** - User authentication and permissions
106
- - [ ] **Plugin System** - Extensible architecture for community plugins
107
-
108
- ---
109
-
110
- ## 📖 Documentation
85
+ ## Documentation
111
86
 
112
87
  - [Technical Decisions](DECISIONS.md) - Architectural and technical decision log
88
+ - [Contributing](CONTRIBUTING.md) - How to contribute to this project
113
89
  - [Development Guidelines](CLAUDE.md) - Guidelines for working with Claude Code on this project
114
90
 
115
91
  ---
116
92
 
117
- ## 🤝 Contributing
118
-
119
- 1. Fork the repository
120
- 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
121
- 3. Make your changes
122
- 4. Run `bun run check` to ensure code quality
123
- 5. Commit your changes (`git commit -m 'Add amazing feature'`)
124
- 6. Push to the branch (`git push origin feature/amazing-feature`)
125
- 7. Open a Pull Request
126
-
127
- ---
128
-
129
- ## 🐛 Troubleshooting
93
+ ## Troubleshooting
130
94
 
131
95
  ### Port 9141 Already in Use
132
96
 
133
- Use a different port:
134
97
  ```bash
135
98
  clopen --port 9150
136
99
  ```
@@ -145,65 +108,35 @@ netstat -ano | findstr :9141
145
108
  taskkill /PID <PID> /F
146
109
  ```
147
110
 
148
- ### Claude Code Not Found
149
-
150
- ```bash
151
- # macOS / Linux / WSL
152
- curl -fsSL https://claude.ai/install.sh | bash
153
-
154
- # Windows PowerShell
155
- irm https://claude.ai/install.ps1 | iex
156
-
157
- # Verify
158
- claude --version
159
- ```
160
-
161
- For complete installation instructions, visit the [official setup guide](https://code.claude.com/docs/en/quickstart).
162
-
163
- ### OpenCode Not Found
164
-
165
- ```bash
166
- # macOS / Linux / WSL
167
- curl -fsSL https://opencode.ai/install | bash
168
-
169
- # Bun
170
- bun add -g opencode-ai
171
-
172
- # Verify
173
- opencode --version
174
- ```
175
-
176
- For complete installation instructions, visit the [official documentation](https://opencode.ai/docs).
177
-
178
- ### Browser Preview Issues
179
-
180
- Browser sessions are automatically managed via Puppeteer's APIs and cleaned up when the preview is closed, the application exits, or the session times out.
181
-
182
111
  ---
183
112
 
184
- ## 📄 License
113
+ ## License
185
114
 
186
115
  MIT License - see [LICENSE](LICENSE) for details.
187
116
 
188
117
  ---
189
118
 
190
- ## 🙏 Acknowledgments
119
+ ## Acknowledgments
191
120
 
192
121
  - [Claude Code](https://github.com/anthropics/claude-code) by Anthropic
193
- - [OpenCode](https://opencode.ai) by SST
122
+ - [OpenCode](https://opencode.ai) by Anomaly
194
123
  - [Bun](https://bun.sh/) runtime
195
124
  - [Svelte](https://svelte.dev/) framework
196
125
 
197
126
  ---
198
127
 
199
- ## 🔗 Links
128
+ ## Support
129
+
130
+ If Clopen is useful to you, consider supporting its development:
200
131
 
201
- - **Repository:** [github.com/myrialabs/clopen](https://github.com/myrialabs/clopen)
202
- - **Organization:** [MyriaLabs](https://github.com/myrialabs)
203
- - **Issues:** [Report a bug or request a feature](https://github.com/myrialabs/clopen/issues)
132
+ | Method | Address / Link |
133
+ |--------|----------------|
134
+ | Bitcoin (BTC) | `bc1qd9fyx4r84cce2a9hkjksetah802knadw5msls3` |
135
+ | Solana (SOL) | `Ev3P4KLF1PNC5C9rZYP8M3DdssyBQAQAiNJkvNmPQPVs` |
136
+ | Ethereum (ERC-20) | `0x61D826e5b666AA5345302EEEd485Acca39b1AFCF` |
137
+ | USDT (TRC-20) | `TLH49i3EoVKhFyLb6u2JUXZWScK7uzksdC` |
138
+ | Saweria | [saweria.co/myrialabs](https://saweria.co/myrialabs) |
204
139
 
205
140
  ---
206
141
 
207
- <div align="center">
208
- <sub>Built with ❤️ by MyriaLabs</sub>
209
- </div>
142
+ **Repository:** [github.com/myrialabs/clopen](https://github.com/myrialabs/clopen) · **Issues:** [Report a bug or request a feature](https://github.com/myrialabs/clopen/issues)
@@ -268,6 +268,14 @@ class StreamManager extends EventEmitter {
268
268
  // Track user message ID for stream-end snapshot capture
269
269
  let userMessageId: string | undefined;
270
270
 
271
+ // Initialize session baseline for snapshot system (non-blocking)
272
+ if (requestData.projectPath && requestData.chatSessionId) {
273
+ snapshotService.initializeSessionBaseline(
274
+ requestData.projectPath,
275
+ requestData.chatSessionId
276
+ ).catch(err => debug.error('snapshot', 'Failed to initialize session baseline:', err));
277
+ }
278
+
271
279
  try {
272
280
  const { projectPath, prompt, chatSessionId, engine: engineType = 'claude-code', model, temperature, claudeAccountId } = requestData;
273
281
 
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Migration: Add session_changes column to message_snapshots
3
+ * Purpose: Store session-scoped file changes (old/new hash per file)
4
+ * instead of full project state. This enables:
5
+ * - Session-scoped restore (only undo changes within the session)
6
+ * - Cross-session conflict detection
7
+ * - Dramatically reduced storage (only changed files stored)
8
+ *
9
+ * session_changes JSON format:
10
+ * {
11
+ * "filepath": { "oldHash": "sha256...", "newHash": "sha256..." },
12
+ * ...
13
+ * }
14
+ */
15
+
16
+ import type { DatabaseConnection } from '$shared/types/database/connection';
17
+ import { debug } from '$shared/utils/logger';
18
+
19
+ export const description = 'Add session_changes for session-scoped snapshot deltas';
20
+
21
+ export const up = (db: DatabaseConnection): void => {
22
+ debug.log('migration', 'Adding session_changes column to message_snapshots...');
23
+
24
+ db.exec(`
25
+ ALTER TABLE message_snapshots
26
+ ADD COLUMN session_changes TEXT
27
+ `);
28
+
29
+ debug.log('migration', 'session_changes column added');
30
+ };
31
+
32
+ export const down = (db: DatabaseConnection): void => {
33
+ debug.log('migration', 'Removing session_changes column...');
34
+ debug.warn('migration', 'Rollback not implemented for session_changes (SQLite limitation)');
35
+ };
@@ -20,6 +20,7 @@ import * as migration018 from './018_create_claude_accounts_table';
20
20
  import * as migration019 from './019_add_claude_account_to_sessions';
21
21
  import * as migration020 from './020_add_snapshot_tree_hash';
22
22
  import * as migration021 from './021_drop_prompt_templates_table';
23
+ import * as migration022 from './022_add_snapshot_changes_column';
23
24
 
24
25
  // Export all migrations in order
25
26
  export const migrations = [
@@ -148,6 +149,12 @@ export const migrations = [
148
149
  description: migration021.description,
149
150
  up: migration021.up,
150
151
  down: migration021.down
152
+ },
153
+ {
154
+ id: '022',
155
+ description: migration022.description,
156
+ up: migration022.up,
157
+ down: migration022.down
151
158
  }
152
159
  ];
153
160
 
@@ -27,6 +27,7 @@ export const snapshotQueries = {
27
27
  deletions?: number;
28
28
  branch_id?: string;
29
29
  tree_hash?: string; // Blob store tree hash (new format)
30
+ session_changes?: any; // SessionScopedChanges object
30
31
  }): MessageSnapshot {
31
32
  const db = getDatabase();
32
33
  const id = data.id || `snapshot_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
@@ -48,7 +49,8 @@ export const snapshotQueries = {
48
49
  deletions: data.deletions || 0,
49
50
  is_deleted: 0,
50
51
  branch_id: data.branch_id || null,
51
- tree_hash: data.tree_hash || null
52
+ tree_hash: data.tree_hash || null,
53
+ session_changes: data.session_changes ? JSON.stringify(data.session_changes) : null
52
54
  };
53
55
 
54
56
  db.prepare(`
@@ -57,8 +59,8 @@ export const snapshotQueries = {
57
59
  files_snapshot, project_metadata, created_at,
58
60
  snapshot_type, parent_snapshot_id, delta_changes,
59
61
  files_changed, insertions, deletions,
60
- is_deleted, branch_id, tree_hash
61
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
62
+ is_deleted, branch_id, tree_hash, session_changes
63
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?)
62
64
  `).run(
63
65
  snapshot.id,
64
66
  snapshot.message_id,
@@ -74,7 +76,8 @@ export const snapshotQueries = {
74
76
  snapshot.insertions,
75
77
  snapshot.deletions,
76
78
  snapshot.branch_id || null,
77
- snapshot.tree_hash || null
79
+ snapshot.tree_hash || null,
80
+ snapshot.session_changes || null
78
81
  );
79
82
 
80
83
  return snapshot;
@@ -82,6 +82,36 @@ interface ProjectWatcher {
82
82
  class FileWatcherManager {
83
83
  private watchers = new Map<string, ProjectWatcher>();
84
84
 
85
+ /**
86
+ * Per-project dirty file tracking for snapshot system.
87
+ * Accumulates changed file relative paths between snapshot captures.
88
+ */
89
+ private dirtyFiles = new Map<string, Set<string>>();
90
+
91
+ /**
92
+ * Get dirty files accumulated since last clear for a project.
93
+ */
94
+ getDirtyFiles(projectId: string): Set<string> {
95
+ return this.dirtyFiles.get(projectId) || new Set();
96
+ }
97
+
98
+ /**
99
+ * Clear dirty files after snapshot capture.
100
+ */
101
+ clearDirtyFiles(projectId: string): void {
102
+ this.dirtyFiles.delete(projectId);
103
+ }
104
+
105
+ /**
106
+ * Track a file as dirty for snapshot purposes.
107
+ */
108
+ private trackDirtyFile(projectId: string, relativePath: string): void {
109
+ if (!this.dirtyFiles.has(projectId)) {
110
+ this.dirtyFiles.set(projectId, new Set());
111
+ }
112
+ this.dirtyFiles.get(projectId)!.add(relativePath);
113
+ }
114
+
85
115
  /**
86
116
  * Start watching a project directory
87
117
  */
@@ -238,6 +268,10 @@ class FileWatcherManager {
238
268
  }
239
269
  }
240
270
 
271
+ // Track dirty file for snapshot system
272
+ const relativePath = relative(projectPath, fullPath).replace(/\\/g, '/');
273
+ this.trackDirtyFile(projectId, relativePath);
274
+
241
275
  // Create file change object
242
276
  const fileChange: FileChange = {
243
277
  path: fullPath,
@@ -8,7 +8,7 @@
8
8
  import type { McpSdkServerConfigWithInstance, McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
9
9
  import type { McpLocalConfig } from '@opencode-ai/sdk';
10
10
  import type { ServerConfig, ParsedMcpToolName, ServerName } from './types';
11
- import { serverRegistry } from './servers';
11
+ import { serverRegistry, serverFactories } from './servers';
12
12
  import { debug } from '$shared/utils/logger';
13
13
  import { resolve } from 'path';
14
14
  import { SERVER_ENV } from '../shared/env';
@@ -84,14 +84,18 @@ export const mcpServers: Record<string, ServerConfig & { instance: McpSdkServerC
84
84
  // ============================================================================
85
85
 
86
86
  /**
87
- * Get all enabled MCP servers for Claude SDK
87
+ * Get all enabled MCP servers for Claude SDK.
88
+ *
89
+ * Creates FRESH server instances each call so that concurrent streams
90
+ * each get their own Protocol — avoids "Already connected to a transport" errors.
88
91
  */
89
92
  export function getEnabledMcpServers(): Record<string, McpServerConfig> {
90
93
  const enabledServers: Record<string, McpServerConfig> = {};
91
94
 
92
95
  Object.entries(mcpServers).forEach(([serverName, serverConfig]) => {
93
96
  if (serverConfig.enabled) {
94
- enabledServers[serverName] = serverConfig.instance;
97
+ const factory = serverFactories[serverName as ServerName];
98
+ enabledServers[serverName] = factory ? factory() : serverConfig.instance;
95
99
  debug.log('mcp', `✓ Enabled MCP server: ${serverName}`);
96
100
  } else {
97
101
  debug.log('mcp', `✗ Disabled MCP server: ${serverName}`);
@@ -52,6 +52,8 @@ interface ServerWithMeta<
52
52
  TToolNames extends readonly string[]
53
53
  > {
54
54
  server: ReturnType<typeof createSdkMcpServer>;
55
+ /** Factory that creates a fresh SDK server instance (new Protocol, safe for concurrent use) */
56
+ createInstance: () => ReturnType<typeof createSdkMcpServer>;
55
57
  meta: {
56
58
  readonly name: TName;
57
59
  readonly tools: TToolNames;
@@ -84,32 +86,36 @@ export function defineServer<
84
86
  // Build raw tool definitions (engine-agnostic)
85
87
  const toolDefs: Record<string, RawToolDef> = {};
86
88
 
87
- // Convert tools object to SDK format (array of tools)
88
- const sdkTools = toolNames.map((toolName) => {
89
+ // Build raw tool definitions (engine-agnostic) and store for reuse
90
+ toolNames.forEach((toolName) => {
89
91
  const toolDef = config.tools[toolName] as any;
90
- // If schema is not provided, use empty object
91
92
  const schema = toolDef.schema || {};
92
93
 
93
- // Store raw definition for reuse
94
94
  toolDefs[toolName as string] = {
95
95
  description: toolDef.description,
96
96
  schema,
97
97
  handler: toolDef.handler,
98
98
  };
99
-
100
- return tool(toolName as string, toolDef.description, schema, toolDef.handler);
101
99
  });
102
100
 
103
- // Create SDK server
104
- const server = createSdkMcpServer({
105
- name: config.name,
106
- version: config.version,
107
- tools: sdkTools
108
- });
101
+ // Factory: creates a fresh SDK server instance with new Protocol (safe for concurrent use)
102
+ const createInstance = () => {
103
+ const sdkTools = toolNames.map((toolName) => {
104
+ const def = toolDefs[toolName as string];
105
+ return tool(toolName as string, def.description, def.schema, def.handler as any);
106
+ });
107
+
108
+ return createSdkMcpServer({
109
+ name: config.name,
110
+ version: config.version,
111
+ tools: sdkTools
112
+ });
113
+ };
109
114
 
110
- // Return server with metadata
115
+ // Return server with metadata and factory
111
116
  return {
112
- server,
117
+ server: createInstance(),
118
+ createInstance,
113
119
  meta: {
114
120
  name: config.name,
115
121
  tools: toolNames as any,
@@ -126,10 +132,12 @@ export function buildServerRegistries<
126
132
  >(servers: T) {
127
133
  const metadata = {} as any;
128
134
  const registry = {} as any;
135
+ const factories = {} as any;
129
136
 
130
137
  for (const server of servers) {
131
138
  metadata[server.meta.name] = server.meta;
132
139
  registry[server.meta.name] = server.server;
140
+ factories[server.meta.name] = server.createInstance;
133
141
  }
134
142
 
135
143
  return {
@@ -138,6 +146,9 @@ export function buildServerRegistries<
138
146
  },
139
147
  registry: registry as {
140
148
  [K in T[number]['meta']['name']]: Extract<T[number], { meta: { name: K } }>['server']
149
+ },
150
+ factories: factories as {
151
+ [K in T[number]['meta']['name']]: () => Extract<T[number], { meta: { name: K } }>['server']
141
152
  }
142
153
  };
143
154
  }
@@ -32,7 +32,7 @@ const allServers = [
32
32
  /**
33
33
  * Auto-build registries from server array
34
34
  */
35
- const { metadata, registry } = buildServerRegistries(allServers);
35
+ const { metadata, registry, factories } = buildServerRegistries(allServers);
36
36
 
37
37
  /**
38
38
  * Server Metadata Registry - Defines available servers and their tools
@@ -42,4 +42,9 @@ export const serverMetadata = metadata;
42
42
  /**
43
43
  * Server Instance Registry - Maps server names to SDK instances
44
44
  */
45
- export const serverRegistry = registry;
45
+ export const serverRegistry = registry;
46
+
47
+ /**
48
+ * Server Factory Registry - Creates fresh SDK instances (safe for concurrent streams)
49
+ */
50
+ export const serverFactories = factories;
@@ -18,11 +18,13 @@ const INTERACTIVE_TOOLS = new Set(['AskUserQuestion']);
18
18
  function detectStreamWaitingInput(stream: StreamState): boolean {
19
19
  if (stream.status !== 'active') return false;
20
20
 
21
+ // SSEEventData.message is SDKMessage: { type, message: { content: [...] } }
22
+ // Content blocks live at msg.message.content, NOT msg.content
21
23
  const answeredToolIds = new Set<string>();
22
24
  for (const event of stream.messages) {
23
25
  const msg = event.message;
24
- if (!msg || (msg as any).type !== 'user' || !(msg as any).content) continue;
25
- const content = Array.isArray((msg as any).content) ? (msg as any).content : [];
26
+ if (!msg || (msg as any).type !== 'user') continue;
27
+ const content = Array.isArray((msg as any).message?.content) ? (msg as any).message.content : [];
26
28
  for (const item of content) {
27
29
  if (item.type === 'tool_result' && item.tool_use_id) {
28
30
  answeredToolIds.add(item.tool_use_id);
@@ -32,8 +34,8 @@ function detectStreamWaitingInput(stream: StreamState): boolean {
32
34
 
33
35
  for (const event of stream.messages) {
34
36
  const msg = event.message;
35
- if (!msg || (msg as any).type !== 'assistant' || !(msg as any).content) continue;
36
- const content = Array.isArray((msg as any).content) ? (msg as any).content : [];
37
+ if (!msg || (msg as any).type !== 'assistant') continue;
38
+ const content = Array.isArray((msg as any).message?.content) ? (msg as any).message.content : [];
37
39
  if (content.some((item: any) =>
38
40
  item.type === 'tool_use' && INTERACTIVE_TOOLS.has(item.name) && item.id && !answeredToolIds.has(item.id)
39
41
  )) {