@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.
- package/HEADLESS_UI.md +4 -6
- package/README.md +121 -56
- package/dist/agents.d.ts +114 -0
- package/dist/agents.js +183 -0
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +16 -0
- package/dist/box.d.ts +68 -0
- package/dist/box.js +13 -0
- package/dist/browser.d.ts +6 -2
- package/dist/browser.js +11 -3
- package/dist/chat.d.ts +41 -7
- package/dist/chat.js +148 -83
- package/dist/client.d.ts +12 -0
- package/dist/client.js +19 -6
- package/dist/copy-tags.d.ts +26 -0
- package/dist/copy-tags.js +133 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +21 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/local-connection.d.ts +39 -0
- package/dist/local-connection.js +56 -0
- package/dist/permission-ui.d.ts +1 -0
- package/dist/permission-ui.js +37 -0
- package/dist/permissions.d.ts +4 -0
- package/dist/permissions.js +7 -0
- package/dist/projects.d.ts +25 -3
- package/dist/projects.js +65 -6
- package/dist/provider-auth.d.ts +5 -3
- package/dist/provider-auth.js +6 -8
- package/dist/realtime.d.ts +40 -6
- package/dist/realtime.js +156 -52
- package/dist/remote.d.ts +59 -0
- package/dist/remote.js +46 -0
- package/dist/sessions.d.ts +31 -1
- package/dist/sessions.js +49 -1
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +19 -3
- package/dist/signals.d.ts +148 -0
- package/dist/signals.js +97 -0
- package/dist/types.d.ts +70 -2
- package/dist/websocket.d.ts +3 -1
- package/dist/websocket.js +1 -1
- package/dist/workspaces.d.ts +11 -0
- package/dist/workspaces.js +17 -0
- 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
|
+
}
|
package/dist/permission-ui.d.ts
CHANGED
|
@@ -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;
|
package/dist/permission-ui.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/permissions.d.ts
CHANGED
|
@@ -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.
|
package/dist/permissions.js
CHANGED
|
@@ -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.
|
package/dist/projects.d.ts
CHANGED
|
@@ -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?:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/provider-auth.d.ts
CHANGED
|
@@ -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
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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;
|
package/dist/provider-auth.js
CHANGED
|
@@ -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
|
|
120
|
-
*
|
|
121
|
-
*
|
|
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
|
|
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. */
|