evolclaw 3.1.2 → 3.1.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.1.3 (2026-05-26)
4
+
5
+ ### New Features
6
+
7
+ - **Menu 协议拆分为 5 动词** — `menu.list` / `menu.query` / `menu.options` / `menu.update` / `menu.action`,response 统一 `error.code` 透传(`NO_ACTIVE_SESSION` / `MISSING_VALUE` / ...),`name → cmd` 由 bridge 内部映射,AUN 白名单接纳全部 `menu.*` 类型
8
+ - **xhigh effort 档位** — Claude/Codex 新增 `xhigh` 推理强度档位,Codex 通过 `codex debug models` 动态拉取模型目录与各模型支持的 effort 列表
9
+ - **结构化 menu options 字段** — session 子菜单返回 `preview`(首条消息预览)/ `lastActive` / `agentSessionId` / `turns` 扩展字段,所有可选项菜单返回 `selected` 标记当前值
10
+
11
+ ### Improvements
12
+
13
+ - **命令收敛** — 移除 `/plist` `/project` `/bind` `/agentmd` `/p` 别名;`/agent` 改为 EvolAgent 管理命令;baseagent 切换统一为 `/baseagent`(别名 `/base`);`/aid` `/rpc` `/storage` `/evolagent` 改为 ctl 专属
14
+ - **execMenu 拆分** — `command-handler` 拆出 `execMenuQuery` / `execMenuUpdate` / `execMenuAction` 三入口,无活跃会话时多项 fallback 到 evolagent config(`/pwd` / `/chatmode` / `/dispatch` 等)
15
+ - **EvolAgent 持久化方法** — 新增 `setActiveBaseagent` / `setChatmodePrivate` / `setDispatch`,配置变更直接落盘
16
+ - **Codex 实例每次重建** — 通过 env 注入 `EVOLCLAW_SESSION_ID`,仅首轮拼接 `systemPromptAppend`,避免 resume 时重复污染历史
17
+ - **Agent plugin 启用判定** — `isEnabled` 改为按 `baseagents.<name>` 配置启用,Codex 额外检测 `@openai/codex-sdk` 是否可用,`init` / `agent new` CLI 同步该判定
18
+ - **context-too-long 提示文案** — 按 agent 是否支持 compact 区分提示,retry 后仍超长时清理 renderer 中混入的错误文本(新增 `IMRenderer.stripContextError`)
19
+
20
+ ### Bug Fixes
21
+
22
+ - **Codex resume 历史污染** — systemPromptAppend 在 resume 轮次重复拼接到 prompt 头部,导致系统提示被反复写入对话历史
23
+
3
24
  ## v3.1.2 (2026-05-25)
4
25
 
5
26
  ### Improvements
package/README.md CHANGED
@@ -243,14 +243,11 @@ evolclaw/
243
243
 
244
244
  ### 管理员级命令(Admin+ 可用)
245
245
 
246
- **项目管理**:
246
+ **项目**:
247
247
  - `/pwd` - 显示当前项目路径
248
- - `/plist` - 列出所有项目(显示会话空闲时间)
249
- - `/p <name|path>` - 切换项目(保留会话历史)
250
- - `/bind <path>` - 绑定新项目目录
251
248
 
252
249
  **Agent 与模型**:
253
- - `/agent [name]` - 查看或切换 Agent 后端(claude / codex / gemini
250
+ - `/baseagent [name]` - 查看或切换 Agent 后端(claude / codex / gemini)(别名 `/base`)
254
251
  - `/model [model]` - 查看或切换模型
255
252
  - `/effort [level]` - 查看或切换推理强度(low / medium / high / max / auto)
256
253
  - `/perm [mode]` - 查看或切换权限模式(auto / edit / default / readonly)
@@ -272,7 +269,6 @@ evolclaw/
272
269
  - `/file <文件路径>` - 发送文件给用户
273
270
  - `/restart` - 重启服务(自愈机制)
274
271
  - `/repair` - 检查并修复会话
275
- - `/agentmd [put|set]` - 管理 AUN agent.md(仅 AUN 渠道)
276
272
 
277
273
  ## 技术栈
278
274
 
@@ -1094,7 +1094,7 @@ export class AgentRunner {
1094
1094
  export class ClaudeAgentPlugin {
1095
1095
  name = 'claude';
1096
1096
  isEnabled(agent) {
1097
- return agent.baseagent === 'claude';
1097
+ return !!agent.config.baseagents?.claude;
1098
1098
  }
1099
1099
  createAgent(agent, callbacks) {
1100
1100
  const override = agent.config.baseagents?.claude;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { resolveOpenaiConfig } from './resolve.js';
9
9
  import { logger } from '../utils/logger.js';
10
+ import { execFileSync } from 'child_process';
10
11
  import fs from 'fs';
11
12
  import path from 'path';
12
13
  import os from 'os';
@@ -17,13 +18,58 @@ const MIME_EXT = {
17
18
  'image/gif': '.gif',
18
19
  'image/webp': '.webp',
19
20
  };
20
- // ── Codex 模型列表 ──
21
- const CODEX_MODELS = ['gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5-codex', 'gpt-5.2', 'gpt-5.4'];
21
+ const CODEX_CATALOG_FALLBACK = [
22
+ { slug: 'gpt-5.5', efforts: ['low', 'medium', 'high', 'xhigh'] },
23
+ { slug: 'gpt-5.4', efforts: ['low', 'medium', 'high', 'xhigh'] },
24
+ { slug: 'gpt-5.4-mini', efforts: ['low', 'medium', 'high', 'xhigh'] },
25
+ { slug: 'gpt-5.3-codex', efforts: ['low', 'medium', 'high', 'xhigh'] },
26
+ { slug: 'gpt-5.2', efforts: ['low', 'medium', 'high', 'xhigh'] },
27
+ ];
28
+ let codexCatalogCache = null;
29
+ export function isCodexSdkAvailable() {
30
+ try {
31
+ import.meta.resolve('@openai/codex-sdk');
32
+ return true;
33
+ }
34
+ catch {
35
+ return false;
36
+ }
37
+ }
38
+ function fetchCodexCatalog() {
39
+ if (codexCatalogCache)
40
+ return codexCatalogCache;
41
+ try {
42
+ const output = execFileSync('codex', ['debug', 'models'], {
43
+ encoding: 'utf-8',
44
+ timeout: 5000,
45
+ stdio: ['pipe', 'pipe', 'pipe'],
46
+ });
47
+ const catalog = JSON.parse(output);
48
+ const models = catalog.models
49
+ .filter(m => m.visibility === 'list')
50
+ .map(m => ({
51
+ slug: m.slug,
52
+ efforts: (m.supported_reasoning_levels || []).map(l => l.effort),
53
+ }));
54
+ if (models.length > 0) {
55
+ codexCatalogCache = models;
56
+ return models;
57
+ }
58
+ }
59
+ catch (e) {
60
+ logger.debug(`[CodexRunner] Failed to fetch model catalog, using fallback: ${e}`);
61
+ }
62
+ return CODEX_CATALOG_FALLBACK;
63
+ }
64
+ export function getCodexEfforts(model) {
65
+ const catalog = fetchCodexCatalog();
66
+ const entry = catalog.find(m => m.slug === model);
67
+ return entry?.efforts ?? catalog[0]?.efforts ?? ['low', 'medium', 'high'];
68
+ }
22
69
  // ── Codex Runner ──
23
70
  export class CodexRunner {
24
71
  name = 'codex';
25
72
  capabilities = { clear: false, compact: false, fork: false };
26
- codex = null;
27
73
  codexModule = null;
28
74
  model;
29
75
  effort;
@@ -39,21 +85,25 @@ export class CodexRunner {
39
85
  this.effort = this.resolvedConfig.effort;
40
86
  this.onSessionIdUpdate = callbacks.onSessionIdUpdate;
41
87
  }
42
- async ensureCodex() {
43
- if (!this.codex || !this.codexModule) {
88
+ async ensureCodex(sessionId) {
89
+ if (!this.codexModule) {
44
90
  const { requireOptional } = await import('../utils/npm-ops.js');
45
91
  this.codexModule = await requireOptional('@openai/codex-sdk');
46
- this.codex = new this.codexModule.Codex({
47
- apiKey: this.resolvedConfig.apiKey,
48
- baseUrl: this.resolvedConfig.baseUrl,
49
- });
50
92
  }
51
- return { codex: this.codex, mod: this.codexModule };
93
+ const codex = new this.codexModule.Codex({
94
+ apiKey: this.resolvedConfig.apiKey,
95
+ baseUrl: this.resolvedConfig.baseUrl,
96
+ env: {
97
+ ...process.env,
98
+ EVOLCLAW_SESSION_ID: sessionId,
99
+ },
100
+ });
101
+ return { codex, mod: this.codexModule };
52
102
  }
53
103
  // ── ModelSwitcher ──
54
104
  setModel(model) { this.model = model; }
55
105
  getModel() { return this.model; }
56
- listModels() { return CODEX_MODELS; }
106
+ listModels() { return fetchCodexCatalog().map(m => m.slug); }
57
107
  // ── Effort ──
58
108
  setEffort(effort) { this.effort = effort; }
59
109
  getEffort() { return this.effort; }
@@ -94,10 +144,14 @@ export class CodexRunner {
94
144
  }
95
145
  // ── Core: runQuery ──
96
146
  async runQuery(sessionId, prompt, projectPath, initialAgentSessionId, images, systemPromptAppend, sessionManager) {
97
- // Agent ctl: 注入 EVOLCLAW_SESSION_ID 供子进程使用
98
- process.env.EVOLCLAW_SESSION_ID = sessionId;
99
- const { codex } = await this.ensureCodex();
147
+ const { codex } = await this.ensureCodex(sessionId);
100
148
  let agentSessionId = initialAgentSessionId || this.activeSessions.get(sessionId);
149
+ let fullPrompt = prompt;
150
+ // Only inject system context on the first turn; resumed Codex threads already
151
+ // have that context in history and repeating it will pollute the conversation.
152
+ if (systemPromptAppend && !agentSessionId) {
153
+ fullPrompt = prompt + '\n\n--- [SYSTEM_PROMPT_END] ---\n' + systemPromptAppend;
154
+ }
101
155
  const threadOptions = {
102
156
  workingDirectory: projectPath,
103
157
  model: this.model,
@@ -116,7 +170,7 @@ export class CodexRunner {
116
170
  let input;
117
171
  if (images?.length) {
118
172
  const tmpDir = os.tmpdir();
119
- const parts = [{ type: 'text', text: prompt }];
173
+ const parts = [{ type: 'text', text: fullPrompt }];
120
174
  for (let i = 0; i < images.length; i++) {
121
175
  const img = images[i];
122
176
  const ext = MIME_EXT[img.mimeType || ''] || '.jpg';
@@ -129,7 +183,7 @@ export class CodexRunner {
129
183
  logger.info(`[CodexRunner] Attached ${images.length} image(s) as local_image`);
130
184
  }
131
185
  else {
132
- input = prompt;
186
+ input = fullPrompt;
133
187
  }
134
188
  const { events } = await thread.runStreamed(input, { signal: controller.signal });
135
189
  // 包装为 AgentEvent 流
@@ -302,10 +356,10 @@ export class CodexRunner {
302
356
  export class CodexAgentPlugin {
303
357
  name = 'codex';
304
358
  isEnabled(agent) {
305
- if (agent.baseagent !== 'codex')
306
- return false;
307
359
  if (!agent.config.baseagents?.codex)
308
360
  return false;
361
+ if (!isCodexSdkAvailable())
362
+ return false;
309
363
  try {
310
364
  const override = agent.config.baseagents.codex;
311
365
  const syntheticConfig = { agents: { codex: override } };
@@ -317,8 +371,10 @@ export class CodexAgentPlugin {
317
371
  }
318
372
  }
319
373
  createAgent(agent, callbacks) {
374
+ if (!isCodexSdkAvailable()) {
375
+ throw new Error('Missing optional dependency @openai/codex-sdk');
376
+ }
320
377
  const override = agent.config.baseagents?.codex;
321
- const syntheticConfig = { agents: { codex: override } };
322
378
  const merged = {
323
379
  agents: { codex: { ...(override || {}) } },
324
380
  };
@@ -407,8 +407,6 @@ export class GeminiRunner {
407
407
  export class GeminiAgentPlugin {
408
408
  name = 'gemini';
409
409
  isEnabled(agent) {
410
- if (agent.baseagent !== 'gemini')
411
- return false;
412
410
  const geminiCfg = agent.config.baseagents?.gemini;
413
411
  if (!geminiCfg)
414
412
  return false;
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { kitsDir, eckDebugDir, resolveRoot } from '../paths.js';
3
+ import { kitsDir, eckDebugDir, resolveRoot, getPackageRoot } from '../paths.js';
4
4
  import { logger } from '../utils/logger.js';
5
5
  // ── Param descriptions (for debug output) ──
6
6
  const PARAM_DESCRIPTIONS = {
@@ -22,13 +22,48 @@ const PARAM_DESCRIPTIONS = {
22
22
  venueUid: 'venue 唯一标识',
23
23
  project: '当前项目目录名(由 CURRENT_PROJECT 派生)',
24
24
  sessionName: '会话名称',
25
- sessionMode: '会话模式',
25
+ chatmode: '会话模式(interactive/proactive)',
26
26
  readonly: '是否只读模式',
27
27
  canSendFile: '当前渠道是否支持发文件',
28
28
  capabilities: '渠道能力列表',
29
29
  baseAgent: '当前 base agent 规范值(claude/codex/gemini/hermes)',
30
30
  baseAgentName: '当前 base agent 显示名',
31
31
  };
32
+ function buildPathMappings(vars) {
33
+ const pkgRoot = getPackageRoot();
34
+ const evolHome = String(vars['EVOLCLAW_HOME'] || resolveRoot());
35
+ const selfAid = vars['selfAid'] ? String(vars['selfAid']) : '';
36
+ const currentProject = vars['CURRENT_PROJECT'] ? String(vars['CURRENT_PROJECT']) : '';
37
+ const mappings = [
38
+ { prefix: path.join(pkgRoot, 'kits', 'rules'), alias: '$KITS_RULES' },
39
+ { prefix: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'), alias: '$KITS_FRAGMENTS' },
40
+ { prefix: path.join(pkgRoot, 'kits', 'templates'), alias: '$KITS_TEMPLATES' },
41
+ { prefix: path.join(pkgRoot, 'kits', 'docs'), alias: '$KITS_DOCS' },
42
+ { prefix: path.join(pkgRoot, 'kits'), alias: '$KITS' },
43
+ { prefix: pkgRoot, alias: '$PACKAGE_ROOT' },
44
+ ];
45
+ if (selfAid) {
46
+ mappings.push({ prefix: path.join(evolHome, 'agents', selfAid), alias: '$AGENT_DIR' });
47
+ }
48
+ mappings.push({ prefix: evolHome, alias: '$EVOLCLAW_HOME' });
49
+ if (currentProject) {
50
+ mappings.push({ prefix: currentProject, alias: '$CURRENT_PROJECT' });
51
+ }
52
+ // Sort by prefix length descending so longer (more specific) paths match first
53
+ mappings.sort((a, b) => b.prefix.length - a.prefix.length);
54
+ return mappings;
55
+ }
56
+ function shortenPath(filePath, mappings) {
57
+ const normalized = filePath.replace(/\\/g, '/');
58
+ for (const { prefix, alias } of mappings) {
59
+ const normalizedPrefix = prefix.replace(/\\/g, '/');
60
+ if (normalized.startsWith(normalizedPrefix)) {
61
+ const rest = normalized.slice(normalizedPrefix.length);
62
+ return alias + rest;
63
+ }
64
+ }
65
+ return filePath;
66
+ }
32
67
  // ── Cache ──
33
68
  let _manifestCache = null;
34
69
  const _sessionPathCache = new Map();
@@ -49,6 +84,8 @@ export function renderKitSections(ctx) {
49
84
  loadKitManifest();
50
85
  const sections = _manifestCache;
51
86
  const fileParts = [];
87
+ const fragmentParts = [];
88
+ const pathMappings = buildPathMappings(ctx.vars);
52
89
  for (const section of sections) {
53
90
  if (section.enabled === false)
54
91
  continue;
@@ -62,14 +99,20 @@ export function renderKitSections(ctx) {
62
99
  if (!content.trim())
63
100
  continue;
64
101
  const label = section.description ? `${section.id} — ${section.description}` : section.id;
65
- fileParts.push(`Contenu de ${filePath} (${label}):\n\n${content.trimEnd()}`);
102
+ const displayPath = shortenPath(filePath, pathMappings);
103
+ const part = `Contenu de ${displayPath} (${label}):\n\n${content.trimEnd()}`;
104
+ fileParts.push(part);
105
+ if (section.needsInjection) {
106
+ fragmentParts.push(part);
107
+ }
66
108
  }
67
109
  }
68
110
  if (fileParts.length === 0)
69
111
  return '';
70
112
  const body = fileParts.join('\n\n');
71
113
  const output = `<system-reminder>\nEvolClaw Context Kit documents are shown below.\n\n${body}\n\nIMPORTANT: Use this context when it affects the current interaction.\n</system-reminder>`;
72
- writeDebugFiles(ctx, output);
114
+ const fragmentsOutput = fragmentParts.length > 0 ? fragmentParts.join('\n\n') : '';
115
+ writeDebugFiles(ctx, output, fragmentsOutput);
73
116
  return output;
74
117
  }
75
118
  export function cleanEckDebug() {
@@ -234,11 +277,14 @@ function isTruthy(val) {
234
277
  // CHUNK_CONTINUE_6
235
278
  // ── Template rendering ──
236
279
  function renderTemplate(template, vars) {
237
- // Pass 1: conditional sections {{?key=value}}...{{/}} and {{?key}}...{{/}}
238
- let result = template.replace(/\{\{\?(\w+)(?:=([^}]*))?\}\}([\s\S]*?)\{\{\/\}\}/g, (_match, key, value, body) => {
239
- if (value !== undefined) {
240
- return String(vars[key]) === value ? body : '';
241
- }
280
+ // Pass 1: conditional sections {{?key=value}}, {{?key!=value}}, {{?key}}...{{/}}
281
+ let result = template.replace(/\{\{\?(\w+)(!=|=)([^}]*)?\}\}([\s\S]*?)\{\{\/\}\}/g, (_match, key, op, value, body) => {
282
+ if (op === '!=')
283
+ return String(vars[key]) !== value ? body : '';
284
+ return String(vars[key]) === value ? body : '';
285
+ });
286
+ // Pass 1b: truthy-only {{?key}}...{{/}}
287
+ result = result.replace(/\{\{\?(\w+)\}\}([\s\S]*?)\{\{\/\}\}/g, (_match, key, body) => {
242
288
  return isTruthy(vars[key]) ? body : '';
243
289
  });
244
290
  // Pass 2: variable substitution {{key}}
@@ -261,7 +307,7 @@ function getSessionCache(sessionId) {
261
307
  return cache;
262
308
  }
263
309
  // ── Debug output ──
264
- function writeDebugFiles(ctx, output) {
310
+ function writeDebugFiles(ctx, output, fragmentsOutput) {
265
311
  const now = new Date();
266
312
  const ts = now.toISOString().replace(/[T:.]/g, '-').slice(0, 19);
267
313
  const dir = eckDebugDir();
@@ -278,4 +324,7 @@ function writeDebugFiles(ctx, output) {
278
324
  };
279
325
  fs.writeFile(path.join(dir, `vars-${ts}.json`), JSON.stringify(varsData, null, 2), () => { });
280
326
  fs.writeFile(path.join(dir, `context-${ts}.md`), output, () => { });
327
+ if (fragmentsOutput) {
328
+ fs.writeFile(path.join(dir, `fragments-${ts}.md`), fragmentsOutput, () => { });
329
+ }
281
330
  }
@@ -1,8 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import { getAunClient } from './client.js';
5
- import { agentMdPath, aidLocalDir } from '../../paths.js';
4
+ import { agentMdPath, aidLocalDir, resolveRoot } from '../../paths.js';
6
5
  export function buildInitialAgentMd(opts) {
7
6
  const agentName = opts.aid.split('.')[0];
8
7
  const agentType = opts.type || 'ai';
@@ -83,15 +82,16 @@ async function verifyContent(content, aid, certPem, client) {
83
82
  * Create a bare AUNClient (no createAid) for read-only operations.
84
83
  */
85
84
  async function createBareClient(aunPath) {
85
+ const p = aunPath ?? resolveRoot();
86
86
  const { AUNClient } = await import('@agentunion/fastaun');
87
- const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
88
- const clientOpts = { aun_path: aunPath, debug: false };
87
+ const caCertPath = path.join(p, 'CA', 'root', 'root.crt');
88
+ const clientOpts = { aun_path: p, debug: false };
89
89
  if (fs.existsSync(caCertPath))
90
90
  clientOpts.root_ca_path = caCertPath;
91
91
  return new AUNClient(clientOpts);
92
92
  }
93
93
  export async function agentmdGet(aid, opts) {
94
- const aunPath = opts?.aunPath ?? path.join(os.homedir(), '.aun');
94
+ const aunPath = opts?.aunPath ?? resolveRoot();
95
95
  const localPath = agentMdPath(aid);
96
96
  // === Path A: local agent.md exists ===
97
97
  if (fs.existsSync(localPath)) {
@@ -109,7 +109,8 @@ export async function agentmdGet(aid, opts) {
109
109
  }
110
110
  // Fallback: local invalid → try remote
111
111
  try {
112
- const remote = await client.auth.downloadAgentMd(aid);
112
+ const info = await client.fetchAgentMd(aid);
113
+ const remote = info.content;
113
114
  if (remote) {
114
115
  const remoteVerification = await verifyContent(remote, aid, certPem, client);
115
116
  if (remoteVerification.status === 'verified') {
@@ -133,20 +134,13 @@ export async function agentmdGet(aid, opts) {
133
134
  const client = opts?.client ?? await createBareClient(aunPath);
134
135
  const ownClient = !opts?.client;
135
136
  try {
136
- const raw = await client.auth.downloadAgentMd(aid);
137
+ const info = await client.fetchAgentMd(aid);
138
+ const raw = info.content;
137
139
  if (!opts?.withVerification) {
138
- // Persist without verification
139
- const aidDir = aidLocalDir(aid);
140
- fs.mkdirSync(aidDir, { recursive: true });
141
- fs.writeFileSync(path.join(aidDir, 'agent.md'), raw, 'utf-8');
142
140
  return raw;
143
141
  }
144
142
  const certPem = await obtainCertPem(aid, aunPath, client);
145
143
  const verification = await verifyContent(raw, aid, certPem, client);
146
- // Persist to local
147
- const aidDir = aidLocalDir(aid);
148
- fs.mkdirSync(aidDir, { recursive: true });
149
- fs.writeFileSync(path.join(aidDir, 'agent.md'), raw, 'utf-8');
150
144
  return { content: raw, verification };
151
145
  }
152
146
  finally {
@@ -158,24 +152,52 @@ export async function agentmdGet(aid, opts) {
158
152
  }
159
153
  }
160
154
  /**
161
- * Upload agent.md: auto-sign + upload + sync to local file.
155
+ * Upload agent.md: write to local file → publishAgentMd (auto-sign + upload).
162
156
  */
163
157
  export async function agentmdPut(content, opts) {
164
- const aunPath = opts.aunPath ?? path.join(os.homedir(), '.aun');
158
+ const aunPath = opts.aunPath ?? resolveRoot();
165
159
  const client = opts.client ?? await getAunClient(opts.aid, { aunPath });
166
160
  const ownClient = !opts.client;
161
+ const dir = aidLocalDir(opts.aid);
162
+ const filePath = path.join(dir, 'agent.md');
163
+ const existed = fs.existsSync(filePath);
167
164
  try {
168
- let signed;
169
- try {
170
- signed = await client.auth.signAgentMd(content);
171
- }
172
- catch {
173
- signed = content;
174
- }
175
- await client.auth.uploadAgentMd(signed);
176
- const dir = aidLocalDir(opts.aid);
177
165
  fs.mkdirSync(dir, { recursive: true });
178
- fs.writeFileSync(path.join(dir, 'agent.md'), signed, 'utf-8');
166
+ fs.writeFileSync(filePath, content, 'utf-8');
167
+ await client.publishAgentMd();
168
+ }
169
+ catch (e) {
170
+ if (!existed)
171
+ try {
172
+ fs.unlinkSync(filePath);
173
+ }
174
+ catch { /* ignore */ }
175
+ throw e;
176
+ }
177
+ finally {
178
+ if (ownClient)
179
+ try {
180
+ await client.close();
181
+ }
182
+ catch { /* ignore */ }
183
+ }
184
+ }
185
+ /**
186
+ * Check if agent.md is up-to-date (30-day cache), fetch if changed.
187
+ * Returns changed=true + content when a new version was downloaded.
188
+ */
189
+ export async function agentmdSync(aid, opts) {
190
+ const client = opts?.client ?? await createBareClient();
191
+ const ownClient = !opts?.client;
192
+ try {
193
+ const state = await client.checkAgentMd(aid, 30);
194
+ if (!state.in_sync || !state.local_found) {
195
+ const info = await client.fetchAgentMd(aid);
196
+ return { changed: true, content: info.content };
197
+ }
198
+ const localPath = agentMdPath(aid);
199
+ const content = fs.existsSync(localPath) ? fs.readFileSync(localPath, 'utf-8') : undefined;
200
+ return { changed: false, content };
179
201
  }
180
202
  finally {
181
203
  if (ownClient)
@@ -1,3 +1,3 @@
1
1
  export { isValidAid, aidList, aidCreate, aidShow, aidDelete, aidLookup, appendAidLifecycle, readAidLifecycle } from './identity.js';
2
- export { buildInitialAgentMd, agentmdGet, agentmdPut } from './agentmd.js';
2
+ export { buildInitialAgentMd, agentmdGet, agentmdPut, agentmdSync } from './agentmd.js';
3
3
  export { MIN_AUN_CORE_SDK, AUN_CORE_SDK_PKG, isAunSdkVersionOk, resolveAunCoreSdkPkg, ensureAunSdk, isAunSdkReady, downloadCaRoot, getAunClient, suppressSdkLogs, } from './client.js';
@@ -6,9 +6,12 @@ export async function createShortConnection(aid, opts) {
6
6
  const slotId = opts?.slotId ?? '';
7
7
  const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
8
8
  const { AUNClient } = await import('@agentunion/fastaun');
9
+ const encryptionSeed = process.env.AUN_ENCRYPTION_SEED || undefined;
9
10
  const clientOpts = { aun_path: aunPath, debug: false };
10
11
  if (fs.existsSync(caCertPath))
11
12
  clientOpts.root_ca_path = caCertPath;
13
+ if (encryptionSeed)
14
+ clientOpts.encryption_seed = encryptionSeed;
12
15
  const client = new AUNClient(clientOpts);
13
16
  await client.auth.createAid({ aid });
14
17
  const authResult = await client.auth.authenticate({ aid });