@openagents-org/agent-launcher 0.2.131 → 0.2.133
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/src/adapters/base.js +113 -0
- package/src/adapters/claude.js +334 -274
- package/src/adapters/codex.js +37 -2
- package/src/adapters/workspace-prompt.js +54 -4
- package/src/cli.js +118 -2
- package/src/daemon.js +12 -3
- package/src/index.js +11 -0
- package/src/installer.js +43 -3
- package/src/mcp-server.js +255 -88
- package/src/paths.js +15 -0
- package/src/skill-catalog.js +125 -0
- package/src/skill-installer.js +382 -0
- package/src/update-check.js +5 -25
- package/src/workspace-client.js +93 -0
package/src/adapters/codex.js
CHANGED
|
@@ -168,7 +168,7 @@ class CodexAdapter extends BaseAdapter {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
_buildSystemContext(channelName) {
|
|
171
|
-
|
|
171
|
+
const base = buildOpenclawSystemPrompt({
|
|
172
172
|
agentName: this.agentName,
|
|
173
173
|
workspaceId: this.workspaceId,
|
|
174
174
|
channelName,
|
|
@@ -177,6 +177,38 @@ class CodexAdapter extends BaseAdapter {
|
|
|
177
177
|
mode: this._mode,
|
|
178
178
|
disabledModules: this.disabledModules,
|
|
179
179
|
});
|
|
180
|
+
const skillsSection = this._buildInstalledSkillsSection();
|
|
181
|
+
return skillsSection ? `${base}\n\n${skillsSection}` : base;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Codex has no native skills-directory discovery (unlike Claude Code), so
|
|
186
|
+
* we inject installed Skill Hub skills directly into its context. Each
|
|
187
|
+
* skill's SKILL.md lives at <workingDir>/.codex/skills/<id>/SKILL.md and
|
|
188
|
+
* Codex (running with cwd=workingDir, full file access) can read it.
|
|
189
|
+
*/
|
|
190
|
+
_buildInstalledSkillsSection() {
|
|
191
|
+
let skills = [];
|
|
192
|
+
try {
|
|
193
|
+
const installer = require('../skill-installer');
|
|
194
|
+
skills = installer.listInstalledSkills({
|
|
195
|
+
agentType: this.agentType || 'codex',
|
|
196
|
+
workingDir: this.workingDir,
|
|
197
|
+
});
|
|
198
|
+
} catch {
|
|
199
|
+
return '';
|
|
200
|
+
}
|
|
201
|
+
if (!skills.length) return '';
|
|
202
|
+
const lines = skills.map((s) => {
|
|
203
|
+
const desc = s.description ? ` — ${s.description}` : '';
|
|
204
|
+
return `- **${s.name}** (\`${s.id}\`)${desc}\n Read its instructions: \`cat ${s.skillMd}\``;
|
|
205
|
+
});
|
|
206
|
+
return (
|
|
207
|
+
'## Installed Skills\n' +
|
|
208
|
+
'You have the following skills installed. When a task matches a skill, ' +
|
|
209
|
+
'read its `SKILL.md` (via the `cat` command shown) and follow it:\n' +
|
|
210
|
+
lines.join('\n')
|
|
211
|
+
);
|
|
180
212
|
}
|
|
181
213
|
|
|
182
214
|
// ------------------------------------------------------------------
|
|
@@ -205,14 +237,17 @@ class CodexAdapter extends BaseAdapter {
|
|
|
205
237
|
} catch {}
|
|
206
238
|
}
|
|
207
239
|
|
|
208
|
-
async _onControlAction(action,
|
|
240
|
+
async _onControlAction(action, payload) {
|
|
209
241
|
if (action === 'stop') {
|
|
210
242
|
for (const [channel, proc] of Object.entries(this._channelProcesses)) {
|
|
211
243
|
await this._stopProcess(proc);
|
|
212
244
|
delete this._channelProcesses[channel];
|
|
213
245
|
try { await this.sendStatus(channel, 'Execution stopped by user'); } catch {}
|
|
214
246
|
}
|
|
247
|
+
return;
|
|
215
248
|
}
|
|
249
|
+
// Shared actions (status, routines, skill.install, skill.uninstall).
|
|
250
|
+
await super._onControlAction(action, payload);
|
|
216
251
|
}
|
|
217
252
|
|
|
218
253
|
// ------------------------------------------------------------------
|
|
@@ -136,6 +136,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
136
136
|
const caps = [];
|
|
137
137
|
if (!disabled.has('files')) caps.push('share and read files with other agents and users');
|
|
138
138
|
if (!disabled.has('browser')) caps.push('browse websites in a shared browser');
|
|
139
|
+
if (!disabled.has('knowledge')) caps.push('create and access a shared knowledge base');
|
|
139
140
|
caps.push('discover other agents in the workspace');
|
|
140
141
|
|
|
141
142
|
sections.push(
|
|
@@ -292,7 +293,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
// To-Dos (planning)
|
|
295
|
-
if (!isPlan) {
|
|
296
|
+
if (!isPlan && !disabled.has('todos')) {
|
|
296
297
|
sections.push(
|
|
297
298
|
'\n### To-Do List (Planning)\n\n' +
|
|
298
299
|
'Create or update your to-do list to track progress. The entire list ' +
|
|
@@ -315,7 +316,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
315
316
|
}
|
|
316
317
|
|
|
317
318
|
// Timers
|
|
318
|
-
if (!isPlan) {
|
|
319
|
+
if (!isPlan && !disabled.has('timers')) {
|
|
319
320
|
sections.push(
|
|
320
321
|
'\n### Timers\n\n' +
|
|
321
322
|
'Set a timer that will send you a message after a delay, waking you up ' +
|
|
@@ -336,7 +337,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
// Routines (recurring scheduled tasks)
|
|
339
|
-
if (!isPlan) {
|
|
340
|
+
if (!isPlan && !disabled.has('routines')) {
|
|
340
341
|
sections.push(
|
|
341
342
|
'\n### Routines (Recurring Tasks)\n\n' +
|
|
342
343
|
'Create a recurring routine that fires on a schedule. Each routine gets ' +
|
|
@@ -366,6 +367,52 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
366
367
|
);
|
|
367
368
|
}
|
|
368
369
|
|
|
370
|
+
// Notifications / Inbox
|
|
371
|
+
if (!isPlan && !disabled.has('notifications')) {
|
|
372
|
+
sections.push(
|
|
373
|
+
'\n### Notifications (Inbox)\n\n' +
|
|
374
|
+
'Send notifications to the workspace inbox. Notifications appear in a ' +
|
|
375
|
+
'dedicated panel separate from the chat stream. Use for task completions, ' +
|
|
376
|
+
'important findings, or anything that needs human attention.\n\n' +
|
|
377
|
+
'**Send a notification:**\n' +
|
|
378
|
+
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
|
|
379
|
+
`${baseUrl}/v1/notifications -d '{"title":"Task Complete","message":"The analysis is ready.",` +
|
|
380
|
+
`"priority":"normal","channel":"${channelName}",` +
|
|
381
|
+
`"network":"${workspaceId}",` +
|
|
382
|
+
`"source":"openagents:${agentName}"}'\`\n\n` +
|
|
383
|
+
'**Priority values:** `low`, `normal`, `high`\n\n' +
|
|
384
|
+
'**List notifications:**\n' +
|
|
385
|
+
`\`${curl} -s -H "${h}" "${baseUrl}/v1/notifications?network=${workspaceId}"\`\n`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Knowledge Base
|
|
390
|
+
if (!isPlan && !disabled.has('knowledge')) {
|
|
391
|
+
sections.push(
|
|
392
|
+
'\n### Knowledge Base\n\n' +
|
|
393
|
+
'The workspace has a shared knowledge base of markdown documents. ' +
|
|
394
|
+
'Use it to store and retrieve shared information like API docs, ' +
|
|
395
|
+
'design decisions, project conventions, and other reference material. ' +
|
|
396
|
+
'Knowledge entries are accessible to all agents via @knowledge:slug mentions.\n\n' +
|
|
397
|
+
'**Create a knowledge entry:**\n' +
|
|
398
|
+
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json" ` +
|
|
399
|
+
`${baseUrl}/v1/knowledge -d '{"title":"API Design Patterns","content":"# API Design Patterns\\n\\n...",` +
|
|
400
|
+
`"description":"Common API patterns used in this project",` +
|
|
401
|
+
`"network":"${workspaceId}",` +
|
|
402
|
+
`"source":"openagents:${agentName}"}'\`\n\n` +
|
|
403
|
+
'**List knowledge entries:**\n' +
|
|
404
|
+
`\`${curl} -s -H "${h}" "${baseUrl}/v1/knowledge?network=${workspaceId}"\`\n\n` +
|
|
405
|
+
'**Read a knowledge entry by slug:**\n' +
|
|
406
|
+
`\`${curl} -s -H "${h}" "${baseUrl}/v1/knowledge/by-slug/api-design-patterns?network=${workspaceId}"\`\n\n` +
|
|
407
|
+
'**Update a knowledge entry:**\n' +
|
|
408
|
+
`\`${curl} -s -X PUT -H "${h}" -H "Content-Type: application/json" ` +
|
|
409
|
+
`${baseUrl}/v1/knowledge/ENTRY_ID -d '{"title":"Updated Title","content":"# Updated\\n\\n...",` +
|
|
410
|
+
`"network":"${workspaceId}","source":"openagents:${agentName}"}'\`\n\n` +
|
|
411
|
+
'**Delete a knowledge entry:**\n' +
|
|
412
|
+
`\`${curl} -s -X DELETE -H "${h}" "${baseUrl}/v1/knowledge/ENTRY_ID?network=${workspaceId}"\`\n`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
369
416
|
// Discovery
|
|
370
417
|
sections.push(
|
|
371
418
|
'\n### Discover Agents\n\n' +
|
|
@@ -408,7 +455,10 @@ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = '
|
|
|
408
455
|
'Use workspace_get_agents to see other agents.\n' +
|
|
409
456
|
'Use workspace_put_todos to track your progress. ALWAYS create a to-do list when given multiple tasks or multi-step work.\n' +
|
|
410
457
|
'Use workspace_create_timer to set a reminder that wakes you up later.\n' +
|
|
411
|
-
'Use workspace_create_routine to set up recurring scheduled tasks (e.g. daily reviews).\n'
|
|
458
|
+
'Use workspace_create_routine to set up recurring scheduled tasks (e.g. daily reviews).\n' +
|
|
459
|
+
'Use workspace_send_notification to send a notification to the workspace inbox when you complete a task or have important results.\n' +
|
|
460
|
+
'Use workspace_write_knowledge to create or update shared knowledge base entries that persist across conversations.\n' +
|
|
461
|
+
'Use workspace_read_knowledge to read knowledge entries by ID or slug (from @knowledge:slug mentions).\n'
|
|
412
462
|
);
|
|
413
463
|
parts.push(buildBrowserDirective(browserEnabled));
|
|
414
464
|
parts.push(buildCollaborationPrompt());
|
package/src/cli.js
CHANGED
|
@@ -128,6 +128,16 @@ async function cmdCreate(connector, flags, positional) {
|
|
|
128
128
|
// Signal daemon to pick up the new agent
|
|
129
129
|
try { connector.sendDaemonCommand('reload'); } catch {}
|
|
130
130
|
|
|
131
|
+
// Newly created agents are local-only until connected to a workspace.
|
|
132
|
+
// Without a workspace connection they will not appear in the Workspace Dashboard.
|
|
133
|
+
const created = connector.config.getAgent(name);
|
|
134
|
+
if (created && !created.network) {
|
|
135
|
+
print('');
|
|
136
|
+
print(`Note: '${name}' is local-only and will not appear in the Workspace Dashboard.`);
|
|
137
|
+
print(`To make it visible, connect it to a workspace:`);
|
|
138
|
+
print(` agn connect ${name} <workspace-token>`);
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
if (!connector.isInstalled(type)) {
|
|
132
142
|
if (!flags.install) {
|
|
133
143
|
print(`Runtime '${type}' is not installed. Run: agn install ${type}`);
|
|
@@ -278,9 +288,30 @@ async function cmdRuntimes(connector) {
|
|
|
278
288
|
|
|
279
289
|
async function cmdConnect(connector, flags, positional) {
|
|
280
290
|
const name = positional[0];
|
|
281
|
-
|
|
282
|
-
|
|
291
|
+
// Token resolution order: positional arg / --token flag, then env vars.
|
|
292
|
+
// OPENAGENTS_WORKSPACE_TOKEN is preferred; OA_WORKSPACE_TOKEN is supported
|
|
293
|
+
// for compatibility with the existing mcp-server env var.
|
|
294
|
+
const token = positional[1]
|
|
295
|
+
|| flags.token
|
|
296
|
+
|| process.env.OPENAGENTS_WORKSPACE_TOKEN
|
|
297
|
+
|| process.env.OA_WORKSPACE_TOKEN;
|
|
298
|
+
|
|
299
|
+
if (!name) {
|
|
283
300
|
print('Usage: agn connect <agent-name> <token>');
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!token) {
|
|
306
|
+
// No token supplied and none in the environment. Never prompt — keep
|
|
307
|
+
// CI / non-interactive environments from hanging. Print a helpful error
|
|
308
|
+
// explaining why the agent stays invisible and how to fix it.
|
|
309
|
+
print(`Error: no workspace token provided for '${name}'.`);
|
|
310
|
+
print(`Without a token, '${name}' stays local-only and will not appear in the Workspace Dashboard.`);
|
|
311
|
+
print('Provide a token in one of these ways:');
|
|
312
|
+
print(` agn connect ${name} <workspace-token>`);
|
|
313
|
+
print(` OPENAGENTS_WORKSPACE_TOKEN=<workspace-token> agn connect ${name}`);
|
|
314
|
+
process.exitCode = 1;
|
|
284
315
|
return;
|
|
285
316
|
}
|
|
286
317
|
|
|
@@ -502,6 +533,89 @@ async function cmdToolMode(connector, _flags, positional) {
|
|
|
502
533
|
}
|
|
503
534
|
}
|
|
504
535
|
|
|
536
|
+
async function cmdSkills(connector, _flags, positional) {
|
|
537
|
+
const { SKILL_CATALOG, getSkillDefaults } = require('./skill-catalog');
|
|
538
|
+
const toggleable = SKILL_CATALOG.filter(s => s.toggleable);
|
|
539
|
+
const first = positional[0];
|
|
540
|
+
const second = positional[1];
|
|
541
|
+
const third = positional[2];
|
|
542
|
+
|
|
543
|
+
// agn skills → list skills for all agents
|
|
544
|
+
if (!first) {
|
|
545
|
+
const agents = connector.config.getAgents();
|
|
546
|
+
if (agents.length === 0) { print('No agents configured'); return; }
|
|
547
|
+
for (const a of agents) {
|
|
548
|
+
const defaults = getSkillDefaults();
|
|
549
|
+
const skills = a.skills || {};
|
|
550
|
+
const parts = toggleable.map(s => {
|
|
551
|
+
const enabled = skills[s.id] !== undefined ? skills[s.id] : defaults[s.id];
|
|
552
|
+
return `${enabled ? '+' : '-'}${s.id}`;
|
|
553
|
+
});
|
|
554
|
+
print(` ${a.name}: ${parts.join(' ')}`);
|
|
555
|
+
}
|
|
556
|
+
print('\nUsage: agn skills <agent> [enable|disable <skill>]');
|
|
557
|
+
print('Available skills: ' + toggleable.map(s => s.id).join(', '));
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// agn skills catalog → show full catalog
|
|
562
|
+
if (first === 'catalog') {
|
|
563
|
+
print('Skill Hub — Available Skills:\n');
|
|
564
|
+
for (const s of SKILL_CATALOG) {
|
|
565
|
+
const tag = s.toggleable ? (s.defaultEnabled ? '[on]' : '[off]') : '[always]';
|
|
566
|
+
print(` ${s.id.padEnd(16)} ${tag.padEnd(10)} ${s.name}`);
|
|
567
|
+
print(` ${''.padEnd(16)} ${''.padEnd(10)} ${s.description}`);
|
|
568
|
+
print('');
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const agent = connector.config.getAgent(first);
|
|
574
|
+
if (!agent) { print(`Agent '${first}' not found`); process.exitCode = 1; return; }
|
|
575
|
+
|
|
576
|
+
// agn skills <agent> → show agent's skills
|
|
577
|
+
if (!second) {
|
|
578
|
+
const defaults = getSkillDefaults();
|
|
579
|
+
const skills = agent.skills || {};
|
|
580
|
+
print(`Skills for ${first}:\n`);
|
|
581
|
+
for (const s of toggleable) {
|
|
582
|
+
const enabled = skills[s.id] !== undefined ? skills[s.id] : defaults[s.id];
|
|
583
|
+
const marker = enabled ? ' ✓' : ' ✗';
|
|
584
|
+
print(`${marker} ${s.id.padEnd(14)} ${s.name} — ${s.description}`);
|
|
585
|
+
}
|
|
586
|
+
print('\nUsage: agn skills <agent> enable|disable <skill>');
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// agn skills <agent> enable|disable <skill>
|
|
591
|
+
if (second !== 'enable' && second !== 'disable') {
|
|
592
|
+
print(`Unknown action: ${second}. Use 'enable' or 'disable'.`);
|
|
593
|
+
process.exitCode = 1;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (!third) {
|
|
597
|
+
print(`Usage: agn skills ${first} ${second} <skill>`);
|
|
598
|
+
print('Available skills: ' + toggleable.map(s => s.id).join(', '));
|
|
599
|
+
process.exitCode = 1;
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const skillDef = toggleable.find(s => s.id === third);
|
|
604
|
+
if (!skillDef) {
|
|
605
|
+
print(`Unknown skill: ${third}`);
|
|
606
|
+
print('Available skills: ' + toggleable.map(s => s.id).join(', '));
|
|
607
|
+
process.exitCode = 1;
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const current = agent.skills || {};
|
|
612
|
+
const updated = { ...current, [third]: second === 'enable' };
|
|
613
|
+
connector.config.updateAgent(first, { skills: updated });
|
|
614
|
+
try { connector.sendDaemonCommand('reload'); } catch {}
|
|
615
|
+
const verb = second === 'enable' ? 'Enabled' : 'Disabled';
|
|
616
|
+
print(`${verb} '${skillDef.name}' for ${first}`);
|
|
617
|
+
}
|
|
618
|
+
|
|
505
619
|
async function cmdTestLLM(connector, _flags, positional) {
|
|
506
620
|
const type = positional[0];
|
|
507
621
|
if (!type) { print('Usage: agn test-llm <type>'); return; }
|
|
@@ -566,6 +680,7 @@ Commands:
|
|
|
566
680
|
connect <agent> <token> Connect agent to workspace
|
|
567
681
|
disconnect <agent> Disconnect agent from workspace
|
|
568
682
|
env <type> [--set K=V] View/set env vars for agent type
|
|
683
|
+
skills [agent] [action] Manage agent skills (enable/disable)
|
|
569
684
|
tool-mode [agent] [mode] View/set tool mode (mcp or skills)
|
|
570
685
|
autostart [--disable] Enable/disable auto-start on login
|
|
571
686
|
test-llm <type> Test LLM connection
|
|
@@ -652,6 +767,7 @@ async function main() {
|
|
|
652
767
|
autostart: () => cmdAutostart(connector, flags),
|
|
653
768
|
workspace: () => cmdWorkspace(connector, flags, positional),
|
|
654
769
|
env: () => cmdEnv(connector, flags, positional),
|
|
770
|
+
skills: () => cmdSkills(connector, flags, positional),
|
|
655
771
|
'tool-mode': () => cmdToolMode(connector, flags, positional),
|
|
656
772
|
'test-llm': () => cmdTestLLM(connector, flags, positional),
|
|
657
773
|
update: () => cmdUpdate(),
|
package/src/daemon.js
CHANGED
|
@@ -58,12 +58,20 @@ class Daemon {
|
|
|
58
58
|
// Write PID file
|
|
59
59
|
this._writePid();
|
|
60
60
|
|
|
61
|
-
// Periodic status +
|
|
61
|
+
// Periodic status (heavy: JSON serialize + write) every 5s.
|
|
62
62
|
this._statusInterval = setInterval(() => {
|
|
63
63
|
this._writeStatus();
|
|
64
|
-
this._processCommands();
|
|
65
64
|
}, 5000);
|
|
66
65
|
|
|
66
|
+
// Command file poll (cheap: existsSync on a tiny file) every 200ms so
|
|
67
|
+
// start/stop/restart from the launcher feels responsive. With a 5s
|
|
68
|
+
// combined interval, users saw up to 5s before the daemon even noticed
|
|
69
|
+
// a Stop click — this was especially painful on Windows where there's
|
|
70
|
+
// no SIGHUP shortcut and Stop landed near the end of a tick.
|
|
71
|
+
this._cmdInterval = setInterval(() => {
|
|
72
|
+
this._processCommands();
|
|
73
|
+
}, 200);
|
|
74
|
+
|
|
67
75
|
// Watch config file for hot-reload
|
|
68
76
|
this._watchConfig();
|
|
69
77
|
|
|
@@ -429,6 +437,7 @@ class Daemon {
|
|
|
429
437
|
|
|
430
438
|
async _adapterLoop(name, agentCfg, info, network) {
|
|
431
439
|
const { createAdapter } = require('./adapters');
|
|
440
|
+
const { skillsToDisabledModules } = require('./skill-catalog');
|
|
432
441
|
const agentType = agentCfg.type || 'openclaw';
|
|
433
442
|
const endpoint = network.endpoint || 'https://workspace-endpoint.openagents.org';
|
|
434
443
|
|
|
@@ -442,7 +451,7 @@ class Daemon {
|
|
|
442
451
|
endpoint,
|
|
443
452
|
agentType,
|
|
444
453
|
openclawAgentId: agentCfg.openclaw_agent_id || 'main',
|
|
445
|
-
disabledModules:
|
|
454
|
+
disabledModules: skillsToDisabledModules(agentCfg.skills),
|
|
446
455
|
agentEnv: this._buildAgentEnv(agentCfg),
|
|
447
456
|
workingDir: agentCfg.path || undefined,
|
|
448
457
|
toolMode: agentCfg.tool_mode || 'skills',
|
package/src/index.js
CHANGED
|
@@ -126,6 +126,17 @@ class AgentConnector {
|
|
|
126
126
|
return { success: true };
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Delete the entire type-level env file (~/.openagents/env/<type>.env).
|
|
131
|
+
* Used by the launcher's "wipe saved credentials" path on uninstall so
|
|
132
|
+
* sensitive values (API keys) don't survive a reinstall as surprise
|
|
133
|
+
* pre-filled defaults in the setup wizard.
|
|
134
|
+
*/
|
|
135
|
+
deleteAgentEnv(agentType) {
|
|
136
|
+
this.env.delete(agentType);
|
|
137
|
+
return { success: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
saveAgentInstanceEnv(agentName, env) {
|
|
130
141
|
const agent = this.config.getAgent(agentName);
|
|
131
142
|
if (!agent) throw new Error(`Agent '${agentName}' not found`);
|
package/src/installer.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync, exec } = require('child_process');
|
|
7
|
-
const { whichBinary, getEnhancedEnv, getRuntimePrefix } = require('./paths');
|
|
7
|
+
const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache } = require('./paths');
|
|
8
8
|
const { EnvManager } = require('./env');
|
|
9
9
|
|
|
10
10
|
const STATUS_CACHE_TTL_MS = 10000;
|
|
@@ -90,7 +90,15 @@ class Installer {
|
|
|
90
90
|
// Fallback: check if binary exists on PATH (system install)
|
|
91
91
|
const binaryPath = this._whichBinary(agentType);
|
|
92
92
|
if (!binaryPath) {
|
|
93
|
-
|
|
93
|
+
// If a successful install wrote a marker, surface installed=true even
|
|
94
|
+
// when binary detection can't (yet) see the freshly-installed CLI —
|
|
95
|
+
// e.g. PATH caches not yet picking up a brand-new ~/.cursor/bin. The
|
|
96
|
+
// marker is the ground truth of what we just installed; UI was
|
|
97
|
+
// silently showing "not installed" right after a successful install
|
|
98
|
+
// because earlier code aggressively deleted the marker here.
|
|
99
|
+
if (this._hasMarker(agentType)) {
|
|
100
|
+
return { installed: true, managed: true, location: 'marker' };
|
|
101
|
+
}
|
|
94
102
|
return { installed: false, managed: false, location: null };
|
|
95
103
|
}
|
|
96
104
|
|
|
@@ -418,6 +426,8 @@ class Installer {
|
|
|
418
426
|
cmd = rawCmd; // pip commands stay as-is
|
|
419
427
|
}
|
|
420
428
|
|
|
429
|
+
cmd = this._wrapForWindowsShell(cmd);
|
|
430
|
+
|
|
421
431
|
if (onData) onData(`$ ${cmd}\n\n`);
|
|
422
432
|
|
|
423
433
|
const env = this._buildShellEnv();
|
|
@@ -579,6 +589,8 @@ class Installer {
|
|
|
579
589
|
cmd = this._resolveNpmCommand(args);
|
|
580
590
|
}
|
|
581
591
|
|
|
592
|
+
cmd = this._wrapForWindowsShell(cmd);
|
|
593
|
+
|
|
582
594
|
if (onData) onData(`$ ${cmd}\n\n`);
|
|
583
595
|
|
|
584
596
|
const env = this._buildShellEnv();
|
|
@@ -708,6 +720,12 @@ class Installer {
|
|
|
708
720
|
fs.mkdirSync(this.markersDir, { recursive: true });
|
|
709
721
|
fs.writeFileSync(path.join(this.markersDir, agentType), '', 'utf-8');
|
|
710
722
|
} catch {}
|
|
723
|
+
|
|
724
|
+
// Drop the 30s PATH/whichBinary caches so the very next getInstallInfo
|
|
725
|
+
// sees the freshly-created bin dir (e.g. ~/.cursor/bin) instead of the
|
|
726
|
+
// pre-install snapshot. Without this, install completes but UI keeps
|
|
727
|
+
// showing "not installed" until the cache expires.
|
|
728
|
+
try { clearBinaryLookupCache(); } catch {}
|
|
711
729
|
}
|
|
712
730
|
|
|
713
731
|
_markUninstalled(agentType) {
|
|
@@ -727,6 +745,9 @@ class Installer {
|
|
|
727
745
|
const markerFile = path.join(this.markersDir, agentType);
|
|
728
746
|
if (fs.existsSync(markerFile)) fs.unlinkSync(markerFile);
|
|
729
747
|
} catch {}
|
|
748
|
+
|
|
749
|
+
// Symmetric with _markInstalled: invalidate so detection re-runs cleanly.
|
|
750
|
+
try { clearBinaryLookupCache(); } catch {}
|
|
730
751
|
}
|
|
731
752
|
|
|
732
753
|
// -- Shell env + exec --
|
|
@@ -1040,6 +1061,24 @@ class Installer {
|
|
|
1040
1061
|
return `${npmBin} ${args}`;
|
|
1041
1062
|
}
|
|
1042
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* Wrap a command in `powershell.exe -Command "..."` when (a) we're on Windows
|
|
1066
|
+
* and (b) the command uses PowerShell-only tokens like `irm`, `iex`,
|
|
1067
|
+
* `Invoke-RestMethod`, etc. The launcher's install shell is cmd.exe, which
|
|
1068
|
+
* doesn't recognize these aliases — without wrapping, e.g. Cursor's
|
|
1069
|
+
* `irm '…' | iex` exits 255. Skips commands already prefixed with
|
|
1070
|
+
* `powershell`/`powershell.exe` so we don't double-wrap.
|
|
1071
|
+
*/
|
|
1072
|
+
_wrapForWindowsShell(cmd) {
|
|
1073
|
+
if (!cmd || process.platform !== 'win32') return cmd;
|
|
1074
|
+
const trimmed = cmd.trimStart();
|
|
1075
|
+
if (/^("[^"]*\\)?powershell(\.exe)?["']?\s/i.test(trimmed)) return cmd;
|
|
1076
|
+
const psTokens = /\b(irm|iwr|iex|Invoke-RestMethod|Invoke-WebRequest|Invoke-Expression|Expand-Archive|Get-[A-Z]\w*|Set-[A-Z]\w*)\b/;
|
|
1077
|
+
if (!psTokens.test(cmd)) return cmd;
|
|
1078
|
+
const escaped = cmd.replace(/"/g, '\\"');
|
|
1079
|
+
return `powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "${escaped}"`;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1043
1082
|
_execShell(cmd, timeoutMs = 300000) {
|
|
1044
1083
|
return new Promise((resolve, reject) => {
|
|
1045
1084
|
const env = this._buildShellEnv();
|
|
@@ -1049,7 +1088,8 @@ class Installer {
|
|
|
1049
1088
|
shell = env.ComSpec || 'C:\\Windows\\System32\\cmd.exe';
|
|
1050
1089
|
}
|
|
1051
1090
|
|
|
1052
|
-
|
|
1091
|
+
const finalCmd = this._wrapForWindowsShell(cmd);
|
|
1092
|
+
exec(finalCmd, {
|
|
1053
1093
|
encoding: 'utf-8',
|
|
1054
1094
|
timeout: timeoutMs,
|
|
1055
1095
|
shell,
|