@swarm-agent/sdk 0.1.51 → 0.1.53

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 CHANGED
@@ -21,8 +21,8 @@ Requires Node 22 or newer (ESM). The package is public on npm:
21
21
  npm install @swarm-agent/sdk
22
22
  </copy>
23
23
 
24
- You need a running Swarm on Linux x86_64. Install one with
25
- `curl -fsSL https://swarmagent.dev/install-launcher | sh -s -- --yes`.
24
+ You need a running Swarm. Install the Swarm box (Ubuntu 24.04 x86_64) with
25
+ `curl -fsSL https://swarmagent.dev/install | sudo bash`.
26
26
 
27
27
  ## Connect to your Swarm
28
28
 
@@ -58,10 +58,14 @@ example, not source tests or node_modules. `prepack` builds locally; there are n
58
58
  install/prepare hooks. TypeScript/tsx are build/test tools, not runtime dependencies.
59
59
  CommonJS `require` and non-Node runtimes are not validated by this packaging step.
60
60
 
61
- For the headless container, use the matching `@swarm-agent/cli` package and explicitly
62
- export a scoped token through `swarm-headless setup -- sdk-token`. Connect to
63
- `http://127.0.0.1:7783`; **do not** use Desktop bootstrap or a privileged socket.
64
- The headless listener allows session operations only, not the complete API below.
61
+ On the Swarm box, `http://127.0.0.1:7783` is the scoped-token listener. The AI
62
+ key the installer wrote to `~/.config/swarm/ai-key` (a new one:
63
+ `sudo bash /var/lib/swarm-headless/install.sh connect-ai`)
64
+ works only through its MCP, `http://127.0.0.1:7783/mcp`, with any MCP client;
65
+ it reaches no SDK route. For the session routes there, use a client key (a full
66
+ AI key's `swarm_create_client_key` mints one); **do not** use Desktop bootstrap
67
+ or a privileged socket. A client key reaches session operations only, not the
68
+ complete API below.
65
69
  The packaged `examples/headless-session.ts` imports `@swarm-agent/sdk` and can be copied
66
70
  into your consumer and run with an explicitly installed `tsx` runner. Protect its
67
71
  `SWARM_SDK_TOKEN_FILE` (0600); the example asks before each tool approval and uses
@@ -69,9 +73,18 @@ explicit refresh, not timer polling. Live agent verification is a separate step.
69
73
 
70
74
  ## Quick Start
71
75
 
72
- ### Chat Flow vs. Autonomous Orchestration Flow
76
+ ### Workers and jobs
73
77
 
74
- Swarm supports two distinct modes of execution depending on your application goals:
78
+ Start with a project and its authorized folders, then give jobs to workers:
79
+ coder for code changes, designer for UI/design, finder for research, or reviewer
80
+ for independent read-only review of another job. Reviewers require `review_job`,
81
+ use detached disposable checkouts, and report without delivering. Worker models
82
+ follow account role settings unless pinned. Read each job's result before
83
+ delivery (local merge, configured pull request, or queued landing).
84
+
85
+ ### Advanced: sessions, plans and orchestration
86
+
87
+ For conversational interfaces or structured multi-stage work:
75
88
 
76
89
  1. **Interactive Chat Flow (`client.chat` / `client.sessions`)**:
77
90
  - Best for conversational assistants, interactive Q&A, and direct tool calling.
@@ -80,7 +93,7 @@ Swarm supports two distinct modes of execution depending on your application goa
80
93
 
81
94
  2. **Autonomous Orchestration Flow (`client.projects` / `client.workers` / `client.deploy`)**:
82
95
  - Best for multi-agent background tasks, codebase modifications, and durable pipelines.
83
- - Deploys specialized subagent fleets (Coder, Finder, Designer, Router, Compact) across bounded repository worktrees with structured checkpoint contracts.
96
+ - Organizes worker jobs across authorized folders with structured plans and checkpoint contracts.
84
97
  - **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.
85
98
  - 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.
86
99
  - 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.
@@ -386,8 +399,11 @@ console.log(`Authenticated as user ${session.user_id}`);
386
399
  const { token, record } = await client.auth.createScopedToken({
387
400
  name: 'GitHub Actions Trigger',
388
401
  scopes: ['automations:trigger'],
389
- expires_in_seconds: 86400, // 24 hours
390
402
  });
403
+ // Every key works while it is used and stops after 90 days unused; nothing to
404
+ // renew. Pass expires_in_seconds only for a fixed end date (at most 365 days).
405
+ // client.auth.createKey({ kind: 'app' | 'bot' | 'ai', ... }), listKeys() and
406
+ // revokeKey(id) cover all three kinds.
391
407
  console.log(`Created deploy token: ${token} (hint: ${record.token_hint})`);
392
408
 
393
409
  // List active tokens
@@ -0,0 +1,33 @@
1
+ import type { SwarmTransport } from './transport.js';
2
+ export interface ActivityEvent {
3
+ seq: number;
4
+ at: number;
5
+ kind: string;
6
+ session_id: string;
7
+ job_id: string;
8
+ worker_id: string;
9
+ worker_name: string;
10
+ project_id: string;
11
+ state: string;
12
+ summary: string;
13
+ tool_name?: string;
14
+ file_path?: string;
15
+ }
16
+ export interface ActivityGap {
17
+ kind: 'gap';
18
+ from: number;
19
+ to: number;
20
+ cursor: string;
21
+ }
22
+ export interface ActivityStreamOptions {
23
+ cursor?: string;
24
+ project?: string;
25
+ worker?: string;
26
+ signal?: AbortSignal;
27
+ }
28
+ export declare class SwarmActivityNamespace {
29
+ private readonly transport;
30
+ constructor(transport: SwarmTransport);
31
+ /** Metadata only. Gap records must be surfaced to users; they are not activity events. */
32
+ stream(options?: ActivityStreamOptions): AsyncGenerator<ActivityEvent | ActivityGap>;
33
+ }
@@ -0,0 +1,83 @@
1
+ export class SwarmActivityNamespace {
2
+ transport;
3
+ constructor(transport) {
4
+ this.transport = transport;
5
+ }
6
+ /** Metadata only. Gap records must be surfaced to users; they are not activity events. */
7
+ async *stream(options = {}) {
8
+ let cursor = options.cursor ?? '0';
9
+ while (!options.signal?.aborted) {
10
+ const config = this.transport.getConfig();
11
+ if (config.socketPath)
12
+ throw new Error('Activity SSE requires an HTTP connection');
13
+ const query = new URLSearchParams({ cursor });
14
+ if (options.project)
15
+ query.set('project', options.project);
16
+ if (options.worker)
17
+ query.set('worker', options.worker);
18
+ const headers = { ...config.defaultHeaders, Accept: 'text/event-stream' };
19
+ if (config.token && !headers.Authorization && !headers.authorization)
20
+ headers.Authorization = `Bearer ${config.token}`;
21
+ try {
22
+ const response = await fetch(`${config.baseUrl.replace(/\/+$/, '')}/v3/activity/stream?${query}`, { headers, signal: options.signal, redirect: 'error' });
23
+ if (!response.ok || !response.body)
24
+ throw new Error(`Activity stream failed: ${response.status}`);
25
+ const reader = response.body.getReader();
26
+ const decoder = new TextDecoder();
27
+ let pending = '';
28
+ try {
29
+ for (;;) {
30
+ const chunk = await reader.read();
31
+ if (chunk.done)
32
+ break;
33
+ pending += decoder.decode(chunk.value, { stream: true }).replace(/\r\n/g, '\n');
34
+ if (pending.length > 1024 * 1024)
35
+ throw new Error('Activity frame too large');
36
+ let end;
37
+ while ((end = pending.indexOf('\n\n')) >= 0) {
38
+ const frame = pending.slice(0, end);
39
+ pending = pending.slice(end + 2);
40
+ const kind = frame.split('\n').find(line => line.startsWith('event:'))?.slice(6).trim();
41
+ const data = frame.split('\n').filter(line => line.startsWith('data:')).map(line => line.slice(5).trimStart()).join('\n');
42
+ if (!data)
43
+ continue;
44
+ const value = JSON.parse(data);
45
+ if (kind === 'cursor')
46
+ cursor = value.cursor;
47
+ else if (kind === 'gap') {
48
+ cursor = value.cursor;
49
+ yield { ...value, kind: 'gap' };
50
+ }
51
+ else if (kind === 'activity') {
52
+ cursor = String(value.seq);
53
+ yield value;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ finally {
59
+ try {
60
+ await reader.cancel();
61
+ }
62
+ catch { /* The disconnected transport is already closed. */ }
63
+ reader.releaseLock();
64
+ }
65
+ }
66
+ catch (error) {
67
+ if (options.signal?.aborted)
68
+ return;
69
+ // fetch and stream transport failures are TypeErrors; malformed frames
70
+ // and HTTP authorization failures are not retried.
71
+ if (!(error instanceof TypeError))
72
+ throw error;
73
+ }
74
+ // Bound reconnect frequency; this is reconnection, not polling for activity.
75
+ if (!options.signal?.aborted)
76
+ await new Promise(resolve => {
77
+ const finish = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', finish); resolve(); };
78
+ const timer = setTimeout(finish, 1000);
79
+ options.signal?.addEventListener('abort', finish, { once: true });
80
+ });
81
+ }
82
+ }
83
+ }