evolclaw 2.8.0 → 2.8.2

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.
@@ -0,0 +1,122 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getPackageRoot, resolveRoot } from '../paths.js';
4
+ import { logger } from '../utils/logger.js';
5
+ const KNOWN_SECTIONS = new Set(['runtime', 'group', 'proactive']);
6
+ const SECTION_RE = /^##\s+(\w+)\s*$/;
7
+ let sections = null;
8
+ let builtinSections = null;
9
+ function parseTemplate(content) {
10
+ const result = new Map();
11
+ let currentSection = null;
12
+ let currentLines = [];
13
+ for (const line of content.split('\n')) {
14
+ // Stop parsing at horizontal rule separator (documentation follows)
15
+ if (/^---\s*$/.test(line)) {
16
+ if (currentSection) {
17
+ result.set(currentSection, currentLines.join('\n').trim());
18
+ }
19
+ break;
20
+ }
21
+ const m = line.match(SECTION_RE);
22
+ if (m) {
23
+ if (currentSection) {
24
+ result.set(currentSection, currentLines.join('\n').trim());
25
+ }
26
+ const name = m[1];
27
+ if (KNOWN_SECTIONS.has(name)) {
28
+ currentSection = name;
29
+ currentLines = [];
30
+ }
31
+ else {
32
+ currentSection = null;
33
+ currentLines = [];
34
+ }
35
+ }
36
+ else if (currentSection) {
37
+ currentLines.push(line);
38
+ }
39
+ }
40
+ if (currentSection) {
41
+ result.set(currentSection, currentLines.join('\n').trim());
42
+ }
43
+ return result;
44
+ }
45
+ function loadBuiltinTemplate() {
46
+ const builtinPath = path.join(getPackageRoot(), 'dist', 'templates', 'prompts.md');
47
+ const srcPath = path.join(getPackageRoot(), 'src', 'templates', 'prompts.md');
48
+ const filePath = fs.existsSync(builtinPath) ? builtinPath : srcPath;
49
+ const content = fs.readFileSync(filePath, 'utf-8');
50
+ return parseTemplate(content);
51
+ }
52
+ export function loadPromptTemplates() {
53
+ builtinSections = loadBuiltinTemplate();
54
+ const userPath = path.join(resolveRoot(), 'data', 'prompts.md');
55
+ if (fs.existsSync(userPath)) {
56
+ try {
57
+ const content = fs.readFileSync(userPath, 'utf-8');
58
+ const parsed = parseTemplate(content);
59
+ sections = new Map(builtinSections);
60
+ for (const [key, value] of parsed) {
61
+ sections.set(key, value);
62
+ }
63
+ logger.info(`[PromptTemplates] Loaded user override: ${userPath}`);
64
+ }
65
+ catch (err) {
66
+ logger.warn(`[PromptTemplates] Failed to load user override (${userPath}), using builtin:`, err);
67
+ sections = builtinSections;
68
+ }
69
+ }
70
+ else {
71
+ sections = builtinSections;
72
+ logger.info(`[PromptTemplates] Using builtin templates`);
73
+ }
74
+ for (const name of KNOWN_SECTIONS) {
75
+ if (!sections.has(name)) {
76
+ logger.warn(`[PromptTemplates] Section "${name}" missing, using builtin fallback`);
77
+ const fallback = builtinSections.get(name);
78
+ if (fallback)
79
+ sections.set(name, fallback);
80
+ }
81
+ }
82
+ }
83
+ function isTruthy(val) {
84
+ if (val === undefined || val === null || val === false || val === '' || val === 0)
85
+ return false;
86
+ return true;
87
+ }
88
+ function renderTemplate(template, vars) {
89
+ // Pass 1: conditional sections {{?key}}...{{/}}
90
+ let result = template.replace(/\{\{\?(\w+)\}\}([\s\S]*?)\{\{\/\}\}/g, (_match, key, body) => {
91
+ return isTruthy(vars[key]) ? body : '';
92
+ });
93
+ // Pass 2: variable substitution {{key}}
94
+ result = result.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
95
+ const val = vars[key];
96
+ if (!isTruthy(val))
97
+ return '';
98
+ return String(val);
99
+ });
100
+ // Pass 3: remove blank lines
101
+ return result.split('\n').filter(line => line.trim() !== '').join('\n');
102
+ }
103
+ export function renderPromptSection(section, vars) {
104
+ if (!sections)
105
+ loadPromptTemplates();
106
+ const template = sections.get(section);
107
+ if (!template) {
108
+ logger.warn(`[PromptTemplates] Section "${section}" not found`);
109
+ return '';
110
+ }
111
+ return renderTemplate(template, vars);
112
+ }
113
+ /** Reset loaded templates (for testing) */
114
+ export function _resetTemplates() {
115
+ sections = null;
116
+ builtinSections = null;
117
+ }
118
+ /** Load templates from a raw string (for testing) */
119
+ export function _loadFromString(content) {
120
+ builtinSections = parseTemplate(content);
121
+ sections = builtinSections;
122
+ }
@@ -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, 17];
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
+ }