dsh-bots 0.0.1 → 0.2.11
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/LICENSE +21 -0
- package/README.md +8 -16
- package/cordis.patch.yml +5 -0
- package/lib/client.js +2199 -0
- package/lib/gateway.js +240 -0
- package/lib/index.js +508 -0
- package/lib/shared.js +37 -0
- package/lib/sse.js +249 -0
- package/lib/types/client.d.ts +35 -0
- package/lib/types/gateway.d.ts +32 -0
- package/lib/types/index.d.ts +210 -0
- package/lib/types/shared.d.ts +191 -0
- package/lib/types/sse.d.ts +79 -0
- package/lib/types/unread.d.ts +74 -0
- package/lib/types/version.d.ts +28 -0
- package/lib/types/workspace.d.ts +49 -0
- package/lib/unread.js +196 -0
- package/lib/version.js +109 -0
- package/lib/workspace.js +129 -0
- package/package.json +50 -14
package/lib/sse.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSE plumbing for the host half — pure ring + parser (unit-tested) and a
|
|
3
|
+
* native `fetch` listener that keeps the ring fed from the sdk-bots gateway
|
|
4
|
+
* `/events` endpoint. The web client has no network, so this is the only live
|
|
5
|
+
* event path; it replays through `bots.eventsSince(seq)`.
|
|
6
|
+
*
|
|
7
|
+
* The wire uses `Accept-Encoding: identity` to sidestep gzip/deflate handling
|
|
8
|
+
* in the host fetch layer (the gateway also supports `?token=` for the old
|
|
9
|
+
* browser path, but we always send the `authorization` header).
|
|
10
|
+
* @module dsh-bots/sse
|
|
11
|
+
*/
|
|
12
|
+
/** Incremental SSE wire parser: feed string chunks, receive frames. */
|
|
13
|
+
export class SseParser {
|
|
14
|
+
onEvent;
|
|
15
|
+
buffer = '';
|
|
16
|
+
event = '';
|
|
17
|
+
dataLines = [];
|
|
18
|
+
constructor(onEvent) {
|
|
19
|
+
this.onEvent = onEvent;
|
|
20
|
+
}
|
|
21
|
+
push(chunk) {
|
|
22
|
+
this.buffer += chunk;
|
|
23
|
+
let idx;
|
|
24
|
+
while ((idx = this.buffer.indexOf('\n')) >= 0) {
|
|
25
|
+
const line = this.buffer.slice(0, idx);
|
|
26
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
27
|
+
this.feedLine(line);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
feedLine(line) {
|
|
31
|
+
if (line.endsWith('\r'))
|
|
32
|
+
line = line.slice(0, -1);
|
|
33
|
+
if (line === '') {
|
|
34
|
+
const data = this.dataLines.join('\n');
|
|
35
|
+
const event = this.event !== '' ? this.event : 'message';
|
|
36
|
+
if (data !== '' || (event !== 'message' && this.dataLines.length > 0)) {
|
|
37
|
+
this.onEvent(event, data);
|
|
38
|
+
}
|
|
39
|
+
this.event = '';
|
|
40
|
+
this.dataLines = [];
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Comment / heartbeat line (`: ping`); ignore.
|
|
44
|
+
if (line.startsWith(':'))
|
|
45
|
+
return;
|
|
46
|
+
const colon = line.indexOf(':');
|
|
47
|
+
const field = colon < 0 ? line : line.slice(0, colon);
|
|
48
|
+
let value = colon < 0 ? '' : line.slice(colon + 1);
|
|
49
|
+
if (value.startsWith(' '))
|
|
50
|
+
value = value.slice(1);
|
|
51
|
+
if (field === 'event')
|
|
52
|
+
this.event = value;
|
|
53
|
+
else if (field === 'data')
|
|
54
|
+
this.dataLines.push(value);
|
|
55
|
+
// `id` / `retry` are intentionally ignored: seq is assigned locally.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Bounded, monotonic ring of normalized events with O(log n) `eventsSince`. */
|
|
59
|
+
export class SseRingBuffer {
|
|
60
|
+
onPush;
|
|
61
|
+
capacity;
|
|
62
|
+
entries = [];
|
|
63
|
+
nextSeq = 0;
|
|
64
|
+
constructor(capacity = 3000,
|
|
65
|
+
/** Optional side-channel observer (unread tracking); must never throw. */
|
|
66
|
+
onPush) {
|
|
67
|
+
this.onPush = onPush;
|
|
68
|
+
this.capacity = Math.max(1, Math.floor(capacity));
|
|
69
|
+
}
|
|
70
|
+
/** Append one event; returns its assigned seq. */
|
|
71
|
+
push(channel, data) {
|
|
72
|
+
const seq = this.nextSeq;
|
|
73
|
+
this.nextSeq += 1;
|
|
74
|
+
if (this.entries.length >= this.capacity)
|
|
75
|
+
this.entries.shift();
|
|
76
|
+
this.entries.push({ seq, channel, data });
|
|
77
|
+
if (this.onPush !== undefined) {
|
|
78
|
+
try {
|
|
79
|
+
this.onPush(channel, data);
|
|
80
|
+
}
|
|
81
|
+
catch { /* observer must not break the ring */ }
|
|
82
|
+
}
|
|
83
|
+
return seq;
|
|
84
|
+
}
|
|
85
|
+
/** Replay events strictly after `seq`, or everything when the seq fell off. */
|
|
86
|
+
eventsSince(seq) {
|
|
87
|
+
if (this.entries.length === 0)
|
|
88
|
+
return [];
|
|
89
|
+
const oldest = this.entries[0].seq;
|
|
90
|
+
if (seq < oldest - 1 || Number.isNaN(seq))
|
|
91
|
+
return this.entries.slice();
|
|
92
|
+
let lo = 0;
|
|
93
|
+
let hi = this.entries.length;
|
|
94
|
+
while (lo < hi) {
|
|
95
|
+
const mid = (lo + hi) >> 1;
|
|
96
|
+
if (this.entries[mid].seq <= seq)
|
|
97
|
+
lo = mid + 1;
|
|
98
|
+
else
|
|
99
|
+
hi = mid;
|
|
100
|
+
}
|
|
101
|
+
return this.entries.slice(lo);
|
|
102
|
+
}
|
|
103
|
+
get size() { return this.entries.length; }
|
|
104
|
+
get total() { return this.nextSeq; }
|
|
105
|
+
}
|
|
106
|
+
function toState(ring, live) {
|
|
107
|
+
return {
|
|
108
|
+
running: live.running,
|
|
109
|
+
ok: live.ok,
|
|
110
|
+
lastError: live.lastError,
|
|
111
|
+
connectedAt: live.connectedAt,
|
|
112
|
+
droppedAt: live.droppedAt,
|
|
113
|
+
buffered: ring.size,
|
|
114
|
+
total: ring.total,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Long-lived `/events` listener. Re-resolves discovery on every connect so a
|
|
119
|
+
* restarted sdk-bots host is picked up automatically; reconnects are silent
|
|
120
|
+
* and bounded. Deliberately never throws — errors surface via `state()`.
|
|
121
|
+
*/
|
|
122
|
+
export class GatewaySseClient {
|
|
123
|
+
ring;
|
|
124
|
+
resolveBase;
|
|
125
|
+
reconnectBaseMs;
|
|
126
|
+
channels;
|
|
127
|
+
onConnected;
|
|
128
|
+
ac = null;
|
|
129
|
+
timer = null;
|
|
130
|
+
stopped = true;
|
|
131
|
+
ok = false;
|
|
132
|
+
lastError = null;
|
|
133
|
+
connectedAt = null;
|
|
134
|
+
droppedAt = null;
|
|
135
|
+
attempt = 0;
|
|
136
|
+
constructor(opts) {
|
|
137
|
+
this.ring = opts.ring ?? new SseRingBuffer();
|
|
138
|
+
this.resolveBase = opts.resolveBase;
|
|
139
|
+
this.reconnectBaseMs = opts.reconnectBaseMs ?? 2000;
|
|
140
|
+
this.channels = opts.channels;
|
|
141
|
+
this.onConnected = opts.onConnected;
|
|
142
|
+
}
|
|
143
|
+
get buffer() { return this.ring; }
|
|
144
|
+
start() {
|
|
145
|
+
if (!this.stopped)
|
|
146
|
+
return;
|
|
147
|
+
this.stopped = false;
|
|
148
|
+
this.attempt = 0;
|
|
149
|
+
void this.connect();
|
|
150
|
+
}
|
|
151
|
+
stop() {
|
|
152
|
+
this.stopped = true;
|
|
153
|
+
if (this.timer !== null) {
|
|
154
|
+
clearTimeout(this.timer);
|
|
155
|
+
this.timer = null;
|
|
156
|
+
}
|
|
157
|
+
this.ac?.abort();
|
|
158
|
+
this.ac = null;
|
|
159
|
+
this.ok = false;
|
|
160
|
+
this.droppedAt = this.connectedAt !== null ? new Date().toISOString() : null;
|
|
161
|
+
}
|
|
162
|
+
state() {
|
|
163
|
+
return toState(this.ring, {
|
|
164
|
+
running: !this.stopped,
|
|
165
|
+
ok: this.ok,
|
|
166
|
+
lastError: this.lastError,
|
|
167
|
+
connectedAt: this.connectedAt,
|
|
168
|
+
droppedAt: this.droppedAt,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
eventsSince(seq) {
|
|
172
|
+
return { events: this.ring.eventsSince(seq), nextSeq: this.ring.total, state: this.state() };
|
|
173
|
+
}
|
|
174
|
+
async connect() {
|
|
175
|
+
if (this.stopped)
|
|
176
|
+
return;
|
|
177
|
+
const base = this.resolveBase();
|
|
178
|
+
if (base === null) {
|
|
179
|
+
this.lastError = 'no-gateway-json';
|
|
180
|
+
this.schedule();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
this.ac = new AbortController();
|
|
184
|
+
let endpoint = `${base.url}/events`;
|
|
185
|
+
if (this.channels !== undefined && this.channels.length > 0) {
|
|
186
|
+
endpoint += `?channels=${encodeURIComponent(this.channels.join(','))}`;
|
|
187
|
+
}
|
|
188
|
+
const headers = { accept: 'text/event-stream', 'accept-encoding': 'identity' };
|
|
189
|
+
if (base.token !== null)
|
|
190
|
+
headers['authorization'] = `Bearer ${base.token}`;
|
|
191
|
+
const parser = new SseParser((event, data) => {
|
|
192
|
+
let parsed = data;
|
|
193
|
+
try {
|
|
194
|
+
parsed = JSON.parse(data);
|
|
195
|
+
}
|
|
196
|
+
catch { /* keep raw string */ }
|
|
197
|
+
this.ring.push(event, parsed);
|
|
198
|
+
});
|
|
199
|
+
try {
|
|
200
|
+
const res = await fetch(endpoint, { headers, signal: this.ac.signal });
|
|
201
|
+
if (this.stopped)
|
|
202
|
+
return;
|
|
203
|
+
if (!res.ok || res.body === null) {
|
|
204
|
+
// Non-2xx or no body → not an SSE stream; treat as a hard connection error.
|
|
205
|
+
this.ok = false;
|
|
206
|
+
this.lastError = `health-http-${res.status}`;
|
|
207
|
+
this.droppedAt = new Date().toISOString();
|
|
208
|
+
this.schedule();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
this.ok = true;
|
|
212
|
+
this.lastError = null;
|
|
213
|
+
this.connectedAt = new Date().toISOString();
|
|
214
|
+
this.droppedAt = null;
|
|
215
|
+
this.attempt = 0;
|
|
216
|
+
if (this.onConnected !== undefined) {
|
|
217
|
+
try {
|
|
218
|
+
this.onConnected();
|
|
219
|
+
}
|
|
220
|
+
catch { /* observer must not break the loop */ }
|
|
221
|
+
}
|
|
222
|
+
const reader = res.body.getReader();
|
|
223
|
+
const decoder = new TextDecoder();
|
|
224
|
+
for (;;) {
|
|
225
|
+
const { done, value } = await reader.read();
|
|
226
|
+
if (done)
|
|
227
|
+
break;
|
|
228
|
+
parser.push(decoder.decode(value, { stream: true }));
|
|
229
|
+
}
|
|
230
|
+
this.ok = false;
|
|
231
|
+
this.droppedAt = new Date().toISOString();
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
if (this.stopped)
|
|
235
|
+
return;
|
|
236
|
+
this.ok = false;
|
|
237
|
+
this.lastError = String(err?.message ?? err);
|
|
238
|
+
this.droppedAt = new Date().toISOString();
|
|
239
|
+
}
|
|
240
|
+
this.schedule();
|
|
241
|
+
}
|
|
242
|
+
schedule() {
|
|
243
|
+
if (this.stopped)
|
|
244
|
+
return;
|
|
245
|
+
this.attempt += 1;
|
|
246
|
+
const delay = Math.min(this.reconnectBaseMs * this.attempt, 15000);
|
|
247
|
+
this.timer = setTimeout(() => { this.timer = null; void this.connect(); }, delay);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-bots — client half (web), formal-plugin runtime.
|
|
3
|
+
*
|
|
4
|
+
* Rendered by the dsh web shell through the standard slot system with full
|
|
5
|
+
* browser DOM access. Two rules keep the surface native rather than
|
|
6
|
+
* native-looking:
|
|
7
|
+
*
|
|
8
|
+
* 1. Components come from `@deepseek-ai/dsh-client-ui-primitives`, which the
|
|
9
|
+
* shell publishes in its static module map alongside `react`. Buttons,
|
|
10
|
+
* inputs, icons, state dots and the markdown renderer are therefore the
|
|
11
|
+
* shipped ones, not reimplementations. Every lookup degrades to a local
|
|
12
|
+
* fallback so a primitives reshuffle can never blank the plugin.
|
|
13
|
+
* 2. Our own CSS uses stable `dbs-` class names carrying the *values* read
|
|
14
|
+
* out of the shipped stylesheets (row heights, radii, the composer var
|
|
15
|
+
* family), expressed through dsh theme variables. No hashed class name is
|
|
16
|
+
* borrowed, so a dsh rebuild cannot break the visuals.
|
|
17
|
+
*
|
|
18
|
+
* Sidebar integration: the left-nav workspace region (`sidebar.workspaces` is
|
|
19
|
+
* a single slot) is shadowed at a low priority — an officially supported move,
|
|
20
|
+
* the shell's own error text reads "register at a different priority to shadow
|
|
21
|
+
* it (lowest renders)" — and re-rendered as a two-group collapsible nav:
|
|
22
|
+
* - 「工作区」 — delegates the ORIGINAL shipped workspace browser, with its
|
|
23
|
+
* child slots and its rail branch intact (see DelegatedBrowser).
|
|
24
|
+
* - 「Bots」 — our bot/group tree; clicking opens the chat.
|
|
25
|
+
*
|
|
26
|
+
* Chat lives in the `shell.overlay` layer but is inset to the frame's centre
|
|
27
|
+
* column, so the sidebar stays visible and usable while a bot conversation is
|
|
28
|
+
* open — matching how a native session behaves.
|
|
29
|
+
*
|
|
30
|
+
* Live data: the host keeps an SSE ring fed from the sdk-bots `/events`
|
|
31
|
+
* channel. ONE bus drains it here and fans channels out to subscribers; no
|
|
32
|
+
* component owns the cursor and no component polls the gateway directly.
|
|
33
|
+
* @module dsh-bots/client
|
|
34
|
+
*/
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sdk-bots gateway client for the host half.
|
|
3
|
+
*
|
|
4
|
+
* The formal plugin runs inside the real host process, so this uses native
|
|
5
|
+
* `fetch` and `node:fs` — the curl-over-shell bridge from the dynamic-plugin
|
|
6
|
+
* prototype is gone. Discovery follows `gateway.json` in the sdk-bots data
|
|
7
|
+
* directory; `SAND_GATEWAY_TOKEN` (or the file's optional `token` field) pins
|
|
8
|
+
* auth when present.
|
|
9
|
+
* @module dsh-plugin-bots/gateway
|
|
10
|
+
*/
|
|
11
|
+
import type { AgentInfo, GatewayInfo, TranscriptEntry } from './shared.js';
|
|
12
|
+
export interface Discovery {
|
|
13
|
+
port: number;
|
|
14
|
+
pid: number | null;
|
|
15
|
+
token: string | null;
|
|
16
|
+
host: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function expandHome(p: string): string;
|
|
19
|
+
/** Read loopback discovery from `<dataDir>/gateway.json`; null when absent/stale. */
|
|
20
|
+
export declare function readDiscovery(dataDir: string): Discovery | null;
|
|
21
|
+
/** Discovery + `/health` probe with pid match validation. */
|
|
22
|
+
export declare function discover(dataDir: string): Promise<GatewayInfo>;
|
|
23
|
+
/** POST one `/api/<method>` command; unwraps `{result}` and throws on errors. */
|
|
24
|
+
export declare function callGateway<T>(dataDir: string, method: string, args?: Record<string, unknown>): Promise<T>;
|
|
25
|
+
/** Project a raw gateway agent onto the wire type. */
|
|
26
|
+
export declare function trimAgent(a: any): AgentInfo | null;
|
|
27
|
+
/** Normalize one transcript entry for the UI's closed render switch. */
|
|
28
|
+
export declare function trimEntry(en: any): TranscriptEntry;
|
|
29
|
+
/** `listAgents` returns a bare array (also tolerates `{agents:[...]}`). */
|
|
30
|
+
export declare function normalizeAgents(raw: any): AgentInfo[];
|
|
31
|
+
/** Client nonce for idempotent create/send calls. */
|
|
32
|
+
export declare function nextNonce(): string;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-bots — host half.
|
|
3
|
+
*
|
|
4
|
+
* Bridges the sdk-bots orchestration gateway (single bots, group chats,
|
|
5
|
+
* transcripts, live SSE) into dsh as a formal plugin. Publishes the `bots`
|
|
6
|
+
* Remote namespace through the Typert gateway: every method takes a single
|
|
7
|
+
* `request` JSON value and returns plain JSON (source-mode descriptors,
|
|
8
|
+
* matching how the web client calls `bots/<method>` over the connection
|
|
9
|
+
* RPC carrier).
|
|
10
|
+
*
|
|
11
|
+
* Typert identity: dsh's host identifies a `TypertRemoteService` by the module
|
|
12
|
+
* instance it was loaded from — not by name. When this plugin is installed by
|
|
13
|
+
* pnpm it can resolve a *separate* copy of `@deepseek-ai/dsh-typert-protocol`
|
|
14
|
+
* from its own node_modules, producing a class that is unequal to the host's
|
|
15
|
+
* and silently dropping every method. We therefore anchor at load time to the
|
|
16
|
+
* running dsh CLI / global layout (the same technique used by dsh-freeroute)
|
|
17
|
+
* and only fall back to plain resolution when anchoring fails. The `Remote`
|
|
18
|
+
* markers are applied with a decorator-context shim for the same reason.
|
|
19
|
+
* @module dsh-bots
|
|
20
|
+
*/
|
|
21
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
22
|
+
import type { AgentInfo, Config, EventsSinceResult, GatewayInfo, McpServerInfo, McpToolInfo, SessionInfo, SseState, TranscriptEntry, WorkspaceInfo } from './shared.js';
|
|
23
|
+
export declare const name = "dsh-bots";
|
|
24
|
+
/** No hard Cordis service requirements: host reads optional services via
|
|
25
|
+
* guarded `ctx.get` so a partially-provisioned kernel never blocks activation. */
|
|
26
|
+
export declare const inject: string[];
|
|
27
|
+
/** Cordis range this plugin is tested against; only surfaces a warning. */
|
|
28
|
+
export declare const TESTED_CORDIS_RANGE = "^4.0.1";
|
|
29
|
+
/**
|
|
30
|
+
* Peer-compatibility probe — non-fatal. An unresolvable version or a caret
|
|
31
|
+
* skew inside the tested range must not brick the whole plugin tree (the
|
|
32
|
+
* guard existed to surface silent mismatches loudly).
|
|
33
|
+
*/
|
|
34
|
+
export declare function assertPeerCompatible(): void;
|
|
35
|
+
declare const TypertRemoteService: any;
|
|
36
|
+
/**
|
|
37
|
+
* The `bots` Remote namespace. One method per gateway capability, plus
|
|
38
|
+
* host-backed lists (workspaces/sessions) and live SSE replay so the web
|
|
39
|
+
* workbench renders natively without polling the transcript API.
|
|
40
|
+
*/
|
|
41
|
+
/** Exported for diagnostics/tests: marker assertions need the prototype. */
|
|
42
|
+
export declare class BotsRemote extends TypertRemoteService {
|
|
43
|
+
private readonly cfg;
|
|
44
|
+
private readonly sse;
|
|
45
|
+
private readonly unread;
|
|
46
|
+
private rebasing;
|
|
47
|
+
constructor(ctx: Context, config: Config);
|
|
48
|
+
/**
|
|
49
|
+
* Ring observer: feed the unread model from live transcript traffic.
|
|
50
|
+
* `appended` is the single-arrival shape; the gateway also replays a
|
|
51
|
+
* `snapshot` payload on (re)connect — both are timestamp-guarded, so
|
|
52
|
+
* replays never double-count.
|
|
53
|
+
*/
|
|
54
|
+
private observeTranscript;
|
|
55
|
+
/**
|
|
56
|
+
* Reconstruct counts from transcript tails (SSE has no replay, so events
|
|
57
|
+
* that fire while the stream is down would otherwise be lost forever).
|
|
58
|
+
* Runs once per (re)connect; never throws; skips while one is in flight.
|
|
59
|
+
*/
|
|
60
|
+
private rebaseUnread;
|
|
61
|
+
gatewayInfo(request: {
|
|
62
|
+
dataDir?: string;
|
|
63
|
+
} | null): Promise<GatewayInfo>;
|
|
64
|
+
/**
|
|
65
|
+
* Descriptor contract: every remote method declares exactly one plain
|
|
66
|
+
* `request` formal parameter (no defaults/destructuring/rest) — the
|
|
67
|
+
* source-mode descriptor derives its wire field from the parameter name,
|
|
68
|
+
* and the client always sends `{ args: { request } }`. A zero-param method
|
|
69
|
+
* would make the gateway reject the envelope with "unexpected request".
|
|
70
|
+
*/
|
|
71
|
+
list(request: unknown): Promise<AgentInfo[]>;
|
|
72
|
+
workspaces(request: unknown): Promise<{
|
|
73
|
+
workspaces: WorkspaceInfo[];
|
|
74
|
+
}>;
|
|
75
|
+
sessions(request: unknown): Promise<{
|
|
76
|
+
sessions: SessionInfo[];
|
|
77
|
+
}>;
|
|
78
|
+
create(request: {
|
|
79
|
+
name?: string;
|
|
80
|
+
description?: string;
|
|
81
|
+
} | null): Promise<AgentInfo | null>;
|
|
82
|
+
createGroup(request: {
|
|
83
|
+
name?: string;
|
|
84
|
+
memberIds?: string[];
|
|
85
|
+
} | null): Promise<AgentInfo | null>;
|
|
86
|
+
/**
|
|
87
|
+
* Replace a group's member list (add + remove in one call — the gateway
|
|
88
|
+
* command is a full-set put, not a delta). Wire field is `memberAgentIds`,
|
|
89
|
+
* same as `createGroup` (§7.2).
|
|
90
|
+
*/
|
|
91
|
+
setGroupMembers(request: {
|
|
92
|
+
id?: string;
|
|
93
|
+
memberIds?: string[];
|
|
94
|
+
} | null): Promise<AgentInfo | null>;
|
|
95
|
+
update(request: {
|
|
96
|
+
id?: string;
|
|
97
|
+
profile?: Record<string, unknown>;
|
|
98
|
+
} | null): Promise<AgentInfo | null>;
|
|
99
|
+
remove(request: {
|
|
100
|
+
id?: string;
|
|
101
|
+
} | null): Promise<unknown>;
|
|
102
|
+
send(request: {
|
|
103
|
+
agentId?: string;
|
|
104
|
+
prompt?: string;
|
|
105
|
+
} | null): Promise<unknown>;
|
|
106
|
+
/**
|
|
107
|
+
* Interrupt an agent's active run (the composer's stop button). Returns the
|
|
108
|
+
* gateway's honest `{hadActiveRun}` so the UI can tell "stopped it" from
|
|
109
|
+
* "there was nothing to stop" — never a fake success.
|
|
110
|
+
*/
|
|
111
|
+
interrupt(request: {
|
|
112
|
+
id?: string;
|
|
113
|
+
} | null): Promise<{
|
|
114
|
+
hadActiveRun: boolean;
|
|
115
|
+
}>;
|
|
116
|
+
transcriptTail(request: {
|
|
117
|
+
id?: string;
|
|
118
|
+
limit?: number;
|
|
119
|
+
} | null): Promise<{
|
|
120
|
+
entries: TranscriptEntry[];
|
|
121
|
+
}>;
|
|
122
|
+
/**
|
|
123
|
+
* Clear an agent's unread badge. The plugin owns the unread model (see
|
|
124
|
+
* `unread.ts`): the marker "上次读到哪" advances to now and the count
|
|
125
|
+
* zeroes. The gateway call is kept best-effort so desktop-app surfaces
|
|
126
|
+
* (spend guard's lastViewedAt) stay consistent with what the user saw.
|
|
127
|
+
*/
|
|
128
|
+
markRead(request: {
|
|
129
|
+
id?: string;
|
|
130
|
+
atMs?: number;
|
|
131
|
+
} | null): Promise<unknown>;
|
|
132
|
+
/** Trim a gateway MCP server row to the settings-page projection. */
|
|
133
|
+
private trimMcpServer;
|
|
134
|
+
/** Trim a routed MCP tool row (schema passed through verbatim). */
|
|
135
|
+
private trimMcpTool;
|
|
136
|
+
/** Installed MCP servers (engine `management.listInstalled`). */
|
|
137
|
+
mcpServers(request: unknown): Promise<{
|
|
138
|
+
servers: McpServerInfo[];
|
|
139
|
+
}>;
|
|
140
|
+
/** All routed MCP tools across servers (engine `mcp.listTools`). */
|
|
141
|
+
mcpTools(request: unknown): Promise<{
|
|
142
|
+
tools: McpToolInfo[];
|
|
143
|
+
}>;
|
|
144
|
+
/**
|
|
145
|
+
* Register one MCP server. `configJson` must decode to a JSON object —
|
|
146
|
+
* either a stdio config (`{"command": "…", "args": […]}`) or a remote URL
|
|
147
|
+
* config; the gateway JSON.parses and re-validates it.
|
|
148
|
+
*/
|
|
149
|
+
mcpAdd(request: {
|
|
150
|
+
name?: string;
|
|
151
|
+
configJson?: string;
|
|
152
|
+
} | null): Promise<{
|
|
153
|
+
servers: McpServerInfo[];
|
|
154
|
+
}>;
|
|
155
|
+
mcpRemove(request: {
|
|
156
|
+
serverId?: string;
|
|
157
|
+
} | null): Promise<unknown>;
|
|
158
|
+
/** Restart / reconnect all MCP servers (engine `management.restart`). */
|
|
159
|
+
mcpRefresh(request: unknown): Promise<unknown>;
|
|
160
|
+
/**
|
|
161
|
+
* Execute one routed MCP tool outside a bot turn (tool try-run panel).
|
|
162
|
+
* Passthrough of the gateway wire shape — note the engine swaps `name`/
|
|
163
|
+
* `toolName` on the way into the executor (DEVELOPMENT.md §13.5), so the
|
|
164
|
+
* caller maps `{name: row.toolName, toolName: row.name}` until实测 confirmed.
|
|
165
|
+
*/
|
|
166
|
+
mcpExecute(request: {
|
|
167
|
+
agentId?: string;
|
|
168
|
+
name?: string;
|
|
169
|
+
toolName?: string;
|
|
170
|
+
providerIdentifier?: string;
|
|
171
|
+
args?: unknown;
|
|
172
|
+
toolCallId?: string;
|
|
173
|
+
} | null): Promise<unknown>;
|
|
174
|
+
workspaceList(request: unknown): Promise<{
|
|
175
|
+
workspaces: import("./workspace.js").AgentWorkspaceConfig[];
|
|
176
|
+
}>;
|
|
177
|
+
workspaceGet(request: {
|
|
178
|
+
agentId?: string;
|
|
179
|
+
} | null): Promise<import("./workspace.js").AgentWorkspaceConfig>;
|
|
180
|
+
workspaceSet(request: {
|
|
181
|
+
agentId?: string;
|
|
182
|
+
workspaceRoot?: string | null;
|
|
183
|
+
allowPaths?: string[];
|
|
184
|
+
} | null): Promise<import("./workspace.js").AgentWorkspaceConfig>;
|
|
185
|
+
/**
|
|
186
|
+
* Append one diagnostic record to `<dataDir>/dsh-bots-diag.jsonl`.
|
|
187
|
+
*
|
|
188
|
+
* The shadow takeover of `sidebar.workspaces` is the one part of this plugin
|
|
189
|
+
* whose failure mode is invisible — a missing shipped entry or a throwing
|
|
190
|
+
* prop synthesis just renders a small notice. Recording those transitions is
|
|
191
|
+
* the only way to tell, after the fact, whether delegation actually worked
|
|
192
|
+
* on a user's machine. Best-effort by construction: diagnostics must never
|
|
193
|
+
* be able to fail a render.
|
|
194
|
+
*/
|
|
195
|
+
diag(request: {
|
|
196
|
+
stage?: string;
|
|
197
|
+
detail?: unknown;
|
|
198
|
+
} | null): Promise<{
|
|
199
|
+
written: boolean;
|
|
200
|
+
}>;
|
|
201
|
+
eventsSince(request: {
|
|
202
|
+
seq?: number;
|
|
203
|
+
} | null): EventsSinceResult;
|
|
204
|
+
sseState(request: unknown): SseState;
|
|
205
|
+
/** Stop the live event loop on plugin teardown. */
|
|
206
|
+
stopSse(): void;
|
|
207
|
+
}
|
|
208
|
+
/** Plugin entry: guard peers, publish the `bots` Remote namespace. */
|
|
209
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
210
|
+
export {};
|