@openagents-org/agent-launcher 0.2.173 → 0.2.176
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/icons/commandcode.svg +3 -0
- package/icons/kimi.svg +5 -10
- package/package.json +1 -1
- package/registry.json +261 -36
- package/src/adapters/base.js +19 -0
- package/src/adapters/codex.js +41 -1
- package/src/adapters/commandcode-stream.js +405 -0
- package/src/adapters/commandcode.js +853 -0
- package/src/adapters/hermes.js +56 -0
- package/src/adapters/index.js +4 -1
- package/src/adapters/kimi-stream.js +360 -0
- package/src/adapters/kimi.js +724 -13
- package/src/adapters/openclaw.js +28 -2
- package/src/adapters/opencode.js +86 -0
- package/src/adapters/pi-stream.js +28 -0
- package/src/adapters/pi.js +21 -2
- package/src/adapters/workspace-prompt.js +42 -8
- package/src/cli.js +182 -6
- package/src/config.js +13 -2
- package/src/daemon.js +192 -59
- package/src/env.js +22 -2
- package/src/index.js +13 -0
- package/src/installer.js +69 -1
- package/src/paths.js +315 -11
- package/src/probe.js +29 -8
- package/src/registry.js +16 -3
package/src/adapters/openclaw.js
CHANGED
|
@@ -212,6 +212,16 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
212
212
|
|
|
213
213
|
async _handleMessage(msg) {
|
|
214
214
|
let content = (msg.content || '').trim();
|
|
215
|
+
// Strip a leading mention of OURSELVES. The workspace UI prefixes targeted
|
|
216
|
+
// messages with "@<agentName>", but OpenClaw's inner agent doesn't know its
|
|
217
|
+
// workspace name — it sees a message addressed to a stranger and its
|
|
218
|
+
// group-chat heuristic answers NO_REPLY (empty payloads, silent agent).
|
|
219
|
+
// Mentions of other participants are left intact.
|
|
220
|
+
const selfMention = new RegExp(
|
|
221
|
+
'^\\s*@' + String(this.agentName || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b[,:]?\\s*',
|
|
222
|
+
'i',
|
|
223
|
+
);
|
|
224
|
+
content = content.replace(selfMention, '').trim();
|
|
215
225
|
const attachments = msg.attachments || [];
|
|
216
226
|
|
|
217
227
|
// Append attachment info
|
|
@@ -509,18 +519,34 @@ class OpenClawAdapter extends BaseAdapter {
|
|
|
509
519
|
const payloads = data.payloads || [];
|
|
510
520
|
this._log(`CLI parsed: ${payloads.length} payloads, keys=${payloads.map(p=>Object.keys(p).join('/')).join(', ')}, text=${payloads.map(p=>(p.text||'').slice(0,50)).join('|')}`);
|
|
511
521
|
if (payloads.length > 0) {
|
|
512
|
-
|
|
522
|
+
// NO_REPLY is OpenClaw's "deliberately silent" sentinel, not an
|
|
523
|
+
// answer — surface it as no-response, same as empty payloads.
|
|
524
|
+
const texts = payloads
|
|
525
|
+
.filter(p => p.text && String(p.text).trim() !== 'NO_REPLY')
|
|
526
|
+
.map(p => p.text);
|
|
513
527
|
if (texts.length > 0) {
|
|
514
528
|
resolve(texts.join('\n\n'));
|
|
515
529
|
return;
|
|
516
530
|
}
|
|
531
|
+
resolve('');
|
|
532
|
+
return;
|
|
517
533
|
}
|
|
534
|
+
// A parsed envelope with no text payloads means the run completed but
|
|
535
|
+
// OpenClaw produced no reply. The raw envelope (sessionFile, usage,
|
|
536
|
+
// contextBudgetStatus, …) must never reach the chat — that is exactly
|
|
537
|
+
// what users saw as a wall of JSON. Resolve empty so the caller posts
|
|
538
|
+
// its clean "no response" notice instead.
|
|
539
|
+
resolve('');
|
|
540
|
+
return;
|
|
518
541
|
} catch (e) {
|
|
519
542
|
this._log(`CLI JSON parse error: ${e.message}`);
|
|
520
543
|
}
|
|
521
544
|
}
|
|
522
545
|
|
|
523
|
-
// Fallback: return non-diagnostic text
|
|
546
|
+
// Fallback: return non-diagnostic text. If the output contains an
|
|
547
|
+
// envelope we failed to parse, suppress it the same way — garbage JSON in
|
|
548
|
+
// chat is worse than the caller's "no response" notice.
|
|
549
|
+
if (text.includes('"payloads"')) { resolve(''); return; }
|
|
524
550
|
const cleanLines = text.split('\n').filter(l =>
|
|
525
551
|
!l.includes('[diagnostic]') && !l.includes('[agent/embedded]') && !l.includes('Registered plugin')
|
|
526
552
|
).map(l => l.trim()).filter(Boolean);
|
package/src/adapters/opencode.js
CHANGED
|
@@ -45,6 +45,16 @@ const OPENCODE_TESTED_MAX_VERSION = '1.17.11';
|
|
|
45
45
|
// window across channels.
|
|
46
46
|
const VERSION_PROBE_TTL_MS = 60000;
|
|
47
47
|
|
|
48
|
+
// Provider id this adapter writes into the run directory's
|
|
49
|
+
// .opencode/opencode.json when the agent points at a custom OpenAI-compatible
|
|
50
|
+
// gateway. opencode's built-in `openai` provider validates model names against
|
|
51
|
+
// its catalog (a gateway model like `deepseek-4-flash` dies with
|
|
52
|
+
// ProviderModelNotFoundError before any request is sent) and speaks the
|
|
53
|
+
// Responses API (`/v1/responses`), which most gateways don't serve (404 →
|
|
54
|
+
// endless retry). Declaring the gateway as an @ai-sdk/openai-compatible
|
|
55
|
+
// provider keeps requests on `/chat/completions` and skips catalog validation.
|
|
56
|
+
const CUSTOM_PROVIDER_ID = 'openagents-gateway';
|
|
57
|
+
|
|
48
58
|
// Short, de-identified, actionable user messages per failure category. These
|
|
49
59
|
// must NEVER default to "run auth login": only credential_missing / auth_failed
|
|
50
60
|
// mention authentication. Diagnostics (exit code, signal, redacted stdout/
|
|
@@ -670,6 +680,10 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
670
680
|
if (!model) return '';
|
|
671
681
|
// Already provider-qualified (e.g. "openai/gpt-4o", "anthropic/claude-…").
|
|
672
682
|
if (model.includes('/')) return model;
|
|
683
|
+
// A custom gateway's models aren't in opencode's openai catalog — route
|
|
684
|
+
// them through the generated openai-compatible provider (see
|
|
685
|
+
// _ensureCustomProviderConfig).
|
|
686
|
+
if (this._customBaseUrl()) return `${CUSTOM_PROVIDER_ID}/${model}`;
|
|
673
687
|
// Infer the provider from which key/base URL the env resolved to. The
|
|
674
688
|
// registry maps LLM_* → OPENAI_* for OpenAI-compatible endpoints, so
|
|
675
689
|
// "openai" is the right default; only switch for an Anthropic endpoint.
|
|
@@ -680,6 +694,73 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
680
694
|
return `${provider}/${model}`;
|
|
681
695
|
}
|
|
682
696
|
|
|
697
|
+
/**
|
|
698
|
+
* The configured base URL when it points at a custom OpenAI-compatible
|
|
699
|
+
* gateway, '' for the real OpenAI / Anthropic endpoints (opencode's built-in
|
|
700
|
+
* providers handle those natively).
|
|
701
|
+
*/
|
|
702
|
+
_customBaseUrl() {
|
|
703
|
+
const env = this.agentEnv || process.env;
|
|
704
|
+
const raw = (env.OPENAI_BASE_URL || env.LLM_BASE_URL || '').trim();
|
|
705
|
+
if (!raw) return '';
|
|
706
|
+
let host = '';
|
|
707
|
+
try { host = new URL(raw).hostname.toLowerCase(); } catch { return ''; }
|
|
708
|
+
if (host === 'openai.com' || host.endsWith('.openai.com')) return '';
|
|
709
|
+
if (host === 'anthropic.com' || host.endsWith('.anthropic.com')) return '';
|
|
710
|
+
return raw;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Declare the custom gateway in `<runCwd>/.opencode/opencode.json` so the
|
|
715
|
+
* `${CUSTOM_PROVIDER_ID}/<model>` id from _resolveModel actually resolves.
|
|
716
|
+
* Merges into an existing config (only our provider entry is touched); a
|
|
717
|
+
* file that exists but doesn't parse is left alone — never clobber a config
|
|
718
|
+
* the user hand-wrote. The API key is referenced as {env:OPENAI_API_KEY}
|
|
719
|
+
* (resolved by opencode from the spawn env) so it is never written to disk.
|
|
720
|
+
*/
|
|
721
|
+
_ensureCustomProviderConfig(runCwd) {
|
|
722
|
+
const baseUrl = this._customBaseUrl();
|
|
723
|
+
if (!baseUrl) return;
|
|
724
|
+
const env = this.agentEnv || process.env;
|
|
725
|
+
const model = (env.OPENCODE_MODEL || env.LLM_MODEL || '').trim();
|
|
726
|
+
if (!model || model.includes('/')) return;
|
|
727
|
+
|
|
728
|
+
const file = path.join(runCwd, '.opencode', 'opencode.json');
|
|
729
|
+
let cfg = {};
|
|
730
|
+
try {
|
|
731
|
+
cfg = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
732
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) cfg = {};
|
|
733
|
+
} catch (e) {
|
|
734
|
+
if (e && e.code !== 'ENOENT') {
|
|
735
|
+
this._log(`Not updating ${file} — existing file is unreadable or not valid JSON`);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const before = JSON.stringify(cfg);
|
|
741
|
+
if (!cfg.$schema) cfg.$schema = 'https://opencode.ai/config.json';
|
|
742
|
+
if (!cfg.provider || typeof cfg.provider !== 'object') cfg.provider = {};
|
|
743
|
+
const prev = (typeof cfg.provider[CUSTOM_PROVIDER_ID] === 'object' && cfg.provider[CUSTOM_PROVIDER_ID]) || {};
|
|
744
|
+
cfg.provider[CUSTOM_PROVIDER_ID] = {
|
|
745
|
+
...prev,
|
|
746
|
+
npm: '@ai-sdk/openai-compatible',
|
|
747
|
+
name: prev.name || 'OpenAgents model gateway',
|
|
748
|
+
options: { ...(prev.options || {}), baseURL: baseUrl, apiKey: '{env:OPENAI_API_KEY}' },
|
|
749
|
+
// Keep previously-declared models so switching models doesn't invalidate
|
|
750
|
+
// sessions that still reference the old id.
|
|
751
|
+
models: { ...(prev.models || {}), [model]: (prev.models || {})[model] || {} },
|
|
752
|
+
};
|
|
753
|
+
if (JSON.stringify(cfg) === before) return;
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
757
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n', 'utf-8');
|
|
758
|
+
this._log(`Declared gateway provider for ${model} in ${file}`);
|
|
759
|
+
} catch (e) {
|
|
760
|
+
this._log(`Failed to write ${file}: ${e.message}`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
683
764
|
_runOpencode(content, msgChannel) {
|
|
684
765
|
// Preflight BEFORE spawning: bail out with a structured, actionable error
|
|
685
766
|
// instead of launching a child that will hang for 5 minutes or exit 1 with
|
|
@@ -692,6 +773,11 @@ class OpenCodeAdapter extends BaseAdapter {
|
|
|
692
773
|
|
|
693
774
|
const binary = this._opencodeBinary;
|
|
694
775
|
const runCwd = this._resolveCwd();
|
|
776
|
+
// Custom gateways must be declared in the run dir's opencode.json before
|
|
777
|
+
// the spawn, or --model ${CUSTOM_PROVIDER_ID}/… won't resolve.
|
|
778
|
+
try { this._ensureCustomProviderConfig(runCwd); } catch (e) {
|
|
779
|
+
this._log(`Gateway provider config failed (continuing): ${e.message}`);
|
|
780
|
+
}
|
|
695
781
|
const cmd = [binary, 'run', '--format', 'json', '--dir', runCwd];
|
|
696
782
|
|
|
697
783
|
// Preflight guarantees a resolvable model; pin it explicitly — without it
|
|
@@ -830,6 +830,33 @@ function isValidSessionId(id) {
|
|
|
830
830
|
* @param {boolean} [o.trustProject=false] true → --approve, false → --no-approve
|
|
831
831
|
* @returns {string[]}
|
|
832
832
|
*/
|
|
833
|
+
/**
|
|
834
|
+
* Infer the Launcher-provider settings for a bare relay configuration.
|
|
835
|
+
*
|
|
836
|
+
* The workspace's Add-agent flow collects only key + base URL + model, which
|
|
837
|
+
* lands as PI_API_KEY / PI_BASE_URL / PI_MODEL — no PI_PROVIDER, no
|
|
838
|
+
* PI_API_FORMAT. The adapter used to refuse that outright ("PI_PROVIDER and
|
|
839
|
+
* PI_MODEL are required when PI_BASE_URL is set"), a dead end for every agent
|
|
840
|
+
* created remotely. When the provider is not set explicitly, derive it from
|
|
841
|
+
* the base URL: anthropic-looking hosts speak anthropic-messages; every other
|
|
842
|
+
* relay/gateway convention is /chat/completions — NOT OpenAI's /responses, so
|
|
843
|
+
* the `openai` provider default of openai-responses would be wrong here.
|
|
844
|
+
* Returns null when PI_PROVIDER is set (explicit config wins) or when there
|
|
845
|
+
* is no base URL to infer from.
|
|
846
|
+
*/
|
|
847
|
+
function inferLauncherProvider(env = {}) {
|
|
848
|
+
const val = (k) => String(env[k] == null ? '' : env[k]).trim();
|
|
849
|
+
if (val('PI_PROVIDER')) return null;
|
|
850
|
+
const base = val('PI_BASE_URL');
|
|
851
|
+
if (!base) return null;
|
|
852
|
+
let host = '';
|
|
853
|
+
try { host = new URL(base).hostname.toLowerCase(); } catch { host = base.toLowerCase(); }
|
|
854
|
+
if (host.includes('anthropic')) {
|
|
855
|
+
return { provider: 'anthropic', apiFormat: 'anthropic-messages' };
|
|
856
|
+
}
|
|
857
|
+
return { provider: 'openai', apiFormat: 'openai-completions' };
|
|
858
|
+
}
|
|
859
|
+
|
|
833
860
|
function buildPiArgs({
|
|
834
861
|
sessionDir,
|
|
835
862
|
sessionId,
|
|
@@ -915,6 +942,7 @@ module.exports = {
|
|
|
915
942
|
parseWindowsCmdShim,
|
|
916
943
|
isValidSessionId,
|
|
917
944
|
buildPiArgs,
|
|
945
|
+
inferLauncherProvider,
|
|
918
946
|
normalizeThinking,
|
|
919
947
|
parseTrustProject,
|
|
920
948
|
redactArgs,
|
package/src/adapters/pi.js
CHANGED
|
@@ -45,6 +45,7 @@ const {
|
|
|
45
45
|
PiStreamParser,
|
|
46
46
|
PiAssistantAccumulator,
|
|
47
47
|
buildPiArgs,
|
|
48
|
+
inferLauncherProvider,
|
|
48
49
|
classifyNodeVersion,
|
|
49
50
|
classifyPiError,
|
|
50
51
|
classifyPiVersion,
|
|
@@ -1184,6 +1185,17 @@ class PiAdapter extends BaseAdapter {
|
|
|
1184
1185
|
/** Environment for the Pi child: agent env plus Pi's own hygiene settings. */
|
|
1185
1186
|
_buildEnv() {
|
|
1186
1187
|
const env = { ...(this.agentEnv || process.env) };
|
|
1188
|
+
// A relay-only config (base URL, no explicit provider — what the
|
|
1189
|
+
// workspace's Add-agent flow produces) gets its provider/API format
|
|
1190
|
+
// inferred. Written into the child env so the launcher provider
|
|
1191
|
+
// extension, which reads PI_PROVIDER/PI_API_FORMAT itself, agrees with
|
|
1192
|
+
// the adapter's own view.
|
|
1193
|
+
const inferred = inferLauncherProvider(env);
|
|
1194
|
+
if (inferred) {
|
|
1195
|
+
env.PI_PROVIDER = inferred.provider;
|
|
1196
|
+
const fmt = String(env.PI_API_FORMAT || '').trim().toLowerCase();
|
|
1197
|
+
if (!fmt || fmt === 'auto') env.PI_API_FORMAT = inferred.apiFormat;
|
|
1198
|
+
}
|
|
1187
1199
|
// The Launcher exposes one provider-agnostic secret field. Pi's native
|
|
1188
1200
|
// providers still expect their conventional env var, so mirror the value
|
|
1189
1201
|
// in memory for the child only. Existing provider-specific env remains
|
|
@@ -1212,11 +1224,18 @@ class PiAdapter extends BaseAdapter {
|
|
|
1212
1224
|
_config() {
|
|
1213
1225
|
const env = this.agentEnv || process.env;
|
|
1214
1226
|
const val = (k) => String(env[k] == null ? '' : env[k]).trim();
|
|
1227
|
+
// Same inference as _buildEnv, so the preflight check in _ensureProc
|
|
1228
|
+
// accepts a relay-only config instead of refusing to start.
|
|
1229
|
+
const inferred = inferLauncherProvider(env);
|
|
1230
|
+
const explicitFormat = val('PI_API_FORMAT');
|
|
1215
1231
|
return {
|
|
1216
|
-
provider: val('PI_PROVIDER') || null,
|
|
1232
|
+
provider: val('PI_PROVIDER') || (inferred && inferred.provider) || null,
|
|
1217
1233
|
model: val('PI_MODEL') || null,
|
|
1218
1234
|
baseUrl: val('PI_BASE_URL') || null,
|
|
1219
|
-
apiFormat:
|
|
1235
|
+
apiFormat:
|
|
1236
|
+
explicitFormat && explicitFormat.toLowerCase() !== 'auto'
|
|
1237
|
+
? explicitFormat
|
|
1238
|
+
: (inferred && inferred.apiFormat) || explicitFormat || 'auto',
|
|
1220
1239
|
thinking: normalizeThinking(val('PI_THINKING')),
|
|
1221
1240
|
trustProject: parseTrustProject(val('PI_TRUST_PROJECT')),
|
|
1222
1241
|
};
|
|
@@ -209,7 +209,7 @@ function buildModePrompt(mode) {
|
|
|
209
209
|
*
|
|
210
210
|
* In plan mode, only read-only operations are documented.
|
|
211
211
|
*/
|
|
212
|
-
function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode = 'execute', isWindows = process.platform === 'win32' }) {
|
|
212
|
+
function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode = 'execute', isWindows = process.platform === 'win32', execTool = 'exec' }) {
|
|
213
213
|
const disabled = disabledModules || new Set();
|
|
214
214
|
const baseUrl = endpoint.replace(/\/+$/, '');
|
|
215
215
|
const isPlan = mode === 'plan';
|
|
@@ -231,14 +231,14 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
231
231
|
'You can ' + caps.join(', ') + '.\n' +
|
|
232
232
|
'These are WORKSPACE tools shared with all agents and users. ' +
|
|
233
233
|
'They are different from your native tools.\n\n' +
|
|
234
|
-
|
|
235
|
-
|
|
234
|
+
`**HOW TO USE:** Call your \`${execTool}\` tool to run the \`curl\` commands below. ` +
|
|
235
|
+
`Do NOT output curl commands as text — EXECUTE them with \`${execTool}\`.\n\n` +
|
|
236
236
|
'**IMPORTANT — tool priority:**\n' +
|
|
237
|
-
|
|
237
|
+
`- ALWAYS use \`${execTool}\` + \`curl\` (documented below) for workspace operations.\n` +
|
|
238
238
|
'- Do NOT use `workspace_browser_*` native tools — they are not configured ' +
|
|
239
239
|
'and will fail.\n' +
|
|
240
240
|
'- Do NOT use `web_fetch`, `browser`, or any native browsing tool ' +
|
|
241
|
-
|
|
241
|
+
`when the user asks to use the workspace browser — use \`${execTool}\` + \`curl\` instead.\n` +
|
|
242
242
|
'- The workspace browser is a *shared* browser visible to all users and agents.\n\n' +
|
|
243
243
|
'**Auth header** (include on every request):\n' +
|
|
244
244
|
`\`X-Workspace-Token: ${token}\`\n`
|
|
@@ -251,7 +251,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
251
251
|
if (!isPlan) {
|
|
252
252
|
if (isWindows) {
|
|
253
253
|
s += (
|
|
254
|
-
|
|
254
|
+
`**To upload a file**, run this (replace filename/content):\n` +
|
|
255
255
|
`$CONTENT = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('YOUR_CONTENT'))\n` +
|
|
256
256
|
`${curl} -s -X POST ${baseUrl}/v1/files/base64 ` +
|
|
257
257
|
`-H "${h}" ` +
|
|
@@ -265,7 +265,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
265
265
|
);
|
|
266
266
|
} else {
|
|
267
267
|
s += (
|
|
268
|
-
|
|
268
|
+
`**To upload a file**, run this (replace filename/content):\n` +
|
|
269
269
|
`CONTENT=$(echo -n 'YOUR_CONTENT' | base64) && ` +
|
|
270
270
|
`${curl} -s -X POST ${baseUrl}/v1/files/base64 ` +
|
|
271
271
|
`-H "${h}" ` +
|
|
@@ -354,7 +354,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
354
354
|
`"source":"openagents:${agentName}"}'\n` +
|
|
355
355
|
'If it returns error_code AUTH_REQUIRED or BOT_CHALLENGE, open the URL ' +
|
|
356
356
|
'in a shared tab (below) and share its `live_url` so a human can log in.\n\n' +
|
|
357
|
-
|
|
357
|
+
`**To browse interactively** (click/type/login), run these steps (use \`${execTool}\` for each):\n` +
|
|
358
358
|
`Step 1 — open tab: ` +
|
|
359
359
|
`${curl} -s -X POST ${baseUrl}/v1/browser/tabs ` +
|
|
360
360
|
`-H "${h}" -H "Content-Type: application/json" ` +
|
|
@@ -1107,6 +1107,39 @@ function buildOpenCodeSkillMd({ endpoint, workspaceId, token, agentName, channel
|
|
|
1107
1107
|
return frontmatter + identity + api + '\n' + buildGuardrails();
|
|
1108
1108
|
}
|
|
1109
1109
|
|
|
1110
|
+
/**
|
|
1111
|
+
* Build a SKILL.md file for Command Code.
|
|
1112
|
+
*
|
|
1113
|
+
* Command Code discovers skills from `.commandcode/skills/` (project) and
|
|
1114
|
+
* `~/.commandcode/skills/` (user); this file is written to neither. It goes to
|
|
1115
|
+
* an OpenAgents-owned directory and is loaded with `--skill`, so a
|
|
1116
|
+
* token-bearing file never lands in the user's repo or personal config. The
|
|
1117
|
+
* shell tool is named for the CLI that will read it — `shell_command`, not the
|
|
1118
|
+
* `exec` other adapters advertise — because an agent told to call a tool it
|
|
1119
|
+
* does not have simply does nothing.
|
|
1120
|
+
*/
|
|
1121
|
+
function buildCommandCodeSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules }) {
|
|
1122
|
+
const api = buildApiSkillsPrompt({
|
|
1123
|
+
endpoint, workspaceId, token, agentName,
|
|
1124
|
+
channelName: channelName || 'general',
|
|
1125
|
+
disabledModules,
|
|
1126
|
+
mode: 'execute',
|
|
1127
|
+
execTool: 'shell_command',
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
const frontmatter =
|
|
1131
|
+
'---\n' +
|
|
1132
|
+
`name: ${workspaceSkillName(agentName)}\n` +
|
|
1133
|
+
'description: OpenAgents Workspace API — shared files, browser, and agent collaboration\n' +
|
|
1134
|
+
'---\n\n';
|
|
1135
|
+
|
|
1136
|
+
const identity =
|
|
1137
|
+
`You are agent '${agentName}' connected to OpenAgents workspace ${workspaceId}.\n` +
|
|
1138
|
+
'Use these APIs via `shell_command` + curl to interact with the workspace.\n\n';
|
|
1139
|
+
|
|
1140
|
+
return frontmatter + identity + api + '\n' + buildGuardrails();
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1110
1143
|
/**
|
|
1111
1144
|
* Build a SKILL.md file for Claude Code's skill auto-discovery.
|
|
1112
1145
|
*
|
|
@@ -1188,6 +1221,7 @@ module.exports = {
|
|
|
1188
1221
|
buildOpenclawSkillMd,
|
|
1189
1222
|
buildOpenCodeSystemPrompt,
|
|
1190
1223
|
buildOpenCodeSkillMd,
|
|
1224
|
+
buildCommandCodeSkillMd,
|
|
1191
1225
|
buildPiSystemPrompt,
|
|
1192
1226
|
buildDeepSeekTaskFile,
|
|
1193
1227
|
buildClaudeSkillMd,
|
package/src/cli.js
CHANGED
|
@@ -54,6 +54,35 @@ function getConnector(flags) {
|
|
|
54
54
|
|
|
55
55
|
function print(msg) { process.stdout.write(msg + '\n'); }
|
|
56
56
|
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Banner — the OpenAgents wordmark (figlet "Small Slant", pure ASCII so it
|
|
59
|
+
// renders on every console including legacy Windows codepages). Colored with
|
|
60
|
+
// a cyan→violet gradient only on a real TTY; pipes and NO_COLOR get plain
|
|
61
|
+
// text. 47 columns wide, so it fits any 80-column terminal.
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
const OA_LOGO = [
|
|
64
|
+
' ____ ___ __ ',
|
|
65
|
+
' / __ \\___ ___ ___ / _ |___ ____ ___ / /____',
|
|
66
|
+
'/ /_/ / _ \\/ -_) _ \\/ __ / _ `/ -_) _ \\/ __(_-<',
|
|
67
|
+
'\\____/ .__/\\__/_//_/_/ |_\\_, /\\__/_//_/\\__/___/',
|
|
68
|
+
' /_/ /___/ ',
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
function printBanner() {
|
|
72
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
73
|
+
// 256-color gradient, one shade per row: cyan → azure → blue → violet.
|
|
74
|
+
const shades = [51, 45, 39, 63, 99];
|
|
75
|
+
let version = '';
|
|
76
|
+
try { version = require('../package.json').version; } catch {}
|
|
77
|
+
print('');
|
|
78
|
+
OA_LOGO.forEach((line, i) => {
|
|
79
|
+
print(useColor ? `\x1b[38;5;${shades[i]}m${line}\x1b[0m` : line);
|
|
80
|
+
});
|
|
81
|
+
const tag = ` v${version} · multi-agent orchestration for your machine`;
|
|
82
|
+
print(useColor ? `\x1b[2m${tag}\x1b[0m` : tag);
|
|
83
|
+
print('');
|
|
84
|
+
}
|
|
85
|
+
|
|
57
86
|
function table(rows, headers) {
|
|
58
87
|
if (rows.length === 0) return;
|
|
59
88
|
const widths = headers.map((h, i) =>
|
|
@@ -178,7 +207,18 @@ async function cmdCreate(connector, flags, positional) {
|
|
|
178
207
|
print('This agent is local-only and will not appear in Workspace Dashboard yet.');
|
|
179
208
|
print('');
|
|
180
209
|
print('To connect it to a Workspace, run:');
|
|
181
|
-
|
|
210
|
+
// A paired device already holds the credential — point at the
|
|
211
|
+
// pairing-first form instead of the deprecated manual-token one.
|
|
212
|
+
let pairings = [];
|
|
213
|
+
try { pairings = require('./node-config').listPairings().filter((p) => p && p.workspace_slug); } catch {}
|
|
214
|
+
if (pairings.length) {
|
|
215
|
+
for (const p of pairings.slice(0, 3)) {
|
|
216
|
+
print(` agn connect ${name} --workspace ${p.workspace_slug}${p.workspace_name ? ` (${p.workspace_name})` : ''}`);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
print(` agn connect ${name} <workspace-token>`);
|
|
220
|
+
print('Or pair this device first: agn node connect <pairing-code>');
|
|
221
|
+
}
|
|
182
222
|
} else {
|
|
183
223
|
print(`Agent '${name}' created (type: ${type})`);
|
|
184
224
|
}
|
|
@@ -323,6 +363,24 @@ async function cmdRuntimes(connector, flags) {
|
|
|
323
363
|
const runtimes = connector.registry.getCatalogSync().map((e) => {
|
|
324
364
|
let h;
|
|
325
365
|
try { h = connector.healthCheck(e.name); } catch { h = {}; }
|
|
366
|
+
// Credential contract, upward half: STATUS goes up, the secret stays on
|
|
367
|
+
// this device. `mode` says how the type authenticates today
|
|
368
|
+
// (subscription = CLI/OAuth login, api_key = a stored key, none = not
|
|
369
|
+
// configured); keyMasked shows only the key's tail.
|
|
370
|
+
let keyMasked = null;
|
|
371
|
+
try {
|
|
372
|
+
const env = connector.env.load(e.name) || {};
|
|
373
|
+
const key = env.LLM_API_KEY || null;
|
|
374
|
+
if (key && key.length > 7) keyMasked = `…${key.slice(-4)}`;
|
|
375
|
+
else if (key) keyMasked = '…';
|
|
376
|
+
} catch {}
|
|
377
|
+
const mode = !h.ready
|
|
378
|
+
? 'none'
|
|
379
|
+
: h.auth_mode === 'cli_login'
|
|
380
|
+
? 'subscription'
|
|
381
|
+
: h.auth_mode === 'api_key'
|
|
382
|
+
? 'api_key'
|
|
383
|
+
: 'none';
|
|
326
384
|
return {
|
|
327
385
|
type: e.name,
|
|
328
386
|
installed: !!h.installed,
|
|
@@ -331,6 +389,7 @@ async function cmdRuntimes(connector, flags) {
|
|
|
331
389
|
reason: h.reason || null,
|
|
332
390
|
message: h.message || null,
|
|
333
391
|
authStatus: h.auth_status || null,
|
|
392
|
+
auth: { mode, keyMasked: mode === 'api_key' ? keyMasked : null },
|
|
334
393
|
};
|
|
335
394
|
});
|
|
336
395
|
process.stdout.write(JSON.stringify(runtimes));
|
|
@@ -372,10 +431,20 @@ async function cmdConnect(connector, flags, positional) {
|
|
|
372
431
|
|
|
373
432
|
if (!name) {
|
|
374
433
|
print('Usage: agn connect <agent-name> <token>');
|
|
434
|
+
print(' agn connect <agent-name> --workspace <slug> (paired/known workspace, no token)');
|
|
375
435
|
process.exitCode = 1;
|
|
376
436
|
return;
|
|
377
437
|
}
|
|
378
438
|
|
|
439
|
+
// --workspace <slug|id>: bind to a workspace this device already knows —
|
|
440
|
+
// a registered network or a device pairing. No token, no /v1/token/resolve,
|
|
441
|
+
// no server round-trip; the credential is already on disk. This is the
|
|
442
|
+
// pairing-first path; the token form below stays for manual connection.
|
|
443
|
+
if (flags.workspace && typeof flags.workspace === 'string') {
|
|
444
|
+
connectKnownWorkspace(connector, name, flags.workspace);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
379
448
|
if (!token) {
|
|
380
449
|
// No token supplied and none in the environment. Never prompt — keep
|
|
381
450
|
// CI / non-interactive environments from hanging. Print a helpful error
|
|
@@ -406,6 +475,14 @@ async function cmdConnect(connector, flags, positional) {
|
|
|
406
475
|
// Connect agent
|
|
407
476
|
connector.connectWorkspace(name, slug);
|
|
408
477
|
print(`'${name}' connected to workspace '${wsName}'`);
|
|
478
|
+
// Manual token connection is being retired in favor of device pairing.
|
|
479
|
+
// It keeps working (backward compatibility is deliberate); this is the
|
|
480
|
+
// CLI half of the dismissible launcher notice.
|
|
481
|
+
if (!process.env.OPENAGENTS_NO_DEPRECATION_NOTES) {
|
|
482
|
+
print('note: manual token connection is being retired in favor of device pairing.');
|
|
483
|
+
print(' Pair once with `agn node connect <code>`, then `agn connect <agent> --workspace <slug>`.');
|
|
484
|
+
print(' (Silence this note with OPENAGENTS_NO_DEPRECATION_NOTES=1.)');
|
|
485
|
+
}
|
|
409
486
|
|
|
410
487
|
// Signal daemon reload
|
|
411
488
|
const pid = connector.getDaemonPid();
|
|
@@ -421,6 +498,78 @@ async function cmdConnect(connector, flags, positional) {
|
|
|
421
498
|
}
|
|
422
499
|
}
|
|
423
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Bind an agent to a workspace already known to this device, by slug or id.
|
|
503
|
+
* Sources, in order: registered networks (daemon.yaml), then device pairings
|
|
504
|
+
* (node.json) — a pairing registers its network on the spot so the daemon can
|
|
505
|
+
* pick up the token. Never touches the server.
|
|
506
|
+
*/
|
|
507
|
+
function connectKnownWorkspace(connector, name, ref) {
|
|
508
|
+
const networks = connector.config.getNetworks() || [];
|
|
509
|
+
let net = networks.find((n) => n.slug === ref || n.id === ref);
|
|
510
|
+
|
|
511
|
+
if (!net) {
|
|
512
|
+
const nodeCfg = require('./node-config');
|
|
513
|
+
const pairing = (nodeCfg.listPairings() || []).find(
|
|
514
|
+
(p) => p.workspace_slug === ref || p.workspace_id === ref,
|
|
515
|
+
);
|
|
516
|
+
if (pairing && pairing.token) {
|
|
517
|
+
connector.config.addNetwork({
|
|
518
|
+
id: pairing.workspace_id,
|
|
519
|
+
slug: pairing.workspace_slug,
|
|
520
|
+
name: pairing.workspace_name || pairing.workspace_slug,
|
|
521
|
+
endpoint: pairing.endpoint || connector.workspace.endpoint,
|
|
522
|
+
token: pairing.token,
|
|
523
|
+
});
|
|
524
|
+
net = { slug: pairing.workspace_slug, name: pairing.workspace_name };
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!net) {
|
|
529
|
+
const known = [
|
|
530
|
+
...new Set([
|
|
531
|
+
...networks.map((n) => n.slug || n.id),
|
|
532
|
+
...(() => {
|
|
533
|
+
try {
|
|
534
|
+
return require('./node-config').listPairings().map((p) => p.workspace_slug);
|
|
535
|
+
} catch { return []; }
|
|
536
|
+
})(),
|
|
537
|
+
]),
|
|
538
|
+
].filter(Boolean);
|
|
539
|
+
print(`No workspace '${ref}' is known on this device.`);
|
|
540
|
+
if (known.length) print(`Known workspaces: ${known.join(', ')}`);
|
|
541
|
+
print('Pair this device first (`agn node connect <code>`), or connect with a token.');
|
|
542
|
+
process.exitCode = 1;
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const slug = net.slug || ref;
|
|
547
|
+
connector.connectWorkspace(name, slug);
|
|
548
|
+
print(`'${name}' connected to workspace '${net.name || slug}'`);
|
|
549
|
+
|
|
550
|
+
const pid = connector.getDaemonPid();
|
|
551
|
+
if (pid) {
|
|
552
|
+
connector.sendDaemonCommand(`restart:${name}`);
|
|
553
|
+
print('Daemon notified');
|
|
554
|
+
} else {
|
|
555
|
+
print('Daemon is not running. Run `agn up` to bring this agent online.');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Browser URL of a workspace, matching the TUI's convention: a localhost/dev
|
|
561
|
+
* endpoint serves its own web app; everything else lives on the hosted
|
|
562
|
+
* dashboard. This is what a freshly paired user actually needs next — NOT
|
|
563
|
+
* another CLI command.
|
|
564
|
+
*/
|
|
565
|
+
function workspaceWebUrl(slug, endpoint) {
|
|
566
|
+
const ep = String(endpoint || '');
|
|
567
|
+
const isLocal = ep.includes('localhost') || ep.includes('127.0.0.1');
|
|
568
|
+
return isLocal
|
|
569
|
+
? `${ep.replace(/\/+$/, '')}/${slug}`
|
|
570
|
+
: `https://workspace.openagents.org/${slug}`;
|
|
571
|
+
}
|
|
572
|
+
|
|
424
573
|
async function cmdNode(connector, flags, positional) {
|
|
425
574
|
const sub = positional[0] || 'status';
|
|
426
575
|
const nodeCfg = require('./node-config');
|
|
@@ -437,9 +586,28 @@ async function cmdNode(connector, flags, positional) {
|
|
|
437
586
|
if (flags.type) info.deviceType = flags.type; // server | laptop | desktop
|
|
438
587
|
if (flags.name) info.name = flags.name;
|
|
439
588
|
|
|
589
|
+
// Existing pairings up front, so a re-run shows where this device already
|
|
590
|
+
// lives instead of looking like a first-time setup.
|
|
591
|
+
const priorPairings = nodeCfg.listPairings().filter((p) => p.node_id);
|
|
592
|
+
if (priorPairings.length) {
|
|
593
|
+
print('This device is already paired with:');
|
|
594
|
+
for (const p of priorPairings) {
|
|
595
|
+
print(` ${p.workspace_name || p.workspace_slug} — ${workspaceWebUrl(p.workspace_slug || p.workspace_id, p.endpoint)}`);
|
|
596
|
+
}
|
|
597
|
+
print('');
|
|
598
|
+
}
|
|
599
|
+
|
|
440
600
|
print('Connecting this device to your workspace...');
|
|
441
601
|
try {
|
|
442
602
|
const res = await connector.redeemNodePairingCode(code, info);
|
|
603
|
+
// A code always redeems against ONE workspace; if that workspace is one
|
|
604
|
+
// this device already belongs to, this is a re-pair, not a new pairing.
|
|
605
|
+
// recordPairing below still runs — it refreshes the (possibly rotated)
|
|
606
|
+
// credential in place — but the user should hear "already paired", not
|
|
607
|
+
// a duplicate "connected".
|
|
608
|
+
const alreadyPaired = priorPairings.some(
|
|
609
|
+
(p) => p.workspace_id === res.workspaceId,
|
|
610
|
+
);
|
|
443
611
|
// recordPairing, not saveNode: this device can be a node in several
|
|
444
612
|
// workspaces at once, and a bare save would drop the others' pairings.
|
|
445
613
|
nodeCfg.recordPairing(info.nodeKey, {
|
|
@@ -458,7 +626,11 @@ async function cmdNode(connector, flags, positional) {
|
|
|
458
626
|
endpoint: connector.workspace.endpoint,
|
|
459
627
|
token: res.token,
|
|
460
628
|
});
|
|
461
|
-
|
|
629
|
+
if (alreadyPaired) {
|
|
630
|
+
print(`Already paired with '${res.workspaceName}' (${res.workspaceSlug}) — refreshed this device's credential.`);
|
|
631
|
+
} else {
|
|
632
|
+
print(`Node connected to workspace '${res.workspaceName}' (${res.workspaceSlug})`);
|
|
633
|
+
}
|
|
462
634
|
print(` This device: ${info.hostname} (${info.deviceType})`);
|
|
463
635
|
|
|
464
636
|
// (Re)start the daemon so it runs the CURRENT launcher build and loads
|
|
@@ -476,8 +648,10 @@ async function cmdNode(connector, flags, positional) {
|
|
|
476
648
|
}
|
|
477
649
|
connector.startDaemon();
|
|
478
650
|
print('');
|
|
479
|
-
|
|
480
|
-
|
|
651
|
+
// The user came from the workspace's "Connect a Node" page — send them
|
|
652
|
+
// straight back there with a clickable link, not more CLI homework.
|
|
653
|
+
print('All set! Open your workspace to add and manage agents on this device:');
|
|
654
|
+
print(` ${workspaceWebUrl(res.workspaceSlug, connector.workspace.endpoint)}`);
|
|
481
655
|
} catch (e) {
|
|
482
656
|
print(`Error: ${e.message}`);
|
|
483
657
|
process.exitCode = 1;
|
|
@@ -493,7 +667,8 @@ async function cmdNode(connector, flags, positional) {
|
|
|
493
667
|
}
|
|
494
668
|
print(`Node connected to ${pairings.length} workspace(s):`);
|
|
495
669
|
for (const p of pairings) {
|
|
496
|
-
|
|
670
|
+
const slug = p.workspace_slug || p.workspace_id;
|
|
671
|
+
print(` ${p.workspace_name || slug} (${slug}) — ${workspaceWebUrl(slug, p.endpoint)}`);
|
|
497
672
|
}
|
|
498
673
|
return;
|
|
499
674
|
}
|
|
@@ -882,6 +1057,7 @@ async function cmdUpdate(connector) {
|
|
|
882
1057
|
}
|
|
883
1058
|
|
|
884
1059
|
async function cmdHelp() {
|
|
1060
|
+
printBanner();
|
|
885
1061
|
print(`Usage: agn <command> [options]
|
|
886
1062
|
|
|
887
1063
|
Commands:
|
|
@@ -898,7 +1074,7 @@ Commands:
|
|
|
898
1074
|
uninstall <type> Uninstall an agent runtime
|
|
899
1075
|
search [query] Browse agent catalog
|
|
900
1076
|
runtimes List installed runtimes
|
|
901
|
-
connect <agent> <token> Connect agent to workspace
|
|
1077
|
+
connect <agent> <token> Connect agent to workspace (or --workspace <slug> for a paired one)
|
|
902
1078
|
disconnect <agent> Disconnect agent from workspace
|
|
903
1079
|
env <type> [--set K=V] View/set env vars for agent type
|
|
904
1080
|
skills [agent] [action] Manage agent skills (enable/disable)
|
package/src/config.js
CHANGED
|
@@ -116,8 +116,19 @@ class Config {
|
|
|
116
116
|
|
|
117
117
|
addNetwork({ id, slug, name, endpoint, token }) {
|
|
118
118
|
const config = this.load();
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// Upsert, not insert-only: re-registering a known workspace refreshes its
|
|
120
|
+
// credentials. The old early-return silently kept a stale token after a
|
|
121
|
+
// rotation or re-pair — both the launcher and the CLI then had to carry
|
|
122
|
+
// their own read-modify-write patches around it.
|
|
123
|
+
const existing = config.networks.find((n) => n.slug === slug || n.id === id);
|
|
124
|
+
if (existing) {
|
|
125
|
+
if (id) existing.id = id;
|
|
126
|
+
if (slug) existing.slug = slug;
|
|
127
|
+
if (name) existing.name = name;
|
|
128
|
+
if (endpoint) existing.endpoint = endpoint;
|
|
129
|
+
if (token) existing.token = token;
|
|
130
|
+
this.save(config);
|
|
131
|
+
return existing;
|
|
121
132
|
}
|
|
122
133
|
const entry = { id, slug };
|
|
123
134
|
if (name) entry.name = name;
|