evolclaw 2.7.3 → 2.8.1
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/agents/claude-runner.js +91 -48
- package/dist/channels/aun-ops.js +275 -0
- package/dist/channels/aun.js +285 -125
- package/dist/cli.js +360 -1
- package/dist/config.js +1 -1
- package/dist/core/agent-registry.js +164 -0
- package/dist/core/command-handler.js +147 -97
- package/dist/core/evolagent-schema.js +72 -0
- package/dist/core/evolagent.js +66 -0
- package/dist/core/message/message-bridge.js +14 -2
- package/dist/core/message/message-processor.js +24 -10
- package/dist/core/message/thought-emitter.js +4 -2
- package/dist/core/session/session-manager.js +22 -3
- package/dist/index.js +8 -12
- package/dist/paths.js +2 -0
- package/dist/templates/prompts.md +1 -0
- package/dist/utils/init-channel.js +91 -221
- package/dist/utils/init.js +18 -42
- package/dist/utils/logger.js +58 -2
- package/evolclaw-install-aun.md +48 -7
- package/package.json +2 -2
|
@@ -204,7 +204,7 @@ export class AgentRunner {
|
|
|
204
204
|
// 没有交互上下文(无渠道适配器),回退到纯文本
|
|
205
205
|
const permCtx = this.permissionContexts.get(sessionId);
|
|
206
206
|
if (!permCtx?.adapter?.sendInteraction || !permCtx?.channelId) {
|
|
207
|
-
return this.handleAskUserQuestionFallback(input, questions);
|
|
207
|
+
return this.handleAskUserQuestionFallback(sessionId, input, questions);
|
|
208
208
|
}
|
|
209
209
|
const answers = {};
|
|
210
210
|
// 从 permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
|
|
@@ -307,20 +307,43 @@ export class AgentRunner {
|
|
|
307
307
|
return { behavior: 'allow', updatedInput, decisionClassification: 'user_temporary' };
|
|
308
308
|
}
|
|
309
309
|
/**
|
|
310
|
-
* AskUserQuestion 纯文本 fallback
|
|
310
|
+
* AskUserQuestion 纯文本 fallback:发送选项列表,等待用户通过 /ask 命令选择
|
|
311
|
+
* 注册到 interactionRouter,用户回复 /ask 1 或 /ask 自定义内容
|
|
311
312
|
*/
|
|
312
|
-
async handleAskUserQuestionFallback(input, questions) {
|
|
313
|
-
|
|
313
|
+
async handleAskUserQuestionFallback(sessionId, input, questions) {
|
|
314
|
+
const permCtx = this.permissionContexts.get(sessionId);
|
|
315
|
+
const sendPrompt = permCtx?.adapter && permCtx?.channelId
|
|
316
|
+
? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
|
|
317
|
+
: this.sendPromptFn;
|
|
314
318
|
const answers = {};
|
|
315
319
|
if (questions?.length) {
|
|
316
|
-
const
|
|
317
|
-
const firstLabel = q.options[0]?.label || '';
|
|
318
|
-
answers[q.question] = firstLabel;
|
|
320
|
+
for (const q of questions) {
|
|
319
321
|
const optText = q.options.map((o, i) => ` ${i + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
const prompt = `💬 ${q.question}\n${optText}\n\n回复 /ask <数字> 选择,或 /ask <自定义内容>`;
|
|
323
|
+
if (sendPrompt && permCtx?.interactionRouter) {
|
|
324
|
+
await sendPrompt(prompt);
|
|
325
|
+
const requestId = `ask-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
326
|
+
const answer = await new Promise((resolve) => {
|
|
327
|
+
permCtx.interactionRouter.register(requestId, sessionId, (action) => {
|
|
328
|
+
const num = parseInt(action.trim(), 10);
|
|
329
|
+
if (num >= 1 && num <= q.options.length) {
|
|
330
|
+
resolve(q.options[num - 1].label);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
resolve(action.trim());
|
|
334
|
+
}
|
|
335
|
+
}, { timeoutMs: 120_000, onTimeout: () => resolve(q.options[0]?.label || '') });
|
|
336
|
+
});
|
|
337
|
+
answers[q.question] = answer;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// 无交互能力,自动选第一项
|
|
341
|
+
const firstLabel = q.options[0]?.label || '';
|
|
342
|
+
answers[q.question] = firstLabel;
|
|
343
|
+
if (sendPrompt) {
|
|
344
|
+
await sendPrompt(`${prompt}\n → 自动选择:${firstLabel}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
324
347
|
}
|
|
325
348
|
}
|
|
326
349
|
const updatedInput = { ...input, answers };
|
|
@@ -331,51 +354,71 @@ export class AgentRunner {
|
|
|
331
354
|
*/
|
|
332
355
|
async handleExitPlanMode(sessionId, input, options) {
|
|
333
356
|
const permCtx = this.permissionContexts.get(sessionId);
|
|
334
|
-
// 从 permCtx 构造 per-session 的发送函数,避免全局 sendPromptFn 被其他 channel 实例覆盖
|
|
335
357
|
const sendPrompt = permCtx?.adapter && permCtx?.channelId
|
|
336
358
|
? async (text) => permCtx.adapter.sendText(permCtx.channelId, text, permCtx.replyContext)
|
|
337
359
|
: this.sendPromptFn;
|
|
338
|
-
//
|
|
339
|
-
if (!permCtx?.
|
|
360
|
+
// 无任何交互能力,直接 allow
|
|
361
|
+
if (!permCtx?.channelId || !sendPrompt) {
|
|
340
362
|
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
|
|
341
363
|
}
|
|
342
|
-
|
|
343
|
-
const interaction = {
|
|
344
|
-
type: 'interaction',
|
|
345
|
-
id: requestId,
|
|
346
|
-
kind: {
|
|
347
|
-
kind: 'action',
|
|
348
|
-
title: '📋 计划审批',
|
|
349
|
-
body: 'AI 已完成规划,等待审批。\n请查看以上计划内容后决定。',
|
|
350
|
-
buttons: [
|
|
351
|
-
{ key: 'approve', label: '✅ 批准执行', style: 'primary' },
|
|
352
|
-
{ key: 'reject', label: '❌ 拒绝', style: 'danger' },
|
|
353
|
-
],
|
|
354
|
-
},
|
|
355
|
-
channelId: permCtx.channelId,
|
|
356
|
-
sessionId,
|
|
357
|
-
};
|
|
364
|
+
// 尝试发送交互卡片
|
|
358
365
|
let cardSent = false;
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
if (permCtx.adapter?.sendInteraction) {
|
|
367
|
+
const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
368
|
+
const interaction = {
|
|
369
|
+
type: 'interaction',
|
|
370
|
+
id: requestId,
|
|
371
|
+
kind: {
|
|
372
|
+
kind: 'action',
|
|
373
|
+
title: '📋 计划审批',
|
|
374
|
+
body: 'AI 已完成规划,等待审批。\n请查看以上计划内容后决定。',
|
|
375
|
+
buttons: [
|
|
376
|
+
{ key: 'approve', label: '✅ 批准执行', style: 'primary' },
|
|
377
|
+
{ key: 'reject', label: '❌ 拒绝', style: 'danger' },
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
channelId: permCtx.channelId,
|
|
381
|
+
sessionId,
|
|
382
|
+
};
|
|
383
|
+
try {
|
|
384
|
+
const result = await permCtx.adapter.sendInteraction(permCtx.channelId, interaction, permCtx.replyContext);
|
|
385
|
+
cardSent = !!result;
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
|
|
389
|
+
}
|
|
390
|
+
if (cardSent) {
|
|
391
|
+
return new Promise((resolve) => {
|
|
392
|
+
permCtx.interactionRouter?.register(requestId, sessionId, (action) => {
|
|
393
|
+
if (action === 'approve') {
|
|
394
|
+
resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
368
402
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
403
|
+
// 文本 fallback:注册到 interactionRouter,等待用户 /ask 回复
|
|
404
|
+
if (permCtx.interactionRouter) {
|
|
405
|
+
await sendPrompt('📋 计划审批\nAI 已完成规划,等待审批。\n\n 1. 批准执行\n 2. 拒绝\n\n回复 /ask 1 批准,/ask 2 拒绝:');
|
|
406
|
+
const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
407
|
+
return new Promise((resolve) => {
|
|
408
|
+
permCtx.interactionRouter.register(requestId, sessionId, (action) => {
|
|
409
|
+
const trimmed = action.trim();
|
|
410
|
+
if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝') {
|
|
411
|
+
resolve({ behavior: 'deny', message: '用户拒绝了计划', decisionClassification: 'user_reject' });
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' });
|
|
415
|
+
}
|
|
416
|
+
}, { timeoutMs: 300_000, onTimeout: () => resolve({ behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' }) });
|
|
377
417
|
});
|
|
378
|
-
}
|
|
418
|
+
}
|
|
419
|
+
// 无交互能力,发提示后直接 allow
|
|
420
|
+
await sendPrompt('📋 计划审批\nAI 已完成规划,自动批准执行。');
|
|
421
|
+
return { behavior: 'allow', updatedInput: input, decisionClassification: 'user_temporary' };
|
|
379
422
|
}
|
|
380
423
|
/**
|
|
381
424
|
* SDK 原始事件 → 标准 AgentEvent 转换
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUN AID / agent.md 原子操作层
|
|
3
|
+
*
|
|
4
|
+
* 短生命周期操作(创建 AID、上传 agent.md 等),供 CLI / ctl / in-chat 三层共享。
|
|
5
|
+
* 与 aun.ts(长连接运行时)分离。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { execFileSync } from 'child_process';
|
|
12
|
+
import { isWindows } from '../utils/cross-platform.js';
|
|
13
|
+
import { resolvePaths } from '../paths.js';
|
|
14
|
+
// ==================== Constants ====================
|
|
15
|
+
export const MIN_AUN_CORE_SDK = [0, 2, 14];
|
|
16
|
+
export const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
|
|
17
|
+
// ==================== SDK & Environment ====================
|
|
18
|
+
function compareVersion(a, min) {
|
|
19
|
+
const parts = a.split('.').map(n => parseInt(n, 10));
|
|
20
|
+
if (parts.length < 3 || parts.some(isNaN))
|
|
21
|
+
return false;
|
|
22
|
+
if (parts[0] !== min[0])
|
|
23
|
+
return parts[0] > min[0];
|
|
24
|
+
if (parts[1] !== min[1])
|
|
25
|
+
return parts[1] > min[1];
|
|
26
|
+
return parts[2] >= min[2];
|
|
27
|
+
}
|
|
28
|
+
export function isAunSdkVersionOk(version) {
|
|
29
|
+
return compareVersion(version, MIN_AUN_CORE_SDK);
|
|
30
|
+
}
|
|
31
|
+
export function resolveAunCoreSdkPkg() {
|
|
32
|
+
try {
|
|
33
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
while (true) {
|
|
35
|
+
const candidate = path.join(dir, 'node_modules', AUN_CORE_SDK_PKG, 'package.json');
|
|
36
|
+
if (fs.existsSync(candidate)) {
|
|
37
|
+
const data = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
|
38
|
+
if (data.name === AUN_CORE_SDK_PKG)
|
|
39
|
+
return { version: data.version, path: candidate };
|
|
40
|
+
}
|
|
41
|
+
const parent = path.dirname(dir);
|
|
42
|
+
if (parent === dir)
|
|
43
|
+
break;
|
|
44
|
+
dir = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch { /* fall through */ }
|
|
48
|
+
try {
|
|
49
|
+
const npmCmd = isWindows ? 'npm.cmd' : 'npm';
|
|
50
|
+
const globalRoot = execFileSync(npmCmd, ['root', '-g'], {
|
|
51
|
+
encoding: 'utf-8', timeout: 10000, shell: isWindows,
|
|
52
|
+
}).trim();
|
|
53
|
+
const pkgPath = path.join(globalRoot, AUN_CORE_SDK_PKG, 'package.json');
|
|
54
|
+
if (fs.existsSync(pkgPath)) {
|
|
55
|
+
const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
56
|
+
return { version: data.version, path: pkgPath };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { /* not found */ }
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/** Non-interactive SDK check + auto-install */
|
|
63
|
+
export async function ensureAunSdk() {
|
|
64
|
+
const installed = resolveAunCoreSdkPkg();
|
|
65
|
+
if (installed && isAunSdkVersionOk(installed.version))
|
|
66
|
+
return;
|
|
67
|
+
const { npmInstallGlobal } = await import('../utils/init-channel.js');
|
|
68
|
+
console.log(`正在安装 ${AUN_CORE_SDK_PKG}@latest...`);
|
|
69
|
+
await npmInstallGlobal(`${AUN_CORE_SDK_PKG}@latest`);
|
|
70
|
+
}
|
|
71
|
+
/** SDK minimum version met? */
|
|
72
|
+
export function isAunSdkReady() {
|
|
73
|
+
const installed = resolveAunCoreSdkPkg();
|
|
74
|
+
return !!(installed && isAunSdkVersionOk(installed.version));
|
|
75
|
+
}
|
|
76
|
+
// ==================== CA Root ====================
|
|
77
|
+
export async function downloadCaRoot(aunPath, gatewayUrl, indent = '') {
|
|
78
|
+
const caDir = path.join(aunPath, 'CA', 'root');
|
|
79
|
+
const caCertPath = path.join(caDir, 'root.crt');
|
|
80
|
+
if (fs.existsSync(caCertPath))
|
|
81
|
+
return true;
|
|
82
|
+
if (!gatewayUrl)
|
|
83
|
+
return false;
|
|
84
|
+
try {
|
|
85
|
+
fs.mkdirSync(caDir, { recursive: true });
|
|
86
|
+
const gwHttp = gatewayUrl.replace(/^wss?:/, 'https:').replace(/\/aun$/, '');
|
|
87
|
+
const resp = await fetch(`${gwHttp}/pki/chain`, { redirect: 'follow' });
|
|
88
|
+
if (!resp.ok) {
|
|
89
|
+
console.warn(`${indent}⚠ CA 根证书下载失败: HTTP ${resp.status}`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const body = await resp.text();
|
|
93
|
+
if (!body.includes('BEGIN CERTIFICATE')) {
|
|
94
|
+
console.warn(`${indent}⚠ CA 根证书响应内容无效,跳过写入`);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
fs.writeFileSync(caCertPath, body);
|
|
98
|
+
console.log(`${indent}✓ CA 根证书已下载`);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
console.warn(`${indent}⚠ CA 根证书下载失败: ${e},可稍后手动下载`);
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ==================== Validation ====================
|
|
107
|
+
export function isValidAid(name) {
|
|
108
|
+
const labels = name.split('.');
|
|
109
|
+
return labels.length >= 3 && labels.every(l => /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(l));
|
|
110
|
+
}
|
|
111
|
+
// ==================== AUNClient Factory ====================
|
|
112
|
+
/**
|
|
113
|
+
* Get a short-lived AUNClient with CA cert loaded and identity ready.
|
|
114
|
+
* Caller is responsible for closing: `try { await client.close(); } catch {}`
|
|
115
|
+
*/
|
|
116
|
+
export async function getAunClient(aid, opts) {
|
|
117
|
+
const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
|
|
118
|
+
const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
|
|
119
|
+
const { AUNClient } = await import('@agentunion/fastaun');
|
|
120
|
+
const clientOpts = { aun_path: aunPath };
|
|
121
|
+
if (fs.existsSync(caCertPath))
|
|
122
|
+
clientOpts.root_ca_path = caCertPath;
|
|
123
|
+
const client = new AUNClient(clientOpts);
|
|
124
|
+
// Ensure identity is loaded (idempotent if already created)
|
|
125
|
+
await client.auth.createAid({ aid });
|
|
126
|
+
return client;
|
|
127
|
+
}
|
|
128
|
+
// ==================== AID Operations ====================
|
|
129
|
+
export function aidList(aunPath) {
|
|
130
|
+
const aidsDir = path.join(aunPath ?? path.join(os.homedir(), '.aun'), 'AIDs');
|
|
131
|
+
if (!fs.existsSync(aidsDir))
|
|
132
|
+
return [];
|
|
133
|
+
const entries = fs.readdirSync(aidsDir, { withFileTypes: true });
|
|
134
|
+
return entries
|
|
135
|
+
.filter(e => e.isDirectory())
|
|
136
|
+
.map(e => ({
|
|
137
|
+
aid: e.name,
|
|
138
|
+
hasPrivateKey: fs.existsSync(path.join(aidsDir, e.name, 'private')),
|
|
139
|
+
hasAgentMd: fs.existsSync(path.join(aidsDir, e.name, 'agent.md')),
|
|
140
|
+
}));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Create AID: keygen + register + CA download.
|
|
144
|
+
* Does NOT touch agent.md — call agentmdPut separately.
|
|
145
|
+
* Returns a reusable client (caller must close).
|
|
146
|
+
*/
|
|
147
|
+
export async function aidCreate(aid, opts) {
|
|
148
|
+
const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
|
|
149
|
+
const aidDir = path.join(aunPath, 'AIDs', aid);
|
|
150
|
+
// Already exists locally
|
|
151
|
+
if (fs.existsSync(aidDir) && fs.existsSync(path.join(aidDir, 'private'))) {
|
|
152
|
+
const client = await getAunClient(aid, { aunPath });
|
|
153
|
+
return { aid, alreadyExisted: true, gateway: '', client };
|
|
154
|
+
}
|
|
155
|
+
const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
|
|
156
|
+
let client = new AUNClient({ aun_path: aunPath });
|
|
157
|
+
try {
|
|
158
|
+
const result = await client.auth.createAid({ aid });
|
|
159
|
+
const gateway = result.gateway || '';
|
|
160
|
+
// Download CA root cert
|
|
161
|
+
const caDownloaded = await downloadCaRoot(aunPath, gateway);
|
|
162
|
+
// Rebuild client with CA cert for subsequent operations
|
|
163
|
+
const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
|
|
164
|
+
if (caDownloaded && fs.existsSync(caCertPath)) {
|
|
165
|
+
try {
|
|
166
|
+
await client.close();
|
|
167
|
+
}
|
|
168
|
+
catch { /* ignore */ }
|
|
169
|
+
client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath });
|
|
170
|
+
await client.auth.createAid({ aid });
|
|
171
|
+
}
|
|
172
|
+
// Set gateway URL for upload operations
|
|
173
|
+
let gatewayUrl = gateway;
|
|
174
|
+
if (!gatewayUrl) {
|
|
175
|
+
try {
|
|
176
|
+
const discovery = new GatewayDiscovery({});
|
|
177
|
+
gatewayUrl = await discovery.discover(`https://${aid}/.well-known/aun-gateway`);
|
|
178
|
+
}
|
|
179
|
+
catch { /* fall through */ }
|
|
180
|
+
}
|
|
181
|
+
if (gatewayUrl) {
|
|
182
|
+
client._gatewayUrl = gatewayUrl;
|
|
183
|
+
}
|
|
184
|
+
return { aid, alreadyExisted: false, gateway: gatewayUrl, client };
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
try {
|
|
188
|
+
await client.close();
|
|
189
|
+
}
|
|
190
|
+
catch { /* ignore */ }
|
|
191
|
+
throw e;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// ==================== AgentMd Operations ====================
|
|
195
|
+
export function buildInitialAgentMd(opts) {
|
|
196
|
+
const agentName = opts.aid.split('.')[0];
|
|
197
|
+
const agentType = opts.type || 'ai';
|
|
198
|
+
return `---\naid: "${opts.aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get agent.md content.
|
|
202
|
+
* For self (local AID with private key): reads local file, falls back to download.
|
|
203
|
+
* For others: downloads from AUN network.
|
|
204
|
+
*/
|
|
205
|
+
export async function agentmdGet(aid, opts) {
|
|
206
|
+
const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
|
|
207
|
+
const localPath = path.join(aunPath, 'AIDs', aid, 'agent.md');
|
|
208
|
+
// Try local first for self AIDs (has private key)
|
|
209
|
+
const hasPrivateKey = fs.existsSync(path.join(aunPath, 'AIDs', aid, 'private'));
|
|
210
|
+
if (hasPrivateKey && fs.existsSync(localPath)) {
|
|
211
|
+
return fs.readFileSync(localPath, 'utf-8');
|
|
212
|
+
}
|
|
213
|
+
// Download from network
|
|
214
|
+
const client = opts?.client ?? await getAunClient(aid, { aunPath });
|
|
215
|
+
const ownClient = !opts?.client;
|
|
216
|
+
try {
|
|
217
|
+
return await client.auth.downloadAgentMd(aid);
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
if (ownClient)
|
|
221
|
+
try {
|
|
222
|
+
await client.close();
|
|
223
|
+
}
|
|
224
|
+
catch { /* ignore */ }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Upload agent.md (sign + upload) and sync to local file.
|
|
229
|
+
* If client is provided, reuses it; otherwise creates a short-lived one.
|
|
230
|
+
*/
|
|
231
|
+
export async function agentmdPut(content, opts) {
|
|
232
|
+
const aunPath = opts.aunPath ?? path.join(os.homedir(), '.aun');
|
|
233
|
+
const client = opts.client ?? await getAunClient(opts.aid, { aunPath });
|
|
234
|
+
const ownClient = !opts.client;
|
|
235
|
+
try {
|
|
236
|
+
await client.auth.uploadAgentMd(content);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
if (ownClient)
|
|
240
|
+
try {
|
|
241
|
+
await client.close();
|
|
242
|
+
}
|
|
243
|
+
catch { /* ignore */ }
|
|
244
|
+
}
|
|
245
|
+
// Sync to local
|
|
246
|
+
const aidDir = path.join(aunPath, 'AIDs', opts.aid);
|
|
247
|
+
fs.mkdirSync(aidDir, { recursive: true });
|
|
248
|
+
fs.writeFileSync(path.join(aidDir, 'agent.md'), content, 'utf-8');
|
|
249
|
+
}
|
|
250
|
+
// ==================== Config ====================
|
|
251
|
+
/**
|
|
252
|
+
* Append a new AUN instance to the config's channels.aun array and save.
|
|
253
|
+
* Handles upgrade from single-object to array format.
|
|
254
|
+
*/
|
|
255
|
+
export function appendAunInstance(config, inst) {
|
|
256
|
+
if (!config.channels)
|
|
257
|
+
config.channels = {};
|
|
258
|
+
const newInst = {
|
|
259
|
+
name: inst.name,
|
|
260
|
+
enabled: inst.enabled ?? true,
|
|
261
|
+
aid: inst.aid,
|
|
262
|
+
...(inst.owner && { owner: inst.owner }),
|
|
263
|
+
};
|
|
264
|
+
if (Array.isArray(config.channels.aun)) {
|
|
265
|
+
config.channels.aun.push(newInst);
|
|
266
|
+
}
|
|
267
|
+
else if (config.channels.aun) {
|
|
268
|
+
const oldInst = { ...config.channels.aun, name: config.channels.aun.name || 'aun' };
|
|
269
|
+
config.channels.aun = [oldInst, newInst];
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
config.channels.aun = [newInst];
|
|
273
|
+
}
|
|
274
|
+
fs.writeFileSync(resolvePaths().config, JSON.stringify(config, null, 2) + '\n');
|
|
275
|
+
}
|