@swarmclawai/swarmclaw 1.5.43 → 1.5.45
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 +22 -9
- package/package.json +1 -1
- package/skills/swarmvault/SKILL.md +83 -0
- package/src/app/api/mcp-servers/route.test.ts +49 -3
- package/src/app/api/mcp-servers/route.ts +1 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +96 -0
- package/src/lib/providers/index.ts +75 -18
- package/src/lib/providers/provider-models.test.ts +44 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +7 -0
- package/src/lib/server/mcp-client.ts +1 -0
- package/src/lib/setup-defaults.ts +15 -11
- package/src/types/misc.ts +1 -0
package/README.md
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
<img src="https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/public/branding/swarmclaw-org-avatar.png" alt="SwarmClaw lobster logo" width="120" />
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
+
<p align="center"><strong>Self-hosted runtime for autonomous AI agents.</strong> Multi-provider, MCP-native, with memory, skills, delegation, and schedules.</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<img src="doc/assets/screenshots/org-chart.png" alt="SwarmClaw org chart with delegation and live agent activity" width="900" />
|
|
15
|
+
</p>
|
|
16
|
+
|
|
11
17
|
SwarmClaw is a self-hosted AI runtime for OpenClaw and multi-agent work. It helps you run autonomous agents and orchestrators with heartbeats, schedules, delegation, memory, runtime skills, and reviewed conversation-to-skill learning across OpenClaw gateways and other providers.
|
|
12
18
|
|
|
13
19
|
GitHub: https://github.com/swarmclawai/swarmclaw
|
|
@@ -175,6 +181,7 @@ Full hosted deployment guides live at https://swarmclaw.ai/docs/deployment
|
|
|
175
181
|
- **Memory**: hybrid recall, graph traversal, journaling, durable documents, project-scoped context, automatic reflection memory, communication preferences, profile and boundary memory, significant events, and open follow-up loops.
|
|
176
182
|
- **Wallets**: linked Base wallet generation, address management, approval-oriented limits, and agent payout identity.
|
|
177
183
|
- **Connectors**: Discord, Slack, Telegram, WhatsApp, Teams, Matrix, OpenClaw, SwarmDock, SwarmFeed, and more.
|
|
184
|
+
- **MCP Servers**: connect any Model Context Protocol server (stdio, SSE, or streamable HTTP) and inject its tools into agents alongside built-ins. Configure, test, and assign per-agent from the MCP Servers panel.
|
|
178
185
|
- **Extensions**: external tool extensions, UI modules, hooks, and install/update flows.
|
|
179
186
|
|
|
180
187
|
## What SwarmClaw Focuses On
|
|
@@ -389,6 +396,21 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
389
396
|
|
|
390
397
|
## Releases
|
|
391
398
|
|
|
399
|
+
### v1.5.45 Highlights
|
|
400
|
+
|
|
401
|
+
- **SwarmVault MCP preset**: a new "SwarmVault" Quick Setup chip in the MCP server sheet pre-fills `npx -y @swarmvaultai/cli mcp` over `stdio` and prompts for the vault directory. One click registers a SwarmVault knowledge vault as an MCP server; agents pick it up via the existing per-agent MCP server selector. SwarmVault docs: https://swarmvault.ai
|
|
402
|
+
- **`cwd` on stdio MCP servers**: `McpServerConfig` now has an optional `cwd` field. The MCP client passes it through to `StdioClientTransport` so servers that discover config from the working directory (SwarmVault, anything that reads from `cwd`-relative files) work correctly. Existing MCP servers are untouched (the field is optional and defaults to the SwarmClaw process cwd, which was the prior behaviour).
|
|
403
|
+
- **Bundled `swarmvault` skill**: ships at `skills/swarmvault/SKILL.md` and is auto-discovered alongside the other bundled skills. Captures the schema-first / graph-query-first conventions (read `swarmvault.schema.md` before compile or query work, treat `raw/` as immutable, prefer `graph query|path|explain` over grep, preserve `page_id` / `source_ids` / `node_ids` / `freshness` / `source_hashes` frontmatter, save high-value answers to `wiki/outputs/`). Pin it on any agent that talks to a SwarmVault vault. Optional and decoupled from the MCP integration.
|
|
404
|
+
|
|
405
|
+
### v1.5.44 Highlights
|
|
406
|
+
|
|
407
|
+
- **Model lists refreshed across every provider**: dropdowns now lead with the April-2026 flagship models instead of mid-2025 names. OpenAI goes to GPT-5.4 / 5.4-mini / 5.4-nano / 5.3 / o3-mini. Google and Gemini CLI lead with Gemini 3.1 Pro, Gemini 3 Flash, and 3.1 Flash-Lite, keeping 2.5 as a legacy fallback. xAI jumps from Grok 3 to Grok 4 plus the Grok 4 / 4.1 Fast reasoning and non-reasoning variants. Groq drops the deprecated `deepseek-r1-distill-llama-70b` and leads with Llama 4 Maverick, Llama 4 Scout, Kimi K2, and gpt-oss 120b/20b. Mistral moves to Magistral 1.2, Devstral 2, Codestral, and Mistral Small 4. Fireworks / Nebius / DeepInfra now lead with DeepSeek V3.2, Kimi K2.5, and Qwen 3 235B instead of the older R1-0528 checkpoint. Anthropic and Claude CLI reorder Opus 4.6 / Sonnet 4.6 / Haiku 4.5 newest-first. OpenCode Web refreshes its `providerID/modelID` seed list.
|
|
408
|
+
- **OpenRouter default set expanded**: was one model (`openai/gpt-4.1-mini`). Now ten flagship routes including `openrouter/auto`, Claude 4.6 Opus / Sonnet / Haiku, GPT-5.4, Gemini 3.1 Pro / 3 Flash, Grok 4, DeepSeek V3.2, and Llama 4 Maverick. Much better first-run experience for the "provider that routes to every other provider".
|
|
409
|
+
- **`DEFAULT_AGENTS` models refreshed**: 11 starter-agent models updated to match the new flagship lineups (OpenAI → GPT-5.4, xAI → Grok 4, Google / Gemini CLI → Gemini 3.1 Pro, Groq → Llama 4 Maverick, Fireworks / Nebius / DeepInfra → DeepSeek V3.2, OpenCode Web / Copilot CLI → Claude Sonnet 4.6, OpenRouter → Claude Sonnet 4.6). Starter agents created from the setup wizard now default to the right model out of the box.
|
|
410
|
+
- **Starter-agent tool bundles now include `droid_cli` and `copilot_cli`**: these delegation backends were added in v1.5.37 and v1.5.3 respectively but never made it into `STARTER_AGENT_TOOLS` / `BUILDER_AGENT_TOOLS`. Every starter kit (Sidekick, Researcher, Builder, Reviewer, Operator, OpenClaw fleet) now picks them up on new workspace creation.
|
|
411
|
+
- **DeepSeek note**: `deepseek-chat` and `deepseek-reasoner` remain the recommended model names — they are stable aliases that auto-track the current `V3.2` weights. No action required.
|
|
412
|
+
- **Registry sanity test**: added `provider-models.test.ts` which asserts every provider declares a non-empty deduplicated models array, matching metadata keys, and a working `handler.streamChat`. Guards against future copy-paste regressions in the registry.
|
|
413
|
+
|
|
392
414
|
### v1.5.43 Highlights
|
|
393
415
|
|
|
394
416
|
- **`/api/version` no longer 500s in Docker**: the route used to shell out to `git` at runtime, which fails in the production image because `.git/` is not copied. The route now returns 200 with `{ source: 'package', version }` from `package.json` when git metadata is unavailable, and `{ source: 'git', version, commit, ... }` when it is. `/api/version/update` short-circuits on Docker-style installs with a clear `no_git_metadata` reason instead of an opaque 500. ([#41](https://github.com/swarmclawai/swarmclaw/issues/41) Bug 1, reported by [@SteamedFish](https://github.com/SteamedFish).)
|
|
@@ -415,15 +437,6 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
415
437
|
- **Classifier timeout raised to 10 s**: 2 s was too tight for Ollama Cloud with a fully-configured agent (observed 4–6 s calls). Result caching means the latency tax only applies to first-seen messages.
|
|
416
438
|
- **Reflection memories dedup across runs**: the supervisor reflection writer now compares candidate notes against recent (last 7 days) reflection memories for the same agent and skips ones that have already been stored, stopping the ~7-per-turn rediscovery churn on top of the within-run dedup shipped in v1.5.38.
|
|
417
439
|
|
|
418
|
-
### v1.5.39 Highlights
|
|
419
|
-
|
|
420
|
-
- **Agents default to scoped tool access**: new agents (and existing agents whose `tools` list is non-empty) now only see the tools they've been given in the system prompt. This trims ~3 k input tokens per turn — an observed CEO/coordinator agent with 14 tools and 4 loaded skills went from 62 k to 38 k chars of system prompt. Opt back into the old firehose by toggling **Universal tool access** in the agent sheet's new "Context & Tool Access" section. Memory, context management, and `ask_human` are always included regardless of the scoped list.
|
|
421
|
-
- **Pinned skills budget hardening**: one long markdown skill was eating 24 k of a 62 k prompt. Inlined pinned-skill content is now capped at 3 k chars with a pointer to `use_skill` action="load" for the full guide, and auto-attached *learned* skills get a dedicated sub-budget (max 6 skills / 8 k chars) so they cannot dominate the main pinned-skills section.
|
|
422
|
-
- **OpenClaw chat fast-fails on dangling credentials**: v1.5.38 added gateway-side fast-fail; the chat streaming path now does the same, emitting a clear `err` event naming the missing credential instead of dialing the gateway unauthenticated and waiting 120 s for the timeout.
|
|
423
|
-
- **Queue: orphan-recovery auto-heals stale checkouts**: pre-1.5.38 storage could leave `queued` tasks with a stale `checkoutRunId` that `checkoutTask()` refused forever. Orphan recovery now clears the stale id in the same sweep that re-queues the task, and `reconcileFinishedRunningTasks` / agent-not-found / capability-mismatch paths also null out the checkout when they terminally fail a task.
|
|
424
|
-
- **Perf ring buffer raised to 2 000 entries**: queue/task repository events fire ~20 Hz during task processing and were evicting chat-execution/prompt perf entries out of the 200-entry buffer before they could be read. The larger buffer lets the perf viewer actually show a full turn.
|
|
425
|
-
- **Tests**: added regression tests for pre-1.5.38 stale-checkout orphan recovery and for the scoped-tool-access algorithm.
|
|
426
|
-
|
|
427
440
|
Older releases: https://swarmclaw.ai/docs/release-notes
|
|
428
441
|
|
|
429
442
|
- GitHub releases: https://github.com/swarmclawai/swarmclaw/releases
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.45",
|
|
4
4
|
"description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
|
|
5
5
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: swarmvault
|
|
3
|
+
description: Use when working with a SwarmVault knowledge vault (raw/, wiki/, swarmvault.schema.md). Establishes schema-first conventions and prefers graph queries over broad search.
|
|
4
|
+
homepage: https://swarmvault.ai
|
|
5
|
+
metadata:
|
|
6
|
+
openclaw:
|
|
7
|
+
capabilities: [knowledge-base, knowledge-graph, retrieval, vault]
|
|
8
|
+
requires:
|
|
9
|
+
bins: [npx]
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# SwarmVault
|
|
13
|
+
|
|
14
|
+
Use when the agent has a SwarmVault MCP server enabled (transport `stdio`, command `npx -y @swarmvaultai/cli mcp`) pointed at a vault directory.
|
|
15
|
+
|
|
16
|
+
A SwarmVault workspace is a three-layer knowledge system:
|
|
17
|
+
|
|
18
|
+
- `raw/` — immutable source inputs (PDFs, transcripts, code, emails, URLs, sheets). Never edit.
|
|
19
|
+
- `wiki/` — generated markdown owned by the agent and the SwarmVault compiler. Pages carry frontmatter (`page_id`, `source_ids`, `node_ids`, `freshness`, `source_hashes`).
|
|
20
|
+
- `state/` — generated indexes, graphs, and approvals. Treat as opaque output of `compile`.
|
|
21
|
+
|
|
22
|
+
The vault contract lives in `swarmvault.schema.md` at the workspace root. The vault config lives in `swarmvault.config.json`.
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
1. **Read `swarmvault.schema.md` first** before any compile or query work. It defines categories, naming, freshness rules, and grounding conventions for this specific vault.
|
|
27
|
+
2. **Read `wiki/graph/report.md` before broad file searching** when it exists; otherwise start with `wiki/index.md`. Both summarize the vault structure so you don't re-scan everything.
|
|
28
|
+
3. **Treat `raw/` as immutable.** Never edit, rename, or delete files there. New sources go through `ingest`.
|
|
29
|
+
4. **Treat `wiki/` as compiler-owned.** Edits should preserve frontmatter fields exactly: `page_id`, `source_ids`, `node_ids`, `freshness`, `source_hashes`. If those drift, the next `compile` will overwrite or flag the page.
|
|
30
|
+
5. **Prefer graph queries over grep/glob** for "how does X relate to Y" or "what depends on Z" questions. The vault's typed graph is more reliable than text search.
|
|
31
|
+
6. **Save high-value answers** to `wiki/outputs/` (use the `query` or `explore` tools) instead of leaving them only in chat. That way they become first-class vault content for next time.
|
|
32
|
+
|
|
33
|
+
## Tool Palette
|
|
34
|
+
|
|
35
|
+
The SwarmVault MCP server exposes the following tools (names are prefixed by SwarmClaw with `mcp_<sanitized server name>_`, e.g. `mcp_SwarmVault_query_vault`). Match the user's intent to the closest tool:
|
|
36
|
+
|
|
37
|
+
Vault inspection:
|
|
38
|
+
- `workspace_info` — return current vault paths and high-level counts. Use this first when you've never seen this vault.
|
|
39
|
+
- `list_sources` — list source manifests under `raw/`.
|
|
40
|
+
- `search_pages` — full-text search across compiled wiki pages.
|
|
41
|
+
- `read_page` — read a specific wiki page by its `wiki/`-relative path.
|
|
42
|
+
|
|
43
|
+
Graph (prefer over grep for relational questions):
|
|
44
|
+
- `graph_report` — machine-readable graph report and trust artifact. Read this before broad searching.
|
|
45
|
+
- `query_graph` — traverse the graph from search seeds without calling an LLM provider.
|
|
46
|
+
- `get_node` — explain a graph node, its page, community, neighbors, and group patterns.
|
|
47
|
+
- `get_neighbors` — neighbors of a node or page target.
|
|
48
|
+
- `get_hyperedges` — list graph hyperedges, optionally filtered.
|
|
49
|
+
- `shortest_path` — shortest path between two graph targets.
|
|
50
|
+
- `god_nodes` — highest-connectivity nodes (the vault's hubs).
|
|
51
|
+
- `blast_radius` — impact analysis: what depends on this file or module?
|
|
52
|
+
|
|
53
|
+
Question answering:
|
|
54
|
+
- `query_vault` — natural-language question against the vault. Returns grounded citations. Pass `save: true` to persist the answer to `wiki/outputs/`.
|
|
55
|
+
|
|
56
|
+
Ingest and maintenance:
|
|
57
|
+
- `ingest_input` — add a file path or URL to `raw/` and register it as a managed source.
|
|
58
|
+
- `compile_vault` — re-derive `wiki/` pages, graph, and search index. Run after ingest, after schema changes, or when freshness is stale.
|
|
59
|
+
- `lint_vault` — anti-drift and vault health checks.
|
|
60
|
+
|
|
61
|
+
If the MCP server is unavailable but the agent has a `shell` or `execute` tool, the same operations are available via `swarmvault <subcommand>` (or `npx -y @swarmvaultai/cli <subcommand>`) with the working directory set to the vault root.
|
|
62
|
+
|
|
63
|
+
## Workflow
|
|
64
|
+
|
|
65
|
+
For a fresh question against the vault:
|
|
66
|
+
|
|
67
|
+
1. Call `workspace_info` if you haven't already, then read `swarmvault.schema.md`. If `wiki/graph/report.md` or `wiki/index.md` exists, skim it.
|
|
68
|
+
2. Use `query_vault` (or `query_graph` / `get_node` / `shortest_path` for relational questions). Cite returned `source_ids` and `node_ids`.
|
|
69
|
+
3. If the answer reveals a gap, propose `ingest_input` for the missing source, then `compile_vault`.
|
|
70
|
+
4. Save the final answer with `query_vault` `save: true` so it becomes vault content under `wiki/outputs/`.
|
|
71
|
+
|
|
72
|
+
For a new source the user mentions:
|
|
73
|
+
|
|
74
|
+
1. `ingest_input` the file/URL.
|
|
75
|
+
2. `compile_vault` to derive new wiki pages, graph, and search index.
|
|
76
|
+
3. `lint_vault` to check frontmatter and links.
|
|
77
|
+
4. Skim the new pages in `wiki/sources/` and confirm provenance.
|
|
78
|
+
|
|
79
|
+
## Boundaries
|
|
80
|
+
|
|
81
|
+
- Don't run `compile` against an unreviewed change to `swarmvault.schema.md` — `lint` first.
|
|
82
|
+
- Don't promote candidate pages (`wiki/candidates/`) to `wiki/concepts/` or `wiki/entities/` without the user's confirmation; the approval flow exists for a reason.
|
|
83
|
+
- Don't push the vault graph to Neo4j or export to Obsidian without an explicit ask.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
2
4
|
import path from 'node:path'
|
|
3
5
|
import { fileURLToPath } from 'node:url'
|
|
4
6
|
import test, { afterEach } from 'node:test'
|
|
@@ -57,12 +59,12 @@ test('MCP server routes exercise a live stdio server end to end', async () => {
|
|
|
57
59
|
assert.equal(healthResponse.status, 200)
|
|
58
60
|
const health = await healthResponse.json() as Record<string, unknown>
|
|
59
61
|
assert.equal(health.ok, true)
|
|
60
|
-
assert.deepEqual(health.tools, ['mcp_smoke_ping', 'mcp_smoke_echo'])
|
|
62
|
+
assert.deepEqual(health.tools, ['mcp_smoke_ping', 'mcp_smoke_echo', 'mcp_smoke_cwd_check'])
|
|
61
63
|
|
|
62
64
|
const toolsResponse = await listMcpTools(new Request(`http://local/api/mcp-servers/${serverId}/tools`), routeParams(serverId))
|
|
63
65
|
assert.equal(toolsResponse.status, 200)
|
|
64
66
|
const tools = await toolsResponse.json() as Array<Record<string, unknown>>
|
|
65
|
-
assert.deepEqual(tools.map((tool) => tool.name), ['ping', 'echo'])
|
|
67
|
+
assert.deepEqual(tools.map((tool) => tool.name), ['ping', 'echo', 'cwd_check'])
|
|
66
68
|
|
|
67
69
|
const invokeResponse = await invokeMcpTool(new Request(`http://local/api/mcp-servers/${serverId}/invoke`, {
|
|
68
70
|
method: 'POST',
|
|
@@ -85,7 +87,7 @@ test('MCP server routes exercise a live stdio server end to end', async () => {
|
|
|
85
87
|
assert.equal(conformanceResponse.status, 200)
|
|
86
88
|
const conformance = await conformanceResponse.json() as Record<string, unknown>
|
|
87
89
|
assert.equal(conformance.ok, true)
|
|
88
|
-
assert.equal(conformance.toolsCount,
|
|
90
|
+
assert.equal(conformance.toolsCount, 3)
|
|
89
91
|
assert.equal(conformance.smokeToolName, 'ping')
|
|
90
92
|
|
|
91
93
|
const updateResponse = await updateMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`, {
|
|
@@ -104,6 +106,50 @@ test('MCP server routes exercise a live stdio server end to end', async () => {
|
|
|
104
106
|
assert.equal(loadMcpServers()[serverId], undefined)
|
|
105
107
|
})
|
|
106
108
|
|
|
109
|
+
test('cwd is persisted and forwarded to the spawned MCP server', async () => {
|
|
110
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mcp-cwd-'))
|
|
111
|
+
// Resolve symlinks (macOS /var → /private/var) so the comparison matches what the child process reports.
|
|
112
|
+
const resolvedTmpDir = fs.realpathSync(tmpDir)
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const createResponse = await createMcpServer(new Request('http://local/api/mcp-servers', {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'content-type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
name: 'cwd-smoke',
|
|
120
|
+
transport: 'stdio',
|
|
121
|
+
command: process.execPath,
|
|
122
|
+
args: [fixturePath],
|
|
123
|
+
cwd: resolvedTmpDir,
|
|
124
|
+
}),
|
|
125
|
+
}))
|
|
126
|
+
assert.equal(createResponse.status, 200)
|
|
127
|
+
const created = await createResponse.json() as Record<string, unknown>
|
|
128
|
+
const serverId = String(created.id)
|
|
129
|
+
assert.equal(created.cwd, resolvedTmpDir, 'POST should persist cwd on the saved record')
|
|
130
|
+
|
|
131
|
+
const detailResponse = await getMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`), routeParams(serverId))
|
|
132
|
+
const detail = await detailResponse.json() as Record<string, unknown>
|
|
133
|
+
assert.equal(detail.cwd, resolvedTmpDir, 'GET should return the persisted cwd')
|
|
134
|
+
|
|
135
|
+
const invokeResponse = await invokeMcpTool(new Request(`http://local/api/mcp-servers/${serverId}/invoke`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: { 'content-type': 'application/json' },
|
|
138
|
+
body: JSON.stringify({ toolName: 'cwd_check', args: '{}' }),
|
|
139
|
+
}), routeParams(serverId))
|
|
140
|
+
assert.equal(invokeResponse.status, 200)
|
|
141
|
+
const invokePayload = await invokeResponse.json() as Record<string, unknown>
|
|
142
|
+
assert.equal(invokePayload.ok, true)
|
|
143
|
+
assert.equal(
|
|
144
|
+
invokePayload.text,
|
|
145
|
+
`cwd: ${resolvedTmpDir}`,
|
|
146
|
+
'Spawned MCP server should report the cwd we configured, not SwarmClaw\'s process cwd',
|
|
147
|
+
)
|
|
148
|
+
} finally {
|
|
149
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
107
153
|
test('MCP invoke route validates required fields before connecting', async () => {
|
|
108
154
|
const serverId = 'mcp-validate-smoke'
|
|
109
155
|
const servers = loadMcpServers()
|
|
@@ -4,11 +4,40 @@ import { useState } from 'react'
|
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
6
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
7
|
+
import { HintTip } from '@/components/shared/hint-tip'
|
|
7
8
|
import { api } from '@/lib/app/api-client'
|
|
8
9
|
import { toast } from 'sonner'
|
|
9
10
|
import type { McpServerConfig, McpTransport } from '@/types'
|
|
10
11
|
import { useMountedRef } from '@/hooks/use-mounted-ref'
|
|
11
12
|
|
|
13
|
+
interface McpPreset {
|
|
14
|
+
id: string
|
|
15
|
+
label: string
|
|
16
|
+
description: string
|
|
17
|
+
helpUrl?: string
|
|
18
|
+
transport: McpTransport
|
|
19
|
+
command?: string
|
|
20
|
+
args?: string[]
|
|
21
|
+
needsCwd?: boolean
|
|
22
|
+
cwdHint?: string
|
|
23
|
+
defaultName: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MCP_PRESETS: McpPreset[] = [
|
|
27
|
+
{
|
|
28
|
+
id: 'swarmvault',
|
|
29
|
+
label: 'SwarmVault',
|
|
30
|
+
description: 'Local-first knowledge vault. Point this at a directory containing swarmvault.config.json.',
|
|
31
|
+
helpUrl: 'https://swarmvault.ai',
|
|
32
|
+
transport: 'stdio',
|
|
33
|
+
command: 'npx',
|
|
34
|
+
args: ['-y', '@swarmvaultai/cli', 'mcp'],
|
|
35
|
+
needsCwd: true,
|
|
36
|
+
cwdHint: 'Absolute path to a SwarmVault workspace (the directory containing swarmvault.config.json). Run `npx @swarmvaultai/cli init` there first if you haven\'t.',
|
|
37
|
+
defaultName: 'SwarmVault',
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
|
|
12
41
|
function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
13
42
|
editing: McpServerConfig | null
|
|
14
43
|
onClose: () => void
|
|
@@ -19,6 +48,8 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
|
19
48
|
const [transport, setTransport] = useState<McpTransport>(editing?.transport || 'stdio')
|
|
20
49
|
const [command, setCommand] = useState(editing?.command || '')
|
|
21
50
|
const [args, setArgs] = useState(editing?.args?.join(', ') || '')
|
|
51
|
+
const [cwd, setCwd] = useState(editing?.cwd || '')
|
|
52
|
+
const [activePresetId, setActivePresetId] = useState<string | null>(null)
|
|
22
53
|
const [url, setUrl] = useState(editing?.url || '')
|
|
23
54
|
const [envText, setEnvText] = useState(
|
|
24
55
|
editing?.env ? Object.entries(editing.env).map(([k, v]) => `${k}=${v}`).join('\n') : '',
|
|
@@ -61,6 +92,7 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
|
61
92
|
if (transport === 'stdio') {
|
|
62
93
|
data.command = command.trim()
|
|
63
94
|
data.args = args.trim() ? args.split(',').map((a) => a.trim()).filter(Boolean) : []
|
|
95
|
+
data.cwd = cwd.trim() || undefined
|
|
64
96
|
} else {
|
|
65
97
|
data.url = url.trim()
|
|
66
98
|
}
|
|
@@ -122,6 +154,16 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
|
122
154
|
|
|
123
155
|
const canSave = name.trim() && (transport === 'stdio' ? command.trim() : url.trim())
|
|
124
156
|
|
|
157
|
+
const applyPreset = (preset: McpPreset) => {
|
|
158
|
+
setActivePresetId(preset.id)
|
|
159
|
+
setTransport(preset.transport)
|
|
160
|
+
if (preset.command !== undefined) setCommand(preset.command)
|
|
161
|
+
if (preset.args !== undefined) setArgs(preset.args.join(', '))
|
|
162
|
+
if (!name.trim()) setName(preset.defaultName)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const activePreset = activePresetId ? MCP_PRESETS.find((p) => p.id === activePresetId) ?? null : null
|
|
166
|
+
|
|
125
167
|
const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
|
|
126
168
|
const labelClass = "block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3"
|
|
127
169
|
|
|
@@ -134,6 +176,44 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
|
134
176
|
<p className="text-[14px] text-text-3">Configure an MCP server to provide tools to agents</p>
|
|
135
177
|
</div>
|
|
136
178
|
|
|
179
|
+
{!editing && MCP_PRESETS.length > 0 && (
|
|
180
|
+
<div className="mb-8">
|
|
181
|
+
<label className={labelClass}>Quick Setup</label>
|
|
182
|
+
<div className="flex flex-wrap gap-2">
|
|
183
|
+
{MCP_PRESETS.map((preset) => {
|
|
184
|
+
const isActive = activePresetId === preset.id
|
|
185
|
+
return (
|
|
186
|
+
<button
|
|
187
|
+
key={preset.id}
|
|
188
|
+
type="button"
|
|
189
|
+
onClick={() => applyPreset(preset)}
|
|
190
|
+
className={`py-2 px-4 rounded-[12px] border text-[13px] font-600 cursor-pointer transition-all ${
|
|
191
|
+
isActive
|
|
192
|
+
? 'border-accent-bright bg-accent-bright/10 text-accent-bright'
|
|
193
|
+
: 'border-white/[0.08] bg-transparent text-text-2 hover:bg-surface-2'
|
|
194
|
+
}`}
|
|
195
|
+
style={{ fontFamily: 'inherit' }}
|
|
196
|
+
title={preset.description}
|
|
197
|
+
>
|
|
198
|
+
{preset.label}
|
|
199
|
+
</button>
|
|
200
|
+
)
|
|
201
|
+
})}
|
|
202
|
+
</div>
|
|
203
|
+
{activePreset && (
|
|
204
|
+
<p className="mt-3 text-[12px] text-text-3">
|
|
205
|
+
{activePreset.description}
|
|
206
|
+
{activePreset.helpUrl && (
|
|
207
|
+
<>
|
|
208
|
+
{' '}
|
|
209
|
+
<a href={activePreset.helpUrl} target="_blank" rel="noopener noreferrer" className="text-accent-bright hover:underline">Learn more</a>
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
</p>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
|
|
137
217
|
<div className="mb-8">
|
|
138
218
|
<label className={labelClass}>Name</label>
|
|
139
219
|
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Filesystem Server" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
@@ -165,6 +245,22 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
|
|
|
165
245
|
</label>
|
|
166
246
|
<input type="text" value={args} onChange={(e) => setArgs(e.target.value)} placeholder="e.g. /path/to/dir, --verbose" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
167
247
|
</div>
|
|
248
|
+
<div className="mb-8">
|
|
249
|
+
<label className={labelClass}>
|
|
250
|
+
<span className="inline-flex items-center gap-2">
|
|
251
|
+
Working Directory <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
252
|
+
<HintTip text={activePreset?.cwdHint || 'Working directory for the spawned process. Useful when the MCP server discovers config from cwd (e.g. SwarmVault).'} />
|
|
253
|
+
</span>
|
|
254
|
+
</label>
|
|
255
|
+
<input
|
|
256
|
+
type="text"
|
|
257
|
+
value={cwd}
|
|
258
|
+
onChange={(e) => setCwd(e.target.value)}
|
|
259
|
+
placeholder={activePreset?.needsCwd ? 'e.g. /Users/you/my-vault' : 'e.g. /path/to/working/dir'}
|
|
260
|
+
className={inputClass}
|
|
261
|
+
style={{ fontFamily: 'inherit' }}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
168
264
|
</>
|
|
169
265
|
) : (
|
|
170
266
|
<div className="mb-8">
|
|
@@ -55,7 +55,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
55
55
|
'claude-cli': {
|
|
56
56
|
id: 'claude-cli',
|
|
57
57
|
name: 'Claude Code CLI',
|
|
58
|
-
models: ['claude-
|
|
58
|
+
models: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5'],
|
|
59
59
|
requiresApiKey: false,
|
|
60
60
|
requiresEndpoint: false,
|
|
61
61
|
handler: { streamChat: streamClaudeCliChat },
|
|
@@ -63,7 +63,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
63
63
|
'codex-cli': {
|
|
64
64
|
id: 'codex-cli',
|
|
65
65
|
name: 'OpenAI Codex CLI',
|
|
66
|
-
models: ['gpt-5.
|
|
66
|
+
models: ['gpt-5.4-codex', 'gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5.1-codex', 'gpt-5-codex-mini'],
|
|
67
67
|
requiresApiKey: false,
|
|
68
68
|
requiresEndpoint: false,
|
|
69
69
|
handler: { streamChat: streamCodexCliChat },
|
|
@@ -71,7 +71,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
71
71
|
openai: {
|
|
72
72
|
id: 'openai',
|
|
73
73
|
name: 'OpenAI',
|
|
74
|
-
models: ['gpt-
|
|
74
|
+
models: ['gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gpt-5.3', 'o3-mini', 'gpt-4.1', 'gpt-4.1-mini'],
|
|
75
75
|
requiresApiKey: true,
|
|
76
76
|
requiresEndpoint: false,
|
|
77
77
|
handler: { streamChat: streamOpenAiChat },
|
|
@@ -79,7 +79,15 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
79
79
|
openrouter: {
|
|
80
80
|
id: 'openrouter',
|
|
81
81
|
name: 'OpenRouter',
|
|
82
|
-
models: [
|
|
82
|
+
models: [
|
|
83
|
+
'openrouter/auto',
|
|
84
|
+
'anthropic/claude-opus-4.6', 'anthropic/claude-sonnet-4.6', 'anthropic/claude-haiku-4.5',
|
|
85
|
+
'openai/gpt-5.4', 'openai/gpt-5.4-mini',
|
|
86
|
+
'google/gemini-3.1-pro', 'google/gemini-3-flash',
|
|
87
|
+
'x-ai/grok-4',
|
|
88
|
+
'deepseek/deepseek-v3.2',
|
|
89
|
+
'meta-llama/llama-4-maverick-17b-128e-instruct',
|
|
90
|
+
],
|
|
83
91
|
requiresApiKey: true,
|
|
84
92
|
requiresEndpoint: false,
|
|
85
93
|
defaultEndpoint: 'https://openrouter.ai/api/v1',
|
|
@@ -96,7 +104,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
96
104
|
anthropic: {
|
|
97
105
|
id: 'anthropic',
|
|
98
106
|
name: 'Anthropic',
|
|
99
|
-
models: ['claude-
|
|
107
|
+
models: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5'],
|
|
100
108
|
requiresApiKey: true,
|
|
101
109
|
requiresEndpoint: false,
|
|
102
110
|
handler: { streamChat: streamAnthropicChat },
|
|
@@ -132,7 +140,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
132
140
|
'opencode-cli': {
|
|
133
141
|
id: 'opencode-cli',
|
|
134
142
|
name: 'OpenCode CLI',
|
|
135
|
-
models: ['claude-sonnet-4-6', 'gpt-4
|
|
143
|
+
models: ['claude-opus-4-6', 'claude-sonnet-4-6', 'gpt-5.4', 'gemini-3.1-pro', 'gemini-3-flash'],
|
|
136
144
|
requiresApiKey: false,
|
|
137
145
|
requiresEndpoint: false,
|
|
138
146
|
handler: { streamChat: streamOpenCodeCliChat },
|
|
@@ -142,7 +150,11 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
142
150
|
name: 'OpenCode Web',
|
|
143
151
|
// OpenCode addresses models as `providerID/modelID`. Free-text entry is
|
|
144
152
|
// supported; these defaults seed the dropdown with common combinations.
|
|
145
|
-
models: [
|
|
153
|
+
models: [
|
|
154
|
+
'anthropic/claude-opus-4-6', 'anthropic/claude-sonnet-4-6', 'anthropic/claude-haiku-4-5',
|
|
155
|
+
'openai/gpt-5.4', 'openai/gpt-5.4-mini',
|
|
156
|
+
'google/gemini-3.1-pro', 'google/gemini-3-flash',
|
|
157
|
+
],
|
|
146
158
|
requiresApiKey: false,
|
|
147
159
|
optionalApiKey: true,
|
|
148
160
|
requiresEndpoint: true,
|
|
@@ -152,7 +164,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
152
164
|
'gemini-cli': {
|
|
153
165
|
id: 'gemini-cli',
|
|
154
166
|
name: 'Gemini CLI',
|
|
155
|
-
models: ['gemini-
|
|
167
|
+
models: ['gemini-3.1-pro', 'gemini-3-flash', 'gemini-3.1-flash-lite', 'gemini-2.5-pro', 'gemini-2.5-flash'],
|
|
156
168
|
requiresApiKey: false,
|
|
157
169
|
requiresEndpoint: false,
|
|
158
170
|
handler: { streamChat: streamGeminiCliChat },
|
|
@@ -160,7 +172,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
160
172
|
'copilot-cli': {
|
|
161
173
|
id: 'copilot-cli',
|
|
162
174
|
name: 'GitHub Copilot CLI',
|
|
163
|
-
models: ['claude-sonnet-4-
|
|
175
|
+
models: ['claude-sonnet-4-6', 'gpt-5.4', 'gemini-3.1-pro'],
|
|
164
176
|
requiresApiKey: false,
|
|
165
177
|
requiresEndpoint: false,
|
|
166
178
|
handler: { streamChat: streamCopilotCliChat },
|
|
@@ -202,7 +214,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
202
214
|
google: {
|
|
203
215
|
id: 'google',
|
|
204
216
|
name: 'Google Gemini',
|
|
205
|
-
models: ['gemini-
|
|
217
|
+
models: ['gemini-3.1-pro', 'gemini-3-flash', 'gemini-3.1-flash-lite', 'gemini-2.5-pro', 'gemini-2.5-flash'],
|
|
206
218
|
requiresApiKey: true,
|
|
207
219
|
requiresEndpoint: false,
|
|
208
220
|
defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai',
|
|
@@ -219,6 +231,9 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
219
231
|
deepseek: {
|
|
220
232
|
id: 'deepseek',
|
|
221
233
|
name: 'DeepSeek',
|
|
234
|
+
// Stable aliases: 'deepseek-chat' is the non-thinking mode of the latest
|
|
235
|
+
// V-series (currently V3.2), 'deepseek-reasoner' is the thinking mode.
|
|
236
|
+
// DeepSeek rotates the underlying weights without changing these names.
|
|
222
237
|
models: ['deepseek-chat', 'deepseek-reasoner'],
|
|
223
238
|
requiresApiKey: true,
|
|
224
239
|
requiresEndpoint: false,
|
|
@@ -236,7 +251,15 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
236
251
|
groq: {
|
|
237
252
|
id: 'groq',
|
|
238
253
|
name: 'Groq',
|
|
239
|
-
models: [
|
|
254
|
+
models: [
|
|
255
|
+
'meta-llama/llama-4-maverick-17b-128e-instruct',
|
|
256
|
+
'meta-llama/llama-4-scout-17b-16e-instruct',
|
|
257
|
+
'moonshotai/kimi-k2-instruct-0905',
|
|
258
|
+
'openai/gpt-oss-120b',
|
|
259
|
+
'openai/gpt-oss-20b',
|
|
260
|
+
'qwen/qwen3-32b',
|
|
261
|
+
'llama-3.3-70b-versatile',
|
|
262
|
+
],
|
|
240
263
|
requiresApiKey: true,
|
|
241
264
|
requiresEndpoint: false,
|
|
242
265
|
defaultEndpoint: 'https://api.groq.com/openai/v1',
|
|
@@ -253,7 +276,14 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
253
276
|
together: {
|
|
254
277
|
id: 'together',
|
|
255
278
|
name: 'Together AI',
|
|
256
|
-
models: [
|
|
279
|
+
models: [
|
|
280
|
+
'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8',
|
|
281
|
+
'meta-llama/Llama-4-Scout-17B-16E-Instruct',
|
|
282
|
+
'deepseek-ai/DeepSeek-V3.2',
|
|
283
|
+
'deepseek-ai/DeepSeek-R1',
|
|
284
|
+
'Qwen/Qwen3-235B-A22B-Instruct',
|
|
285
|
+
'moonshotai/Kimi-K2-Instruct-0905',
|
|
286
|
+
],
|
|
257
287
|
requiresApiKey: true,
|
|
258
288
|
requiresEndpoint: false,
|
|
259
289
|
defaultEndpoint: 'https://api.together.xyz/v1',
|
|
@@ -270,7 +300,16 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
270
300
|
mistral: {
|
|
271
301
|
id: 'mistral',
|
|
272
302
|
name: 'Mistral AI',
|
|
273
|
-
models: [
|
|
303
|
+
models: [
|
|
304
|
+
'magistral-medium-1.2',
|
|
305
|
+
'magistral-small-1.2',
|
|
306
|
+
'devstral-medium',
|
|
307
|
+
'devstral-small-1.1',
|
|
308
|
+
'codestral-latest',
|
|
309
|
+
'mistral-small-4',
|
|
310
|
+
'mistral-large-latest',
|
|
311
|
+
'ministral-3b-latest',
|
|
312
|
+
],
|
|
274
313
|
requiresApiKey: true,
|
|
275
314
|
requiresEndpoint: false,
|
|
276
315
|
defaultEndpoint: 'https://api.mistral.ai/v1',
|
|
@@ -287,7 +326,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
287
326
|
xai: {
|
|
288
327
|
id: 'xai',
|
|
289
328
|
name: 'xAI (Grok)',
|
|
290
|
-
models: ['grok-
|
|
329
|
+
models: ['grok-4', 'grok-4-fast-reasoning', 'grok-4-fast-non-reasoning', 'grok-4-1-fast-reasoning', 'grok-4-1-fast-non-reasoning'],
|
|
291
330
|
requiresApiKey: true,
|
|
292
331
|
requiresEndpoint: false,
|
|
293
332
|
defaultEndpoint: 'https://api.x.ai/v1',
|
|
@@ -304,7 +343,13 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
304
343
|
fireworks: {
|
|
305
344
|
id: 'fireworks',
|
|
306
345
|
name: 'Fireworks AI',
|
|
307
|
-
models: [
|
|
346
|
+
models: [
|
|
347
|
+
'accounts/fireworks/models/deepseek-v3p2',
|
|
348
|
+
'accounts/fireworks/models/kimi-k2-instruct-0905',
|
|
349
|
+
'accounts/fireworks/models/glm-5',
|
|
350
|
+
'accounts/fireworks/models/qwen3-235b-a22b',
|
|
351
|
+
'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
352
|
+
],
|
|
308
353
|
requiresApiKey: true,
|
|
309
354
|
requiresEndpoint: false,
|
|
310
355
|
defaultEndpoint: 'https://api.fireworks.ai/inference/v1',
|
|
@@ -321,7 +366,13 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
321
366
|
nebius: {
|
|
322
367
|
id: 'nebius',
|
|
323
368
|
name: 'Nebius',
|
|
324
|
-
models: [
|
|
369
|
+
models: [
|
|
370
|
+
'deepseek-ai/DeepSeek-V3.2',
|
|
371
|
+
'moonshotai/Kimi-K2-Instruct',
|
|
372
|
+
'Qwen/Qwen3-235B-A22B',
|
|
373
|
+
'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8',
|
|
374
|
+
'deepseek-ai/DeepSeek-R1',
|
|
375
|
+
],
|
|
325
376
|
requiresApiKey: true,
|
|
326
377
|
requiresEndpoint: false,
|
|
327
378
|
defaultEndpoint: 'https://api.tokenfactory.nebius.com/v1',
|
|
@@ -338,7 +389,13 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
338
389
|
deepinfra: {
|
|
339
390
|
id: 'deepinfra',
|
|
340
391
|
name: 'DeepInfra',
|
|
341
|
-
models: [
|
|
392
|
+
models: [
|
|
393
|
+
'deepseek-ai/DeepSeek-V3.2',
|
|
394
|
+
'moonshotai/Kimi-K2-Instruct',
|
|
395
|
+
'Qwen/Qwen3-235B-A22B',
|
|
396
|
+
'meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8',
|
|
397
|
+
'deepseek-ai/DeepSeek-R1',
|
|
398
|
+
],
|
|
342
399
|
requiresApiKey: true,
|
|
343
400
|
requiresEndpoint: false,
|
|
344
401
|
defaultEndpoint: 'https://api.deepinfra.com/v1/openai',
|
|
@@ -364,7 +421,7 @@ export const PROVIDERS: Record<string, BuiltinProviderConfig> = {
|
|
|
364
421
|
'gemini-3-flash-preview', 'gemma3',
|
|
365
422
|
'devstral-2', 'devstral-small-2', 'ministral-3', 'mistral-large-3',
|
|
366
423
|
'gpt-oss', 'cogito-2.1', 'rnj-1', 'nemotron-3-nano',
|
|
367
|
-
'llama3.3', 'llama3.2',
|
|
424
|
+
'llama3.3', 'llama3.2',
|
|
368
425
|
],
|
|
369
426
|
requiresApiKey: false,
|
|
370
427
|
optionalApiKey: true,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { PROVIDERS } from '@/lib/providers'
|
|
4
|
+
|
|
5
|
+
describe('PROVIDERS model list sanity', () => {
|
|
6
|
+
it('every provider declares a non-empty models array', () => {
|
|
7
|
+
for (const [id, entry] of Object.entries(PROVIDERS)) {
|
|
8
|
+
assert.ok(Array.isArray(entry.models), `${id}: models must be an array`)
|
|
9
|
+
assert.ok(entry.models.length > 0, `${id}: models must be non-empty`)
|
|
10
|
+
}
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('every model id is a non-empty trimmed string', () => {
|
|
14
|
+
for (const [id, entry] of Object.entries(PROVIDERS)) {
|
|
15
|
+
for (const model of entry.models) {
|
|
16
|
+
assert.equal(typeof model, 'string', `${id}: model entries must be strings`)
|
|
17
|
+
assert.ok(model.length > 0, `${id}: model id must be non-empty`)
|
|
18
|
+
assert.equal(model, model.trim(), `${id}: model id must be trimmed (got "${model}")`)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('no duplicate model ids within a single provider', () => {
|
|
24
|
+
for (const [id, entry] of Object.entries(PROVIDERS)) {
|
|
25
|
+
const seen = new Set<string>()
|
|
26
|
+
for (const model of entry.models) {
|
|
27
|
+
assert.ok(!seen.has(model), `${id}: duplicate model id "${model}"`)
|
|
28
|
+
seen.add(model)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('every provider declares the required metadata fields', () => {
|
|
34
|
+
for (const [id, entry] of Object.entries(PROVIDERS)) {
|
|
35
|
+
assert.equal(typeof entry.id, 'string', `${id}: id must be a string`)
|
|
36
|
+
assert.equal(entry.id, id, `${id}: id field must match registry key`)
|
|
37
|
+
assert.equal(typeof entry.name, 'string', `${id}: name must be a string`)
|
|
38
|
+
assert.ok(entry.name.length > 0, `${id}: name must be non-empty`)
|
|
39
|
+
assert.equal(typeof entry.requiresApiKey, 'boolean', `${id}: requiresApiKey must be boolean`)
|
|
40
|
+
assert.equal(typeof entry.requiresEndpoint, 'boolean', `${id}: requiresEndpoint must be boolean`)
|
|
41
|
+
assert.equal(typeof entry.handler?.streamChat, 'function', `${id}: handler.streamChat must be a function`)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -23,5 +23,12 @@ server.registerTool('echo', {
|
|
|
23
23
|
content: [{ type: 'text', text: `echo: ${message}` }],
|
|
24
24
|
}))
|
|
25
25
|
|
|
26
|
+
server.registerTool('cwd_check', {
|
|
27
|
+
description: 'Returns the working directory the server was launched in',
|
|
28
|
+
inputSchema: {},
|
|
29
|
+
}, async () => ({
|
|
30
|
+
content: [{ type: 'text', text: `cwd: ${process.cwd()}` }],
|
|
31
|
+
}))
|
|
32
|
+
|
|
26
33
|
const transport = new StdioServerTransport()
|
|
27
34
|
await server.connect(transport)
|
|
@@ -77,6 +77,7 @@ export async function connectMcpServer(
|
|
|
77
77
|
transport = new StdioClientTransport({
|
|
78
78
|
command: config.command!,
|
|
79
79
|
args: config.args ?? [],
|
|
80
|
+
cwd: config.cwd || undefined,
|
|
80
81
|
env: { ...process.env, ...config.env } as Record<string, string>,
|
|
81
82
|
})
|
|
82
83
|
} else if (config.transport === 'sse') {
|
|
@@ -361,6 +361,8 @@ export const STARTER_AGENT_TOOLS = [
|
|
|
361
361
|
'codex_cli',
|
|
362
362
|
'opencode_cli',
|
|
363
363
|
'gemini_cli',
|
|
364
|
+
'copilot_cli',
|
|
365
|
+
'droid_cli',
|
|
364
366
|
'cursor_cli',
|
|
365
367
|
'qwen_code_cli',
|
|
366
368
|
'openclaw_workspace',
|
|
@@ -545,6 +547,8 @@ const BUILDER_AGENT_TOOLS = [
|
|
|
545
547
|
'codex_cli',
|
|
546
548
|
'opencode_cli',
|
|
547
549
|
'gemini_cli',
|
|
550
|
+
'copilot_cli',
|
|
551
|
+
'droid_cli',
|
|
548
552
|
'cursor_cli',
|
|
549
553
|
'qwen_code_cli',
|
|
550
554
|
]
|
|
@@ -761,21 +765,21 @@ export const DEFAULT_AGENTS: Record<SetupProvider, DefaultAgentConfig> = {
|
|
|
761
765
|
name: 'OpenCode Web',
|
|
762
766
|
description: 'A helpful assistant powered by a remote OpenCode HTTP server.',
|
|
763
767
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
764
|
-
model: 'anthropic/claude-sonnet-4-
|
|
768
|
+
model: 'anthropic/claude-sonnet-4-6',
|
|
765
769
|
tools: STARTER_AGENT_TOOLS,
|
|
766
770
|
},
|
|
767
771
|
'gemini-cli': {
|
|
768
772
|
name: 'Gemini CLI',
|
|
769
773
|
description: 'A helpful assistant powered by Gemini CLI.',
|
|
770
774
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
771
|
-
model: 'gemini-
|
|
775
|
+
model: 'gemini-3.1-pro',
|
|
772
776
|
tools: STARTER_AGENT_TOOLS,
|
|
773
777
|
},
|
|
774
778
|
'copilot-cli': {
|
|
775
779
|
name: 'Copilot CLI',
|
|
776
780
|
description: 'A helpful assistant powered by GitHub Copilot CLI.',
|
|
777
781
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
778
|
-
model: 'claude-sonnet-4-
|
|
782
|
+
model: 'claude-sonnet-4-6',
|
|
779
783
|
tools: STARTER_AGENT_TOOLS,
|
|
780
784
|
},
|
|
781
785
|
'droid-cli': {
|
|
@@ -817,21 +821,21 @@ export const DEFAULT_AGENTS: Record<SetupProvider, DefaultAgentConfig> = {
|
|
|
817
821
|
name: 'Atlas',
|
|
818
822
|
description: 'A helpful GPT-powered assistant.',
|
|
819
823
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
820
|
-
model: 'gpt-
|
|
824
|
+
model: 'gpt-5.4',
|
|
821
825
|
tools: STARTER_AGENT_TOOLS,
|
|
822
826
|
},
|
|
823
827
|
openrouter: {
|
|
824
828
|
name: 'Router',
|
|
825
829
|
description: 'A helpful assistant powered through OpenRouter.',
|
|
826
830
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
827
|
-
model: '
|
|
831
|
+
model: 'anthropic/claude-sonnet-4.6',
|
|
828
832
|
tools: STARTER_AGENT_TOOLS,
|
|
829
833
|
},
|
|
830
834
|
google: {
|
|
831
835
|
name: 'Gemini',
|
|
832
836
|
description: 'A helpful Gemini-powered assistant.',
|
|
833
837
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
834
|
-
model: 'gemini-
|
|
838
|
+
model: 'gemini-3.1-pro',
|
|
835
839
|
tools: STARTER_AGENT_TOOLS,
|
|
836
840
|
},
|
|
837
841
|
deepseek: {
|
|
@@ -845,7 +849,7 @@ export const DEFAULT_AGENTS: Record<SetupProvider, DefaultAgentConfig> = {
|
|
|
845
849
|
name: 'Bolt',
|
|
846
850
|
description: 'A low-latency assistant powered by Groq.',
|
|
847
851
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
848
|
-
model: 'llama-
|
|
852
|
+
model: 'meta-llama/llama-4-maverick-17b-128e-instruct',
|
|
849
853
|
tools: STARTER_AGENT_TOOLS,
|
|
850
854
|
},
|
|
851
855
|
together: {
|
|
@@ -866,28 +870,28 @@ export const DEFAULT_AGENTS: Record<SetupProvider, DefaultAgentConfig> = {
|
|
|
866
870
|
name: 'Grok',
|
|
867
871
|
description: 'A helpful assistant powered by xAI Grok.',
|
|
868
872
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
869
|
-
model: 'grok-
|
|
873
|
+
model: 'grok-4',
|
|
870
874
|
tools: STARTER_AGENT_TOOLS,
|
|
871
875
|
},
|
|
872
876
|
fireworks: {
|
|
873
877
|
name: 'Spark',
|
|
874
878
|
description: 'A helpful assistant powered by Fireworks AI.',
|
|
875
879
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
876
|
-
model: 'accounts/fireworks/models/deepseek-
|
|
880
|
+
model: 'accounts/fireworks/models/deepseek-v3p2',
|
|
877
881
|
tools: STARTER_AGENT_TOOLS,
|
|
878
882
|
},
|
|
879
883
|
nebius: {
|
|
880
884
|
name: 'Nebius Agent',
|
|
881
885
|
description: 'A helpful assistant powered by Nebius.',
|
|
882
886
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
883
|
-
model: 'deepseek-ai/DeepSeek-
|
|
887
|
+
model: 'deepseek-ai/DeepSeek-V3.2',
|
|
884
888
|
tools: STARTER_AGENT_TOOLS,
|
|
885
889
|
},
|
|
886
890
|
deepinfra: {
|
|
887
891
|
name: 'DeepInfra Agent',
|
|
888
892
|
description: 'A helpful assistant powered by DeepInfra.',
|
|
889
893
|
systemPrompt: SWARMCLAW_ASSISTANT_PROMPT,
|
|
890
|
-
model: 'deepseek-ai/DeepSeek-
|
|
894
|
+
model: 'deepseek-ai/DeepSeek-V3.2',
|
|
891
895
|
tools: STARTER_AGENT_TOOLS,
|
|
892
896
|
},
|
|
893
897
|
ollama: {
|
package/src/types/misc.ts
CHANGED
|
@@ -613,6 +613,7 @@ export interface McpServerConfig {
|
|
|
613
613
|
transport: McpTransport
|
|
614
614
|
command?: string // for stdio transport
|
|
615
615
|
args?: string[] // for stdio transport
|
|
616
|
+
cwd?: string // working directory for stdio transport (e.g. SwarmVault vault dir)
|
|
616
617
|
url?: string // for sse/streamable-http transport
|
|
617
618
|
env?: Record<string, string> // environment variables
|
|
618
619
|
headers?: Record<string, string> // HTTP headers for sse/streamable-http
|