@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.
- package/CONTRIBUTING.md +40 -355
- package/README.md +46 -113
- package/backend/lib/chat/stream-manager.ts +8 -0
- package/backend/lib/database/migrations/022_add_snapshot_changes_column.ts +35 -0
- package/backend/lib/database/migrations/index.ts +7 -0
- package/backend/lib/database/queries/snapshot-queries.ts +7 -4
- package/backend/lib/files/file-watcher.ts +34 -0
- package/backend/lib/mcp/config.ts +7 -3
- package/backend/lib/mcp/servers/helper.ts +25 -14
- package/backend/lib/mcp/servers/index.ts +7 -2
- package/backend/lib/project/status-manager.ts +6 -4
- package/backend/lib/snapshot/snapshot-service.ts +471 -316
- package/backend/lib/terminal/pty-session-manager.ts +1 -32
- package/backend/ws/chat/stream.ts +45 -2
- package/backend/ws/snapshot/restore.ts +77 -67
- package/frontend/lib/components/chat/ChatInterface.svelte +21 -14
- package/frontend/lib/components/chat/input/ChatInput.svelte +2 -2
- package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +1 -1
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +24 -12
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +12 -2
- package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +3 -8
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +222 -30
- package/frontend/lib/components/common/MonacoEditor.svelte +14 -0
- package/frontend/lib/components/common/xterm/XTerm.svelte +9 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +9 -0
- package/frontend/lib/components/git/DiffViewer.svelte +16 -2
- package/frontend/lib/components/history/HistoryModal.svelte +3 -4
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +59 -0
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +1 -1
- package/frontend/lib/components/terminal/Terminal.svelte +1 -7
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +11 -19
- package/frontend/lib/components/workspace/MobileNavigator.svelte +4 -15
- package/frontend/lib/components/workspace/PanelHeader.svelte +623 -616
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
- package/frontend/lib/components/workspace/panels/FilesPanel.svelte +3 -2
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +3 -2
- package/frontend/lib/services/notification/global-stream-monitor.ts +56 -16
- package/frontend/lib/services/snapshot/snapshot.service.ts +71 -32
- package/frontend/lib/stores/core/presence.svelte.ts +63 -1
- package/frontend/lib/stores/features/settings.svelte.ts +9 -1
- package/frontend/lib/stores/features/terminal.svelte.ts +6 -0
- package/frontend/lib/stores/ui/workspace.svelte.ts +4 -3
- package/package.json +1 -1
- package/shared/types/database/schema.ts +18 -0
- package/shared/types/stores/settings.ts +2 -0
- package/scripts/pre-publish-check.sh +0 -142
- package/scripts/setup-hooks.sh +0 -134
- package/scripts/validate-branch-name.sh +0 -47
- package/scripts/validate-commit-msg.sh +0 -42
package/README.md
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
> All-in-one web workspace for Claude Code & OpenCode
|
|
1
|
+
# Clopen
|
|
4
2
|
|
|
5
3
|
[](LICENSE)
|
|
6
4
|
[](https://bun.sh)
|
|
@@ -9,27 +7,28 @@
|
|
|
9
7
|
|
|
10
8
|
---
|
|
11
9
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
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
|
-
##
|
|
26
|
+
## Quick Start
|
|
28
27
|
|
|
29
28
|
### Prerequisites
|
|
30
29
|
|
|
31
|
-
- [Bun](https://bun.sh/) v1.2.12
|
|
32
|
-
- [Claude Code](https://github.com/anthropics/claude-code) and/or [OpenCode](https://opencode
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
### Usage
|
|
39
|
+
### Update
|
|
43
40
|
|
|
44
41
|
```bash
|
|
45
|
-
clopen
|
|
42
|
+
bun add -g @myrialabs/clopen
|
|
46
43
|
```
|
|
47
44
|
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
113
|
+
## License
|
|
185
114
|
|
|
186
115
|
MIT License - see [LICENSE](LICENSE) for details.
|
|
187
116
|
|
|
188
117
|
---
|
|
189
118
|
|
|
190
|
-
##
|
|
119
|
+
## Acknowledgments
|
|
191
120
|
|
|
192
121
|
- [Claude Code](https://github.com/anthropics/claude-code) by Anthropic
|
|
193
|
-
- [OpenCode](https://opencode.ai) by
|
|
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
|
-
##
|
|
128
|
+
## Support
|
|
129
|
+
|
|
130
|
+
If Clopen is useful to you, consider supporting its development:
|
|
200
131
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
88
|
-
|
|
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
|
-
//
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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'
|
|
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'
|
|
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
|
)) {
|