@openagents-org/agent-launcher 0.2.193 → 0.2.195
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -2
- package/package.json +1 -1
- package/registry.json +9 -3
- package/src/adapters/antigravity-stream.js +65 -5
- package/src/adapters/base.js +297 -11
- package/src/adapters/claude.js +154 -23
- package/src/adapters/codex.js +40 -4
- package/src/adapters/cursor-node-entry.js +64 -0
- package/src/adapters/cursor.js +31 -1
- package/src/adapters/hermes.js +354 -58
- package/src/adapters/opencode.js +25 -3
- package/src/adapters/response-artifacts.js +72 -0
- package/src/adapters/thread-history.js +49 -0
- package/src/adapters/workspace-prompt.js +102 -12
- package/src/cli-models.js +244 -0
- package/src/cli.js +69 -13
- package/src/config.js +1 -0
- package/src/daemon.js +153 -15
- package/src/index.js +10 -1
- package/src/installer.js +413 -16
- package/src/mcp-server.js +138 -9
- package/src/node-config.js +6 -4
- package/src/postinstall-stub.js +69 -0
- package/src/tools-v11/brief.js +97 -0
- package/src/tools-v11/index.js +28 -0
- package/src/tools-v11/presence-handoff.js +199 -0
- package/src/workspace-client.js +230 -18
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Roadmap v1.1 M3/M6 — presence detail + structured agent-to-agent hand-off.
|
|
4
|
+
*
|
|
5
|
+
* workspace_agent_availability is <agent> free / busy / offline / runtime down?
|
|
6
|
+
* workspace_handoff hand work to another agent with request, context,
|
|
7
|
+
* output so far and next owner travelling together
|
|
8
|
+
* workspace_get_handoffs the recent hand-offs in a thread
|
|
9
|
+
*
|
|
10
|
+
* A hand-off is an ordinary `workspace.message.posted` chat message that
|
|
11
|
+
* OPENS with `@<to_agent>` (the backend's deterministic explicit-target path:
|
|
12
|
+
* it routes to exactly that agent and adds it to the thread) and carries the
|
|
13
|
+
* structured record twice — `payload.handoff` for readers of the thread and
|
|
14
|
+
* `metadata.handoff` for routing/analytics — so nothing depends on parsing
|
|
15
|
+
* the markdown back out.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const MAX_HANDOFFS = 10;
|
|
19
|
+
|
|
20
|
+
const defs = [
|
|
21
|
+
{
|
|
22
|
+
name: 'workspace_agent_availability',
|
|
23
|
+
description:
|
|
24
|
+
'Check whether another agent can take work right now. Returns its status, whether it is busy ' +
|
|
25
|
+
'(and in which threads / how many messages are queued), whether the device it runs on is ' +
|
|
26
|
+
'online, and one reason: available | busy | agent_offline | runtime_offline.',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
agent: { type: 'string', description: 'Agent name (without the openagents: prefix)' },
|
|
31
|
+
},
|
|
32
|
+
required: ['agent'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'workspace_handoff',
|
|
37
|
+
description:
|
|
38
|
+
'Hand a piece of work to another agent in the current thread. Posts "@<to_agent> <request>" with ' +
|
|
39
|
+
'optional Context / Output so far / Next owner sections so the request, what it needs, what you ' +
|
|
40
|
+
'already produced and who owns the next step travel together. Only the addressed agent is woken. ' +
|
|
41
|
+
'Use workspace_agent_availability first if timing matters.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
to_agent: { type: 'string', description: 'Agent to hand the work to (name only)' },
|
|
46
|
+
request: { type: 'string', description: 'What you are asking the agent to do — one or two sentences' },
|
|
47
|
+
context: { type: 'string', description: 'Background the agent needs (inputs, constraints, links, file ids)' },
|
|
48
|
+
output: { type: 'string', description: 'What you have already produced (results, paths, partial work)' },
|
|
49
|
+
next_owner: { type: 'string', description: 'Who owns the next step after this (defaults to to_agent)' },
|
|
50
|
+
},
|
|
51
|
+
required: ['to_agent', 'request'],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'workspace_get_handoffs',
|
|
56
|
+
description:
|
|
57
|
+
'List the most recent structured hand-offs in a thread (newest first, max 10): who handed what to ' +
|
|
58
|
+
'whom, with the context, output and next owner they carried. Defaults to the current thread.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
channel: { type: 'string', description: 'Thread name; omit for the current one' },
|
|
63
|
+
limit: { type: 'integer', description: 'How many to return (1-10, default 10)' },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function cleanName(name) {
|
|
70
|
+
return String(name || '').trim().replace(/^@/, '').replace(/^openagents:/, '');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function cleanText(v, max) {
|
|
74
|
+
if (v === undefined || v === null) return null;
|
|
75
|
+
const s = String(v).trim();
|
|
76
|
+
if (!s) return null;
|
|
77
|
+
return max && s.length > max ? s.slice(0, max) + '…' : s;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build the hand-off record + message content. Pure; exported for tests.
|
|
82
|
+
* Returns { handoff, content } where handoff =
|
|
83
|
+
* { from, to, request, context, output, next_owner }.
|
|
84
|
+
*/
|
|
85
|
+
function buildHandoff(from, { to_agent, request, context, output, next_owner } = {}) {
|
|
86
|
+
const to = cleanName(to_agent);
|
|
87
|
+
const req = cleanText(request, 4000);
|
|
88
|
+
if (!to) throw new Error('to_agent is required');
|
|
89
|
+
if (!req) throw new Error('request is required');
|
|
90
|
+
const handoff = {
|
|
91
|
+
from: cleanName(from),
|
|
92
|
+
to,
|
|
93
|
+
request: req,
|
|
94
|
+
context: cleanText(context, 8000),
|
|
95
|
+
output: cleanText(output, 8000),
|
|
96
|
+
next_owner: cleanName(next_owner) || to,
|
|
97
|
+
};
|
|
98
|
+
let content = `@${to} ${req}`;
|
|
99
|
+
if (handoff.context) content += `\n\nContext:\n${handoff.context}`;
|
|
100
|
+
if (handoff.output) content += `\n\nOutput so far:\n${handoff.output}`;
|
|
101
|
+
content += `\n\nNext owner: ${handoff.next_owner}`;
|
|
102
|
+
return { handoff, content };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The hand-off records found in a list of raw events (newest first). */
|
|
106
|
+
function extractHandoffs(events, limit = MAX_HANDOFFS) {
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const e of events || []) {
|
|
109
|
+
const payload = e.payload || {};
|
|
110
|
+
const meta = e.metadata || {};
|
|
111
|
+
const h = (payload.handoff && typeof payload.handoff === 'object') ? payload.handoff
|
|
112
|
+
: (meta.handoff && typeof meta.handoff === 'object') ? meta.handoff : null;
|
|
113
|
+
if (!h) continue;
|
|
114
|
+
out.push({
|
|
115
|
+
id: e.id || null,
|
|
116
|
+
timestamp: e.timestamp || null,
|
|
117
|
+
from: h.from || String(e.source || '').replace(/^openagents:/, ''),
|
|
118
|
+
to: h.to || null,
|
|
119
|
+
request: h.request || null,
|
|
120
|
+
context: h.context || null,
|
|
121
|
+
output: h.output || null,
|
|
122
|
+
next_owner: h.next_owner || h.to || null,
|
|
123
|
+
});
|
|
124
|
+
if (out.length >= limit) break;
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function describeAvailability(agent, a) {
|
|
130
|
+
const bits = [`${agent}: ${a.reason || 'unknown'}`];
|
|
131
|
+
bits.push(`status=${a.status || '?'}`);
|
|
132
|
+
if (a.presence_state) bits.push(`presence=${a.presence_state}`);
|
|
133
|
+
if (Array.isArray(a.busy_channels) && a.busy_channels.length) bits.push(`busy in: ${a.busy_channels.join(', ')}`);
|
|
134
|
+
if (a.queue_depth) bits.push(`queued: ${a.queue_depth}`);
|
|
135
|
+
if (a.runtime_status) bits.push(`runtime ${a.runtime_status}${a.runtime_name ? ` (${a.runtime_name})` : ''}`);
|
|
136
|
+
const hint = {
|
|
137
|
+
available: 'It can take work now.',
|
|
138
|
+
busy: 'It will answer after the current run — a hand-off is queued, not lost.',
|
|
139
|
+
agent_offline: 'Its connector is not running; a message will wait until it reconnects.',
|
|
140
|
+
runtime_offline: 'The device it runs on is offline; nobody will pick this up until it is back.',
|
|
141
|
+
}[a.reason];
|
|
142
|
+
return bits.join(' · ') + (hint ? `\n${hint}` : '');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function handle(server, name, args, helpers) {
|
|
146
|
+
const { text } = helpers;
|
|
147
|
+
args = args || {};
|
|
148
|
+
switch (name) {
|
|
149
|
+
case 'workspace_agent_availability': {
|
|
150
|
+
const agent = cleanName(args.agent);
|
|
151
|
+
if (!agent) throw new Error('agent is required');
|
|
152
|
+
const a = await server.ws.getAgentAvailability(server.workspaceId, server.token, agent);
|
|
153
|
+
return text(describeAvailability(agent, a || {}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case 'workspace_handoff': {
|
|
157
|
+
const { handoff, content } = buildHandoff(server.agentName, args);
|
|
158
|
+
if (handoff.to === handoff.from) throw new Error('You cannot hand work to yourself');
|
|
159
|
+
const event = {
|
|
160
|
+
type: 'workspace.message.posted',
|
|
161
|
+
source: `openagents:${server.agentName}`,
|
|
162
|
+
target: `channel/${server.channelName}`,
|
|
163
|
+
payload: { content, message_type: 'chat', handoff },
|
|
164
|
+
metadata: { handoff, explicit_targets: [handoff.to] },
|
|
165
|
+
};
|
|
166
|
+
if (server.requesterEmail) event.metadata.requester_email = server.requesterEmail;
|
|
167
|
+
const result = await server.ws.sendEvent(server.workspaceId, event, server.token);
|
|
168
|
+
const targets = (result && result.metadata && result.metadata.target_agents) || [];
|
|
169
|
+
const delivered = targets.includes(handoff.to);
|
|
170
|
+
return text(
|
|
171
|
+
`Handed off to @${handoff.to}${delivered ? '' : ' (warning: the workspace did not route it — is that agent in this workspace?)'}. ` +
|
|
172
|
+
`Next owner: ${handoff.next_owner}. End your turn now unless you have other work; ` +
|
|
173
|
+
`@${handoff.to} will reply in this thread.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'workspace_get_handoffs': {
|
|
178
|
+
const channel = args.channel || server.channelName;
|
|
179
|
+
const limit = Math.min(Math.max(1, parseInt(args.limit, 10) || MAX_HANDOFFS), MAX_HANDOFFS);
|
|
180
|
+
const events = await server.ws.getChannelEvents(server.workspaceId, channel, server.token, { limit: 200, sort: 'desc' });
|
|
181
|
+
const handoffs = extractHandoffs(events, limit);
|
|
182
|
+
if (!handoffs.length) return text(`No hand-offs in ${channel} yet.`);
|
|
183
|
+
const lines = handoffs.map((h, i) => {
|
|
184
|
+
const parts = [`${i + 1}. ${h.from || '?'} → ${h.to || '?'}: ${h.request || ''}`];
|
|
185
|
+
if (h.context) parts.push(` Context: ${h.context}`);
|
|
186
|
+
if (h.output) parts.push(` Output so far: ${h.output}`);
|
|
187
|
+
if (h.next_owner) parts.push(` Next owner: ${h.next_owner}`);
|
|
188
|
+
if (h.timestamp) parts.push(` at ${new Date(h.timestamp).toISOString()}`);
|
|
189
|
+
return parts.join('\n');
|
|
190
|
+
});
|
|
191
|
+
return text(`Hand-offs in ${channel} (newest first):\n${lines.join('\n')}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
default:
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = { defs, handle, buildHandoff, extractHandoffs };
|
package/src/workspace-client.js
CHANGED
|
@@ -41,6 +41,30 @@ class SessionRevokedError extends Error {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Reject when a response is cut off after its headers arrived.
|
|
46
|
+
*
|
|
47
|
+
* Node reports that on the RESPONSE, and nowhere else: once a response
|
|
48
|
+
* exists the request emits no 'error' for a connection that closes early,
|
|
49
|
+
* 'end' only fires for a complete body, and the request's abort signal is a
|
|
50
|
+
* no-op by then because the socket is already gone. A request helper that
|
|
51
|
+
* listens for 'end' alone therefore never settles — and each poll loop is a
|
|
52
|
+
* single await, so one truncated response (a backend restart mid-reply is
|
|
53
|
+
* enough) silences that loop for the life of the daemon. When it is the
|
|
54
|
+
* control poller the agent keeps working and answering, and ignores Stop.
|
|
55
|
+
*/
|
|
56
|
+
function rejectOnTruncatedResponse(res, reject) {
|
|
57
|
+
res.on('error', reject);
|
|
58
|
+
res.on('close', () => {
|
|
59
|
+
if (res.complete) return;
|
|
60
|
+
// Same code Node puts on the 'error' above, so callers count either as
|
|
61
|
+
// the network blip it is.
|
|
62
|
+
const err = new Error('Response ended before it was complete');
|
|
63
|
+
err.code = 'ECONNRESET';
|
|
64
|
+
reject(err);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
/**
|
|
45
69
|
* HTTP client for workspace API operations.
|
|
46
70
|
*
|
|
@@ -48,8 +72,34 @@ class SessionRevokedError extends Error {
|
|
|
48
72
|
* auth headers (X-Workspace-Token), same request/response shapes.
|
|
49
73
|
*/
|
|
50
74
|
class WorkspaceClient {
|
|
51
|
-
|
|
75
|
+
/**
|
|
76
|
+
* @param {string} [endpoint]
|
|
77
|
+
* @param {{ agentName?: string }} [opts] — when the client acts for ONE
|
|
78
|
+
* agent, pass its name: every request then carries `X-Agent-Name` so the
|
|
79
|
+
* workspace can apply that agent's permissions (permission model v1.1 §3)
|
|
80
|
+
* instead of treating the call as an anonymous machine. Machine-level
|
|
81
|
+
* clients (daemon device heartbeat, pairing redeem, workspace removal)
|
|
82
|
+
* pass nothing and send no agent identity.
|
|
83
|
+
*/
|
|
84
|
+
constructor(endpoint, { agentName } = {}) {
|
|
52
85
|
this.endpoint = (endpoint || DEFAULT_ENDPOINT).replace(/\/$/, '');
|
|
86
|
+
this.agentName = typeof agentName === 'string' && agentName.trim() ? agentName.trim() : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A client for the same endpoint that acts as `agentName` (or none). */
|
|
90
|
+
withAgent(agentName) {
|
|
91
|
+
return new WorkspaceClient(this.endpoint, { agentName });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Request headers + the agent identity header. Applied in every transport
|
|
96
|
+
* helper (_get/_getRaw/_post/_put/_patch/_delete) so no call site can forget
|
|
97
|
+
* it. Returns a copy — callers' header objects are never mutated.
|
|
98
|
+
*/
|
|
99
|
+
_withAgent(headers) {
|
|
100
|
+
const out = { ...(headers || {}) };
|
|
101
|
+
if (this.agentName && !out['X-Agent-Name']) out['X-Agent-Name'] = this.agentName;
|
|
102
|
+
return out;
|
|
53
103
|
}
|
|
54
104
|
|
|
55
105
|
/**
|
|
@@ -217,16 +267,80 @@ class WorkspaceClient {
|
|
|
217
267
|
* If the server's current session for this agent differs, _post()
|
|
218
268
|
* throws SessionRevokedError and the caller should stop its adapter.
|
|
219
269
|
*/
|
|
220
|
-
async heartbeat(workspaceId, agentName, token, sessionId) {
|
|
270
|
+
async heartbeat(workspaceId, agentName, token, sessionId, presence) {
|
|
221
271
|
const body = {
|
|
222
272
|
agent_name: agentName,
|
|
223
273
|
network: workspaceId,
|
|
224
274
|
};
|
|
225
275
|
if (sessionId) body.session_id = sessionId;
|
|
276
|
+
// v1.1 M3 — busy/queued detail ({ presence_state, busy_channels,
|
|
277
|
+
// queue_depth }, see BaseAdapter.buildPresenceFields). Optional so older
|
|
278
|
+
// callers keep sending the legacy body.
|
|
279
|
+
if (presence && typeof presence === 'object') {
|
|
280
|
+
if (presence.presence_state !== undefined) body.presence_state = presence.presence_state;
|
|
281
|
+
if (Array.isArray(presence.busy_channels)) body.busy_channels = presence.busy_channels;
|
|
282
|
+
if (Number.isFinite(presence.queue_depth)) body.queue_depth = presence.queue_depth;
|
|
283
|
+
}
|
|
226
284
|
const data = await this._post('/v1/heartbeat', body, this._wsHeaders(token));
|
|
227
285
|
return data.data || data;
|
|
228
286
|
}
|
|
229
287
|
|
|
288
|
+
// ── Agent profile / presence (v1.1 M3/M4) ────────────────────────────────
|
|
289
|
+
|
|
290
|
+
/** GET /v1/agents/{agent}/availability → { status, presence_state,
|
|
291
|
+
* busy_channels, queue_depth, runtime_status, runtime_name, reason }. */
|
|
292
|
+
async getAgentAvailability(workspaceId, token, agentName) {
|
|
293
|
+
const data = await this._get(
|
|
294
|
+
`/v1/agents/${encodeURIComponent(agentName)}/availability?network=${encodeURIComponent(workspaceId)}`,
|
|
295
|
+
this._wsHeaders(token),
|
|
296
|
+
);
|
|
297
|
+
return data.data || data;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** GET /v1/agents/{agent}/profile — the specialist card. */
|
|
301
|
+
async getAgentProfile(workspaceId, token, agentName) {
|
|
302
|
+
const data = await this._get(
|
|
303
|
+
`/v1/agents/${encodeURIComponent(agentName)}/profile?network=${encodeURIComponent(workspaceId)}`,
|
|
304
|
+
this._wsHeaders(token),
|
|
305
|
+
);
|
|
306
|
+
return data.data || data;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* GET /v1/agents/{agent}/shared-context — what THIS agent's run may use when
|
|
311
|
+
* the request comes from a teammate rather than its owner. Machine-only.
|
|
312
|
+
* Returns { apply, owner_email, requester_email, shared_instructions,
|
|
313
|
+
* allowed_knowledge: [{ slug, title, content }], cost_owner }.
|
|
314
|
+
*/
|
|
315
|
+
async getSharedContext(workspaceId, token, agentName, { requesterEmail, channel, timeout } = {}) {
|
|
316
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
317
|
+
if (requesterEmail) params.set('requester_email', requesterEmail);
|
|
318
|
+
if (channel) params.set('channel', channel);
|
|
319
|
+
const data = await this._get(
|
|
320
|
+
`/v1/agents/${encodeURIComponent(agentName)}/shared-context?${params}`,
|
|
321
|
+
this._wsHeaders(token),
|
|
322
|
+
timeout || 5000,
|
|
323
|
+
);
|
|
324
|
+
return data.data || data;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Raw message events of a channel (newest first by default) — for callers
|
|
329
|
+
* that need payload/metadata fields _eventToMessage drops (e.g. hand-offs).
|
|
330
|
+
*/
|
|
331
|
+
async getChannelEvents(workspaceId, channelName, token, { limit = 100, sort = 'desc' } = {}) {
|
|
332
|
+
const params = new URLSearchParams({
|
|
333
|
+
network: workspaceId,
|
|
334
|
+
channel: channelName,
|
|
335
|
+
type: 'workspace.message',
|
|
336
|
+
sort,
|
|
337
|
+
limit: String(Math.min(Math.max(1, limit), 500)),
|
|
338
|
+
});
|
|
339
|
+
const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
|
|
340
|
+
const result = data.data || data;
|
|
341
|
+
return (result && result.events) || [];
|
|
342
|
+
}
|
|
343
|
+
|
|
230
344
|
/**
|
|
231
345
|
* Disconnect agent via POST /v1/leave. Best-effort (ignores errors).
|
|
232
346
|
*/
|
|
@@ -323,7 +437,7 @@ class WorkspaceClient {
|
|
|
323
437
|
* previous session fails (the channel's chat history is the only thing
|
|
324
438
|
* that survives a session-storage rotation).
|
|
325
439
|
*/
|
|
326
|
-
async getRecentMessages(workspaceId, channelName, token, limit = 30, { sort = 'desc' } = {}) {
|
|
440
|
+
async getRecentMessages(workspaceId, channelName, token, limit = 30, { sort = 'desc', before, timeout, throwOnError = false } = {}) {
|
|
327
441
|
try {
|
|
328
442
|
const params = new URLSearchParams({
|
|
329
443
|
network: workspaceId,
|
|
@@ -332,14 +446,16 @@ class WorkspaceClient {
|
|
|
332
446
|
sort,
|
|
333
447
|
limit: String(limit),
|
|
334
448
|
});
|
|
335
|
-
|
|
449
|
+
if (before) params.set('before', before);
|
|
450
|
+
const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token), timeout);
|
|
336
451
|
const result = data.data || data;
|
|
337
452
|
const events = (result && result.events) || [];
|
|
338
453
|
// A desc window arrives newest-first; reverse so the caller always gets
|
|
339
454
|
// chronological order. An asc window is already chronological.
|
|
340
455
|
const ordered = sort === 'asc' ? events.slice() : events.slice().reverse();
|
|
341
456
|
return ordered.map((e) => this._eventToMessage(e));
|
|
342
|
-
} catch {
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (throwOnError) throw error;
|
|
343
459
|
return [];
|
|
344
460
|
}
|
|
345
461
|
}
|
|
@@ -587,8 +703,17 @@ class WorkspaceClient {
|
|
|
587
703
|
/**
|
|
588
704
|
* Upload a file via POST /v1/files/base64.
|
|
589
705
|
*/
|
|
706
|
+
async publishWebProject(workspaceId, token, project, projectId) {
|
|
707
|
+
const body = { ...project, network: workspaceId };
|
|
708
|
+
const route = '/v1/web-projects' + (projectId ? '/' + encodeURIComponent(projectId) : '');
|
|
709
|
+
const data = projectId
|
|
710
|
+
? await this._put(route, body, this._wsHeaders(token), 60000)
|
|
711
|
+
: await this._post(route, body, this._wsHeaders(token), 60000);
|
|
712
|
+
return data.data || data;
|
|
713
|
+
}
|
|
714
|
+
|
|
590
715
|
async uploadFile(workspaceId, token, filename, contentBase64, {
|
|
591
|
-
contentType = 'application/octet-stream', source = 'human:user', channelName,
|
|
716
|
+
contentType = 'application/octet-stream', source = 'human:user', channelName, visibility,
|
|
592
717
|
} = {}) {
|
|
593
718
|
const body = {
|
|
594
719
|
filename,
|
|
@@ -598,6 +723,7 @@ class WorkspaceClient {
|
|
|
598
723
|
source,
|
|
599
724
|
};
|
|
600
725
|
if (channelName) body.channel_name = channelName;
|
|
726
|
+
if (visibility) body.visibility = visibility;
|
|
601
727
|
|
|
602
728
|
const data = await this._post('/v1/files/base64', body, this._wsHeaders(token), 60000);
|
|
603
729
|
return data.data || data;
|
|
@@ -675,8 +801,8 @@ class WorkspaceClient {
|
|
|
675
801
|
/**
|
|
676
802
|
* Open a new browser tab via POST /v1/browser/tabs.
|
|
677
803
|
*/
|
|
678
|
-
async browserOpenTab(workspaceId, token, { url = 'about:blank', source = 'human:user', context_id } = {}) {
|
|
679
|
-
const body = { url, network: workspaceId, source };
|
|
804
|
+
async browserOpenTab(workspaceId, token, { url = 'about:blank', source = 'human:user', context_id, channel } = {}) {
|
|
805
|
+
const body = { url, network: workspaceId, source, channel };
|
|
680
806
|
if (context_id) body.context_id = context_id;
|
|
681
807
|
const data = await this._post('/v1/browser/tabs', body, this._wsHeaders(token));
|
|
682
808
|
return data.data || data;
|
|
@@ -694,24 +820,24 @@ class WorkspaceClient {
|
|
|
694
820
|
/**
|
|
695
821
|
* Navigate a browser tab via POST /v1/browser/tabs/{tabId}/navigate.
|
|
696
822
|
*/
|
|
697
|
-
async browserNavigate(workspaceId, token, tabId, url) {
|
|
698
|
-
const data = await this._post(`/v1/browser/tabs/${tabId}/navigate`, { url }, this._wsHeaders(token));
|
|
823
|
+
async browserNavigate(workspaceId, token, tabId, url, { source, channel } = {}) {
|
|
824
|
+
const data = await this._post(`/v1/browser/tabs/${tabId}/navigate`, { url, source, channel }, this._wsHeaders(token));
|
|
699
825
|
return data.data || data;
|
|
700
826
|
}
|
|
701
827
|
|
|
702
828
|
/**
|
|
703
829
|
* Click an element via POST /v1/browser/tabs/{tabId}/click.
|
|
704
830
|
*/
|
|
705
|
-
async browserClick(workspaceId, token, tabId, selector) {
|
|
706
|
-
const data = await this._post(`/v1/browser/tabs/${tabId}/click`, { selector }, this._wsHeaders(token));
|
|
831
|
+
async browserClick(workspaceId, token, tabId, selector, { source, channel } = {}) {
|
|
832
|
+
const data = await this._post(`/v1/browser/tabs/${tabId}/click`, { selector, source, channel }, this._wsHeaders(token));
|
|
707
833
|
return data.data || data;
|
|
708
834
|
}
|
|
709
835
|
|
|
710
836
|
/**
|
|
711
837
|
* Type text via POST /v1/browser/tabs/{tabId}/type.
|
|
712
838
|
*/
|
|
713
|
-
async browserType(workspaceId, token, tabId, selector, text) {
|
|
714
|
-
const data = await this._post(`/v1/browser/tabs/${tabId}/type`, { selector, text }, this._wsHeaders(token));
|
|
839
|
+
async browserType(workspaceId, token, tabId, selector, text, { source, channel } = {}) {
|
|
840
|
+
const data = await this._post(`/v1/browser/tabs/${tabId}/type`, { selector, text, source, channel }, this._wsHeaders(token));
|
|
715
841
|
return data.data || data;
|
|
716
842
|
}
|
|
717
843
|
|
|
@@ -719,16 +845,22 @@ class WorkspaceClient {
|
|
|
719
845
|
* Get screenshot via GET /v1/browser/tabs/{tabId}/screenshot.
|
|
720
846
|
* @returns {Buffer}
|
|
721
847
|
*/
|
|
722
|
-
async browserScreenshot(workspaceId, token, tabId) {
|
|
723
|
-
|
|
848
|
+
async browserScreenshot(workspaceId, token, tabId, { source, channel } = {}) {
|
|
849
|
+
const query = new URLSearchParams();
|
|
850
|
+
if (source) query.set('source', source);
|
|
851
|
+
if (channel) query.set('channel', channel);
|
|
852
|
+
return this._getRaw(`/v1/browser/tabs/${tabId}/screenshot?${query}`, this._wsHeaders(token), 30000);
|
|
724
853
|
}
|
|
725
854
|
|
|
726
855
|
/**
|
|
727
856
|
* Get accessibility snapshot via GET /v1/browser/tabs/{tabId}/snapshot.
|
|
728
857
|
* @returns {string}
|
|
729
858
|
*/
|
|
730
|
-
async browserSnapshot(workspaceId, token, tabId) {
|
|
731
|
-
const
|
|
859
|
+
async browserSnapshot(workspaceId, token, tabId, { source, channel } = {}) {
|
|
860
|
+
const query = new URLSearchParams();
|
|
861
|
+
if (source) query.set('source', source);
|
|
862
|
+
if (channel) query.set('channel', channel);
|
|
863
|
+
const buf = await this._getRaw(`/v1/browser/tabs/${tabId}/snapshot?${query}`, this._wsHeaders(token));
|
|
732
864
|
return buf.toString('utf-8');
|
|
733
865
|
}
|
|
734
866
|
|
|
@@ -943,11 +1075,25 @@ class WorkspaceClient {
|
|
|
943
1075
|
sessionId,
|
|
944
1076
|
senderType: isHuman ? 'human' : 'agent',
|
|
945
1077
|
senderName,
|
|
1078
|
+
senderDisplayName: payload.sender_display_name || (event.metadata || {}).sender_display_name || null,
|
|
946
1079
|
content: (payload.content || event.content || ''),
|
|
947
1080
|
mentions: payload.mentions || [],
|
|
948
1081
|
messageType: payload.message_type || 'chat',
|
|
949
1082
|
metadata: event.metadata || {},
|
|
950
1083
|
};
|
|
1084
|
+
// The person behind a human message (v1.1): mobile puts it in the
|
|
1085
|
+
// payload, the Launcher in metadata, the web app uses its email as the
|
|
1086
|
+
// sender id. Used to fetch the owner-reviewed shared context for a
|
|
1087
|
+
// teammate's request and to tell the owner who asked.
|
|
1088
|
+
if (isHuman) {
|
|
1089
|
+
const meta = event.metadata || {};
|
|
1090
|
+
const email = [payload.sender_email, meta.sender_email, payload.sender_id]
|
|
1091
|
+
.find((v) => typeof v === 'string' && /^[^\s@]+@[^\s@]+$/.test(v.trim()));
|
|
1092
|
+
if (email) msg.senderEmail = email.trim().toLowerCase();
|
|
1093
|
+
}
|
|
1094
|
+
if (payload.handoff && typeof payload.handoff === 'object') {
|
|
1095
|
+
msg.handoff = payload.handoff;
|
|
1096
|
+
}
|
|
951
1097
|
if (ts) {
|
|
952
1098
|
msg.createdAt = new Date(ts).toISOString();
|
|
953
1099
|
}
|
|
@@ -957,6 +1103,60 @@ class WorkspaceClient {
|
|
|
957
1103
|
return msg;
|
|
958
1104
|
}
|
|
959
1105
|
|
|
1106
|
+
// ── Approvals ─────────────────────────────────────────────────────────────
|
|
1107
|
+
|
|
1108
|
+
/** File an approval request (POST /v1/approvals). Returns the record; its
|
|
1109
|
+
* `status` may already be approved/rejected when policy auto-decided. */
|
|
1110
|
+
async createApproval(workspaceId, token, {
|
|
1111
|
+
channel, kind, action, details, risk, source, assignee_email, requester_email,
|
|
1112
|
+
} = {}) {
|
|
1113
|
+
const body = {
|
|
1114
|
+
network: workspaceId,
|
|
1115
|
+
channel,
|
|
1116
|
+
kind: kind || 'other',
|
|
1117
|
+
action,
|
|
1118
|
+
source: source || 'openagents:unknown',
|
|
1119
|
+
};
|
|
1120
|
+
if (details) body.details = String(details).slice(0, 4000);
|
|
1121
|
+
if (risk) body.risk = risk;
|
|
1122
|
+
if (assignee_email) body.assignee_email = assignee_email;
|
|
1123
|
+
if (requester_email) body.requester_email = requester_email;
|
|
1124
|
+
const data = await this._post('/v1/approvals', body, this._wsHeaders(token));
|
|
1125
|
+
return data.data || data;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
async getApproval(workspaceId, token, approvalId) {
|
|
1129
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
1130
|
+
const data = await this._get(`/v1/approvals/${encodeURIComponent(approvalId)}?${params}`, this._wsHeaders(token));
|
|
1131
|
+
return data.data || data;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// ── Work brief (v1.1 M5) ──────────────────────────────────────────────────
|
|
1135
|
+
|
|
1136
|
+
/** Read a thread's shared work brief (GET /v1/channels/{channel}/brief).
|
|
1137
|
+
* Empty fields come back null / [] when nothing has been written yet. */
|
|
1138
|
+
async getBrief(workspaceId, token, channelName) {
|
|
1139
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
1140
|
+
const data = await this._get(
|
|
1141
|
+
`/v1/channels/${encodeURIComponent(channelName)}/brief?${params}`,
|
|
1142
|
+
this._wsHeaders(token),
|
|
1143
|
+
);
|
|
1144
|
+
return data.data || data;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** Partial upsert of a thread's brief (PUT /v1/channels/{channel}/brief).
|
|
1148
|
+
* Only the keys present in `fields` change; `source` is recorded as the
|
|
1149
|
+
* writer ("openagents:<agent>"). */
|
|
1150
|
+
async putBrief(workspaceId, token, channelName, fields = {}, { source } = {}) {
|
|
1151
|
+
const body = { network: workspaceId, source: source || 'openagents:unknown', ...fields };
|
|
1152
|
+
const data = await this._put(
|
|
1153
|
+
`/v1/channels/${encodeURIComponent(channelName)}/brief`,
|
|
1154
|
+
body,
|
|
1155
|
+
this._wsHeaders(token),
|
|
1156
|
+
);
|
|
1157
|
+
return data.data || data;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
960
1160
|
_wsHeaders(token) {
|
|
961
1161
|
return {
|
|
962
1162
|
'Content-Type': 'application/json',
|
|
@@ -965,6 +1165,7 @@ class WorkspaceClient {
|
|
|
965
1165
|
}
|
|
966
1166
|
|
|
967
1167
|
_get(urlPath, headers = {}, timeout = 15000) {
|
|
1168
|
+
headers = this._withAgent(headers);
|
|
968
1169
|
const fullUrl = this.endpoint + urlPath;
|
|
969
1170
|
|
|
970
1171
|
return new Promise((resolve, reject) => {
|
|
@@ -985,6 +1186,7 @@ class WorkspaceClient {
|
|
|
985
1186
|
signal: AbortSignal.timeout(timeout),
|
|
986
1187
|
}, (res) => {
|
|
987
1188
|
let data = '';
|
|
1189
|
+
rejectOnTruncatedResponse(res, reject);
|
|
988
1190
|
res.on('data', (chunk) => { data += chunk; });
|
|
989
1191
|
res.on('end', () => {
|
|
990
1192
|
try {
|
|
@@ -1014,6 +1216,7 @@ class WorkspaceClient {
|
|
|
1014
1216
|
}
|
|
1015
1217
|
|
|
1016
1218
|
_getRaw(urlPath, headers = {}, timeout = 15000) {
|
|
1219
|
+
headers = this._withAgent(headers);
|
|
1017
1220
|
const fullUrl = this.endpoint + urlPath;
|
|
1018
1221
|
|
|
1019
1222
|
return new Promise((resolve, reject) => {
|
|
@@ -1027,6 +1230,7 @@ class WorkspaceClient {
|
|
|
1027
1230
|
signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
|
|
1028
1231
|
}, (res) => {
|
|
1029
1232
|
const chunks = [];
|
|
1233
|
+
rejectOnTruncatedResponse(res, reject);
|
|
1030
1234
|
res.on('data', (chunk) => { chunks.push(chunk); });
|
|
1031
1235
|
res.on('end', () => {
|
|
1032
1236
|
const buf = Buffer.concat(chunks);
|
|
@@ -1045,6 +1249,7 @@ class WorkspaceClient {
|
|
|
1045
1249
|
}
|
|
1046
1250
|
|
|
1047
1251
|
_post(urlPath, body, headers = {}, timeout = 30000) {
|
|
1252
|
+
headers = this._withAgent(headers);
|
|
1048
1253
|
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
1049
1254
|
const jsonBody = JSON.stringify(body);
|
|
1050
1255
|
const fullUrl = this.endpoint + urlPath;
|
|
@@ -1060,6 +1265,7 @@ class WorkspaceClient {
|
|
|
1060
1265
|
signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
|
|
1061
1266
|
}, (res) => {
|
|
1062
1267
|
let data = '';
|
|
1268
|
+
rejectOnTruncatedResponse(res, reject);
|
|
1063
1269
|
res.on('data', (chunk) => { data += chunk; });
|
|
1064
1270
|
res.on('end', () => {
|
|
1065
1271
|
try {
|
|
@@ -1096,6 +1302,7 @@ class WorkspaceClient {
|
|
|
1096
1302
|
}
|
|
1097
1303
|
|
|
1098
1304
|
_put(urlPath, body, headers = {}, timeout = 30000) {
|
|
1305
|
+
headers = this._withAgent(headers);
|
|
1099
1306
|
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
1100
1307
|
const jsonBody = JSON.stringify(body);
|
|
1101
1308
|
const fullUrl = this.endpoint + urlPath;
|
|
@@ -1111,6 +1318,7 @@ class WorkspaceClient {
|
|
|
1111
1318
|
signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
|
|
1112
1319
|
}, (res) => {
|
|
1113
1320
|
let data = '';
|
|
1321
|
+
rejectOnTruncatedResponse(res, reject);
|
|
1114
1322
|
res.on('data', (chunk) => { data += chunk; });
|
|
1115
1323
|
res.on('end', () => {
|
|
1116
1324
|
try {
|
|
@@ -1139,6 +1347,7 @@ class WorkspaceClient {
|
|
|
1139
1347
|
}
|
|
1140
1348
|
|
|
1141
1349
|
_patch(urlPath, body, headers = {}, timeout = 15000) {
|
|
1350
|
+
headers = this._withAgent(headers);
|
|
1142
1351
|
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
1143
1352
|
const jsonBody = JSON.stringify(body);
|
|
1144
1353
|
const fullUrl = this.endpoint + urlPath;
|
|
@@ -1154,6 +1363,7 @@ class WorkspaceClient {
|
|
|
1154
1363
|
signal: AbortSignal.timeout(timeout), // hard deadline (covers DNS/connect); see _get
|
|
1155
1364
|
}, (res) => {
|
|
1156
1365
|
let data = '';
|
|
1366
|
+
rejectOnTruncatedResponse(res, reject);
|
|
1157
1367
|
res.on('data', (chunk) => { data += chunk; });
|
|
1158
1368
|
res.on('end', () => {
|
|
1159
1369
|
try {
|
|
@@ -1177,6 +1387,7 @@ class WorkspaceClient {
|
|
|
1177
1387
|
}
|
|
1178
1388
|
|
|
1179
1389
|
_delete(urlPath, headers = {}, network) {
|
|
1390
|
+
headers = this._withAgent(headers);
|
|
1180
1391
|
const fullUrl = this.endpoint + urlPath + (network ? `?network=${network}` : '');
|
|
1181
1392
|
|
|
1182
1393
|
return new Promise((resolve, reject) => {
|
|
@@ -1190,6 +1401,7 @@ class WorkspaceClient {
|
|
|
1190
1401
|
signal: AbortSignal.timeout(15000), // hard deadline (covers DNS/connect); see _get
|
|
1191
1402
|
}, (res) => {
|
|
1192
1403
|
let data = '';
|
|
1404
|
+
rejectOnTruncatedResponse(res, reject);
|
|
1193
1405
|
res.on('data', (chunk) => { data += chunk; });
|
|
1194
1406
|
res.on('end', () => {
|
|
1195
1407
|
try {
|