evolclaw 2.8.3 → 3.0.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 (105) hide show
  1. package/README.md +21 -12
  2. package/dist/agents/claude-runner.js +102 -38
  3. package/dist/agents/codex-runner.js +11 -14
  4. package/dist/agents/gemini-runner.js +10 -12
  5. package/dist/agents/resolve.js +134 -0
  6. package/dist/agents/templates.js +3 -3
  7. package/dist/aun/aid/agentmd.js +186 -0
  8. package/dist/aun/aid/client.js +134 -0
  9. package/dist/aun/aid/identity.js +131 -0
  10. package/dist/aun/aid/index.js +3 -0
  11. package/dist/aun/aid/types.js +1 -0
  12. package/dist/aun/aid/validation.js +21 -0
  13. package/dist/aun/msg/group.js +291 -0
  14. package/dist/aun/msg/index.js +4 -0
  15. package/dist/aun/msg/p2p.js +144 -0
  16. package/dist/aun/msg/payload-type.js +27 -0
  17. package/dist/aun/msg/upload.js +98 -0
  18. package/dist/aun/outbox.js +138 -0
  19. package/dist/aun/rpc/caller.js +42 -0
  20. package/dist/aun/rpc/connection.js +34 -0
  21. package/dist/aun/rpc/index.js +2 -0
  22. package/dist/aun/storage/download.js +29 -0
  23. package/dist/aun/storage/index.js +3 -0
  24. package/dist/aun/storage/manage.js +10 -0
  25. package/dist/aun/storage/upload.js +35 -0
  26. package/dist/channels/aun.js +1051 -288
  27. package/dist/channels/dingtalk.js +58 -5
  28. package/dist/channels/feishu.js +266 -30
  29. package/dist/channels/qqbot.js +67 -12
  30. package/dist/channels/wechat.js +61 -4
  31. package/dist/channels/wecom.js +58 -5
  32. package/dist/cli/agent.js +800 -0
  33. package/dist/cli/index.js +4253 -0
  34. package/dist/{utils → cli}/init-channel.js +211 -621
  35. package/dist/cli/init.js +178 -0
  36. package/dist/config-store.js +613 -0
  37. package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
  38. package/dist/core/channel-loader.js +162 -11
  39. package/dist/core/command-handler.js +858 -847
  40. package/dist/core/evolagent-registry.js +191 -371
  41. package/dist/core/evolagent.js +203 -234
  42. package/dist/core/interaction-router.js +52 -5
  43. package/dist/core/message/im-renderer.js +480 -0
  44. package/dist/core/message/items-formatter.js +61 -0
  45. package/dist/core/message/message-bridge.js +104 -56
  46. package/dist/core/message/message-log.js +91 -0
  47. package/dist/core/message/message-processor.js +309 -142
  48. package/dist/core/message/message-queue.js +3 -3
  49. package/dist/core/permission.js +21 -8
  50. package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
  51. package/dist/core/session/session-fs-store.js +230 -0
  52. package/dist/core/session/session-manager.js +704 -775
  53. package/dist/core/session/session-mapper.js +87 -0
  54. package/dist/core/trigger/manager.js +122 -0
  55. package/dist/core/trigger/parser.js +128 -0
  56. package/dist/core/trigger/scheduler.js +224 -0
  57. package/dist/{templates → data}/prompts.md +34 -1
  58. package/dist/index.js +431 -275
  59. package/dist/ipc.js +49 -0
  60. package/dist/paths.js +82 -9
  61. package/dist/types.js +8 -2
  62. package/dist/utils/atomic-write.js +79 -0
  63. package/dist/utils/channel-helpers.js +46 -0
  64. package/dist/utils/cross-platform.js +0 -18
  65. package/dist/utils/instance-registry.js +433 -0
  66. package/dist/utils/log-writer.js +216 -0
  67. package/dist/utils/logger.js +24 -77
  68. package/dist/utils/media-cache.js +23 -0
  69. package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
  70. package/dist/utils/process-introspect.js +144 -0
  71. package/dist/utils/stats.js +192 -0
  72. package/dist/watch-msg.js +529 -0
  73. package/evolclaw-install-aun.md +114 -46
  74. package/kits/aun/meta.md +25 -0
  75. package/kits/aun/role.md +25 -0
  76. package/kits/channels/aun.md +25 -0
  77. package/kits/evolclaw/commands.md +31 -0
  78. package/kits/evolclaw/identity-tools.md +26 -0
  79. package/kits/evolclaw/self-summary.md +29 -0
  80. package/kits/evolclaw/tools.md +25 -0
  81. package/kits/templates/group.md +20 -0
  82. package/kits/templates/private.md +9 -0
  83. package/kits/templates/system-fragments/personal-context.md +3 -0
  84. package/kits/templates/system-fragments/self-intro.md +5 -0
  85. package/kits/templates/system-fragments/speaker-intro.md +5 -0
  86. package/kits/templates/system-fragments/venue-intro.md +5 -0
  87. package/package.json +7 -5
  88. package/data/evolclaw.sample.json +0 -60
  89. package/dist/channels/aun-ops.js +0 -275
  90. package/dist/cli.js +0 -2178
  91. package/dist/config.js +0 -591
  92. package/dist/core/agent-registry.js +0 -450
  93. package/dist/core/evolagent-schema.js +0 -72
  94. package/dist/core/message/stream-flusher.js +0 -238
  95. package/dist/core/message/thought-emitter.js +0 -162
  96. package/dist/core/reload-hooks.js +0 -87
  97. package/dist/prompts/templates.js +0 -122
  98. package/dist/templates/skills.md +0 -66
  99. package/dist/utils/channel-fingerprint.js +0 -59
  100. package/dist/utils/error-dict.js +0 -63
  101. package/dist/utils/format.js +0 -32
  102. package/dist/utils/init.js +0 -645
  103. package/dist/utils/migrate-project.js +0 -122
  104. package/dist/utils/reload-hooks.js +0 -87
  105. package/dist/utils/stats-collector.js +0 -99
@@ -1,450 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { EvolAgent } from './evolagent.js';
4
- import { validateEvolAgentConfig } from './evolagent-schema.js';
5
- import { extractFingerprint } from '../utils/channel-fingerprint.js';
6
- import { logger } from '../utils/logger.js';
7
- export class AgentRegistry {
8
- agentsDir;
9
- agents = new Map();
10
- defaultAgent = null;
11
- channelIndex = new Map();
12
- globalWriter;
13
- constructor(agentsDir, globalWriter) {
14
- this.agentsDir = agentsDir;
15
- this.globalWriter = globalWriter;
16
- }
17
- /** Late-binding setter for tests / index.ts wiring order. */
18
- setGlobalWriter(writer) {
19
- this.globalWriter = writer;
20
- }
21
- loadAll(globalConfig) {
22
- this.agents.clear();
23
- this.channelIndex.clear();
24
- const files = fs.existsSync(this.agentsDir)
25
- ? fs.readdirSync(this.agentsDir).filter(f => f.endsWith('.json'))
26
- : [];
27
- for (const file of files) {
28
- const fullPath = path.join(this.agentsDir, file);
29
- try {
30
- const raw = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
31
- const validation = validateEvolAgentConfig(raw);
32
- if (!validation.valid) {
33
- const name = raw?.name || path.basename(file, '.json');
34
- const errorAgent = new EvolAgent(fullPath, { ...raw, name });
35
- errorAgent.status = 'error';
36
- errorAgent.error = validation.errors.join('; ');
37
- this.agents.set(name, errorAgent);
38
- logger.warn(`[AgentRegistry] ${file}: ${validation.errors.join('; ')}`);
39
- continue;
40
- }
41
- const agent = new EvolAgent(fullPath, raw);
42
- this.agents.set(agent.name, agent);
43
- }
44
- catch (e) {
45
- logger.warn(`[AgentRegistry] Failed to load ${file}: ${e}`);
46
- }
47
- }
48
- this.defaultAgent = this.buildDefaultAgent(globalConfig);
49
- this.detectAndFlagConflicts();
50
- this.buildChannelIndex();
51
- }
52
- buildDefaultAgent(globalConfig) {
53
- const agents = globalConfig.agents || {};
54
- const defaultName = agents.defaultAgent || 'claude';
55
- const cfg = {
56
- name: '[default]',
57
- enabled: true,
58
- agents: { [defaultName]: agents[defaultName] || {} },
59
- channels: globalConfig.channels || {},
60
- projects: { defaultPath: globalConfig.projects?.defaultPath || process.cwd() },
61
- chatmode: globalConfig.chatmode,
62
- };
63
- return new EvolAgent(null, cfg, { isDefault: true });
64
- }
65
- detectAndFlagConflicts() {
66
- const seen = new Map();
67
- const record = (agentName, channelsBlock) => {
68
- for (const [type, raw] of Object.entries(channelsBlock || {})) {
69
- if (type === 'defaultChannel')
70
- continue;
71
- const instances = Array.isArray(raw) ? raw : [raw];
72
- for (const inst of instances) {
73
- if (!inst || typeof inst !== 'object')
74
- continue;
75
- const fp = extractFingerprint(type, inst);
76
- if (!fp)
77
- continue;
78
- const instName = inst.name ?? type;
79
- const entry = seen.get(fp) || [];
80
- entry.push({ agent: agentName, instance: instName });
81
- seen.set(fp, entry);
82
- }
83
- }
84
- };
85
- for (const agent of this.agents.values()) {
86
- if (agent.status === 'error')
87
- continue;
88
- record(agent.name, agent.config.channels);
89
- }
90
- if (this.defaultAgent) {
91
- record(this.defaultAgent.name, this.defaultAgent.config.channels);
92
- }
93
- for (const [_fp, occurrences] of seen) {
94
- if (occurrences.length <= 1)
95
- continue;
96
- const msg = `Channel conflict: fingerprint claimed by ${occurrences.map(o => `${o.agent}(${o.instance})`).join(', ')}`;
97
- const involvedNames = [...new Set(occurrences.map(o => o.agent))];
98
- for (const name of involvedNames) {
99
- if (name === '[default]')
100
- continue;
101
- const a = this.agents.get(name);
102
- if (a && a.status !== 'error') {
103
- a.status = 'error';
104
- a.error = msg;
105
- }
106
- }
107
- logger.error(`[AgentRegistry] ${msg}`);
108
- }
109
- }
110
- buildChannelIndex() {
111
- for (const agent of this.agents.values()) {
112
- if (agent.status === 'error' || agent.status === 'disabled')
113
- continue;
114
- for (const name of agent.channelInstanceNames()) {
115
- this.channelIndex.set(name, agent.name);
116
- }
117
- }
118
- if (this.defaultAgent) {
119
- for (const name of this.defaultAgent.channelInstanceNames()) {
120
- if (this.channelIndex.has(name))
121
- continue;
122
- this.channelIndex.set(name, '[default]');
123
- }
124
- }
125
- }
126
- resolveByChannel(channelName) {
127
- const agentName = this.channelIndex.get(channelName);
128
- if (!agentName)
129
- return null;
130
- if (agentName === '[default]')
131
- return this.defaultAgent;
132
- return this.agents.get(agentName) || null;
133
- }
134
- /**
135
- * Check ownership of a channel via the agent that owns it.
136
- * - For named EvolAgent: reads agent.config (memory; reflects agent.json).
137
- * - For DefaultAgent or unknown channel: invokes `globalFallback`, which
138
- * should consult the global config (evolclaw.json) via `config.ts:isOwner`.
139
- */
140
- isOwner(channelName, userId, globalFallback) {
141
- const agent = this.resolveByChannel(channelName);
142
- if (agent && !agent.isDefault)
143
- return agent.isOwner(channelName, userId);
144
- return globalFallback(channelName, userId);
145
- }
146
- /** Same routing logic as `isOwner`, applied to admin checks. */
147
- isAdmin(channelName, userId, globalFallback) {
148
- const agent = this.resolveByChannel(channelName);
149
- if (agent && !agent.isDefault)
150
- return agent.isAdmin(channelName, userId);
151
- return globalFallback(channelName, userId);
152
- }
153
- /** Lookup current owner — agent first, then DefaultAgent (which mirrors evolclaw.json). */
154
- getOwner(channelName) {
155
- const agent = this.resolveByChannel(channelName);
156
- if (!agent)
157
- return undefined;
158
- return agent.getOwner(channelName);
159
- }
160
- /**
161
- * Persist owner. Routes to agent.json for named agents, or to evolclaw.json
162
- * via the configured `globalWriter` for DefaultAgent. No-ops with a warning
163
- * when the channel is unknown or no global writer is wired.
164
- */
165
- setChannelOwner(channelName, userId) {
166
- const agent = this.resolveByChannel(channelName);
167
- if (!agent) {
168
- logger.warn(`[AgentRegistry] setChannelOwner: channel "${channelName}" not found`);
169
- return;
170
- }
171
- if (agent.isDefault) {
172
- if (!this.globalWriter) {
173
- logger.warn(`[AgentRegistry] setChannelOwner: no globalWriter wired for default channel "${channelName}"`);
174
- return;
175
- }
176
- this.globalWriter.setOwner(channelName, userId);
177
- }
178
- else {
179
- agent.setOwner(channelName, userId);
180
- }
181
- }
182
- /**
183
- * Read showActivities mode. Falls back to `'all'` when the channel is
184
- * unknown — matches the prior behavior of `config.ts:getChannelShowActivities`.
185
- */
186
- getShowActivities(channelName) {
187
- const agent = this.resolveByChannel(channelName);
188
- if (!agent)
189
- return 'all';
190
- return agent.getShowActivities(channelName);
191
- }
192
- /** Persist showActivities. Routes to agent.json or evolclaw.json. */
193
- setShowActivities(channelName, mode) {
194
- const agent = this.resolveByChannel(channelName);
195
- if (!agent) {
196
- logger.warn(`[AgentRegistry] setShowActivities: channel "${channelName}" not found`);
197
- return;
198
- }
199
- if (agent.isDefault) {
200
- if (!this.globalWriter?.setShowActivities) {
201
- logger.warn(`[AgentRegistry] setShowActivities: no globalWriter wired for default channel "${channelName}"`);
202
- return;
203
- }
204
- this.globalWriter.setShowActivities(channelName, mode);
205
- }
206
- else {
207
- agent.setShowActivities(channelName, mode);
208
- }
209
- }
210
- get(name) {
211
- if (name === '[default]')
212
- return this.defaultAgent;
213
- return this.agents.get(name) || null;
214
- }
215
- list() {
216
- const result = [];
217
- for (const agent of this.agents.values()) {
218
- result.push(this.toInfo(agent));
219
- }
220
- if (this.defaultAgent) {
221
- result.push(this.toInfo(this.defaultAgent));
222
- }
223
- return result;
224
- }
225
- runnableAgents() {
226
- return [...this.agents.values()].filter(a => a.status === 'stopped');
227
- }
228
- async reload(name, hooks) {
229
- const oldAgent = this.agents.get(name);
230
- if (!oldAgent)
231
- throw new Error(`Agent "${name}" not found`);
232
- if (!oldAgent.configPath)
233
- throw new Error(`Cannot reload DefaultAgent`);
234
- // 1. Re-read config from disk
235
- const raw = JSON.parse(fs.readFileSync(oldAgent.configPath, 'utf-8'));
236
- const validation = validateEvolAgentConfig(raw);
237
- if (!validation.valid) {
238
- throw new Error(`Invalid config after edit: ${validation.errors.join('; ')}`);
239
- }
240
- const newAgent = new EvolAgent(oldAgent.configPath, raw);
241
- // Warn if projectPath changed — existing sessions retain old path (by design,
242
- // to avoid breaking SDK conversation history at .claude/<encoded-path>/...)
243
- if (oldAgent.projectPath !== newAgent.projectPath) {
244
- logger.warn(`[AgentRegistry] Agent "${name}" projectPath changed: ${oldAgent.projectPath} → ${newAgent.projectPath}. ` +
245
- `Existing sessions retain the old path; only new sessions will use the new path. ` +
246
- `To migrate, manually UPDATE sessions SET project_path=? WHERE id=? (warning: SDK conversation history may be lost).`);
247
- }
248
- // 2. Fingerprint conflict check (against all others except self)
249
- const conflict = this.checkConflictForReload(newAgent, name);
250
- if (conflict) {
251
- throw new Error(`Channel conflict: ${conflict}`);
252
- }
253
- // 3. Compute channel diff
254
- const oldChannels = new Set(oldAgent.channelInstanceNames());
255
- const newChannels = new Set(newAgent.channelInstanceNames());
256
- const toRemove = [...oldChannels].filter(c => !newChannels.has(c));
257
- const toAdd = [...newChannels].filter(c => !oldChannels.has(c));
258
- const kept = [...oldChannels].filter(c => newChannels.has(c));
259
- // I6: detect kept-channel credential changes — treat as remove+add so
260
- // the channel reconnects with new credentials (e.g. appSecret rotated).
261
- const credentialsChanged = [];
262
- const trulyKept = [];
263
- for (const ch of kept) {
264
- const oldCh = getChannelInstanceConfig(oldAgent, ch);
265
- const newCh = getChannelInstanceConfig(newAgent, ch);
266
- if (oldCh && newCh && channelConfigChanged(oldCh.config, newCh.config)) {
267
- credentialsChanged.push(ch);
268
- }
269
- else {
270
- trulyKept.push(ch);
271
- }
272
- }
273
- toRemove.push(...credentialsChanged);
274
- toAdd.push(...credentialsChanged);
275
- // Track what was removed/added so we can roll back on failure
276
- const removedSuccessfully = [];
277
- const addedSuccessfully = [];
278
- try {
279
- // 4. Drain channels being removed
280
- for (const ch of toRemove) {
281
- await hooks.drainChannel(ch);
282
- }
283
- // 5. Disconnect removed channels
284
- for (const ch of toRemove) {
285
- await hooks.disconnectChannel(ch);
286
- removedSuccessfully.push(ch);
287
- }
288
- // 6. Start new channels
289
- for (const ch of toAdd) {
290
- await hooks.startChannel(newAgent, ch);
291
- addedSuccessfully.push(ch);
292
- }
293
- // 7. Transfer kept channel adapters from old to new (only truly unchanged ones)
294
- for (const ch of trulyKept) {
295
- const adapter = oldAgent.channels.get(ch);
296
- if (adapter)
297
- newAgent.channels.set(ch, adapter);
298
- }
299
- // 8. Preserve runtime state
300
- // I5: only set 'running' when oldAgent was running; preserve error/disabled
301
- newAgent.activeSessions = oldAgent.activeSessions;
302
- newAgent.lastActivity = oldAgent.lastActivity;
303
- if (oldAgent.status === 'error' || oldAgent.status === 'disabled') {
304
- newAgent.status = oldAgent.status;
305
- newAgent.error = oldAgent.error;
306
- }
307
- else {
308
- newAgent.status = 'running';
309
- }
310
- // 9. Swap in registry
311
- this.agents.set(name, newAgent);
312
- // 10. Rebuild channel index
313
- this.channelIndex.clear();
314
- this.buildChannelIndex();
315
- }
316
- catch (err) {
317
- // C1: Rollback — restore original channels, keep oldAgent in registry
318
- logger.error(`[Reload] Failed: ${err}. Attempting rollback for "${name}".`);
319
- for (const ch of addedSuccessfully) {
320
- try {
321
- await hooks.disconnectChannel(ch);
322
- }
323
- catch (_) { /* best effort */ }
324
- }
325
- for (const ch of removedSuccessfully) {
326
- try {
327
- await hooks.startChannel(oldAgent, ch);
328
- }
329
- catch (_) { /* best effort */ }
330
- }
331
- // Don't swap registry — oldAgent stays in place
332
- oldAgent.status = 'error';
333
- oldAgent.error = `Reload failed (rollback attempted): ${err instanceof Error ? err.message : String(err)}`;
334
- throw err;
335
- }
336
- }
337
- checkConflictForReload(newAgent, excludeName) {
338
- const newFingerprints = new Map(); // fp → instanceName
339
- for (const [type, raw] of Object.entries(newAgent.config.channels || {})) {
340
- if (type === 'defaultChannel')
341
- continue;
342
- const instances = Array.isArray(raw) ? raw : [raw];
343
- for (const inst of instances) {
344
- if (!inst || typeof inst !== 'object')
345
- continue;
346
- const fp = extractFingerprint(type, inst);
347
- if (!fp)
348
- continue;
349
- const instName = inst.name ?? type;
350
- newFingerprints.set(fp, instName);
351
- }
352
- }
353
- // Check against all other agents (excluding self)
354
- for (const [agentName, agent] of this.agents) {
355
- if (agentName === excludeName)
356
- continue;
357
- if (agent.status === 'error' || agent.status === 'disabled')
358
- continue;
359
- for (const [type, raw] of Object.entries(agent.config.channels || {})) {
360
- if (type === 'defaultChannel')
361
- continue;
362
- const instances = Array.isArray(raw) ? raw : [raw];
363
- for (const inst of instances) {
364
- if (!inst || typeof inst !== 'object')
365
- continue;
366
- const fp = extractFingerprint(type, inst);
367
- if (!fp)
368
- continue;
369
- if (newFingerprints.has(fp)) {
370
- return `${fp} conflicts with agent "${agentName}"`;
371
- }
372
- }
373
- }
374
- }
375
- // Check against DefaultAgent
376
- if (this.defaultAgent) {
377
- for (const [type, raw] of Object.entries(this.defaultAgent.config.channels || {})) {
378
- if (type === 'defaultChannel')
379
- continue;
380
- const instances = Array.isArray(raw) ? raw : [raw];
381
- for (const inst of instances) {
382
- if (!inst || typeof inst !== 'object')
383
- continue;
384
- const fp = extractFingerprint(type, inst);
385
- if (!fp)
386
- continue;
387
- if (newFingerprints.has(fp)) {
388
- return `${fp} conflicts with DefaultAgent`;
389
- }
390
- }
391
- }
392
- }
393
- return null;
394
- }
395
- toInfo(agent) {
396
- let baseagent = 'claude';
397
- let model;
398
- let effort;
399
- try {
400
- baseagent = agent.baseagent;
401
- model = agent.model;
402
- effort = agent.effort;
403
- }
404
- catch { /* invalid config */ }
405
- return {
406
- name: agent.name,
407
- status: agent.status,
408
- channels: agent.channelInstanceNames(),
409
- projectPath: agent.config.projects?.defaultPath ?? '',
410
- baseagent,
411
- model,
412
- effort,
413
- lastActivity: agent.lastActivity,
414
- activeSessions: agent.activeSessions,
415
- error: agent.error,
416
- isDefault: agent.isDefault,
417
- };
418
- }
419
- }
420
- /**
421
- * Locate the raw config of a channel instance by name within an agent config.
422
- * Returns `{ type, config }` or null if not found.
423
- *
424
- * Matches against the effective channel name (with agent prefix for EvolAgents),
425
- * mirroring `EvolAgent.findChannelInstance`. Only used by `reload()` to detect
426
- * kept-channel credential changes.
427
- */
428
- function getChannelInstanceConfig(agent, channelName) {
429
- for (const [type, raw] of Object.entries(agent.config?.channels || {})) {
430
- if (type === 'defaultChannel')
431
- continue;
432
- const instances = Array.isArray(raw) ? raw : [raw];
433
- for (const inst of instances) {
434
- if (!inst || typeof inst !== 'object')
435
- continue;
436
- const effName = agent.effectiveChannelName(type, inst.name);
437
- if (effName === channelName)
438
- return { type, config: inst };
439
- }
440
- }
441
- return null;
442
- }
443
- /**
444
- * Compare two channel-instance configs by serialized JSON. Channel configs
445
- * are plain JSON-shaped objects (no functions/Buffers), so this is a sound
446
- * structural compare for "did anything in this channel block change".
447
- */
448
- function channelConfigChanged(oldConfig, newConfig) {
449
- return JSON.stringify(oldConfig) !== JSON.stringify(newConfig);
450
- }
@@ -1,72 +0,0 @@
1
- import path from 'path';
2
- const VALID_BASEAGENTS = new Set(['claude', 'codex', 'gemini', 'hermes']);
3
- const VALID_CHANNEL_TYPES = new Set(['feishu', 'aun', 'wechat', 'wecom', 'dingtalk', 'qqbot']);
4
- const VALID_CHATMODES = new Set(['interactive', 'proactive']);
5
- export function validateEvolAgentConfig(raw) {
6
- const errors = [];
7
- if (!raw || typeof raw !== 'object') {
8
- return { valid: false, errors: ['config must be an object'] };
9
- }
10
- if (typeof raw.name !== 'string' || raw.name.trim() === '') {
11
- errors.push('name is required and must be a non-empty string');
12
- }
13
- if (raw.enabled !== undefined && typeof raw.enabled !== 'boolean') {
14
- errors.push('enabled must be a boolean if present');
15
- }
16
- if (!raw.agents || typeof raw.agents !== 'object') {
17
- errors.push('agents must be an object with exactly one baseagent block');
18
- }
19
- else {
20
- const keys = Object.keys(raw.agents).filter(k => VALID_BASEAGENTS.has(k));
21
- const unknownKeys = Object.keys(raw.agents).filter(k => !VALID_BASEAGENTS.has(k));
22
- if (unknownKeys.length > 0) {
23
- errors.push(`agents contains unknown baseagent keys: ${unknownKeys.join(', ')}`);
24
- }
25
- if (keys.length === 0) {
26
- errors.push('agents must contain exactly one of: claude | codex | gemini | hermes');
27
- }
28
- else if (keys.length > 1) {
29
- errors.push(`agents must contain exactly one baseagent (single baseagent only), got: ${keys.join(', ')}`);
30
- }
31
- }
32
- if (!raw.channels || typeof raw.channels !== 'object') {
33
- errors.push('channels is required');
34
- }
35
- else {
36
- const channelKeys = Object.keys(raw.channels);
37
- if (channelKeys.length === 0) {
38
- errors.push('channels must contain at least one channel type');
39
- }
40
- for (const key of channelKeys) {
41
- if (!VALID_CHANNEL_TYPES.has(key)) {
42
- errors.push(`unknown channel type: ${key}`);
43
- }
44
- }
45
- }
46
- if (!raw.projects || typeof raw.projects !== 'object') {
47
- errors.push('projects is required');
48
- }
49
- else {
50
- const p = raw.projects.defaultPath;
51
- if (typeof p !== 'string' || p === '') {
52
- errors.push('projects.defaultPath is required');
53
- }
54
- else if (!path.isAbsolute(p)) {
55
- errors.push(`projects.defaultPath must be absolute, got: ${p}`);
56
- }
57
- }
58
- if (raw.chatmode !== undefined) {
59
- if (typeof raw.chatmode !== 'object' || raw.chatmode === null) {
60
- errors.push('chatmode must be an object if present');
61
- }
62
- else {
63
- for (const key of ['private', 'group']) {
64
- const val = raw.chatmode[key];
65
- if (val !== undefined && !VALID_CHATMODES.has(val)) {
66
- errors.push(`chatmode.${key} must be 'interactive' or 'proactive'`);
67
- }
68
- }
69
- }
70
- }
71
- return { valid: errors.length === 0, errors };
72
- }