evolclaw 3.0.0 → 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.
Files changed (99) hide show
  1. package/README.md +1 -1
  2. package/bin/ec.js +29 -0
  3. package/dist/agents/baseagent-normalize.js +19 -0
  4. package/dist/agents/claude-runner.js +7 -9
  5. package/dist/agents/codex-runner.js +2 -0
  6. package/dist/agents/gemini-runner.js +9 -9
  7. package/dist/agents/kit-renderer.js +281 -0
  8. package/dist/aun/aid/identity.js +28 -0
  9. package/dist/aun/aid/index.js +1 -1
  10. package/dist/aun/aid/lifecycle-log.js +33 -0
  11. package/dist/aun/msg/group.js +3 -1
  12. package/dist/aun/msg/p2p.js +4 -1
  13. package/dist/channels/aun.js +353 -125
  14. package/dist/channels/dingtalk.js +2 -1
  15. package/dist/channels/feishu.js +118 -5
  16. package/dist/channels/qqbot.js +2 -1
  17. package/dist/channels/wechat.js +3 -1
  18. package/dist/channels/wecom.js +2 -1
  19. package/dist/cli/bench.js +1219 -0
  20. package/dist/cli/index.js +279 -19
  21. package/dist/cli/link-rules.js +245 -0
  22. package/dist/cli/net-check.js +640 -0
  23. package/dist/cli/watch-msg.js +589 -0
  24. package/dist/config-store.js +37 -5
  25. package/dist/core/channel-loader.js +23 -10
  26. package/dist/core/command-handler.js +46 -22
  27. package/dist/core/evolagent.js +5 -10
  28. package/dist/core/message/im-renderer.js +50 -44
  29. package/dist/core/message/items-formatter.js +11 -4
  30. package/dist/core/message/message-bridge.js +7 -2
  31. package/dist/core/message/message-log.js +2 -0
  32. package/dist/core/message/message-processor.js +150 -99
  33. package/dist/core/message/message-queue.js +10 -3
  34. package/dist/core/permission.js +95 -3
  35. package/dist/core/session/session-manager.js +98 -64
  36. package/dist/core/trigger/scheduler.js +1 -1
  37. package/dist/data/error-dict.json +118 -0
  38. package/dist/eck/baseagent-caps.js +18 -0
  39. package/dist/eck/detect.js +47 -0
  40. package/dist/eck/init.js +77 -0
  41. package/dist/eck/rules-loader.js +28 -0
  42. package/dist/index.js +137 -16
  43. package/dist/net-check.js +640 -0
  44. package/dist/paths.js +31 -40
  45. package/dist/utils/aid-lifecycle-log.js +33 -0
  46. package/dist/utils/atomic-write.js +10 -0
  47. package/dist/utils/cross-platform.js +17 -8
  48. package/dist/utils/error-utils.js +10 -2
  49. package/dist/utils/instance-registry.js +6 -5
  50. package/dist/utils/log-writer.js +2 -1
  51. package/dist/utils/logger.js +10 -0
  52. package/dist/utils/npm-ops.js +35 -3
  53. package/dist/utils/process-introspect.js +16 -38
  54. package/dist/watch-msg.js +26 -11
  55. package/evolclaw-install-aun.md +14 -2
  56. package/kits/docs/GUIDE.md +20 -0
  57. package/kits/docs/INDEX.md +52 -0
  58. package/kits/docs/aun/CHEATSHEET.md +17 -0
  59. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  60. package/kits/docs/channels/feishu.md +27 -0
  61. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  62. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  63. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  64. package/kits/docs/eck_templates/runtime.template.md +19 -0
  65. package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
  66. package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
  67. package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
  68. package/kits/docs/identity/PATH_OPS.md +16 -0
  69. package/kits/docs/identity/ROLE_DETAIL.md +20 -0
  70. package/kits/docs/path-registry.md +43 -0
  71. package/kits/eck_manifest.json +95 -0
  72. package/kits/rules/01-overview.md +120 -0
  73. package/kits/rules/02-navigation.md +75 -0
  74. package/kits/rules/03-identity.md +34 -0
  75. package/kits/rules/04-relation.md +49 -0
  76. package/kits/rules/05-venue.md +45 -0
  77. package/kits/rules/06-channel.md +43 -0
  78. package/kits/templates/system-fragments/baseagent.md +2 -0
  79. package/kits/templates/system-fragments/channel.md +10 -0
  80. package/kits/templates/system-fragments/identity.md +12 -0
  81. package/kits/templates/system-fragments/relation.md +9 -0
  82. package/kits/templates/system-fragments/runtime.md +19 -0
  83. package/kits/templates/system-fragments/venue.md +5 -0
  84. package/package.json +7 -5
  85. package/dist/agents/templates.js +0 -122
  86. package/dist/data/prompts.md +0 -137
  87. package/kits/aun/meta.md +0 -25
  88. package/kits/aun/role.md +0 -25
  89. package/kits/templates/group.md +0 -20
  90. package/kits/templates/private.md +0 -9
  91. package/kits/templates/system-fragments/personal-context.md +0 -3
  92. package/kits/templates/system-fragments/self-intro.md +0 -5
  93. package/kits/templates/system-fragments/speaker-intro.md +0 -5
  94. package/kits/templates/system-fragments/venue-intro.md +0 -5
  95. /package/kits/{channels → docs/channels}/aun.md +0 -0
  96. /package/kits/{evolclaw/commands.md → docs/evolclaw/AGENT_CMD.md} +0 -0
  97. /package/kits/{evolclaw → docs/evolclaw}/self-summary.md +0 -0
  98. /package/kits/{evolclaw → docs/evolclaw}/tools.md +0 -0
  99. /package/kits/{evolclaw → docs/identity}/identity-tools.md +0 -0
@@ -31,7 +31,10 @@ export class SessionManager {
31
31
  setSessionModeResolver(resolver) {
32
32
  this.sessionModeResolver = resolver;
33
33
  }
34
- resolveDefaultSessionMode(channel, chatType) {
34
+ resolveDefaultSessionMode(channel, chatType, peerType) {
35
+ // 非 human 对端(ai/bot)强制 proactive,无视 agent 的默认 chatmode 配置
36
+ if (peerType && peerType !== 'human' && peerType !== 'unknown')
37
+ return 'proactive';
35
38
  const ct = chatType || 'private';
36
39
  const resolved = this.sessionModeResolver?.(channel, ct);
37
40
  return resolved || 'interactive';
@@ -176,12 +179,75 @@ export class SessionManager {
176
179
  const metaPath = this.metaFilePath(targetDir, session.id);
177
180
  appendJsonl(metaPath, file);
178
181
  }
182
+ /**
183
+ * 比较两个 SessionFile 是否在内容上相等(忽略 updatedAt / updatedAtStr)。
184
+ * 用于跳过"没真变化"的写入,避免 jsonl 写放大。
185
+ */
186
+ sessionFilesEqual(a, b) {
187
+ const stripVolatile = ({ updatedAt, updatedAtStr, ...rest }) => rest;
188
+ return JSON.stringify(stripVolatile(a)) === JSON.stringify(stripVolatile(b));
189
+ }
190
+ /**
191
+ * Append meta + write active.json,但只在 session 内容(除 updatedAt 外)真正变化时才写。
192
+ * prev 是修改前的快照(用于 diff),next 是修改后的 session。
193
+ * 返回是否发生了写入。
194
+ */
195
+ writeSessionIfChanged(channel, channelId, prev, next) {
196
+ if (prev) {
197
+ const prevFile = sessionToFile(prev);
198
+ const nextFile = sessionToFile(next);
199
+ if (this.sessionFilesEqual(prevFile, nextFile))
200
+ return false;
201
+ }
202
+ next.updatedAt = Date.now();
203
+ this.appendMeta(channel, channelId, next);
204
+ const active = this.readActive(channel, channelId);
205
+ if (active && active.id === next.id) {
206
+ // 保留 active.json 中已有的 activeTask(markProcessing 写入的处理状态)
207
+ if (active.processingState && !next.processingState) {
208
+ next.processingState = active.processingState;
209
+ }
210
+ this.writeActive(channel, channelId, next);
211
+ }
212
+ return true;
213
+ }
179
214
  readMetaLatest(metaFilePath) {
180
215
  const file = readLastJsonlLine(metaFilePath);
181
216
  if (!file)
182
217
  return undefined;
183
218
  return fileToSession(file);
184
219
  }
220
+ /**
221
+ * 为 by-sessionId 改方法加载"当前 session 状态"。
222
+ *
223
+ * 设计契约(docs/refactor/01-db-to-fs.md):
224
+ * active.json 是热路径权威源。.jsonl 是历史档案。
225
+ *
226
+ * 读取策略:
227
+ * 1. 先按 sessionId 定位 .jsonl 文件(确认 session 存在 + 拿到 channel/channelId)
228
+ * 2. 优先读 active.json(如果 active.id === sessionId)—— 当前状态
229
+ * 3. 否则 fallback 到 .jsonl 末行 —— 非活跃 session 的更新(如多 session 并存时改非 active 那个)
230
+ *
231
+ * 返回 { current, prev }:
232
+ * - current 用于 caller 修改后写回
233
+ * - prev 是 current 的初始快照(用于 writeSessionIfChanged 的 diff 检查)
234
+ */
235
+ loadSessionForUpdate(sessionId) {
236
+ const found = this.findSessionFileById(sessionId);
237
+ if (!found)
238
+ return undefined;
239
+ // 先读 .jsonl 末行拿 channel/channelId(active.json 文件路径需要这两个)
240
+ const fromJsonl = this.readMetaLatest(found.metaPath);
241
+ if (!fromJsonl)
242
+ return undefined;
243
+ // 优先用 active.json 的当前状态(如果它就是这个 sessionId)
244
+ const active = this.readActive(fromJsonl.channel, fromJsonl.channelId);
245
+ const base = (active && active.id === sessionId) ? active : fromJsonl;
246
+ // 深拷贝避免 caller 改 current 时污染 prev
247
+ const current = JSON.parse(JSON.stringify(base));
248
+ const prev = JSON.parse(JSON.stringify(base));
249
+ return { current, prev };
250
+ }
185
251
  validateSessionFile(session) {
186
252
  const agentSessionId = session.agentSessionId;
187
253
  if (!agentSessionId)
@@ -193,12 +259,9 @@ export class SessionManager {
193
259
  if (adapter.checkExists(session.projectPath, agentSessionId))
194
260
  return agentSessionId;
195
261
  logger.warn(`Session file not found for ${agentId}: ${agentSessionId}, clearing session ID`);
262
+ const prev = JSON.parse(JSON.stringify(session));
196
263
  session.agentSessionId = undefined;
197
- this.appendMeta(session.channel, session.channelId, session);
198
- const active = this.readActive(session.channel, session.channelId);
199
- if (active && active.id === session.id) {
200
- this.writeActive(session.channel, session.channelId, session);
201
- }
264
+ this.writeSessionIfChanged(session.channel, session.channelId, prev, session);
202
265
  return undefined;
203
266
  }
204
267
  getActiveChatType(channel, channelId) {
@@ -332,9 +395,9 @@ export class SessionManager {
332
395
  return result;
333
396
  }
334
397
  // ─── Session lifecycle ───
335
- async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, agentId, selfId, channelType) {
398
+ async getOrCreateSession(channel, channelId, defaultProjectPath, threadId, metadata, name, userId, chatType, agentId, selfId, channelType, peerType) {
336
399
  if (threadId) {
337
- const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType);
400
+ const session = this.getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType, peerType);
338
401
  session.identity = this.resolveIdentity(channel, userId);
339
402
  if (session.metadata && !session.metadata.permissionMode) {
340
403
  session.metadata.permissionMode = DEFAULT_PERMISSION_MODE;
@@ -399,6 +462,7 @@ export class SessionManager {
399
462
  .sort((a, b) => b.updatedAt - a.updatedAt)[0];
400
463
  if (existing) {
401
464
  const validSessionId = this.validateSessionFile(existing);
465
+ const prev = JSON.parse(JSON.stringify({ ...existing, agentSessionId: validSessionId }));
402
466
  const session = { ...existing, agentSessionId: validSessionId };
403
467
  session.identity = this.resolveIdentity(channel, userId);
404
468
  if (!session.metadata)
@@ -416,9 +480,7 @@ export class SessionManager {
416
480
  if (chatType === 'private' && metadata?.peerName && !session.metadata.peerName) {
417
481
  session.metadata.peerName = metadata.peerName;
418
482
  }
419
- session.updatedAt = Date.now();
420
- this.appendMeta(channel, channelId, session);
421
- this.writeActive(channel, channelId, session);
483
+ this.writeSessionIfChanged(channel, channelId, prev, session);
422
484
  return session;
423
485
  }
424
486
  // Create new session
@@ -435,7 +497,7 @@ export class SessionManager {
435
497
  threadId: '',
436
498
  agentId: agentId || 'claude',
437
499
  chatType: chatType || 'private',
438
- sessionMode: this.resolveDefaultSessionMode(channel, chatType || 'private'),
500
+ sessionMode: this.resolveDefaultSessionMode(channel, chatType || 'private', peerType),
439
501
  metadata: sessionMetadata,
440
502
  name: name || '默认会话',
441
503
  createdAt: Date.now(),
@@ -457,12 +519,10 @@ export class SessionManager {
457
519
  return session;
458
520
  }
459
521
  async updateSession(sessionId, updates) {
460
- const found = this.findSessionFileById(sessionId);
461
- if (!found)
462
- return;
463
- const current = this.readMetaLatest(found.metaPath);
464
- if (!current)
522
+ const loaded = this.loadSessionForUpdate(sessionId);
523
+ if (!loaded)
465
524
  return;
525
+ const { current, prev } = loaded;
466
526
  if (updates.chatType !== undefined)
467
527
  current.chatType = updates.chatType;
468
528
  if (updates.name !== undefined)
@@ -473,14 +533,9 @@ export class SessionManager {
473
533
  current.metadata = updates.metadata;
474
534
  if ('agentSessionId' in updates)
475
535
  current.agentSessionId = updates.agentSessionId ?? undefined;
476
- current.updatedAt = Date.now();
477
- this.appendMeta(current.channel, current.channelId, current);
478
- const active = this.readActive(current.channel, current.channelId);
479
- if (active && active.id === sessionId) {
480
- this.writeActive(current.channel, current.channelId, current);
481
- }
536
+ this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
482
537
  }
483
- getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType) {
538
+ getOrCreateThreadSession(channel, channelId, threadId, defaultProjectPath, metadata, name, agentId, selfId, channelType, peerType) {
484
539
  const chatDir = this.ensureResolvedChatDir(channel, channelId);
485
540
  const threadIndex = readThreadIndex(chatDir);
486
541
  const existingMetaId = threadIndex[threadId];
@@ -511,7 +566,7 @@ export class SessionManager {
511
566
  threadId,
512
567
  agentId: agentId || 'claude',
513
568
  chatType: inheritedChatType,
514
- sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType),
569
+ sessionMode: this.resolveDefaultSessionMode(channel, inheritedChatType, peerType),
515
570
  metadata,
516
571
  name: name || '话题会话',
517
572
  createdAt: Date.now(),
@@ -581,25 +636,19 @@ export class SessionManager {
581
636
  const active = this.readActive(channel, channelId);
582
637
  if (!active)
583
638
  return;
639
+ const prev = JSON.parse(JSON.stringify(active));
584
640
  active.agentSessionId = agentSessionId;
585
- active.updatedAt = Date.now();
586
- this.appendMeta(channel, channelId, active);
587
- this.writeActive(channel, channelId, active);
641
+ this.writeSessionIfChanged(channel, channelId, prev, active);
588
642
  }
589
643
  async updateAgentSessionIdBySessionId(sessionId, agentSessionId) {
590
- logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
591
- const found = this.findSessionFileById(sessionId);
592
- if (!found)
593
- return;
594
- const current = this.readMetaLatest(found.metaPath);
595
- if (!current)
644
+ const loaded = this.loadSessionForUpdate(sessionId);
645
+ if (!loaded)
596
646
  return;
647
+ const { current, prev } = loaded;
597
648
  current.agentSessionId = agentSessionId;
598
- current.updatedAt = Date.now();
599
- this.appendMeta(current.channel, current.channelId, current);
600
- const active = this.readActive(current.channel, current.channelId);
601
- if (active && active.id === sessionId) {
602
- this.writeActive(current.channel, current.channelId, current);
649
+ const wrote = this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
650
+ if (wrote) {
651
+ logger.info(`[SessionManager] Updating agent_session_id: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
603
652
  }
604
653
  }
605
654
  async switchAgent(channel, channelId, projectPath, newAgentId) {
@@ -650,10 +699,9 @@ export class SessionManager {
650
699
  const active = this.readActive(channel, channelId);
651
700
  if (!active)
652
701
  return;
702
+ const prev = JSON.parse(JSON.stringify(active));
653
703
  active.agentSessionId = undefined;
654
- active.updatedAt = Date.now();
655
- this.appendMeta(channel, channelId, active);
656
- this.writeActive(channel, channelId, active);
704
+ this.writeSessionIfChanged(channel, channelId, prev, active);
657
705
  }
658
706
  getOwnerChatId(targetChannel, ownerPeerId) {
659
707
  const chatDirs = scanChatDirs(this.sessionsDir);
@@ -742,34 +790,20 @@ export class SessionManager {
742
790
  return target;
743
791
  }
744
792
  updateMetadata(sessionId, metadata) {
745
- const found = this.findSessionFileById(sessionId);
746
- if (!found)
747
- return;
748
- const current = this.readMetaLatest(found.metaPath);
749
- if (!current)
793
+ const loaded = this.loadSessionForUpdate(sessionId);
794
+ if (!loaded)
750
795
  return;
796
+ const { current, prev } = loaded;
751
797
  current.metadata = metadata;
752
- current.updatedAt = Date.now();
753
- this.appendMeta(current.channel, current.channelId, current);
754
- const active = this.readActive(current.channel, current.channelId);
755
- if (active && active.id === sessionId) {
756
- this.writeActive(current.channel, current.channelId, current);
757
- }
798
+ this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
758
799
  }
759
800
  async renameSession(sessionId, newName) {
760
- const found = this.findSessionFileById(sessionId);
761
- if (!found)
762
- return false;
763
- const current = this.readMetaLatest(found.metaPath);
764
- if (!current)
801
+ const loaded = this.loadSessionForUpdate(sessionId);
802
+ if (!loaded)
765
803
  return false;
804
+ const { current, prev } = loaded;
766
805
  current.name = newName;
767
- current.updatedAt = Date.now();
768
- this.appendMeta(current.channel, current.channelId, current);
769
- const active = this.readActive(current.channel, current.channelId);
770
- if (active && active.id === sessionId) {
771
- this.writeActive(current.channel, current.channelId, current);
772
- }
806
+ this.writeSessionIfChanged(current.channel, current.channelId, prev, current);
773
807
  return true;
774
808
  }
775
809
  async unbindSession(sessionId) {
@@ -153,7 +153,7 @@ export class TriggerScheduler {
153
153
  if (!top)
154
154
  return;
155
155
  const delay = Math.max(0, top.nextFireAt - Date.now());
156
- this.timer = setTimeout(() => this.onFire(), delay);
156
+ this.timer = setTimeout(() => this.onFire(), delay).unref();
157
157
  }
158
158
  onFire() {
159
159
  this.timer = null;
@@ -0,0 +1,118 @@
1
+ {
2
+ "rules": [
3
+ {
4
+ "id": "context-too-long-cn",
5
+ "match": "上下文过长",
6
+ "action": "stop",
7
+ "type": "context_too_long",
8
+ "message": "⚠️ 上下文过长,请手动输入 /compact 压缩上下文"
9
+ },
10
+ {
11
+ "id": "context-too-long-en",
12
+ "match": "context too long",
13
+ "action": "stop",
14
+ "type": "context_too_long",
15
+ "message": "⚠️ 上下文过长,请手动输入 /compact 压缩上下文"
16
+ },
17
+ {
18
+ "id": "prompt-too-long",
19
+ "match": "prompt is too long",
20
+ "action": "stop",
21
+ "type": "context_too_long",
22
+ "message": "⚠️ 输入过长,请精简提问或使用 /compact 压缩上下文"
23
+ },
24
+ {
25
+ "id": "invalid-api-key",
26
+ "match": "invalid api key",
27
+ "action": "stop",
28
+ "type": "auth_error",
29
+ "message": "❌ API Key 无效,请检查密钥配置。使用 /status 查看当前配置"
30
+ },
31
+ {
32
+ "id": "key-not-found",
33
+ "match": "key_not_found",
34
+ "action": "stop",
35
+ "type": "auth_error",
36
+ "message": "❌ API Key 未找到,请检查密钥配置"
37
+ },
38
+ {
39
+ "id": "credit-query-timeout",
40
+ "match": "积分查询超时",
41
+ "action": "retry",
42
+ "type": "api_error",
43
+ "message": "⚠️ 积分查询超时,正在重试..."
44
+ },
45
+ {
46
+ "id": "api-error-400",
47
+ "match": "api error: 400",
48
+ "action": "stop",
49
+ "type": "api_error",
50
+ "message": "❌ 请求格式错误,请检查输入内容"
51
+ },
52
+ {
53
+ "id": "api-error-403",
54
+ "match": "api error: 403",
55
+ "action": "retry",
56
+ "type": "api_error",
57
+ "message": "⚠️ API 访问受限,正在重试..."
58
+ },
59
+ {
60
+ "id": "api-error-429",
61
+ "match": "api error: 429",
62
+ "action": "retry",
63
+ "type": "api_error",
64
+ "message": "⚠️ 请求过于频繁,正在重试..."
65
+ },
66
+ {
67
+ "id": "api-error-500",
68
+ "match": "api error: 500",
69
+ "action": "retry",
70
+ "type": "api_error",
71
+ "message": "❌ API 服务暂时不可用,正在重试..."
72
+ },
73
+ {
74
+ "id": "api-error-502",
75
+ "match": "api error: 502",
76
+ "action": "retry",
77
+ "type": "api_error"
78
+ },
79
+ {
80
+ "id": "api-error-503",
81
+ "match": "api error: 503",
82
+ "action": "retry",
83
+ "type": "api_error"
84
+ },
85
+ {
86
+ "id": "api-error-504",
87
+ "match": "api error: 504",
88
+ "action": "retry",
89
+ "type": "api_error"
90
+ },
91
+ {
92
+ "id": "not-valid-json",
93
+ "match": "is not valid json",
94
+ "action": "retry",
95
+ "type": "api_error",
96
+ "message": "⚠️ API 返回异常响应,正在重试..."
97
+ },
98
+ {
99
+ "id": "feishu-permission",
100
+ "match": "im:resource",
101
+ "action": "stop",
102
+ "type": "unknown",
103
+ "message": "❌ 权限不足,请联系管理员配置应用权限"
104
+ },
105
+ {
106
+ "id": "stream-error",
107
+ "match": "stream",
108
+ "action": "retry",
109
+ "type": "stream_error"
110
+ },
111
+ {
112
+ "id": "request-aborted",
113
+ "match": "request was aborted",
114
+ "action": "ignore",
115
+ "type": "stream_error"
116
+ }
117
+ ]
118
+ }
@@ -0,0 +1,18 @@
1
+ export const BASEAGENT_CAPS = {
2
+ 'claude-code': {
3
+ autoLoadsRules: true,
4
+ supportsSystemPrompt: true,
5
+ },
6
+ 'claude': {
7
+ autoLoadsRules: true,
8
+ supportsSystemPrompt: true,
9
+ },
10
+ 'codex': {
11
+ autoLoadsRules: false,
12
+ supportsSystemPrompt: true,
13
+ },
14
+ 'gemini': {
15
+ autoLoadsRules: false,
16
+ supportsSystemPrompt: true,
17
+ },
18
+ };
@@ -0,0 +1,47 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { BASEAGENT_CAPS } from './baseagent-caps.js';
4
+ const MAX_DEPTH = 5;
5
+ export function resolveEckInjection(agentConfig, projectPath, kitsRulesPath) {
6
+ const caps = BASEAGENT_CAPS[agentConfig.baseAgent]
7
+ ?? { autoLoadsRules: false, supportsSystemPrompt: true };
8
+ if (!caps.autoLoadsRules) {
9
+ return { shouldInject: true, reason: 'baseagent-no-autoload' };
10
+ }
11
+ const symlinkActive = detectEckSymlink(projectPath, kitsRulesPath);
12
+ if (symlinkActive) {
13
+ return { shouldInject: false, reason: 'symlink-active' };
14
+ }
15
+ return { shouldInject: true, reason: 'symlink-not-found' };
16
+ }
17
+ export function detectEckSymlink(projectPath, kitsRulesPath) {
18
+ let dir = projectPath;
19
+ let depth = 0;
20
+ while (depth < MAX_DEPTH) {
21
+ const eckDir = path.join(dir, '.claude', 'rules', 'eck');
22
+ if (fs.existsSync(eckDir)) {
23
+ try {
24
+ const realPath = fs.realpathSync(eckDir);
25
+ const kitsRulesReal = fs.realpathSync(kitsRulesPath);
26
+ if (pathEquals(realPath, kitsRulesReal)) {
27
+ return true;
28
+ }
29
+ }
30
+ catch {
31
+ // detection failure → conservatively assume not loaded
32
+ }
33
+ }
34
+ const parent = path.dirname(dir);
35
+ if (parent === dir)
36
+ break;
37
+ dir = parent;
38
+ depth++;
39
+ }
40
+ return false;
41
+ }
42
+ function pathEquals(a, b) {
43
+ if (process.platform === 'win32') {
44
+ return path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
45
+ }
46
+ return path.resolve(a) === path.resolve(b);
47
+ }
@@ -0,0 +1,77 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { resolvePaths, kitsDocsDir, agentDir, getPackageRoot } from '../paths.js';
4
+ import { atomicWriteText } from '../utils/atomic-write.js';
5
+ import { logger } from '../utils/logger.js';
6
+ export function initEck() {
7
+ const p = resolvePaths();
8
+ const eckDir = p.eckDir;
9
+ fs.mkdirSync(eckDir, { recursive: true });
10
+ initEckRuntime(eckDir);
11
+ initEckPathRegistry(eckDir);
12
+ }
13
+ export function initAgentIndex(aid) {
14
+ const indexDir = path.join(agentDir(aid), 'index');
15
+ fs.mkdirSync(indexDir, { recursive: true });
16
+ const indexFile = path.join(indexDir, 'INDEX.md');
17
+ const guideFile = path.join(indexDir, 'GUIDE.md');
18
+ if (!fs.existsSync(indexFile)) {
19
+ const template = loadTemplate('INDEX.template.md');
20
+ if (template) {
21
+ atomicWriteText(indexFile, renderTemplate(template, { AID: aid }));
22
+ logger.info(`[eck] Created ${indexFile}`);
23
+ }
24
+ }
25
+ if (!fs.existsSync(guideFile)) {
26
+ const template = loadTemplate('GUIDE.template.md');
27
+ if (template) {
28
+ atomicWriteText(guideFile, renderTemplate(template, { AID: aid }));
29
+ logger.info(`[eck] Created ${guideFile}`);
30
+ }
31
+ }
32
+ }
33
+ function initEckRuntime(eckDir) {
34
+ const runtimeFile = path.join(eckDir, 'runtime.md');
35
+ if (fs.existsSync(runtimeFile))
36
+ return;
37
+ const template = loadTemplate('runtime.template.md');
38
+ if (!template)
39
+ return;
40
+ const vars = {
41
+ EVOLCLAW_HOME: resolvePaths().root,
42
+ PACKAGE_ROOT: getPackageRoot(),
43
+ };
44
+ atomicWriteText(runtimeFile, renderTemplate(template, vars));
45
+ logger.info(`[eck] Created ${runtimeFile}`);
46
+ }
47
+ function initEckPathRegistry(eckDir) {
48
+ const registryFile = path.join(eckDir, 'path-registry.md');
49
+ if (fs.existsSync(registryFile))
50
+ return;
51
+ const template = loadTemplate('path-registry.template.md');
52
+ if (!template)
53
+ return;
54
+ const vars = {
55
+ EVOLCLAW_HOME: resolvePaths().root,
56
+ PACKAGE_ROOT: getPackageRoot(),
57
+ };
58
+ atomicWriteText(registryFile, renderTemplate(template, vars));
59
+ logger.info(`[eck] Created ${registryFile}`);
60
+ }
61
+ function loadTemplate(filename) {
62
+ const templatePath = path.join(kitsDocsDir(), 'eck_templates', filename);
63
+ try {
64
+ return fs.readFileSync(templatePath, 'utf-8');
65
+ }
66
+ catch {
67
+ logger.warn(`[eck] Template not found: ${templatePath}`);
68
+ return null;
69
+ }
70
+ }
71
+ function renderTemplate(template, vars) {
72
+ let result = template;
73
+ for (const [key, value] of Object.entries(vars)) {
74
+ result = result.replaceAll(`{{${key}}}`, value);
75
+ }
76
+ return result;
77
+ }
@@ -0,0 +1,28 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { kitsRulesDir } from '../paths.js';
4
+ let _cachedRules = null;
5
+ export function loadRulesForInjection() {
6
+ if (_cachedRules !== null)
7
+ return _cachedRules;
8
+ const rulesDir = kitsRulesDir();
9
+ if (!fs.existsSync(rulesDir)) {
10
+ _cachedRules = '';
11
+ return '';
12
+ }
13
+ const files = fs.readdirSync(rulesDir)
14
+ .filter(f => f.endsWith('.md'))
15
+ .sort();
16
+ const parts = [];
17
+ for (const file of files) {
18
+ try {
19
+ parts.push(fs.readFileSync(path.join(rulesDir, file), 'utf-8'));
20
+ }
21
+ catch { /* skip unreadable files */ }
22
+ }
23
+ _cachedRules = parts.join('\n\n');
24
+ return _cachedRules;
25
+ }
26
+ export function invalidateRulesCache() {
27
+ _cachedRules = null;
28
+ }