@openagents-org/agent-launcher 0.2.130 → 0.2.131
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 +7 -5
- package/src/adapters/base.js +117 -7
- package/src/adapters/claude.js +29 -2
- package/src/adapters/cursor.js +27 -8
- package/src/adapters/workspace-prompt.js +79 -24
- package/src/github-client.js +165 -0
- package/src/index.js +2 -1
- package/src/installer.js +151 -19
- package/src/mcp-server.js +34 -13
- package/src/paths.js +31 -3
- package/src/registry.js +4 -4
- package/src/tui.js +1 -16
- package/src/workspace-client.js +32 -6
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -209,14 +209,16 @@
|
|
|
209
209
|
"order": 4,
|
|
210
210
|
"builtin": true,
|
|
211
211
|
"install": {
|
|
212
|
-
"binary": "agent",
|
|
213
|
-
"
|
|
214
|
-
|
|
212
|
+
"binary": "cursor-agent",
|
|
213
|
+
"binary_aliases": [
|
|
214
|
+
"agent"
|
|
215
|
+
],
|
|
216
|
+
"verify": "cursor-agent --version 2>/dev/null | head -1",
|
|
217
|
+
"verify_win": "cursor-agent --version 2>nul",
|
|
215
218
|
"requires": [],
|
|
216
219
|
"macos": "curl https://cursor.com/install -fsSL | bash",
|
|
217
220
|
"linux": "curl https://cursor.com/install -fsSL | bash",
|
|
218
|
-
"windows": "
|
|
219
|
-
"npm": "npm install -g @cursor/cli"
|
|
221
|
+
"windows": "\"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoProfile -ExecutionPolicy Bypass -Command \"Invoke-RestMethod -UseBasicParsing 'https://cursor.com/install?win32=true' | Invoke-Expression\""
|
|
220
222
|
},
|
|
221
223
|
"adapter": {
|
|
222
224
|
"module": "openagents.adapters.cursor",
|
package/src/adapters/base.js
CHANGED
|
@@ -53,6 +53,11 @@ class BaseAdapter {
|
|
|
53
53
|
// Per-channel task tracking for parallel execution
|
|
54
54
|
this._channelBusy = new Set();
|
|
55
55
|
this._channelQueues = {};
|
|
56
|
+
// Cached workspace.browser_enabled. Populated lazily on first read so we
|
|
57
|
+
// don't pay an HTTP roundtrip per message — adapters that toggle the
|
|
58
|
+
// workspace flag must reconnect/restart to pick up the change (matches
|
|
59
|
+
// the Python adapter behavior in workspace_prompt.py).
|
|
60
|
+
this._browserEnabledCache = null;
|
|
56
61
|
// Wall-clock timestamp of adapter init, used by the `status` control
|
|
57
62
|
// action to report uptime back to the channel. Reset on reinstantiation
|
|
58
63
|
// (e.g. after a `restart` IPC bounce) so uptime tracks "time since last
|
|
@@ -210,6 +215,8 @@ class BaseAdapter {
|
|
|
210
215
|
async _onControlAction(action, payload) {
|
|
211
216
|
if (action === 'status') {
|
|
212
217
|
await this._postStatusReport(payload);
|
|
218
|
+
} else if (action === 'routines') {
|
|
219
|
+
await this._postRoutinesReport(payload);
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
222
|
|
|
@@ -262,6 +269,74 @@ class BaseAdapter {
|
|
|
262
269
|
}
|
|
263
270
|
}
|
|
264
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Post a markdown table of the agent's active routines back to the
|
|
274
|
+
* requesting channel. Used by the `/routines` slash command. Each agent
|
|
275
|
+
* reports only routines it owns (created_by === openagents:<agentName>)
|
|
276
|
+
* so the user sees a clear "my routines" view per agent, mirroring how
|
|
277
|
+
* /status reports per-agent uptime.
|
|
278
|
+
*/
|
|
279
|
+
async _postRoutinesReport(payload) {
|
|
280
|
+
const channel = (payload && typeof payload === 'object') ? payload.channel : null;
|
|
281
|
+
if (!channel) return;
|
|
282
|
+
|
|
283
|
+
let routines = [];
|
|
284
|
+
try {
|
|
285
|
+
const data = await this.client.listRoutines(this.workspaceId, channel, this.token);
|
|
286
|
+
// Accept both the canonical `openagents:<name>` source and the bare
|
|
287
|
+
// `<name>` form. Agents that follow the workspace prompt verbatim
|
|
288
|
+
// produce the prefixed form, but some agents send the bare name when
|
|
289
|
+
// they construct the POST body themselves.
|
|
290
|
+
const prefixed = `openagents:${this.agentName}`;
|
|
291
|
+
routines = ((data && data.routines) || []).filter(
|
|
292
|
+
(r) => r.created_by === prefixed || r.created_by === this.agentName,
|
|
293
|
+
);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
this._log(`Routines: failed to list: ${e && e.message ? e.message : e}`);
|
|
296
|
+
try {
|
|
297
|
+
await this.client.sendMessage(
|
|
298
|
+
this.workspaceId, channel, this.token,
|
|
299
|
+
`**Routines for \`${this.agentName}\`**\n\n_Failed to fetch routines._`,
|
|
300
|
+
{ senderType: 'agent', senderName: this.agentName, messageType: 'chat', sessionId: this._sessionId },
|
|
301
|
+
);
|
|
302
|
+
} catch {}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let content;
|
|
307
|
+
if (!routines.length) {
|
|
308
|
+
content = `**Routines for \`${this.agentName}\`**\n\n_No active routines._`;
|
|
309
|
+
} else {
|
|
310
|
+
const rows = routines.map((r) => {
|
|
311
|
+
const schedule = (r.schedule_interval_minutes != null)
|
|
312
|
+
? `every ${r.schedule_interval_minutes} min`
|
|
313
|
+
: `${String(r.schedule_hour ?? 0).padStart(2, '0')}:${String(r.schedule_minute ?? 0).padStart(2, '0')} UTC` +
|
|
314
|
+
(r.schedule_days ? ` (days [${r.schedule_days.join(',')}])` : ' daily');
|
|
315
|
+
const next = r.next_fires_at || '—';
|
|
316
|
+
const name = String(r.name || '').replace(/\|/g, '\\|');
|
|
317
|
+
const id = String(r.id || '').slice(0, 8);
|
|
318
|
+
return `| \`${id}\` | ${name} | ${schedule} | ${next} |`;
|
|
319
|
+
});
|
|
320
|
+
content =
|
|
321
|
+
`**Routines for \`${this.agentName}\`** (${routines.length})\n\n` +
|
|
322
|
+
'| ID | Name | Schedule | Next fires |\n' +
|
|
323
|
+
'|---|---|---|---|\n' +
|
|
324
|
+
rows.join('\n');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
try {
|
|
328
|
+
await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
|
|
329
|
+
senderType: 'agent',
|
|
330
|
+
senderName: this.agentName,
|
|
331
|
+
messageType: 'chat',
|
|
332
|
+
metadata: { agent_mode: this._mode },
|
|
333
|
+
sessionId: this._sessionId,
|
|
334
|
+
});
|
|
335
|
+
} catch (e) {
|
|
336
|
+
this._log(`Routines: failed to post: ${e && e.message ? e.message : e}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
265
340
|
_hasActiveWork() {
|
|
266
341
|
return this._channelBusy.size > 0;
|
|
267
342
|
}
|
|
@@ -306,7 +381,7 @@ class BaseAdapter {
|
|
|
306
381
|
|
|
307
382
|
while (this._running) {
|
|
308
383
|
pollCount++;
|
|
309
|
-
let messages, rawCursor;
|
|
384
|
+
let messages, rawCursor, composingActive = false;
|
|
310
385
|
try {
|
|
311
386
|
const result = await this.client.pollPending(
|
|
312
387
|
this.workspaceId, this.agentName, this.token,
|
|
@@ -314,8 +389,9 @@ class BaseAdapter {
|
|
|
314
389
|
);
|
|
315
390
|
messages = result.messages;
|
|
316
391
|
rawCursor = result.cursor;
|
|
392
|
+
composingActive = !!result.composing;
|
|
317
393
|
if (pollCount <= 3 || pollCount % 20 === 0) {
|
|
318
|
-
this._log(`Poll #${pollCount}: ${messages.length} messages, cursor=${rawCursor || 'none'}`);
|
|
394
|
+
this._log(`Poll #${pollCount}: ${messages.length} messages, cursor=${rawCursor || 'none'}${composingActive ? ' composing' : ''}`);
|
|
319
395
|
}
|
|
320
396
|
} catch (e) {
|
|
321
397
|
this._log(`Poll #${pollCount} failed: ${e.message} \nStack: ${e.stack}`);
|
|
@@ -384,11 +460,26 @@ class BaseAdapter {
|
|
|
384
460
|
}
|
|
385
461
|
}
|
|
386
462
|
|
|
387
|
-
// Adaptive polling
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
|
|
463
|
+
// Adaptive polling with warm plateau:
|
|
464
|
+
// Active (messages incoming): 2s
|
|
465
|
+
// Warm (≤5 min since last msg): 5s
|
|
466
|
+
// Cooldown (5-7 min): 5s → 15s (ramp 1s per idle poll)
|
|
467
|
+
// Cold (>7 min): 15s
|
|
468
|
+
// The warm plateau keeps the agent responsive during typical user
|
|
469
|
+
// think-time between messages without hammering the backend.
|
|
470
|
+
const WARM_INTERVAL = 5000;
|
|
471
|
+
const WARM_POLLS = 60; // 60 × 5s = 5 minutes warm plateau
|
|
472
|
+
let delay;
|
|
473
|
+
if (incoming.length > 0) {
|
|
474
|
+
delay = 2000;
|
|
475
|
+
} else if (composingActive) {
|
|
476
|
+
delay = 2000;
|
|
477
|
+
idleCount = Math.min(idleCount, WARM_POLLS);
|
|
478
|
+
} else if (idleCount <= WARM_POLLS) {
|
|
479
|
+
delay = WARM_INTERVAL;
|
|
480
|
+
} else {
|
|
481
|
+
delay = Math.min(WARM_INTERVAL + (idleCount - WARM_POLLS) * 1000, 15000);
|
|
482
|
+
}
|
|
392
483
|
await this._sleep(delay);
|
|
393
484
|
}
|
|
394
485
|
}
|
|
@@ -636,6 +727,25 @@ class BaseAdapter {
|
|
|
636
727
|
_sleep(ms) {
|
|
637
728
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
638
729
|
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Return whether the workspace has the Browser Fabric viewer toggle on.
|
|
733
|
+
* Cached for the lifetime of the adapter — restart to pick up a flip.
|
|
734
|
+
* Falls back to false on error so the prompt builders don't accidentally
|
|
735
|
+
* inject the strong directive against an older backend that can't route
|
|
736
|
+
* to Browser Fabric.
|
|
737
|
+
*/
|
|
738
|
+
async getBrowserEnabled() {
|
|
739
|
+
if (this._browserEnabledCache === null) {
|
|
740
|
+
try {
|
|
741
|
+
const meta = await this.client.getWorkspaceMetadata(this.workspaceId, this.token);
|
|
742
|
+
this._browserEnabledCache = !!(meta && meta.browserEnabled);
|
|
743
|
+
} catch (e) {
|
|
744
|
+
this._browserEnabledCache = false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return this._browserEnabledCache;
|
|
748
|
+
}
|
|
639
749
|
}
|
|
640
750
|
|
|
641
751
|
// ------------------------------------------------------------------
|
package/src/adapters/claude.js
CHANGED
|
@@ -358,7 +358,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
358
358
|
return null;
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
_buildClaudeCmd(prompt, channelName, { skipResume = false } = {}) {
|
|
361
|
+
_buildClaudeCmd(prompt, channelName, { skipResume = false, browserEnabled = false } = {}) {
|
|
362
362
|
const claudeBin = this._findClaudeBinary();
|
|
363
363
|
if (!claudeBin) {
|
|
364
364
|
throw new Error('claude CLI not found. Install with: curl -fsSL https://claude.ai/install.sh | bash');
|
|
@@ -369,6 +369,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
369
369
|
workspaceId: this.workspaceId,
|
|
370
370
|
channelName,
|
|
371
371
|
mode: this._mode,
|
|
372
|
+
browserEnabled,
|
|
372
373
|
});
|
|
373
374
|
|
|
374
375
|
// In skills mode, replace MCP tool references with curl-based instructions
|
|
@@ -432,6 +433,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
432
433
|
agentName: this.agentName,
|
|
433
434
|
channelName,
|
|
434
435
|
disabledModules: this.disabledModules,
|
|
436
|
+
browserEnabled: this._browserEnabledCache === true,
|
|
435
437
|
});
|
|
436
438
|
fs.writeFileSync(skillFile, skillContent, 'utf-8');
|
|
437
439
|
this._log(`Wrote workspace skill to ${skillFile}`);
|
|
@@ -648,7 +650,11 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
648
650
|
}
|
|
649
651
|
|
|
650
652
|
try {
|
|
651
|
-
const
|
|
653
|
+
const browserEnabled = await this.getBrowserEnabled();
|
|
654
|
+
const built = this._buildClaudeCmd(effectiveContent, msgChannel, {
|
|
655
|
+
skipResume: attempt > 0,
|
|
656
|
+
browserEnabled,
|
|
657
|
+
});
|
|
652
658
|
cmd = built.cmd;
|
|
653
659
|
mcpConfigFile = built.mcpConfigFile;
|
|
654
660
|
} catch (e) {
|
|
@@ -853,6 +859,27 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
853
859
|
}
|
|
854
860
|
}
|
|
855
861
|
|
|
862
|
+
// In plan mode, read the plan file and include it in the response
|
|
863
|
+
if (this._mode === 'plan') {
|
|
864
|
+
try {
|
|
865
|
+
const planDir = path.join(this.workingDir || process.cwd(), '.claude', 'plans');
|
|
866
|
+
if (fs.existsSync(planDir)) {
|
|
867
|
+
const planFiles = fs.readdirSync(planDir)
|
|
868
|
+
.filter((f) => f.endsWith('.md'))
|
|
869
|
+
.map((f) => ({ name: f, mtime: fs.statSync(path.join(planDir, f)).mtimeMs }))
|
|
870
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
871
|
+
if (planFiles.length > 0) {
|
|
872
|
+
const planContent = fs.readFileSync(path.join(planDir, planFiles[0].name), 'utf-8').trim();
|
|
873
|
+
if (planContent) {
|
|
874
|
+
lastResponseText.push('\n\n---\n\n**Plan:**\n\n' + planContent);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
} catch (e) {
|
|
879
|
+
this._log(`Failed to read plan file: ${e.message}`);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
856
883
|
if (lastResponseText.length > 0) {
|
|
857
884
|
const fullResponse = lastResponseText.join('\n').trim();
|
|
858
885
|
// "Prompt is too long" means the resumed session's context
|
package/src/adapters/cursor.js
CHANGED
|
@@ -171,40 +171,59 @@ class CursorAdapter extends BaseAdapter {
|
|
|
171
171
|
_findCursorBinary() {
|
|
172
172
|
const home = os.homedir();
|
|
173
173
|
const ext = IS_WINDOWS ? '.cmd' : '';
|
|
174
|
+
const names = ['cursor-agent', 'agent'];
|
|
174
175
|
|
|
175
176
|
// Tier 0: Isolated runtime prefix
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
for (const name of names) {
|
|
178
|
+
const runtimeCandidate = path.join(home, '.openagents', 'runtimes', 'cursor', 'node_modules', '.bin', `${name}${ext}`);
|
|
179
|
+
if (fs.existsSync(runtimeCandidate)) return runtimeCandidate;
|
|
180
|
+
}
|
|
178
181
|
|
|
179
182
|
// Tier 0b: Legacy portable install
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
for (const name of names) {
|
|
184
|
+
const portableCandidate = path.join(home, '.openagents', 'nodejs', 'node_modules', '.bin', `${name}${ext}`);
|
|
185
|
+
if (fs.existsSync(portableCandidate)) return portableCandidate;
|
|
186
|
+
}
|
|
182
187
|
|
|
183
188
|
// Tier 1: PATH search
|
|
184
189
|
try {
|
|
185
190
|
if (IS_WINDOWS) {
|
|
186
|
-
const r = execSync('where agent.cmd 2>nul || where agent.exe 2>nul || where agent 2>nul', {
|
|
191
|
+
const r = execSync('where cursor-agent.cmd 2>nul || where cursor-agent.exe 2>nul || where cursor-agent 2>nul || where agent.cmd 2>nul || where agent.exe 2>nul || where agent 2>nul', {
|
|
187
192
|
encoding: 'utf-8', timeout: 5000,
|
|
188
193
|
});
|
|
189
194
|
return r.split(/\r?\n/)[0].trim();
|
|
190
195
|
} else {
|
|
191
|
-
return execSync('which agent', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
196
|
+
return execSync('which cursor-agent || which agent', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
192
197
|
}
|
|
193
198
|
} catch {}
|
|
194
199
|
|
|
195
200
|
// Tier 2: Next to current Node.js interpreter (npm global)
|
|
196
201
|
const nodeBinDir = path.dirname(process.execPath);
|
|
197
|
-
const
|
|
198
|
-
|
|
202
|
+
for (const name of names) {
|
|
203
|
+
const nearNode = path.join(nodeBinDir, `${name}${ext}`);
|
|
204
|
+
if (fs.existsSync(nearNode)) return nearNode;
|
|
205
|
+
}
|
|
199
206
|
|
|
200
207
|
// Tier 3: Common install locations
|
|
201
208
|
const candidates = IS_WINDOWS ? [
|
|
209
|
+
path.join(home, '.cursor', 'bin', 'cursor-agent.cmd'),
|
|
210
|
+
path.join(home, '.cursor', 'bin', 'cursor-agent.exe'),
|
|
211
|
+
path.join(home, '.cursor', 'bin', 'cursor-agent'),
|
|
212
|
+
path.join(home, '.cursor', 'bin', 'agent.cmd'),
|
|
213
|
+
path.join(home, '.cursor', 'bin', 'agent.exe'),
|
|
214
|
+
path.join(home, '.cursor', 'bin', 'agent'),
|
|
215
|
+
path.join(process.env.APPDATA || '', 'npm', 'cursor-agent.cmd'),
|
|
202
216
|
path.join(process.env.APPDATA || '', 'npm', 'agent.cmd'),
|
|
203
217
|
] : [
|
|
218
|
+
path.join(home, '.local', 'bin', 'cursor-agent'),
|
|
204
219
|
path.join(home, '.local', 'bin', 'agent'),
|
|
220
|
+
path.join(home, '.npm-global', 'bin', 'cursor-agent'),
|
|
205
221
|
path.join(home, '.npm-global', 'bin', 'agent'),
|
|
222
|
+
path.join(home, '.cursor', 'bin', 'cursor-agent'),
|
|
206
223
|
path.join(home, '.cursor', 'bin', 'agent'),
|
|
224
|
+
'/opt/homebrew/bin/cursor-agent',
|
|
207
225
|
'/opt/homebrew/bin/agent',
|
|
226
|
+
'/usr/local/bin/cursor-agent',
|
|
208
227
|
'/usr/local/bin/agent',
|
|
209
228
|
];
|
|
210
229
|
for (const c of candidates) {
|
|
@@ -11,6 +11,51 @@
|
|
|
11
11
|
|
|
12
12
|
'use strict';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Strong directive forcing agents to use the workspace browser when the
|
|
16
|
+
* workspace has Browser Fabric enabled. Emitted high in the system prompt
|
|
17
|
+
* so it wins against any earlier guidance that suggests local browsing
|
|
18
|
+
* tools.
|
|
19
|
+
*
|
|
20
|
+
* Returns '' when the toggle is off; the caller can unconditionally
|
|
21
|
+
* concatenate the result.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors `build_browser_directive` in
|
|
24
|
+
* sdk/src/openagents/adapters/workspace_prompt.py.
|
|
25
|
+
*/
|
|
26
|
+
function buildBrowserDirective(browserEnabled) {
|
|
27
|
+
if (!browserEnabled) return '';
|
|
28
|
+
return (
|
|
29
|
+
'\n## Browser Use (MANDATORY)\n' +
|
|
30
|
+
'This workspace has the **shared Browser Fabric session** enabled. ' +
|
|
31
|
+
'All web browsing MUST go through it so the user can watch the ' +
|
|
32
|
+
'session live in their right-side panel and so cookies / state ' +
|
|
33
|
+
'persist across agents.\n\n' +
|
|
34
|
+
'**Use ONLY these tools for any web browsing:**\n' +
|
|
35
|
+
'- `mcp__openagents-workspace__workspace_browser_open`\n' +
|
|
36
|
+
'- `mcp__openagents-workspace__workspace_browser_navigate`\n' +
|
|
37
|
+
'- `mcp__openagents-workspace__workspace_browser_click`\n' +
|
|
38
|
+
'- `mcp__openagents-workspace__workspace_browser_type`\n' +
|
|
39
|
+
'- `mcp__openagents-workspace__workspace_browser_snapshot`\n' +
|
|
40
|
+
'- `mcp__openagents-workspace__workspace_browser_screenshot`\n' +
|
|
41
|
+
'- `mcp__openagents-workspace__workspace_browser_list_tabs`\n' +
|
|
42
|
+
'- `mcp__openagents-workspace__workspace_browser_close`\n' +
|
|
43
|
+
'\n' +
|
|
44
|
+
'If you don\'t have these MCP tools, use `Bash` + `curl` against ' +
|
|
45
|
+
'`/v1/browser/tabs` (documented below in Shared Browser).\n\n' +
|
|
46
|
+
'**FORBIDDEN — do NOT call any of these:**\n' +
|
|
47
|
+
'- `mcp__browsermcp__*` (any local Browser MCP extension tool)\n' +
|
|
48
|
+
'- `mcp__playwright__*`, `mcp__puppeteer__*`, `mcp__chrome-devtools__*`, or any other local-browser MCP\n' +
|
|
49
|
+
'- `WebFetch`, `WebSearch`, `web_fetch`, `web_search`, or any built-in network/browser tool\n' +
|
|
50
|
+
'\n' +
|
|
51
|
+
'If a local browser tool errors with "extension isn\'t connected" or ' +
|
|
52
|
+
'"connect your browser", do NOT ask the user to connect anything — ' +
|
|
53
|
+
'the local extension is irrelevant here. Immediately switch to the ' +
|
|
54
|
+
'workspace browser tools above. The Browser Fabric session is already ' +
|
|
55
|
+
'running on the backend.\n'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
14
59
|
/**
|
|
15
60
|
* Build the identity section common to all adapters.
|
|
16
61
|
*/
|
|
@@ -294,25 +339,28 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
294
339
|
if (!isPlan) {
|
|
295
340
|
sections.push(
|
|
296
341
|
'\n### Routines (Recurring Tasks)\n\n' +
|
|
297
|
-
'Create a recurring routine that fires on a schedule
|
|
298
|
-
'
|
|
299
|
-
'
|
|
300
|
-
'**
|
|
301
|
-
'
|
|
302
|
-
'
|
|
342
|
+
'Create a recurring routine that fires on a schedule. Each routine gets ' +
|
|
343
|
+
'**its own dedicated thread** (`routine:<id>`) so different routines never ' +
|
|
344
|
+
'interfere, and the full context is preserved.\n\n' +
|
|
345
|
+
'**`context` is required** — provide a thorough description of what the ' +
|
|
346
|
+
'routine should do, any background info, and relevant details from the ' +
|
|
347
|
+
'current conversation. This context is posted at the start of the routine\'s ' +
|
|
348
|
+
'thread every time it fires, so you have full background.\n\n' +
|
|
349
|
+
'**Two schedule modes:**\n' +
|
|
350
|
+
'- **Daily**: `hour` (0-23 UTC) + `minute` (0-59), optional `days` ' +
|
|
351
|
+
'array (0=Mon, 6=Sun). Omit `days` for every day.\n' +
|
|
352
|
+
'- **Interval**: `interval_minutes` (1-1440). Fires every N minutes. ' +
|
|
353
|
+
'Mutually exclusive with `hour`/`minute`.\n\n' +
|
|
354
|
+
'**Create a daily routine:**\n' +
|
|
303
355
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
|
|
304
356
|
`${baseUrl}/v1/routines -d '{"name":"Daily PR Review","message":"Review open PRs",` +
|
|
357
|
+
`"context":"Review all open pull requests on the main repo. Check for merge conflicts, ` +
|
|
358
|
+
`CI failures, and stale PRs older than 3 days. Post a summary to the workspace.",` +
|
|
305
359
|
`"hour":8,"minute":0,` +
|
|
306
|
-
`"network":"${workspaceId}"
|
|
307
|
-
`"source":"openagents:${agentName}"}'\`\n\n` +
|
|
308
|
-
'**Create a weekday-only routine (Mon-Fri):**\n' +
|
|
309
|
-
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
|
|
310
|
-
`${baseUrl}/v1/routines -d '{"name":"Morning Standup","message":"Post standup summary",` +
|
|
311
|
-
`"hour":9,"minute":0,"days":[0,1,2,3,4],` +
|
|
312
|
-
`"network":"${workspaceId}","channel":"${channelName}",` +
|
|
360
|
+
`"network":"${workspaceId}",` +
|
|
313
361
|
`"source":"openagents:${agentName}"}'\`\n\n` +
|
|
314
362
|
'**List active routines:**\n' +
|
|
315
|
-
`\`${curl} -s -H "${h}" "${baseUrl}/v1/routines?network=${workspaceId}
|
|
363
|
+
`\`${curl} -s -H "${h}" "${baseUrl}/v1/routines?network=${workspaceId}"\`\n\n` +
|
|
316
364
|
'**Cancel a routine:**\n' +
|
|
317
365
|
`\`${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/routines/ROUTINE_ID\`\n`
|
|
318
366
|
);
|
|
@@ -352,7 +400,7 @@ function buildGuardrails() {
|
|
|
352
400
|
* Build the system prompt for Claude adapter (MCP-based).
|
|
353
401
|
* Claude gets identity + collaboration instructions but NOT API skills.
|
|
354
402
|
*/
|
|
355
|
-
function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = 'execute' }) {
|
|
403
|
+
function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = 'execute', browserEnabled = false }) {
|
|
356
404
|
const parts = [];
|
|
357
405
|
parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode));
|
|
358
406
|
parts.push(
|
|
@@ -362,6 +410,7 @@ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = '
|
|
|
362
410
|
'Use workspace_create_timer to set a reminder that wakes you up later.\n' +
|
|
363
411
|
'Use workspace_create_routine to set up recurring scheduled tasks (e.g. daily reviews).\n'
|
|
364
412
|
);
|
|
413
|
+
parts.push(buildBrowserDirective(browserEnabled));
|
|
365
414
|
parts.push(buildCollaborationPrompt());
|
|
366
415
|
parts.push(buildA2UIPrompt());
|
|
367
416
|
|
|
@@ -457,9 +506,10 @@ function buildA2UIPrompt() {
|
|
|
457
506
|
/**
|
|
458
507
|
* Build the full system prompt for OpenClaw/non-MCP agents.
|
|
459
508
|
*/
|
|
460
|
-
function buildOpenclawSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules }) {
|
|
509
|
+
function buildOpenclawSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false }) {
|
|
461
510
|
const parts = [];
|
|
462
511
|
parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode));
|
|
512
|
+
parts.push(buildBrowserDirective(browserEnabled));
|
|
463
513
|
parts.push(buildCollaborationPrompt());
|
|
464
514
|
parts.push(buildA2UIPrompt());
|
|
465
515
|
parts.push(buildModePrompt(mode));
|
|
@@ -473,12 +523,13 @@ function buildOpenclawSystemPrompt({ agentName, workspaceId, channelName, endpoi
|
|
|
473
523
|
/**
|
|
474
524
|
* Build a SKILL.md file for OpenClaw's skill auto-discovery.
|
|
475
525
|
*/
|
|
476
|
-
function buildOpenclawSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules }) {
|
|
526
|
+
function buildOpenclawSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules, browserEnabled = false }) {
|
|
477
527
|
const body = buildApiSkillsPrompt({
|
|
478
528
|
endpoint, workspaceId, token, agentName, channelName, disabledModules, mode: 'execute',
|
|
479
529
|
});
|
|
480
530
|
|
|
481
531
|
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute');
|
|
532
|
+
const directive = buildBrowserDirective(browserEnabled);
|
|
482
533
|
const collab = buildCollaborationPrompt();
|
|
483
534
|
|
|
484
535
|
const frontmatter = (
|
|
@@ -495,18 +546,19 @@ function buildOpenclawSkillMd({ endpoint, workspaceId, token, agentName, channel
|
|
|
495
546
|
'---\n\n'
|
|
496
547
|
);
|
|
497
548
|
|
|
498
|
-
return frontmatter + identity + '\n' + collab + '\n' + body + '\n' + buildGuardrails();
|
|
549
|
+
return frontmatter + identity + directive + '\n' + collab + '\n' + body + '\n' + buildGuardrails();
|
|
499
550
|
}
|
|
500
551
|
|
|
501
552
|
/**
|
|
502
553
|
* Build system prompt for OpenCode adapter.
|
|
503
554
|
*/
|
|
504
|
-
function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules }) {
|
|
555
|
+
function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false }) {
|
|
505
556
|
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode);
|
|
557
|
+
const directive = buildBrowserDirective(browserEnabled);
|
|
506
558
|
const collab = buildCollaborationPrompt();
|
|
507
559
|
const modePrompt = buildModePrompt(mode);
|
|
508
560
|
const api = buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode });
|
|
509
|
-
return identity + '\n' + collab + '\n' + modePrompt + '\n' + api + '\n' + buildGuardrails();
|
|
561
|
+
return identity + directive + '\n' + collab + '\n' + modePrompt + '\n' + api + '\n' + buildGuardrails();
|
|
510
562
|
}
|
|
511
563
|
|
|
512
564
|
/**
|
|
@@ -540,7 +592,7 @@ function buildOpenCodeSkillMd({ endpoint, workspaceId, token, agentName, channel
|
|
|
540
592
|
* of spawning an MCP server. Claude Code discovers the skill via its
|
|
541
593
|
* .claude/skills/ directory and uses Bash + curl to call workspace APIs.
|
|
542
594
|
*/
|
|
543
|
-
function buildClaudeSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules }) {
|
|
595
|
+
function buildClaudeSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules, browserEnabled = false }) {
|
|
544
596
|
const api = buildApiSkillsPrompt({
|
|
545
597
|
endpoint, workspaceId, token, agentName,
|
|
546
598
|
channelName: channelName || 'general',
|
|
@@ -549,6 +601,7 @@ function buildClaudeSkillMd({ endpoint, workspaceId, token, agentName, channelNa
|
|
|
549
601
|
});
|
|
550
602
|
|
|
551
603
|
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute');
|
|
604
|
+
const directive = buildBrowserDirective(browserEnabled);
|
|
552
605
|
const collab = buildCollaborationPrompt();
|
|
553
606
|
|
|
554
607
|
const frontmatter =
|
|
@@ -561,7 +614,7 @@ function buildClaudeSkillMd({ endpoint, workspaceId, token, agentName, channelNa
|
|
|
561
614
|
' or collaborating with other agents via @mentions.\n' +
|
|
562
615
|
'---\n\n';
|
|
563
616
|
|
|
564
|
-
return frontmatter + identity + '\n' + collab + '\n' + api + '\n' + buildGuardrails();
|
|
617
|
+
return frontmatter + identity + directive + '\n' + collab + '\n' + api + '\n' + buildGuardrails();
|
|
565
618
|
}
|
|
566
619
|
|
|
567
620
|
/**
|
|
@@ -570,7 +623,7 @@ function buildClaudeSkillMd({ endpoint, workspaceId, token, agentName, channelNa
|
|
|
570
623
|
* Written to .cursor/skills/openagents-workspace.md before each CLI spawn.
|
|
571
624
|
* Cursor discovers skills from the .cursor/skills/ directory automatically.
|
|
572
625
|
*/
|
|
573
|
-
function buildCursorSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules }) {
|
|
626
|
+
function buildCursorSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules, browserEnabled = false }) {
|
|
574
627
|
const api = buildApiSkillsPrompt({
|
|
575
628
|
endpoint, workspaceId, token, agentName,
|
|
576
629
|
channelName: channelName || 'general',
|
|
@@ -579,6 +632,7 @@ function buildCursorSkillMd({ endpoint, workspaceId, token, agentName, channelNa
|
|
|
579
632
|
});
|
|
580
633
|
|
|
581
634
|
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute');
|
|
635
|
+
const directive = buildBrowserDirective(browserEnabled);
|
|
582
636
|
const collab = buildCollaborationPrompt();
|
|
583
637
|
|
|
584
638
|
const frontmatter =
|
|
@@ -591,11 +645,12 @@ function buildCursorSkillMd({ endpoint, workspaceId, token, agentName, channelNa
|
|
|
591
645
|
' or collaborating with other agents via @mentions.\n' +
|
|
592
646
|
'---\n\n';
|
|
593
647
|
|
|
594
|
-
return frontmatter + identity + '\n' + collab + '\n' + api + '\n' + buildGuardrails();
|
|
648
|
+
return frontmatter + identity + directive + '\n' + collab + '\n' + api + '\n' + buildGuardrails();
|
|
595
649
|
}
|
|
596
650
|
|
|
597
651
|
module.exports = {
|
|
598
652
|
buildWorkspaceIdentity,
|
|
653
|
+
buildBrowserDirective,
|
|
599
654
|
buildCollaborationPrompt,
|
|
600
655
|
buildModePrompt,
|
|
601
656
|
buildGuardrails,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_BASE_URL = 'https://api.github.com';
|
|
6
|
+
const USER_AGENT = '@openagents-org/agent-launcher';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Thin GitHub REST API client used by the launcher's GitHub integration.
|
|
10
|
+
*
|
|
11
|
+
* Auth is via a personal access token (classic or fine-grained) or GitHub
|
|
12
|
+
* App installation token. Each call accepts an explicit token so the caller
|
|
13
|
+
* can resolve it from the launcher's credentials store at request time.
|
|
14
|
+
*/
|
|
15
|
+
class GitHubClient {
|
|
16
|
+
constructor(opts = {}) {
|
|
17
|
+
this.baseUrl = (opts.baseUrl || DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Verify a token and return identity / rate-limit info.
|
|
22
|
+
* Mirrors `GET /user` + `GET /rate_limit`.
|
|
23
|
+
*/
|
|
24
|
+
async probe(token) {
|
|
25
|
+
if (!token) throw new Error('Missing GitHub token');
|
|
26
|
+
const user = await this._get('/user', token);
|
|
27
|
+
const rate = await this._get('/rate_limit', token).catch(() => null);
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
login: user.login,
|
|
31
|
+
name: user.name,
|
|
32
|
+
avatarUrl: user.avatar_url,
|
|
33
|
+
scopes: user.__scopes || [],
|
|
34
|
+
rate: rate ? rate.rate : null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getRepo(owner, name, token) {
|
|
39
|
+
return this._get(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`, token);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} owner
|
|
44
|
+
* @param {string} name
|
|
45
|
+
* @param {{ state?: 'open'|'closed'|'all', perPage?: number, page?: number }} opts
|
|
46
|
+
* @param {string} token
|
|
47
|
+
*/
|
|
48
|
+
async listIssues(owner, name, opts = {}, token) {
|
|
49
|
+
const state = opts.state || 'open';
|
|
50
|
+
const perPage = opts.perPage || 20;
|
|
51
|
+
const page = opts.page || 1;
|
|
52
|
+
// GitHub's /issues endpoint returns PRs too — filter to issue-only.
|
|
53
|
+
const items = await this._get(
|
|
54
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/issues?state=${state}&per_page=${perPage}&page=${page}`,
|
|
55
|
+
token,
|
|
56
|
+
);
|
|
57
|
+
return items.filter((i) => !i.pull_request);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async listPullRequests(owner, name, opts = {}, token) {
|
|
61
|
+
const state = opts.state || 'open';
|
|
62
|
+
const perPage = opts.perPage || 20;
|
|
63
|
+
const page = opts.page || 1;
|
|
64
|
+
return this._get(
|
|
65
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls?state=${state}&per_page=${perPage}&page=${page}`,
|
|
66
|
+
token,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createIssueComment(owner, name, issueNumber, body, token) {
|
|
71
|
+
return this._post(
|
|
72
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/issues/${issueNumber}/comments`,
|
|
73
|
+
{ body },
|
|
74
|
+
token,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse a GitHub-ish input into { owner, name }.
|
|
80
|
+
* Accepts: "owner/name", "https://github.com/owner/name", "git@github.com:owner/name.git".
|
|
81
|
+
*/
|
|
82
|
+
static parseRepo(input) {
|
|
83
|
+
if (!input) return null;
|
|
84
|
+
const v = String(input).trim();
|
|
85
|
+
if (!v) return null;
|
|
86
|
+
// SSH form: git@github.com:owner/name(.git)
|
|
87
|
+
const ssh = v.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
88
|
+
if (ssh) return { owner: ssh[1], name: ssh[2] };
|
|
89
|
+
// URL form
|
|
90
|
+
try {
|
|
91
|
+
const u = new URL(v);
|
|
92
|
+
if (/github\.com$/i.test(u.hostname)) {
|
|
93
|
+
const parts = u.pathname.replace(/^\//, '').replace(/\.git$/, '').split('/');
|
|
94
|
+
if (parts.length >= 2 && parts[0] && parts[1]) {
|
|
95
|
+
return { owner: parts[0], name: parts[1] };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// not a URL — fall through
|
|
100
|
+
}
|
|
101
|
+
// Plain "owner/name"
|
|
102
|
+
const plain = v.match(/^([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
|
|
103
|
+
if (plain) return { owner: plain[1], name: plain[2] };
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_headers(token, extra = {}) {
|
|
108
|
+
return {
|
|
109
|
+
Accept: 'application/vnd.github+json',
|
|
110
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
111
|
+
'User-Agent': USER_AGENT,
|
|
112
|
+
Authorization: `Bearer ${token}`,
|
|
113
|
+
...extra,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_get(urlPath, token, timeout = 20000) {
|
|
118
|
+
return this._request('GET', urlPath, null, token, timeout);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_post(urlPath, body, token, timeout = 30000) {
|
|
122
|
+
return this._request('POST', urlPath, body, token, timeout);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_request(method, urlPath, body, token, timeout) {
|
|
126
|
+
const fullUrl = this.baseUrl + urlPath;
|
|
127
|
+
const jsonBody = body == null ? null : JSON.stringify(body);
|
|
128
|
+
const headers = this._headers(token);
|
|
129
|
+
if (jsonBody != null) headers['Content-Type'] = 'application/json';
|
|
130
|
+
|
|
131
|
+
return new Promise((resolve, reject) => {
|
|
132
|
+
const req = https.request(fullUrl, { method, headers, timeout }, (res) => {
|
|
133
|
+
let data = '';
|
|
134
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
135
|
+
res.on('end', () => {
|
|
136
|
+
const status = res.statusCode || 0;
|
|
137
|
+
let parsed = null;
|
|
138
|
+
try { parsed = data ? JSON.parse(data) : null; } catch { parsed = null; }
|
|
139
|
+
if (status >= 400) {
|
|
140
|
+
const msg = (parsed && (parsed.message || parsed.error)) || `HTTP ${status}`;
|
|
141
|
+
const err = new Error(`GitHub ${method} ${urlPath} failed: ${msg}`);
|
|
142
|
+
err.status = status;
|
|
143
|
+
err.body = parsed;
|
|
144
|
+
return reject(err);
|
|
145
|
+
}
|
|
146
|
+
// Attach OAuth scopes for /user probe convenience.
|
|
147
|
+
if (parsed && method === 'GET' && urlPath === '/user') {
|
|
148
|
+
const scopes = res.headers['x-oauth-scopes'];
|
|
149
|
+
if (typeof scopes === 'string') {
|
|
150
|
+
parsed.__scopes = scopes.split(',').map((s) => s.trim()).filter(Boolean);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
resolve(parsed);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
req.on('error', reject);
|
|
158
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('GitHub request timed out')); });
|
|
159
|
+
if (jsonBody != null) req.write(jsonBody);
|
|
160
|
+
req.end();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { GitHubClient };
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const { Registry } = require('./registry');
|
|
|
6
6
|
const { Installer } = require('./installer');
|
|
7
7
|
const { Daemon } = require('./daemon');
|
|
8
8
|
const { WorkspaceClient } = require('./workspace-client');
|
|
9
|
+
const { GitHubClient } = require('./github-client');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Main entry point for the agent-connector library.
|
|
@@ -274,4 +275,4 @@ class AgentConnector {
|
|
|
274
275
|
const adapters = require('./adapters');
|
|
275
276
|
|
|
276
277
|
const paths = require('./paths');
|
|
277
|
-
module.exports = { AgentConnector, Daemon, WorkspaceClient, adapters, paths };
|
|
278
|
+
module.exports = { AgentConnector, Daemon, WorkspaceClient, GitHubClient, adapters, paths };
|
package/src/installer.js
CHANGED
|
@@ -433,8 +433,15 @@ class Installer {
|
|
|
433
433
|
if (proc.stdout) proc.stdout.setEncoding('utf-8');
|
|
434
434
|
if (proc.stderr) proc.stderr.setEncoding('utf-8');
|
|
435
435
|
|
|
436
|
-
|
|
437
|
-
|
|
436
|
+
let outputTail = '';
|
|
437
|
+
const captureOutput = (d) => {
|
|
438
|
+
const text = String(d);
|
|
439
|
+
outputTail = (outputTail + text).slice(-4000);
|
|
440
|
+
if (onData) onData(text);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
if (proc.stdout) proc.stdout.on('data', captureOutput);
|
|
444
|
+
if (proc.stderr) proc.stderr.on('data', captureOutput);
|
|
438
445
|
|
|
439
446
|
proc.on('error', (err) => reject(err));
|
|
440
447
|
proc.on('close', (code) => {
|
|
@@ -443,7 +450,21 @@ class Installer {
|
|
|
443
450
|
if (onData) onData(`\nDone! ${agentType} is now installed.\n`);
|
|
444
451
|
resolve({ success: true, command: cmd });
|
|
445
452
|
} else {
|
|
446
|
-
|
|
453
|
+
// A partial install can leave a placeholder stub at
|
|
454
|
+
// bin/<binary>.exe and npm-generated cmd-shims under
|
|
455
|
+
// node_modules/.bin/<name>{,.cmd,.ps1} that point to it. On
|
|
456
|
+
// Windows, anything that later tries to run the binary (a health
|
|
457
|
+
// check, a shell open) triggers a "this app can't run on your PC"
|
|
458
|
+
// dialog because the stub isn't a real PE executable. Sweep them
|
|
459
|
+
// up so the next install (or uninstall+reinstall) starts clean.
|
|
460
|
+
try {
|
|
461
|
+
this._cleanStaleShims(agentType);
|
|
462
|
+
this._cleanStubBinary(agentType);
|
|
463
|
+
} catch {}
|
|
464
|
+
const tail = outputTail.trim();
|
|
465
|
+
const msg = tail
|
|
466
|
+
? `Install failed with exit code ${code}\nCommand: ${cmd}\n\n${tail}`
|
|
467
|
+
: `Install failed with exit code ${code}\nCommand: ${cmd}`;
|
|
447
468
|
if (onData) onData(`\n${msg}\n`);
|
|
448
469
|
reject(new Error(msg));
|
|
449
470
|
}
|
|
@@ -451,6 +472,35 @@ class Installer {
|
|
|
451
472
|
});
|
|
452
473
|
}
|
|
453
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Remove the placeholder bin/<binary>.exe (and the npm package directory
|
|
477
|
+
* if it's effectively empty) left over from a failed postinstall.
|
|
478
|
+
*/
|
|
479
|
+
_cleanStubBinary(agentType) {
|
|
480
|
+
const entry = this.registry.getEntry(agentType);
|
|
481
|
+
const install = entry && entry.install;
|
|
482
|
+
if (!install) return;
|
|
483
|
+
const binary = install.binary || agentType;
|
|
484
|
+
const npmPkg = install.npm_package
|
|
485
|
+
|| (() => {
|
|
486
|
+
const cmd = this._getInstallCommand(install);
|
|
487
|
+
if (!cmd || !cmd.includes('npm install')) return null;
|
|
488
|
+
const m = cmd.match(/npm install\s+(?:-g\s+)?(@?[\w-]+(?:\/[\w-]+)?)(?:@\S*)?$/);
|
|
489
|
+
return m ? m[1] : null;
|
|
490
|
+
})();
|
|
491
|
+
if (!npmPkg) return;
|
|
492
|
+
|
|
493
|
+
const prefix = getRuntimePrefix(agentType);
|
|
494
|
+
const pkgDir = path.join(prefix, 'node_modules', npmPkg);
|
|
495
|
+
const stubDir = path.join(pkgDir, 'bin');
|
|
496
|
+
for (const ext of ['', '.exe', '.cmd', '.ps1']) {
|
|
497
|
+
try {
|
|
498
|
+
const f = path.join(stubDir, binary + ext);
|
|
499
|
+
if (fs.existsSync(f) && fs.statSync(f).size < 4096) fs.unlinkSync(f);
|
|
500
|
+
} catch {}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
454
504
|
/**
|
|
455
505
|
* Uninstall an agent runtime.
|
|
456
506
|
* @returns {Promise<{success: boolean, output: string}>}
|
|
@@ -566,7 +616,7 @@ class Installer {
|
|
|
566
616
|
*/
|
|
567
617
|
_getInstallCommand(installCfg) {
|
|
568
618
|
const plat = Installer.platform();
|
|
569
|
-
return installCfg[plat] || installCfg.command || null;
|
|
619
|
+
return installCfg[plat] || installCfg.command || installCfg.npm || null;
|
|
570
620
|
}
|
|
571
621
|
|
|
572
622
|
/**
|
|
@@ -608,7 +658,14 @@ class Installer {
|
|
|
608
658
|
_whichBinary(agentType) {
|
|
609
659
|
const entry = this.registry.getEntry(agentType);
|
|
610
660
|
const binary = entry && entry.install ? entry.install.binary : agentType;
|
|
611
|
-
|
|
661
|
+
const aliases = entry && entry.install && Array.isArray(entry.install.binary_aliases)
|
|
662
|
+
? entry.install.binary_aliases
|
|
663
|
+
: [];
|
|
664
|
+
for (const candidate of [binary, ...aliases]) {
|
|
665
|
+
const found = whichBinary(candidate);
|
|
666
|
+
if (found) return found;
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
612
669
|
}
|
|
613
670
|
|
|
614
671
|
// -- Markers --
|
|
@@ -680,28 +737,58 @@ class Installer {
|
|
|
680
737
|
const extraDirs = [];
|
|
681
738
|
try { extraDirs.push(path.dirname(process.execPath)); } catch {}
|
|
682
739
|
|
|
683
|
-
//
|
|
740
|
+
// Whether a usable `node` is already reachable WITHOUT the bundled
|
|
741
|
+
// standalone node.exe. We must avoid shadowing a working system node with
|
|
742
|
+
// the launcher's bundled exe: that file is an unsigned standalone
|
|
743
|
+
// node.exe and on some Windows machines Defender/SmartScreen blocks
|
|
744
|
+
// CreateProcess on it, which surfaces as `cmd: 拒绝访问` ("Access
|
|
745
|
+
// Denied") when npm runs `node install.cjs` for packages like
|
|
746
|
+
// @anthropic-ai/claude-code, plus a "this app can't run on your PC"
|
|
747
|
+
// dialog.
|
|
748
|
+
const bundledDir = path.join(this.configDir, 'nodejs');
|
|
749
|
+
const hasSystemNode = this._hasSystemNode(bundledDir);
|
|
750
|
+
|
|
684
751
|
if (process.platform === 'win32') {
|
|
685
|
-
try {
|
|
686
|
-
const bundledDir = path.join(this.configDir, 'nodejs');
|
|
687
|
-
if (fs.existsSync(bundledDir)) {
|
|
688
|
-
// Node.js may be directly in nodejs/ (launcher symlink style) or in nodejs/node-v*/
|
|
689
|
-
if (fs.existsSync(path.join(bundledDir, 'node.exe'))) {
|
|
690
|
-
extraDirs.push(bundledDir);
|
|
691
|
-
}
|
|
692
|
-
const entries = fs.readdirSync(bundledDir).filter(e => e.startsWith('node-'));
|
|
693
|
-
if (entries.length > 0) {
|
|
694
|
-
extraDirs.push(path.join(bundledDir, entries[0]));
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
} catch {}
|
|
698
752
|
const appData = env.APPDATA || '';
|
|
699
753
|
if (appData) extraDirs.push(path.join(appData, 'npm'));
|
|
700
754
|
extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'nodejs') : 'C:\\Program Files\\nodejs');
|
|
701
755
|
extraDirs.push(env.SystemRoot ? path.join(env.SystemRoot, 'System32') : 'C:\\Windows\\System32');
|
|
702
756
|
extraDirs.push(env.ProgramFiles ? path.join(env.ProgramFiles, 'Git', 'cmd') : 'C:\\Program Files\\Git\\cmd');
|
|
757
|
+
|
|
758
|
+
// Bundled node — fallback only. Skip if system node works, otherwise
|
|
759
|
+
// verify the bundled exe can actually launch before exposing it.
|
|
760
|
+
if (!hasSystemNode) {
|
|
761
|
+
try {
|
|
762
|
+
if (fs.existsSync(bundledDir)) {
|
|
763
|
+
const directExe = path.join(bundledDir, 'node.exe');
|
|
764
|
+
if (fs.existsSync(directExe) && this._canExecute(directExe)) {
|
|
765
|
+
extraDirs.unshift(bundledDir);
|
|
766
|
+
}
|
|
767
|
+
for (const entry of fs.readdirSync(bundledDir).filter(e => e.startsWith('node-'))) {
|
|
768
|
+
const nested = path.join(bundledDir, entry);
|
|
769
|
+
if (fs.existsSync(path.join(nested, 'node.exe')) && this._canExecute(path.join(nested, 'node.exe'))) {
|
|
770
|
+
extraDirs.unshift(nested);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
} catch {}
|
|
775
|
+
}
|
|
703
776
|
} else {
|
|
704
777
|
extraDirs.push('/usr/local/bin', '/opt/homebrew/bin');
|
|
778
|
+
if (!hasSystemNode) {
|
|
779
|
+
try {
|
|
780
|
+
const candidates = [
|
|
781
|
+
path.join(bundledDir, 'bin', 'node'),
|
|
782
|
+
path.join(bundledDir, 'node'),
|
|
783
|
+
];
|
|
784
|
+
for (const bin of candidates) {
|
|
785
|
+
if (fs.existsSync(bin) && this._canExecute(bin)) {
|
|
786
|
+
extraDirs.unshift(path.dirname(bin));
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
} catch {}
|
|
791
|
+
}
|
|
705
792
|
}
|
|
706
793
|
for (const d of extraDirs) {
|
|
707
794
|
if (d && !(env.PATH || '').includes(d)) {
|
|
@@ -711,6 +798,51 @@ class Installer {
|
|
|
711
798
|
return env;
|
|
712
799
|
}
|
|
713
800
|
|
|
801
|
+
/**
|
|
802
|
+
* True if `node` exists outside the bundled launcher directory. Uses raw
|
|
803
|
+
* process.env.PATH (no enhancement) so we don't accidentally detect the
|
|
804
|
+
* bundled node and conclude that "system node exists".
|
|
805
|
+
*/
|
|
806
|
+
_hasSystemNode(bundledDir) {
|
|
807
|
+
try {
|
|
808
|
+
const { execFileSync } = require('child_process');
|
|
809
|
+
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
810
|
+
const args = process.platform === 'win32' ? ['node'] : ['node'];
|
|
811
|
+
const out = execFileSync(cmd, args, {
|
|
812
|
+
encoding: 'utf-8',
|
|
813
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
814
|
+
timeout: 5000,
|
|
815
|
+
windowsHide: true,
|
|
816
|
+
env: { ...process.env },
|
|
817
|
+
});
|
|
818
|
+
const prefix = (bundledDir || '').toLowerCase();
|
|
819
|
+
for (const line of out.split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
820
|
+
if (!prefix || !line.toLowerCase().startsWith(prefix)) return true;
|
|
821
|
+
}
|
|
822
|
+
return false;
|
|
823
|
+
} catch {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Smoke-test a node binary by running `--version`. Returns false on any
|
|
830
|
+
* failure (missing exe, blocked by AV, arch mismatch, signature policy).
|
|
831
|
+
*/
|
|
832
|
+
_canExecute(binaryPath) {
|
|
833
|
+
try {
|
|
834
|
+
const { spawnSync } = require('child_process');
|
|
835
|
+
const r = spawnSync(binaryPath, ['--version'], {
|
|
836
|
+
timeout: 5000,
|
|
837
|
+
windowsHide: true,
|
|
838
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
839
|
+
});
|
|
840
|
+
return r.status === 0 && !r.error;
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
714
846
|
/**
|
|
715
847
|
* Check if Node.js/npm is available on the system.
|
|
716
848
|
*/
|
package/src/mcp-server.js
CHANGED
|
@@ -301,21 +301,26 @@ function buildToolDefs(disabledModules) {
|
|
|
301
301
|
},
|
|
302
302
|
{
|
|
303
303
|
name: 'workspace_create_routine',
|
|
304
|
-
description: 'Create a recurring scheduled routine
|
|
304
|
+
description: 'Create a recurring scheduled routine. Each routine gets its own dedicated thread with full context. Two schedule modes: daily (hour+minute, optional days) or interval (every N minutes).',
|
|
305
305
|
inputSchema: {
|
|
306
306
|
type: 'object',
|
|
307
307
|
properties: {
|
|
308
308
|
name: { type: 'string', description: 'Human-readable label for the routine (e.g. "Daily PR Review")' },
|
|
309
|
-
message: { type: 'string', description: '
|
|
310
|
-
|
|
311
|
-
|
|
309
|
+
message: { type: 'string', description: 'Short trigger message posted each time the routine fires' },
|
|
310
|
+
context: { type: 'string', description: 'Comprehensive context for the routine: what it should do, background info, goals, relevant details from the current conversation. This is posted at the start of the routine\'s dedicated thread so you have full background every time it fires. Be thorough — this is your only memory of why this routine exists.' },
|
|
311
|
+
hour: { type: 'integer', description: 'Daily mode: hour in UTC (0-23). Omit if using interval_minutes.' },
|
|
312
|
+
minute: { type: 'integer', description: 'Daily mode: minute (0-59). Omit if using interval_minutes.' },
|
|
312
313
|
days: {
|
|
313
314
|
type: 'array',
|
|
314
315
|
items: { type: 'integer' },
|
|
315
|
-
description: '
|
|
316
|
+
description: 'Daily mode: days of week to fire (0=Mon, 6=Sun). Omit for every day. Not allowed in interval mode.',
|
|
317
|
+
},
|
|
318
|
+
interval_minutes: {
|
|
319
|
+
type: 'integer',
|
|
320
|
+
description: 'Interval mode: fire every N minutes (1-1440). Mutually exclusive with hour/minute.',
|
|
316
321
|
},
|
|
317
322
|
},
|
|
318
|
-
required: ['name', 'message', '
|
|
323
|
+
required: ['name', 'message', 'context'],
|
|
319
324
|
},
|
|
320
325
|
},
|
|
321
326
|
{
|
|
@@ -760,25 +765,41 @@ class McpServer {
|
|
|
760
765
|
{
|
|
761
766
|
name: args.name,
|
|
762
767
|
message: args.message,
|
|
768
|
+
context: args.context,
|
|
763
769
|
hour: args.hour,
|
|
764
770
|
minute: args.minute,
|
|
765
771
|
days: args.days,
|
|
772
|
+
interval_minutes: args.interval_minutes,
|
|
766
773
|
source: `openagents:${this.agentName}`,
|
|
767
774
|
},
|
|
768
775
|
);
|
|
769
|
-
|
|
770
|
-
|
|
776
|
+
let scheduleStr;
|
|
777
|
+
if (args.interval_minutes != null) {
|
|
778
|
+
scheduleStr = `every ${args.interval_minutes} min`;
|
|
779
|
+
} else {
|
|
780
|
+
const daysStr = args.days ? `days [${args.days.join(',')}]` : 'every day';
|
|
781
|
+
scheduleStr = `at ${String(args.hour).padStart(2,'0')}:${String(args.minute).padStart(2,'0')} UTC, ${daysStr}`;
|
|
782
|
+
}
|
|
783
|
+
const channel = (result && result.channel_name) || `routine:${result.id}`;
|
|
784
|
+
return text(
|
|
785
|
+
`Routine created: "${args.name}" ${scheduleStr} in dedicated thread \`${channel}\` (id: ${result.id})`,
|
|
786
|
+
);
|
|
771
787
|
}
|
|
772
788
|
|
|
773
789
|
case 'workspace_list_routines': {
|
|
774
790
|
const data = await this.ws.listRoutines(this.workspaceId, this.channelName, this.token);
|
|
775
791
|
const routines = (data && data.routines) || [];
|
|
776
792
|
if (!routines.length) return text('No active routines.');
|
|
777
|
-
const lines = routines.map((r) =>
|
|
778
|
-
|
|
779
|
-
(r.
|
|
780
|
-
|
|
781
|
-
|
|
793
|
+
const lines = routines.map((r) => {
|
|
794
|
+
let when;
|
|
795
|
+
if (r.schedule_interval_minutes != null) {
|
|
796
|
+
when = `every ${r.schedule_interval_minutes} min`;
|
|
797
|
+
} else {
|
|
798
|
+
when = `at ${String(r.schedule_hour).padStart(2,'0')}:${String(r.schedule_minute).padStart(2,'0')} UTC` +
|
|
799
|
+
(r.schedule_days ? ` [days: ${r.schedule_days.join(',')}]` : ' (daily)');
|
|
800
|
+
}
|
|
801
|
+
return `- ${r.id}: "${r.name}" ${when} — next: ${r.next_fires_at} (by ${r.created_by})`;
|
|
802
|
+
});
|
|
782
803
|
return text(lines.join('\n'));
|
|
783
804
|
}
|
|
784
805
|
|
package/src/paths.js
CHANGED
|
@@ -16,12 +16,25 @@ const IS_WINDOWS = process.platform === 'win32';
|
|
|
16
16
|
const IS_MACOS = process.platform === 'darwin';
|
|
17
17
|
const SEP = IS_WINDOWS ? ';' : ':';
|
|
18
18
|
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
19
|
+
const PATH_LOOKUP_CACHE_TTL_MS = 30 * 1000;
|
|
20
|
+
|
|
21
|
+
let extraBinDirsCache = { value: null, at: 0, path: '' };
|
|
22
|
+
const whichBinaryCache = new Map();
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* Get all extra binary directories that should be checked beyond process.env.PATH.
|
|
22
26
|
* Returns deduplicated list of existing directories.
|
|
23
27
|
*/
|
|
24
28
|
function getExtraBinDirs() {
|
|
29
|
+
const currentPATH = process.env.PATH || '';
|
|
30
|
+
if (
|
|
31
|
+
extraBinDirsCache.value &&
|
|
32
|
+
extraBinDirsCache.path === currentPATH &&
|
|
33
|
+
Date.now() - extraBinDirsCache.at < PATH_LOOKUP_CACHE_TTL_MS
|
|
34
|
+
) {
|
|
35
|
+
return [...extraBinDirsCache.value];
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
const dirs = [];
|
|
26
39
|
|
|
27
40
|
if (IS_WINDOWS) {
|
|
@@ -62,8 +75,7 @@ function getExtraBinDirs() {
|
|
|
62
75
|
|
|
63
76
|
// Filter to existing directories only, deduplicate
|
|
64
77
|
const seen = new Set();
|
|
65
|
-
const
|
|
66
|
-
return dirs.filter(d => {
|
|
78
|
+
const value = dirs.filter(d => {
|
|
67
79
|
if (!d || seen.has(d)) return false;
|
|
68
80
|
// Skip if already in PATH (case-insensitive on Windows)
|
|
69
81
|
if (IS_WINDOWS ? currentPATH.toLowerCase().includes(d.toLowerCase()) : currentPATH.includes(d)) return false;
|
|
@@ -74,6 +86,8 @@ function getExtraBinDirs() {
|
|
|
74
86
|
return false;
|
|
75
87
|
}
|
|
76
88
|
});
|
|
89
|
+
extraBinDirsCache = { value, at: Date.now(), path: currentPATH };
|
|
90
|
+
return [...value];
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
/**
|
|
@@ -114,6 +128,13 @@ function getEnhancedEnv(baseEnv) {
|
|
|
114
128
|
*/
|
|
115
129
|
function whichBinary(name) {
|
|
116
130
|
if (!name) return null;
|
|
131
|
+
const currentPATH = process.env.PATH || '';
|
|
132
|
+
const cacheKey = `${name}\0${currentPATH}`;
|
|
133
|
+
const cached = whichBinaryCache.get(cacheKey);
|
|
134
|
+
if (cached && Date.now() - cached.at < PATH_LOOKUP_CACHE_TTL_MS) {
|
|
135
|
+
return cached.value;
|
|
136
|
+
}
|
|
137
|
+
|
|
117
138
|
const cmd = IS_WINDOWS ? `where ${name}` : `which ${name}`;
|
|
118
139
|
try {
|
|
119
140
|
const result = execSync(cmd, {
|
|
@@ -121,9 +142,13 @@ function whichBinary(name) {
|
|
|
121
142
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
122
143
|
env: { ...process.env, PATH: getEnhancedPATH() },
|
|
123
144
|
timeout: 5000,
|
|
145
|
+
windowsHide: true,
|
|
124
146
|
}).trim();
|
|
125
|
-
|
|
147
|
+
const value = result.split(/\r?\n/)[0] || null;
|
|
148
|
+
whichBinaryCache.set(cacheKey, { value, at: Date.now() });
|
|
149
|
+
return value;
|
|
126
150
|
} catch {
|
|
151
|
+
whichBinaryCache.set(cacheKey, { value: null, at: Date.now() });
|
|
127
152
|
return null;
|
|
128
153
|
}
|
|
129
154
|
}
|
|
@@ -154,6 +179,9 @@ function _addWindowsPaths(dirs) {
|
|
|
154
179
|
// Portable Node.js installed by OpenAgents Launcher
|
|
155
180
|
_push(dirs, path.join(HOME, '.openagents', 'nodejs'));
|
|
156
181
|
|
|
182
|
+
// Cursor CLI native installer
|
|
183
|
+
_push(dirs, path.join(HOME, '.cursor', 'bin'));
|
|
184
|
+
|
|
157
185
|
// Node.js install
|
|
158
186
|
_push(dirs, path.join(programFiles, 'nodejs'));
|
|
159
187
|
|
package/src/registry.js
CHANGED
|
@@ -85,7 +85,7 @@ class Registry {
|
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Merge bundled env_config/resolve_env/install into catalog entries.
|
|
88
|
-
* Remote/cached entries may lack these fields.
|
|
88
|
+
* Remote/cached entries may lack these fields or carry stale install data.
|
|
89
89
|
*/
|
|
90
90
|
_mergeBundled(catalog) {
|
|
91
91
|
const bundled = this._loadBundled();
|
|
@@ -94,7 +94,7 @@ class Registry {
|
|
|
94
94
|
if (b) {
|
|
95
95
|
if ((!entry.env_config || entry.env_config.length === 0) && b.env_config && b.env_config.length > 0) entry.env_config = b.env_config;
|
|
96
96
|
if (!entry.resolve_env && b.resolve_env) entry.resolve_env = b.resolve_env;
|
|
97
|
-
if (
|
|
97
|
+
if (b.install) entry.install = { ...b.install };
|
|
98
98
|
if (!entry.check_ready && b.check_ready) entry.check_ready = b.check_ready;
|
|
99
99
|
if (!entry.launch && b.launch) entry.launch = b.launch;
|
|
100
100
|
// Always take featured/order/support from bundled (source of truth for ordering)
|
|
@@ -136,11 +136,11 @@ class Registry {
|
|
|
136
136
|
const catalog = this.getCatalogSync();
|
|
137
137
|
const entry = catalog.find((e) => e.name === agentType) || null;
|
|
138
138
|
if (!entry) return null;
|
|
139
|
-
// Merge
|
|
139
|
+
// Merge bundled registry fields into cached/remote entries.
|
|
140
140
|
const bundled = this._loadBundled();
|
|
141
141
|
const b = bundled.find((e) => e.name === agentType);
|
|
142
142
|
if (b) {
|
|
143
|
-
if (
|
|
143
|
+
if (b.install) entry.install = { ...b.install };
|
|
144
144
|
if (!entry.check_ready && b.check_ready) entry.check_ready = b.check_ready;
|
|
145
145
|
if (!entry.launch && b.launch) entry.launch = b.launch;
|
|
146
146
|
if ((!entry.env_config || !entry.env_config.length) && b.env_config?.length) entry.env_config = b.env_config;
|
package/src/tui.js
CHANGED
|
@@ -120,25 +120,10 @@ function describeHealth(health) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
function loadCatalog(connector) {
|
|
123
|
-
const { execSync } = require('child_process');
|
|
124
123
|
const entries = connector.registry.getCatalogSync();
|
|
125
124
|
return entries.map(e => {
|
|
126
125
|
let installed = false;
|
|
127
|
-
|
|
128
|
-
// If a verify command exists, use it for accurate detection
|
|
129
|
-
const verifyCmd = IS_WINDOWS ? (e.install && e.install.verify_win) : (e.install && e.install.verify);
|
|
130
|
-
if (verifyCmd) {
|
|
131
|
-
try { execSync(verifyCmd, { stdio: 'ignore', timeout: 5000 }); installed = true; } catch {}
|
|
132
|
-
} else {
|
|
133
|
-
try { const { whichBinary } = require('./paths'); installed = !!whichBinary((e.install && e.install.binary) || e.name); } catch {}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (!installed) {
|
|
137
|
-
try {
|
|
138
|
-
const f = path.join(connector._configDir, 'installed_agents.json');
|
|
139
|
-
if (fs.existsSync(f)) installed = !!JSON.parse(fs.readFileSync(f, 'utf-8'))[e.name];
|
|
140
|
-
} catch {}
|
|
141
|
-
}
|
|
126
|
+
try { installed = !!connector.installer.getInstallInfo(e.name).installed; } catch {}
|
|
142
127
|
return {
|
|
143
128
|
name: e.name,
|
|
144
129
|
label: e.label || e.name,
|
package/src/workspace-client.js
CHANGED
|
@@ -271,7 +271,7 @@ class WorkspaceClient {
|
|
|
271
271
|
* Poll for pending messages targeted at an agent via GET /v1/events.
|
|
272
272
|
* Returns { messages, cursor } where cursor is the last event ID.
|
|
273
273
|
*/
|
|
274
|
-
async pollPending(workspaceId, agentName, token, { after, limit =
|
|
274
|
+
async pollPending(workspaceId, agentName, token, { after, limit = 500 } = {}) {
|
|
275
275
|
const params = new URLSearchParams({
|
|
276
276
|
network: workspaceId,
|
|
277
277
|
type: 'workspace.message.posted',
|
|
@@ -333,7 +333,28 @@ class WorkspaceClient {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
-
|
|
336
|
+
const composing = !!(result && result.composing);
|
|
337
|
+
return { messages, cursor, composing };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get the workspace's top-level metadata via GET /v1/workspaces/{id}.
|
|
342
|
+
*
|
|
343
|
+
* Returns the full data block from the backend — adapters care about
|
|
344
|
+
* `browserEnabled` today, but more keys may be surfaced over time. On
|
|
345
|
+
* error, returns null so callers can fall back to defaults rather than
|
|
346
|
+
* crashing.
|
|
347
|
+
*/
|
|
348
|
+
async getWorkspaceMetadata(workspaceId, token) {
|
|
349
|
+
try {
|
|
350
|
+
const data = await this._get(
|
|
351
|
+
`/v1/workspaces/${workspaceId}`,
|
|
352
|
+
this._wsHeaders(token),
|
|
353
|
+
);
|
|
354
|
+
return data.data || data || {};
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
337
358
|
}
|
|
338
359
|
|
|
339
360
|
/**
|
|
@@ -606,17 +627,22 @@ class WorkspaceClient {
|
|
|
606
627
|
|
|
607
628
|
// ── Routines ──
|
|
608
629
|
|
|
609
|
-
async createRoutine(workspaceId, channelName, token, { name, message, hour, minute, days, source } = {}) {
|
|
630
|
+
async createRoutine(workspaceId, channelName, token, { name, message, context, hour, minute, days, interval_minutes, source } = {}) {
|
|
610
631
|
const body = {
|
|
611
632
|
name,
|
|
612
633
|
message,
|
|
613
|
-
|
|
614
|
-
minute,
|
|
634
|
+
context: context || '',
|
|
615
635
|
network: workspaceId,
|
|
616
636
|
channel: channelName,
|
|
617
637
|
source: source || 'openagents:unknown',
|
|
618
638
|
};
|
|
619
|
-
if (
|
|
639
|
+
if (interval_minutes != null) {
|
|
640
|
+
body.interval_minutes = interval_minutes;
|
|
641
|
+
} else {
|
|
642
|
+
body.hour = hour;
|
|
643
|
+
body.minute = minute;
|
|
644
|
+
if (days) body.days = days;
|
|
645
|
+
}
|
|
620
646
|
const data = await this._post('/v1/routines', body, this._wsHeaders(token));
|
|
621
647
|
return data.data || data;
|
|
622
648
|
}
|