@openagents-org/agent-launcher 0.1.17 → 0.2.1
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/package.json +1 -1
- package/registry.json +50 -0
- package/src/adapters/claude.js +98 -9
- package/src/adapters/cursor.js +22 -0
- package/src/adapters/index.js +10 -1
- package/src/adapters/llm-direct.js +180 -0
- package/src/adapters/nanoclaw.js +22 -0
- package/src/adapters/openclaw.js +0 -9
- package/src/adapters/opencode.js +380 -0
- package/src/adapters/workspace-prompt.js +37 -0
- package/src/daemon.js +16 -2
- package/src/identity.js +113 -0
- package/src/index.js +5 -0
- package/src/tui.js +140 -3
- package/src/workspace-client.js +311 -21
package/src/tui.js
CHANGED
|
@@ -85,6 +85,39 @@ function loadAgentRows(connector) {
|
|
|
85
85
|
workspace = agent.network;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
+
// Check if agent type needs configuration (API key etc.)
|
|
89
|
+
let notReadyMsg = '';
|
|
90
|
+
try {
|
|
91
|
+
const agentType = agent.type || 'openclaw';
|
|
92
|
+
const entry = connector.registry.getEntry(agentType);
|
|
93
|
+
if (entry && entry.check_ready) {
|
|
94
|
+
const cr = entry.check_ready;
|
|
95
|
+
let isReady = false;
|
|
96
|
+
// Check saved env
|
|
97
|
+
if (cr.saved_env_key) {
|
|
98
|
+
const saved = connector.env.load(agentType);
|
|
99
|
+
if (saved[cr.saved_env_key]) isReady = true;
|
|
100
|
+
}
|
|
101
|
+
// Check process env vars
|
|
102
|
+
if (!isReady && cr.env_vars) {
|
|
103
|
+
for (const v of cr.env_vars) {
|
|
104
|
+
if (process.env[v]) { isReady = true; break; }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check creds file (for claude)
|
|
108
|
+
if (!isReady && cr.creds_file) {
|
|
109
|
+
const credsPath = cr.creds_file.replace('~', process.env.HOME || process.env.USERPROFILE || '');
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(credsPath)) {
|
|
112
|
+
const creds = JSON.parse(fs.readFileSync(credsPath, 'utf-8'));
|
|
113
|
+
if (cr.creds_key && creds[cr.creds_key]) isReady = true;
|
|
114
|
+
}
|
|
115
|
+
} catch {}
|
|
116
|
+
}
|
|
117
|
+
if (!isReady) notReadyMsg = cr.not_ready_message || 'Not configured';
|
|
118
|
+
}
|
|
119
|
+
} catch {}
|
|
120
|
+
|
|
88
121
|
return {
|
|
89
122
|
name: agent.name,
|
|
90
123
|
type: agent.type || 'openclaw',
|
|
@@ -93,6 +126,7 @@ function loadAgentRows(connector) {
|
|
|
93
126
|
path: agent.path || '',
|
|
94
127
|
network: agent.network || '',
|
|
95
128
|
lastError: info.last_error || '',
|
|
129
|
+
notReadyMsg,
|
|
96
130
|
configured: true,
|
|
97
131
|
};
|
|
98
132
|
});
|
|
@@ -308,7 +342,8 @@ function createTUI() {
|
|
|
308
342
|
const state = stateMarkup(r.state, !!r.workspace);
|
|
309
343
|
const ws = r.workspace || '{gray-fg}-{/gray-fg}';
|
|
310
344
|
const pathInfo = r.path ? `{gray-fg} ${r.path}{/gray-fg}` : '';
|
|
311
|
-
|
|
345
|
+
const warning = r.notReadyMsg ? ` {yellow-fg}⚠ ${r.notReadyMsg}{/yellow-fg}` : '';
|
|
346
|
+
return ` ${r.name.padEnd(22)} ${r.type.padEnd(14)} ${state.padEnd(30)} ${ws}${pathInfo}${warning}`;
|
|
312
347
|
});
|
|
313
348
|
agentList.setItems(items);
|
|
314
349
|
}
|
|
@@ -328,7 +363,20 @@ function createTUI() {
|
|
|
328
363
|
const dot = pid ? `{green-fg}\u25CF{/green-fg}` : `{gray-fg}\u25CB{/gray-fg}`;
|
|
329
364
|
const state = pid ? 'Daemon running' : 'Daemon idle';
|
|
330
365
|
const count = agentRows.length;
|
|
331
|
-
|
|
366
|
+
|
|
367
|
+
// Show installed runtimes
|
|
368
|
+
let installed = [];
|
|
369
|
+
try {
|
|
370
|
+
const catalog = loadCatalog(connector);
|
|
371
|
+
installed = catalog.filter(e => e.installed).map(e => e.name);
|
|
372
|
+
} catch {}
|
|
373
|
+
const installedStr = installed.length
|
|
374
|
+
? ` {gray-fg}|{/gray-fg} {green-fg}${installed.join(', ')}{/green-fg} installed`
|
|
375
|
+
: '';
|
|
376
|
+
|
|
377
|
+
header.setContent(
|
|
378
|
+
` ${dot} ${state} {gray-fg}|{/gray-fg} ${count} agent${count !== 1 ? 's' : ''} configured${installedStr}`
|
|
379
|
+
);
|
|
332
380
|
}
|
|
333
381
|
|
|
334
382
|
// Update footer when selection changes
|
|
@@ -542,6 +590,8 @@ function createTUI() {
|
|
|
542
590
|
}).then(() => {
|
|
543
591
|
installLog.log('');
|
|
544
592
|
installLog.log(`{green-fg}\u2713 ${entry.name} installed successfully!{/green-fg}`);
|
|
593
|
+
installLog.log('');
|
|
594
|
+
installLog.log(`{cyan-fg}Press c to create a ${entry.name} agent, or Esc to go back.{/cyan-fg}`);
|
|
545
595
|
logPanel.setLabel(` {bold}{green-fg}Install Complete{/green-fg}{/bold} `);
|
|
546
596
|
log(`{green-fg}\u2713{/green-fg} ${entry.name} installed`);
|
|
547
597
|
const idx = catalog.findIndex(c => c.name === entry.name);
|
|
@@ -550,6 +600,36 @@ function createTUI() {
|
|
|
550
600
|
onDone();
|
|
551
601
|
list.focus();
|
|
552
602
|
screen.render();
|
|
603
|
+
|
|
604
|
+
// Listen for 'c' to create agent from just-installed type
|
|
605
|
+
const onCreateKey = (ch) => {
|
|
606
|
+
if (ch === 'c') {
|
|
607
|
+
screen.unkey(['c', 'escape'], onCreateKey);
|
|
608
|
+
// Go back to main and start agent creation flow
|
|
609
|
+
list.emit('keypress', null, { name: 'escape' });
|
|
610
|
+
setTimeout(() => {
|
|
611
|
+
showStartAgentScreen(entry.name, (result) => {
|
|
612
|
+
try {
|
|
613
|
+
connector.addAgent({ name: result.name, type: result.type, path: result.path });
|
|
614
|
+
log(`{green-fg}\u2713{/green-fg} Created agent {cyan-fg}${result.name}{/cyan-fg} (${result.type})`);
|
|
615
|
+
const pid = connector.getDaemonPid();
|
|
616
|
+
if (!pid) {
|
|
617
|
+
connector.startDaemon();
|
|
618
|
+
log('{green-fg}\u2713{/green-fg} Daemon starting...');
|
|
619
|
+
} else {
|
|
620
|
+
signalDaemonReload();
|
|
621
|
+
}
|
|
622
|
+
} catch (e) {
|
|
623
|
+
log(`{red-fg}\u2717{/red-fg} ${e.message}`);
|
|
624
|
+
}
|
|
625
|
+
setTimeout(refreshAgentTable, 3000);
|
|
626
|
+
});
|
|
627
|
+
}, 200);
|
|
628
|
+
} else {
|
|
629
|
+
screen.unkey(['c', 'escape'], onCreateKey);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
screen.key(['c', 'escape'], onCreateKey);
|
|
553
633
|
}).catch((e) => {
|
|
554
634
|
installLog.log('');
|
|
555
635
|
installLog.log(`{red-fg}\u2717 Failed: ${e.message}{/red-fg}`);
|
|
@@ -652,6 +732,7 @@ function createTUI() {
|
|
|
652
732
|
const pathInput = blessed.textbox({
|
|
653
733
|
parent: dialog, top: 6, left: 2, width: 50, height: 3,
|
|
654
734
|
border: { type: 'line' }, inputOnFocus: true,
|
|
735
|
+
value: defaultPath,
|
|
655
736
|
style: { fg: 'white', bg: COLORS.surface, focus: { border: { fg: COLORS.accent } }, border: { fg: 'grey' } },
|
|
656
737
|
});
|
|
657
738
|
|
|
@@ -667,6 +748,18 @@ function createTUI() {
|
|
|
667
748
|
nameInput.focus();
|
|
668
749
|
screen.render();
|
|
669
750
|
|
|
751
|
+
// Override _listener on textboxes to intercept Tab before it's inserted
|
|
752
|
+
const origNameListener = nameInput._listener.bind(nameInput);
|
|
753
|
+
nameInput._listener = function(ch, key) {
|
|
754
|
+
if (key.name === 'tab') { nameInput._done(null, nameInput.value); pathInput.focus(); return; }
|
|
755
|
+
return origNameListener(ch, key);
|
|
756
|
+
};
|
|
757
|
+
const origPathListener = pathInput._listener.bind(pathInput);
|
|
758
|
+
pathInput._listener = function(ch, key) {
|
|
759
|
+
if (key.name === 'tab') { pathInput._done(null, pathInput.value); nameInput.focus(); return; }
|
|
760
|
+
return origPathListener(ch, key);
|
|
761
|
+
};
|
|
762
|
+
|
|
670
763
|
const close = () => {
|
|
671
764
|
screen.remove(dialog);
|
|
672
765
|
dialog.destroy();
|
|
@@ -768,7 +861,7 @@ function createTUI() {
|
|
|
768
861
|
parent: box, bottom: 0, left: 0, width: '100%', height: 1,
|
|
769
862
|
tags: true,
|
|
770
863
|
style: { bg: COLORS.footerBg, fg: COLORS.footerFg },
|
|
771
|
-
content: ' {cyan-fg}
|
|
864
|
+
content: ' {cyan-fg}Tab{/cyan-fg} Next {cyan-fg}Ctrl+U{/cyan-fg} Clear {cyan-fg}Ctrl+S{/cyan-fg} Save {cyan-fg}Ctrl+T{/cyan-fg} Test {cyan-fg}Esc{/cyan-fg} Back',
|
|
772
865
|
});
|
|
773
866
|
|
|
774
867
|
screen.append(box);
|
|
@@ -786,6 +879,35 @@ function createTUI() {
|
|
|
786
879
|
});
|
|
787
880
|
}
|
|
788
881
|
|
|
882
|
+
// Override _listener on textboxes to intercept Tab and Escape
|
|
883
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
884
|
+
const orig = inputs[i]._listener.bind(inputs[i]);
|
|
885
|
+
const idx = i;
|
|
886
|
+
inputs[i]._listener = function(ch, key) {
|
|
887
|
+
if (key.name === 'tab' && inputs.length > 1) {
|
|
888
|
+
inputs[idx]._done(null, inputs[idx].value);
|
|
889
|
+
inputs[(idx + 1) % inputs.length].focus();
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
if (key.name === 'escape') {
|
|
893
|
+
inputs[idx]._done(null, inputs[idx].value);
|
|
894
|
+
closeConfig();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
// Ctrl+U to clear field
|
|
898
|
+
if (key.ctrl && key.name === 'u') {
|
|
899
|
+
inputs[idx].value = '';
|
|
900
|
+
inputs[idx].setValue('');
|
|
901
|
+
screen.render();
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
// Ctrl+S to save, Ctrl+T to test
|
|
905
|
+
if (key.ctrl && key.name === 's') { inputs[idx]._done(null, inputs[idx].value); doSave(); return; }
|
|
906
|
+
if (key.ctrl && key.name === 't') { inputs[idx]._done(null, inputs[idx].value); doTest(); return; }
|
|
907
|
+
return orig(ch, key);
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
789
911
|
function gatherEnv() {
|
|
790
912
|
const env = {};
|
|
791
913
|
for (const input of inputs) {
|
|
@@ -1341,6 +1463,21 @@ function createTUI() {
|
|
|
1341
1463
|
agentList.focus();
|
|
1342
1464
|
refreshAgentTable();
|
|
1343
1465
|
log('Welcome to {bold}OpenAgents{/bold}. Press {cyan-fg}i{/cyan-fg} to install agents, {cyan-fg}n{/cyan-fg} to create one.');
|
|
1466
|
+
|
|
1467
|
+
// Show installed runtimes that don't have any agent instances yet
|
|
1468
|
+
try {
|
|
1469
|
+
const catalog = loadCatalog(connector);
|
|
1470
|
+
const installed = catalog.filter(e => e.installed).map(e => e.name);
|
|
1471
|
+
const configuredTypes = new Set(agentRows.map(r => r.type));
|
|
1472
|
+
const unused = installed.filter(t => !configuredTypes.has(t));
|
|
1473
|
+
if (unused.length > 0) {
|
|
1474
|
+
log(`{green-fg}\u2713{/green-fg} Installed: {bold}${unused.join(', ')}{/bold} — press {cyan-fg}n{/cyan-fg} to create an agent`);
|
|
1475
|
+
}
|
|
1476
|
+
if (installed.length === 0) {
|
|
1477
|
+
log('{yellow-fg}!{/yellow-fg} No agent runtimes installed. Press {cyan-fg}i{/cyan-fg} to install one.');
|
|
1478
|
+
}
|
|
1479
|
+
} catch {}
|
|
1480
|
+
|
|
1344
1481
|
setInterval(refreshAgentTable, 5000);
|
|
1345
1482
|
screen.render();
|
|
1346
1483
|
}
|
package/src/workspace-client.js
CHANGED
|
@@ -106,7 +106,7 @@ class WorkspaceClient {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
|
-
* Post a
|
|
109
|
+
* Post a raw event via POST /v1/events.
|
|
110
110
|
*/
|
|
111
111
|
async sendEvent(workspaceId, event, token) {
|
|
112
112
|
event.network = workspaceId;
|
|
@@ -118,20 +118,42 @@ class WorkspaceClient {
|
|
|
118
118
|
* Send a chat message to a workspace channel.
|
|
119
119
|
*/
|
|
120
120
|
async sendMessage(workspaceId, channelName, token, content, {
|
|
121
|
-
senderType = 'agent', senderName, messageType = 'chat', metadata,
|
|
121
|
+
senderType = 'agent', senderName, messageType = 'chat', metadata, attachments,
|
|
122
122
|
} = {}) {
|
|
123
123
|
const sourcePrefix = senderType === 'agent' ? 'openagents' : 'human';
|
|
124
124
|
const source = senderName ? `${sourcePrefix}:${senderName}` : `${sourcePrefix}:unknown`;
|
|
125
125
|
|
|
126
|
+
const payload = { content, message_type: messageType };
|
|
127
|
+
if (attachments && attachments.length) payload.attachments = attachments;
|
|
128
|
+
|
|
126
129
|
return this.sendEvent(workspaceId, {
|
|
127
130
|
type: 'workspace.message.posted',
|
|
128
131
|
source,
|
|
129
132
|
target: `channel/${channelName}`,
|
|
130
|
-
payload
|
|
133
|
+
payload,
|
|
131
134
|
metadata: metadata || {},
|
|
132
135
|
}, token);
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Poll messages in a channel via GET /v1/events.
|
|
140
|
+
* @returns {Array} message-compatible objects
|
|
141
|
+
*/
|
|
142
|
+
async pollMessages(workspaceId, channelName, token, { after, limit = 50 } = {}) {
|
|
143
|
+
const params = new URLSearchParams({
|
|
144
|
+
network: workspaceId,
|
|
145
|
+
channel: channelName,
|
|
146
|
+
type: 'workspace.message',
|
|
147
|
+
limit: String(limit),
|
|
148
|
+
});
|
|
149
|
+
if (after) params.set('after', after);
|
|
150
|
+
|
|
151
|
+
const data = await this._get(`/v1/events?${params}`, this._wsHeaders(token));
|
|
152
|
+
const result = data.data || data;
|
|
153
|
+
const events = (result && result.events) || [];
|
|
154
|
+
return events.map((e) => this._eventToMessage(e));
|
|
155
|
+
}
|
|
156
|
+
|
|
135
157
|
/**
|
|
136
158
|
* Poll for pending messages targeted at an agent via GET /v1/events.
|
|
137
159
|
* Returns { messages, cursor } where cursor is the last event ID.
|
|
@@ -180,27 +202,41 @@ class WorkspaceClient {
|
|
|
180
202
|
}
|
|
181
203
|
|
|
182
204
|
/**
|
|
183
|
-
* Get session/channel info via GET /v1/
|
|
205
|
+
* Get session/channel info via GET /v1/workspaces/{id}/channels/{name}.
|
|
184
206
|
*/
|
|
185
207
|
async getSession(workspaceId, channelName, token) {
|
|
186
208
|
try {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
const data = await this._get(
|
|
210
|
+
`/v1/workspaces/${workspaceId}/channels/${channelName}`,
|
|
211
|
+
this._wsHeaders(token),
|
|
212
|
+
);
|
|
213
|
+
const result = data.data || data;
|
|
214
|
+
return {
|
|
215
|
+
sessionId: result.name || channelName,
|
|
216
|
+
title: result.title || channelName,
|
|
217
|
+
titleManuallySet: result.titleManuallySet || false,
|
|
218
|
+
resumeFrom: result.resumeFrom || null,
|
|
219
|
+
status: result.status || 'active',
|
|
220
|
+
};
|
|
190
221
|
} catch {
|
|
191
|
-
return {};
|
|
222
|
+
return { sessionId: channelName, title: channelName, status: 'active' };
|
|
192
223
|
}
|
|
193
224
|
}
|
|
194
225
|
|
|
195
226
|
/**
|
|
196
|
-
* Update session/channel info via
|
|
227
|
+
* Update session/channel info via PATCH /v1/workspaces/{id}/channels/{name}.
|
|
197
228
|
*/
|
|
198
|
-
async updateSession(workspaceId, channelName, token, { title, autoTitle } = {}) {
|
|
199
|
-
const body = {
|
|
229
|
+
async updateSession(workspaceId, channelName, token, { title, status, autoTitle } = {}) {
|
|
230
|
+
const body = {};
|
|
200
231
|
if (title !== undefined) body.title = title;
|
|
232
|
+
if (status !== undefined) body.status = status;
|
|
201
233
|
if (autoTitle !== undefined) body.auto_title = autoTitle;
|
|
202
234
|
try {
|
|
203
|
-
await this.
|
|
235
|
+
await this._patch(
|
|
236
|
+
`/v1/workspaces/${workspaceId}/channels/${channelName}`,
|
|
237
|
+
body,
|
|
238
|
+
this._wsHeaders(token),
|
|
239
|
+
);
|
|
204
240
|
} catch {}
|
|
205
241
|
}
|
|
206
242
|
|
|
@@ -211,7 +247,7 @@ class WorkspaceClient {
|
|
|
211
247
|
try {
|
|
212
248
|
const params = new URLSearchParams({
|
|
213
249
|
network: workspaceId,
|
|
214
|
-
type: 'workspace.control',
|
|
250
|
+
type: 'workspace.agent.control',
|
|
215
251
|
limit: '10',
|
|
216
252
|
});
|
|
217
253
|
if (after) params.set('after', after);
|
|
@@ -219,14 +255,165 @@ class WorkspaceClient {
|
|
|
219
255
|
const result = data.data || data;
|
|
220
256
|
const events = (result && result.events) || [];
|
|
221
257
|
return events.filter((e) => {
|
|
222
|
-
const
|
|
223
|
-
return
|
|
258
|
+
const target = e.target || '';
|
|
259
|
+
return target === `openagents:${agentName}`;
|
|
224
260
|
});
|
|
225
261
|
} catch {
|
|
226
262
|
return [];
|
|
227
263
|
}
|
|
228
264
|
}
|
|
229
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Get workspace agents via GET /v1/discover.
|
|
268
|
+
* @returns {Array<{ agentName, role, status }>}
|
|
269
|
+
*/
|
|
270
|
+
async getAgents(workspaceId, token) {
|
|
271
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
272
|
+
const data = await this._get(`/v1/discover?${params}`, this._wsHeaders(token));
|
|
273
|
+
const result = data.data || data;
|
|
274
|
+
const agents = (result && result.agents) || [];
|
|
275
|
+
return agents.map((a) => ({
|
|
276
|
+
agentName: (a.address || '').replace('openagents:', ''),
|
|
277
|
+
role: a.role || 'member',
|
|
278
|
+
status: a.status || 'offline',
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── File methods ──
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Upload a file via POST /v1/files/base64.
|
|
286
|
+
*/
|
|
287
|
+
async uploadFile(workspaceId, token, filename, contentBase64, {
|
|
288
|
+
contentType = 'application/octet-stream', source = 'human:user', channelName,
|
|
289
|
+
} = {}) {
|
|
290
|
+
const body = {
|
|
291
|
+
filename,
|
|
292
|
+
content_base64: contentBase64,
|
|
293
|
+
content_type: contentType,
|
|
294
|
+
network: workspaceId,
|
|
295
|
+
source,
|
|
296
|
+
};
|
|
297
|
+
if (channelName) body.channel_name = channelName;
|
|
298
|
+
|
|
299
|
+
const data = await this._post('/v1/files/base64', body, this._wsHeaders(token), 60000);
|
|
300
|
+
return data.data || data;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* List files via GET /v1/files.
|
|
305
|
+
*/
|
|
306
|
+
async listFiles(workspaceId, token, { limit = 50, offset = 0 } = {}) {
|
|
307
|
+
const params = new URLSearchParams({
|
|
308
|
+
network: workspaceId,
|
|
309
|
+
limit: String(limit),
|
|
310
|
+
offset: String(offset),
|
|
311
|
+
});
|
|
312
|
+
const data = await this._get(`/v1/files?${params}`, this._wsHeaders(token));
|
|
313
|
+
return data.data || data;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get file metadata via GET /v1/files/{fileId}/info.
|
|
318
|
+
*/
|
|
319
|
+
async getFileInfo(token, fileId) {
|
|
320
|
+
try {
|
|
321
|
+
const data = await this._get(`/v1/files/${fileId}/info`, this._wsHeaders(token));
|
|
322
|
+
return data.data || data;
|
|
323
|
+
} catch {
|
|
324
|
+
return { id: fileId, filename: fileId, content_type: 'application/octet-stream' };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Download a file via GET /v1/files/{fileId}.
|
|
330
|
+
* @returns {Buffer}
|
|
331
|
+
*/
|
|
332
|
+
async readFile(workspaceId, token, fileId) {
|
|
333
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
334
|
+
return this._getRaw(`/v1/files/${fileId}?${params}`, this._wsHeaders(token), 60000);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Delete a file via DELETE /v1/files/{fileId}.
|
|
339
|
+
*/
|
|
340
|
+
async deleteFile(workspaceId, token, fileId) {
|
|
341
|
+
const data = await this._delete(`/v1/files/${fileId}`, this._wsHeaders(token), workspaceId);
|
|
342
|
+
return data.data || data;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Browser methods ──
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Open a new browser tab via POST /v1/browser/tabs.
|
|
349
|
+
*/
|
|
350
|
+
async browserOpenTab(workspaceId, token, { url = 'about:blank', source = 'human:user' } = {}) {
|
|
351
|
+
const data = await this._post('/v1/browser/tabs', {
|
|
352
|
+
url, network: workspaceId, source,
|
|
353
|
+
}, this._wsHeaders(token));
|
|
354
|
+
return data.data || data;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* List browser tabs via GET /v1/browser/tabs.
|
|
359
|
+
*/
|
|
360
|
+
async browserListTabs(workspaceId, token) {
|
|
361
|
+
const params = new URLSearchParams({ network: workspaceId });
|
|
362
|
+
const data = await this._get(`/v1/browser/tabs?${params}`, this._wsHeaders(token));
|
|
363
|
+
return data.data || data;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Navigate a browser tab via POST /v1/browser/tabs/{tabId}/navigate.
|
|
368
|
+
*/
|
|
369
|
+
async browserNavigate(workspaceId, token, tabId, url) {
|
|
370
|
+
const data = await this._post(`/v1/browser/tabs/${tabId}/navigate`, { url }, this._wsHeaders(token));
|
|
371
|
+
return data.data || data;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Click an element via POST /v1/browser/tabs/{tabId}/click.
|
|
376
|
+
*/
|
|
377
|
+
async browserClick(workspaceId, token, tabId, selector) {
|
|
378
|
+
const data = await this._post(`/v1/browser/tabs/${tabId}/click`, { selector }, this._wsHeaders(token));
|
|
379
|
+
return data.data || data;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Type text via POST /v1/browser/tabs/{tabId}/type.
|
|
384
|
+
*/
|
|
385
|
+
async browserType(workspaceId, token, tabId, selector, text) {
|
|
386
|
+
const data = await this._post(`/v1/browser/tabs/${tabId}/type`, { selector, text }, this._wsHeaders(token));
|
|
387
|
+
return data.data || data;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Get screenshot via GET /v1/browser/tabs/{tabId}/screenshot.
|
|
392
|
+
* @returns {Buffer}
|
|
393
|
+
*/
|
|
394
|
+
async browserScreenshot(workspaceId, token, tabId) {
|
|
395
|
+
return this._getRaw(`/v1/browser/tabs/${tabId}/screenshot`, this._wsHeaders(token), 30000);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get accessibility snapshot via GET /v1/browser/tabs/{tabId}/snapshot.
|
|
400
|
+
* @returns {string}
|
|
401
|
+
*/
|
|
402
|
+
async browserSnapshot(workspaceId, token, tabId) {
|
|
403
|
+
const buf = await this._getRaw(`/v1/browser/tabs/${tabId}/snapshot`, this._wsHeaders(token));
|
|
404
|
+
return buf.toString('utf-8');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Close a browser tab via DELETE /v1/browser/tabs/{tabId}.
|
|
409
|
+
*/
|
|
410
|
+
async browserCloseTab(workspaceId, token, tabId) {
|
|
411
|
+
const data = await this._delete(`/v1/browser/tabs/${tabId}`, this._wsHeaders(token));
|
|
412
|
+
return data.data || data;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── Internal helpers ──
|
|
416
|
+
|
|
230
417
|
/**
|
|
231
418
|
* Convert an ONM event to a message-compatible object.
|
|
232
419
|
*/
|
|
@@ -244,17 +431,19 @@ class WorkspaceClient {
|
|
|
244
431
|
senderType: isHuman ? 'human' : 'agent',
|
|
245
432
|
senderName,
|
|
246
433
|
content: (payload.content || ''),
|
|
434
|
+
mentions: payload.mentions || [],
|
|
247
435
|
messageType: payload.message_type || 'chat',
|
|
248
436
|
metadata: event.metadata || {},
|
|
249
437
|
};
|
|
250
438
|
if (ts) {
|
|
251
439
|
msg.createdAt = new Date(ts).toISOString();
|
|
252
440
|
}
|
|
441
|
+
if (payload.attachments) {
|
|
442
|
+
msg.attachments = payload.attachments;
|
|
443
|
+
}
|
|
253
444
|
return msg;
|
|
254
445
|
}
|
|
255
446
|
|
|
256
|
-
// -- Internal --
|
|
257
|
-
|
|
258
447
|
_wsHeaders(token) {
|
|
259
448
|
return {
|
|
260
449
|
'Content-Type': 'application/json',
|
|
@@ -262,7 +451,7 @@ class WorkspaceClient {
|
|
|
262
451
|
};
|
|
263
452
|
}
|
|
264
453
|
|
|
265
|
-
_get(urlPath, headers = {}) {
|
|
454
|
+
_get(urlPath, headers = {}, timeout = 15000) {
|
|
266
455
|
const fullUrl = this.endpoint + urlPath;
|
|
267
456
|
|
|
268
457
|
return new Promise((resolve, reject) => {
|
|
@@ -272,7 +461,7 @@ class WorkspaceClient {
|
|
|
272
461
|
const req = transport.request(fullUrl, {
|
|
273
462
|
method: 'GET',
|
|
274
463
|
headers,
|
|
275
|
-
timeout
|
|
464
|
+
timeout,
|
|
276
465
|
}, (res) => {
|
|
277
466
|
let data = '';
|
|
278
467
|
res.on('data', (chunk) => { data += chunk; });
|
|
@@ -296,7 +485,37 @@ class WorkspaceClient {
|
|
|
296
485
|
});
|
|
297
486
|
}
|
|
298
487
|
|
|
299
|
-
|
|
488
|
+
_getRaw(urlPath, headers = {}, timeout = 15000) {
|
|
489
|
+
const fullUrl = this.endpoint + urlPath;
|
|
490
|
+
|
|
491
|
+
return new Promise((resolve, reject) => {
|
|
492
|
+
const parsedUrl = new URL(fullUrl);
|
|
493
|
+
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
494
|
+
|
|
495
|
+
const req = transport.request(fullUrl, {
|
|
496
|
+
method: 'GET',
|
|
497
|
+
headers,
|
|
498
|
+
timeout,
|
|
499
|
+
}, (res) => {
|
|
500
|
+
const chunks = [];
|
|
501
|
+
res.on('data', (chunk) => { chunks.push(chunk); });
|
|
502
|
+
res.on('end', () => {
|
|
503
|
+
const buf = Buffer.concat(chunks);
|
|
504
|
+
if (res.statusCode >= 400) {
|
|
505
|
+
reject(new Error(`HTTP ${res.statusCode}: ${buf.toString('utf-8').slice(0, 200)}`));
|
|
506
|
+
} else {
|
|
507
|
+
resolve(buf);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
req.on('error', reject);
|
|
513
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
514
|
+
req.end();
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
_post(urlPath, body, headers = {}, timeout = 30000) {
|
|
300
519
|
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
301
520
|
const jsonBody = JSON.stringify(body);
|
|
302
521
|
const fullUrl = this.endpoint + urlPath;
|
|
@@ -308,7 +527,7 @@ class WorkspaceClient {
|
|
|
308
527
|
const req = transport.request(fullUrl, {
|
|
309
528
|
method: 'POST',
|
|
310
529
|
headers: { ...headers, 'Content-Length': Buffer.byteLength(jsonBody) },
|
|
311
|
-
timeout
|
|
530
|
+
timeout,
|
|
312
531
|
}, (res) => {
|
|
313
532
|
let data = '';
|
|
314
533
|
res.on('data', (chunk) => { data += chunk; });
|
|
@@ -333,6 +552,77 @@ class WorkspaceClient {
|
|
|
333
552
|
req.end();
|
|
334
553
|
});
|
|
335
554
|
}
|
|
555
|
+
|
|
556
|
+
_patch(urlPath, body, headers = {}, timeout = 15000) {
|
|
557
|
+
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
|
558
|
+
const jsonBody = JSON.stringify(body);
|
|
559
|
+
const fullUrl = this.endpoint + urlPath;
|
|
560
|
+
|
|
561
|
+
return new Promise((resolve, reject) => {
|
|
562
|
+
const parsedUrl = new URL(fullUrl);
|
|
563
|
+
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
564
|
+
|
|
565
|
+
const req = transport.request(fullUrl, {
|
|
566
|
+
method: 'PATCH',
|
|
567
|
+
headers: { ...headers, 'Content-Length': Buffer.byteLength(jsonBody) },
|
|
568
|
+
timeout,
|
|
569
|
+
}, (res) => {
|
|
570
|
+
let data = '';
|
|
571
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
572
|
+
res.on('end', () => {
|
|
573
|
+
try {
|
|
574
|
+
const parsed = JSON.parse(data);
|
|
575
|
+
if (res.statusCode >= 400) {
|
|
576
|
+
reject(new Error(parsed.message || `HTTP ${res.statusCode}`));
|
|
577
|
+
} else {
|
|
578
|
+
resolve(parsed);
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
reject(new Error(`Invalid response: ${data.slice(0, 200)}`));
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
req.on('error', reject);
|
|
587
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
588
|
+
req.write(jsonBody);
|
|
589
|
+
req.end();
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
_delete(urlPath, headers = {}, network) {
|
|
594
|
+
const fullUrl = this.endpoint + urlPath + (network ? `?network=${network}` : '');
|
|
595
|
+
|
|
596
|
+
return new Promise((resolve, reject) => {
|
|
597
|
+
const parsedUrl = new URL(fullUrl);
|
|
598
|
+
const transport = parsedUrl.protocol === 'https:' ? https : http;
|
|
599
|
+
|
|
600
|
+
const req = transport.request(fullUrl, {
|
|
601
|
+
method: 'DELETE',
|
|
602
|
+
headers,
|
|
603
|
+
timeout: 15000,
|
|
604
|
+
}, (res) => {
|
|
605
|
+
let data = '';
|
|
606
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
607
|
+
res.on('end', () => {
|
|
608
|
+
try {
|
|
609
|
+
const parsed = JSON.parse(data);
|
|
610
|
+
if (res.statusCode >= 400) {
|
|
611
|
+
reject(new Error(parsed.message || `HTTP ${res.statusCode}`));
|
|
612
|
+
} else {
|
|
613
|
+
resolve(parsed);
|
|
614
|
+
}
|
|
615
|
+
} catch {
|
|
616
|
+
reject(new Error(`Invalid response: ${data.slice(0, 200)}`));
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
req.on('error', reject);
|
|
622
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
623
|
+
req.end();
|
|
624
|
+
});
|
|
625
|
+
}
|
|
336
626
|
}
|
|
337
627
|
|
|
338
628
|
module.exports = { WorkspaceClient };
|