evolclaw 2.8.3 → 3.1.0
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/README.md +21 -12
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +108 -46
- package/dist/agents/codex-runner.js +13 -14
- package/dist/agents/gemini-runner.js +15 -17
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/agents/resolve.js +134 -0
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +159 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +293 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +147 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1340 -349
- package/dist/channels/dingtalk.js +59 -5
- package/dist/channels/feishu.js +381 -32
- package/dist/channels/qqbot.js +68 -12
- package/dist/channels/wechat.js +63 -4
- package/dist/channels/wecom.js +59 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +4513 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +589 -0
- package/dist/config-store.js +645 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +176 -12
- package/dist/core/command-handler.js +883 -848
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +202 -238
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +486 -0
- package/dist/core/message/items-formatter.js +68 -0
- package/dist/core/message/message-bridge.js +109 -56
- package/dist/core/message/message-log.js +93 -0
- package/dist/core/message/message-processor.js +430 -212
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/permission.js +116 -11
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +740 -777
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +560 -283
- package/dist/ipc.js +49 -0
- package/dist/net-check.js +640 -0
- package/dist/paths.js +73 -9
- package/dist/types.js +8 -2
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +89 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +17 -26
- package/dist/utils/error-utils.js +10 -2
- package/dist/utils/instance-registry.js +434 -0
- package/dist/utils/log-writer.js +217 -0
- package/dist/utils/logger.js +34 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/npm-ops.js +163 -0
- package/dist/utils/process-introspect.js +122 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +544 -0
- package/evolclaw-install-aun.md +127 -47
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +25 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
- package/kits/docs/evolclaw/self-summary.md +29 -0
- package/kits/docs/evolclaw/tools.md +25 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +43 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +10 -6
- package/data/evolclaw.sample.json +0 -60
- package/dist/agents/templates.js +0 -122
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/prompts.md +0 -104
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
- package/dist/utils/upgrade.js +0 -100
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
import { resolvePaths, ensureDataDirs } from '../paths.js';
|
|
5
|
+
import { commandExists } from '../utils/cross-platform.js';
|
|
6
|
+
import { scanInstances } from '../utils/instance-registry.js';
|
|
7
|
+
// ==================== Helpers ====================
|
|
8
|
+
function ask(rl, question) {
|
|
9
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
10
|
+
}
|
|
11
|
+
const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
|
|
12
|
+
const BASEAGENT_ENV_KEY = {
|
|
13
|
+
claude: 'ANTHROPIC_API_KEY',
|
|
14
|
+
codex: 'OPENAI_API_KEY',
|
|
15
|
+
gemini: 'GEMINI_API_KEY',
|
|
16
|
+
};
|
|
17
|
+
function detectAvailable() {
|
|
18
|
+
return BASEAGENT_CANDIDATES.filter(b => commandExists(b));
|
|
19
|
+
}
|
|
20
|
+
function pickDefault(available) {
|
|
21
|
+
return (available.includes('claude') ? 'claude' : available[0]);
|
|
22
|
+
}
|
|
23
|
+
function buildDefaults(chosen) {
|
|
24
|
+
const env = BASEAGENT_ENV_KEY[chosen];
|
|
25
|
+
return {
|
|
26
|
+
$schema_version: 1,
|
|
27
|
+
active_baseagent: chosen,
|
|
28
|
+
baseagents: { [chosen]: env ? { apiKey: `$ENV:${env}` } : {} },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function writeDefaults(defaultsPath, chosen) {
|
|
32
|
+
fs.mkdirSync(path.dirname(defaultsPath), { recursive: true });
|
|
33
|
+
fs.writeFileSync(defaultsPath, JSON.stringify(buildDefaults(chosen), null, 2) + '\n');
|
|
34
|
+
}
|
|
35
|
+
// ==================== Main ====================
|
|
36
|
+
export async function cmdInit(options) {
|
|
37
|
+
const p = resolvePaths();
|
|
38
|
+
ensureDataDirs();
|
|
39
|
+
// ── 1. 单进程互斥 ──
|
|
40
|
+
const aliveMains = scanInstances().mains.filter(m => m.alive);
|
|
41
|
+
if (aliveMains.length > 0) {
|
|
42
|
+
const pids = aliveMains.map(m => m.record.pid).join(', ');
|
|
43
|
+
console.log(`❌ EvolClaw 正在运行 (PID: ${pids}),请先执行 evolclaw stop`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// ── 2. 探测 baseagent ──
|
|
47
|
+
const available = detectAvailable();
|
|
48
|
+
if (available.length === 0) {
|
|
49
|
+
console.log('❌ 未检测到任何 baseagent CLI,请先安装至少一款:');
|
|
50
|
+
for (const b of BASEAGENT_CANDIDATES)
|
|
51
|
+
console.log(` - ${b}`);
|
|
52
|
+
console.log('\n安装后重新运行 evolclaw init');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
console.log('检测到可用的 baseagent:');
|
|
56
|
+
for (const b of available)
|
|
57
|
+
console.log(` ● ${b}`);
|
|
58
|
+
console.log('');
|
|
59
|
+
const defaultsPath = p.defaultsConfig;
|
|
60
|
+
const exists = fs.existsSync(defaultsPath);
|
|
61
|
+
// ── 3. 非交互式分支 ──
|
|
62
|
+
if (options?.nonInteractive) {
|
|
63
|
+
if (exists && !options.force) {
|
|
64
|
+
console.log(`❌ 配置已存在: ${defaultsPath}(加 --force 可覆盖)`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let chosen;
|
|
68
|
+
if (options.baseagent) {
|
|
69
|
+
if (!BASEAGENT_CANDIDATES.includes(options.baseagent)) {
|
|
70
|
+
console.log(`❌ 无效 baseagent: ${options.baseagent}(可选: ${BASEAGENT_CANDIDATES.join('/')})`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!available.includes(options.baseagent)) {
|
|
74
|
+
console.log(`❌ ${options.baseagent} 未在 PATH 中检测到(可用: ${available.join('/')})`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
chosen = options.baseagent;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
chosen = pickDefault(available);
|
|
81
|
+
}
|
|
82
|
+
writeDefaults(defaultsPath, chosen);
|
|
83
|
+
console.log(`✓ 已${exists ? '覆盖' : '创建'}: ${defaultsPath}`);
|
|
84
|
+
console.log(` active_baseagent: ${chosen}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// ── 4. 交互式分支 ──
|
|
88
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
89
|
+
try {
|
|
90
|
+
if (exists) {
|
|
91
|
+
const ans = (await ask(rl, `配置文件已存在: ${defaultsPath}\n 是否覆盖?[y/N] `)).trim().toLowerCase();
|
|
92
|
+
if (ans !== 'y' && ans !== 'yes') {
|
|
93
|
+
console.log(' 已取消');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const defaultBa = pickDefault(available);
|
|
98
|
+
let chosen = null;
|
|
99
|
+
while (chosen === null) {
|
|
100
|
+
const input = (await ask(rl, `默认 baseagent (${available.join('/')}) [${defaultBa}]: `)).trim() || defaultBa;
|
|
101
|
+
if (!BASEAGENT_CANDIDATES.includes(input)) {
|
|
102
|
+
console.log(` 无效选择,可选: ${BASEAGENT_CANDIDATES.join('/')}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!available.includes(input)) {
|
|
106
|
+
console.log(` ${input} 未在 PATH 中检测到(可用: ${available.join('/')})`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
chosen = input;
|
|
110
|
+
}
|
|
111
|
+
writeDefaults(defaultsPath, chosen);
|
|
112
|
+
console.log(`\n✓ 已${exists ? '覆盖' : '创建'}: ${defaultsPath}`);
|
|
113
|
+
console.log(` active_baseagent: ${chosen}\n`);
|
|
114
|
+
rl.close();
|
|
115
|
+
// ── 5. 嵌套 agent new ──
|
|
116
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
117
|
+
console.log('下一步:创建 agent\n');
|
|
118
|
+
const { agentCreateInteractive } = await import('./agent.js');
|
|
119
|
+
const result = await agentCreateInteractive();
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
console.error(`❌ ${result.error}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
try {
|
|
126
|
+
rl.close();
|
|
127
|
+
}
|
|
128
|
+
catch { /* ignore */ }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Present instance selection menu when existing instances are found.
|
|
133
|
+
* Returns the user's choice, or null if cancelled.
|
|
134
|
+
*/
|
|
135
|
+
export async function selectInstance(rl, channelType, instances) {
|
|
136
|
+
const typeLabel = channelType === 'feishu' ? '飞书' : channelType === 'wechat' ? '微信' : channelType === 'dingtalk' ? '钉钉' : channelType === 'qqbot' ? 'QQ机器人' : channelType === 'wecom' ? '企业微信' : channelType.toUpperCase();
|
|
137
|
+
console.log(`\n发现已有 ${typeLabel} 配置:`);
|
|
138
|
+
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
|
139
|
+
for (let i = 0; i < instances.length; i++) {
|
|
140
|
+
const inst = instances[i];
|
|
141
|
+
const id = inst.aid || inst.appId || inst.botId || inst.clientId || inst.token?.slice(0, 16) || '';
|
|
142
|
+
const suffix = id ? ` (${id})` : '';
|
|
143
|
+
console.log(` ${letters[i]}. ${inst.name}${suffix}`);
|
|
144
|
+
}
|
|
145
|
+
const addLetter = letters[instances.length];
|
|
146
|
+
console.log(` ${addLetter}. 添加新配置`);
|
|
147
|
+
console.log('');
|
|
148
|
+
const validOptions = letters.slice(0, instances.length + 1).split('');
|
|
149
|
+
let choice = '';
|
|
150
|
+
while (!validOptions.includes(choice)) {
|
|
151
|
+
choice = (await new Promise(r => rl.question('请选择: ', r))).trim().toLowerCase();
|
|
152
|
+
if (!validOptions.includes(choice)) {
|
|
153
|
+
console.log(`无效选择,请输入 ${validOptions.join('/')}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const choiceIndex = letters.indexOf(choice);
|
|
157
|
+
if (choiceIndex === instances.length) {
|
|
158
|
+
let name = '';
|
|
159
|
+
while (!name) {
|
|
160
|
+
name = (await new Promise(r => rl.question('请输入新配置名称: ', r))).trim();
|
|
161
|
+
if (!name)
|
|
162
|
+
console.log(' 名称不能为空');
|
|
163
|
+
if (instances.some(i => i.name === name)) {
|
|
164
|
+
console.log(` 名称 "${name}" 已存在,请换一个`);
|
|
165
|
+
name = '';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { action: 'add', name };
|
|
169
|
+
}
|
|
170
|
+
const target = instances[choiceIndex];
|
|
171
|
+
console.log(`\n已选择:${target.name}`);
|
|
172
|
+
const confirm = (await new Promise(r => rl.question(`⚠️ 即将覆盖该配置,确认?(y/N) `, r))).trim().toLowerCase();
|
|
173
|
+
if (confirm !== 'y' && confirm !== 'yes') {
|
|
174
|
+
console.log('已取消');
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return { action: 'overwrite', index: choiceIndex, name: target.name };
|
|
178
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { kitsRulesDir, resolvePaths } from '../paths.js';
|
|
4
|
+
import { atomicWriteJson, atomicReadJson } from '../utils/atomic-write.js';
|
|
5
|
+
const isWindows = process.platform === 'win32';
|
|
6
|
+
const KNOWN_BASEAGENTS = ['cc', 'codex', 'gemini'];
|
|
7
|
+
function statePath() {
|
|
8
|
+
return path.join(resolvePaths().eckDir, 'link-state.json');
|
|
9
|
+
}
|
|
10
|
+
function loadState() {
|
|
11
|
+
return atomicReadJson(statePath()) ?? {};
|
|
12
|
+
}
|
|
13
|
+
function saveState(state) {
|
|
14
|
+
atomicWriteJson(statePath(), state);
|
|
15
|
+
}
|
|
16
|
+
function getEntry(state, ba) {
|
|
17
|
+
return state[ba] ?? { current: null, history: [] };
|
|
18
|
+
}
|
|
19
|
+
function pushHistory(entry, dir) {
|
|
20
|
+
entry.history = [dir, ...entry.history.filter(h => h !== dir)].slice(0, 5);
|
|
21
|
+
}
|
|
22
|
+
// ── symlink 目标路径(baseagent 决定放哪)──
|
|
23
|
+
function resolveTarget(ba, dir) {
|
|
24
|
+
switch (ba) {
|
|
25
|
+
case 'cc':
|
|
26
|
+
return path.join(dir, '.claude', 'rules', 'eck');
|
|
27
|
+
case 'codex':
|
|
28
|
+
return path.join(dir, '.codex', 'rules', 'eck');
|
|
29
|
+
case 'gemini':
|
|
30
|
+
return path.join(dir, '.gemini', 'rules', 'eck');
|
|
31
|
+
default:
|
|
32
|
+
return path.join(dir, '.claude', 'rules', 'eck');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ── 创建 symlink/junction ──
|
|
36
|
+
function createLink(source, target) {
|
|
37
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
38
|
+
if (isWindows) {
|
|
39
|
+
fs.symlinkSync(source, target, 'junction');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
fs.symlinkSync(source, target, 'dir');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// ── 删除 symlink/junction + 逐级清理空目录 ──
|
|
46
|
+
function removeLink(target) {
|
|
47
|
+
if (!fs.existsSync(target))
|
|
48
|
+
return false;
|
|
49
|
+
const stat = fs.lstatSync(target);
|
|
50
|
+
if (stat.isSymbolicLink()) {
|
|
51
|
+
fs.rmSync(target, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
else if (isWindows && stat.isDirectory()) {
|
|
54
|
+
const real = fs.realpathSync(target);
|
|
55
|
+
const resolved = path.resolve(target);
|
|
56
|
+
if (real.toLowerCase() === resolved.toLowerCase()) {
|
|
57
|
+
return false; // regular directory, not a junction
|
|
58
|
+
}
|
|
59
|
+
fs.rmSync(target, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
cleanEmptyParents(path.dirname(target));
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
function cleanEmptyParents(dir) {
|
|
68
|
+
try {
|
|
69
|
+
const entries = fs.readdirSync(dir);
|
|
70
|
+
if (entries.length > 0)
|
|
71
|
+
return;
|
|
72
|
+
fs.rmdirSync(dir);
|
|
73
|
+
cleanEmptyParents(path.dirname(dir));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// stop on error (permission, root, etc.)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ── 子命令 ──
|
|
80
|
+
function showHelp() {
|
|
81
|
+
console.log(`Usage: evolclaw link-rules <subcommand> [baseagent] [--dir <path>]
|
|
82
|
+
|
|
83
|
+
Subcommands:
|
|
84
|
+
connect Connect ECK rules to a project directory (default: cwd)
|
|
85
|
+
disconnect Remove ECK rules connection for a baseagent
|
|
86
|
+
status Show connection state for all baseagents
|
|
87
|
+
|
|
88
|
+
Arguments:
|
|
89
|
+
baseagent Target base agent: ${KNOWN_BASEAGENTS.join(', ')} (default: cc)
|
|
90
|
+
--dir <path> Target directory (default: current working directory)
|
|
91
|
+
|
|
92
|
+
Supported baseagents:
|
|
93
|
+
cc Claude Code (.claude/rules/eck/)
|
|
94
|
+
codex Codex (.codex/rules/eck/)
|
|
95
|
+
gemini Gemini CLI (.gemini/rules/eck/)
|
|
96
|
+
|
|
97
|
+
Examples:
|
|
98
|
+
evolclaw link-rules connect # connect cc in cwd
|
|
99
|
+
evolclaw link-rules connect codex # connect codex in cwd
|
|
100
|
+
evolclaw link-rules connect cc --dir /x # connect cc in specific dir
|
|
101
|
+
evolclaw link-rules disconnect cc # disconnect cc
|
|
102
|
+
evolclaw link-rules status # show all connections`);
|
|
103
|
+
}
|
|
104
|
+
function showStatus() {
|
|
105
|
+
const state = loadState();
|
|
106
|
+
const source = kitsRulesDir();
|
|
107
|
+
console.log(`ECK rules source: ${source}\n`);
|
|
108
|
+
for (const ba of KNOWN_BASEAGENTS) {
|
|
109
|
+
const entry = getEntry(state, ba);
|
|
110
|
+
const status = entry.current ? '● connected' : '○ disconnected';
|
|
111
|
+
console.log(`[${ba}] ${status}`);
|
|
112
|
+
if (entry.current) {
|
|
113
|
+
const target = resolveTarget(ba, entry.current);
|
|
114
|
+
console.log(` path: ${entry.current}`);
|
|
115
|
+
console.log(` link: ${target}`);
|
|
116
|
+
console.log(` rules: ${source}`);
|
|
117
|
+
}
|
|
118
|
+
if (entry.history.length > 0) {
|
|
119
|
+
console.log(` history:`);
|
|
120
|
+
for (const h of entry.history) {
|
|
121
|
+
console.log(` - ${h}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log('');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function connect(ba, dir) {
|
|
128
|
+
const source = kitsRulesDir();
|
|
129
|
+
if (!fs.existsSync(source)) {
|
|
130
|
+
console.error(`❌ kits/rules/ not found at: ${source}`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
const absDir = path.resolve(dir);
|
|
134
|
+
if (!fs.existsSync(absDir)) {
|
|
135
|
+
console.error(`❌ Directory does not exist: ${absDir}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
const state = loadState();
|
|
139
|
+
const entry = getEntry(state, ba);
|
|
140
|
+
// disconnect old link if exists (different dir)
|
|
141
|
+
if (entry.current && entry.current !== absDir) {
|
|
142
|
+
const oldTarget = resolveTarget(ba, entry.current);
|
|
143
|
+
removeLink(oldTarget);
|
|
144
|
+
console.log(` disconnected old: ${entry.current}`);
|
|
145
|
+
}
|
|
146
|
+
// create new link
|
|
147
|
+
const target = resolveTarget(ba, absDir);
|
|
148
|
+
if (fs.existsSync(target)) {
|
|
149
|
+
// already linked here — check if it points to our source
|
|
150
|
+
try {
|
|
151
|
+
const real = fs.realpathSync(target);
|
|
152
|
+
const sourceReal = fs.realpathSync(source);
|
|
153
|
+
if (pathEquals(real, sourceReal)) {
|
|
154
|
+
console.log(`✓ Already connected: [${ba}] → ${absDir}`);
|
|
155
|
+
entry.current = absDir;
|
|
156
|
+
pushHistory(entry, absDir);
|
|
157
|
+
state[ba] = entry;
|
|
158
|
+
saveState(state);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch { /* fall through */ }
|
|
163
|
+
console.error(`❌ Target already exists and points elsewhere: ${target}`);
|
|
164
|
+
console.error(` Run 'evolclaw link-rules disconnect ${ba}' first, or remove manually.`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
createLink(source, target);
|
|
168
|
+
entry.current = absDir;
|
|
169
|
+
pushHistory(entry, absDir);
|
|
170
|
+
state[ba] = entry;
|
|
171
|
+
saveState(state);
|
|
172
|
+
console.log(`✓ Connected: [${ba}] ${absDir}`);
|
|
173
|
+
console.log(` ${target} → ${source}`);
|
|
174
|
+
}
|
|
175
|
+
function disconnect(ba) {
|
|
176
|
+
const state = loadState();
|
|
177
|
+
const entry = getEntry(state, ba);
|
|
178
|
+
if (!entry.current) {
|
|
179
|
+
console.log(`[${ba}] not connected.`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const target = resolveTarget(ba, entry.current);
|
|
183
|
+
const removed = removeLink(target);
|
|
184
|
+
if (removed) {
|
|
185
|
+
console.log(`✓ Disconnected: [${ba}] ${entry.current}`);
|
|
186
|
+
}
|
|
187
|
+
else if (!fs.existsSync(target)) {
|
|
188
|
+
console.log(`✓ Disconnected: [${ba}] (link was already gone)`);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
console.error(`❌ Could not remove: ${target} (not a symlink/junction)`);
|
|
192
|
+
}
|
|
193
|
+
entry.current = null;
|
|
194
|
+
state[ba] = entry;
|
|
195
|
+
saveState(state);
|
|
196
|
+
}
|
|
197
|
+
// ── 入口 ──
|
|
198
|
+
export function cmdLinkRules(args) {
|
|
199
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
200
|
+
showHelp();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const sub = args[0];
|
|
204
|
+
if (sub === 'status') {
|
|
205
|
+
showStatus();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (sub === 'disconnect') {
|
|
209
|
+
const ba = resolveBaseAgent(args[1]);
|
|
210
|
+
disconnect(ba);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (sub === 'connect') {
|
|
214
|
+
const ba = resolveBaseAgent(args[1] && !args[1].startsWith('-') ? args[1] : undefined);
|
|
215
|
+
const dir = getArgValue(args, '--dir') || process.cwd();
|
|
216
|
+
connect(ba, dir);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// no subcommand or unknown → show help
|
|
220
|
+
showHelp();
|
|
221
|
+
}
|
|
222
|
+
function resolveBaseAgent(input) {
|
|
223
|
+
if (!input || input === 'claude-code')
|
|
224
|
+
return 'cc';
|
|
225
|
+
if (KNOWN_BASEAGENTS.includes(input))
|
|
226
|
+
return input;
|
|
227
|
+
// allow full names
|
|
228
|
+
if (input === 'claude' || input === 'claude-code')
|
|
229
|
+
return 'cc';
|
|
230
|
+
console.error(`❌ Unknown baseagent: ${input}`);
|
|
231
|
+
console.error(` Supported: ${KNOWN_BASEAGENTS.join(', ')}`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
function getArgValue(args, flag) {
|
|
235
|
+
const idx = args.indexOf(flag);
|
|
236
|
+
if (idx === -1 || idx + 1 >= args.length)
|
|
237
|
+
return undefined;
|
|
238
|
+
return args[idx + 1];
|
|
239
|
+
}
|
|
240
|
+
function pathEquals(a, b) {
|
|
241
|
+
if (isWindows) {
|
|
242
|
+
return path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
|
|
243
|
+
}
|
|
244
|
+
return path.resolve(a) === path.resolve(b);
|
|
245
|
+
}
|