@atel-ai/agent 0.2.23 → 0.2.28
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/dist/binding.d.ts +29 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +45 -0
- package/dist/binding.js.map +1 -0
- package/dist/cli.js +1274 -20
- package/dist/cli.js.map +1 -1
- package/dist/mcp-shim.d.ts.map +1 -1
- package/dist/mcp-shim.js +9 -1
- package/dist/mcp-shim.js.map +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,8 @@ import crypto from 'node:crypto';
|
|
|
15
15
|
import { execFileSync, spawn } from 'node:child_process';
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
19
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
18
20
|
const require = createRequire(import.meta.url);
|
|
19
21
|
const __cliPath = fileURLToPath(import.meta.url);
|
|
20
22
|
// AC-11:打印实际版本,便于测试追踪(`@latest` 解析到的具体版本)。
|
|
@@ -25,12 +27,14 @@ catch {
|
|
|
25
27
|
return 'unknown';
|
|
26
28
|
} })();
|
|
27
29
|
import { defaultConfig, PROD_DEFAULTS, newIdentity, loadIdentity, serializeIdentity, AuthManager, Companion, spineWalletBalance, spineIdentityVerify, registryRemoteRegister, registryHeartbeat, postSigned, relayPoll, relayAck, detectAll, getAdapter, ADAPTERS, } from '@atel-ai/runtime-core';
|
|
30
|
+
import { assertBindingCompatible, bindingPath, machineFingerprint, readBinding, writeBinding, } from './binding.js';
|
|
28
31
|
import { runMcpShim } from './mcp-shim.js';
|
|
29
32
|
// ── arg parsing ────────────────────────────────────────────────
|
|
30
33
|
function parseArgs(argv) {
|
|
31
34
|
const [cmd = 'help', ...rest] = argv;
|
|
32
35
|
const flags = {};
|
|
33
36
|
const bools = new Set();
|
|
37
|
+
const positionals = [];
|
|
34
38
|
for (let i = 0; i < rest.length; i++) {
|
|
35
39
|
const a = rest[i];
|
|
36
40
|
if (a.startsWith('--')) {
|
|
@@ -44,14 +48,20 @@ function parseArgs(argv) {
|
|
|
44
48
|
bools.add(key);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
51
|
+
else {
|
|
52
|
+
positionals.push(a);
|
|
53
|
+
}
|
|
47
54
|
}
|
|
48
|
-
return { cmd, flags, bools };
|
|
55
|
+
return { cmd, positionals, flags, bools };
|
|
49
56
|
}
|
|
50
57
|
const log = (m) => console.log(m);
|
|
51
58
|
const err = (m) => console.error(m);
|
|
52
59
|
function configPath(dataDir) {
|
|
53
60
|
return path.join(dataDir, 'config.json');
|
|
54
61
|
}
|
|
62
|
+
function runtimeEnvPath(dataDir) {
|
|
63
|
+
return path.join(dataDir, 'runtime.env');
|
|
64
|
+
}
|
|
55
65
|
function loadConfig(dataDir) {
|
|
56
66
|
try {
|
|
57
67
|
return JSON.parse(fs.readFileSync(configPath(dataDir), 'utf8'));
|
|
@@ -64,6 +74,115 @@ function saveConfig(cfg) {
|
|
|
64
74
|
fs.mkdirSync(cfg.dataDir, { recursive: true });
|
|
65
75
|
fs.writeFileSync(configPath(cfg.dataDir), JSON.stringify(cfg, null, 2));
|
|
66
76
|
}
|
|
77
|
+
const COMMON_RUNTIME_ENV_KEYS = [
|
|
78
|
+
'HTTP_PROXY',
|
|
79
|
+
'HTTPS_PROXY',
|
|
80
|
+
'ALL_PROXY',
|
|
81
|
+
'NO_PROXY',
|
|
82
|
+
'http_proxy',
|
|
83
|
+
'https_proxy',
|
|
84
|
+
'all_proxy',
|
|
85
|
+
'no_proxy',
|
|
86
|
+
];
|
|
87
|
+
const RUNTIME_ENV_KEYS = {
|
|
88
|
+
codex: ['OPENAI_API_KEY', 'CODEX_HOME'],
|
|
89
|
+
'claude-code': ['ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'API_TIMEOUT_MS', 'CLAUDE_CONFIG_DIR'],
|
|
90
|
+
'gemini-cli': ['GOOGLE_GEMINI_BASE_URL', 'GEMINI_API_KEY', 'GEMINI_MODEL', 'ATEL_GEMINI_MODEL', 'GEMINI_CLI_TRUST_WORKSPACE'],
|
|
91
|
+
hermes: ['ATEL_HERMES_MODEL', 'OPENAI_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_MODEL'],
|
|
92
|
+
'qwen-agent': ['ATEL_PYTHON', 'ATEL_QWEN_MODEL', 'ATEL_QWEN_BASE_URL', 'ATEL_QWEN_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_MODEL', 'DASHSCOPE_BASE_URL', 'DASHSCOPE_API_KEY'],
|
|
93
|
+
openclaw: ['OPENAI_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_MODEL'],
|
|
94
|
+
};
|
|
95
|
+
function shellQuoteEnv(value) {
|
|
96
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$').replace(/`/g, '\\`')}"`;
|
|
97
|
+
}
|
|
98
|
+
function parseRuntimeEnvFile(dataDir) {
|
|
99
|
+
const out = {};
|
|
100
|
+
try {
|
|
101
|
+
const body = fs.readFileSync(runtimeEnvPath(dataDir), 'utf8');
|
|
102
|
+
for (const line of body.split(/\r?\n/)) {
|
|
103
|
+
const trimmed = line.trim();
|
|
104
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
105
|
+
continue;
|
|
106
|
+
const m = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
107
|
+
if (!m)
|
|
108
|
+
continue;
|
|
109
|
+
let value = m[2] || '';
|
|
110
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
111
|
+
value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\$/g, '$').replace(/\\`/g, '`').replace(/\\\\/g, '\\');
|
|
112
|
+
}
|
|
113
|
+
out[m[1]] = value;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* no runtime env yet */
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
function loadRuntimeEnv(dataDir) {
|
|
122
|
+
return parseRuntimeEnvFile(dataDir);
|
|
123
|
+
}
|
|
124
|
+
function cleanEnv(env) {
|
|
125
|
+
const out = {};
|
|
126
|
+
for (const [key, value] of Object.entries(env)) {
|
|
127
|
+
if (typeof value === 'string')
|
|
128
|
+
out[key] = value;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
function captureRuntimeEnv(cfg) {
|
|
133
|
+
const keys = [...COMMON_RUNTIME_ENV_KEYS, ...(RUNTIME_ENV_KEYS[cfg.runtime] || [])];
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
const lines = [
|
|
136
|
+
'# Generated by atel connect. Contains runtime model credentials/env needed by companion.',
|
|
137
|
+
'# Keep this file private.',
|
|
138
|
+
];
|
|
139
|
+
const captured = [];
|
|
140
|
+
for (const key of keys) {
|
|
141
|
+
if (seen.has(key))
|
|
142
|
+
continue;
|
|
143
|
+
seen.add(key);
|
|
144
|
+
const value = process.env[key];
|
|
145
|
+
if (!value || !value.trim())
|
|
146
|
+
continue;
|
|
147
|
+
lines.push(`${key}=${shellQuoteEnv(value)}`);
|
|
148
|
+
captured.push(key);
|
|
149
|
+
}
|
|
150
|
+
if (cfg.runtime === 'gemini-cli' && !captured.includes('GEMINI_CLI_TRUST_WORKSPACE')) {
|
|
151
|
+
lines.push('GEMINI_CLI_TRUST_WORKSPACE="true"');
|
|
152
|
+
captured.push('GEMINI_CLI_TRUST_WORKSPACE');
|
|
153
|
+
}
|
|
154
|
+
if (cfg.runtime === 'hermes' && !captured.includes('ATEL_HERMES_MODEL')) {
|
|
155
|
+
lines.push('ATEL_HERMES_MODEL="gpt-5.5"');
|
|
156
|
+
captured.push('ATEL_HERMES_MODEL');
|
|
157
|
+
}
|
|
158
|
+
if (captured.length === 0)
|
|
159
|
+
return [];
|
|
160
|
+
fs.mkdirSync(cfg.dataDir, { recursive: true });
|
|
161
|
+
fs.writeFileSync(runtimeEnvPath(cfg.dataDir), `${lines.join('\n')}\n`, { mode: 0o600 });
|
|
162
|
+
return captured;
|
|
163
|
+
}
|
|
164
|
+
function runtimeEnvReadiness(cfg) {
|
|
165
|
+
const env = { ...loadRuntimeEnv(cfg.dataDir), ...process.env };
|
|
166
|
+
if (cfg.runtime === 'claude-code') {
|
|
167
|
+
return {
|
|
168
|
+
ok: !!(env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY),
|
|
169
|
+
detail: env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY ? 'Claude 模型凭据已可用' : '缺 ANTHROPIC_AUTH_TOKEN/ANTHROPIC_API_KEY',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (cfg.runtime === 'gemini-cli') {
|
|
173
|
+
return {
|
|
174
|
+
ok: !!env.GEMINI_API_KEY,
|
|
175
|
+
detail: env.GEMINI_API_KEY ? 'Gemini 模型凭据已可用' : '缺 GEMINI_API_KEY',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (cfg.runtime === 'qwen-agent') {
|
|
179
|
+
return {
|
|
180
|
+
ok: !!(env.ATEL_QWEN_API_KEY || env.OPENAI_API_KEY || env.DASHSCOPE_API_KEY),
|
|
181
|
+
detail: env.ATEL_QWEN_API_KEY || env.OPENAI_API_KEY || env.DASHSCOPE_API_KEY ? 'Qwen 模型凭据已可用' : '缺 ATEL_QWEN_API_KEY/OPENAI_API_KEY/DASHSCOPE_API_KEY',
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, detail: fs.existsSync(runtimeEnvPath(cfg.dataDir)) ? `已保存 ${runtimeEnvPath(cfg.dataDir)}` : '无需额外 runtime env' };
|
|
185
|
+
}
|
|
67
186
|
// 身份绑定在 data-dir(identity.json),与 --name 无关。返回 reused 让 cmdInstall 决定
|
|
68
187
|
// 是否对"复用 + 传了 --name"硬闸(A2:避免静默把同一 DID 改名)。
|
|
69
188
|
function ensureIdentity(cfg) {
|
|
@@ -182,6 +301,12 @@ function reconcileOpenclawIdentity(cfg) {
|
|
|
182
301
|
return 'none'; // 已是同一身份
|
|
183
302
|
}
|
|
184
303
|
const VALID_CAPABILITIES = ['coding', 'writing', 'translation', 'art', 'data', 'automation'];
|
|
304
|
+
function parseCapabilities(value) {
|
|
305
|
+
return (value || 'writing')
|
|
306
|
+
.split(',')
|
|
307
|
+
.map((s) => s.trim())
|
|
308
|
+
.filter(Boolean);
|
|
309
|
+
}
|
|
185
310
|
// A6:拒绝文档占位符 / 空 / 非法 capability,避免 <CODEX>、<coding,writing> 被字面注册。
|
|
186
311
|
function validateInputs(flags) {
|
|
187
312
|
const name = flags['name'];
|
|
@@ -285,9 +410,7 @@ function interactiveMcpConfigured(runtime) {
|
|
|
285
410
|
}
|
|
286
411
|
function writeSkill(cfg, adapter) {
|
|
287
412
|
try {
|
|
288
|
-
|
|
289
|
-
const src = require.resolve('@atel-ai/runtime-core/skill/SKILL.md');
|
|
290
|
-
const skillBody = fs.readFileSync(src, 'utf8');
|
|
413
|
+
const skillBody = readPackagedSkill();
|
|
291
414
|
const dest = path.join(cfg.dataDir, 'SKILL.md');
|
|
292
415
|
fs.writeFileSync(dest, skillBody);
|
|
293
416
|
log(`📖 SKILL.md → ${dest}`);
|
|
@@ -306,6 +429,12 @@ function writeSkill(cfg, adapter) {
|
|
|
306
429
|
err(`⚠️ 复制 SKILL.md 失败: ${e.message}`);
|
|
307
430
|
}
|
|
308
431
|
}
|
|
432
|
+
function readPackagedSkill() {
|
|
433
|
+
// SKILL.md 随 runtime-core 包发布。`atel skill` 暴露这个本地副本,让 agent
|
|
434
|
+
// 在 DNS/公网不可用时仍可零网络读取接入说明。
|
|
435
|
+
const src = require.resolve('@atel-ai/runtime-core/skill/SKILL.md');
|
|
436
|
+
return fs.readFileSync(src, 'utf8');
|
|
437
|
+
}
|
|
309
438
|
// A8:--profile <别名> → ~/.atel/agents/<别名>,一机多 agent 各自独立 data-dir(单 agent 仍默认 ~/.atel,非破坏)。
|
|
310
439
|
function profileDir(alias) {
|
|
311
440
|
return path.join(os.homedir(), '.atel', 'agents', alias);
|
|
@@ -332,6 +461,12 @@ function upsertAgent(alias, e) {
|
|
|
332
461
|
fs.mkdirSync(path.dirname(agentsRegistryPath()), { recursive: true });
|
|
333
462
|
fs.writeFileSync(agentsRegistryPath(), JSON.stringify({ agents: all }, null, 2));
|
|
334
463
|
}
|
|
464
|
+
function removeAgent(alias) {
|
|
465
|
+
const all = readAgents();
|
|
466
|
+
delete all[alias];
|
|
467
|
+
fs.mkdirSync(path.dirname(agentsRegistryPath()), { recursive: true });
|
|
468
|
+
fs.writeFileSync(agentsRegistryPath(), JSON.stringify({ agents: all }, null, 2));
|
|
469
|
+
}
|
|
335
470
|
function aliasFromFlags(flags) {
|
|
336
471
|
if (flags['profile'])
|
|
337
472
|
return flags['profile'];
|
|
@@ -339,6 +474,29 @@ function aliasFromFlags(flags) {
|
|
|
339
474
|
return path.basename(path.resolve(flags['data-dir']));
|
|
340
475
|
return 'default';
|
|
341
476
|
}
|
|
477
|
+
function withConnectDefaults(flags) {
|
|
478
|
+
const out = { ...flags };
|
|
479
|
+
if (out['auto'] === 'true' || out['auto'] === '1') {
|
|
480
|
+
delete out['auto'];
|
|
481
|
+
}
|
|
482
|
+
if (!out['profile'] && !out['data-dir']) {
|
|
483
|
+
out['profile'] = out['runtime'] || process.env.ATEL_RUNTIME || 'default';
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
function singleAgentDataDirOrNull() {
|
|
488
|
+
const all = readAgents();
|
|
489
|
+
const entries = Object.values(all);
|
|
490
|
+
if (entries.length === 1 && entries[0]?.dataDir)
|
|
491
|
+
return entries[0].dataDir;
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
function describeProfileSelectionError() {
|
|
495
|
+
const aliases = Object.keys(readAgents());
|
|
496
|
+
if (aliases.length === 0)
|
|
497
|
+
return '本机还没有 ATEL profile,请先跑 atel connect。';
|
|
498
|
+
return `本机有多个 ATEL profile: ${aliases.join(', ')}。请加 --profile <别名> 或 --agent <别名>。`;
|
|
499
|
+
}
|
|
342
500
|
// --agent <alias> 把后续命令(whoami/doctor)解析到该 agent 的 data-dir。
|
|
343
501
|
function resolveDataDir(flags) {
|
|
344
502
|
if (flags['agent']) {
|
|
@@ -348,6 +506,14 @@ function resolveDataDir(flags) {
|
|
|
348
506
|
err(`❌ agents.json 里没有 alias "${flags['agent']}"。用 \`atel-agent list\` 看已有的。`);
|
|
349
507
|
process.exit(1);
|
|
350
508
|
}
|
|
509
|
+
if (!flags['profile'] && !flags['data-dir']) {
|
|
510
|
+
const legacy = path.join(os.homedir(), '.atel');
|
|
511
|
+
if (fs.existsSync(configPath(legacy)))
|
|
512
|
+
return legacy;
|
|
513
|
+
const only = singleAgentDataDirOrNull();
|
|
514
|
+
if (only)
|
|
515
|
+
return only;
|
|
516
|
+
}
|
|
351
517
|
return dataDirFrom(flags);
|
|
352
518
|
}
|
|
353
519
|
// A10:扫描已知 identity 路径,非 0600 自动收紧 + 同 DID 多份私钥副本告警。
|
|
@@ -392,11 +558,13 @@ function auditIdentityPerms() {
|
|
|
392
558
|
* atel-mcp-openclaw 插件提供(本接入器不装)。SKILL 仅落在
|
|
393
559
|
* dataDir,普通 cwd 不自动加载 —— 所以交互态要让 agent 先读 dataDir/SKILL.md。
|
|
394
560
|
*/
|
|
395
|
-
function printUsageModes(cfg, adapter) {
|
|
561
|
+
function printUsageModes(cfg, adapter, passiveStarted = true) {
|
|
396
562
|
const skillPath = path.join(cfg.dataDir, 'SKILL.md');
|
|
397
563
|
log('');
|
|
398
564
|
log('📌 两种使用模式(请知悉,避免"新会话里找不到 ATEL"):');
|
|
399
|
-
log(
|
|
565
|
+
log(passiveStarted
|
|
566
|
+
? ' ① 被动·自动接单:companion 守护已起,无人值守自动推进 A2A/A2B —— 这半不需要你干预。'
|
|
567
|
+
: ` ① 被动·自动接单:本次使用了 --no-start,companion 尚未启动。需要被动接单时运行 atel run --data-dir ${cfg.dataDir} 或重新 connect 不带 --no-start。`);
|
|
400
568
|
log(' ② 主动·你自己新开会话直接用 ATEL:新会话默认不一定带上身份/工具,按 runtime:');
|
|
401
569
|
if (adapter.id === 'openclaw') {
|
|
402
570
|
log(' • OpenClaw:交互态 atel_* 工具由 atel-mcp-openclaw 插件提供(本接入器只装被动 companion + 身份)。');
|
|
@@ -414,7 +582,14 @@ function printUsageModes(cfg, adapter) {
|
|
|
414
582
|
}
|
|
415
583
|
function buildConfig(flags, bools, runtime) {
|
|
416
584
|
const testnet = bools.has('server') || flags['env'] === 'testnet';
|
|
585
|
+
const now = new Date().toISOString();
|
|
417
586
|
const base = {
|
|
587
|
+
schemaVersion: 2,
|
|
588
|
+
profile: aliasFromFlags(flags),
|
|
589
|
+
agentName: flags['name'],
|
|
590
|
+
capabilities: parseCapabilities(flags['capabilities']),
|
|
591
|
+
createdAt: now,
|
|
592
|
+
updatedAt: now,
|
|
418
593
|
runtime,
|
|
419
594
|
runtimeAgentId: flags['runtime-agent-id'], // A9:openclaw 多 agent 隔离(默认 main)
|
|
420
595
|
dataDir: dataDirFrom(flags),
|
|
@@ -470,6 +645,25 @@ function codexHomeWritable() {
|
|
|
470
645
|
return false;
|
|
471
646
|
}
|
|
472
647
|
}
|
|
648
|
+
function isCodexSandbox() {
|
|
649
|
+
if (process.env.CODEX_SANDBOX_NETWORK_DISABLED || process.env.CODEX_THREAD_ID)
|
|
650
|
+
return true;
|
|
651
|
+
try {
|
|
652
|
+
const cmdline = fs.readFileSync('/proc/1/cmdline', 'utf8');
|
|
653
|
+
return /codex-linux-sandbox|bwrap/.test(cmdline);
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function requireOutsideCodexSandbox(action) {
|
|
660
|
+
if (!isCodexSandbox())
|
|
661
|
+
return;
|
|
662
|
+
err(`❌ ${action} 需要访问 ATEL 远端 MCP/写宿主配置,当前正在 Codex 沙箱内执行。`);
|
|
663
|
+
err(' 请让 Codex 以“沙箱外/提升权限”重跑同一条命令,或由人类在普通终端执行。');
|
|
664
|
+
err(' 在 Codex 里应请求授权运行该命令,不要继续重试普通沙箱命令。');
|
|
665
|
+
process.exit(7);
|
|
666
|
+
}
|
|
473
667
|
async function cmdInstall(flags, bools) {
|
|
474
668
|
log(`▶ @atel-ai/agent v${VERSION}`); // AC-11:打印实际版本
|
|
475
669
|
validateInputs(flags); // A6:拦截文档占位符 / 空 / 非法 capability
|
|
@@ -528,12 +722,26 @@ async function cmdInstall(flags, bools) {
|
|
|
528
722
|
}
|
|
529
723
|
const cfg = buildConfig(flags, bools, adapter.id);
|
|
530
724
|
saveConfig(cfg);
|
|
725
|
+
const capturedEnv = captureRuntimeEnv(cfg);
|
|
726
|
+
if (capturedEnv.length) {
|
|
727
|
+
log(`🔐 已保存 runtime 环境到 ${runtimeEnvPath(cfg.dataDir)}(${capturedEnv.join(', ')})`);
|
|
728
|
+
}
|
|
531
729
|
// 双身份统一(openclaw):被动 companion 必须与插件交互用**同一个 DID** —— 一个 agent 不能两个身份
|
|
532
730
|
// (否则接单收益记在 onboard 的 DID、聊天里主动操作记在插件的 DID,钱/声誉算两处)。onboard 时若插件
|
|
533
731
|
// 已有身份,**复用它**而不是新建。只在 data-dir 还没身份时 adopt(re-onboard 不动)。
|
|
534
732
|
const preExisted = fs.existsSync(cfg.identityPath);
|
|
535
733
|
const reconcile = reconcileOpenclawIdentity(cfg); // adopted(新装复用)| migrated(存量迁移)| none
|
|
536
734
|
const { identity } = ensureIdentity(cfg);
|
|
735
|
+
const alias = aliasFromFlags(flags);
|
|
736
|
+
const capabilities = parseCapabilities(flags['capabilities']);
|
|
737
|
+
const existingBinding = readBinding(cfg.dataDir);
|
|
738
|
+
try {
|
|
739
|
+
assertBindingCompatible(existingBinding, { profile: alias, runtime: cfg.runtime, did: identity.did });
|
|
740
|
+
}
|
|
741
|
+
catch (e) {
|
|
742
|
+
err(`❌ 身份绑定冲突:${e.message}`);
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
537
745
|
// A2:真 re-onboard(已有**同一**身份)+ 传 --name → 硬闸,避免静默改名。迁移(身份已换成插件的)不触发。
|
|
538
746
|
if (preExisted && reconcile !== 'migrated') {
|
|
539
747
|
log(`♻️ 复用 data-dir(${cfg.dataDir})已有身份 ${identity.did}`);
|
|
@@ -589,10 +797,6 @@ async function cmdInstall(flags, bools) {
|
|
|
589
797
|
}
|
|
590
798
|
// 注册/更新本 agent 到 registry(可被发现 + 可作为 A2A 接活方/雇主目标)
|
|
591
799
|
if (flags['name']) {
|
|
592
|
-
const capabilities = (flags['capabilities'] || 'writing')
|
|
593
|
-
.split(',')
|
|
594
|
-
.map((s) => s.trim())
|
|
595
|
-
.filter(Boolean);
|
|
596
800
|
try {
|
|
597
801
|
const r = await registryRemoteRegister(cfg.platformBaseUrl, jwt, {
|
|
598
802
|
name: flags['name'],
|
|
@@ -616,6 +820,25 @@ async function cmdInstall(flags, bools) {
|
|
|
616
820
|
else {
|
|
617
821
|
log('ℹ️ 未传 --name,跳过 registry 注册(仅主动调工具不需要;要被搜到/当接活方目标则需注册)');
|
|
618
822
|
}
|
|
823
|
+
const now = new Date().toISOString();
|
|
824
|
+
writeBinding(cfg.dataDir, {
|
|
825
|
+
schemaVersion: 1,
|
|
826
|
+
profile: alias,
|
|
827
|
+
runtime: cfg.runtime,
|
|
828
|
+
runtimeAgentId: cfg.runtimeAgentId,
|
|
829
|
+
did: identity.did,
|
|
830
|
+
agentName: flags['name'],
|
|
831
|
+
capabilities,
|
|
832
|
+
runtimeFingerprint: {
|
|
833
|
+
version: chosen.result.version,
|
|
834
|
+
binPath: chosen.result.binPath,
|
|
835
|
+
},
|
|
836
|
+
machineFingerprint: machineFingerprint(),
|
|
837
|
+
createdAt: existingBinding?.createdAt || now,
|
|
838
|
+
updatedAt: now,
|
|
839
|
+
lastVerifiedAt: now,
|
|
840
|
+
});
|
|
841
|
+
log(`🔗 身份绑定已写入 ${bindingPath(cfg.dataDir)}`);
|
|
619
842
|
writeSkill(cfg, adapter);
|
|
620
843
|
// 一次性安装步骤(如 hermes 必须 mcp add 启用工具)
|
|
621
844
|
if (adapter.onInstall) {
|
|
@@ -636,11 +859,13 @@ async function cmdInstall(flags, bools) {
|
|
|
636
859
|
// 否则同一个 DID 有两个 relay 消费者(插件 listener + 我的 companion),订单会被随机分走/重复 ack。
|
|
637
860
|
const openclawPluginActive = cfg.runtime === 'openclaw'
|
|
638
861
|
&& fs.existsSync(path.join(os.homedir(), '.openclaw', 'extensions', 'atel-mcp-openclaw'));
|
|
862
|
+
let passiveStarted = true;
|
|
639
863
|
if (openclawPluginActive) {
|
|
640
864
|
log(' OpenClaw 被动接单由 atel-mcp-openclaw 插件 listener 负责(DID-Sig,已确保在跑)。');
|
|
641
865
|
log(' ✅ 不另起 companion —— 避免同一 DID 两个消费者抢同一 relay 队列(被动与交互统一一个身份)。');
|
|
642
866
|
}
|
|
643
867
|
else if (bools.has('no-start')) {
|
|
868
|
+
passiveStarted = false;
|
|
644
869
|
log(` (--no-start)请自行启动并守护: npx @atel-ai/agent run --data-dir ${cfg.dataDir}`);
|
|
645
870
|
}
|
|
646
871
|
else {
|
|
@@ -650,15 +875,23 @@ async function cmdInstall(flags, bools) {
|
|
|
650
875
|
}
|
|
651
876
|
}
|
|
652
877
|
// A8:写入 ~/.atel/agents.json 映射(供 `atel-agent list` / `--agent <alias>`)。
|
|
653
|
-
|
|
878
|
+
const existingAgent = readAgents()[alias];
|
|
879
|
+
upsertAgent(alias, {
|
|
880
|
+
schemaVersion: 2,
|
|
881
|
+
profile: alias,
|
|
654
882
|
did: identity.did,
|
|
655
883
|
runtime: cfg.runtime,
|
|
656
884
|
runtimeAgentId: cfg.runtimeAgentId,
|
|
885
|
+
name: flags['name'] || existingAgent?.name,
|
|
886
|
+
capabilities,
|
|
657
887
|
dataDir: cfg.dataDir,
|
|
888
|
+
identityPath: cfg.identityPath,
|
|
889
|
+
bindingPath: bindingPath(cfg.dataDir),
|
|
658
890
|
service: systemdUnitName(cfg.dataDir),
|
|
891
|
+
createdAt: existingAgent?.createdAt || now,
|
|
659
892
|
updatedAt: new Date().toISOString(),
|
|
660
893
|
});
|
|
661
|
-
printUsageModes(cfg, adapter);
|
|
894
|
+
printUsageModes(cfg, adapter, passiveStarted);
|
|
662
895
|
}
|
|
663
896
|
/**
|
|
664
897
|
* 收单回路自检(②):平台能不能把订单真的投递给本 agent?
|
|
@@ -670,7 +903,7 @@ async function cmdInstall(flags, bools) {
|
|
|
670
903
|
* 探针 → 用 companion 同款的 relayPoll 取回 → ack。取不回 = 收单回路不通,当场报红。
|
|
671
904
|
*/
|
|
672
905
|
async function checkReceivePath(platformBaseUrl, identity) {
|
|
673
|
-
const nonce = `doctor-probe-${
|
|
906
|
+
const nonce = `doctor-probe-${crypto.randomUUID()}`;
|
|
674
907
|
const send = await postSigned(platformBaseUrl, '/relay/v1/send', identity, { target: identity.did, message: { kind: 'delivery_probe', nonce } }, 'relay/send');
|
|
675
908
|
if (!send.ok)
|
|
676
909
|
return { ok: false, detail: `探针发送失败 HTTP ${send.status}: ${send.body.slice(0, 120)}` };
|
|
@@ -698,11 +931,16 @@ async function checkReceivePath(platformBaseUrl, identity) {
|
|
|
698
931
|
}
|
|
699
932
|
async function cmdDoctor(flags, bools) {
|
|
700
933
|
const dataDir = resolveDataDir(flags); // 支持 --agent <alias>
|
|
934
|
+
Object.assign(process.env, loadRuntimeEnv(dataDir));
|
|
701
935
|
const cfg = loadConfig(dataDir);
|
|
702
936
|
if (!cfg) {
|
|
703
937
|
err('❌ 未找到配置,先跑 install。');
|
|
704
938
|
process.exit(1);
|
|
705
939
|
}
|
|
940
|
+
if (bools.has('json')) {
|
|
941
|
+
await cmdDoctorJson(dataDir, cfg, bools);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
706
944
|
let allGreen = true;
|
|
707
945
|
let anyYellow = false;
|
|
708
946
|
// AC-16:把检查归到 6 大接入面,末尾给"完整接入"硬门。worst-status wins(红>黄>绿)。
|
|
@@ -737,6 +975,18 @@ async function cmdDoctor(flags, bools) {
|
|
|
737
975
|
if (!identity) {
|
|
738
976
|
process.exit(1);
|
|
739
977
|
}
|
|
978
|
+
const binding = readBinding(dataDir);
|
|
979
|
+
if (!binding) {
|
|
980
|
+
softCheck(false, '身份绑定(binding.json)', `缺 ${bindingPath(dataDir)} —— 旧安装可用,但产品级强绑定需要重新 connect`, '身份绑定');
|
|
981
|
+
}
|
|
982
|
+
else if (binding.did !== identity.did || binding.runtime !== cfg.runtime) {
|
|
983
|
+
check(false, '身份绑定(binding.json)', `不一致:binding did=${binding.did},runtime=${binding.runtime};当前 did=${identity.did},runtime=${cfg.runtime}`, '身份绑定');
|
|
984
|
+
}
|
|
985
|
+
else {
|
|
986
|
+
softCheck(true, '身份绑定(binding.json)', `profile=${binding.profile},runtime=${binding.runtime},did=${binding.did}`, '身份绑定');
|
|
987
|
+
}
|
|
988
|
+
const envReady = runtimeEnvReadiness(cfg);
|
|
989
|
+
softCheck(envReady.ok, '模型环境(runtime.env)', envReady.detail, '模型环境');
|
|
740
990
|
// 适配器在场
|
|
741
991
|
const adapter = getAdapter(cfg.runtime);
|
|
742
992
|
let det = adapter ? await adapter.detect() : { present: false };
|
|
@@ -883,6 +1133,99 @@ async function cmdDoctor(flags, bools) {
|
|
|
883
1133
|
? '✅ 完整接入成功(被动接单 + 交互态新会话都就绪)。'
|
|
884
1134
|
: '🟢 核心通过(被动接单可用);⚠️ 交互态见上方 🟡 —— 自己开新会话用 ATEL 需按提示处理。');
|
|
885
1135
|
}
|
|
1136
|
+
async function cmdDoctorJson(dataDir, cfg, bools) {
|
|
1137
|
+
const checks = {};
|
|
1138
|
+
let ok = true;
|
|
1139
|
+
let identity = null;
|
|
1140
|
+
try {
|
|
1141
|
+
identity = loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8')));
|
|
1142
|
+
checks.identity = { ok: true, did: identity.did, identityPath: cfg.identityPath };
|
|
1143
|
+
}
|
|
1144
|
+
catch (e) {
|
|
1145
|
+
ok = false;
|
|
1146
|
+
checks.identity = { ok: false, error: e.message, identityPath: cfg.identityPath };
|
|
1147
|
+
}
|
|
1148
|
+
const binding = readBinding(dataDir);
|
|
1149
|
+
checks.binding = {
|
|
1150
|
+
ok: !!binding && !!identity && binding.did === identity.did && binding.runtime === cfg.runtime,
|
|
1151
|
+
path: bindingPath(dataDir),
|
|
1152
|
+
binding,
|
|
1153
|
+
};
|
|
1154
|
+
if (!checks.binding.ok)
|
|
1155
|
+
ok = false;
|
|
1156
|
+
const envReady = runtimeEnvReadiness(cfg);
|
|
1157
|
+
checks.runtimeEnv = envReady;
|
|
1158
|
+
if (!envReady.ok)
|
|
1159
|
+
ok = false;
|
|
1160
|
+
const adapter = getAdapter(cfg.runtime);
|
|
1161
|
+
let det = null;
|
|
1162
|
+
try {
|
|
1163
|
+
det = adapter ? await adapter.detect() : null;
|
|
1164
|
+
checks.adapter = { ok: !!adapter && !!det?.present, runtime: cfg.runtime, detect: det };
|
|
1165
|
+
if (!adapter || !det?.present)
|
|
1166
|
+
ok = false;
|
|
1167
|
+
}
|
|
1168
|
+
catch (e) {
|
|
1169
|
+
ok = false;
|
|
1170
|
+
checks.adapter = { ok: false, runtime: cfg.runtime, error: e.message };
|
|
1171
|
+
}
|
|
1172
|
+
if (identity) {
|
|
1173
|
+
try {
|
|
1174
|
+
const r = await spineIdentityVerify(cfg.spineBaseUrl, identity);
|
|
1175
|
+
checks.jwt = { ok: !!r.token, expiresAt: new Date(r.expiresAt).toISOString() };
|
|
1176
|
+
if (!r.token)
|
|
1177
|
+
ok = false;
|
|
1178
|
+
}
|
|
1179
|
+
catch (e) {
|
|
1180
|
+
ok = false;
|
|
1181
|
+
checks.jwt = { ok: false, error: e.message };
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
const b = await spineWalletBalance(cfg.spineBaseUrl, identity);
|
|
1185
|
+
checks.balance = { ok: b.ok, status: b.status, json: b.json };
|
|
1186
|
+
if (!b.ok)
|
|
1187
|
+
ok = false;
|
|
1188
|
+
}
|
|
1189
|
+
catch (e) {
|
|
1190
|
+
ok = false;
|
|
1191
|
+
checks.balance = { ok: false, error: e.message };
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
const rp = await checkReceivePath(cfg.platformBaseUrl, identity);
|
|
1195
|
+
checks.receivePath = rp;
|
|
1196
|
+
if (!rp.ok)
|
|
1197
|
+
ok = false;
|
|
1198
|
+
}
|
|
1199
|
+
catch (e) {
|
|
1200
|
+
ok = false;
|
|
1201
|
+
checks.receivePath = { ok: false, detail: e.message };
|
|
1202
|
+
}
|
|
1203
|
+
if (bools.has('probe') && adapter && det?.present) {
|
|
1204
|
+
try {
|
|
1205
|
+
const waker = adapter.createWaker({ config: cfg, identity, log: () => { } });
|
|
1206
|
+
const res = await waker.wake({ prompt: '调用 ATEL MCP 工具 atel_whoami,只回复返回的 did 字符串本身,别的都不要说。' });
|
|
1207
|
+
checks.agentProbe = { ok: (res.output || '').includes(identity.did), output: res.output, error: res.error, durationMs: res.durationMs };
|
|
1208
|
+
if (!checks.agentProbe.ok)
|
|
1209
|
+
ok = false;
|
|
1210
|
+
}
|
|
1211
|
+
catch (e) {
|
|
1212
|
+
ok = false;
|
|
1213
|
+
checks.agentProbe = { ok: false, error: e.message };
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
checks.companion = companionStatus(dataDir);
|
|
1218
|
+
const result = {
|
|
1219
|
+
ok,
|
|
1220
|
+
profile: cfg.profile || path.basename(dataDir),
|
|
1221
|
+
runtime: cfg.runtime,
|
|
1222
|
+
dataDir,
|
|
1223
|
+
checks,
|
|
1224
|
+
};
|
|
1225
|
+
log(JSON.stringify(result, null, 2));
|
|
1226
|
+
if (!ok)
|
|
1227
|
+
process.exit(1);
|
|
1228
|
+
}
|
|
886
1229
|
/** onboard = install + 后台起 companion(一步到位,喂给 agent 一句话即可)。 */
|
|
887
1230
|
async function cmdOnboard(flags, bools) {
|
|
888
1231
|
// cmdInstall 末尾已 superviseCompanion 守护 companion(systemd --user 或 detached)。
|
|
@@ -890,23 +1233,71 @@ async function cmdOnboard(flags, bools) {
|
|
|
890
1233
|
await cmdInstall(flags, bools);
|
|
891
1234
|
const dataDir = dataDirFrom(flags);
|
|
892
1235
|
log('');
|
|
893
|
-
|
|
1236
|
+
if (bools.has('no-start')) {
|
|
1237
|
+
log('🚀 onboard 完成:本次 --no-start,companion 未启动。');
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
log('🚀 onboard 完成:companion 已守护启动,正在无人值守自动推进 A2A/A2B。');
|
|
1241
|
+
}
|
|
894
1242
|
log(` 日志:${path.join(dataDir, 'companion.log')}`);
|
|
895
1243
|
log(` 状态:systemctl --user status 'atel-agent*' 或 ps aux | grep "run.*${dataDir}"`);
|
|
896
1244
|
}
|
|
1245
|
+
async function cmdConnect(flags, bools) {
|
|
1246
|
+
const next = withConnectDefaults(flags);
|
|
1247
|
+
if (bools.has('auto') && !next['runtime']) {
|
|
1248
|
+
const caller = detectCallerRuntime();
|
|
1249
|
+
if (!caller) {
|
|
1250
|
+
err('❌ --auto 无法判断当前 runtime。');
|
|
1251
|
+
err(' 请明确选择一个 runtime 重跑:');
|
|
1252
|
+
for (const a of ADAPTERS) {
|
|
1253
|
+
const profile = a.id === 'claude-code' ? 'claude'
|
|
1254
|
+
: a.id === 'gemini-cli' ? 'gemini'
|
|
1255
|
+
: a.id === 'qwen-agent' ? 'qwen'
|
|
1256
|
+
: a.id;
|
|
1257
|
+
err(` atel connect --runtime ${a.id} --profile ${profile} --name "${next['name'] || 'my-agent'}" --capabilities ${next['capabilities'] || 'writing'}`);
|
|
1258
|
+
}
|
|
1259
|
+
process.exit(1);
|
|
1260
|
+
}
|
|
1261
|
+
next['runtime'] = caller;
|
|
1262
|
+
}
|
|
1263
|
+
if (bools.has('auto') && !flags['profile'] && !flags['data-dir'] && next['runtime']) {
|
|
1264
|
+
next['profile'] = next['runtime'] === 'claude-code' ? 'claude'
|
|
1265
|
+
: next['runtime'] === 'gemini-cli' ? 'gemini'
|
|
1266
|
+
: next['runtime'] === 'qwen-agent' ? 'qwen'
|
|
1267
|
+
: next['runtime'];
|
|
1268
|
+
}
|
|
1269
|
+
await cmdOnboard(next, bools);
|
|
1270
|
+
}
|
|
897
1271
|
/** companion 自报 pid 的文件:cmdRun(companion 本体)写入,doctor 读它判活。 */
|
|
898
1272
|
function companionPidPath(dataDir) {
|
|
899
1273
|
return path.join(dataDir, 'companion.pid');
|
|
900
1274
|
}
|
|
901
1275
|
/** 后台拉起 companion(detached,父进程退出后存活;跨平台,不依赖 systemd)。 */
|
|
1276
|
+
function waitForCompanionRunning(dataDir, expectedPid) {
|
|
1277
|
+
for (let i = 0; i < 20; i++) {
|
|
1278
|
+
const st = companionStatus(dataDir);
|
|
1279
|
+
if (st.state === 'running' && (!expectedPid || st.pid === expectedPid))
|
|
1280
|
+
return;
|
|
1281
|
+
try {
|
|
1282
|
+
execFileSync('sleep', ['0.25'], { stdio: 'ignore' });
|
|
1283
|
+
}
|
|
1284
|
+
catch { /* ignore */ }
|
|
1285
|
+
}
|
|
1286
|
+
const st = companionStatus(dataDir);
|
|
1287
|
+
throw new Error(`companion 未保持运行:${st.detail}`);
|
|
1288
|
+
}
|
|
902
1289
|
function startCompanionDetached(dataDir) {
|
|
903
1290
|
const logPath = path.join(dataDir, 'companion.log');
|
|
904
1291
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
905
1292
|
const out = fs.openSync(logPath, 'a');
|
|
906
1293
|
const cli = resolveStableCliPath(); // 不能把 npx 临时缓存路径喂给长期 detached 进程
|
|
907
|
-
const
|
|
1294
|
+
const childEnv = { ...process.env, ...loadRuntimeEnv(dataDir) };
|
|
1295
|
+
const child = spawn(cli ? process.execPath : 'npx', cli ? [cli, 'run', '--data-dir', dataDir] : ['-y', `@atel-ai/agent@${VERSION}`, 'run', '--data-dir', dataDir], { detached: true, stdio: ['ignore', out, out], env: childEnv });
|
|
908
1296
|
child.unref();
|
|
909
1297
|
log(`▶️ companion 后台运行 pid=${child.pid}`);
|
|
1298
|
+
if (!child.pid)
|
|
1299
|
+
throw new Error('companion 子进程未返回 pid');
|
|
1300
|
+
return child.pid;
|
|
910
1301
|
}
|
|
911
1302
|
/**
|
|
912
1303
|
* 让 companion 无人值守长活。Linux 上装 systemd --user 单元(Restart=always,崩溃/
|
|
@@ -917,16 +1308,19 @@ function startCompanionDetached(dataDir) {
|
|
|
917
1308
|
* 重启带走 companion 后再没收单。守护是收单可靠性的前提,不能依赖人记得做。
|
|
918
1309
|
*/
|
|
919
1310
|
function superviseCompanion(dataDir) {
|
|
1311
|
+
requireOutsideCodexSandbox('启动 ATEL companion');
|
|
920
1312
|
if (process.platform === 'linux' && hasSystemctlUser()) {
|
|
921
1313
|
try {
|
|
922
1314
|
installSystemdUserUnit(dataDir);
|
|
1315
|
+
waitForCompanionRunning(dataDir);
|
|
923
1316
|
return 'systemd';
|
|
924
1317
|
}
|
|
925
1318
|
catch (e) {
|
|
926
1319
|
err(`⚠️ systemd 守护安装失败(${e.message}),回退后台进程。`);
|
|
927
1320
|
}
|
|
928
1321
|
}
|
|
929
|
-
startCompanionDetached(dataDir);
|
|
1322
|
+
const pid = startCompanionDetached(dataDir);
|
|
1323
|
+
waitForCompanionRunning(dataDir, pid);
|
|
930
1324
|
return 'detached';
|
|
931
1325
|
}
|
|
932
1326
|
/**
|
|
@@ -1015,6 +1409,37 @@ async function cmdMcp(flags) {
|
|
|
1015
1409
|
await runMcpShim(dataDir, VERSION);
|
|
1016
1410
|
await new Promise(() => { }); // 保活(stdio server 由宿主 CLI 生命周期驱动)
|
|
1017
1411
|
}
|
|
1412
|
+
async function withLocalMcp(dataDir, fn) {
|
|
1413
|
+
requireOutsideCodexSandbox('ATEL MCP 工具调用');
|
|
1414
|
+
const launcher = mcpLauncher(dataDir);
|
|
1415
|
+
const transport = new StdioClientTransport({
|
|
1416
|
+
command: launcher.command,
|
|
1417
|
+
args: launcher.args,
|
|
1418
|
+
env: { ...cleanEnv(process.env), ...loadRuntimeEnv(dataDir) },
|
|
1419
|
+
cwd: dataDir,
|
|
1420
|
+
stderr: 'pipe',
|
|
1421
|
+
});
|
|
1422
|
+
let stderr = '';
|
|
1423
|
+
transport.stderr?.on('data', (d) => {
|
|
1424
|
+
stderr += d.toString();
|
|
1425
|
+
});
|
|
1426
|
+
const client = new Client({ name: 'atel-cli-probe', version: VERSION }, { capabilities: {} });
|
|
1427
|
+
try {
|
|
1428
|
+
await client.connect(transport);
|
|
1429
|
+
return await fn(client);
|
|
1430
|
+
}
|
|
1431
|
+
catch (e) {
|
|
1432
|
+
const detail = stderr.trim();
|
|
1433
|
+
const msg = e.message;
|
|
1434
|
+
const hint = /Connection closed|ECONN|fetch failed|network/i.test(`${msg}\n${detail}`) && isCodexSandbox()
|
|
1435
|
+
? '\n提示:当前在 Codex 沙箱内,请授权沙箱外执行同一条 atel 命令。'
|
|
1436
|
+
: '';
|
|
1437
|
+
throw new Error(`${msg}${hint}${detail ? `\nMCP stderr:\n${detail.slice(-1500)}` : ''}`);
|
|
1438
|
+
}
|
|
1439
|
+
finally {
|
|
1440
|
+
await client.close().catch(() => { });
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1018
1443
|
function installSystemdUserUnit(dataDir) {
|
|
1019
1444
|
const unitName = systemdUnitName(dataDir);
|
|
1020
1445
|
const unitDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
@@ -1029,6 +1454,7 @@ function installSystemdUserUnit(dataDir) {
|
|
|
1029
1454
|
}
|
|
1030
1455
|
}
|
|
1031
1456
|
const logPath = path.join(dataDir, 'companion.log');
|
|
1457
|
+
const envPath = runtimeEnvPath(dataDir);
|
|
1032
1458
|
const cliPath = resolveStableCliPath(); // 不能把 npx 临时缓存路径写进 systemd 单元
|
|
1033
1459
|
// systemd 默认 PATH 很窄(/usr/bin:/bin),companion 要 spawn 底座 CLI(openclaw/claude/
|
|
1034
1460
|
// codex/gemini/hermes)就会 ENOENT(2026-06-12 BigBaby:openclaw 在 pnpm 全局 bin,不在
|
|
@@ -1051,6 +1477,7 @@ function installSystemdUserUnit(dataDir) {
|
|
|
1051
1477
|
'[Service]',
|
|
1052
1478
|
'Type=simple',
|
|
1053
1479
|
`Environment=PATH=${fullPath}`,
|
|
1480
|
+
`EnvironmentFile=-${envPath}`,
|
|
1054
1481
|
`ExecStart=${cliPath ? `${process.execPath} ${cliPath}` : `npx -y @atel-ai/agent@${VERSION}`} run --data-dir ${dataDir}`,
|
|
1055
1482
|
'Restart=always',
|
|
1056
1483
|
'RestartSec=5',
|
|
@@ -1077,6 +1504,7 @@ function installSystemdUserUnit(dataDir) {
|
|
|
1077
1504
|
}
|
|
1078
1505
|
async function cmdRun(flags) {
|
|
1079
1506
|
const dataDir = dataDirFrom(flags);
|
|
1507
|
+
Object.assign(process.env, loadRuntimeEnv(dataDir));
|
|
1080
1508
|
const cfg = loadConfig(dataDir);
|
|
1081
1509
|
if (!cfg) {
|
|
1082
1510
|
err('❌ 未找到配置,先跑 install。');
|
|
@@ -1137,6 +1565,306 @@ function cmdUpdate() {
|
|
|
1137
1565
|
process.exit(1);
|
|
1138
1566
|
}
|
|
1139
1567
|
}
|
|
1568
|
+
async function rewriteRuntimeConfig(dataDir) {
|
|
1569
|
+
Object.assign(process.env, loadRuntimeEnv(dataDir));
|
|
1570
|
+
const cfg = loadConfig(dataDir);
|
|
1571
|
+
if (!cfg)
|
|
1572
|
+
throw new Error('未找到 config.json');
|
|
1573
|
+
const adapter = getAdapter(cfg.runtime);
|
|
1574
|
+
if (!adapter)
|
|
1575
|
+
throw new Error(`未知 runtime ${cfg.runtime}`);
|
|
1576
|
+
const identity = loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8')));
|
|
1577
|
+
const auth = new AuthManager(identity, cfg.spineBaseUrl, {
|
|
1578
|
+
onToken: (token) => adapter.writeMcpConfig({ mcpUrl: cfg.mcpUrl, token, identity, config: cfg, mcpCommand: mcpLauncher(cfg.dataDir) }),
|
|
1579
|
+
log,
|
|
1580
|
+
});
|
|
1581
|
+
await auth.start();
|
|
1582
|
+
auth.stop();
|
|
1583
|
+
writeSkill(cfg, adapter);
|
|
1584
|
+
}
|
|
1585
|
+
async function cmdRepair(flags) {
|
|
1586
|
+
const dataDir = resolveDataDir(flags);
|
|
1587
|
+
try {
|
|
1588
|
+
await rewriteRuntimeConfig(dataDir);
|
|
1589
|
+
log('🟢 repair 完成:MCP 配置与 SKILL 已重写。');
|
|
1590
|
+
}
|
|
1591
|
+
catch (e) {
|
|
1592
|
+
err(`❌ repair 失败:${e.message}`);
|
|
1593
|
+
process.exit(1);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
function removeTomlSection(file, section, mustContain) {
|
|
1597
|
+
let content = '';
|
|
1598
|
+
try {
|
|
1599
|
+
content = fs.readFileSync(file, 'utf8');
|
|
1600
|
+
}
|
|
1601
|
+
catch {
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
const header = `[${section}]`;
|
|
1605
|
+
const lines = content.split(/\r?\n/);
|
|
1606
|
+
const out = [];
|
|
1607
|
+
let changed = false;
|
|
1608
|
+
for (let i = 0; i < lines.length;) {
|
|
1609
|
+
if (lines[i].trim() === header) {
|
|
1610
|
+
let j = i + 1;
|
|
1611
|
+
while (j < lines.length && !/^\[.+\]\s*$/.test(lines[j].trim()))
|
|
1612
|
+
j++;
|
|
1613
|
+
const block = lines.slice(i, j).join('\n');
|
|
1614
|
+
if (!mustContain || block.includes(mustContain)) {
|
|
1615
|
+
changed = true;
|
|
1616
|
+
i = j;
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
out.push(lines[i++]);
|
|
1621
|
+
}
|
|
1622
|
+
if (changed)
|
|
1623
|
+
fs.writeFileSync(file, out.join('\n').replace(/\n{3,}/g, '\n\n'));
|
|
1624
|
+
return changed;
|
|
1625
|
+
}
|
|
1626
|
+
function removeJsonMcpServer(file, serverName, mustContain) {
|
|
1627
|
+
let j = {};
|
|
1628
|
+
try {
|
|
1629
|
+
j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1630
|
+
}
|
|
1631
|
+
catch {
|
|
1632
|
+
return false;
|
|
1633
|
+
}
|
|
1634
|
+
const server = j.mcpServers?.[serverName];
|
|
1635
|
+
if (!server)
|
|
1636
|
+
return false;
|
|
1637
|
+
if (mustContain && !JSON.stringify(server).includes(mustContain))
|
|
1638
|
+
return false;
|
|
1639
|
+
delete j.mcpServers[serverName];
|
|
1640
|
+
fs.writeFileSync(file, JSON.stringify(j, null, 2));
|
|
1641
|
+
return true;
|
|
1642
|
+
}
|
|
1643
|
+
function removeYamlMcpServer(file, serverName, mustContain) {
|
|
1644
|
+
let lines;
|
|
1645
|
+
try {
|
|
1646
|
+
lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
1647
|
+
}
|
|
1648
|
+
catch {
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
const parent = lines.findIndex((line) => line.trim() === 'mcp_servers:');
|
|
1652
|
+
if (parent < 0)
|
|
1653
|
+
return false;
|
|
1654
|
+
let serverStart = -1;
|
|
1655
|
+
for (let i = parent + 1; i < lines.length; i++) {
|
|
1656
|
+
const line = lines[i];
|
|
1657
|
+
if (line && !line.startsWith(' ') && !line.startsWith('\t'))
|
|
1658
|
+
break;
|
|
1659
|
+
if (new RegExp(`^\\s{2}${serverName}:\\s*$`).test(line)) {
|
|
1660
|
+
serverStart = i;
|
|
1661
|
+
break;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
if (serverStart < 0)
|
|
1665
|
+
return false;
|
|
1666
|
+
let serverEnd = lines.length;
|
|
1667
|
+
for (let i = serverStart + 1; i < lines.length; i++) {
|
|
1668
|
+
const line = lines[i];
|
|
1669
|
+
if (line && !line.startsWith(' ') && !line.startsWith('\t')) {
|
|
1670
|
+
serverEnd = i;
|
|
1671
|
+
break;
|
|
1672
|
+
}
|
|
1673
|
+
if (/^\s{2}\S[^:]*:\s*$/.test(line)) {
|
|
1674
|
+
serverEnd = i;
|
|
1675
|
+
break;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
const block = lines.slice(serverStart, serverEnd).join('\n');
|
|
1679
|
+
if (mustContain && !block.includes(mustContain))
|
|
1680
|
+
return false;
|
|
1681
|
+
const next = [...lines.slice(0, serverStart), ...lines.slice(serverEnd)];
|
|
1682
|
+
let parentStillHasChild = false;
|
|
1683
|
+
for (let i = parent + 1; i < next.length; i++) {
|
|
1684
|
+
const line = next[i];
|
|
1685
|
+
if (!line.trim() || line.trim().startsWith('#'))
|
|
1686
|
+
continue;
|
|
1687
|
+
if (!line.startsWith(' ') && !line.startsWith('\t'))
|
|
1688
|
+
break;
|
|
1689
|
+
if (/^\s{2}\S[^:]*:/.test(line)) {
|
|
1690
|
+
parentStillHasChild = true;
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (!parentStillHasChild)
|
|
1695
|
+
next.splice(parent, 1);
|
|
1696
|
+
fs.writeFileSync(file, next.join('\n').replace(/\n{3,}/g, '\n\n'));
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1699
|
+
function removeRuntimeMcpConfig(cfg) {
|
|
1700
|
+
const removed = [];
|
|
1701
|
+
const marker = cfg.dataDir;
|
|
1702
|
+
if (cfg.runtime === 'codex') {
|
|
1703
|
+
const f = path.join(os.homedir(), '.codex', 'config.toml');
|
|
1704
|
+
if (removeTomlSection(f, 'mcp_servers.atel', marker))
|
|
1705
|
+
removed.push(f);
|
|
1706
|
+
}
|
|
1707
|
+
else if (cfg.runtime === 'claude-code') {
|
|
1708
|
+
const global = path.join(os.homedir(), '.claude.json');
|
|
1709
|
+
const local = path.join(cfg.dataDir, 'claude-mcp.json');
|
|
1710
|
+
if (removeJsonMcpServer(global, 'atel', marker))
|
|
1711
|
+
removed.push(global);
|
|
1712
|
+
if (fs.existsSync(local)) {
|
|
1713
|
+
fs.rmSync(local, { force: true });
|
|
1714
|
+
removed.push(local);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
else if (cfg.runtime === 'gemini-cli') {
|
|
1718
|
+
const f = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
1719
|
+
if (removeJsonMcpServer(f, 'atel', marker))
|
|
1720
|
+
removed.push(f);
|
|
1721
|
+
}
|
|
1722
|
+
else if (cfg.runtime === 'qwen-agent') {
|
|
1723
|
+
const f = path.join(cfg.dataDir, 'qwen-mcp.json');
|
|
1724
|
+
if (fs.existsSync(f)) {
|
|
1725
|
+
fs.rmSync(f, { force: true });
|
|
1726
|
+
removed.push(f);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
else if (cfg.runtime === 'hermes') {
|
|
1730
|
+
const token = path.join(cfg.dataDir, 'hermes-mcp-token.txt');
|
|
1731
|
+
const envPath = path.join(os.homedir(), '.hermes', '.env');
|
|
1732
|
+
const yamlPath = path.join(os.homedir(), '.hermes', 'config.yaml');
|
|
1733
|
+
if (fs.existsSync(token)) {
|
|
1734
|
+
fs.rmSync(token, { force: true });
|
|
1735
|
+
removed.push(token);
|
|
1736
|
+
}
|
|
1737
|
+
if (removeYamlMcpServer(yamlPath, 'atel', cfg.mcpUrl))
|
|
1738
|
+
removed.push(yamlPath);
|
|
1739
|
+
try {
|
|
1740
|
+
let s = fs.readFileSync(envPath, 'utf8');
|
|
1741
|
+
if (s.includes('MCP_ATEL_API_KEY=')) {
|
|
1742
|
+
s = s.split(/\r?\n/).filter((line) => !line.startsWith('MCP_ATEL_API_KEY=')).join('\n');
|
|
1743
|
+
fs.writeFileSync(envPath, `${s.replace(/\n*$/, '')}\n`);
|
|
1744
|
+
removed.push(envPath);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
catch { /* ignore */ }
|
|
1748
|
+
}
|
|
1749
|
+
else if (cfg.runtime === 'openclaw') {
|
|
1750
|
+
const f = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
1751
|
+
if (removeJsonMcpServer(f, 'atel', cfg.mcpUrl))
|
|
1752
|
+
removed.push(f);
|
|
1753
|
+
}
|
|
1754
|
+
return removed;
|
|
1755
|
+
}
|
|
1756
|
+
async function cmdDisconnect(flags, bools) {
|
|
1757
|
+
const dataDir = resolveDataDir(flags);
|
|
1758
|
+
const cfg = loadConfig(dataDir);
|
|
1759
|
+
if (!cfg) {
|
|
1760
|
+
err('❌ 未找到配置。');
|
|
1761
|
+
process.exit(1);
|
|
1762
|
+
}
|
|
1763
|
+
stopCompanion(dataDir);
|
|
1764
|
+
const removedConfigs = removeRuntimeMcpConfig(cfg);
|
|
1765
|
+
const alias = cfg.profile || aliasFromFlags(flags);
|
|
1766
|
+
removeAgent(alias);
|
|
1767
|
+
if (bools.has('delete-data')) {
|
|
1768
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
1769
|
+
log(`🟡 已断开并删除 profile 数据:${dataDir}`);
|
|
1770
|
+
}
|
|
1771
|
+
else {
|
|
1772
|
+
log(`🟡 已断开 profile=${alias};身份文件仍保留在 ${dataDir}`);
|
|
1773
|
+
log(' 如需删除本地身份与配置,重跑: atel disconnect --profile <profile> --delete-data');
|
|
1774
|
+
}
|
|
1775
|
+
if (removedConfigs.length)
|
|
1776
|
+
log(` 已移除 runtime MCP 配置:${removedConfigs.join(', ')}`);
|
|
1777
|
+
}
|
|
1778
|
+
async function cmdUninstall(flags, bools) {
|
|
1779
|
+
if (flags['profile'] || flags['agent'] || flags['data-dir']) {
|
|
1780
|
+
await cmdDisconnect(flags, new Set([...bools, 'delete-data']));
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (!bools.has('confirm')) {
|
|
1784
|
+
err('❌ 这会删除本机 ~/.atel 下的 ATEL profiles。确认请加 --confirm。');
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
const all = readAgents();
|
|
1788
|
+
for (const e of Object.values(all))
|
|
1789
|
+
stopCompanion(e.dataDir);
|
|
1790
|
+
fs.rmSync(path.join(os.homedir(), '.atel'), { recursive: true, force: true });
|
|
1791
|
+
log('🟡 已删除 ~/.atel 本地数据。全局 atel 程序请用 npm uninstall -g @atel-ai/agent 或安装器卸载。');
|
|
1792
|
+
}
|
|
1793
|
+
function stopCompanion(dataDir) {
|
|
1794
|
+
const unit = systemdUnitName(dataDir);
|
|
1795
|
+
const env = userSystemctlEnv();
|
|
1796
|
+
try {
|
|
1797
|
+
execFileSync('systemctl', ['--user', 'stop', unit], { stdio: 'ignore', timeout: 10_000, env });
|
|
1798
|
+
}
|
|
1799
|
+
catch { /* not systemd or not running */ }
|
|
1800
|
+
const st = companionStatus(dataDir);
|
|
1801
|
+
if (st.pid) {
|
|
1802
|
+
try {
|
|
1803
|
+
process.kill(st.pid, 'SIGTERM');
|
|
1804
|
+
}
|
|
1805
|
+
catch { /* already gone */ }
|
|
1806
|
+
}
|
|
1807
|
+
try {
|
|
1808
|
+
fs.rmSync(companionPidPath(dataDir), { force: true });
|
|
1809
|
+
}
|
|
1810
|
+
catch { /* ignore */ }
|
|
1811
|
+
}
|
|
1812
|
+
function cmdService(flags, positionals) {
|
|
1813
|
+
const action = positionals[0] || 'status';
|
|
1814
|
+
const dataDir = resolveDataDir(flags);
|
|
1815
|
+
const cfg = loadConfig(dataDir);
|
|
1816
|
+
if (!cfg) {
|
|
1817
|
+
err('❌ 未找到配置,先跑 connect。');
|
|
1818
|
+
process.exit(1);
|
|
1819
|
+
}
|
|
1820
|
+
if (action === 'status') {
|
|
1821
|
+
const s = companionStatus(dataDir);
|
|
1822
|
+
log(`${s.state === 'running' ? '🟢' : s.state === 'stale' ? '🔴' : '🟡'} companion ${s.state} — ${s.detail}`);
|
|
1823
|
+
log(` service=${s.service}`);
|
|
1824
|
+
log(` log=${path.join(dataDir, 'companion.log')}`);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (action === 'start') {
|
|
1828
|
+
try {
|
|
1829
|
+
const mode = superviseCompanion(dataDir);
|
|
1830
|
+
log(`🟢 companion start requested (${mode})`);
|
|
1831
|
+
}
|
|
1832
|
+
catch (e) {
|
|
1833
|
+
err(`❌ companion 启动失败:${e.message}`);
|
|
1834
|
+
process.exit(1);
|
|
1835
|
+
}
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
if (action === 'stop') {
|
|
1839
|
+
stopCompanion(dataDir);
|
|
1840
|
+
log('🟡 companion stopped');
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (action === 'restart') {
|
|
1844
|
+
stopCompanion(dataDir);
|
|
1845
|
+
try {
|
|
1846
|
+
const mode = superviseCompanion(dataDir);
|
|
1847
|
+
log(`🟢 companion restarted (${mode})`);
|
|
1848
|
+
}
|
|
1849
|
+
catch (e) {
|
|
1850
|
+
err(`❌ companion 重启失败:${e.message}`);
|
|
1851
|
+
process.exit(1);
|
|
1852
|
+
}
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
if (action === 'logs' || action === 'log') {
|
|
1856
|
+
const logPath = path.join(dataDir, 'companion.log');
|
|
1857
|
+
if (!fs.existsSync(logPath)) {
|
|
1858
|
+
log(`(暂无日志 ${logPath})`);
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
const out = execFileSync('tail', ['-n', flags['lines'] || '80', logPath], { encoding: 'utf8' });
|
|
1862
|
+
process.stdout.write(out);
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
err(`❌ 未知 service 子命令:${action}。可用: status/start/stop/restart/logs`);
|
|
1866
|
+
process.exit(1);
|
|
1867
|
+
}
|
|
1140
1868
|
function cmdList() {
|
|
1141
1869
|
const all = readAgents();
|
|
1142
1870
|
const aliases = Object.keys(all);
|
|
@@ -1147,7 +1875,75 @@ function cmdList() {
|
|
|
1147
1875
|
log('本机 ATEL agents:');
|
|
1148
1876
|
for (const a of aliases) {
|
|
1149
1877
|
const e = all[a];
|
|
1150
|
-
|
|
1878
|
+
const name = e.name ? ` name=${e.name}` : '';
|
|
1879
|
+
const caps = e.capabilities?.length ? ` capabilities=${e.capabilities.join(',')}` : '';
|
|
1880
|
+
log(` • ${a} ${e.did} [${e.runtime}${e.runtimeAgentId ? '/' + e.runtimeAgentId : ''}]${name}${caps} dataDir=${e.dataDir}`);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function companionStatus(dataDir) {
|
|
1884
|
+
const service = systemdUnitName(dataDir);
|
|
1885
|
+
const pidPath = companionPidPath(dataDir);
|
|
1886
|
+
if (!fs.existsSync(pidPath))
|
|
1887
|
+
return { state: 'stopped', detail: '无 companion.pid', service };
|
|
1888
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
1889
|
+
if (!Number.isFinite(pid))
|
|
1890
|
+
return { state: 'stale', detail: `pid 文件无效:${pidPath}`, service };
|
|
1891
|
+
try {
|
|
1892
|
+
process.kill(pid, 0);
|
|
1893
|
+
return { state: 'running', detail: `pid=${pid}`, pid, service };
|
|
1894
|
+
}
|
|
1895
|
+
catch (e) {
|
|
1896
|
+
if (e.code === 'EPERM')
|
|
1897
|
+
return { state: 'running', detail: `pid=${pid}(EPERM)`, pid, service };
|
|
1898
|
+
return { state: 'stale', detail: `pid=${pid} 已退出`, pid, service };
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
function statusFor(dataDir) {
|
|
1902
|
+
const cfg = loadConfig(dataDir);
|
|
1903
|
+
const binding = readBinding(dataDir);
|
|
1904
|
+
let did = '';
|
|
1905
|
+
try {
|
|
1906
|
+
if (cfg && fs.existsSync(cfg.identityPath))
|
|
1907
|
+
did = loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8'))).did;
|
|
1908
|
+
}
|
|
1909
|
+
catch { /* ignore */ }
|
|
1910
|
+
const svc = companionStatus(dataDir);
|
|
1911
|
+
return {
|
|
1912
|
+
ok: !!cfg && !!did && !!binding && binding.did === did && binding.runtime === cfg.runtime,
|
|
1913
|
+
profile: cfg?.profile || binding?.profile || path.basename(dataDir),
|
|
1914
|
+
runtime: cfg?.runtime || binding?.runtime || null,
|
|
1915
|
+
did: did || binding?.did || null,
|
|
1916
|
+
dataDir,
|
|
1917
|
+
binding: binding ? 'present' : 'missing',
|
|
1918
|
+
companion: svc.state,
|
|
1919
|
+
companionDetail: svc.detail,
|
|
1920
|
+
service: svc.service,
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
function cmdStatus(flags, bools) {
|
|
1924
|
+
if (flags['profile'] || flags['data-dir'] || flags['agent']) {
|
|
1925
|
+
const s = statusFor(resolveDataDir(flags));
|
|
1926
|
+
if (bools.has('json'))
|
|
1927
|
+
log(JSON.stringify(s, null, 2));
|
|
1928
|
+
else {
|
|
1929
|
+
log(`${s.ok ? '🟢' : '🟡'} ${s.profile} [${s.runtime}] ${s.did || '(no did)'}`);
|
|
1930
|
+
log(` dataDir=${s.dataDir}`);
|
|
1931
|
+
log(` binding=${s.binding}, companion=${s.companion} (${s.companionDetail})`);
|
|
1932
|
+
}
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
const all = readAgents();
|
|
1936
|
+
const statuses = Object.values(all).map((e) => statusFor(e.dataDir));
|
|
1937
|
+
if (bools.has('json')) {
|
|
1938
|
+
log(JSON.stringify({ agents: statuses }, null, 2));
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (!statuses.length) {
|
|
1942
|
+
log('(还没有 agent;用 atel connect --runtime <id> --profile <profile> 接入)');
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
for (const s of statuses) {
|
|
1946
|
+
log(`${s.ok ? '🟢' : '🟡'} ${s.profile} [${s.runtime}] ${s.did || '(no did)'} companion=${s.companion}`);
|
|
1151
1947
|
}
|
|
1152
1948
|
}
|
|
1153
1949
|
function cmdWhoami(flags) {
|
|
@@ -1160,6 +1956,386 @@ function cmdWhoami(flags) {
|
|
|
1160
1956
|
const id = loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8')));
|
|
1161
1957
|
log(id.did);
|
|
1162
1958
|
}
|
|
1959
|
+
async function cmdBalance(flags) {
|
|
1960
|
+
const dataDir = resolveDataDir(flags); // 支持 --agent <alias>
|
|
1961
|
+
const cfg = loadConfig(dataDir);
|
|
1962
|
+
if (!cfg || !fs.existsSync(cfg.identityPath)) {
|
|
1963
|
+
err('❌ 未接入,先跑 connect。');
|
|
1964
|
+
process.exit(1);
|
|
1965
|
+
}
|
|
1966
|
+
const id = loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8')));
|
|
1967
|
+
try {
|
|
1968
|
+
const b = await spineWalletBalance(cfg.spineBaseUrl, id);
|
|
1969
|
+
if (!b.ok) {
|
|
1970
|
+
err(`❌ 余额查询失败 HTTP ${b.status}: ${b.body.slice(0, 200)}`);
|
|
1971
|
+
process.exit(1);
|
|
1972
|
+
}
|
|
1973
|
+
log(typeof b.json === 'object' ? JSON.stringify(b.json, null, 2) : String(b.body));
|
|
1974
|
+
}
|
|
1975
|
+
catch (e) {
|
|
1976
|
+
err(`❌ 余额查询失败:${e.message}`);
|
|
1977
|
+
process.exit(1);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
async function cmdTools(flags, bools) {
|
|
1981
|
+
const dataDir = resolveDataDir(flags);
|
|
1982
|
+
if (!loadConfig(dataDir)) {
|
|
1983
|
+
err('❌ 未找到配置,先跑 connect。');
|
|
1984
|
+
process.exit(1);
|
|
1985
|
+
}
|
|
1986
|
+
const tools = await withLocalMcp(dataDir, async (client) => (await client.listTools()).tools || []);
|
|
1987
|
+
if (bools.has('json')) {
|
|
1988
|
+
log(JSON.stringify({ tools }, null, 2));
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
log(`ATEL tools (${tools.length}):`);
|
|
1992
|
+
for (const t of tools) {
|
|
1993
|
+
const name = t.name;
|
|
1994
|
+
const desc = t.description;
|
|
1995
|
+
if (typeof name === 'string')
|
|
1996
|
+
log(` • ${name}${typeof desc === 'string' && desc ? ` — ${desc}` : ''}`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
async function cmdMcpProbe(flags, bools) {
|
|
2000
|
+
const dataDir = resolveDataDir(flags);
|
|
2001
|
+
const cfg = loadConfig(dataDir);
|
|
2002
|
+
if (!cfg) {
|
|
2003
|
+
err('❌ 未找到配置,先跑 connect。');
|
|
2004
|
+
process.exit(1);
|
|
2005
|
+
}
|
|
2006
|
+
const id = fs.existsSync(cfg.identityPath)
|
|
2007
|
+
? loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8')))
|
|
2008
|
+
: null;
|
|
2009
|
+
const result = await withLocalMcp(dataDir, async (client) => {
|
|
2010
|
+
const tools = (await client.listTools()).tools || [];
|
|
2011
|
+
const who = await client.callTool({ name: 'atel_whoami', arguments: {} });
|
|
2012
|
+
const text = JSON.stringify(who);
|
|
2013
|
+
return {
|
|
2014
|
+
ok: !!id && text.includes(id.did),
|
|
2015
|
+
did: id?.did || null,
|
|
2016
|
+
toolCount: tools.length,
|
|
2017
|
+
hasWhoami: tools.some((t) => t.name === 'atel_whoami'),
|
|
2018
|
+
whoami: who,
|
|
2019
|
+
};
|
|
2020
|
+
});
|
|
2021
|
+
if (bools.has('json')) {
|
|
2022
|
+
log(JSON.stringify(result, null, 2));
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
if (!result.ok) {
|
|
2026
|
+
err(`🔴 MCP probe 失败: whoami 未返回当前 DID ${result.did || '(unknown)'}`);
|
|
2027
|
+
process.exit(1);
|
|
2028
|
+
}
|
|
2029
|
+
log(`🟢 MCP probe 通过 — tools=${result.toolCount}, did=${result.did}`);
|
|
2030
|
+
}
|
|
2031
|
+
function parseJsonArg(flags) {
|
|
2032
|
+
const raw = flags['args'] || flags['json-args'] || '{}';
|
|
2033
|
+
if (raw.startsWith('@')) {
|
|
2034
|
+
return JSON.parse(fs.readFileSync(raw.slice(1), 'utf8'));
|
|
2035
|
+
}
|
|
2036
|
+
return JSON.parse(raw);
|
|
2037
|
+
}
|
|
2038
|
+
const READONLY_TOOLS = new Set([
|
|
2039
|
+
'atel_whoami',
|
|
2040
|
+
'atel_balance',
|
|
2041
|
+
'atel_deposit_info',
|
|
2042
|
+
'atel_agent_search',
|
|
2043
|
+
'atel_inbox_list',
|
|
2044
|
+
'atel_order_get',
|
|
2045
|
+
'atel_milestone_list',
|
|
2046
|
+
'atel_a2b_countries',
|
|
2047
|
+
'atel_a2b_search',
|
|
2048
|
+
'atel_a2b_quote',
|
|
2049
|
+
'atel_a2b_purchase_get',
|
|
2050
|
+
'atel_fm_browse',
|
|
2051
|
+
'atel_fm_listing_get',
|
|
2052
|
+
'atel_fm_delivery_get',
|
|
2053
|
+
'atel_fm_deal_list',
|
|
2054
|
+
'atel_fm_deal_get',
|
|
2055
|
+
'atel_fm_negotiate_get',
|
|
2056
|
+
]);
|
|
2057
|
+
async function callAtelTool(flags, bools, tool, args) {
|
|
2058
|
+
const dataDir = resolveDataDir(flags);
|
|
2059
|
+
if (!loadConfig(dataDir)) {
|
|
2060
|
+
err(`❌ 未找到配置。${describeProfileSelectionError()}`);
|
|
2061
|
+
process.exit(1);
|
|
2062
|
+
}
|
|
2063
|
+
if (!READONLY_TOOLS.has(tool) && !bools.has('confirm')) {
|
|
2064
|
+
err(`❌ ${tool} 是写操作。确认执行请加 --confirm。`);
|
|
2065
|
+
process.exit(1);
|
|
2066
|
+
}
|
|
2067
|
+
return withLocalMcp(dataDir, async (client) => client.callTool({ name: tool, arguments: args }));
|
|
2068
|
+
}
|
|
2069
|
+
function printToolResult(result, bools) {
|
|
2070
|
+
if (bools.has('json')) {
|
|
2071
|
+
log(JSON.stringify(result, null, 2));
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
const content = result?.content;
|
|
2075
|
+
const text = Array.isArray(content) ? content.find((c) => c.type === 'text' && typeof c.text === 'string')?.text : '';
|
|
2076
|
+
log(text || JSON.stringify(result, null, 2));
|
|
2077
|
+
}
|
|
2078
|
+
async function cmdInvoke(flags, bools) {
|
|
2079
|
+
const tool = flags['tool'];
|
|
2080
|
+
if (!tool) {
|
|
2081
|
+
err('❌ 缺 --tool <工具名>,例如 --tool atel_whoami');
|
|
2082
|
+
process.exit(1);
|
|
2083
|
+
}
|
|
2084
|
+
let args;
|
|
2085
|
+
try {
|
|
2086
|
+
args = parseJsonArg(flags);
|
|
2087
|
+
}
|
|
2088
|
+
catch (e) {
|
|
2089
|
+
err(`❌ --args 不是合法 JSON:${e.message}`);
|
|
2090
|
+
process.exit(1);
|
|
2091
|
+
}
|
|
2092
|
+
const result = await callAtelTool(flags, bools, tool, args);
|
|
2093
|
+
printToolResult(result, bools);
|
|
2094
|
+
}
|
|
2095
|
+
function requireFlag(flags, key, example) {
|
|
2096
|
+
const v = flags[key];
|
|
2097
|
+
if (v === undefined || v.trim() === '') {
|
|
2098
|
+
err(`❌ 缺 --${key}。例:${example}`);
|
|
2099
|
+
process.exit(1);
|
|
2100
|
+
}
|
|
2101
|
+
return v;
|
|
2102
|
+
}
|
|
2103
|
+
function numFlag(flags, key, required = false) {
|
|
2104
|
+
const raw = flags[key];
|
|
2105
|
+
if (raw === undefined || raw === '') {
|
|
2106
|
+
if (required) {
|
|
2107
|
+
err(`❌ 缺 --${key}`);
|
|
2108
|
+
process.exit(1);
|
|
2109
|
+
}
|
|
2110
|
+
return undefined;
|
|
2111
|
+
}
|
|
2112
|
+
const n = Number(raw);
|
|
2113
|
+
if (!Number.isFinite(n)) {
|
|
2114
|
+
err(`❌ --${key} 必须是数字,当前是 ${raw}`);
|
|
2115
|
+
process.exit(1);
|
|
2116
|
+
}
|
|
2117
|
+
return n;
|
|
2118
|
+
}
|
|
2119
|
+
async function cmdFm(positionals, flags, bools) {
|
|
2120
|
+
const sub = positionals[0] || 'help';
|
|
2121
|
+
let tool = '';
|
|
2122
|
+
let args = {};
|
|
2123
|
+
if (sub === 'sell') {
|
|
2124
|
+
const title = flags['title'] || flags['sku'] || flags['sku-text'] || flags['name'];
|
|
2125
|
+
if (!title) {
|
|
2126
|
+
err('❌ 缺 --title。例: atel fm sell --title "codex哆啦卡" --price 10 --min-price 0.001 --confirm');
|
|
2127
|
+
process.exit(1);
|
|
2128
|
+
}
|
|
2129
|
+
const mandate = {
|
|
2130
|
+
category: flags['category'] || 'voucher/generic-redemption-code',
|
|
2131
|
+
price_usdc: numFlag(flags, 'price', true),
|
|
2132
|
+
min_price_usdc: numFlag(flags, 'min-price'),
|
|
2133
|
+
sku_text: title,
|
|
2134
|
+
chain: flags['chain'] || 'base',
|
|
2135
|
+
};
|
|
2136
|
+
tool = 'atel_fm_sell';
|
|
2137
|
+
args = { mandateText: JSON.stringify(mandate) };
|
|
2138
|
+
}
|
|
2139
|
+
else if (sub === 'browse' || sub === 'search') {
|
|
2140
|
+
tool = 'atel_fm_browse';
|
|
2141
|
+
args = { query: flags['query'] || positionals.slice(1).join(' ') || '' };
|
|
2142
|
+
}
|
|
2143
|
+
else if (sub === 'get') {
|
|
2144
|
+
tool = 'atel_fm_listing_get';
|
|
2145
|
+
args = { listingId: requireFlag(flags, 'listing-id', 'atel fm get --listing-id <id>') };
|
|
2146
|
+
}
|
|
2147
|
+
else if (sub === 'buy') {
|
|
2148
|
+
tool = 'atel_fm_buy';
|
|
2149
|
+
args = {
|
|
2150
|
+
listingId: requireFlag(flags, 'listing-id', 'atel fm buy --listing-id <id> --price 10 --confirm'),
|
|
2151
|
+
offerPrice: numFlag(flags, 'price', true),
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
else if (sub === 'deliver') {
|
|
2155
|
+
tool = 'atel_fm_deliver';
|
|
2156
|
+
args = {
|
|
2157
|
+
dealId: requireFlag(flags, 'deal-id', 'atel fm deliver --deal-id <id> --content "..." --confirm'),
|
|
2158
|
+
content: requireFlag(flags, 'content', 'atel fm deliver --deal-id <id> --content "..." --confirm'),
|
|
2159
|
+
};
|
|
2160
|
+
}
|
|
2161
|
+
else if (sub === 'delivery' || sub === 'get-delivery') {
|
|
2162
|
+
tool = 'atel_fm_delivery_get';
|
|
2163
|
+
args = { dealId: requireFlag(flags, 'deal-id', 'atel fm delivery --deal-id <id>') };
|
|
2164
|
+
}
|
|
2165
|
+
else if (sub === 'confirm') {
|
|
2166
|
+
tool = 'atel_fm_confirm';
|
|
2167
|
+
args = { dealId: requireFlag(flags, 'deal-id', 'atel fm confirm --deal-id <id> --confirm') };
|
|
2168
|
+
}
|
|
2169
|
+
else if (sub === 'dispute') {
|
|
2170
|
+
tool = 'atel_fm_dispute';
|
|
2171
|
+
args = {
|
|
2172
|
+
dealId: requireFlag(flags, 'deal-id', 'atel fm dispute --deal-id <id> --reason "..." --confirm'),
|
|
2173
|
+
reason: requireFlag(flags, 'reason', 'atel fm dispute --deal-id <id> --reason "..." --confirm'),
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
else if (sub === 'deals') {
|
|
2177
|
+
tool = 'atel_fm_deal_list';
|
|
2178
|
+
args = {};
|
|
2179
|
+
}
|
|
2180
|
+
else if (sub === 'deal') {
|
|
2181
|
+
tool = 'atel_fm_deal_get';
|
|
2182
|
+
args = { dealId: requireFlag(flags, 'deal-id', 'atel fm deal --deal-id <id>') };
|
|
2183
|
+
}
|
|
2184
|
+
else {
|
|
2185
|
+
log('用法: atel fm sell|browse|get|buy|deliver|delivery|confirm|dispute|deals|deal [...]');
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
printToolResult(await callAtelTool(flags, bools, tool, args), bools);
|
|
2189
|
+
}
|
|
2190
|
+
async function cmdA2b(positionals, flags, bools) {
|
|
2191
|
+
const sub = positionals[0] || 'help';
|
|
2192
|
+
let tool = '';
|
|
2193
|
+
let args = {};
|
|
2194
|
+
if (sub === 'countries') {
|
|
2195
|
+
tool = 'atel_a2b_countries';
|
|
2196
|
+
}
|
|
2197
|
+
else if (sub === 'search') {
|
|
2198
|
+
tool = 'atel_a2b_search';
|
|
2199
|
+
args = { query: flags['query'] || positionals.slice(1).join(' ') || '', country: flags['country'] || 'US' };
|
|
2200
|
+
}
|
|
2201
|
+
else if (sub === 'quote') {
|
|
2202
|
+
tool = 'atel_a2b_quote';
|
|
2203
|
+
args = {
|
|
2204
|
+
query: flags['query'] || '',
|
|
2205
|
+
productId: requireFlag(flags, 'product-id', 'atel a2b quote --product-id <id> --value 10'),
|
|
2206
|
+
value: numFlag(flags, 'value', true),
|
|
2207
|
+
country: flags['country'] || 'US',
|
|
2208
|
+
};
|
|
2209
|
+
}
|
|
2210
|
+
else if (sub === 'buy' || sub === 'purchase') {
|
|
2211
|
+
tool = 'atel_a2b_purchase';
|
|
2212
|
+
args = {
|
|
2213
|
+
query: flags['query'] || '',
|
|
2214
|
+
productId: requireFlag(flags, 'product-id', 'atel a2b buy --product-id <id> --value 10 --max-usdc 10.5 --confirm'),
|
|
2215
|
+
value: numFlag(flags, 'value', true),
|
|
2216
|
+
country: flags['country'] || 'US',
|
|
2217
|
+
maxAmountUsdc: numFlag(flags, 'max-usdc', true),
|
|
2218
|
+
autoReveal: !bools.has('no-reveal'),
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
else if (sub === 'get') {
|
|
2222
|
+
tool = 'atel_a2b_purchase_get';
|
|
2223
|
+
args = { intentId: requireFlag(flags, 'intent-id', 'atel a2b get --intent-id intent_...') };
|
|
2224
|
+
}
|
|
2225
|
+
else {
|
|
2226
|
+
log('用法: atel a2b countries|search|quote|buy|get [...]');
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
printToolResult(await callAtelTool(flags, bools, tool, args), bools);
|
|
2230
|
+
}
|
|
2231
|
+
async function cmdOrder(positionals, flags, bools) {
|
|
2232
|
+
const sub = positionals[0] || 'help';
|
|
2233
|
+
let tool = '';
|
|
2234
|
+
let args = {};
|
|
2235
|
+
if (sub === 'create') {
|
|
2236
|
+
tool = 'atel_order_create';
|
|
2237
|
+
args = {
|
|
2238
|
+
chain: flags['chain'] || 'base',
|
|
2239
|
+
executorDid: requireFlag(flags, 'executor-did', 'atel order create --executor-did did:atel:... --capability coding --description "..." --price 1 --confirm'),
|
|
2240
|
+
capabilityType: flags['capability'] || 'coding',
|
|
2241
|
+
description: requireFlag(flags, 'description', 'atel order create --description "..."'),
|
|
2242
|
+
priceUsdc: numFlag(flags, 'price', true),
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
else if (sub === 'get') {
|
|
2246
|
+
tool = 'atel_order_get';
|
|
2247
|
+
args = { orderId: requireFlag(flags, 'order-id', 'atel order get --order-id ord-...') };
|
|
2248
|
+
}
|
|
2249
|
+
else if (sub === 'accept') {
|
|
2250
|
+
tool = 'atel_order_accept';
|
|
2251
|
+
args = { orderId: requireFlag(flags, 'order-id', 'atel order accept --order-id ord-... --confirm') };
|
|
2252
|
+
}
|
|
2253
|
+
else {
|
|
2254
|
+
log('用法: atel order create|get|accept [...]');
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
printToolResult(await callAtelTool(flags, bools, tool, args), bools);
|
|
2258
|
+
}
|
|
2259
|
+
async function cmdWallet(positionals, flags, bools) {
|
|
2260
|
+
const sub = positionals[0] || 'balance';
|
|
2261
|
+
let tool = '';
|
|
2262
|
+
let args = {};
|
|
2263
|
+
if (sub === 'balance') {
|
|
2264
|
+
tool = 'atel_balance';
|
|
2265
|
+
}
|
|
2266
|
+
else if (sub === 'deposit-info') {
|
|
2267
|
+
tool = 'atel_deposit_info';
|
|
2268
|
+
}
|
|
2269
|
+
else if (sub === 'transfer') {
|
|
2270
|
+
tool = 'atel_wallet_transfer';
|
|
2271
|
+
args = {
|
|
2272
|
+
chain: flags['chain'] || 'base',
|
|
2273
|
+
address: requireFlag(flags, 'address', 'atel wallet transfer --address 0x... --amount 1 --confirm'),
|
|
2274
|
+
amount: numFlag(flags, 'amount', true),
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
else {
|
|
2278
|
+
log('用法: atel wallet balance|deposit-info|transfer [...]');
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
printToolResult(await callAtelTool(flags, bools, tool, args), bools);
|
|
2282
|
+
}
|
|
2283
|
+
async function cmdSmoke(flags, bools) {
|
|
2284
|
+
const dataDir = resolveDataDir(flags);
|
|
2285
|
+
if (!loadConfig(dataDir)) {
|
|
2286
|
+
err('❌ 未找到配置,先跑 connect。');
|
|
2287
|
+
process.exit(1);
|
|
2288
|
+
}
|
|
2289
|
+
const checks = await withLocalMcp(dataDir, async (client) => {
|
|
2290
|
+
const tools = (await client.listTools()).tools || [];
|
|
2291
|
+
const toolNames = new Set(tools.map((t) => t.name));
|
|
2292
|
+
const call = async (name, args = {}) => {
|
|
2293
|
+
if (!toolNames.has(name))
|
|
2294
|
+
return { ok: false, skipped: true, reason: 'tool_missing' };
|
|
2295
|
+
try {
|
|
2296
|
+
const result = await client.callTool({ name, arguments: args });
|
|
2297
|
+
return { ok: true, result };
|
|
2298
|
+
}
|
|
2299
|
+
catch (e) {
|
|
2300
|
+
return { ok: false, error: e.message };
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
const readonly = {
|
|
2304
|
+
whoami: await call('atel_whoami'),
|
|
2305
|
+
balance: await call('atel_balance'),
|
|
2306
|
+
agentSearch: await call('atel_agent_search', {}),
|
|
2307
|
+
inboxList: await call('atel_inbox_list', {}),
|
|
2308
|
+
};
|
|
2309
|
+
const prepare = {
|
|
2310
|
+
orderCreate: toolNames.has('atel_order_create') ? 'available_requires_user_confirm' : 'missing',
|
|
2311
|
+
fmSell: toolNames.has('atel_fm_sell') ? 'available_requires_user_confirm' : 'missing',
|
|
2312
|
+
walletTransfer: toolNames.has('atel_wallet_transfer') ? 'available_requires_user_confirm' : 'missing',
|
|
2313
|
+
a2bSearch: toolNames.has('atel_a2b_search') ? 'available_readonly_search' : 'missing',
|
|
2314
|
+
};
|
|
2315
|
+
return { toolCount: tools.length, readonly, prepare };
|
|
2316
|
+
});
|
|
2317
|
+
const readonlyOk = Object.values(checks.readonly).every((v) => v.ok === true);
|
|
2318
|
+
const requiredToolsPresent = checks.toolCount >= 10
|
|
2319
|
+
&& checks.prepare.orderCreate !== 'missing'
|
|
2320
|
+
&& checks.prepare.fmSell !== 'missing'
|
|
2321
|
+
&& checks.prepare.walletTransfer !== 'missing'
|
|
2322
|
+
&& checks.prepare.a2bSearch !== 'missing';
|
|
2323
|
+
const ok = readonlyOk && requiredToolsPresent;
|
|
2324
|
+
const result = { ok, ...checks };
|
|
2325
|
+
if (bools.has('json')) {
|
|
2326
|
+
log(JSON.stringify(result, null, 2));
|
|
2327
|
+
}
|
|
2328
|
+
else {
|
|
2329
|
+
log(`${ok ? '🟢' : '🔴'} smoke ${ok ? '通过' : '有失败'} — tools=${checks.toolCount}`);
|
|
2330
|
+
for (const [k, v] of Object.entries(checks.readonly)) {
|
|
2331
|
+
const item = v;
|
|
2332
|
+
log(` • ${k}: ${item.ok ? 'ok' : item.skipped ? `skipped(${item.reason})` : `fail(${item.error})`}`);
|
|
2333
|
+
}
|
|
2334
|
+
log(` • writes: order=${checks.prepare.orderCreate}, fm=${checks.prepare.fmSell}, transfer=${checks.prepare.walletTransfer}, a2b=${checks.prepare.a2bSearch}`);
|
|
2335
|
+
}
|
|
2336
|
+
if (!ok)
|
|
2337
|
+
process.exit(1);
|
|
2338
|
+
}
|
|
1163
2339
|
async function cmdDetect() {
|
|
1164
2340
|
const all = await detectAll();
|
|
1165
2341
|
if (all.length === 0) {
|
|
@@ -1170,10 +2346,54 @@ async function cmdDetect() {
|
|
|
1170
2346
|
log(`🟢 ${adapter.displayName} (${adapter.id}) ${result.version || ''} ${result.detail || ''}`);
|
|
1171
2347
|
}
|
|
1172
2348
|
}
|
|
2349
|
+
function cmdPrompt(flags) {
|
|
2350
|
+
const dataDir = resolveDataDir(flags);
|
|
2351
|
+
const cfg = loadConfig(dataDir);
|
|
2352
|
+
const binding = readBinding(dataDir);
|
|
2353
|
+
if (!cfg || !binding) {
|
|
2354
|
+
err('❌ 未接入或缺 binding.json,先跑 connect。');
|
|
2355
|
+
process.exit(1);
|
|
2356
|
+
}
|
|
2357
|
+
const toolPrefix = cfg.runtime === 'codex' || cfg.runtime === 'gemini-cli' || cfg.runtime === 'claude-code'
|
|
2358
|
+
? 'atel__atel_whoami'
|
|
2359
|
+
: 'atel_whoami';
|
|
2360
|
+
log(`你已经接入 ATEL。profile=${binding.profile}, runtime=${binding.runtime}, DID=${binding.did}。读取本机 ${path.join(dataDir, 'SKILL.md')};使用已配置的 ATEL MCP 工具处理 ATEL 请求。先调用 ${toolPrefix} 验证身份,确认返回 DID=${binding.did}。`);
|
|
2361
|
+
}
|
|
2362
|
+
function cmdSkill(flags) {
|
|
2363
|
+
const outPath = flags['out'];
|
|
2364
|
+
const body = readPackagedSkill();
|
|
2365
|
+
if (outPath) {
|
|
2366
|
+
fs.mkdirSync(path.dirname(path.resolve(outPath)), { recursive: true });
|
|
2367
|
+
fs.writeFileSync(outPath, body);
|
|
2368
|
+
log(path.resolve(outPath));
|
|
2369
|
+
return;
|
|
2370
|
+
}
|
|
2371
|
+
log(body.trimEnd());
|
|
2372
|
+
}
|
|
1173
2373
|
function help() {
|
|
1174
|
-
log(
|
|
2374
|
+
log(`atel — ATEL 产品级 CLI
|
|
1175
2375
|
|
|
1176
2376
|
用法:
|
|
2377
|
+
atel connect [--runtime <id>] [--name <agent名>] [--capabilities coding,writing]
|
|
2378
|
+
[--profile <别名>] [--runtime-agent-id <id>] [--update-existing] [...] # 推荐入口:接入 + 起 companion
|
|
2379
|
+
atel status [--profile <别名> | --agent <别名>] [--json]
|
|
2380
|
+
atel service status|start|stop|restart|logs [--profile <别名> | --agent <别名>]
|
|
2381
|
+
atel tools [--profile <别名> | --agent <别名>] [--json]
|
|
2382
|
+
atel mcp-probe [--profile <别名> | --agent <别名>] [--json]
|
|
2383
|
+
atel invoke [--profile <别名> | --agent <别名>] --tool atel_whoami [--args '{}'] [--json] [--confirm]
|
|
2384
|
+
atel fm sell --title "codex哆啦卡" --price 10 --min-price 0.001 [--category voucher/generic-redemption-code] [--confirm]
|
|
2385
|
+
atel fm browse --query "gift card"
|
|
2386
|
+
atel fm get --listing-id <id> | buy --listing-id <id> --price <USDC> --confirm
|
|
2387
|
+
atel fm delivery --deal-id <id> | deal --deal-id <id> | deals
|
|
2388
|
+
atel a2b search --query amazon --country US | quote --product-id <id> --value 10 | buy ... --max-usdc 10.5 --confirm
|
|
2389
|
+
atel order create --executor-did did:atel:... --capability coding --description "..." --price 1 --confirm
|
|
2390
|
+
atel wallet balance | deposit-info | transfer --address 0x... --amount 1 --confirm
|
|
2391
|
+
atel smoke [--profile <别名> | --agent <别名>] [--json] # 只读/安全业务验活
|
|
2392
|
+
atel skill [--out ./SKILL.md] # 输出 npm 包内置 SKILL.md(离线可用)
|
|
2393
|
+
atel prompt [--profile <别名> | --agent <别名>]
|
|
2394
|
+
atel repair [--profile <别名> | --agent <别名>] # 重写 MCP 配置与 SKILL
|
|
2395
|
+
atel disconnect [--profile <别名> | --agent <别名>] [--delete-data] # 断开一个 profile
|
|
2396
|
+
atel uninstall [--confirm] # 删除本机 ATEL profile 数据
|
|
1177
2397
|
npx @atel-ai/agent onboard [--runtime <id>] [--name <agent名>] [--capabilities coding,writing]
|
|
1178
2398
|
[--profile <别名>] [--runtime-agent-id <id>] [--update-existing] [...] # install + 起 companion(推荐)
|
|
1179
2399
|
npx @atel-ai/agent install [--runtime <id>] [--name <agent名>] [--capabilities coding,writing]
|
|
@@ -1185,28 +2405,62 @@ function help() {
|
|
|
1185
2405
|
npx @atel-ai/agent doctor [--data-dir D | --profile <别名> | --agent <别名>] [--probe]
|
|
1186
2406
|
npx @atel-ai/agent list # 列出本机所有 ATEL agent(~/.atel/agents.json)
|
|
1187
2407
|
npx @atel-ai/agent whoami [--data-dir D | --profile <别名> | --agent <别名>]
|
|
2408
|
+
npx @atel-ai/agent balance [--data-dir D | --profile <别名> | --agent <别名>]
|
|
1188
2409
|
npx @atel-ai/agent detect
|
|
1189
2410
|
npx @atel-ai/agent update
|
|
1190
2411
|
|
|
1191
2412
|
说明:
|
|
1192
2413
|
• 多 runtime 同机时不传 --runtime 会安全失败 —— 用 --runtime <id> 或环境变量 ATEL_RUNTIME。
|
|
1193
|
-
•
|
|
2414
|
+
• 多 profile 同用户时业务命令需 --profile/--agent;只有一个 profile 时会自动选择。
|
|
2415
|
+
• Codex 沙箱内写操作/远端 MCP 会要求授权沙箱外执行,这是正常安全边界。
|
|
2416
|
+
• 通用接入句可用:先执行 atel skill 读取本机说明,再按说明执行 atel connect --auto。
|
|
2417
|
+
• 产品级 connect 默认使用 --profile 隔离(→ ~/.atel/agents/<别名>);openclaw 多 agent 再加 --runtime-agent-id。
|
|
1194
2418
|
• 同 data-dir 改名需 --update-existing(否则拒绝,防静默改同一 DID 的名)。
|
|
1195
2419
|
|
|
1196
2420
|
支持的 runtime: ${ADAPTERS.map((a) => a.id).join(', ')}
|
|
1197
2421
|
`);
|
|
1198
2422
|
}
|
|
1199
2423
|
async function main() {
|
|
1200
|
-
const { cmd, flags, bools } = parseArgs(process.argv.slice(2));
|
|
2424
|
+
const { cmd, positionals, flags, bools } = parseArgs(process.argv.slice(2));
|
|
1201
2425
|
switch (cmd) {
|
|
2426
|
+
case '--version':
|
|
2427
|
+
case '-v':
|
|
2428
|
+
case 'version':
|
|
2429
|
+
log(VERSION);
|
|
2430
|
+
return;
|
|
2431
|
+
case 'connect': return cmdConnect(flags, bools);
|
|
1202
2432
|
case 'install': return cmdInstall(flags, bools);
|
|
1203
2433
|
case 'onboard': return cmdOnboard(flags, bools);
|
|
1204
2434
|
case 'run': return cmdRun(flags);
|
|
1205
2435
|
case 'mcp': return cmdMcp(flags);
|
|
1206
2436
|
case 'doctor': return cmdDoctor(flags, bools);
|
|
2437
|
+
case 'status':
|
|
2438
|
+
cmdStatus(flags, bools);
|
|
2439
|
+
return;
|
|
2440
|
+
case 'service':
|
|
2441
|
+
cmdService(flags, positionals);
|
|
2442
|
+
return;
|
|
2443
|
+
case 'tools': return cmdTools(flags, bools);
|
|
2444
|
+
case 'mcp-probe': return cmdMcpProbe(flags, bools);
|
|
2445
|
+
case 'invoke': return cmdInvoke(flags, bools);
|
|
2446
|
+
case 'fm': return cmdFm(positionals, flags, bools);
|
|
2447
|
+
case 'a2b': return cmdA2b(positionals, flags, bools);
|
|
2448
|
+
case 'order': return cmdOrder(positionals, flags, bools);
|
|
2449
|
+
case 'wallet': return cmdWallet(positionals, flags, bools);
|
|
2450
|
+
case 'smoke': return cmdSmoke(flags, bools);
|
|
2451
|
+
case 'skill':
|
|
2452
|
+
cmdSkill(flags);
|
|
2453
|
+
return;
|
|
2454
|
+
case 'prompt':
|
|
2455
|
+
cmdPrompt(flags);
|
|
2456
|
+
return;
|
|
2457
|
+
case 'repair': return cmdRepair(flags);
|
|
2458
|
+
case 'disconnect': return cmdDisconnect(flags, bools);
|
|
2459
|
+
case 'uninstall': return cmdUninstall(flags, bools);
|
|
1207
2460
|
case 'detect': return cmdDetect();
|
|
1208
2461
|
case 'update': return cmdUpdate();
|
|
1209
2462
|
case 'whoami': return cmdWhoami(flags);
|
|
2463
|
+
case 'balance': return cmdBalance(flags);
|
|
1210
2464
|
case 'list':
|
|
1211
2465
|
cmdList();
|
|
1212
2466
|
return;
|