evolcore 0.0.2 → 0.0.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +44 -793
  2. package/dist/agents/claude-runner.js +197 -17
  3. package/dist/agents/codex-runner.js +46 -3
  4. package/dist/aun/outbox.js +8 -0
  5. package/dist/channels/aun.js +21 -4
  6. package/dist/channels/contact-bind-code.js +134 -0
  7. package/dist/channels/dingtalk.js +979 -149
  8. package/dist/channels/feishu.js +130 -54
  9. package/dist/channels/wecom-card.js +101 -0
  10. package/dist/channels/wecom-onboarding.js +82 -0
  11. package/dist/channels/wecom-state.js +191 -0
  12. package/dist/channels/wecom.js +755 -163
  13. package/dist/cli/agent-command.js +2 -1
  14. package/dist/cli/aun-commands.js +88 -33
  15. package/dist/cli/bench.js +2 -2
  16. package/dist/cli/contact.js +71 -0
  17. package/dist/cli/ctl-command.js +2 -2
  18. package/dist/cli/daemon-commands.js +77 -214
  19. package/dist/cli/handoff-command.js +2 -2
  20. package/dist/cli/help.js +9 -5
  21. package/dist/cli/index.js +132 -116
  22. package/dist/cli/init-channel.js +92 -97
  23. package/dist/cli/init.js +63 -26
  24. package/dist/cli/model.js +2 -1
  25. package/dist/cli/net-check.js +2 -2
  26. package/dist/cli/queue-command.js +30 -6
  27. package/dist/cli/raw-key-input.js +25 -0
  28. package/dist/cli/response.js +5 -6
  29. package/dist/cli/restart-monitor.js +25 -1
  30. package/dist/cli/stats.js +6 -4
  31. package/dist/cli/trigger-command.js +55 -15
  32. package/dist/cli/version.js +6 -1
  33. package/dist/config/builtin-role-templates.js +22 -10
  34. package/dist/config/builtin-roles.js +7 -1
  35. package/dist/config/config-manager.js +221 -17
  36. package/dist/config/contact-alias.js +68 -0
  37. package/dist/config/contact-book-store.js +454 -0
  38. package/dist/config/contact-book-v2-startup.js +35 -0
  39. package/dist/config/contact-book.js +156 -303
  40. package/dist/config/contact-operation-service.js +110 -0
  41. package/dist/config/peer-role-resolver.js +133 -54
  42. package/dist/config/role-ranks.js +18 -0
  43. package/dist/config/role-service.js +16 -19
  44. package/dist/config/role-store.js +16 -5
  45. package/dist/config/roles.js +10 -1
  46. package/dist/config-store.js +0 -2
  47. package/dist/core/auth/authorization-audit.js +10 -1
  48. package/dist/core/auth/operation-authorizer.js +2 -0
  49. package/dist/core/auth/operation-catalog.js +56 -0
  50. package/dist/core/command/command-handler.js +105 -10
  51. package/dist/core/command/connect-menu.js +374 -0
  52. package/dist/core/command/menu-handler.js +114 -16
  53. package/dist/core/command/role-menu.js +128 -29
  54. package/dist/core/command/slash-handler.js +28 -18
  55. package/dist/core/daemon-file-cache.js +12 -6
  56. package/dist/core/event-catalog.js +70 -0
  57. package/dist/core/evolagent-registry.js +0 -1
  58. package/dist/core/evolagent.js +20 -9
  59. package/dist/core/message/im-renderer.js +2 -0
  60. package/dist/core/message/message-bridge.js +79 -8
  61. package/dist/core/message/message-queue.js +10 -0
  62. package/dist/core/message/message-utils.js +8 -2
  63. package/dist/core/message/response-engine.js +269 -25
  64. package/dist/core/message/send-receipt.js +24 -0
  65. package/dist/core/message/stream-debouncer.js +11 -2
  66. package/dist/core/permission/tool-policy.js +47 -15
  67. package/dist/core/protected-paths.js +2 -0
  68. package/dist/core/session/session-fs-store.js +44 -3
  69. package/dist/core/session/session-manager.js +61 -5
  70. package/dist/index.js +62 -6
  71. package/dist/ipc.js +34 -5
  72. package/dist/stats/billing.js +20 -8
  73. package/dist/trigger/manager.js +3 -0
  74. package/dist/trigger/parser.js +77 -2
  75. package/dist/trigger/patch.js +8 -1
  76. package/dist/trigger/scheduler.js +3 -1
  77. package/dist/trigger/validation.js +13 -0
  78. package/dist/utils/aid-bind.js +43 -29
  79. package/dist/utils/instance-registry.js +14 -7
  80. package/dist/utils/log-writer.js +46 -0
  81. package/dist/utils/media-cache.js +4 -1
  82. package/dist/utils/model-prices.jsonl +6 -3
  83. package/dist/utils/restart-safety.js +31 -0
  84. package/dist/utils/system-memory.js +62 -0
  85. package/kits/docs/INDEX.md +2 -1
  86. package/kits/docs/evolcore/INDEX.md +5 -3
  87. package/kits/docs/evolcore/agent.md +9 -1
  88. package/kits/docs/evolcore/aid.md +5 -2
  89. package/kits/docs/evolcore/contact.md +57 -0
  90. package/kits/docs/evolcore/fs.md +9 -0
  91. package/kits/docs/evolcore/group.md +12 -3
  92. package/kits/docs/evolcore/model.md +4 -1
  93. package/kits/docs/evolcore/msg.md +9 -3
  94. package/kits/docs/evolcore/response.md +16 -21
  95. package/kits/docs/evolcore/rpc.md +2 -0
  96. package/kits/docs/evolcore/stats.md +15 -2
  97. package/kits/docs/evolcore/storage.md +1 -0
  98. package/kits/docs/evolcore/trigger.md +17 -2
  99. package/kits/eck_manifest.json +12 -0
  100. package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
  101. package/kits/schemas/_meta.json +7 -4
  102. package/kits/schemas/agent-config.schema.6.json +322 -0
  103. package/kits/schemas/contact-book.schema.2.json +43 -0
  104. package/kits/schemas/relation-config.schema.5.json +47 -0
  105. package/kits/schemas/role-config.schema.1.json +1 -0
  106. package/kits/schemas/role-registry.schema.1.json +2 -2
  107. package/kits/templates/roles/admin.json +1 -0
  108. package/kits/templates/roles/member.json +1 -0
  109. package/kits/templates/roles/owner.json +1 -0
  110. package/kits/templates/roles/visitor.json +1 -0
  111. package/kits/templates/system-fragments/commands.md +3 -1
  112. package/package.json +4 -4
  113. package/assets/brand/evolcore/README.md +0 -19
  114. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  115. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  116. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  117. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  118. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  119. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  120. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  121. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  122. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  123. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  124. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -1,6 +1,9 @@
1
1
  import { agentContactConfig } from '../paths.js';
2
2
  import { isValidAid } from '../aun/aid/validation.js';
3
- import { ConfigTarget, read, validateConfig, write } from './config-manager.js';
3
+ import { isExplicitGroupId } from '../aun/group-identity.js';
4
+ import { ConfigTarget, read, validateConfig } from './config-manager.js';
5
+ import { ContactMutationError, contactRevision, mutateContactBookSync } from './contact-book-store.js';
6
+ import { formatContactAlias, normalizeContactChannelKey, parseContactAlias, } from './contact-alias.js';
4
7
  export class ContactBookError extends Error {
5
8
  code;
6
9
  constructor(code, message) {
@@ -10,13 +13,11 @@ export class ContactBookError extends Error {
10
13
  }
11
14
  }
12
15
  let aliasMapCache = new WeakMap();
13
- const emptyAliasMap = {
14
- scopedAliasToPrimary: new Map(),
15
- conflictingScopedAliases: new Set(),
16
- legacyAliasToPrimary: new Map(),
17
- conflictingLegacyAliases: new Set(),
16
+ let snapshotByBook = new WeakMap();
17
+ const EMPTY_ALIAS_MAP = {
18
+ aliasToPrimary: new Map(),
19
+ conflictingAliases: new Set(),
18
20
  };
19
- const SCOPED_ALIAS_PREFIX = 'channel-key:';
20
21
  export function agentContactConfigPath(aid) {
21
22
  return agentContactConfig(aid);
22
23
  }
@@ -26,346 +27,198 @@ export function readContactBook(selfAid, opts = {}) {
26
27
  return read(ConfigTarget.Contact, { self: selfAid }, { cache: opts.cache ?? true });
27
28
  }
28
29
  export function resolvePrincipal(selfAid, channelType, actorId, channelKey) {
29
- const normalizedActor = String(actorId || '').trim();
30
- const normalizedChannel = String(channelType || '').trim().toLowerCase();
31
- if (normalizedChannel === 'aun') {
32
- return isValidAid(normalizedActor)
33
- ? { principalId: normalizedActor, principalStatus: 'native_aid' }
34
- : { principalStatus: normalizedActor ? 'unmapped' : 'error' };
35
- }
36
- const normalizedSelf = String(selfAid || '').trim();
37
- const normalizedChannelKey = normalizeChannelKey(normalizedSelf, normalizedChannel, channelKey);
38
- const legacyAlias = normalizeLegacyAlias(normalizedChannel, normalizedActor);
39
- if (!isValidAid(normalizedSelf) || !normalizedChannelKey || !legacyAlias) {
30
+ const actor = String(actorId || '').trim();
31
+ const type = String(channelType || '').trim().toLowerCase();
32
+ if (type === 'aun') {
33
+ return isValidAid(actor)
34
+ ? { principalId: actor, principalStatus: 'native_aid' }
35
+ : { principalStatus: actor ? 'unmapped' : 'error' };
36
+ }
37
+ const canonicalChannelKey = normalizeContactChannelKey(selfAid, type, channelKey);
38
+ if (!isValidAid(String(selfAid || '').trim()) || !actor || !canonicalChannelKey) {
40
39
  return { principalStatus: 'error' };
41
40
  }
42
- const scopedAlias = scopedAliasLookupKey(normalizedChannelKey, normalizedActor);
43
41
  try {
44
- const aliases = loadAliasMap(normalizedSelf);
45
- if (aliases.conflictingScopedAliases.has(scopedAlias)) {
46
- return { principalStatus: 'conflict' };
47
- }
48
- const scopedPrincipalId = aliases.scopedAliasToPrimary.get(scopedAlias);
49
- if (scopedPrincipalId) {
50
- return { principalId: scopedPrincipalId, principalStatus: 'mapped' };
51
- }
52
- // Legacy aliases did not identify a channel instance. They remain trusted only
53
- // while the configured channel type has exactly one matching instance.
54
- if (!isUnambiguousLegacyChannel(normalizedSelf, normalizedChannel, normalizedChannelKey)) {
55
- return { principalStatus: 'unmapped' };
56
- }
57
- if (aliases.conflictingLegacyAliases.has(legacyAlias)) {
42
+ const aliases = loadAliasMap(selfAid);
43
+ const exact = formatContactAlias(canonicalChannelKey, actor);
44
+ if (aliases.conflictingAliases.has(exact))
58
45
  return { principalStatus: 'conflict' };
59
- }
60
- const legacyPrincipalId = aliases.legacyAliasToPrimary.get(legacyAlias);
61
- return legacyPrincipalId
62
- ? { principalId: legacyPrincipalId, principalStatus: 'mapped' }
63
- : { principalStatus: 'unmapped' };
46
+ const mapped = aliases.aliasToPrimary.get(exact);
47
+ if (mapped)
48
+ return { principalId: mapped, principalStatus: 'mapped' };
49
+ return { principalStatus: 'unmapped' };
64
50
  }
65
51
  catch {
66
52
  return { principalStatus: 'error' };
67
53
  }
68
54
  }
69
55
  export function resolvePrimaryId(selfAid, channelType, actorId, channelKey) {
70
- const normalizedActor = String(actorId || '').trim();
71
- return resolvePrincipal(selfAid, channelType, normalizedActor, channelKey).principalId ?? normalizedActor;
56
+ const actor = String(actorId || '').trim();
57
+ return resolvePrincipal(selfAid, channelType, actor, channelKey).principalId ?? actor;
72
58
  }
73
59
  export function bindContactAlias(selfAid, primaryId, channelType, actorId, channelKey) {
74
60
  const primary = normalizePrimaryId(primaryId);
75
- const alias = requireScopedAlias(selfAid, channelType, actorId, channelKey);
76
- const legacyAlias = requireLegacyAlias(channelType, actorId);
77
- ensureNativeAidIsNotRemapped(primary, channelType, actorId);
78
- const next = cloneContactBook(readContactBook(selfAid, { cache: true }));
79
- ensureScopedAliasAvailable(next, selfAid, primary, alias.lookupKey);
80
- const contacts = next.contacts ?? {};
81
- let removedLegacyAlias = false;
82
- // An explicit scoped bind supersedes every unscoped copy of the same alias.
83
- // Otherwise unbinding the scoped entry could silently reactivate a legacy
84
- // mapping owned by another principal.
85
- for (const [contactId, value] of Object.entries(contacts)) {
86
- const entry = normalizeContactEntry(value);
87
- const filtered = entry.aliases.filter(item => normalizeLegacyAliasString(item) !== legacyAlias);
88
- if (filtered.length !== entry.aliases.length) {
89
- contacts[contactId] = { ...entry, aliases: filtered };
90
- removedLegacyAlias = true;
61
+ const type = String(channelType || '').trim().toLowerCase();
62
+ const actor = String(actorId || '').trim();
63
+ if (type === 'aun') {
64
+ if (!isValidAid(actor) || actor !== primary) {
65
+ throw new ContactBookError('NATIVE_AID_REMAP_FORBIDDEN', `Native AID "${actor}" cannot be remapped to "${primary}"`);
91
66
  }
92
- }
93
- const current = normalizeContactEntry(contacts[primary]);
94
- const alreadyScoped = current.aliases.some(value => (normalizeScopedAliasString(value, selfAid)?.lookupKey === alias.lookupKey));
95
- if (alreadyScoped && !removedLegacyAlias) {
96
67
  return false;
97
68
  }
98
- contacts[primary] = {
99
- ...current,
100
- aliases: alreadyScoped
101
- ? current.aliases
102
- : [...current.aliases, alias.serialized],
103
- };
104
- next.contacts = contacts;
105
- write(ConfigTarget.Contact, next, { self: selfAid });
106
- return true;
69
+ const canonicalChannelKey = normalizeContactChannelKey(selfAid, type, channelKey);
70
+ if (!canonicalChannelKey || !actor) {
71
+ throw new ContactBookError('INVALID_ALIAS', `Invalid contact alias: ${String(channelKey)}:${String(actorId)}`);
72
+ }
73
+ const alias = formatContactAlias(canonicalChannelKey, actor);
74
+ try {
75
+ return mutateContactBookSync({
76
+ selfAid,
77
+ mutation: { type: 'bind-alias', primaryId: primary, alias },
78
+ }).changed;
79
+ }
80
+ catch (error) {
81
+ if (error instanceof ContactMutationError)
82
+ throw new ContactBookError(error.code, error.message);
83
+ throw error;
84
+ }
107
85
  }
108
86
  export function unbindContactAlias(selfAid, primaryId, channelType, actorId, channelKey) {
109
87
  const primary = normalizePrimaryId(primaryId);
110
- const alias = requireScopedAlias(selfAid, channelType, actorId, channelKey);
111
- const legacyAlias = requireLegacyAlias(channelType, actorId);
112
- const next = cloneContactBook(readContactBook(selfAid, { cache: true }));
113
- const contacts = next.contacts ?? {};
114
- let changed = false;
115
- // Remove the requested scoped binding from the target principal and purge
116
- // every unscoped copy of the same raw alias. Legacy aliases do not carry a
117
- // channel-instance identity, so retaining one on another contact would
118
- // silently reactivate that principal after this scoped binding is revoked.
119
- for (const [contactId, value] of Object.entries(contacts)) {
120
- const entry = normalizeContactEntry(value);
121
- const filtered = entry.aliases.filter(item => {
122
- const isTargetScoped = contactId.trim() === primary
123
- && normalizeScopedAliasString(item, selfAid)?.lookupKey === alias.lookupKey;
124
- const isLegacyCopy = normalizeLegacyAliasString(item) === legacyAlias;
125
- return !isTargetScoped && !isLegacyCopy;
126
- });
127
- if (filtered.length !== entry.aliases.length) {
128
- contacts[contactId] = { ...entry, aliases: filtered };
129
- changed = true;
130
- }
88
+ const type = String(channelType || '').trim().toLowerCase();
89
+ const actor = String(actorId || '').trim();
90
+ const canonicalChannelKey = normalizeContactChannelKey(selfAid, type, channelKey);
91
+ if (!canonicalChannelKey || type === 'aun' || !actor) {
92
+ throw new ContactBookError('INVALID_ALIAS', `Invalid contact alias: ${String(channelKey)}:${String(actorId)}`);
131
93
  }
132
- if (!changed) {
133
- return false;
94
+ const alias = formatContactAlias(canonicalChannelKey, actor);
95
+ try {
96
+ return mutateContactBookSync({
97
+ selfAid,
98
+ mutation: { type: 'unbind-alias', primaryId: primary, alias },
99
+ }).changed;
100
+ }
101
+ catch (error) {
102
+ if (error instanceof ContactMutationError)
103
+ throw new ContactBookError(error.code, error.message);
104
+ throw error;
134
105
  }
135
- next.contacts = contacts;
136
- write(ConfigTarget.Contact, next, { self: selfAid });
137
- return true;
138
106
  }
139
107
  export function clearContactBookCache() {
140
108
  aliasMapCache = new WeakMap();
109
+ snapshotByBook = new WeakMap();
110
+ }
111
+ export function getContactSnapshot(selfAid) {
112
+ const book = readContactBook(selfAid, { cache: true }) ?? { $schema_version: 2, contacts: {} };
113
+ const cached = snapshotByBook.get(book);
114
+ if (cached)
115
+ return cached;
116
+ const errors = validateConfig(ConfigTarget.Contact, book);
117
+ if (errors.length) {
118
+ throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact book for ${selfAid}: ${errors.join('; ')}`);
119
+ }
120
+ const aliases = buildAliasMap(book, selfAid);
121
+ if (aliases.conflictingAliases.size) {
122
+ throw new ContactBookError('ALIAS_CONFLICT', `Conflicting contact aliases: ${[...aliases.conflictingAliases].join(', ')}`);
123
+ }
124
+ const snapshot = buildSnapshot(book, selfAid, aliases);
125
+ snapshotByBook.set(book, snapshot);
126
+ return snapshot;
127
+ }
128
+ export function resolveContactView(selfAid, primaryId) {
129
+ const id = String(primaryId || '').trim();
130
+ const owner = listAgentOwners(selfAid).includes(id);
131
+ const entry = getContactSnapshot(selfAid).contacts.get(id);
132
+ const status = owner ? 'active' : entry?.status ?? 'active';
133
+ return {
134
+ primaryId: id,
135
+ ...(entry?.displayName ? { displayName: entry.displayName } : {}),
136
+ aliases: entry?.aliases ?? [],
137
+ status,
138
+ blocked: status === 'blocked',
139
+ isOwner: owner,
140
+ source: owner ? 'owner' : entry ? 'contact' : 'none',
141
+ };
142
+ }
143
+ export function isBlockedContact(selfAid, primaryId) {
144
+ return resolveContactView(selfAid, primaryId).blocked;
145
+ }
146
+ function buildSnapshot(book, selfAid, aliases) {
147
+ const contacts = new Map();
148
+ const blockedIndex = new Set();
149
+ const owners = new Set(listAgentOwners(selfAid));
150
+ for (const [primaryId, rawEntry] of Object.entries(book.contacts)) {
151
+ if (!isValidAid(primaryId) || isExplicitGroupId(primaryId)) {
152
+ throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact AID: ${primaryId}`);
153
+ }
154
+ if (rawEntry.status === 'blocked' && owners.has(primaryId)) {
155
+ throw new ContactBookError('INVALID_CONTACT_BOOK', `Owner contact ${primaryId} cannot be blocked`);
156
+ }
157
+ const entry = Object.freeze({
158
+ ...rawEntry,
159
+ ...(rawEntry.aliases ? { aliases: Object.freeze([...rawEntry.aliases]) } : {}),
160
+ });
161
+ contacts.set(primaryId, entry);
162
+ if (entry.status === 'blocked')
163
+ blockedIndex.add(primaryId);
164
+ }
165
+ return Object.freeze({
166
+ contacts,
167
+ aliasIndex: aliases.aliasToPrimary,
168
+ conflictingAliases: aliases.conflictingAliases,
169
+ blockedIndex,
170
+ contactRevision: contactRevision(book),
171
+ });
141
172
  }
142
173
  function loadAliasMap(selfAid) {
143
174
  const book = readContactBook(selfAid, { cache: true });
144
175
  if (!book)
145
- return emptyAliasMap;
176
+ return EMPTY_ALIAS_MAP;
146
177
  const cached = aliasMapCache.get(book);
147
178
  if (cached)
148
179
  return cached;
149
- const validationErrors = validateConfig(ConfigTarget.Contact, book);
150
- if (validationErrors.length > 0) {
151
- throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact book for ${selfAid}: ${validationErrors.join('; ')}`);
180
+ const errors = validateConfig(ConfigTarget.Contact, book);
181
+ if (errors.length) {
182
+ throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact book for ${selfAid}: ${errors.join('; ')}`);
152
183
  }
153
184
  const built = buildAliasMap(book, selfAid);
154
185
  aliasMapCache.set(book, built);
155
186
  return built;
156
187
  }
157
- function buildAliasMap(config, selfAid) {
158
- const scopedAliasToPrimary = new Map();
159
- const conflictingScopedAliases = new Set();
160
- const legacyAliasToPrimary = new Map();
161
- const conflictingLegacyAliases = new Set();
162
- const contacts = config?.contacts;
163
- if (!contacts || typeof contacts !== 'object' || Array.isArray(contacts)) {
164
- return {
165
- scopedAliasToPrimary,
166
- conflictingScopedAliases,
167
- legacyAliasToPrimary,
168
- conflictingLegacyAliases,
169
- };
170
- }
171
- for (const [rawPrimaryId, entry] of Object.entries(contacts)) {
172
- const primaryId = rawPrimaryId.trim();
173
- if (!isValidAid(primaryId) || !entry || typeof entry !== 'object' || Array.isArray(entry))
188
+ function buildAliasMap(book, selfAid) {
189
+ const aliasToPrimary = new Map();
190
+ const conflictingAliases = new Set();
191
+ for (const [primaryId, entry] of Object.entries(book?.contacts ?? {})) {
192
+ if (!isValidAid(primaryId) || isExplicitGroupId(primaryId) || !entry || typeof entry !== 'object')
174
193
  continue;
175
- const aliases = Array.isArray(entry.aliases) ? entry.aliases : [];
176
- for (const rawAlias of aliases) {
177
- const scoped = normalizeScopedAliasString(rawAlias, selfAid);
178
- if (scoped) {
179
- addAliasMapping(scopedAliasToPrimary, conflictingScopedAliases, scoped.lookupKey, primaryId);
194
+ for (const rawAlias of entry.aliases ?? []) {
195
+ const parsed = parseContactAlias(rawAlias, selfAid);
196
+ if (!parsed) {
197
+ throw new ContactBookError('INVALID_CONTACT_BOOK', `Invalid contact alias for ${selfAid}: ${String(rawAlias)}`);
198
+ }
199
+ const alias = parsed.alias;
200
+ if (conflictingAliases.has(alias))
201
+ continue;
202
+ const existing = aliasToPrimary.get(alias);
203
+ if (existing && existing !== primaryId) {
204
+ aliasToPrimary.delete(alias);
205
+ conflictingAliases.add(alias);
180
206
  }
181
207
  else {
182
- const legacy = normalizeLegacyAliasString(rawAlias);
183
- if (legacy) {
184
- addAliasMapping(legacyAliasToPrimary, conflictingLegacyAliases, legacy, primaryId);
185
- }
208
+ aliasToPrimary.set(alias, primaryId);
186
209
  }
187
210
  }
188
211
  }
189
- return {
190
- scopedAliasToPrimary,
191
- conflictingScopedAliases,
192
- legacyAliasToPrimary,
193
- conflictingLegacyAliases,
194
- };
195
- }
196
- function addAliasMapping(aliases, conflicts, alias, primaryId) {
197
- if (conflicts.has(alias))
198
- return;
199
- const existing = aliases.get(alias);
200
- if (existing && existing !== primaryId) {
201
- aliases.delete(alias);
202
- conflicts.add(alias);
203
- return;
204
- }
205
- aliases.set(alias, primaryId);
206
- }
207
- function cloneContactBook(config) {
208
- const next = config ? { ...config } : {};
209
- const contacts = {};
210
- const rawContacts = config?.contacts;
211
- if (rawContacts && typeof rawContacts === 'object' && !Array.isArray(rawContacts)) {
212
- for (const [primaryId, entry] of Object.entries(rawContacts)) {
213
- contacts[primaryId] = normalizeContactEntry(entry);
214
- }
215
- }
216
- next.contacts = contacts;
217
- return next;
218
- }
219
- function normalizeContactEntry(value) {
220
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
221
- return { aliases: [] };
222
- }
223
- const entry = value;
224
- return {
225
- ...entry,
226
- aliases: Array.isArray(entry.aliases)
227
- ? entry.aliases.filter((item) => typeof item === 'string')
228
- : [],
229
- };
230
- }
231
- function ensureScopedAliasAvailable(config, selfAid, primaryId, alias) {
232
- const owners = findScopedAliasOwners(config, selfAid, alias);
233
- if (owners.size === 0)
234
- return;
235
- if (owners.size === 1 && owners.has(primaryId))
236
- return;
237
- throw new ContactBookError('ALIAS_CONFLICT', `Alias "${alias}" is already bound to ${[...owners].join(', ')}`);
238
- }
239
- function findScopedAliasOwners(config, selfAid, alias) {
240
- const owners = new Set();
241
- const contacts = config.contacts;
242
- if (!contacts || typeof contacts !== 'object' || Array.isArray(contacts))
243
- return owners;
244
- for (const [rawPrimaryId, entry] of Object.entries(contacts)) {
245
- const primaryId = rawPrimaryId.trim();
246
- if (!primaryId || !entry || typeof entry !== 'object' || Array.isArray(entry))
247
- continue;
248
- const aliases = Array.isArray(entry.aliases) ? entry.aliases : [];
249
- if (aliases.some(value => normalizeScopedAliasString(value, selfAid)?.lookupKey === alias)) {
250
- owners.add(primaryId);
251
- }
252
- }
253
- return owners;
212
+ return { aliasToPrimary, conflictingAliases };
254
213
  }
255
214
  function normalizePrimaryId(value) {
256
- const primaryId = String(value || '').trim();
257
- if (!isValidAid(primaryId)) {
258
- throw new ContactBookError('INVALID_PRIMARY_ID', `Invalid contact primary AID: ${String(value)}`);
259
- }
260
- return primaryId;
261
- }
262
- function requireLegacyAlias(channelType, actorId) {
263
- const alias = normalizeLegacyAlias(channelType, actorId);
264
- if (!alias) {
265
- throw new ContactBookError('INVALID_ALIAS', `Invalid contact alias: ${String(channelType)}:${String(actorId)}`);
266
- }
267
- return alias;
268
- }
269
- function normalizeLegacyAlias(channelType, actorId) {
270
- const channel = String(channelType || '').trim().toLowerCase();
271
- const id = String(actorId || '').trim();
272
- if (!/^[a-z0-9_-]+$/.test(channel) || !id)
273
- return null;
274
- return `${channel}:${id}`;
275
- }
276
- function normalizeLegacyAliasString(value) {
277
- if (typeof value !== 'string')
278
- return null;
279
- const trimmed = value.trim();
280
- if (trimmed.startsWith(SCOPED_ALIAS_PREFIX))
281
- return null;
282
- const idx = trimmed.indexOf(':');
283
- if (idx <= 0 || idx === trimmed.length - 1)
284
- return null;
285
- return normalizeLegacyAlias(trimmed.slice(0, idx), trimmed.slice(idx + 1));
286
- }
287
- function requireScopedAlias(selfAid, channelType, actorId, channelKey) {
288
- const normalizedActor = String(actorId || '').trim();
289
- const normalizedChannel = String(channelType || '').trim().toLowerCase();
290
- const normalizedChannelKey = normalizeChannelKey(selfAid, normalizedChannel, channelKey);
291
- if (!normalizedActor || !normalizedChannelKey) {
292
- throw new ContactBookError('INVALID_CHANNEL_ALIAS', `Invalid scoped contact alias: ${String(channelKey)}:${String(actorId)}`);
293
- }
294
- return {
295
- lookupKey: scopedAliasLookupKey(normalizedChannelKey, normalizedActor),
296
- serialized: serializeScopedAlias(normalizedChannelKey, normalizedActor),
297
- };
298
- }
299
- function normalizeScopedAliasString(value, selfAid) {
300
- if (typeof value !== 'string')
301
- return null;
302
- const trimmed = value.trim();
303
- if (!trimmed.startsWith(SCOPED_ALIAS_PREFIX))
304
- return null;
305
- const encoded = trimmed.slice(SCOPED_ALIAS_PREFIX.length);
306
- const separator = encoded.indexOf(':');
307
- if (separator <= 0 || separator === encoded.length - 1)
308
- return null;
309
- try {
310
- const channelKey = decodeURIComponent(encoded.slice(0, separator));
311
- const actorId = decodeURIComponent(encoded.slice(separator + 1)).trim();
312
- const channelType = channelKey.split('#', 1)[0]?.toLowerCase() ?? '';
313
- const normalizedChannelKey = normalizeChannelKey(selfAid, channelType, channelKey);
314
- if (!normalizedChannelKey || !actorId)
315
- return null;
316
- return {
317
- lookupKey: scopedAliasLookupKey(normalizedChannelKey, actorId),
318
- serialized: serializeScopedAlias(normalizedChannelKey, actorId),
319
- };
320
- }
321
- catch {
322
- return null;
323
- }
324
- }
325
- function serializeScopedAlias(channelKey, actorId) {
326
- return `${SCOPED_ALIAS_PREFIX}${encodeURIComponent(channelKey)}:${encodeURIComponent(actorId)}`;
327
- }
328
- function scopedAliasLookupKey(channelKey, actorId) {
329
- return JSON.stringify([channelKey, actorId]);
330
- }
331
- function normalizeChannelKey(selfAid, channelType, value) {
332
- const key = typeof value === 'string' ? value.trim() : '';
333
- const parts = key.split('#');
334
- if (parts.length !== 3)
335
- return null;
336
- const [rawType, keySelfAid, name] = parts;
337
- const normalizedType = rawType.toLowerCase();
338
- if (!/^[a-z0-9_-]+$/.test(normalizedType)
339
- || normalizedType !== channelType
340
- || keySelfAid !== selfAid
341
- || !name) {
342
- return null;
215
+ const id = String(value || '').trim();
216
+ if (!isValidAid(id) || isExplicitGroupId(id)) {
217
+ throw new ContactBookError('INVALID_PRIMARY_ID', `Invalid individual contact AID: ${String(value)}`);
343
218
  }
344
- return `${normalizedType}#${keySelfAid}#${name}`;
219
+ return id;
345
220
  }
346
- function isUnambiguousLegacyChannel(selfAid, channelType, channelKey) {
347
- try {
348
- const agent = read(ConfigTarget.Agent, { self: selfAid }, { cache: true });
349
- const matches = new Set();
350
- for (const candidate of agent?.channels ?? []) {
351
- if (!candidate || typeof candidate !== 'object')
352
- continue;
353
- const candidateType = String(candidate.type || '').trim().toLowerCase();
354
- const candidateName = String(candidate.name || '').trim();
355
- if (candidateType !== channelType || !candidateName || candidateName.includes('#'))
356
- continue;
357
- matches.add(`${candidateType}#${selfAid}#${candidateName}`);
358
- }
359
- return matches.size === 1 && matches.has(channelKey);
360
- }
361
- catch {
362
- return false;
363
- }
364
- }
365
- function ensureNativeAidIsNotRemapped(primaryId, channelType, actorId) {
366
- const channel = String(channelType || '').trim().toLowerCase();
367
- const actor = String(actorId || '').trim();
368
- if (channel === 'aun' && isValidAid(actor) && actor !== primaryId) {
369
- throw new ContactBookError('NATIVE_AID_REMAP_FORBIDDEN', `Native AID "${actor}" cannot be remapped to "${primaryId}"`);
370
- }
221
+ function listAgentOwners(selfAid) {
222
+ const agent = read(ConfigTarget.Agent, { self: selfAid }, { cache: true });
223
+ return [...new Set((agent?.owners ?? []).map(value => String(value || '').trim()).filter(Boolean))];
371
224
  }
@@ -0,0 +1,110 @@
1
+ import fs from 'fs';
2
+ import { isValidAid } from '../aun/aid/validation.js';
3
+ import { isExplicitGroupId } from '../aun/group-identity.js';
4
+ import { agentConfig } from '../paths.js';
5
+ import { getContactSnapshot, resolveContactView } from './contact-book.js';
6
+ import { ContactMutationError, mutateContactBook } from './contact-book-store.js';
7
+ export function resolveContactCommand(argv) {
8
+ const args = argv[0] === 'contact' ? argv.slice(1) : [...argv];
9
+ const selfAid = String(argValue(args, '--self') || '').trim();
10
+ if (!isValidAid(selfAid))
11
+ return invalid('INVALID_SELF', '--self must be a valid Agent AID');
12
+ const positional = stripOptions(args);
13
+ const [command, subcommand, ...extra] = positional;
14
+ if (extra.length > 0)
15
+ return invalid('INVALID_ARGUMENT', `Unexpected contact arguments: ${extra.join(' ')}`);
16
+ if (command === 'list') {
17
+ if (subcommand !== undefined || !args.includes('--blocked')) {
18
+ return invalid('INVALID_ARGUMENT', 'Only ec contact list --blocked is currently supported');
19
+ }
20
+ return {
21
+ ok: true,
22
+ command: { kind: 'list-blocked', operationId: 'contact.read', selfAid, canonicalArgv: canonicalArgv(argv) },
23
+ };
24
+ }
25
+ if (command === 'block' || command === 'unblock') {
26
+ if (!isValidAid(subcommand || '') || isExplicitGroupId(subcommand || '')) {
27
+ return invalid('INVALID_AID', 'primaryId must be a valid individual AID');
28
+ }
29
+ return {
30
+ ok: true,
31
+ command: {
32
+ kind: 'set-blocked',
33
+ operationId: 'contact.block',
34
+ selfAid,
35
+ primaryId: subcommand,
36
+ blocked: command === 'block',
37
+ canonicalArgv: canonicalArgv(argv),
38
+ },
39
+ };
40
+ }
41
+ return invalid('UNKNOWN_COMMAND', `Unknown contact command: ${positional.join(' ')}`);
42
+ }
43
+ export async function executeResolvedContactCommand(command, actor = 'local-cli') {
44
+ if (!fs.existsSync(agentConfig(command.selfAid))) {
45
+ return { ok: false, code: 'AGENT_NOT_FOUND', error: `Agent does not exist: ${command.selfAid}` };
46
+ }
47
+ try {
48
+ if (command.kind === 'list-blocked') {
49
+ const snapshot = getContactSnapshot(command.selfAid);
50
+ return {
51
+ ok: true,
52
+ operation: 'list-blocked',
53
+ selfAid: command.selfAid,
54
+ contactRevision: snapshot.contactRevision,
55
+ items: [...snapshot.blockedIndex].sort().map(id => resolveContactView(command.selfAid, id)),
56
+ };
57
+ }
58
+ if (command.kind === 'set-blocked') {
59
+ const result = await mutateContactBook({
60
+ selfAid: command.selfAid,
61
+ actor,
62
+ mutation: {
63
+ type: 'set-status',
64
+ primaryId: command.primaryId,
65
+ status: command.blocked ? 'blocked' : 'active',
66
+ },
67
+ });
68
+ return {
69
+ ok: true,
70
+ operation: 'set-blocked',
71
+ selfAid: command.selfAid,
72
+ primaryId: command.primaryId,
73
+ blocked: command.blocked,
74
+ changed: result.changed,
75
+ contactRevision: result.contactRevision,
76
+ };
77
+ }
78
+ return { ok: false, code: 'CONTACT_OPERATION_FAILED', error: 'Unsupported contact operation' };
79
+ }
80
+ catch (error) {
81
+ if (error instanceof ContactMutationError) {
82
+ return { ok: false, code: error.code, error: error.message, ...(error.data ? { data: error.data } : {}) };
83
+ }
84
+ return { ok: false, code: 'CONTACT_OPERATION_FAILED', error: error instanceof Error ? error.message : String(error) };
85
+ }
86
+ }
87
+ function argValue(args, flag) {
88
+ const index = args.indexOf(flag);
89
+ return index >= 0 ? args[index + 1] : undefined;
90
+ }
91
+ function stripOptions(args) {
92
+ const result = [];
93
+ for (let index = 0; index < args.length; index += 1) {
94
+ const arg = args[index];
95
+ if (arg === '--self' || arg === '--format') {
96
+ index += 1;
97
+ continue;
98
+ }
99
+ if (arg === '--blocked')
100
+ continue;
101
+ result.push(arg);
102
+ }
103
+ return result;
104
+ }
105
+ function canonicalArgv(argv) {
106
+ return argv[0] === 'contact' ? [...argv] : ['contact', ...argv];
107
+ }
108
+ function invalid(code, reason) {
109
+ return { ok: false, code, reason };
110
+ }