@openagents-org/agent-launcher 0.2.150 → 0.2.152
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 +2 -2
- package/registry.json +95 -0
- package/src/adapters/aider.js +5 -3
- package/src/adapters/base.js +34 -6
- package/src/adapters/claude.js +358 -158
- package/src/adapters/codex.js +58 -4
- package/src/adapters/cursor.js +21 -3
- package/src/adapters/decision-log.js +181 -0
- package/src/adapters/index.js +4 -1
- package/src/adapters/mini.js +572 -0
- package/src/adapters/openclaw.js +39 -2
- package/src/adapters/opencode.js +58 -10
- package/src/adapters/workspace-prompt.js +255 -29
- package/src/mcp-server.js +112 -0
- package/src/skill-installer.js +218 -0
- package/src/workspace-client.js +59 -14
package/src/adapters/codex.js
CHANGED
|
@@ -324,12 +324,15 @@ class CodexAdapter extends BaseAdapter {
|
|
|
324
324
|
this._saveSessions();
|
|
325
325
|
continue;
|
|
326
326
|
} else {
|
|
327
|
-
|
|
327
|
+
// Surface the actual reason (turn.failed message, stderr, exit code)
|
|
328
|
+
// instead of a generic "no response" — mirrors the OpenCode adapter
|
|
329
|
+
// so auth/model/network problems are actionable from the chat.
|
|
330
|
+
await this._sendRunFailure(msgChannel, result);
|
|
328
331
|
return;
|
|
329
332
|
}
|
|
330
333
|
} catch (e) {
|
|
331
334
|
this._log(`Error in subprocess: ${e.message}`);
|
|
332
|
-
await this.sendError(msgChannel,
|
|
335
|
+
await this.sendError(msgChannel, `⚠️ **Codex couldn't run** — ${CodexAdapter._redact(e.message)}`);
|
|
333
336
|
return;
|
|
334
337
|
}
|
|
335
338
|
}
|
|
@@ -351,6 +354,7 @@ class CodexAdapter extends BaseAdapter {
|
|
|
351
354
|
let hasToolUseSinceLastText = false;
|
|
352
355
|
let lineBuffer = '';
|
|
353
356
|
let stderrBuf = '';
|
|
357
|
+
let lastErrorMessage = '';
|
|
354
358
|
let _pendingLines = Promise.resolve();
|
|
355
359
|
|
|
356
360
|
if (proc.stderr) {
|
|
@@ -407,7 +411,12 @@ class CodexAdapter extends BaseAdapter {
|
|
|
407
411
|
} else if (eventType === 'turn.failed') {
|
|
408
412
|
const error = event.error || {};
|
|
409
413
|
const errMsg = error.message || JSON.stringify(error);
|
|
414
|
+
lastErrorMessage = errMsg;
|
|
410
415
|
this._log(`Turn failed: ${errMsg}`);
|
|
416
|
+
} else if (eventType === 'error') {
|
|
417
|
+
const errMsg = event.message || JSON.stringify(event);
|
|
418
|
+
lastErrorMessage = errMsg;
|
|
419
|
+
this._log(`Error event: ${errMsg}`);
|
|
411
420
|
}
|
|
412
421
|
};
|
|
413
422
|
|
|
@@ -442,6 +451,7 @@ class CodexAdapter extends BaseAdapter {
|
|
|
442
451
|
responseText: responseTexts.join('\n').trim(),
|
|
443
452
|
exitCode: code,
|
|
444
453
|
stderr: stderrBuf,
|
|
454
|
+
errorMessage: lastErrorMessage,
|
|
445
455
|
});
|
|
446
456
|
});
|
|
447
457
|
|
|
@@ -452,6 +462,50 @@ class CodexAdapter extends BaseAdapter {
|
|
|
452
462
|
});
|
|
453
463
|
}
|
|
454
464
|
|
|
465
|
+
// ------------------------------------------------------------------
|
|
466
|
+
// Failure reporting
|
|
467
|
+
// ------------------------------------------------------------------
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Post a user-visible failure carrying the actual reason a run produced no
|
|
471
|
+
* reply, instead of a generic "No response generated".
|
|
472
|
+
*/
|
|
473
|
+
async _sendRunFailure(msgChannel, result) {
|
|
474
|
+
const detail = CodexAdapter._failureDetail(result);
|
|
475
|
+
const body = detail
|
|
476
|
+
? `Codex failed to complete this run.\n\n> ${detail}`
|
|
477
|
+
: 'Codex finished without producing a reply. Please try again.';
|
|
478
|
+
await this.sendError(msgChannel, `⚠️ **Codex couldn't run** — ${body}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Pick the most informative failure detail from a run result:
|
|
483
|
+
* turn.failed / error event message → stderr tail → exit code. Redacted.
|
|
484
|
+
*/
|
|
485
|
+
static _failureDetail({ errorMessage, stderr, exitCode } = {}) {
|
|
486
|
+
const stderrTail = String(stderr || '').trim().split('\n').slice(-5).join('\n').trim();
|
|
487
|
+
const raw = String(errorMessage || '').trim()
|
|
488
|
+
|| stderrTail
|
|
489
|
+
|| (exitCode ? `codex exited with code ${exitCode}` : '');
|
|
490
|
+
return CodexAdapter._redact(raw).slice(0, 500).trim();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Redact secrets (keys, tokens, bearer/authorization, query secrets) from diagnostics. */
|
|
494
|
+
static _redact(s) {
|
|
495
|
+
let out = String(s == null ? '' : s);
|
|
496
|
+
out = out
|
|
497
|
+
.replace(/\bsk-[A-Za-z0-9_-]{6,}/g, 'sk-[REDACTED]')
|
|
498
|
+
.replace(/\b(?:github_pat|gh[pousr])_[A-Za-z0-9_]{10,}/g, '[REDACTED_TOKEN]')
|
|
499
|
+
.replace(/\bxox[baprs]-[A-Za-z0-9-]{8,}/g, '[REDACTED_TOKEN]')
|
|
500
|
+
.replace(/\bAKIA[0-9A-Z]{12,}/g, '[REDACTED_KEY]')
|
|
501
|
+
.replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}/g, '[REDACTED_JWT]')
|
|
502
|
+
.replace(/(authorization|api[_-]?key|x-api-key|token|bearer|secret|password|passwd)(["'\s:=]+)([^\s"',}]+)/gi,
|
|
503
|
+
(m, k, sep) => `${k}${sep}[REDACTED]`)
|
|
504
|
+
.replace(/([?&](?:api[_-]?key|key|token|access_token)=)[^&\s"']+/gi, '$1[REDACTED]')
|
|
505
|
+
.replace(/\b[A-Za-z0-9_-]{40,}\b/g, '[REDACTED]');
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
|
|
455
509
|
// ------------------------------------------------------------------
|
|
456
510
|
// Direct HTTP mode (fallback when CLI not available)
|
|
457
511
|
// ------------------------------------------------------------------
|
|
@@ -467,11 +521,11 @@ class CodexAdapter extends BaseAdapter {
|
|
|
467
521
|
}
|
|
468
522
|
await this.sendResponse(msgChannel, responseText);
|
|
469
523
|
} else {
|
|
470
|
-
await this.
|
|
524
|
+
await this._sendRunFailure(msgChannel, {});
|
|
471
525
|
}
|
|
472
526
|
} catch (e) {
|
|
473
527
|
this._log(`Error in direct API: ${e.message}`);
|
|
474
|
-
await this.sendError(msgChannel,
|
|
528
|
+
await this.sendError(msgChannel, `⚠️ **Codex couldn't run** — ${CodexAdapter._redact(e.message)}`);
|
|
475
529
|
}
|
|
476
530
|
}
|
|
477
531
|
|
package/src/adapters/cursor.js
CHANGED
|
@@ -16,7 +16,7 @@ const { execSync, spawn } = require('child_process');
|
|
|
16
16
|
|
|
17
17
|
const BaseAdapter = require('./base');
|
|
18
18
|
const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
|
|
19
|
-
const { buildCursorSkillMd } = require('./workspace-prompt');
|
|
19
|
+
const { buildCursorSkillMd, workspaceSkillName } = require('./workspace-prompt');
|
|
20
20
|
const { defaultAgentWorkdir, whichBinary, whereBinary } = require('../paths');
|
|
21
21
|
|
|
22
22
|
const IS_WINDOWS = process.platform === 'win32';
|
|
@@ -308,7 +308,12 @@ class CursorAdapter extends BaseAdapter {
|
|
|
308
308
|
const workDir = this.workingDir || defaultAgentWorkdir(this.agentName);
|
|
309
309
|
const skillDir = path.join(workDir, '.cursor', 'skills');
|
|
310
310
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
311
|
-
|
|
311
|
+
// Per-agent filename: agents sharing a working directory used to clobber
|
|
312
|
+
// one shared openagents-workspace.md, so uploads carried the identity of
|
|
313
|
+
// whichever agent wrote the file last. Drop that legacy file so a stale
|
|
314
|
+
// copy (with another agent's embedded identity) can never be read again.
|
|
315
|
+
const skillFile = path.join(skillDir, `${workspaceSkillName(this.agentName)}.md`);
|
|
316
|
+
try { fs.unlinkSync(path.join(skillDir, 'openagents-workspace.md')); } catch {}
|
|
312
317
|
|
|
313
318
|
const skillContent = buildCursorSkillMd({
|
|
314
319
|
endpoint: this.endpoint,
|
|
@@ -330,7 +335,20 @@ class CursorAdapter extends BaseAdapter {
|
|
|
330
335
|
throw new Error('Cursor CLI not found. Install with: curl https://cursor.com/install -fsSL | bash');
|
|
331
336
|
}
|
|
332
337
|
|
|
333
|
-
|
|
338
|
+
// Cursor has no system-prompt flag, so the CLI would otherwise learn its
|
|
339
|
+
// identity ONLY from whichever workspace skill it happens to open. With a
|
|
340
|
+
// shared working directory several agents' skills coexist (same
|
|
341
|
+
// description, different embedded identity), so every spawn must pin who
|
|
342
|
+
// this agent is and exactly which skill file is its interface — otherwise
|
|
343
|
+
// it can read a sibling's skill and upload under that agent's name.
|
|
344
|
+
const identityHeader =
|
|
345
|
+
`[workspace] You are agent '${this.agentName}'. For ANY workspace ` +
|
|
346
|
+
`operation (messages, files, agents, browser) read and follow ONLY the ` +
|
|
347
|
+
`'${workspaceSkillName(this.agentName)}' skill. Other ` +
|
|
348
|
+
`openagents-workspace-* skills belong to OTHER agents and carry the ` +
|
|
349
|
+
`wrong identity — never use them.\n\n`;
|
|
350
|
+
|
|
351
|
+
const cmd = [agentBin, '-p', identityHeader + prompt, '--output-format', 'stream-json', '--trust', '--force'];
|
|
334
352
|
|
|
335
353
|
// Model selection
|
|
336
354
|
const model = (this.agentEnv || process.env).CURSOR_MODEL;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the Claude adapter's decision log ("constraint pinning").
|
|
3
|
+
*
|
|
4
|
+
* The decision log is a per-channel knowledge entry that records every
|
|
5
|
+
* decision the user has confirmed. The adapter re-reads it on every message
|
|
6
|
+
* and injects it into the CLI system prompt — the only position that survives
|
|
7
|
+
* both the CLI's auto-compaction and a session reset. Everything here is
|
|
8
|
+
* side-effect free so it can be unit tested without an adapter instance.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
/** Max characters a single recap line may occupy before being cut. */
|
|
16
|
+
const RECAP_LINE_MAX_CHARS = 2000;
|
|
17
|
+
|
|
18
|
+
/** Default budget for the pinned-decisions block inside the system prompt. */
|
|
19
|
+
const PINNED_DECISIONS_MAX_CHARS = 4000;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Canonical knowledge-entry title for a channel's decision log. The slug the
|
|
23
|
+
* backend derives from it is NOT stable (collisions get a numeric suffix), so
|
|
24
|
+
* lookups must match on this exact title, never on the slug.
|
|
25
|
+
*/
|
|
26
|
+
function decisionLogTitle(channelName) {
|
|
27
|
+
return `Decisions for channel ${channelName}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Stable hash of the decision log content. null/undefined and '' hash
|
|
32
|
+
* identically so "no entry yet" never reads as a change.
|
|
33
|
+
*/
|
|
34
|
+
function hashDecisions(content) {
|
|
35
|
+
return crypto.createHash('sha256').update(String(content || '').trim(), 'utf-8').digest('hex');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fingerprint of everything the system prompt pins from the decision log —
|
|
40
|
+
* the entry id AND the content — used to detect that a persistent CLI
|
|
41
|
+
* process was spawned with an outdated prompt. Content alone is not enough:
|
|
42
|
+
* a deleted-and-recreated log can carry identical content under a new id,
|
|
43
|
+
* and a prompt still holding the old id would send updates to a dead entry.
|
|
44
|
+
*/
|
|
45
|
+
function decisionFingerprint(entryId, content) {
|
|
46
|
+
return crypto.createHash('sha256')
|
|
47
|
+
.update(`${entryId || ''}\u0000${String(content || '').trim()}`, 'utf-8')
|
|
48
|
+
.digest('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Pick the channel's decision entry from a knowledge listing: exact-title
|
|
53
|
+
* matches only, earliest created_at wins (concurrent creates produce
|
|
54
|
+
* duplicates — the earliest is the one other turns have been updating).
|
|
55
|
+
* Returns { entry, duplicates } where duplicates counts the extra matches.
|
|
56
|
+
*/
|
|
57
|
+
function pickDecisionEntry(entries, channelName) {
|
|
58
|
+
const title = decisionLogTitle(channelName);
|
|
59
|
+
const matches = (entries || []).filter((e) => e && e.title === title);
|
|
60
|
+
if (matches.length === 0) return { entry: null, duplicates: 0 };
|
|
61
|
+
const sorted = matches.slice().sort((a, b) => {
|
|
62
|
+
// ISO-8601 strings compare chronologically as strings; entries without a
|
|
63
|
+
// timestamp are anomalous and sort last so a dated original wins.
|
|
64
|
+
const ka = a.created_at || '\uffff';
|
|
65
|
+
const kb = b.created_at || '\uffff';
|
|
66
|
+
return ka < kb ? -1 : ka > kb ? 1 : 0;
|
|
67
|
+
});
|
|
68
|
+
return { entry: sorted[0], duplicates: matches.length - 1 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fit the decision log into a character budget without ever cutting a line
|
|
73
|
+
* in half. Over budget, whole lines are kept from the head (earliest
|
|
74
|
+
* decisions) and the tail (latest decisions) with the middle dropped, so
|
|
75
|
+
* neither end of the log silently disappears.
|
|
76
|
+
* Returns { text, truncated, omitted }.
|
|
77
|
+
*/
|
|
78
|
+
function renderPinnedDecisions(content, { maxChars = PINNED_DECISIONS_MAX_CHARS } = {}) {
|
|
79
|
+
const text = String(content || '').trim();
|
|
80
|
+
if (!text) return { text: '', truncated: false, omitted: 0 };
|
|
81
|
+
if (text.length <= maxChars) return { text, truncated: false, omitted: 0 };
|
|
82
|
+
|
|
83
|
+
const lines = text.split('\n');
|
|
84
|
+
// Reserve room for the omission marker inserted between head and tail.
|
|
85
|
+
const budget = Math.max(maxChars - 80, 0);
|
|
86
|
+
const headBudget = Math.floor(budget / 2);
|
|
87
|
+
|
|
88
|
+
const head = [];
|
|
89
|
+
let used = 0;
|
|
90
|
+
let i = 0;
|
|
91
|
+
for (; i < lines.length; i++) {
|
|
92
|
+
const cost = lines[i].length + 1;
|
|
93
|
+
if (used + cost > headBudget) break;
|
|
94
|
+
head.push(lines[i]);
|
|
95
|
+
used += cost;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const tail = [];
|
|
99
|
+
let j = lines.length - 1;
|
|
100
|
+
for (; j >= i; j--) {
|
|
101
|
+
const cost = lines[j].length + 1;
|
|
102
|
+
if (used + cost > budget) break;
|
|
103
|
+
tail.unshift(lines[j]);
|
|
104
|
+
used += cost;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const omitted = lines.length - head.length - tail.length;
|
|
108
|
+
if (omitted <= 0) {
|
|
109
|
+
// Everything fit after all (short middle) — no marker needed.
|
|
110
|
+
return { text: [...head, ...tail].join('\n'), truncated: false, omitted: 0 };
|
|
111
|
+
}
|
|
112
|
+
const marker = `[… ${omitted} middle line(s) omitted — read the decision log entry for the full list …]`;
|
|
113
|
+
return {
|
|
114
|
+
text: [...head, marker, ...tail].join('\n'),
|
|
115
|
+
truncated: true,
|
|
116
|
+
omitted,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** A message qualifies for the recap when it is real chat with content. */
|
|
121
|
+
function isRecapEligible(msg, currentMessage) {
|
|
122
|
+
if (!msg) return false;
|
|
123
|
+
const mt = msg.messageType || 'chat';
|
|
124
|
+
if (mt === 'status' || mt === 'thinking' || mt === 'loading') return false;
|
|
125
|
+
const text = (msg.content || '').trim();
|
|
126
|
+
if (!text) return false;
|
|
127
|
+
if (text === currentMessage) return false;
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatRecapLine(msg) {
|
|
132
|
+
const text = (msg.content || '').trim();
|
|
133
|
+
const who = msg.senderType === 'human'
|
|
134
|
+
? (msg.senderName || 'user')
|
|
135
|
+
: (msg.senderName || 'agent');
|
|
136
|
+
const cut = text.length > RECAP_LINE_MAX_CHARS
|
|
137
|
+
? text.slice(0, RECAP_LINE_MAX_CHARS) + '…'
|
|
138
|
+
: text;
|
|
139
|
+
return `[${who}] ${cut}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build recap lines from the channel's opening messages (fetched ascending)
|
|
144
|
+
* and its most recent ones (fetched descending, already reversed to
|
|
145
|
+
* chronological). Head keeps the original requirement, tail keeps the live
|
|
146
|
+
* discussion; overlap is removed by event ID. An omission marker is inserted
|
|
147
|
+
* only when the two windows do not overlap, i.e. messages in between were
|
|
148
|
+
* actually dropped.
|
|
149
|
+
*/
|
|
150
|
+
function sampleRecap(headMsgs, tailMsgs, currentMessage, { headKeep = 5, tailKeep = 15 } = {}) {
|
|
151
|
+
const head = (headMsgs || [])
|
|
152
|
+
.filter((m) => isRecapEligible(m, currentMessage))
|
|
153
|
+
.slice(0, headKeep);
|
|
154
|
+
const tail = (tailMsgs || [])
|
|
155
|
+
.filter((m) => isRecapEligible(m, currentMessage))
|
|
156
|
+
.slice(-tailKeep);
|
|
157
|
+
|
|
158
|
+
const headIds = new Set(head.map((m) => m.messageId).filter(Boolean));
|
|
159
|
+
const tailDeduped = tail.filter((m) => !m.messageId || !headIds.has(m.messageId));
|
|
160
|
+
const windowsOverlap = tailDeduped.length < tail.length;
|
|
161
|
+
|
|
162
|
+
const lines = head.map(formatRecapLine);
|
|
163
|
+
if (head.length > 0 && tailDeduped.length > 0 && !windowsOverlap) {
|
|
164
|
+
lines.push('[… earlier messages omitted …]');
|
|
165
|
+
}
|
|
166
|
+
lines.push(...tailDeduped.map(formatRecapLine));
|
|
167
|
+
return lines;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
PINNED_DECISIONS_MAX_CHARS,
|
|
172
|
+
RECAP_LINE_MAX_CHARS,
|
|
173
|
+
decisionLogTitle,
|
|
174
|
+
hashDecisions,
|
|
175
|
+
decisionFingerprint,
|
|
176
|
+
pickDecisionEntry,
|
|
177
|
+
renderPinnedDecisions,
|
|
178
|
+
isRecapEligible,
|
|
179
|
+
formatRecapLine,
|
|
180
|
+
sampleRecap,
|
|
181
|
+
};
|
package/src/adapters/index.js
CHANGED
|
@@ -19,6 +19,7 @@ const GooseAdapter = require('./goose');
|
|
|
19
19
|
const CopilotAdapter = require('./copilot');
|
|
20
20
|
const ClineAdapter = require('./cline');
|
|
21
21
|
const AmpAdapter = require('./amp');
|
|
22
|
+
const MiniSweAgentAdapter = require('./mini');
|
|
22
23
|
|
|
23
24
|
const ADAPTER_MAP = {
|
|
24
25
|
openclaw: OpenClawAdapter,
|
|
@@ -35,11 +36,12 @@ const ADAPTER_MAP = {
|
|
|
35
36
|
copilot: CopilotAdapter,
|
|
36
37
|
cline: ClineAdapter,
|
|
37
38
|
amp: AmpAdapter,
|
|
39
|
+
'mini-swe-agent': MiniSweAgentAdapter,
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
43
|
* Create an adapter instance for the given agent type.
|
|
42
|
-
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp)
|
|
44
|
+
* @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor, hermes, gemini, kimi, aider, goose, copilot, cline, amp, mini-swe-agent)
|
|
43
45
|
* @param {object} opts - Adapter constructor options
|
|
44
46
|
* @returns {BaseAdapter}
|
|
45
47
|
*/
|
|
@@ -67,6 +69,7 @@ module.exports = {
|
|
|
67
69
|
CopilotAdapter,
|
|
68
70
|
ClineAdapter,
|
|
69
71
|
AmpAdapter,
|
|
72
|
+
MiniSweAgentAdapter,
|
|
70
73
|
createAdapter,
|
|
71
74
|
ADAPTER_MAP,
|
|
72
75
|
};
|