@atel-ai/agent 0.2.23 → 0.2.27
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 +949 -16
- 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'];
|
|
@@ -332,6 +457,12 @@ function upsertAgent(alias, e) {
|
|
|
332
457
|
fs.mkdirSync(path.dirname(agentsRegistryPath()), { recursive: true });
|
|
333
458
|
fs.writeFileSync(agentsRegistryPath(), JSON.stringify({ agents: all }, null, 2));
|
|
334
459
|
}
|
|
460
|
+
function removeAgent(alias) {
|
|
461
|
+
const all = readAgents();
|
|
462
|
+
delete all[alias];
|
|
463
|
+
fs.mkdirSync(path.dirname(agentsRegistryPath()), { recursive: true });
|
|
464
|
+
fs.writeFileSync(agentsRegistryPath(), JSON.stringify({ agents: all }, null, 2));
|
|
465
|
+
}
|
|
335
466
|
function aliasFromFlags(flags) {
|
|
336
467
|
if (flags['profile'])
|
|
337
468
|
return flags['profile'];
|
|
@@ -339,6 +470,16 @@ function aliasFromFlags(flags) {
|
|
|
339
470
|
return path.basename(path.resolve(flags['data-dir']));
|
|
340
471
|
return 'default';
|
|
341
472
|
}
|
|
473
|
+
function withConnectDefaults(flags) {
|
|
474
|
+
const out = { ...flags };
|
|
475
|
+
if (out['auto'] === 'true' || out['auto'] === '1') {
|
|
476
|
+
delete out['auto'];
|
|
477
|
+
}
|
|
478
|
+
if (!out['profile'] && !out['data-dir']) {
|
|
479
|
+
out['profile'] = out['runtime'] || process.env.ATEL_RUNTIME || 'default';
|
|
480
|
+
}
|
|
481
|
+
return out;
|
|
482
|
+
}
|
|
342
483
|
// --agent <alias> 把后续命令(whoami/doctor)解析到该 agent 的 data-dir。
|
|
343
484
|
function resolveDataDir(flags) {
|
|
344
485
|
if (flags['agent']) {
|
|
@@ -392,11 +533,13 @@ function auditIdentityPerms() {
|
|
|
392
533
|
* atel-mcp-openclaw 插件提供(本接入器不装)。SKILL 仅落在
|
|
393
534
|
* dataDir,普通 cwd 不自动加载 —— 所以交互态要让 agent 先读 dataDir/SKILL.md。
|
|
394
535
|
*/
|
|
395
|
-
function printUsageModes(cfg, adapter) {
|
|
536
|
+
function printUsageModes(cfg, adapter, passiveStarted = true) {
|
|
396
537
|
const skillPath = path.join(cfg.dataDir, 'SKILL.md');
|
|
397
538
|
log('');
|
|
398
539
|
log('📌 两种使用模式(请知悉,避免"新会话里找不到 ATEL"):');
|
|
399
|
-
log(
|
|
540
|
+
log(passiveStarted
|
|
541
|
+
? ' ① 被动·自动接单:companion 守护已起,无人值守自动推进 A2A/A2B —— 这半不需要你干预。'
|
|
542
|
+
: ` ① 被动·自动接单:本次使用了 --no-start,companion 尚未启动。需要被动接单时运行 atel run --data-dir ${cfg.dataDir} 或重新 connect 不带 --no-start。`);
|
|
400
543
|
log(' ② 主动·你自己新开会话直接用 ATEL:新会话默认不一定带上身份/工具,按 runtime:');
|
|
401
544
|
if (adapter.id === 'openclaw') {
|
|
402
545
|
log(' • OpenClaw:交互态 atel_* 工具由 atel-mcp-openclaw 插件提供(本接入器只装被动 companion + 身份)。');
|
|
@@ -414,7 +557,14 @@ function printUsageModes(cfg, adapter) {
|
|
|
414
557
|
}
|
|
415
558
|
function buildConfig(flags, bools, runtime) {
|
|
416
559
|
const testnet = bools.has('server') || flags['env'] === 'testnet';
|
|
560
|
+
const now = new Date().toISOString();
|
|
417
561
|
const base = {
|
|
562
|
+
schemaVersion: 2,
|
|
563
|
+
profile: aliasFromFlags(flags),
|
|
564
|
+
agentName: flags['name'],
|
|
565
|
+
capabilities: parseCapabilities(flags['capabilities']),
|
|
566
|
+
createdAt: now,
|
|
567
|
+
updatedAt: now,
|
|
418
568
|
runtime,
|
|
419
569
|
runtimeAgentId: flags['runtime-agent-id'], // A9:openclaw 多 agent 隔离(默认 main)
|
|
420
570
|
dataDir: dataDirFrom(flags),
|
|
@@ -528,12 +678,26 @@ async function cmdInstall(flags, bools) {
|
|
|
528
678
|
}
|
|
529
679
|
const cfg = buildConfig(flags, bools, adapter.id);
|
|
530
680
|
saveConfig(cfg);
|
|
681
|
+
const capturedEnv = captureRuntimeEnv(cfg);
|
|
682
|
+
if (capturedEnv.length) {
|
|
683
|
+
log(`🔐 已保存 runtime 环境到 ${runtimeEnvPath(cfg.dataDir)}(${capturedEnv.join(', ')})`);
|
|
684
|
+
}
|
|
531
685
|
// 双身份统一(openclaw):被动 companion 必须与插件交互用**同一个 DID** —— 一个 agent 不能两个身份
|
|
532
686
|
// (否则接单收益记在 onboard 的 DID、聊天里主动操作记在插件的 DID,钱/声誉算两处)。onboard 时若插件
|
|
533
687
|
// 已有身份,**复用它**而不是新建。只在 data-dir 还没身份时 adopt(re-onboard 不动)。
|
|
534
688
|
const preExisted = fs.existsSync(cfg.identityPath);
|
|
535
689
|
const reconcile = reconcileOpenclawIdentity(cfg); // adopted(新装复用)| migrated(存量迁移)| none
|
|
536
690
|
const { identity } = ensureIdentity(cfg);
|
|
691
|
+
const alias = aliasFromFlags(flags);
|
|
692
|
+
const capabilities = parseCapabilities(flags['capabilities']);
|
|
693
|
+
const existingBinding = readBinding(cfg.dataDir);
|
|
694
|
+
try {
|
|
695
|
+
assertBindingCompatible(existingBinding, { profile: alias, runtime: cfg.runtime, did: identity.did });
|
|
696
|
+
}
|
|
697
|
+
catch (e) {
|
|
698
|
+
err(`❌ 身份绑定冲突:${e.message}`);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
537
701
|
// A2:真 re-onboard(已有**同一**身份)+ 传 --name → 硬闸,避免静默改名。迁移(身份已换成插件的)不触发。
|
|
538
702
|
if (preExisted && reconcile !== 'migrated') {
|
|
539
703
|
log(`♻️ 复用 data-dir(${cfg.dataDir})已有身份 ${identity.did}`);
|
|
@@ -589,10 +753,6 @@ async function cmdInstall(flags, bools) {
|
|
|
589
753
|
}
|
|
590
754
|
// 注册/更新本 agent 到 registry(可被发现 + 可作为 A2A 接活方/雇主目标)
|
|
591
755
|
if (flags['name']) {
|
|
592
|
-
const capabilities = (flags['capabilities'] || 'writing')
|
|
593
|
-
.split(',')
|
|
594
|
-
.map((s) => s.trim())
|
|
595
|
-
.filter(Boolean);
|
|
596
756
|
try {
|
|
597
757
|
const r = await registryRemoteRegister(cfg.platformBaseUrl, jwt, {
|
|
598
758
|
name: flags['name'],
|
|
@@ -616,6 +776,25 @@ async function cmdInstall(flags, bools) {
|
|
|
616
776
|
else {
|
|
617
777
|
log('ℹ️ 未传 --name,跳过 registry 注册(仅主动调工具不需要;要被搜到/当接活方目标则需注册)');
|
|
618
778
|
}
|
|
779
|
+
const now = new Date().toISOString();
|
|
780
|
+
writeBinding(cfg.dataDir, {
|
|
781
|
+
schemaVersion: 1,
|
|
782
|
+
profile: alias,
|
|
783
|
+
runtime: cfg.runtime,
|
|
784
|
+
runtimeAgentId: cfg.runtimeAgentId,
|
|
785
|
+
did: identity.did,
|
|
786
|
+
agentName: flags['name'],
|
|
787
|
+
capabilities,
|
|
788
|
+
runtimeFingerprint: {
|
|
789
|
+
version: chosen.result.version,
|
|
790
|
+
binPath: chosen.result.binPath,
|
|
791
|
+
},
|
|
792
|
+
machineFingerprint: machineFingerprint(),
|
|
793
|
+
createdAt: existingBinding?.createdAt || now,
|
|
794
|
+
updatedAt: now,
|
|
795
|
+
lastVerifiedAt: now,
|
|
796
|
+
});
|
|
797
|
+
log(`🔗 身份绑定已写入 ${bindingPath(cfg.dataDir)}`);
|
|
619
798
|
writeSkill(cfg, adapter);
|
|
620
799
|
// 一次性安装步骤(如 hermes 必须 mcp add 启用工具)
|
|
621
800
|
if (adapter.onInstall) {
|
|
@@ -636,11 +815,13 @@ async function cmdInstall(flags, bools) {
|
|
|
636
815
|
// 否则同一个 DID 有两个 relay 消费者(插件 listener + 我的 companion),订单会被随机分走/重复 ack。
|
|
637
816
|
const openclawPluginActive = cfg.runtime === 'openclaw'
|
|
638
817
|
&& fs.existsSync(path.join(os.homedir(), '.openclaw', 'extensions', 'atel-mcp-openclaw'));
|
|
818
|
+
let passiveStarted = true;
|
|
639
819
|
if (openclawPluginActive) {
|
|
640
820
|
log(' OpenClaw 被动接单由 atel-mcp-openclaw 插件 listener 负责(DID-Sig,已确保在跑)。');
|
|
641
821
|
log(' ✅ 不另起 companion —— 避免同一 DID 两个消费者抢同一 relay 队列(被动与交互统一一个身份)。');
|
|
642
822
|
}
|
|
643
823
|
else if (bools.has('no-start')) {
|
|
824
|
+
passiveStarted = false;
|
|
644
825
|
log(` (--no-start)请自行启动并守护: npx @atel-ai/agent run --data-dir ${cfg.dataDir}`);
|
|
645
826
|
}
|
|
646
827
|
else {
|
|
@@ -650,15 +831,23 @@ async function cmdInstall(flags, bools) {
|
|
|
650
831
|
}
|
|
651
832
|
}
|
|
652
833
|
// A8:写入 ~/.atel/agents.json 映射(供 `atel-agent list` / `--agent <alias>`)。
|
|
653
|
-
|
|
834
|
+
const existingAgent = readAgents()[alias];
|
|
835
|
+
upsertAgent(alias, {
|
|
836
|
+
schemaVersion: 2,
|
|
837
|
+
profile: alias,
|
|
654
838
|
did: identity.did,
|
|
655
839
|
runtime: cfg.runtime,
|
|
656
840
|
runtimeAgentId: cfg.runtimeAgentId,
|
|
841
|
+
name: flags['name'] || existingAgent?.name,
|
|
842
|
+
capabilities,
|
|
657
843
|
dataDir: cfg.dataDir,
|
|
844
|
+
identityPath: cfg.identityPath,
|
|
845
|
+
bindingPath: bindingPath(cfg.dataDir),
|
|
658
846
|
service: systemdUnitName(cfg.dataDir),
|
|
847
|
+
createdAt: existingAgent?.createdAt || now,
|
|
659
848
|
updatedAt: new Date().toISOString(),
|
|
660
849
|
});
|
|
661
|
-
printUsageModes(cfg, adapter);
|
|
850
|
+
printUsageModes(cfg, adapter, passiveStarted);
|
|
662
851
|
}
|
|
663
852
|
/**
|
|
664
853
|
* 收单回路自检(②):平台能不能把订单真的投递给本 agent?
|
|
@@ -670,7 +859,7 @@ async function cmdInstall(flags, bools) {
|
|
|
670
859
|
* 探针 → 用 companion 同款的 relayPoll 取回 → ack。取不回 = 收单回路不通,当场报红。
|
|
671
860
|
*/
|
|
672
861
|
async function checkReceivePath(platformBaseUrl, identity) {
|
|
673
|
-
const nonce = `doctor-probe-${
|
|
862
|
+
const nonce = `doctor-probe-${crypto.randomUUID()}`;
|
|
674
863
|
const send = await postSigned(platformBaseUrl, '/relay/v1/send', identity, { target: identity.did, message: { kind: 'delivery_probe', nonce } }, 'relay/send');
|
|
675
864
|
if (!send.ok)
|
|
676
865
|
return { ok: false, detail: `探针发送失败 HTTP ${send.status}: ${send.body.slice(0, 120)}` };
|
|
@@ -698,11 +887,16 @@ async function checkReceivePath(platformBaseUrl, identity) {
|
|
|
698
887
|
}
|
|
699
888
|
async function cmdDoctor(flags, bools) {
|
|
700
889
|
const dataDir = resolveDataDir(flags); // 支持 --agent <alias>
|
|
890
|
+
Object.assign(process.env, loadRuntimeEnv(dataDir));
|
|
701
891
|
const cfg = loadConfig(dataDir);
|
|
702
892
|
if (!cfg) {
|
|
703
893
|
err('❌ 未找到配置,先跑 install。');
|
|
704
894
|
process.exit(1);
|
|
705
895
|
}
|
|
896
|
+
if (bools.has('json')) {
|
|
897
|
+
await cmdDoctorJson(dataDir, cfg, bools);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
706
900
|
let allGreen = true;
|
|
707
901
|
let anyYellow = false;
|
|
708
902
|
// AC-16:把检查归到 6 大接入面,末尾给"完整接入"硬门。worst-status wins(红>黄>绿)。
|
|
@@ -737,6 +931,18 @@ async function cmdDoctor(flags, bools) {
|
|
|
737
931
|
if (!identity) {
|
|
738
932
|
process.exit(1);
|
|
739
933
|
}
|
|
934
|
+
const binding = readBinding(dataDir);
|
|
935
|
+
if (!binding) {
|
|
936
|
+
softCheck(false, '身份绑定(binding.json)', `缺 ${bindingPath(dataDir)} —— 旧安装可用,但产品级强绑定需要重新 connect`, '身份绑定');
|
|
937
|
+
}
|
|
938
|
+
else if (binding.did !== identity.did || binding.runtime !== cfg.runtime) {
|
|
939
|
+
check(false, '身份绑定(binding.json)', `不一致:binding did=${binding.did},runtime=${binding.runtime};当前 did=${identity.did},runtime=${cfg.runtime}`, '身份绑定');
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
942
|
+
softCheck(true, '身份绑定(binding.json)', `profile=${binding.profile},runtime=${binding.runtime},did=${binding.did}`, '身份绑定');
|
|
943
|
+
}
|
|
944
|
+
const envReady = runtimeEnvReadiness(cfg);
|
|
945
|
+
softCheck(envReady.ok, '模型环境(runtime.env)', envReady.detail, '模型环境');
|
|
740
946
|
// 适配器在场
|
|
741
947
|
const adapter = getAdapter(cfg.runtime);
|
|
742
948
|
let det = adapter ? await adapter.detect() : { present: false };
|
|
@@ -883,6 +1089,99 @@ async function cmdDoctor(flags, bools) {
|
|
|
883
1089
|
? '✅ 完整接入成功(被动接单 + 交互态新会话都就绪)。'
|
|
884
1090
|
: '🟢 核心通过(被动接单可用);⚠️ 交互态见上方 🟡 —— 自己开新会话用 ATEL 需按提示处理。');
|
|
885
1091
|
}
|
|
1092
|
+
async function cmdDoctorJson(dataDir, cfg, bools) {
|
|
1093
|
+
const checks = {};
|
|
1094
|
+
let ok = true;
|
|
1095
|
+
let identity = null;
|
|
1096
|
+
try {
|
|
1097
|
+
identity = loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8')));
|
|
1098
|
+
checks.identity = { ok: true, did: identity.did, identityPath: cfg.identityPath };
|
|
1099
|
+
}
|
|
1100
|
+
catch (e) {
|
|
1101
|
+
ok = false;
|
|
1102
|
+
checks.identity = { ok: false, error: e.message, identityPath: cfg.identityPath };
|
|
1103
|
+
}
|
|
1104
|
+
const binding = readBinding(dataDir);
|
|
1105
|
+
checks.binding = {
|
|
1106
|
+
ok: !!binding && !!identity && binding.did === identity.did && binding.runtime === cfg.runtime,
|
|
1107
|
+
path: bindingPath(dataDir),
|
|
1108
|
+
binding,
|
|
1109
|
+
};
|
|
1110
|
+
if (!checks.binding.ok)
|
|
1111
|
+
ok = false;
|
|
1112
|
+
const envReady = runtimeEnvReadiness(cfg);
|
|
1113
|
+
checks.runtimeEnv = envReady;
|
|
1114
|
+
if (!envReady.ok)
|
|
1115
|
+
ok = false;
|
|
1116
|
+
const adapter = getAdapter(cfg.runtime);
|
|
1117
|
+
let det = null;
|
|
1118
|
+
try {
|
|
1119
|
+
det = adapter ? await adapter.detect() : null;
|
|
1120
|
+
checks.adapter = { ok: !!adapter && !!det?.present, runtime: cfg.runtime, detect: det };
|
|
1121
|
+
if (!adapter || !det?.present)
|
|
1122
|
+
ok = false;
|
|
1123
|
+
}
|
|
1124
|
+
catch (e) {
|
|
1125
|
+
ok = false;
|
|
1126
|
+
checks.adapter = { ok: false, runtime: cfg.runtime, error: e.message };
|
|
1127
|
+
}
|
|
1128
|
+
if (identity) {
|
|
1129
|
+
try {
|
|
1130
|
+
const r = await spineIdentityVerify(cfg.spineBaseUrl, identity);
|
|
1131
|
+
checks.jwt = { ok: !!r.token, expiresAt: new Date(r.expiresAt).toISOString() };
|
|
1132
|
+
if (!r.token)
|
|
1133
|
+
ok = false;
|
|
1134
|
+
}
|
|
1135
|
+
catch (e) {
|
|
1136
|
+
ok = false;
|
|
1137
|
+
checks.jwt = { ok: false, error: e.message };
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
const b = await spineWalletBalance(cfg.spineBaseUrl, identity);
|
|
1141
|
+
checks.balance = { ok: b.ok, status: b.status, json: b.json };
|
|
1142
|
+
if (!b.ok)
|
|
1143
|
+
ok = false;
|
|
1144
|
+
}
|
|
1145
|
+
catch (e) {
|
|
1146
|
+
ok = false;
|
|
1147
|
+
checks.balance = { ok: false, error: e.message };
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
const rp = await checkReceivePath(cfg.platformBaseUrl, identity);
|
|
1151
|
+
checks.receivePath = rp;
|
|
1152
|
+
if (!rp.ok)
|
|
1153
|
+
ok = false;
|
|
1154
|
+
}
|
|
1155
|
+
catch (e) {
|
|
1156
|
+
ok = false;
|
|
1157
|
+
checks.receivePath = { ok: false, detail: e.message };
|
|
1158
|
+
}
|
|
1159
|
+
if (bools.has('probe') && adapter && det?.present) {
|
|
1160
|
+
try {
|
|
1161
|
+
const waker = adapter.createWaker({ config: cfg, identity, log: () => { } });
|
|
1162
|
+
const res = await waker.wake({ prompt: '调用 ATEL MCP 工具 atel_whoami,只回复返回的 did 字符串本身,别的都不要说。' });
|
|
1163
|
+
checks.agentProbe = { ok: (res.output || '').includes(identity.did), output: res.output, error: res.error, durationMs: res.durationMs };
|
|
1164
|
+
if (!checks.agentProbe.ok)
|
|
1165
|
+
ok = false;
|
|
1166
|
+
}
|
|
1167
|
+
catch (e) {
|
|
1168
|
+
ok = false;
|
|
1169
|
+
checks.agentProbe = { ok: false, error: e.message };
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
checks.companion = companionStatus(dataDir);
|
|
1174
|
+
const result = {
|
|
1175
|
+
ok,
|
|
1176
|
+
profile: cfg.profile || path.basename(dataDir),
|
|
1177
|
+
runtime: cfg.runtime,
|
|
1178
|
+
dataDir,
|
|
1179
|
+
checks,
|
|
1180
|
+
};
|
|
1181
|
+
log(JSON.stringify(result, null, 2));
|
|
1182
|
+
if (!ok)
|
|
1183
|
+
process.exit(1);
|
|
1184
|
+
}
|
|
886
1185
|
/** onboard = install + 后台起 companion(一步到位,喂给 agent 一句话即可)。 */
|
|
887
1186
|
async function cmdOnboard(flags, bools) {
|
|
888
1187
|
// cmdInstall 末尾已 superviseCompanion 守护 companion(systemd --user 或 detached)。
|
|
@@ -890,10 +1189,41 @@ async function cmdOnboard(flags, bools) {
|
|
|
890
1189
|
await cmdInstall(flags, bools);
|
|
891
1190
|
const dataDir = dataDirFrom(flags);
|
|
892
1191
|
log('');
|
|
893
|
-
|
|
1192
|
+
if (bools.has('no-start')) {
|
|
1193
|
+
log('🚀 onboard 完成:本次 --no-start,companion 未启动。');
|
|
1194
|
+
}
|
|
1195
|
+
else {
|
|
1196
|
+
log('🚀 onboard 完成:companion 已守护启动,正在无人值守自动推进 A2A/A2B。');
|
|
1197
|
+
}
|
|
894
1198
|
log(` 日志:${path.join(dataDir, 'companion.log')}`);
|
|
895
1199
|
log(` 状态:systemctl --user status 'atel-agent*' 或 ps aux | grep "run.*${dataDir}"`);
|
|
896
1200
|
}
|
|
1201
|
+
async function cmdConnect(flags, bools) {
|
|
1202
|
+
const next = withConnectDefaults(flags);
|
|
1203
|
+
if (bools.has('auto') && !next['runtime']) {
|
|
1204
|
+
const caller = detectCallerRuntime();
|
|
1205
|
+
if (!caller) {
|
|
1206
|
+
err('❌ --auto 无法判断当前 runtime。');
|
|
1207
|
+
err(' 请明确选择一个 runtime 重跑:');
|
|
1208
|
+
for (const a of ADAPTERS) {
|
|
1209
|
+
const profile = a.id === 'claude-code' ? 'claude'
|
|
1210
|
+
: a.id === 'gemini-cli' ? 'gemini'
|
|
1211
|
+
: a.id === 'qwen-agent' ? 'qwen'
|
|
1212
|
+
: a.id;
|
|
1213
|
+
err(` atel connect --runtime ${a.id} --profile ${profile} --name "${next['name'] || 'my-agent'}" --capabilities ${next['capabilities'] || 'writing'}`);
|
|
1214
|
+
}
|
|
1215
|
+
process.exit(1);
|
|
1216
|
+
}
|
|
1217
|
+
next['runtime'] = caller;
|
|
1218
|
+
}
|
|
1219
|
+
if (bools.has('auto') && !flags['profile'] && !flags['data-dir'] && next['runtime']) {
|
|
1220
|
+
next['profile'] = next['runtime'] === 'claude-code' ? 'claude'
|
|
1221
|
+
: next['runtime'] === 'gemini-cli' ? 'gemini'
|
|
1222
|
+
: next['runtime'] === 'qwen-agent' ? 'qwen'
|
|
1223
|
+
: next['runtime'];
|
|
1224
|
+
}
|
|
1225
|
+
await cmdOnboard(next, bools);
|
|
1226
|
+
}
|
|
897
1227
|
/** companion 自报 pid 的文件:cmdRun(companion 本体)写入,doctor 读它判活。 */
|
|
898
1228
|
function companionPidPath(dataDir) {
|
|
899
1229
|
return path.join(dataDir, 'companion.pid');
|
|
@@ -904,7 +1234,8 @@ function startCompanionDetached(dataDir) {
|
|
|
904
1234
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
905
1235
|
const out = fs.openSync(logPath, 'a');
|
|
906
1236
|
const cli = resolveStableCliPath(); // 不能把 npx 临时缓存路径喂给长期 detached 进程
|
|
907
|
-
const
|
|
1237
|
+
const childEnv = { ...process.env, ...loadRuntimeEnv(dataDir) };
|
|
1238
|
+
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
1239
|
child.unref();
|
|
909
1240
|
log(`▶️ companion 后台运行 pid=${child.pid}`);
|
|
910
1241
|
}
|
|
@@ -1015,6 +1346,32 @@ async function cmdMcp(flags) {
|
|
|
1015
1346
|
await runMcpShim(dataDir, VERSION);
|
|
1016
1347
|
await new Promise(() => { }); // 保活(stdio server 由宿主 CLI 生命周期驱动)
|
|
1017
1348
|
}
|
|
1349
|
+
async function withLocalMcp(dataDir, fn) {
|
|
1350
|
+
const launcher = mcpLauncher(dataDir);
|
|
1351
|
+
const transport = new StdioClientTransport({
|
|
1352
|
+
command: launcher.command,
|
|
1353
|
+
args: launcher.args,
|
|
1354
|
+
env: { ...cleanEnv(process.env), ...loadRuntimeEnv(dataDir) },
|
|
1355
|
+
cwd: dataDir,
|
|
1356
|
+
stderr: 'pipe',
|
|
1357
|
+
});
|
|
1358
|
+
let stderr = '';
|
|
1359
|
+
transport.stderr?.on('data', (d) => {
|
|
1360
|
+
stderr += d.toString();
|
|
1361
|
+
});
|
|
1362
|
+
const client = new Client({ name: 'atel-cli-probe', version: VERSION }, { capabilities: {} });
|
|
1363
|
+
try {
|
|
1364
|
+
await client.connect(transport);
|
|
1365
|
+
return await fn(client);
|
|
1366
|
+
}
|
|
1367
|
+
catch (e) {
|
|
1368
|
+
const detail = stderr.trim();
|
|
1369
|
+
throw new Error(`${e.message}${detail ? `\nMCP stderr:\n${detail.slice(-1500)}` : ''}`);
|
|
1370
|
+
}
|
|
1371
|
+
finally {
|
|
1372
|
+
await client.close().catch(() => { });
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1018
1375
|
function installSystemdUserUnit(dataDir) {
|
|
1019
1376
|
const unitName = systemdUnitName(dataDir);
|
|
1020
1377
|
const unitDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
@@ -1029,6 +1386,7 @@ function installSystemdUserUnit(dataDir) {
|
|
|
1029
1386
|
}
|
|
1030
1387
|
}
|
|
1031
1388
|
const logPath = path.join(dataDir, 'companion.log');
|
|
1389
|
+
const envPath = runtimeEnvPath(dataDir);
|
|
1032
1390
|
const cliPath = resolveStableCliPath(); // 不能把 npx 临时缓存路径写进 systemd 单元
|
|
1033
1391
|
// systemd 默认 PATH 很窄(/usr/bin:/bin),companion 要 spawn 底座 CLI(openclaw/claude/
|
|
1034
1392
|
// codex/gemini/hermes)就会 ENOENT(2026-06-12 BigBaby:openclaw 在 pnpm 全局 bin,不在
|
|
@@ -1051,6 +1409,7 @@ function installSystemdUserUnit(dataDir) {
|
|
|
1051
1409
|
'[Service]',
|
|
1052
1410
|
'Type=simple',
|
|
1053
1411
|
`Environment=PATH=${fullPath}`,
|
|
1412
|
+
`EnvironmentFile=-${envPath}`,
|
|
1054
1413
|
`ExecStart=${cliPath ? `${process.execPath} ${cliPath}` : `npx -y @atel-ai/agent@${VERSION}`} run --data-dir ${dataDir}`,
|
|
1055
1414
|
'Restart=always',
|
|
1056
1415
|
'RestartSec=5',
|
|
@@ -1077,6 +1436,7 @@ function installSystemdUserUnit(dataDir) {
|
|
|
1077
1436
|
}
|
|
1078
1437
|
async function cmdRun(flags) {
|
|
1079
1438
|
const dataDir = dataDirFrom(flags);
|
|
1439
|
+
Object.assign(process.env, loadRuntimeEnv(dataDir));
|
|
1080
1440
|
const cfg = loadConfig(dataDir);
|
|
1081
1441
|
if (!cfg) {
|
|
1082
1442
|
err('❌ 未找到配置,先跑 install。');
|
|
@@ -1137,6 +1497,294 @@ function cmdUpdate() {
|
|
|
1137
1497
|
process.exit(1);
|
|
1138
1498
|
}
|
|
1139
1499
|
}
|
|
1500
|
+
async function rewriteRuntimeConfig(dataDir) {
|
|
1501
|
+
Object.assign(process.env, loadRuntimeEnv(dataDir));
|
|
1502
|
+
const cfg = loadConfig(dataDir);
|
|
1503
|
+
if (!cfg)
|
|
1504
|
+
throw new Error('未找到 config.json');
|
|
1505
|
+
const adapter = getAdapter(cfg.runtime);
|
|
1506
|
+
if (!adapter)
|
|
1507
|
+
throw new Error(`未知 runtime ${cfg.runtime}`);
|
|
1508
|
+
const identity = loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8')));
|
|
1509
|
+
const auth = new AuthManager(identity, cfg.spineBaseUrl, {
|
|
1510
|
+
onToken: (token) => adapter.writeMcpConfig({ mcpUrl: cfg.mcpUrl, token, identity, config: cfg, mcpCommand: mcpLauncher(cfg.dataDir) }),
|
|
1511
|
+
log,
|
|
1512
|
+
});
|
|
1513
|
+
await auth.start();
|
|
1514
|
+
auth.stop();
|
|
1515
|
+
writeSkill(cfg, adapter);
|
|
1516
|
+
}
|
|
1517
|
+
async function cmdRepair(flags) {
|
|
1518
|
+
const dataDir = resolveDataDir(flags);
|
|
1519
|
+
try {
|
|
1520
|
+
await rewriteRuntimeConfig(dataDir);
|
|
1521
|
+
log('🟢 repair 完成:MCP 配置与 SKILL 已重写。');
|
|
1522
|
+
}
|
|
1523
|
+
catch (e) {
|
|
1524
|
+
err(`❌ repair 失败:${e.message}`);
|
|
1525
|
+
process.exit(1);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function removeTomlSection(file, section, mustContain) {
|
|
1529
|
+
let content = '';
|
|
1530
|
+
try {
|
|
1531
|
+
content = fs.readFileSync(file, 'utf8');
|
|
1532
|
+
}
|
|
1533
|
+
catch {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
const header = `[${section}]`;
|
|
1537
|
+
const lines = content.split(/\r?\n/);
|
|
1538
|
+
const out = [];
|
|
1539
|
+
let changed = false;
|
|
1540
|
+
for (let i = 0; i < lines.length;) {
|
|
1541
|
+
if (lines[i].trim() === header) {
|
|
1542
|
+
let j = i + 1;
|
|
1543
|
+
while (j < lines.length && !/^\[.+\]\s*$/.test(lines[j].trim()))
|
|
1544
|
+
j++;
|
|
1545
|
+
const block = lines.slice(i, j).join('\n');
|
|
1546
|
+
if (!mustContain || block.includes(mustContain)) {
|
|
1547
|
+
changed = true;
|
|
1548
|
+
i = j;
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
out.push(lines[i++]);
|
|
1553
|
+
}
|
|
1554
|
+
if (changed)
|
|
1555
|
+
fs.writeFileSync(file, out.join('\n').replace(/\n{3,}/g, '\n\n'));
|
|
1556
|
+
return changed;
|
|
1557
|
+
}
|
|
1558
|
+
function removeJsonMcpServer(file, serverName, mustContain) {
|
|
1559
|
+
let j = {};
|
|
1560
|
+
try {
|
|
1561
|
+
j = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1562
|
+
}
|
|
1563
|
+
catch {
|
|
1564
|
+
return false;
|
|
1565
|
+
}
|
|
1566
|
+
const server = j.mcpServers?.[serverName];
|
|
1567
|
+
if (!server)
|
|
1568
|
+
return false;
|
|
1569
|
+
if (mustContain && !JSON.stringify(server).includes(mustContain))
|
|
1570
|
+
return false;
|
|
1571
|
+
delete j.mcpServers[serverName];
|
|
1572
|
+
fs.writeFileSync(file, JSON.stringify(j, null, 2));
|
|
1573
|
+
return true;
|
|
1574
|
+
}
|
|
1575
|
+
function removeYamlMcpServer(file, serverName, mustContain) {
|
|
1576
|
+
let lines;
|
|
1577
|
+
try {
|
|
1578
|
+
lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
1579
|
+
}
|
|
1580
|
+
catch {
|
|
1581
|
+
return false;
|
|
1582
|
+
}
|
|
1583
|
+
const parent = lines.findIndex((line) => line.trim() === 'mcp_servers:');
|
|
1584
|
+
if (parent < 0)
|
|
1585
|
+
return false;
|
|
1586
|
+
let serverStart = -1;
|
|
1587
|
+
for (let i = parent + 1; i < lines.length; i++) {
|
|
1588
|
+
const line = lines[i];
|
|
1589
|
+
if (line && !line.startsWith(' ') && !line.startsWith('\t'))
|
|
1590
|
+
break;
|
|
1591
|
+
if (new RegExp(`^\\s{2}${serverName}:\\s*$`).test(line)) {
|
|
1592
|
+
serverStart = i;
|
|
1593
|
+
break;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
if (serverStart < 0)
|
|
1597
|
+
return false;
|
|
1598
|
+
let serverEnd = lines.length;
|
|
1599
|
+
for (let i = serverStart + 1; i < lines.length; i++) {
|
|
1600
|
+
const line = lines[i];
|
|
1601
|
+
if (line && !line.startsWith(' ') && !line.startsWith('\t')) {
|
|
1602
|
+
serverEnd = i;
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
if (/^\s{2}\S[^:]*:\s*$/.test(line)) {
|
|
1606
|
+
serverEnd = i;
|
|
1607
|
+
break;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const block = lines.slice(serverStart, serverEnd).join('\n');
|
|
1611
|
+
if (mustContain && !block.includes(mustContain))
|
|
1612
|
+
return false;
|
|
1613
|
+
const next = [...lines.slice(0, serverStart), ...lines.slice(serverEnd)];
|
|
1614
|
+
let parentStillHasChild = false;
|
|
1615
|
+
for (let i = parent + 1; i < next.length; i++) {
|
|
1616
|
+
const line = next[i];
|
|
1617
|
+
if (!line.trim() || line.trim().startsWith('#'))
|
|
1618
|
+
continue;
|
|
1619
|
+
if (!line.startsWith(' ') && !line.startsWith('\t'))
|
|
1620
|
+
break;
|
|
1621
|
+
if (/^\s{2}\S[^:]*:/.test(line)) {
|
|
1622
|
+
parentStillHasChild = true;
|
|
1623
|
+
break;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (!parentStillHasChild)
|
|
1627
|
+
next.splice(parent, 1);
|
|
1628
|
+
fs.writeFileSync(file, next.join('\n').replace(/\n{3,}/g, '\n\n'));
|
|
1629
|
+
return true;
|
|
1630
|
+
}
|
|
1631
|
+
function removeRuntimeMcpConfig(cfg) {
|
|
1632
|
+
const removed = [];
|
|
1633
|
+
const marker = cfg.dataDir;
|
|
1634
|
+
if (cfg.runtime === 'codex') {
|
|
1635
|
+
const f = path.join(os.homedir(), '.codex', 'config.toml');
|
|
1636
|
+
if (removeTomlSection(f, 'mcp_servers.atel', marker))
|
|
1637
|
+
removed.push(f);
|
|
1638
|
+
}
|
|
1639
|
+
else if (cfg.runtime === 'claude-code') {
|
|
1640
|
+
const global = path.join(os.homedir(), '.claude.json');
|
|
1641
|
+
const local = path.join(cfg.dataDir, 'claude-mcp.json');
|
|
1642
|
+
if (removeJsonMcpServer(global, 'atel', marker))
|
|
1643
|
+
removed.push(global);
|
|
1644
|
+
if (fs.existsSync(local)) {
|
|
1645
|
+
fs.rmSync(local, { force: true });
|
|
1646
|
+
removed.push(local);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
else if (cfg.runtime === 'gemini-cli') {
|
|
1650
|
+
const f = path.join(os.homedir(), '.gemini', 'settings.json');
|
|
1651
|
+
if (removeJsonMcpServer(f, 'atel', marker))
|
|
1652
|
+
removed.push(f);
|
|
1653
|
+
}
|
|
1654
|
+
else if (cfg.runtime === 'qwen-agent') {
|
|
1655
|
+
const f = path.join(cfg.dataDir, 'qwen-mcp.json');
|
|
1656
|
+
if (fs.existsSync(f)) {
|
|
1657
|
+
fs.rmSync(f, { force: true });
|
|
1658
|
+
removed.push(f);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
else if (cfg.runtime === 'hermes') {
|
|
1662
|
+
const token = path.join(cfg.dataDir, 'hermes-mcp-token.txt');
|
|
1663
|
+
const envPath = path.join(os.homedir(), '.hermes', '.env');
|
|
1664
|
+
const yamlPath = path.join(os.homedir(), '.hermes', 'config.yaml');
|
|
1665
|
+
if (fs.existsSync(token)) {
|
|
1666
|
+
fs.rmSync(token, { force: true });
|
|
1667
|
+
removed.push(token);
|
|
1668
|
+
}
|
|
1669
|
+
if (removeYamlMcpServer(yamlPath, 'atel', cfg.mcpUrl))
|
|
1670
|
+
removed.push(yamlPath);
|
|
1671
|
+
try {
|
|
1672
|
+
let s = fs.readFileSync(envPath, 'utf8');
|
|
1673
|
+
if (s.includes('MCP_ATEL_API_KEY=')) {
|
|
1674
|
+
s = s.split(/\r?\n/).filter((line) => !line.startsWith('MCP_ATEL_API_KEY=')).join('\n');
|
|
1675
|
+
fs.writeFileSync(envPath, `${s.replace(/\n*$/, '')}\n`);
|
|
1676
|
+
removed.push(envPath);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
catch { /* ignore */ }
|
|
1680
|
+
}
|
|
1681
|
+
else if (cfg.runtime === 'openclaw') {
|
|
1682
|
+
const f = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
1683
|
+
if (removeJsonMcpServer(f, 'atel', cfg.mcpUrl))
|
|
1684
|
+
removed.push(f);
|
|
1685
|
+
}
|
|
1686
|
+
return removed;
|
|
1687
|
+
}
|
|
1688
|
+
async function cmdDisconnect(flags, bools) {
|
|
1689
|
+
const dataDir = resolveDataDir(flags);
|
|
1690
|
+
const cfg = loadConfig(dataDir);
|
|
1691
|
+
if (!cfg) {
|
|
1692
|
+
err('❌ 未找到配置。');
|
|
1693
|
+
process.exit(1);
|
|
1694
|
+
}
|
|
1695
|
+
stopCompanion(dataDir);
|
|
1696
|
+
const removedConfigs = removeRuntimeMcpConfig(cfg);
|
|
1697
|
+
const alias = cfg.profile || aliasFromFlags(flags);
|
|
1698
|
+
removeAgent(alias);
|
|
1699
|
+
if (bools.has('delete-data')) {
|
|
1700
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
1701
|
+
log(`🟡 已断开并删除 profile 数据:${dataDir}`);
|
|
1702
|
+
}
|
|
1703
|
+
else {
|
|
1704
|
+
log(`🟡 已断开 profile=${alias};身份文件仍保留在 ${dataDir}`);
|
|
1705
|
+
log(' 如需删除本地身份与配置,重跑: atel disconnect --profile <profile> --delete-data');
|
|
1706
|
+
}
|
|
1707
|
+
if (removedConfigs.length)
|
|
1708
|
+
log(` 已移除 runtime MCP 配置:${removedConfigs.join(', ')}`);
|
|
1709
|
+
}
|
|
1710
|
+
async function cmdUninstall(flags, bools) {
|
|
1711
|
+
if (flags['profile'] || flags['agent'] || flags['data-dir']) {
|
|
1712
|
+
await cmdDisconnect(flags, new Set([...bools, 'delete-data']));
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (!bools.has('confirm')) {
|
|
1716
|
+
err('❌ 这会删除本机 ~/.atel 下的 ATEL profiles。确认请加 --confirm。');
|
|
1717
|
+
process.exit(1);
|
|
1718
|
+
}
|
|
1719
|
+
const all = readAgents();
|
|
1720
|
+
for (const e of Object.values(all))
|
|
1721
|
+
stopCompanion(e.dataDir);
|
|
1722
|
+
fs.rmSync(path.join(os.homedir(), '.atel'), { recursive: true, force: true });
|
|
1723
|
+
log('🟡 已删除 ~/.atel 本地数据。全局 atel 程序请用 npm uninstall -g @atel-ai/agent 或安装器卸载。');
|
|
1724
|
+
}
|
|
1725
|
+
function stopCompanion(dataDir) {
|
|
1726
|
+
const unit = systemdUnitName(dataDir);
|
|
1727
|
+
const env = userSystemctlEnv();
|
|
1728
|
+
try {
|
|
1729
|
+
execFileSync('systemctl', ['--user', 'stop', unit], { stdio: 'ignore', timeout: 10_000, env });
|
|
1730
|
+
}
|
|
1731
|
+
catch { /* not systemd or not running */ }
|
|
1732
|
+
const st = companionStatus(dataDir);
|
|
1733
|
+
if (st.pid) {
|
|
1734
|
+
try {
|
|
1735
|
+
process.kill(st.pid, 'SIGTERM');
|
|
1736
|
+
}
|
|
1737
|
+
catch { /* already gone */ }
|
|
1738
|
+
}
|
|
1739
|
+
try {
|
|
1740
|
+
fs.rmSync(companionPidPath(dataDir), { force: true });
|
|
1741
|
+
}
|
|
1742
|
+
catch { /* ignore */ }
|
|
1743
|
+
}
|
|
1744
|
+
function cmdService(flags, positionals) {
|
|
1745
|
+
const action = positionals[0] || 'status';
|
|
1746
|
+
const dataDir = resolveDataDir(flags);
|
|
1747
|
+
const cfg = loadConfig(dataDir);
|
|
1748
|
+
if (!cfg) {
|
|
1749
|
+
err('❌ 未找到配置,先跑 connect。');
|
|
1750
|
+
process.exit(1);
|
|
1751
|
+
}
|
|
1752
|
+
if (action === 'status') {
|
|
1753
|
+
const s = companionStatus(dataDir);
|
|
1754
|
+
log(`${s.state === 'running' ? '🟢' : s.state === 'stale' ? '🔴' : '🟡'} companion ${s.state} — ${s.detail}`);
|
|
1755
|
+
log(` service=${s.service}`);
|
|
1756
|
+
log(` log=${path.join(dataDir, 'companion.log')}`);
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (action === 'start') {
|
|
1760
|
+
const mode = superviseCompanion(dataDir);
|
|
1761
|
+
log(`🟢 companion start requested (${mode})`);
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (action === 'stop') {
|
|
1765
|
+
stopCompanion(dataDir);
|
|
1766
|
+
log('🟡 companion stopped');
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (action === 'restart') {
|
|
1770
|
+
stopCompanion(dataDir);
|
|
1771
|
+
const mode = superviseCompanion(dataDir);
|
|
1772
|
+
log(`🟢 companion restarted (${mode})`);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
if (action === 'logs' || action === 'log') {
|
|
1776
|
+
const logPath = path.join(dataDir, 'companion.log');
|
|
1777
|
+
if (!fs.existsSync(logPath)) {
|
|
1778
|
+
log(`(暂无日志 ${logPath})`);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
const out = execFileSync('tail', ['-n', flags['lines'] || '80', logPath], { encoding: 'utf8' });
|
|
1782
|
+
process.stdout.write(out);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
err(`❌ 未知 service 子命令:${action}。可用: status/start/stop/restart/logs`);
|
|
1786
|
+
process.exit(1);
|
|
1787
|
+
}
|
|
1140
1788
|
function cmdList() {
|
|
1141
1789
|
const all = readAgents();
|
|
1142
1790
|
const aliases = Object.keys(all);
|
|
@@ -1147,7 +1795,75 @@ function cmdList() {
|
|
|
1147
1795
|
log('本机 ATEL agents:');
|
|
1148
1796
|
for (const a of aliases) {
|
|
1149
1797
|
const e = all[a];
|
|
1150
|
-
|
|
1798
|
+
const name = e.name ? ` name=${e.name}` : '';
|
|
1799
|
+
const caps = e.capabilities?.length ? ` capabilities=${e.capabilities.join(',')}` : '';
|
|
1800
|
+
log(` • ${a} ${e.did} [${e.runtime}${e.runtimeAgentId ? '/' + e.runtimeAgentId : ''}]${name}${caps} dataDir=${e.dataDir}`);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function companionStatus(dataDir) {
|
|
1804
|
+
const service = systemdUnitName(dataDir);
|
|
1805
|
+
const pidPath = companionPidPath(dataDir);
|
|
1806
|
+
if (!fs.existsSync(pidPath))
|
|
1807
|
+
return { state: 'stopped', detail: '无 companion.pid', service };
|
|
1808
|
+
const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
1809
|
+
if (!Number.isFinite(pid))
|
|
1810
|
+
return { state: 'stale', detail: `pid 文件无效:${pidPath}`, service };
|
|
1811
|
+
try {
|
|
1812
|
+
process.kill(pid, 0);
|
|
1813
|
+
return { state: 'running', detail: `pid=${pid}`, pid, service };
|
|
1814
|
+
}
|
|
1815
|
+
catch (e) {
|
|
1816
|
+
if (e.code === 'EPERM')
|
|
1817
|
+
return { state: 'running', detail: `pid=${pid}(EPERM)`, pid, service };
|
|
1818
|
+
return { state: 'stale', detail: `pid=${pid} 已退出`, pid, service };
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
function statusFor(dataDir) {
|
|
1822
|
+
const cfg = loadConfig(dataDir);
|
|
1823
|
+
const binding = readBinding(dataDir);
|
|
1824
|
+
let did = '';
|
|
1825
|
+
try {
|
|
1826
|
+
if (cfg && fs.existsSync(cfg.identityPath))
|
|
1827
|
+
did = loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8'))).did;
|
|
1828
|
+
}
|
|
1829
|
+
catch { /* ignore */ }
|
|
1830
|
+
const svc = companionStatus(dataDir);
|
|
1831
|
+
return {
|
|
1832
|
+
ok: !!cfg && !!did && !!binding && binding.did === did && binding.runtime === cfg.runtime,
|
|
1833
|
+
profile: cfg?.profile || binding?.profile || path.basename(dataDir),
|
|
1834
|
+
runtime: cfg?.runtime || binding?.runtime || null,
|
|
1835
|
+
did: did || binding?.did || null,
|
|
1836
|
+
dataDir,
|
|
1837
|
+
binding: binding ? 'present' : 'missing',
|
|
1838
|
+
companion: svc.state,
|
|
1839
|
+
companionDetail: svc.detail,
|
|
1840
|
+
service: svc.service,
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1843
|
+
function cmdStatus(flags, bools) {
|
|
1844
|
+
if (flags['profile'] || flags['data-dir'] || flags['agent']) {
|
|
1845
|
+
const s = statusFor(resolveDataDir(flags));
|
|
1846
|
+
if (bools.has('json'))
|
|
1847
|
+
log(JSON.stringify(s, null, 2));
|
|
1848
|
+
else {
|
|
1849
|
+
log(`${s.ok ? '🟢' : '🟡'} ${s.profile} [${s.runtime}] ${s.did || '(no did)'}`);
|
|
1850
|
+
log(` dataDir=${s.dataDir}`);
|
|
1851
|
+
log(` binding=${s.binding}, companion=${s.companion} (${s.companionDetail})`);
|
|
1852
|
+
}
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
const all = readAgents();
|
|
1856
|
+
const statuses = Object.values(all).map((e) => statusFor(e.dataDir));
|
|
1857
|
+
if (bools.has('json')) {
|
|
1858
|
+
log(JSON.stringify({ agents: statuses }, null, 2));
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (!statuses.length) {
|
|
1862
|
+
log('(还没有 agent;用 atel connect --runtime <id> --profile <profile> 接入)');
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
for (const s of statuses) {
|
|
1866
|
+
log(`${s.ok ? '🟢' : '🟡'} ${s.profile} [${s.runtime}] ${s.did || '(no did)'} companion=${s.companion}`);
|
|
1151
1867
|
}
|
|
1152
1868
|
}
|
|
1153
1869
|
function cmdWhoami(flags) {
|
|
@@ -1160,6 +1876,173 @@ function cmdWhoami(flags) {
|
|
|
1160
1876
|
const id = loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8')));
|
|
1161
1877
|
log(id.did);
|
|
1162
1878
|
}
|
|
1879
|
+
async function cmdBalance(flags) {
|
|
1880
|
+
const dataDir = resolveDataDir(flags); // 支持 --agent <alias>
|
|
1881
|
+
const cfg = loadConfig(dataDir);
|
|
1882
|
+
if (!cfg || !fs.existsSync(cfg.identityPath)) {
|
|
1883
|
+
err('❌ 未接入,先跑 connect。');
|
|
1884
|
+
process.exit(1);
|
|
1885
|
+
}
|
|
1886
|
+
const id = loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8')));
|
|
1887
|
+
try {
|
|
1888
|
+
const b = await spineWalletBalance(cfg.spineBaseUrl, id);
|
|
1889
|
+
if (!b.ok) {
|
|
1890
|
+
err(`❌ 余额查询失败 HTTP ${b.status}: ${b.body.slice(0, 200)}`);
|
|
1891
|
+
process.exit(1);
|
|
1892
|
+
}
|
|
1893
|
+
log(typeof b.json === 'object' ? JSON.stringify(b.json, null, 2) : String(b.body));
|
|
1894
|
+
}
|
|
1895
|
+
catch (e) {
|
|
1896
|
+
err(`❌ 余额查询失败:${e.message}`);
|
|
1897
|
+
process.exit(1);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
async function cmdTools(flags, bools) {
|
|
1901
|
+
const dataDir = resolveDataDir(flags);
|
|
1902
|
+
if (!loadConfig(dataDir)) {
|
|
1903
|
+
err('❌ 未找到配置,先跑 connect。');
|
|
1904
|
+
process.exit(1);
|
|
1905
|
+
}
|
|
1906
|
+
const tools = await withLocalMcp(dataDir, async (client) => (await client.listTools()).tools || []);
|
|
1907
|
+
if (bools.has('json')) {
|
|
1908
|
+
log(JSON.stringify({ tools }, null, 2));
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
log(`ATEL tools (${tools.length}):`);
|
|
1912
|
+
for (const t of tools) {
|
|
1913
|
+
const name = t.name;
|
|
1914
|
+
const desc = t.description;
|
|
1915
|
+
if (typeof name === 'string')
|
|
1916
|
+
log(` • ${name}${typeof desc === 'string' && desc ? ` — ${desc}` : ''}`);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
async function cmdMcpProbe(flags, bools) {
|
|
1920
|
+
const dataDir = resolveDataDir(flags);
|
|
1921
|
+
const cfg = loadConfig(dataDir);
|
|
1922
|
+
if (!cfg) {
|
|
1923
|
+
err('❌ 未找到配置,先跑 connect。');
|
|
1924
|
+
process.exit(1);
|
|
1925
|
+
}
|
|
1926
|
+
const id = fs.existsSync(cfg.identityPath)
|
|
1927
|
+
? loadIdentity(JSON.parse(fs.readFileSync(cfg.identityPath, 'utf8')))
|
|
1928
|
+
: null;
|
|
1929
|
+
const result = await withLocalMcp(dataDir, async (client) => {
|
|
1930
|
+
const tools = (await client.listTools()).tools || [];
|
|
1931
|
+
const who = await client.callTool({ name: 'atel_whoami', arguments: {} });
|
|
1932
|
+
const text = JSON.stringify(who);
|
|
1933
|
+
return {
|
|
1934
|
+
ok: !!id && text.includes(id.did),
|
|
1935
|
+
did: id?.did || null,
|
|
1936
|
+
toolCount: tools.length,
|
|
1937
|
+
hasWhoami: tools.some((t) => t.name === 'atel_whoami'),
|
|
1938
|
+
whoami: who,
|
|
1939
|
+
};
|
|
1940
|
+
});
|
|
1941
|
+
if (bools.has('json')) {
|
|
1942
|
+
log(JSON.stringify(result, null, 2));
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
if (!result.ok) {
|
|
1946
|
+
err(`🔴 MCP probe 失败: whoami 未返回当前 DID ${result.did || '(unknown)'}`);
|
|
1947
|
+
process.exit(1);
|
|
1948
|
+
}
|
|
1949
|
+
log(`🟢 MCP probe 通过 — tools=${result.toolCount}, did=${result.did}`);
|
|
1950
|
+
}
|
|
1951
|
+
function parseJsonArg(flags) {
|
|
1952
|
+
const raw = flags['args'] || flags['json-args'] || '{}';
|
|
1953
|
+
if (raw.startsWith('@')) {
|
|
1954
|
+
return JSON.parse(fs.readFileSync(raw.slice(1), 'utf8'));
|
|
1955
|
+
}
|
|
1956
|
+
return JSON.parse(raw);
|
|
1957
|
+
}
|
|
1958
|
+
async function cmdInvoke(flags, bools) {
|
|
1959
|
+
const dataDir = resolveDataDir(flags);
|
|
1960
|
+
if (!loadConfig(dataDir)) {
|
|
1961
|
+
err('❌ 未找到配置,先跑 connect。');
|
|
1962
|
+
process.exit(1);
|
|
1963
|
+
}
|
|
1964
|
+
const tool = flags['tool'];
|
|
1965
|
+
if (!tool) {
|
|
1966
|
+
err('❌ 缺 --tool <工具名>,例如 --tool atel_whoami');
|
|
1967
|
+
process.exit(1);
|
|
1968
|
+
}
|
|
1969
|
+
const writeTool = !['atel_whoami', 'atel_balance', 'atel_deposit_info', 'atel_agent_search', 'atel_inbox_list'].includes(tool);
|
|
1970
|
+
if (writeTool && !bools.has('confirm')) {
|
|
1971
|
+
err(`❌ ${tool} 可能是写操作。若确认要执行,加 --confirm。只读排障建议先用 atel_whoami/atel_balance。`);
|
|
1972
|
+
process.exit(1);
|
|
1973
|
+
}
|
|
1974
|
+
let args;
|
|
1975
|
+
try {
|
|
1976
|
+
args = parseJsonArg(flags);
|
|
1977
|
+
}
|
|
1978
|
+
catch (e) {
|
|
1979
|
+
err(`❌ --args 不是合法 JSON:${e.message}`);
|
|
1980
|
+
process.exit(1);
|
|
1981
|
+
}
|
|
1982
|
+
const result = await withLocalMcp(dataDir, async (client) => client.callTool({ name: tool, arguments: args }));
|
|
1983
|
+
if (bools.has('json')) {
|
|
1984
|
+
log(JSON.stringify(result, null, 2));
|
|
1985
|
+
}
|
|
1986
|
+
else {
|
|
1987
|
+
log(JSON.stringify(result, null, 2));
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
async function cmdSmoke(flags, bools) {
|
|
1991
|
+
const dataDir = resolveDataDir(flags);
|
|
1992
|
+
if (!loadConfig(dataDir)) {
|
|
1993
|
+
err('❌ 未找到配置,先跑 connect。');
|
|
1994
|
+
process.exit(1);
|
|
1995
|
+
}
|
|
1996
|
+
const checks = await withLocalMcp(dataDir, async (client) => {
|
|
1997
|
+
const tools = (await client.listTools()).tools || [];
|
|
1998
|
+
const toolNames = new Set(tools.map((t) => t.name));
|
|
1999
|
+
const call = async (name, args = {}) => {
|
|
2000
|
+
if (!toolNames.has(name))
|
|
2001
|
+
return { ok: false, skipped: true, reason: 'tool_missing' };
|
|
2002
|
+
try {
|
|
2003
|
+
const result = await client.callTool({ name, arguments: args });
|
|
2004
|
+
return { ok: true, result };
|
|
2005
|
+
}
|
|
2006
|
+
catch (e) {
|
|
2007
|
+
return { ok: false, error: e.message };
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
const readonly = {
|
|
2011
|
+
whoami: await call('atel_whoami'),
|
|
2012
|
+
balance: await call('atel_balance'),
|
|
2013
|
+
agentSearch: await call('atel_agent_search', {}),
|
|
2014
|
+
inboxList: await call('atel_inbox_list', {}),
|
|
2015
|
+
};
|
|
2016
|
+
const prepare = {
|
|
2017
|
+
orderCreate: toolNames.has('atel_order_create') ? 'available_requires_user_confirm' : 'missing',
|
|
2018
|
+
fmSell: toolNames.has('atel_fm_sell') ? 'available_requires_user_confirm' : 'missing',
|
|
2019
|
+
walletTransfer: toolNames.has('atel_wallet_transfer') ? 'available_requires_user_confirm' : 'missing',
|
|
2020
|
+
a2bSearch: toolNames.has('atel_a2b_search') ? 'available_readonly_search' : 'missing',
|
|
2021
|
+
};
|
|
2022
|
+
return { toolCount: tools.length, readonly, prepare };
|
|
2023
|
+
});
|
|
2024
|
+
const readonlyOk = Object.values(checks.readonly).every((v) => v.ok === true);
|
|
2025
|
+
const requiredToolsPresent = checks.toolCount >= 10
|
|
2026
|
+
&& checks.prepare.orderCreate !== 'missing'
|
|
2027
|
+
&& checks.prepare.fmSell !== 'missing'
|
|
2028
|
+
&& checks.prepare.walletTransfer !== 'missing'
|
|
2029
|
+
&& checks.prepare.a2bSearch !== 'missing';
|
|
2030
|
+
const ok = readonlyOk && requiredToolsPresent;
|
|
2031
|
+
const result = { ok, ...checks };
|
|
2032
|
+
if (bools.has('json')) {
|
|
2033
|
+
log(JSON.stringify(result, null, 2));
|
|
2034
|
+
}
|
|
2035
|
+
else {
|
|
2036
|
+
log(`${ok ? '🟢' : '🔴'} smoke ${ok ? '通过' : '有失败'} — tools=${checks.toolCount}`);
|
|
2037
|
+
for (const [k, v] of Object.entries(checks.readonly)) {
|
|
2038
|
+
const item = v;
|
|
2039
|
+
log(` • ${k}: ${item.ok ? 'ok' : item.skipped ? `skipped(${item.reason})` : `fail(${item.error})`}`);
|
|
2040
|
+
}
|
|
2041
|
+
log(` • writes: order=${checks.prepare.orderCreate}, fm=${checks.prepare.fmSell}, transfer=${checks.prepare.walletTransfer}, a2b=${checks.prepare.a2bSearch}`);
|
|
2042
|
+
}
|
|
2043
|
+
if (!ok)
|
|
2044
|
+
process.exit(1);
|
|
2045
|
+
}
|
|
1163
2046
|
async function cmdDetect() {
|
|
1164
2047
|
const all = await detectAll();
|
|
1165
2048
|
if (all.length === 0) {
|
|
@@ -1170,10 +2053,35 @@ async function cmdDetect() {
|
|
|
1170
2053
|
log(`🟢 ${adapter.displayName} (${adapter.id}) ${result.version || ''} ${result.detail || ''}`);
|
|
1171
2054
|
}
|
|
1172
2055
|
}
|
|
2056
|
+
function cmdPrompt(flags) {
|
|
2057
|
+
const dataDir = resolveDataDir(flags);
|
|
2058
|
+
const cfg = loadConfig(dataDir);
|
|
2059
|
+
const binding = readBinding(dataDir);
|
|
2060
|
+
if (!cfg || !binding) {
|
|
2061
|
+
err('❌ 未接入或缺 binding.json,先跑 connect。');
|
|
2062
|
+
process.exit(1);
|
|
2063
|
+
}
|
|
2064
|
+
const toolPrefix = cfg.runtime === 'codex' || cfg.runtime === 'gemini-cli' || cfg.runtime === 'claude-code'
|
|
2065
|
+
? 'atel__atel_whoami'
|
|
2066
|
+
: 'atel_whoami';
|
|
2067
|
+
log(`你已经接入 ATEL。profile=${binding.profile}, runtime=${binding.runtime}, DID=${binding.did}。读取本机 ${path.join(dataDir, 'SKILL.md')};使用已配置的 ATEL MCP 工具处理 ATEL 请求。先调用 ${toolPrefix} 验证身份,确认返回 DID=${binding.did}。`);
|
|
2068
|
+
}
|
|
1173
2069
|
function help() {
|
|
1174
|
-
log(
|
|
2070
|
+
log(`atel — ATEL 产品级 CLI
|
|
1175
2071
|
|
|
1176
2072
|
用法:
|
|
2073
|
+
atel connect [--runtime <id>] [--name <agent名>] [--capabilities coding,writing]
|
|
2074
|
+
[--profile <别名>] [--runtime-agent-id <id>] [--update-existing] [...] # 推荐入口:接入 + 起 companion
|
|
2075
|
+
atel status [--profile <别名> | --agent <别名>] [--json]
|
|
2076
|
+
atel service status|start|stop|restart|logs [--profile <别名> | --agent <别名>]
|
|
2077
|
+
atel tools [--profile <别名> | --agent <别名>] [--json]
|
|
2078
|
+
atel mcp-probe [--profile <别名> | --agent <别名>] [--json]
|
|
2079
|
+
atel invoke [--profile <别名> | --agent <别名>] --tool atel_whoami [--args '{}'] [--json] [--confirm]
|
|
2080
|
+
atel smoke [--profile <别名> | --agent <别名>] [--json] # 只读/安全业务验活
|
|
2081
|
+
atel prompt [--profile <别名> | --agent <别名>]
|
|
2082
|
+
atel repair [--profile <别名> | --agent <别名>] # 重写 MCP 配置与 SKILL
|
|
2083
|
+
atel disconnect [--profile <别名> | --agent <别名>] [--delete-data] # 断开一个 profile
|
|
2084
|
+
atel uninstall [--confirm] # 删除本机 ATEL profile 数据
|
|
1177
2085
|
npx @atel-ai/agent onboard [--runtime <id>] [--name <agent名>] [--capabilities coding,writing]
|
|
1178
2086
|
[--profile <别名>] [--runtime-agent-id <id>] [--update-existing] [...] # install + 起 companion(推荐)
|
|
1179
2087
|
npx @atel-ai/agent install [--runtime <id>] [--name <agent名>] [--capabilities coding,writing]
|
|
@@ -1185,28 +2093,53 @@ function help() {
|
|
|
1185
2093
|
npx @atel-ai/agent doctor [--data-dir D | --profile <别名> | --agent <别名>] [--probe]
|
|
1186
2094
|
npx @atel-ai/agent list # 列出本机所有 ATEL agent(~/.atel/agents.json)
|
|
1187
2095
|
npx @atel-ai/agent whoami [--data-dir D | --profile <别名> | --agent <别名>]
|
|
2096
|
+
npx @atel-ai/agent balance [--data-dir D | --profile <别名> | --agent <别名>]
|
|
1188
2097
|
npx @atel-ai/agent detect
|
|
1189
2098
|
npx @atel-ai/agent update
|
|
1190
2099
|
|
|
1191
2100
|
说明:
|
|
1192
2101
|
• 多 runtime 同机时不传 --runtime 会安全失败 —— 用 --runtime <id> 或环境变量 ATEL_RUNTIME。
|
|
1193
|
-
•
|
|
2102
|
+
• 通用接入句可用: atel connect --auto --name "my-agent" --capabilities coding,writing。
|
|
2103
|
+
• 产品级 connect 默认使用 --profile 隔离(→ ~/.atel/agents/<别名>);openclaw 多 agent 再加 --runtime-agent-id。
|
|
1194
2104
|
• 同 data-dir 改名需 --update-existing(否则拒绝,防静默改同一 DID 的名)。
|
|
1195
2105
|
|
|
1196
2106
|
支持的 runtime: ${ADAPTERS.map((a) => a.id).join(', ')}
|
|
1197
2107
|
`);
|
|
1198
2108
|
}
|
|
1199
2109
|
async function main() {
|
|
1200
|
-
const { cmd, flags, bools } = parseArgs(process.argv.slice(2));
|
|
2110
|
+
const { cmd, positionals, flags, bools } = parseArgs(process.argv.slice(2));
|
|
1201
2111
|
switch (cmd) {
|
|
2112
|
+
case '--version':
|
|
2113
|
+
case '-v':
|
|
2114
|
+
case 'version':
|
|
2115
|
+
log(VERSION);
|
|
2116
|
+
return;
|
|
2117
|
+
case 'connect': return cmdConnect(flags, bools);
|
|
1202
2118
|
case 'install': return cmdInstall(flags, bools);
|
|
1203
2119
|
case 'onboard': return cmdOnboard(flags, bools);
|
|
1204
2120
|
case 'run': return cmdRun(flags);
|
|
1205
2121
|
case 'mcp': return cmdMcp(flags);
|
|
1206
2122
|
case 'doctor': return cmdDoctor(flags, bools);
|
|
2123
|
+
case 'status':
|
|
2124
|
+
cmdStatus(flags, bools);
|
|
2125
|
+
return;
|
|
2126
|
+
case 'service':
|
|
2127
|
+
cmdService(flags, positionals);
|
|
2128
|
+
return;
|
|
2129
|
+
case 'tools': return cmdTools(flags, bools);
|
|
2130
|
+
case 'mcp-probe': return cmdMcpProbe(flags, bools);
|
|
2131
|
+
case 'invoke': return cmdInvoke(flags, bools);
|
|
2132
|
+
case 'smoke': return cmdSmoke(flags, bools);
|
|
2133
|
+
case 'prompt':
|
|
2134
|
+
cmdPrompt(flags);
|
|
2135
|
+
return;
|
|
2136
|
+
case 'repair': return cmdRepair(flags);
|
|
2137
|
+
case 'disconnect': return cmdDisconnect(flags, bools);
|
|
2138
|
+
case 'uninstall': return cmdUninstall(flags, bools);
|
|
1207
2139
|
case 'detect': return cmdDetect();
|
|
1208
2140
|
case 'update': return cmdUpdate();
|
|
1209
2141
|
case 'whoami': return cmdWhoami(flags);
|
|
2142
|
+
case 'balance': return cmdBalance(flags);
|
|
1210
2143
|
case 'list':
|
|
1211
2144
|
cmdList();
|
|
1212
2145
|
return;
|