@swarm-agent/sdk 0.1.47 → 0.1.48

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/HEADLESS_UI.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Build a real headless UI with the SDK
2
2
 
3
+ For planned SDK completeness, safe tool control and non-production qualification, see the [five-step headless application plan](HEADLESS_PLAN.md). That roadmap is not a claim of implemented capabilities.
4
+
3
5
  ## Trust boundary
4
6
 
5
7
  Run the administrative Node SDK in your backend-for-frontend **inside the same container as swarmd**, using its configured private Unix socket. Do not mount/export that socket, put daemon credentials in browser code, or forward arbitrary browser-selected daemon paths. Authenticate your app's users, check Origin/CSRF, and allowlist app operations. Never log credential request bodies. The separate `--container-sdk-port` listener remains session-only: it cannot onboard providers, change settings, or create workspaces.
package/README.md CHANGED
@@ -49,19 +49,282 @@ explicit refresh, not timer polling. Live agent verification is a separate step.
49
49
 
50
50
  ## Quick Start
51
51
 
52
- ### 1. Initialize Client
52
+ ### Chat Flow vs. Autonomous Orchestration Flow
53
+
54
+ Swarm supports two distinct modes of execution depending on your application goals:
55
+
56
+ 1. **Interactive Chat Flow (`client.chat` / `client.sessions`)**:
57
+ - Best for conversational assistants, interactive Q&A, and direct tool calling.
58
+ - A standalone session has one primary workspace, can use additional authorized workspaces, and follows normal worktree/permission/plan rules. It does not carry a project's high-level Markdown context.
59
+ - Use `client.chat.createSession()`, `client.chat.run()`, and `client.chat.stream()`.
60
+
61
+ 2. **Autonomous Orchestration Flow (`client.projects` / `client.workers` / `client.deploy`)**:
62
+ - Best for multi-agent background tasks, codebase modifications, and durable pipelines.
63
+ - Deploys specialized subagent fleets (Coder, Finder, Designer, Router, Compact) across bounded repository worktrees with structured checkpoint contracts.
64
+ - **Project → orchestrator conversations → delegated tasks → task sessions.** Project Markdown and linked workspaces provide high-level context; each deployed task resolves its own authorized execution workspace.
65
+ - Use `client.projects.listConversations(projectId)` and `createConversation(projectId)` for the session picker and New Conversation button. These call canonical `/v3/projects/{id}/sessions`, not generic workspace session creation. `getOrchestratorSession` reuses an existing conversation or creates one; it does not mean there can only be one.
66
+ - Use `client.projects.createTask()` / `listTasks()` for custom task organization. Task-linked sessions must be continued through `reopenTask` with exact revision guards, not ordinary chat messages.
67
+ - Naming a workspace session “Orchestrator” does not grant orchestration authority. Use the project conversation helper; do not supply a workspace path or spoof task metadata.
68
+ - Models and thinking resolve from daemon account settings. Conversation creation must not overwrite provider/model preferences.
69
+
70
+ ---
71
+
72
+ ### 1. Initialize Client & Auto-Configure Credentials
73
+
74
+ The SDK can automatically discover credentials from your environment or Codex OAuth:
53
75
 
54
76
  ```typescript
55
- import { createSwarmClient } from '@swarm-agent/sdk';
77
+ import { SwarmClient } from '@swarm-agent/sdk';
78
+
79
+ const client = new SwarmClient({
80
+ baseUrl: process.env.SWARM_API_URL || 'http://127.0.0.1:5555',
81
+ socketPath: process.env.SWARM_SOCKET_PATH, // e.g. /var/lib/swarmd/local-transport/api.sock
82
+ });
83
+
84
+ // 1. Ensure headless identity is bootstrapped
85
+ await client.onboarding.ensureBootstrapped({ username: 'developer', swarm_name: 'My Swarm' });
86
+
87
+ // 2. Auto-configure credentials from environment variables:
88
+ // (checks OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY/GOOGLE_API_KEY, DEEPSEEK_API_KEY, etc.)
89
+ // When found, automatically applies verified provider model recommendations to the fleet & router!
90
+ const auth = await client.auth.autoConfigure();
91
+ console.log(`Provider: ${auth.primaryProvider} (Fleet configured: ${auth.fleetApplied})`);
92
+
93
+ // Or if using OpenAI Codex OAuth device flow:
94
+ if (!auth.primaryProvider) {
95
+ await client.auth.codex.loginDevice({
96
+ onCode: (code) => console.log(`Open ${code.verification_url} and enter ${code.user_code}`),
97
+ });
98
+ await client.settings.applyProviderFleet('codex');
99
+ }
100
+ ```
101
+
102
+ ---
103
+
104
+ ### 2. Fleet & Router Model Settings
105
+
106
+ Swarm automatically optimizes models for each specific role in the agent fleet. You can customize them or apply verified recommendations in a single call:
107
+
108
+ ```typescript
109
+ // Apply verified recommendations for a provider across the fleet:
110
+ // Sets Swarm Action (auto), Swarm Plan (plan), Coder, Finder, Designer, Router, and Compact.
111
+ await client.settings.applyProviderFleet('codex');
112
+
113
+ // Inspect current fleet configuration
114
+ const settings = await client.settings.agentModels();
115
+ console.log('Action Model:', settings.agent_model_settings.swarm.action.model);
116
+ console.log('Router Model:', settings.agent_model_settings.system_agents.router.model);
117
+ console.log('Coder Model:', settings.agent_model_settings.system_agents.coder.model);
118
+
119
+ // Customize individual slots:
120
+ await client.settings.setSystemAgentModel('router', {
121
+ provider: 'codex',
122
+ model: 'gpt-6-luna',
123
+ thinking: 'low',
124
+ });
125
+
126
+ // Restore system defaults at any time:
127
+ await client.settings.restoreDefaults();
128
+ ```
129
+
130
+ ---
131
+
132
+ ### 3. Explicit permission decisions
133
+
134
+ Use `describePermission(record)` to render tool details, conservative per-request `actions`,
135
+ question IDs, options and custom-response fields. Treat all text/arguments as untrusted text,
136
+ not HTML. `parseError` leaves malformed questions deny-only. These helpers do not grant authority:
137
+ the backend enforces policy, project scope and specialized plan/proposal acceptance.
138
+
139
+ ```typescript
140
+ import { describePermission, answerPermission } from '@swarm-agent/sdk';
141
+
142
+ const pending = await client.permissions.listSessionPending(sessionId);
143
+ for (const permission of pending) {
144
+ renderPermission(describePermission(permission)); // Your UI; does not approve anything.
145
+ }
146
+
147
+ // Call from a user submit handler, with actual user-entered input:
148
+ async function submitAnswer(permission, userInput) {
149
+ const options = answerPermission(permission, userInput);
150
+ return await client.permissions.resolve(sessionId, permission.id, 'allow_once', options);
151
+ }
152
+ // Single question: userInput is a string (choice value or custom text).
153
+ // Structured questions: { questionId: 'actual answer', q_2: 'custom text' }.
154
+ // answerPermission serializes the backend reason/answers-map contract, not approved_arguments.answer.
155
+ // Ordinary tools: resolve(..., 'allow_once') or resolve(..., 'deny') only after a user click.
156
+ ```
157
+
158
+ `resolve` returns the confirmed permission record. Transport failures and mismatched/stale
159
+ results reject; `PermissionResolutionError.result` retains a conflicting authoritative record.
160
+ A cancelled request or a prior different answer is not success. Backend sanitization can also
161
+ change returned text; review the returned record instead of blindly resubmitting. Duplicate
162
+ in-flight resolutions are rejected. `approvedArguments` supports an explicitly reviewed JSON
163
+ object where the backend supports it; it is not a question-answer channel.
164
+
165
+ Persistent actions and policy methods are advanced account-policy operations, not default UI
166
+ buttons. Project conversations require individual one-time decisions. Avoid `resolveAll` for
167
+ interactive forms: it can partially mutate on failure. Never enable bypass or change models
168
+ as incidental chat setup. Explicit auto-approval options affect ordinary tools only; ask-user
169
+ always requires actual user input.
170
+
171
+ ---
56
172
 
57
- // Connect to local Swarm daemon (defaults to http://127.0.0.1:18080 or SWARM_API_URL)
58
- const client = createSwarmClient({
59
- baseUrl: process.env.SWARM_API_URL || 'http://127.0.0.1:18080',
60
- token: process.env.SWARM_DEPLOY_TOKEN, // Optional: scoped deploy token
173
+ ### 4. Interactive Chat & Live Tool Stream
174
+
175
+ Build responsive chat UIs that stream model text, reasoning, and live tool execution cards like Swarm Desktop:
176
+
177
+ ```typescript
178
+ // 1. Create a chat session
179
+ const session = await client.chat.createSession({
180
+ title: 'Repository Exploration',
181
+ workspace_path: '/path/to/project',
182
+ });
183
+
184
+ // 2. Stream live text, reasoning, and tool calls
185
+ const stream = await client.chat.stream(session.id, {
186
+ onText: (delta, fullText) => process.stdout.write(delta),
187
+ onReasoning: (delta) => console.log('[Thinking]', delta),
188
+ onToolStarted: (tool) => console.log(`[Tool Started] ${tool.name}`),
189
+ onToolDelta: (tool, delta) => console.log(`[Tool Output] ${delta}`),
190
+ onToolCompleted: (tool) => console.log(`[Tool Done] ${tool.name} in ${tool.durationMs}ms`),
191
+ onPermissionRequested: async (perm, resolve) => {
192
+ // Your UI must wait for a real user gesture. Keep errors visible and retain the form.
193
+ renderPermissionForm(describePermission(perm), async (action, userInput) => {
194
+ try {
195
+ const result = await resolve(action, action === 'allow_once' && describePermission(perm).kind === 'ask-user'
196
+ ? answerPermission(perm, userInput) : {});
197
+ showConfirmedDecision(result.permission);
198
+ } catch (error) { showPermissionError(perm.id, error); }
199
+ });
200
+ },
201
+ onPermissionRemoved: id => removePermissionForm(id), // No longer pending; not necessarily approved.
202
+ onPermissionError: (error, perm) => showPermissionError(perm.id, error),
203
+ onState: (state) => renderConversation(state.messages, state.live, state.snapshot),
204
+ onComplete: () => console.log('Turn finished; stream remains open'),
205
+ });
206
+
207
+ await stream.ready; // Hydration and replay MUST finish before dispatch.
208
+ await client.chat.sendMessage(session.id, 'Find all configuration files and summarize them.');
209
+ // Keep this watch for subsequent messages. Dispose only on switch/unmount/shutdown.
210
+ // stream.done means the watch ended, not that a single assistant turn finished.
211
+
212
+ // 3. Or use chat.run while the watch above serves the explicit permission UI
213
+ const response = await client.chat.run(session.id, {
214
+ message: 'What is the git status of this repository?',
215
+ timeoutMs: 120_000,
216
+ });
217
+ console.log(response.reply);
218
+ ```
219
+
220
+ ---
221
+
222
+ ### 5. Native WebSocket Streaming Bridge & Browser Client
223
+
224
+ Swarmagent communicates in real-time over bidirectional WebSockets (`/v3/realtime/stream`). The SDK provides an out-of-the-box WebSocket Bridge for Node.js backends and a universal browser client for web frontends (React, Vue, Svelte, or Vanilla JS):
225
+
226
+ #### Backend (Node.js): Mount WebSocket Bridge
227
+ ```typescript
228
+ import http from 'node:http';
229
+ import { SwarmClient } from '@swarm-agent/sdk';
230
+
231
+ const server = http.createServer((req, res) => { /* app routes */ });
232
+ const client = new SwarmClient();
233
+
234
+ // Mounts bidirectional WebSocket bridge on /ws:
235
+ // Hooks directly into Swarm daemon's realtime WebSocket outbox, multiplexing tokens, reasoning, and tool calls.
236
+ client.chat.attachWebSocket(server, {
237
+ path: '/ws',
238
+ autoApprovePermissions: false, // Explicit server policy; browser cannot enable it
239
+ });
240
+
241
+ server.listen(3456, '127.0.0.1');
242
+ ```
243
+
244
+ #### Frontend (Browser): Real-Time WebSocket Hooking
245
+ ```typescript
246
+ import { SwarmBrowserChat, describePermission, answerPermission } from '@swarm-agent/sdk/browser';
247
+
248
+ const chat = new SwarmBrowserChat(); // same-origin ws/wss selected automatically
249
+ chat.on('state', state => {
250
+ // Replace durable history and transient streams separately; never append fullText to history.
251
+ renderConversation(state.messages, state.live, state.snapshot);
252
+ // snapshot.current_run_state_by_session[state.sessionId] owns busy/waiting/failed status.
253
+ });
254
+ chat.on('error', showError);
255
+ chat.on('disconnected', disableComposer);
256
+
257
+ // 1. Hook into live tool execution and token streams
258
+ chat.on('text', (delta, fullText) => renderLiveMarkdown(fullText));
259
+ chat.on('reasoning', (delta, fullText) => renderModelThinking(fullText));
260
+ chat.on('tool_start', (tool) => renderToolStartedCard(tool.id, tool.name, tool.arguments));
261
+ chat.on('tool_delta', (tool, delta) => appendToolOutput(tool.id, delta));
262
+ chat.on('tool_done', (tool) => markToolCompleted(tool.id, tool.durationMs));
263
+ // Replace pending cards on hydration/live updates/reconnect, keyed by permission.id.
264
+ // Preserve unsent input for unchanged requests; do not submit during render.
265
+ chat.on('permissions', pending => renderPendingForms(pending.map(describePermission)));
266
+ chat.on('permission_error', (id, error) => showPermissionError(id, error));
267
+ chat.on('permission_removed', id => removePermissionForm(id));
268
+
269
+ // Wire to your form's explicit user-submit handler. Never remove a card on send alone.
270
+ async function onPermissionSubmit(permission, action, userInput) {
271
+ try {
272
+ const result = await chat.resolvePermission(permission.id, action,
273
+ action === 'allow_once' && describePermission(permission).kind === 'ask-user'
274
+ ? answerPermission(permission, userInput) : {});
275
+ showConfirmedDecision(result.permission);
276
+ } catch (error) { showPermissionError(permission.id, error); }
277
+ }
278
+
279
+ // 2. Connect & subscribe to session
280
+ await chat.connect();
281
+ await chat.subscribe('session_123');
282
+
283
+ // 3. Send messages directly over WebSocket
284
+ const requestId = chat.sendMessage('Inspect the git repository and list modified files.');
285
+ // `accepted` acknowledges durable dispatch. Never blindly resend an uncertain message.
286
+ // Retain requestId if your UI offers an explicit retry, and pass it as argument 4.
287
+ // Switching: await chat.subscribe(otherSession.id). Reconnect rehydrates selected history.
288
+ // Unmount: chat.dispose(). Disconnected sends throw instead of silently disappearing.
289
+ ```
290
+
291
+ Backend conversation routes should use the same project identity for listing and creation:
292
+ ```typescript
293
+ const project = await client.projects.ensureProject('Social content');
294
+ const conversations = await client.projects.listConversations(project.id, { limit: 100 });
295
+ const created = await client.projects.createConversation(project.id, {
296
+ title: 'New campaign', clientRequestId: crypto.randomUUID(),
61
297
  });
298
+ // Return created.id to the browser; await chat.subscribe(created.id) before sending.
62
299
  ```
63
300
 
64
- ### 2. Local Desktop Bootstrap & Scoped Tokens
301
+ Use `state` as the conversation rendering authority, including initial history, failed/empty
302
+ turns and durable reconnect recovery. `text`/tool callbacks are convenience notifications.
303
+ History is a bounded 200-message tail; use canonical sync hydration for additional history.
304
+ The browser bridge requires an explicit selected session and never guesses a recent account
305
+ session. Keep it on host loopback; network/multi-user apps must supply `authorize` and their
306
+ own per-session authorization. Foreign browser origins are rejected. Auto approval never
307
+ chooses an `ask-user` answer: render the question and send the operator's reply explicitly.
308
+
309
+ `resolvePermission` now returns a promise: await it and handle rejection. Browser and bridge
310
+ must be upgraded together; resolution frames require `requestId` and acknowledgements carry
311
+ `result`. `permission_removed` means no longer pending, not approval. `permissions` is a
312
+ rendering projection of the canonical session view, not a second durable authority. Timeout,
313
+ disconnect, disposal or session switch rejects uncertain submissions; reconnect rehydrates
314
+ without replaying decisions. To explicitly rehydrate after an acknowledgement timeout, retain
315
+ `chat.sessionId`, call `chat.unsubscribe()`, then `await chat.subscribe(retainedId)`; never resend
316
+ the decision automatically. Review refreshed state before manually retrying. No UI can infer
317
+ exactly-once application from a lost acknowledgement. `onPermissionError` is recoverable and
318
+ does not close the watch; `onError` reports stream failures. Existing string-reason resolver
319
+ calls remain supported, while options objects carry structured replies and reviewed arguments.
320
+
321
+ Runnable terminal examples: `examples/chat-quickstart.ts`, `examples/orchestrator-quickstart.ts`
322
+ and their shared `examples/interactive-permissions.ts`. They require a preconfigured daemon,
323
+ ask for actual terminal input, and never configure credentials, model fleets or bypass.
324
+
325
+ ---
326
+
327
+ ### 5. Local Desktop Bootstrap & Scoped Tokens
65
328
 
66
329
  ```typescript
67
330
  // Bootstrap local session credentials from Desktop
@@ -0,0 +1,77 @@
1
+ # Headless worker control (configuration preview)
2
+
3
+ `client.workers.control` uses authenticated `/v3/workers/{worker_id}` APIs.
4
+ It extends canonical worker identity, run receipts and V3 worker realtime
5
+ invalidation. It is not a second executor or a direct SSH/cloud deployment helper.
6
+ Use a trusted backend/private full API connection, not the restricted container
7
+ session listener. Never give a browser the privileged daemon socket.
8
+
9
+ ## Supported operations
10
+
11
+ | SDK operation | Route suffix | Effect |
12
+ | --- | --- | --- |
13
+ | `registerSSHTarget` | POST `ssh-targets` | Register an existing-host configuration in the canonical workspace/account connection catalog; idempotent, no network contact. |
14
+ | `resolveTarget` | POST `target-reference` | Authorize an existing SSH connection or GCP topology reference and compute its configuration digest. |
15
+ | `getContext`, `updateContext` | GET/PUT `context` | Read current/exact revision; publish explicit bounded knowledge with CAS and provenance. |
16
+ | `proposeDeployment` | POST `deployments` | Persist pending intent pinned to worker/context/target and explicit lifecycle. |
17
+ | `listDeployments`, `getDeployment` | GET `deployments[/{id}]` | Inspect persisted intent, not runtime health. |
18
+ | `approveDeployment` | POST `deployments/{id}/approve` | Explicit user approves exact revision/digest; stale worker/context/target rejects. |
19
+ | `queueJob` | POST `deployments/{id}/jobs` | Admit a canonical run with resolved account model, context, target, deployment and pending-attempt pins. Does not execute it. |
20
+ | `command`, `commands` | POST/GET `deployments/{id}/commands` | Stop is durable pending intent; start returns 503 unavailable. |
21
+
22
+ Use existing `workers.listRuns`, `getRun`, and `cancelRun` for job receipts.
23
+ Cancellation of a still-unstarted remote intent releases reservations atomically
24
+ and records `cancelled_before_dispatch`. It is not remote runtime cleanup.
25
+
26
+ ## Safety and semantics
27
+
28
+ - SDK transport/user identity is authenticated; request JSON cannot supply an
29
+ account or agent role. Scopes remain `automations:read`/`automations:write`.
30
+ Trigger/worker-scoped credentials cannot use control-plane operations.
31
+ - Ordinary chat/subagents cannot manage workers. Canonical Orchestrator agent
32
+ callers can propose intent through the shared service; approvals and explicit
33
+ context publication require a user. This preview adds no AI tool schema.
34
+ - Existing workers keep IDs and local behavior. Queued remote intent reserves
35
+ one job per deployment and one context writer per worker. Local admission is
36
+ also blocked while a remote context writer owns the worker. The first target
37
+ capacity policy is shared across deployments; a later proposal cannot silently
38
+ enlarge it. Reservations survive restart.
39
+ - `persistent` and `on_demand` are explicit for SSH and GCP. Approval sets desired
40
+ state `running`, while observed state remains `unavailable`. A saved intent is
41
+ never readiness. A stop command closes further admission, stays `pending`,
42
+ and does not fabricate acknowledgement. Cancel pending jobs separately.
43
+ - Registered existing targets authorize only `owned_runtime` cleanup. No host
44
+ delete/shutdown capability exists. Future GCP provisioning must establish exact
45
+ owned resource identity before adding compute deletion.
46
+ - Context is at most 64 KiB plus 32 bounded opaque artifact/checkpoint references;
47
+ references grant no access and must be reauthorized before use. Updates reject
48
+ while jobs own context. No automatic transcript ingestion or vector store.
49
+ - Worker changes require new reviewed deployment intent. The preview requires a
50
+ currently approved active worker and its existing primary workspace binding to
51
+ admit a job; it does not invent a different worker approval engine.
52
+
53
+ ## Durability and replay
54
+
55
+ Deployment, context, admission and command mutations use the canonical durable
56
+ worker transaction/outbox. Subscribe to existing V3 `worker.updated` account
57
+ invalidation and rehydrate these APIs; retain opaque scoped endpoint cursors.
58
+ Do not parse cursor numbers or poll for fabricated runtime progress. Worker runs
59
+ retain session identity, but a queued remote attempt has no materialized V3
60
+ session until an adapter actually prepares execution. Do not call a session
61
+ watcher on that reserved ID as if it were a live session.
62
+
63
+ ## Explicitly unavailable
64
+
65
+ No SSH process launch, GCP provisioning, remote readiness/heartbeat, execution,
66
+ remote acknowledgement, remote context publication, result manifest or cleanup
67
+ is implemented by this preview. Legacy direct dispatch is not a remote fallback.
68
+ Remote outcome writes through the local writer fail closed. Generation is pinned
69
+ at one until a qualified adapter supports replacement/retry fencing. Schedule
70
+ occurrence deduplication is retained at canonical storage; selecting remote
71
+ placement in the live scheduler is deferred, not silently enabled by a proposal.
72
+ GCP references must already exist in canonical account topology with transport
73
+ `gcp`; this API does not enroll cloud credentials or create machines.
74
+
75
+ `examples/headless-worker-control.ts` is an SDK-only configuration example. It
76
+ registers up to ten explicit SSH configurations and proposes placement. Approval
77
+ is a separate user decision; running the example is not live execution proof.
package/dist/auth.d.ts CHANGED
@@ -42,4 +42,30 @@ export declare class SwarmAuthNamespace {
42
42
  ok: boolean;
43
43
  deleted: boolean;
44
44
  }>;
45
+ /**
46
+ * Automatically configures provider credentials and the AI fleet from environment variables or existing credentials.
47
+ * Checks for:
48
+ * - OPENAI_API_KEY -> provider 'openai'
49
+ * - ANTHROPIC_API_KEY -> provider 'anthropic'
50
+ * - GEMINI_API_KEY / GOOGLE_API_KEY -> provider 'google'
51
+ * - DEEPSEEK_API_KEY -> provider 'deepseek'
52
+ * - GROQ_API_KEY -> provider 'groq'
53
+ * - MISTRAL_API_KEY -> provider 'mistral'
54
+ * Also verifies if Codex is authenticated via OAuth.
55
+ * When credentials are found or active, automatically configures the whole fleet (Swarm Core & System Agents)
56
+ * with verified catalog recommendations.
57
+ */
58
+ autoConfigure(options?: AutoConfigureAuthOptions): Promise<AutoConfigureAuthResult>;
59
+ }
60
+ export interface AutoConfigureAuthOptions {
61
+ /** If true (default), automatically applies verified catalog recommendations to the fleet */
62
+ applyFleetRecommendations?: boolean;
63
+ /** Custom env map (defaults to process.env) */
64
+ env?: Record<string, string | undefined>;
65
+ }
66
+ export interface AutoConfigureAuthResult {
67
+ configuredProviders: string[];
68
+ primaryProvider?: string;
69
+ source: 'env' | 'existing_credential' | 'none';
70
+ fleetApplied: boolean;
45
71
  }
package/dist/auth.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { SwarmCredentialsNamespace, SwarmCodexAuthNamespace } from './provider-auth.js';
2
+ import { SwarmSettingsNamespace } from './settings.js';
2
3
  export class SwarmAuthNamespace {
3
4
  credentials;
4
5
  codex;
@@ -86,4 +87,84 @@ export class SwarmAuthNamespace {
86
87
  });
87
88
  return res.data;
88
89
  }
90
+ /**
91
+ * Automatically configures provider credentials and the AI fleet from environment variables or existing credentials.
92
+ * Checks for:
93
+ * - OPENAI_API_KEY -> provider 'openai'
94
+ * - ANTHROPIC_API_KEY -> provider 'anthropic'
95
+ * - GEMINI_API_KEY / GOOGLE_API_KEY -> provider 'google'
96
+ * - DEEPSEEK_API_KEY -> provider 'deepseek'
97
+ * - GROQ_API_KEY -> provider 'groq'
98
+ * - MISTRAL_API_KEY -> provider 'mistral'
99
+ * Also verifies if Codex is authenticated via OAuth.
100
+ * When credentials are found or active, automatically configures the whole fleet (Swarm Core & System Agents)
101
+ * with verified catalog recommendations.
102
+ */
103
+ async autoConfigure(options = {}) {
104
+ const env = options.env || (typeof process !== 'undefined' && process?.env ? process.env : {});
105
+ const configuredProviders = [];
106
+ const providerKeyMap = [
107
+ { provider: 'openai', envVar: 'OPENAI_API_KEY' },
108
+ { provider: 'anthropic', envVar: 'ANTHROPIC_API_KEY' },
109
+ { provider: 'google', envVar: 'GEMINI_API_KEY', fallbackEnv: 'GOOGLE_API_KEY' },
110
+ { provider: 'deepseek', envVar: 'DEEPSEEK_API_KEY' },
111
+ { provider: 'groq', envVar: 'GROQ_API_KEY' },
112
+ { provider: 'mistral', envVar: 'MISTRAL_API_KEY' },
113
+ ];
114
+ for (const mapping of providerKeyMap) {
115
+ const key = env[mapping.envVar] || (mapping.fallbackEnv ? env[mapping.fallbackEnv] : undefined);
116
+ if (key && key.trim()) {
117
+ try {
118
+ await this.credentials.save({
119
+ provider: mapping.provider,
120
+ type: 'api',
121
+ api_key: key.trim(),
122
+ active: true,
123
+ });
124
+ configuredProviders.push(mapping.provider);
125
+ }
126
+ catch {
127
+ // Continue if already registered or error
128
+ }
129
+ }
130
+ }
131
+ let source = configuredProviders.length > 0 ? 'env' : 'none';
132
+ let primaryProvider = configuredProviders[0];
133
+ // Check if Codex is authenticated via OAuth
134
+ const codexAuthed = await this.codex.isAuthenticated();
135
+ if (codexAuthed) {
136
+ primaryProvider = 'codex';
137
+ if (source === 'none')
138
+ source = 'existing_credential';
139
+ }
140
+ else if (!primaryProvider) {
141
+ // Check if existing credentials exist in daemon
142
+ try {
143
+ const creds = await this.credentials.list();
144
+ const activeCred = creds.records.find((r) => r.active);
145
+ if (activeCred) {
146
+ primaryProvider = activeCred.provider;
147
+ source = 'existing_credential';
148
+ }
149
+ }
150
+ catch { }
151
+ }
152
+ let fleetApplied = false;
153
+ if (primaryProvider && options.applyFleetRecommendations !== false) {
154
+ try {
155
+ const settings = new SwarmSettingsNamespace(this.transport);
156
+ await settings.applyProviderFleet(primaryProvider);
157
+ fleetApplied = true;
158
+ }
159
+ catch {
160
+ // Fallback or retry
161
+ }
162
+ }
163
+ return {
164
+ configuredProviders,
165
+ primaryProvider,
166
+ source,
167
+ fleetApplied,
168
+ };
169
+ }
89
170
  }
@@ -0,0 +1,94 @@
1
+ import type { ToolStreamItem } from './chat.js';
2
+ import type { PendingPermissionRecord, ResolvePermissionResult } from './permissions.js';
3
+ import type { PermissionAction, ResolvePermissionOptions } from './permission-ui.js';
4
+ export * from './permission-ui.js';
5
+ import type { SessionWatchState, V3Message } from './realtime.js';
6
+ export interface SwarmBrowserChatOptions {
7
+ /** WebSocket URL, e.g. ws://localhost:3456/ws. Defaults to ws(s)://${location.host}/ws */
8
+ url?: string;
9
+ /** Whether to automatically connect on construction (default: true) */
10
+ autoConnect?: boolean;
11
+ /** Whether to automatically reconnect on drop (default: true) */
12
+ autoReconnect?: boolean;
13
+ /** Max reconnection attempts before giving up (default: 10) */
14
+ maxReconnectAttempts?: number;
15
+ /** Bounds subscription acknowledgement waits (default 45 seconds). */
16
+ subscribeTimeoutMs?: number;
17
+ /** Lost acknowledgements reject with an uncertain outcome; decisions are never replayed. */
18
+ permissionTimeoutMs?: number;
19
+ /** Optional custom WebSocket constructor (useful in Node.js test environments) */
20
+ webSocketClass?: any;
21
+ }
22
+ type EventMap = {
23
+ state: (state: SessionWatchState) => void;
24
+ accepted: (requestId: string) => void;
25
+ connected: () => void;
26
+ disconnected: () => void;
27
+ subscribed: (sessionId: string) => void;
28
+ unsubscribed: () => void;
29
+ text: (delta: string, fullText: string) => void;
30
+ reasoning: (delta: string, fullText: string) => void;
31
+ tool_start: (tool: ToolStreamItem) => void;
32
+ tool_delta: (tool: ToolStreamItem, delta: string) => void;
33
+ tool_done: (tool: ToolStreamItem) => void;
34
+ tool_permission: (permission: PendingPermissionRecord) => void;
35
+ permission_resolved: (permissionId: string, result: ResolvePermissionResult) => void;
36
+ permission_removed: (permissionId: string) => void;
37
+ permission_error: (permissionId: string, error: Error) => void;
38
+ permissions: (pending: PendingPermissionRecord[]) => void;
39
+ done: (messages: V3Message[]) => void;
40
+ error: (error: string) => void;
41
+ };
42
+ /**
43
+ * Universal browser-side WebSocket client for Swarm.
44
+ * Hooks directly into the Swarm WebSocket Bridge with typed callbacks for live
45
+ * assistant tokens, model reasoning deltas, real-time tool events, and permission approvals.
46
+ */
47
+ export declare class SwarmBrowserChat {
48
+ private readonly options;
49
+ private ws;
50
+ private currentSessionId;
51
+ private isDisposed;
52
+ private reconnectAttempts;
53
+ private reconnectTimer;
54
+ private subscribedSessionId;
55
+ private permissionsLive;
56
+ private cancelSubscription?;
57
+ private connectPromise;
58
+ private readonly listeners;
59
+ private readonly pendingPermissions;
60
+ private readonly decisions;
61
+ get permissions(): PendingPermissionRecord[];
62
+ private rejectDecisions;
63
+ private removePermission;
64
+ constructor(options?: SwarmBrowserChatOptions);
65
+ get sessionId(): string | null;
66
+ get isSubscribed(): boolean;
67
+ get isConnected(): boolean;
68
+ private resolveUrl;
69
+ /**
70
+ * Connects to the Swarm WebSocket Bridge. Returns a Promise that resolves when connected.
71
+ */
72
+ connect(): Promise<void>;
73
+ private maybeReconnect;
74
+ private handleServerMessage;
75
+ private send;
76
+ /**
77
+ * Subscribes to real-time events for a specific session.
78
+ * Returns a promise that resolves once the session subscription is acknowledged.
79
+ */
80
+ subscribe(sessionId: string, autoApprovePermissions?: boolean): Promise<string>;
81
+ /** Unsubscribes from active session streaming */
82
+ unsubscribe(): void;
83
+ /** Sends a chat message over the WebSocket */
84
+ sendMessage(message: string, sessionId?: string, autoApprovePermissions?: boolean, requestId?: string): string;
85
+ /** Resolve only after a user gesture. Await confirmation; on failure keep the form recoverable. */
86
+ resolvePermission(permissionId: string, action: PermissionAction, options?: ResolvePermissionOptions & {
87
+ sessionId?: string;
88
+ }): Promise<ResolvePermissionResult>;
89
+ on<K extends keyof EventMap>(event: K, handler: EventMap[K]): () => void;
90
+ off<K extends keyof EventMap>(event: K, handler: EventMap[K]): void;
91
+ private emit;
92
+ /** Closes and disposes the WebSocket connection */
93
+ dispose(): void;
94
+ }