@swarm-agent/sdk 0.1.48 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/HEADLESS_UI.md +4 -6
  2. package/README.md +121 -56
  3. package/dist/agents.d.ts +114 -0
  4. package/dist/agents.js +183 -0
  5. package/dist/auth.d.ts +14 -0
  6. package/dist/auth.js +16 -0
  7. package/dist/box.d.ts +68 -0
  8. package/dist/box.js +13 -0
  9. package/dist/browser.d.ts +6 -2
  10. package/dist/browser.js +11 -3
  11. package/dist/chat.d.ts +41 -7
  12. package/dist/chat.js +148 -83
  13. package/dist/client.d.ts +12 -0
  14. package/dist/client.js +19 -6
  15. package/dist/copy-tags.d.ts +26 -0
  16. package/dist/copy-tags.js +133 -0
  17. package/dist/errors.d.ts +16 -0
  18. package/dist/errors.js +21 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +6 -0
  21. package/dist/local-connection.d.ts +39 -0
  22. package/dist/local-connection.js +56 -0
  23. package/dist/permission-ui.d.ts +1 -0
  24. package/dist/permission-ui.js +37 -0
  25. package/dist/permissions.d.ts +4 -0
  26. package/dist/permissions.js +7 -0
  27. package/dist/projects.d.ts +25 -3
  28. package/dist/projects.js +65 -6
  29. package/dist/provider-auth.d.ts +5 -3
  30. package/dist/provider-auth.js +6 -8
  31. package/dist/realtime.d.ts +40 -6
  32. package/dist/realtime.js +156 -52
  33. package/dist/remote.d.ts +59 -0
  34. package/dist/remote.js +46 -0
  35. package/dist/sessions.d.ts +31 -1
  36. package/dist/sessions.js +49 -1
  37. package/dist/settings.d.ts +3 -0
  38. package/dist/settings.js +19 -3
  39. package/dist/signals.d.ts +148 -0
  40. package/dist/signals.js +97 -0
  41. package/dist/types.d.ts +70 -2
  42. package/dist/websocket.d.ts +3 -1
  43. package/dist/websocket.js +1 -1
  44. package/dist/workspaces.d.ts +11 -0
  45. package/dist/workspaces.js +17 -0
  46. package/package.json +1 -1
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Swarm agents wrap copyable snippets (commands, config, file payloads) in
3
+ * `<copy label="...">…</copy>` tags. Swarm's own desktop and terminal UIs render
4
+ * them as copy buttons; everywhere else they are noise. `copyTagsToMarkdown`
5
+ * turns each tag into a plain Markdown code block (with its label, if any, as a
6
+ * bold line above it), so any Markdown renderer shows it correctly.
7
+ *
8
+ * Rules (same parsing as Swarm's own renderers):
9
+ * - Tags inside fenced code blocks (``` or ~~~) are left untouched.
10
+ * - The label comes from `label`, `title` or `name`.
11
+ * - A tag that is never closed becomes a code block running to the end.
12
+ * - Text without copy tags is returned unchanged.
13
+ *
14
+ * Pass `{ streaming: true }` for text that is still arriving: a half-written tag
15
+ * at the very end is hidden, so converting the growing text only ever appends.
16
+ *
17
+ * The SDK applies it by default to assistant text from `chat.run`,
18
+ * `chat.listMessages` and `chat.stream` (`onText`, `onComplete`); pass
19
+ * `rawCopyTags: true` there to get the original text. Lower-level reads
20
+ * (`sessions.get`, `realtime.watchSession`) return the stored text unchanged.
21
+ */
22
+ export function copyTagsToMarkdown(text, options = {}) {
23
+ if (typeof text !== 'string' || !text)
24
+ return text;
25
+ const streaming = options.streaming === true;
26
+ if (!(streaming ? /</.test(text) : /<copy[\s>]/i.test(text)))
27
+ return text;
28
+ const source = text.replace(/\r\n?/g, '\n');
29
+ const protectedRanges = fencedRanges(source);
30
+ let out = '';
31
+ let cursor = 0;
32
+ while (cursor < source.length) {
33
+ const open = nextOpenTag(source, cursor, protectedRanges);
34
+ if (!open) {
35
+ out += source.slice(cursor);
36
+ break;
37
+ }
38
+ out += source.slice(cursor, open.start);
39
+ const afterOpen = source.slice(open.end);
40
+ const closeIndex = afterOpen.toLowerCase().indexOf('</copy>');
41
+ if (closeIndex < 0) {
42
+ let body = afterOpen;
43
+ if (streaming)
44
+ body = body.replace(PARTIAL_CLOSE_TAIL, '');
45
+ return out + renderBlock(out, tagLabel(open.tag), body.replace(/^\n+/, ''), false, streaming);
46
+ }
47
+ // Streaming keeps trailing newlines it may already have delivered.
48
+ const body = afterOpen.slice(0, closeIndex).replace(streaming ? /^\n+/ : /^\n+|\n+$/g, '');
49
+ out += renderBlock(out, tagLabel(open.tag), body, true, streaming);
50
+ cursor = open.end + closeIndex + '</copy>'.length;
51
+ if (cursor < source.length && source[cursor] !== '\n')
52
+ out += '\n';
53
+ }
54
+ if (streaming && !inside(out.length - 1, fencedRanges(out)))
55
+ out = out.replace(PARTIAL_OPEN_TAIL, '');
56
+ return out;
57
+ }
58
+ /** True when `text` contains at least one `<copy>` tag outside fenced code. */
59
+ export function hasCopyTags(text) {
60
+ if (typeof text !== 'string' || !/<copy[\s>]/i.test(text))
61
+ return false;
62
+ const source = text.replace(/\r\n?/g, '\n');
63
+ return nextOpenTag(source, 0, fencedRanges(source)) !== undefined;
64
+ }
65
+ const OPEN_TAG = /<copy(?:\s+[^>]*)?>/gi;
66
+ const LABEL_ATTR = /\b(?:label|title|name)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i;
67
+ // Streaming only: a tag cut off at the very end (`<`, `<co`, `<copy label="x`).
68
+ const PARTIAL_OPEN_TAIL = /<(?:c(?:o(?:p(?:y(?:\s[^<>]*)?)?)?)?)?$/i;
69
+ const PARTIAL_CLOSE_TAIL = /<\/?(?:c(?:o(?:p(?:y)?)?)?)?$/i;
70
+ function renderBlock(before, label, body, closed, streaming) {
71
+ let longest = 0;
72
+ for (const run of body.match(/`+/g) ?? [])
73
+ longest = Math.max(longest, run.length);
74
+ // Streaming uses a 4-backtick fence so a payload that later shows a ``` line does
75
+ // not force re-fencing text that was already delivered.
76
+ const fence = '`'.repeat(Math.max(streaming ? 4 : 3, longest + 1));
77
+ const lead = before === '' || before.endsWith('\n') ? '' : '\n';
78
+ const title = label ? `**${label}**\n` : '';
79
+ // An open (still streaming) block gets no trailing newline, so later text only appends.
80
+ return closed
81
+ ? `${lead}${title}${fence}\n${body}${body && !body.endsWith('\n') ? '\n' : ''}${fence}`
82
+ : `${lead}${title}${fence}\n${body}`;
83
+ }
84
+ function tagLabel(tag) {
85
+ const match = LABEL_ATTR.exec(tag);
86
+ if (!match)
87
+ return '';
88
+ return (match[1] ?? match[2] ?? match[3] ?? '').trim();
89
+ }
90
+ function inside(index, ranges) {
91
+ return ranges.some((range) => index >= range.start && index < range.end);
92
+ }
93
+ function nextOpenTag(text, from, ranges) {
94
+ OPEN_TAG.lastIndex = from;
95
+ for (let match = OPEN_TAG.exec(text); match; match = OPEN_TAG.exec(text)) {
96
+ const start = match.index;
97
+ const range = ranges.find((r) => start >= r.start && start < r.end);
98
+ if (range) {
99
+ OPEN_TAG.lastIndex = Math.max(range.end, start + match[0].length);
100
+ continue;
101
+ }
102
+ return { start, end: start + match[0].length, tag: match[0] };
103
+ }
104
+ return undefined;
105
+ }
106
+ function fencedRanges(text) {
107
+ const ranges = [];
108
+ let fence;
109
+ let lineStart = 0;
110
+ while (lineStart < text.length) {
111
+ const newline = text.indexOf('\n', lineStart);
112
+ const lineEnd = newline < 0 ? text.length : newline;
113
+ const next = newline < 0 ? text.length : newline + 1;
114
+ const line = text.slice(lineStart, lineEnd).trim();
115
+ const marker = line[0];
116
+ if (marker === '`' || marker === '~') {
117
+ let count = 0;
118
+ while (line[count] === marker)
119
+ count++;
120
+ const info = line.slice(count).trim();
121
+ if (!fence && count >= 3)
122
+ fence = { marker, count, start: lineStart };
123
+ else if (fence && marker === fence.marker && count >= fence.count && info === '') {
124
+ ranges.push({ start: fence.start, end: next });
125
+ fence = undefined;
126
+ }
127
+ }
128
+ lineStart = next;
129
+ }
130
+ if (fence)
131
+ ranges.push({ start: fence.start, end: text.length });
132
+ return ranges;
133
+ }
package/dist/errors.d.ts CHANGED
@@ -34,3 +34,19 @@ export declare class SwarmValidationError extends SwarmError {
34
34
  readonly details?: unknown;
35
35
  constructor(message: string, details?: unknown);
36
36
  }
37
+ /**
38
+ * `chat_reconnecting`: the connection to this conversation is being restored; retry shortly.
39
+ * `chat_disconnected`: the connection is down and will not come back on its own; call `connect()`.
40
+ * `chat_not_subscribed`: the conversation is not open in this client; `subscribe()` to it first.
41
+ */
42
+ export type SwarmChatNotReadyCode = 'chat_reconnecting' | 'chat_disconnected' | 'chat_not_subscribed';
43
+ /**
44
+ * A browser chat send refused before anything reached Swarm. The message is plain enough
45
+ * to show people; apps branch on `code` (or `retryable`) instead of matching text.
46
+ */
47
+ export declare class SwarmChatNotReadyError extends SwarmError {
48
+ readonly code: SwarmChatNotReadyCode;
49
+ /** True when retrying the same send shortly is expected to work without app action. */
50
+ readonly retryable: boolean;
51
+ constructor(code: SwarmChatNotReadyCode);
52
+ }
package/dist/errors.js CHANGED
@@ -66,3 +66,24 @@ export class SwarmValidationError extends SwarmError {
66
66
  Object.setPrototypeOf(this, new.target.prototype);
67
67
  }
68
68
  }
69
+ const chatNotReadyMessages = {
70
+ chat_reconnecting: 'Not sent: reconnecting to Swarm, try again in a moment',
71
+ chat_disconnected: 'Not sent: disconnected from Swarm, reconnect and try again',
72
+ chat_not_subscribed: 'Not sent: this conversation is not open yet, open it and try again',
73
+ };
74
+ /**
75
+ * A browser chat send refused before anything reached Swarm. The message is plain enough
76
+ * to show people; apps branch on `code` (or `retryable`) instead of matching text.
77
+ */
78
+ export class SwarmChatNotReadyError extends SwarmError {
79
+ code;
80
+ /** True when retrying the same send shortly is expected to work without app action. */
81
+ retryable;
82
+ constructor(code) {
83
+ super(chatNotReadyMessages[code]);
84
+ this.name = 'SwarmChatNotReadyError';
85
+ this.code = code;
86
+ this.retryable = code === 'chat_reconnecting';
87
+ Object.setPrototypeOf(this, new.target.prototype);
88
+ }
89
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './client.js';
2
+ export * from './local-connection.js';
2
3
  export * from './auth.js';
3
4
  export * from './automations.js';
4
5
  export * from './workers.js';
@@ -6,12 +7,17 @@ export * from './worker-control.js';
6
7
  export * from './deliverables.js';
7
8
  export * from './deploy/index.js';
8
9
  export * from './notifications.js';
10
+ export * from './signals.js';
11
+ export * from './box.js';
9
12
  export * from './sessions.js';
10
13
  export * from './projects.js';
11
14
  export * from './permissions.js';
12
15
  export * from './chat.js';
16
+ export * from './copy-tags.js';
13
17
  export * from './workspaces.js';
14
18
  export * from './system.js';
19
+ export * from './remote.js';
20
+ export * from './agents.js';
15
21
  export * from './transport.js';
16
22
  export * from './errors.js';
17
23
  export * from './types.js';
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './client.js';
2
+ export * from './local-connection.js';
2
3
  export * from './auth.js';
3
4
  export * from './automations.js';
4
5
  export * from './workers.js';
@@ -6,12 +7,17 @@ export * from './worker-control.js';
6
7
  export * from './deliverables.js';
7
8
  export * from './deploy/index.js';
8
9
  export * from './notifications.js';
10
+ export * from './signals.js';
11
+ export * from './box.js';
9
12
  export * from './sessions.js';
10
13
  export * from './projects.js';
11
14
  export * from './permissions.js';
12
15
  export * from './chat.js';
16
+ export * from './copy-tags.js';
13
17
  export * from './workspaces.js';
14
18
  export * from './system.js';
19
+ export * from './remote.js';
20
+ export * from './agents.js';
15
21
  export * from './transport.js';
16
22
  export * from './errors.js';
17
23
  export * from './types.js';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * How a client with no explicit address finds this machine's Swarm.
3
+ *
4
+ * Order: an explicit `socketPath`/`baseUrl`, then `SWARM_SOCKET_PATH`, then
5
+ * `SWARM_API_URL`/`SWARM_DESKTOP_URL`, then (Node only) the owner socket of a
6
+ * system install when it exists and this process may connect to it, then the
7
+ * daemon's default loopback API at http://127.0.0.1:7781.
8
+ *
9
+ * Browsers never touch the filesystem: the probe uses Node's synchronous
10
+ * `process.getBuiltinModule`, which no bundler resolves and no browser has.
11
+ */
12
+ /** Owner socket of a Linux system install (`/var/lib/swarmd` data root, main lane). */
13
+ export declare const SWARM_OWNER_SOCKET_PATH = "/var/lib/swarmd/local-transport/api.sock";
14
+ /** The daemon's default loopback API address. */
15
+ export declare const SWARM_DEFAULT_API_URL = "http://127.0.0.1:7781";
16
+ export interface SwarmConnectionInput {
17
+ baseUrl?: string;
18
+ socketPath?: string;
19
+ }
20
+ export interface SwarmConnectionOptions {
21
+ /** Environment to read; defaults to `process.env` in Node, `{}` elsewhere. */
22
+ env?: Record<string, string | undefined>;
23
+ /** Owner socket to probe; defaults to {@link SWARM_OWNER_SOCKET_PATH}. */
24
+ ownerSocketPath?: string;
25
+ /** Returns true when `path` is a socket this process may connect to. */
26
+ isUsableSocket?: (path: string) => boolean;
27
+ }
28
+ export interface SwarmConnection {
29
+ baseUrl: string;
30
+ socketPath?: string;
31
+ }
32
+ /**
33
+ * True when `path` exists, is a Unix socket and is readable and writable by
34
+ * this process. Any error (missing, no permission on a parent directory, not
35
+ * Node) means "not usable"; it never throws.
36
+ */
37
+ export declare function isUsableSwarmSocket(path: string): boolean;
38
+ /** Resolves where a client with the given explicit settings connects. */
39
+ export declare function resolveSwarmConnection(input?: SwarmConnectionInput, options?: SwarmConnectionOptions): SwarmConnection;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * How a client with no explicit address finds this machine's Swarm.
3
+ *
4
+ * Order: an explicit `socketPath`/`baseUrl`, then `SWARM_SOCKET_PATH`, then
5
+ * `SWARM_API_URL`/`SWARM_DESKTOP_URL`, then (Node only) the owner socket of a
6
+ * system install when it exists and this process may connect to it, then the
7
+ * daemon's default loopback API at http://127.0.0.1:7781.
8
+ *
9
+ * Browsers never touch the filesystem: the probe uses Node's synchronous
10
+ * `process.getBuiltinModule`, which no bundler resolves and no browser has.
11
+ */
12
+ /** Owner socket of a Linux system install (`/var/lib/swarmd` data root, main lane). */
13
+ export const SWARM_OWNER_SOCKET_PATH = '/var/lib/swarmd/local-transport/api.sock';
14
+ /** The daemon's default loopback API address. */
15
+ export const SWARM_DEFAULT_API_URL = 'http://127.0.0.1:7781';
16
+ function processEnv() {
17
+ return typeof process !== 'undefined' && process?.env ? process.env : {};
18
+ }
19
+ /**
20
+ * True when `path` exists, is a Unix socket and is readable and writable by
21
+ * this process. Any error (missing, no permission on a parent directory, not
22
+ * Node) means "not usable"; it never throws.
23
+ */
24
+ export function isUsableSwarmSocket(path) {
25
+ if (typeof process === 'undefined' || typeof process?.getBuiltinModule !== 'function')
26
+ return false;
27
+ try {
28
+ const fs = process.getBuiltinModule('node:fs');
29
+ if (!fs?.statSync(path)?.isSocket())
30
+ return false;
31
+ fs.accessSync(path, fs.constants.R_OK | fs.constants.W_OK);
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ /** Resolves where a client with the given explicit settings connects. */
39
+ export function resolveSwarmConnection(input = {}, options = {}) {
40
+ const env = options.env ?? processEnv();
41
+ const explicitUrl = input.baseUrl || env.SWARM_API_URL || env.SWARM_DESKTOP_URL;
42
+ const baseUrl = (explicitUrl || SWARM_DEFAULT_API_URL).replace(/\/+$/, '');
43
+ if (input.socketPath)
44
+ return { baseUrl, socketPath: input.socketPath };
45
+ if (input.baseUrl)
46
+ return { baseUrl };
47
+ if (env.SWARM_SOCKET_PATH)
48
+ return { baseUrl, socketPath: env.SWARM_SOCKET_PATH };
49
+ if (explicitUrl)
50
+ return { baseUrl };
51
+ const ownerSocket = options.ownerSocketPath ?? SWARM_OWNER_SOCKET_PATH;
52
+ const usable = options.isUsableSocket ?? isUsableSwarmSocket;
53
+ if (usable(ownerSocket))
54
+ return { baseUrl, socketPath: ownerSocket };
55
+ return { baseUrl };
56
+ }
@@ -27,6 +27,7 @@ export interface PermissionRequestView {
27
27
  arguments: Record<string, unknown> | null;
28
28
  rawArguments: string;
29
29
  parseError?: string;
30
+ /** Plain-text heading: the request's own title, else a fallback (bash command, first ask-user question, tool + action). Never HTML. */
30
31
  title?: string;
31
32
  context?: string;
32
33
  structured: boolean;
@@ -15,6 +15,11 @@ export function isAskUserPermission(permission) {
15
15
  }
16
16
  /** Parse untrusted request arguments for text rendering. Never render these fields as HTML. */
17
17
  export function describePermission(permission) {
18
+ const view = describePermissionRequest(permission);
19
+ view.title = view.title || permissionFallbackTitle(view);
20
+ return view;
21
+ }
22
+ function describePermissionRequest(permission) {
18
23
  const id = permissionId(permission?.id);
19
24
  const sessionId = permissionId(permission?.session_id, 'sessionId');
20
25
  const raw = permission.tool_arguments ?? permission.tool_call_arguments ?? '';
@@ -165,3 +170,35 @@ export function confirmPermissionResolution(result, sessionId, id, action, optio
165
170
  }
166
171
  return result;
167
172
  }
173
+ const TITLE_MAX = 160;
174
+ /** One plain-text line: control characters and runs of whitespace collapse, long text is cut. */
175
+ function titleLine(value) {
176
+ const line = value.replace(/[\u0000-\u001f\u007f\s]+/g, ' ').trim();
177
+ return line.length > TITLE_MAX ? `${line.slice(0, TITLE_MAX - 1).trimEnd()}…` : line;
178
+ }
179
+ function humanize(value) {
180
+ const words = value.trim().split('.').at(-1).replace(/[_-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').trim().toLowerCase();
181
+ return words ? words[0].toUpperCase() + words.slice(1) : '';
182
+ }
183
+ /**
184
+ * Plain-text fallback title when the request carries none: bash shows its command,
185
+ * ask-user its first question, any other tool its humanized name plus action/subject.
186
+ * Built only from untrusted strings; render it as text, never as HTML.
187
+ */
188
+ function permissionFallbackTitle(view) {
189
+ const args = view.arguments ?? {};
190
+ const tool = text(view.toolName).toLowerCase().split('.').at(-1) ?? '';
191
+ if ((tool === 'bash' || tool === 'shell') && text(args.command))
192
+ return titleLine(text(args.command));
193
+ if (view.kind === 'ask-user') {
194
+ const first = view.questions[0]?.question
195
+ || (Array.isArray(args.questions) && object(args.questions[0]) ? text(args.questions[0].question) : '')
196
+ || text(args.question);
197
+ if (first)
198
+ return titleLine(first);
199
+ }
200
+ const name = humanize(text(view.toolName)) || 'Tool request';
201
+ const action = text(args.action) ? humanize(text(args.action)).toLowerCase() : '';
202
+ const subject = [args.title, args.name, args.path, args.file_path, args.url, args.query].map(text).find(Boolean) ?? '';
203
+ return titleLine(`${name}${action ? `: ${action}` : ''}${subject ? ` (${subject})` : ''}`);
204
+ }
@@ -74,6 +74,10 @@ export declare class SwarmPermissionsNamespace {
74
74
  * Retrieves the current account-wide permission policy, including bash profile and persistent rules.
75
75
  */
76
76
  getPolicy(): Promise<PermissionPolicy>;
77
+ /**
78
+ * Whether permissionless mode (bypass permissions) is on.
79
+ */
80
+ bypass(): Promise<boolean>;
77
81
  /**
78
82
  * Toggles permissionless mode (bypass permissions).
79
83
  * When enabled, tool executions run autonomously without pausing for manual approvals.
@@ -13,6 +13,13 @@ export class SwarmPermissionsNamespace {
13
13
  const res = await this.transport.request('/v1/permissions');
14
14
  return res.data.policy;
15
15
  }
16
+ /**
17
+ * Whether permissionless mode (bypass permissions) is on.
18
+ */
19
+ async bypass() {
20
+ const res = await this.transport.request('/v1/permissions');
21
+ return res.data.bypass_permissions === true;
22
+ }
16
23
  /**
17
24
  * Toggles permissionless mode (bypass permissions).
18
25
  * When enabled, tool executions run autonomously without pausing for manual approvals.
@@ -1,5 +1,5 @@
1
1
  import type { SwarmTransport } from './transport.js';
2
- import type { ApproveProjectTaskResult, ClearProjectContextResult, CompleteProjectTaskResult, CreateProjectParams, CreateProjectTaskParams, CreateProjectTaskResult, DeleteProjectMediaResult, DeleteProjectResult, DeleteProjectTaskResult, DeployProjectTaskProgramResult, GetProjectTaskProgramResult, GetProjectTaskResult, IntegrateProjectTaskResult, ListProjectsParams, PreviewProjectTaskParams, ProjectRecord, ProjectTaskApprovalGuards, ProjectTaskMediaRef, ProjectTaskModelPreviewResult, ProjectTaskPreviewResult, ProjectTaskRecord, RedeployProjectTaskJobResult, RefineProjectTaskParams, RefineProjectTaskResult, RejectProjectTaskResult, ReopenProjectTaskResult, RequestOptions, SessionRecord, SyncStreamParams, SyncStreamResult, SynthesizeProjectContextParams, UpdateProjectParams, UpdateProjectTaskParams } from './types.js';
2
+ import type { ApproveProjectTaskResult, ClearProjectContextResult, CompleteProjectTaskResult, CreateProjectParams, CreateProjectTaskParams, CreateProjectTaskResult, DeleteProjectMediaResult, DeleteProjectResult, DeleteProjectTaskResult, DeployProjectTaskProgramResult, GetProjectTaskProgramResult, GetProjectTaskResult, IntegrateProjectTaskOptions, IntegrateProjectTaskResult, IntegrateProjectTaskSelection, ListProjectsParams, PreviewProjectTaskParams, ProjectRecord, ProjectTaskApprovalGuards, ProjectTaskMediaRef, ProjectTaskModelPreviewResult, ProjectTaskPreviewResult, ProjectTaskRecord, RedeployProjectTaskJobResult, RefineProjectTaskParams, RefineProjectTaskResult, RejectProjectTaskResult, ReopenProjectTaskResult, RequestOptions, SessionRecord, SyncStreamParams, SyncStreamResult, SynthesizeProjectContextParams, UpdateProjectParams, UpdateProjectTaskParams } from './types.js';
3
3
  export declare class SwarmProjectsNamespace {
4
4
  private readonly transport;
5
5
  constructor(transport: SwarmTransport);
@@ -38,9 +38,18 @@ export declare class SwarmProjectsNamespace {
38
38
  listConversations(projectId: string, params?: {
39
39
  limit?: number;
40
40
  }, reqOptions?: RequestOptions): Promise<SessionRecord[]>;
41
+ /** Rename a project conversation (a conversation ID is its session ID); see sessions.rename. */
42
+ renameConversation(conversationId: string, title: string, options?: {
43
+ clientRequestId?: string;
44
+ }): Promise<{
45
+ ok: boolean;
46
+ session_id: string;
47
+ title: string;
48
+ }>;
41
49
  /** Create an independent project conversation without archiving existing conversations.
42
50
  * Models/context resolve on the daemon. Project conversations never bind a workspace.
43
- * Reuse clientRequestId when retrying an uncertain creation result.
51
+ * Reuse clientRequestId when retrying an uncertain creation result. Without a title the
52
+ * conversation is named from its first message; rename it with renameConversation.
44
53
  */
45
54
  createConversation(projectId: string, options?: {
46
55
  title?: string;
@@ -136,8 +145,14 @@ export declare class SwarmProjectsNamespace {
136
145
  refineTask(projectId: string, taskId: string, params: RefineProjectTaskParams, options?: RequestOptions): Promise<RefineProjectTaskResult>;
137
146
  /**
138
147
  * Integrates task commits into the parent workspace repository.
148
+ *
149
+ * The daemon only integrates an exact selection: the task's current session and its
150
+ * captured source/target branches. With no options the SDK fetches the task
151
+ * (`getTask`, which runs a fresh delivery assessment) and builds that selection from
152
+ * it; pass `options.selection` to override individual fields. A stale selection is
153
+ * refused by the daemon with a conflict error rather than integrating the wrong lane.
139
154
  */
140
- integrateTask(projectId: string, taskId: string, options?: RequestOptions): Promise<IntegrateProjectTaskResult>;
155
+ integrateTask(projectId: string, taskId: string, options?: IntegrateProjectTaskOptions): Promise<IntegrateProjectTaskResult>;
141
156
  /**
142
157
  * Deploys a task program for staged multi-agent execution.
143
158
  */
@@ -180,3 +195,10 @@ export declare class SwarmProjectsNamespace {
180
195
  timeoutMs?: number;
181
196
  }): AsyncIterableIterator<TEvent>;
182
197
  }
198
+ /**
199
+ * Builds the integrate selection the daemon checks from a fresh task record.
200
+ * Branch precedence: the detail GET's delivery assessment (the daemon's own view of
201
+ * the captured lane), then the task's recorded worktree/base branch, then the active
202
+ * attempt's. The daemon still verifies every field against the owned session.
203
+ */
204
+ export declare function integrationSelectionFromTask(task: ProjectTaskRecord): Partial<IntegrateProjectTaskSelection>;
package/dist/projects.js CHANGED
@@ -1,4 +1,5 @@
1
- import { SwarmApiError, SwarmTimeoutError, } from './errors.js';
1
+ import { SwarmApiError, SwarmTimeoutError, SwarmValidationError, } from './errors.js';
2
+ import { SwarmSessionsNamespace } from './sessions.js';
2
3
  export class SwarmProjectsNamespace {
3
4
  transport;
4
5
  constructor(transport) {
@@ -115,9 +116,14 @@ export class SwarmProjectsNamespace {
115
116
  return session;
116
117
  });
117
118
  }
119
+ /** Rename a project conversation (a conversation ID is its session ID); see sessions.rename. */
120
+ async renameConversation(conversationId, title, options = {}) {
121
+ return new SwarmSessionsNamespace(this.transport).rename(conversationId, title, options);
122
+ }
118
123
  /** Create an independent project conversation without archiving existing conversations.
119
124
  * Models/context resolve on the daemon. Project conversations never bind a workspace.
120
- * Reuse clientRequestId when retrying an uncertain creation result.
125
+ * Reuse clientRequestId when retrying an uncertain creation result. Without a title the
126
+ * conversation is named from its first message; rename it with renameConversation.
121
127
  */
122
128
  async createConversation(projectId, options = {}, reqOptions) {
123
129
  const id = projectId.trim();
@@ -132,7 +138,7 @@ export class SwarmProjectsNamespace {
132
138
  body: {
133
139
  client_request_id: options.clientRequestId ?? `proj-orch-${globalThis.crypto.randomUUID()}`,
134
140
  agent_name: options.agentName ?? 'system-orchestrator',
135
- title: options.title ?? 'Orchestrator AI',
141
+ title: options.title,
136
142
  mode: options.mode ?? 'auto',
137
143
  },
138
144
  ...reqOptions,
@@ -485,11 +491,36 @@ export class SwarmProjectsNamespace {
485
491
  // ==========================================================================
486
492
  /**
487
493
  * Integrates task commits into the parent workspace repository.
488
- */
489
- async integrateTask(projectId, taskId, options) {
494
+ *
495
+ * The daemon only integrates an exact selection: the task's current session and its
496
+ * captured source/target branches. With no options the SDK fetches the task
497
+ * (`getTask`, which runs a fresh delivery assessment) and builds that selection from
498
+ * it; pass `options.selection` to override individual fields. A stale selection is
499
+ * refused by the daemon with a conflict error rather than integrating the wrong lane.
500
+ */
501
+ async integrateTask(projectId, taskId, options = {}) {
502
+ const { selection: overrides, body, ...requestOptions } = options;
490
503
  const pId = encodeURIComponent(projectId.trim());
491
504
  const tId = encodeURIComponent(taskId.trim());
492
- const res = await this.transport.request(`/v3/projects/${pId}/tasks/${tId}/integrate`, { method: 'POST', ...options });
505
+ // A caller-supplied raw body is treated as overrides too (older workaround shape).
506
+ const given = {
507
+ ...(body && typeof body === 'object' ? body : {}),
508
+ ...(overrides ?? {}),
509
+ };
510
+ let selection = given;
511
+ if (!given.session_id || !given.source_branch || !given.target_branch) {
512
+ const { task } = await this.getTask(projectId, taskId, {
513
+ signal: requestOptions.signal,
514
+ timeoutMs: requestOptions.timeoutMs,
515
+ headers: requestOptions.headers,
516
+ });
517
+ selection = { ...integrationSelectionFromTask(task), ...stripUndefined(given) };
518
+ }
519
+ const missing = ['session_id', 'source_branch', 'target_branch'].filter((key) => typeof selection[key] !== 'string' || !selection[key].trim());
520
+ if (missing.length > 0) {
521
+ throw new SwarmValidationError(`Task ${taskId} cannot be integrated: no ${missing.join(', ')} on the task record (it has no captured worktree lane yet). Pass options.selection to supply them.`, { missing, selection });
522
+ }
523
+ const res = await this.transport.request(`/v3/projects/${pId}/tasks/${tId}/integrate`, { ...requestOptions, method: 'POST', body: stripUndefined(selection) });
493
524
  return res.data;
494
525
  }
495
526
  /**
@@ -606,3 +637,31 @@ export class SwarmProjectsNamespace {
606
637
  }
607
638
  }
608
639
  }
640
+ function nonEmpty(value) {
641
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
642
+ }
643
+ function stripUndefined(value) {
644
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined));
645
+ }
646
+ /**
647
+ * Builds the integrate selection the daemon checks from a fresh task record.
648
+ * Branch precedence: the detail GET's delivery assessment (the daemon's own view of
649
+ * the captured lane), then the task's recorded worktree/base branch, then the active
650
+ * attempt's. The daemon still verifies every field against the owned session.
651
+ */
652
+ export function integrationSelectionFromTask(task) {
653
+ const assessment = task.delivery_assessment;
654
+ const attempts = Array.isArray(task.attempts) ? task.attempts : [];
655
+ const activeAttempt = attempts.find((a) => a && a.id && a.id === task.active_attempt_id) ??
656
+ attempts.find((a) => a && nonEmpty(a.session_id) && a.session_id === task.session_id);
657
+ const observed = assessment?.freshness === 'observed';
658
+ return stripUndefined({
659
+ session_id: nonEmpty(task.session_id) ?? nonEmpty(assessment?.session_id),
660
+ source_branch: nonEmpty(assessment?.source_branch) ?? nonEmpty(task.worktree_branch) ?? nonEmpty(activeAttempt?.worktree_branch),
661
+ target_branch: nonEmpty(assessment?.target_branch) ?? nonEmpty(task.base_branch) ?? nonEmpty(activeAttempt?.base_branch),
662
+ revision: typeof task.revision === 'number' ? task.revision : undefined,
663
+ attempt_id: nonEmpty(task.active_attempt_id) ?? nonEmpty(assessment?.attempt_id),
664
+ source_head: observed ? nonEmpty(assessment?.source_oid) : undefined,
665
+ target_head: observed ? nonEmpty(assessment?.target_oid) : undefined,
666
+ });
667
+ }
@@ -185,9 +185,11 @@ export declare class SwarmOnboardingNamespace {
185
185
  get(): Promise<OnboardingStatus>;
186
186
  update(body: OnboardingUpdate): Promise<OnboardingStatus>;
187
187
  /**
188
- * Ensures the headless daemon identity is bootstrapped.
189
- * If the daemon requires onboarding, it automatically sets default identity parameters
190
- * so provider authentication and sessions can immediately proceed without manual wizard steps.
188
+ * Ensures the daemon identity (owner name and Swarm name) is bootstrapped, so
189
+ * provider authentication and sessions can proceed. The owner name is kept as typed.
190
+ * This only creates identity: it does not mark onboarding finished, so
191
+ * `config.desktop_onboarding_complete` stays false until your app (or the desktop)
192
+ * finishes its own setup and calls `update({ desktop_onboarding_complete: true })`.
191
193
  */
192
194
  ensureBootstrapped(options?: {
193
195
  username?: string;
@@ -116,22 +116,20 @@ export class SwarmOnboardingNamespace {
116
116
  return (await this.transport.request('/v1/onboarding', { method: 'POST', body })).data;
117
117
  }
118
118
  /**
119
- * Ensures the headless daemon identity is bootstrapped.
120
- * If the daemon requires onboarding, it automatically sets default identity parameters
121
- * so provider authentication and sessions can immediately proceed without manual wizard steps.
119
+ * Ensures the daemon identity (owner name and Swarm name) is bootstrapped, so
120
+ * provider authentication and sessions can proceed. The owner name is kept as typed.
121
+ * This only creates identity: it does not mark onboarding finished, so
122
+ * `config.desktop_onboarding_complete` stays false until your app (or the desktop)
123
+ * finishes its own setup and calls `update({ desktop_onboarding_complete: true })`.
122
124
  */
123
125
  async ensureBootstrapped(options = {}) {
124
126
  const status = await this.get();
125
- if (!status.needs_onboarding && status.identity?.bootstrapped) {
126
- return status;
127
- }
128
127
  if (status.identity?.bootstrapped) {
129
- return this.update({ desktop_onboarding_complete: true });
128
+ return status;
130
129
  }
131
130
  return this.update({
132
131
  username: options.username ?? 'owner',
133
132
  swarm_name: options.swarm_name ?? 'Headless Swarm',
134
- desktop_onboarding_complete: true,
135
133
  });
136
134
  }
137
135
  /** First credential only. Use auth.credentials.save for subsequent credentials. */