aegiscode 3.1.8 → 3.1.10

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 (206) hide show
  1. package/README.md +23 -15
  2. package/dist/main.js +549 -552
  3. package/package.json +4 -2
  4. package/src/agent/Agent.ts +903 -0
  5. package/src/agent/SimpleAgent.ts +48 -0
  6. package/src/agent/index.ts +54 -0
  7. package/src/agent/orchestrator/AppBuilder.ts +443 -0
  8. package/src/agent/orchestrator/CouncilAgent.ts +310 -0
  9. package/src/agent/orchestrator/DiscussionRoom.ts +462 -0
  10. package/src/agent/orchestrator/OrchestratorAgent.ts +637 -0
  11. package/src/agent/orchestrator/index.ts +38 -0
  12. package/src/agent/orchestrator/utils.ts +397 -0
  13. package/src/agent/pricing.ts +115 -0
  14. package/src/agent/router.ts +74 -0
  15. package/src/agent/routerStats.ts +121 -0
  16. package/src/agent/types.ts +318 -0
  17. package/src/auth/login.ts +383 -0
  18. package/src/cli/config.ts +189 -0
  19. package/src/cli/index.ts +17 -0
  20. package/src/cli/middleware.ts +119 -0
  21. package/src/cli/types.ts +75 -0
  22. package/src/config/ConfigManager.ts +587 -0
  23. package/src/config/index.ts +7 -0
  24. package/src/config/types.ts +584 -0
  25. package/src/context/CompactionService.ts +300 -0
  26. package/src/context/ContextManager.ts +450 -0
  27. package/src/context/FileAnalyzer.ts +267 -0
  28. package/src/context/TokenCounter.ts +265 -0
  29. package/src/context/index.ts +27 -0
  30. package/src/context/storage/CacheStore.ts +176 -0
  31. package/src/context/storage/JSONLStore.ts +201 -0
  32. package/src/context/storage/MemoryStore.ts +205 -0
  33. package/src/context/storage/PersistentStore.ts +327 -0
  34. package/src/context/storage/index.ts +9 -0
  35. package/src/context/storage/pathUtils.ts +114 -0
  36. package/src/context/test.ts +309 -0
  37. package/src/context/types.ts +268 -0
  38. package/src/hooks/HookExecutor.ts +434 -0
  39. package/src/hooks/HookManager.ts +596 -0
  40. package/src/hooks/HookService.ts +269 -0
  41. package/src/hooks/Matcher.ts +157 -0
  42. package/src/hooks/index.ts +63 -0
  43. package/src/hooks/types.ts +424 -0
  44. package/src/main.tsx +596 -0
  45. package/src/mcp/HealthMonitor.ts +150 -0
  46. package/src/mcp/McpClient.ts +491 -0
  47. package/src/mcp/McpRegistry.ts +321 -0
  48. package/src/mcp/createMcpTool.ts +251 -0
  49. package/src/mcp/index.ts +15 -0
  50. package/src/mcp/server.ts +334 -0
  51. package/src/mcp/test-server.ts +88 -0
  52. package/src/mcp/test.ts +372 -0
  53. package/src/mcp/types.ts +247 -0
  54. package/src/memory/AgentMemoryBus.ts +432 -0
  55. package/src/memory/CloudSync.ts +99 -0
  56. package/src/memory/DriveSync.ts +106 -0
  57. package/src/memory/SharedMemory.ts +951 -0
  58. package/src/memory/index.ts +14 -0
  59. package/src/memory/machineFingerprint.ts +40 -0
  60. package/src/orchestrator/SubAgentMetadata.ts +136 -0
  61. package/src/prompts/builder.ts +213 -0
  62. package/src/prompts/default.ts +144 -0
  63. package/src/prompts/index.ts +16 -0
  64. package/src/prompts/plan.ts +64 -0
  65. package/src/prompts/test.ts +78 -0
  66. package/src/services/AnthropicChatService.ts +341 -0
  67. package/src/services/ChatService.ts +347 -0
  68. package/src/services/ClaudeCliChatService.ts +256 -0
  69. package/src/services/CloudSync.ts +168 -0
  70. package/src/services/CostLedger.ts +211 -0
  71. package/src/services/Heartbeat.ts +135 -0
  72. package/src/services/LearningCollector.ts +291 -0
  73. package/src/services/OllamaInstaller.ts +342 -0
  74. package/src/services/VersionChecker.ts +445 -0
  75. package/src/services/index.ts +57 -0
  76. package/src/services/streaming/OpenAIEventAdapter.ts +126 -0
  77. package/src/services/streaming/RenderingProfile.ts +90 -0
  78. package/src/services/streaming/StreamEventParser.ts +181 -0
  79. package/src/services/streaming/ThrottledRenderer.ts +139 -0
  80. package/src/services/streaming/TranscriptBuffer.ts +574 -0
  81. package/src/services/streaming/eventStatusMap.ts +52 -0
  82. package/src/services/streaming/index.ts +46 -0
  83. package/src/services/streaming/renderFormatting.ts +79 -0
  84. package/src/services/streaming/types.ts +234 -0
  85. package/src/skills/SkillLoader.ts +126 -0
  86. package/src/skills/SkillRegistry.ts +366 -0
  87. package/src/skills/index.ts +48 -0
  88. package/src/skills/types.ts +146 -0
  89. package/src/slash-commands/billing.ts +70 -0
  90. package/src/slash-commands/build.ts +413 -0
  91. package/src/slash-commands/builtinCommands.ts +2733 -0
  92. package/src/slash-commands/clone.ts +242 -0
  93. package/src/slash-commands/council.ts +125 -0
  94. package/src/slash-commands/custom/CustomCommandExecutor.ts +143 -0
  95. package/src/slash-commands/custom/CustomCommandLoader.ts +251 -0
  96. package/src/slash-commands/custom/CustomCommandRegistry.ts +144 -0
  97. package/src/slash-commands/custom/index.ts +7 -0
  98. package/src/slash-commands/debate.ts +254 -0
  99. package/src/slash-commands/gmail.ts +105 -0
  100. package/src/slash-commands/index.ts +388 -0
  101. package/src/slash-commands/mcpCommand.ts +205 -0
  102. package/src/slash-commands/types.ts +201 -0
  103. package/src/store/index.ts +76 -0
  104. package/src/store/selectors.ts +246 -0
  105. package/src/store/slices/appSlice.ts +205 -0
  106. package/src/store/slices/commandSlice.ts +115 -0
  107. package/src/store/slices/configSlice.ts +46 -0
  108. package/src/store/slices/focusSlice.ts +64 -0
  109. package/src/store/slices/index.ts +9 -0
  110. package/src/store/slices/sessionSlice.ts +424 -0
  111. package/src/store/streaming-buffer.ts +425 -0
  112. package/src/store/test.ts +296 -0
  113. package/src/store/types.ts +274 -0
  114. package/src/store/vanilla.ts +186 -0
  115. package/src/tools/builtin/bash.ts +236 -0
  116. package/src/tools/builtin/council.ts +105 -0
  117. package/src/tools/builtin/edit.ts +213 -0
  118. package/src/tools/builtin/glob.ts +136 -0
  119. package/src/tools/builtin/grep.ts +263 -0
  120. package/src/tools/builtin/index.ts +61 -0
  121. package/src/tools/builtin/memory.ts +66 -0
  122. package/src/tools/builtin/read.ts +168 -0
  123. package/src/tools/builtin/skill.ts +97 -0
  124. package/src/tools/builtin/snapshot.ts +40 -0
  125. package/src/tools/builtin/task.ts +106 -0
  126. package/src/tools/builtin/write.ts +134 -0
  127. package/src/tools/createTool.ts +221 -0
  128. package/src/tools/execution/ExecutionPipeline.ts +263 -0
  129. package/src/tools/execution/index.ts +40 -0
  130. package/src/tools/execution/stages/CacheStage.ts +131 -0
  131. package/src/tools/execution/stages/ConfirmationStage.ts +203 -0
  132. package/src/tools/execution/stages/DiscoveryStage.ts +46 -0
  133. package/src/tools/execution/stages/ExecutionStage.ts +48 -0
  134. package/src/tools/execution/stages/FormattingStage.ts +44 -0
  135. package/src/tools/execution/stages/HookStage.ts +72 -0
  136. package/src/tools/execution/stages/PermissionStage.ts +287 -0
  137. package/src/tools/execution/stages/PostHookStage.ts +71 -0
  138. package/src/tools/execution/stages/index.ts +12 -0
  139. package/src/tools/execution/test.ts +266 -0
  140. package/src/tools/execution/types.ts +273 -0
  141. package/src/tools/index.ts +81 -0
  142. package/src/tools/registry.ts +304 -0
  143. package/src/tools/schemas.ts +109 -0
  144. package/src/tools/test.ts +220 -0
  145. package/src/tools/types.ts +175 -0
  146. package/src/tools/validation/PermissionChecker.ts +242 -0
  147. package/src/tools/validation/SensitiveFileDetector.ts +210 -0
  148. package/src/tools/validation/index.ts +11 -0
  149. package/src/ui/App.tsx +166 -0
  150. package/src/ui/components/AegisInterface.tsx +484 -0
  151. package/src/ui/components/common/ChatSearch.tsx +150 -0
  152. package/src/ui/components/common/ErrorBoundary.tsx +82 -0
  153. package/src/ui/components/common/ExitMessage.tsx +120 -0
  154. package/src/ui/components/common/LoadingIndicator.tsx +49 -0
  155. package/src/ui/components/common/index.ts +6 -0
  156. package/src/ui/components/dialog/ConfirmationPrompt.tsx +208 -0
  157. package/src/ui/components/dialog/InteractiveSelector.tsx +149 -0
  158. package/src/ui/components/dialog/SetupWizard.tsx +297 -0
  159. package/src/ui/components/dialog/UpdatePrompt.tsx +155 -0
  160. package/src/ui/components/dialog/index.ts +8 -0
  161. package/src/ui/components/index.ts +28 -0
  162. package/src/ui/components/input/CommandSuggestions.tsx +139 -0
  163. package/src/ui/components/input/CustomTextInput.tsx +220 -0
  164. package/src/ui/components/input/InputArea.tsx +361 -0
  165. package/src/ui/components/input/PromptSuggestions.tsx +66 -0
  166. package/src/ui/components/input/index.ts +6 -0
  167. package/src/ui/components/layout/ChatStatusBar.tsx +90 -0
  168. package/src/ui/components/layout/ContextBar.tsx +79 -0
  169. package/src/ui/components/layout/MessageArea.tsx +96 -0
  170. package/src/ui/components/layout/MessageList.tsx +647 -0
  171. package/src/ui/components/layout/MessageSeparator.tsx +26 -0
  172. package/src/ui/components/layout/WelcomeMessage.tsx +93 -0
  173. package/src/ui/components/layout/index.ts +7 -0
  174. package/src/ui/components/markdown/CodeHighlighter.tsx +292 -0
  175. package/src/ui/components/markdown/MessageRenderer.tsx +1211 -0
  176. package/src/ui/components/markdown/index.ts +8 -0
  177. package/src/ui/components/markdown/parser.ts +336 -0
  178. package/src/ui/components/markdown/types.ts +66 -0
  179. package/src/ui/focus/FocusManager.ts +137 -0
  180. package/src/ui/focus/index.ts +13 -0
  181. package/src/ui/focus/types.ts +54 -0
  182. package/src/ui/focus/useFocus.ts +75 -0
  183. package/src/ui/hooks/index.ts +11 -0
  184. package/src/ui/hooks/useAgent.ts +284 -0
  185. package/src/ui/hooks/useCommandHistory.ts +87 -0
  186. package/src/ui/hooks/useCommandProcessor.ts +443 -0
  187. package/src/ui/hooks/useConfirmation.ts +99 -0
  188. package/src/ui/hooks/useCtrlCHandler.ts +100 -0
  189. package/src/ui/hooks/useInputBuffer.ts +122 -0
  190. package/src/ui/hooks/useTerminalSize.ts +68 -0
  191. package/src/ui/hooks/useTerminalWidth.ts +5 -0
  192. package/src/ui/hooks/useWindowedList.ts +118 -0
  193. package/src/ui/render-debugger.ts +621 -0
  194. package/src/ui/test.ts +189 -0
  195. package/src/ui/themes/ThemeManager.ts +332 -0
  196. package/src/ui/themes/aegisTheme.ts +87 -0
  197. package/src/ui/themes/darkTheme.ts +87 -0
  198. package/src/ui/themes/defaultTheme.ts +85 -0
  199. package/src/ui/themes/index.ts +10 -0
  200. package/src/ui/themes/lightTheme.ts +89 -0
  201. package/src/ui/themes/popularThemes.ts +187 -0
  202. package/src/ui/themes/types.ts +130 -0
  203. package/src/utils/clipboard.ts +48 -0
  204. package/src/utils/debug.ts +43 -0
  205. package/src/utils/environment.ts +68 -0
  206. package/src/utils/index.ts +10 -0
@@ -0,0 +1,432 @@
1
+ /**
2
+ * AgentMemoryBus — Shared memory bus for multi-agent communication
3
+ *
4
+ * Purpose:
5
+ * Allows multiple agents (Orchestrator sub-agents, Council members)
6
+ * to read/write shared contextual memory in real-time.
7
+ *
8
+ * Design:
9
+ * - In-memory fast cache for active session + SQLite persistence
10
+ * - Channels: each agent publishes to typed channels
11
+ * - Sub-agents automatically get relevant context from prior agents
12
+ * - Supports TTL-based expiration, importance scoring, cross-referencing
13
+ *
14
+ * Integration with existing SharedMemory:
15
+ * AgentMemoryBus uses SharedMemory as its persistent backing store,
16
+ * adding a lightweight pub/sub layer on top for multi-agent coordination.
17
+ */
18
+
19
+ import { sharedMemory, type MemoryEntry } from './SharedMemory.js';
20
+ import { v4 as uuid } from 'uuid';
21
+ import { agentDebug } from '../utils/debug.js';
22
+
23
+ // ── Types ────────────────────────────────────────────────────────────────────
24
+
25
+ export type AgentChannel =
26
+ | 'decision' // Agent decisions / conclusions
27
+ | 'fact' // Facts discovered during work
28
+ | 'context' // Contextual information (files, code, env)
29
+ | 'intermediate' // Intermediate results
30
+ | 'error' // Errors encountered
31
+ | 'question' // Questions to other agents
32
+ | 'suggestion'; // Suggestions for next steps
33
+
34
+ export interface AgentMemoryMessage {
35
+ id: string;
36
+ channel: AgentChannel;
37
+ sourceAgent: string;
38
+ targetAgent?: string; // If specified, only this agent should consume
39
+ sessionId: string;
40
+ content: string;
41
+ timestamp: string;
42
+ importance: number;
43
+ references?: string[]; // IDs of related messages
44
+ ttl?: number; // Seconds until auto-expire (0 = forever)
45
+ tags?: string[];
46
+ metadata?: Record<string, unknown>;
47
+ }
48
+
49
+ export interface AgentMemoryQuery {
50
+ channels?: AgentChannel[];
51
+ sourceAgent?: string;
52
+ targetAgent?: string;
53
+ sessionId?: string;
54
+ query?: string; // Free-text search
55
+ limit?: number;
56
+ minImportance?: number;
57
+ maxAgeSeconds?: number;
58
+ }
59
+
60
+ // ── In-memory fast cache ─────────────────────────────────────────────────────
61
+
62
+ interface CacheEntry {
63
+ msg: AgentMemoryMessage;
64
+ expiresAt: number; // 0 = never
65
+ }
66
+
67
+ class MemoryCache {
68
+ private cache = new Map<string, CacheEntry>();
69
+ private byChannel = new Map<AgentChannel, Set<string>>();
70
+ private bySession = new Map<string, Set<string>>();
71
+ private bySource = new Map<string, Set<string>>();
72
+
73
+ put(msg: AgentMemoryMessage): void {
74
+ const expiresAt = msg.ttl && msg.ttl > 0 ? Date.now() + msg.ttl * 1000 : 0;
75
+ this.cache.set(msg.id, { msg, expiresAt });
76
+
77
+ if (!this.byChannel.has(msg.channel)) this.byChannel.set(msg.channel, new Set());
78
+ this.byChannel.get(msg.channel)!.add(msg.id);
79
+
80
+ if (!this.bySession.has(msg.sessionId)) this.bySession.set(msg.sessionId, new Set());
81
+ this.bySession.get(msg.sessionId)!.add(msg.id);
82
+
83
+ if (!this.bySource.has(msg.sourceAgent)) this.bySource.set(msg.sourceAgent, new Set());
84
+ this.bySource.get(msg.sourceAgent)!.add(msg.id);
85
+ }
86
+
87
+ get(id: string): AgentMemoryMessage | null {
88
+ const entry = this.cache.get(id);
89
+ if (!entry) return null;
90
+ if (entry.expiresAt > 0 && Date.now() > entry.expiresAt) {
91
+ this.cache.delete(id);
92
+ return null;
93
+ }
94
+ return entry.msg;
95
+ }
96
+
97
+ query(q: AgentMemoryQuery): AgentMemoryMessage[] {
98
+ this.evictExpired();
99
+
100
+ let candidateIds = new Set(this.cache.keys());
101
+
102
+ if (q.channels && q.channels.length > 0) {
103
+ const channelIds = new Set<string>();
104
+ for (const ch of q.channels) {
105
+ const ids = this.byChannel.get(ch);
106
+ if (ids) for (const id of ids) channelIds.add(id);
107
+ }
108
+ candidateIds = intersect(candidateIds, channelIds);
109
+ }
110
+
111
+ if (q.sessionId) {
112
+ const sessionIds = this.bySession.get(q.sessionId);
113
+ candidateIds = sessionIds
114
+ ? intersect(candidateIds, sessionIds)
115
+ : new Set();
116
+ }
117
+
118
+ if (q.sourceAgent) {
119
+ const sourceIds = this.bySource.get(q.sourceAgent);
120
+ candidateIds = sourceIds
121
+ ? intersect(candidateIds, sourceIds)
122
+ : new Set();
123
+ }
124
+
125
+ if (q.targetAgent) {
126
+ const filtered = new Set<string>();
127
+ for (const id of candidateIds) {
128
+ const msg = this.cache.get(id)?.msg;
129
+ if (msg && (!msg.targetAgent || msg.targetAgent === q.targetAgent)) {
130
+ filtered.add(id);
131
+ }
132
+ }
133
+ candidateIds = filtered;
134
+ }
135
+
136
+ let results = Array.from(candidateIds)
137
+ .map(id => this.cache.get(id)!.msg)
138
+ .filter(msg => {
139
+ if (q.minImportance !== undefined && msg.importance < q.minImportance) return false;
140
+ if (q.maxAgeSeconds !== undefined) {
141
+ const age = (Date.now() - new Date(msg.timestamp).getTime()) / 1000;
142
+ if (age > q.maxAgeSeconds) return false;
143
+ }
144
+ return true;
145
+ });
146
+
147
+ // Free-text filter
148
+ if (q.query) {
149
+ const lower = q.query.toLowerCase();
150
+ results = results.filter(m =>
151
+ m.content.toLowerCase().includes(lower) ||
152
+ (m.tags || []).some(t => t.toLowerCase().includes(lower))
153
+ );
154
+ }
155
+
156
+ // Sort by importance desc, then timestamp desc
157
+ results.sort((a, b) => {
158
+ const imp = b.importance - a.importance;
159
+ if (imp !== 0) return imp;
160
+ return b.timestamp.localeCompare(a.timestamp);
161
+ });
162
+
163
+ return results.slice(0, q.limit || 20);
164
+ }
165
+
166
+ clearSession(sessionId: string): void {
167
+ const ids = this.bySession.get(sessionId);
168
+ if (!ids) return;
169
+ for (const id of ids) {
170
+ const msg = this.cache.get(id)?.msg;
171
+ if (msg) {
172
+ this.byChannel.get(msg.channel)?.delete(id);
173
+ this.bySource.get(msg.sourceAgent)?.delete(id);
174
+ this.cache.delete(id);
175
+ }
176
+ }
177
+ this.bySession.delete(sessionId);
178
+ }
179
+
180
+ size(): number {
181
+ this.evictExpired();
182
+ return this.cache.size;
183
+ }
184
+
185
+ private evictExpired(): void {
186
+ const now = Date.now();
187
+ for (const [id, entry] of this.cache) {
188
+ if (entry.expiresAt > 0 && now > entry.expiresAt) {
189
+ const msg = entry.msg;
190
+ this.byChannel.get(msg.channel)?.delete(id);
191
+ this.bySession.get(msg.sessionId)?.delete(id);
192
+ this.bySource.get(msg.sourceAgent)?.delete(id);
193
+ this.cache.delete(id);
194
+ }
195
+ }
196
+ }
197
+ }
198
+
199
+ function intersect(a: Set<string>, b: Set<string>): Set<string> {
200
+ const result = new Set<string>();
201
+ for (const item of a) {
202
+ if (b.has(item)) result.add(item);
203
+ }
204
+ return result;
205
+ }
206
+
207
+ // ── AgentMemoryBus ───────────────────────────────────────────────────────────
208
+
209
+ export class AgentMemoryBus {
210
+ private cache: MemoryCache;
211
+ private subscribers: Map<AgentChannel, Set<(msg: AgentMemoryMessage) => void>> = new Map();
212
+ private globalSubscribers: Set<(msg: AgentMemoryMessage) => void> = new Set();
213
+ private persistenceEnabled: boolean;
214
+
215
+ constructor(persistToSharedMemory = true) {
216
+ this.cache = new MemoryCache();
217
+ this.persistenceEnabled = persistToSharedMemory;
218
+ }
219
+
220
+ /**
221
+ * Publish a message to the shared memory bus.
222
+ * Stored in fast cache + optionally persisted to SharedMemory SQLite.
223
+ */
224
+ async publish(msg: Omit<AgentMemoryMessage, 'id' | 'timestamp'>): Promise<AgentMemoryMessage> {
225
+ const full: AgentMemoryMessage = {
226
+ ...msg,
227
+ id: uuid(),
228
+ timestamp: new Date().toISOString(),
229
+ };
230
+
231
+ // 1. Fast in-memory cache
232
+ this.cache.put(full);
233
+
234
+ // 2. Notify subscribers
235
+ this.notifySubscribers(full);
236
+
237
+ // 3. Persist to SharedMemory SQLite (if enabled)
238
+ if (this.persistenceEnabled) {
239
+ const channelTag = `agent:${msg.channel}`;
240
+ const sourceTag = `agent:${msg.sourceAgent}`;
241
+ const tags = [...(msg.tags || []), channelTag, sourceTag];
242
+ if (msg.targetAgent) tags.push(`target:${msg.targetAgent}`);
243
+
244
+ const importance = msg.importance;
245
+
246
+ // Store as a memory entry so it appears in semantic search
247
+ sharedMemory.add(
248
+ `[AgentBus:${msg.channel}] [${msg.sourceAgent}]${msg.targetAgent ? ` → ${msg.targetAgent}` : ''}: ${msg.content}`,
249
+ `agent-bus-${msg.channel}`,
250
+ msg.sessionId,
251
+ tags,
252
+ 'assistant',
253
+ false, // defer commit for batching
254
+ ).catch(() => {});
255
+
256
+ // High-importance items get stored with extra metadata
257
+ if (importance >= 0.7) {
258
+ sharedMemory.add(
259
+ `[AgentBus:${msg.channel}] [DECISION] ${msg.sourceAgent}: ${msg.content}`,
260
+ 'agent-bus-decision',
261
+ msg.sessionId,
262
+ [...tags, 'high-importance', 'decision'],
263
+ 'assistant',
264
+ true,
265
+ ).catch(() => {});
266
+ }
267
+ }
268
+
269
+ return full;
270
+ }
271
+
272
+ /**
273
+ * Query messages from the shared memory bus (cache + persistent)
274
+ */
275
+ async query(q: AgentMemoryQuery): Promise<AgentMemoryMessage[]> {
276
+ // 1. Get from fast cache
277
+ const cached = this.cache.query(q);
278
+
279
+ // 2. If we need more, search SharedMemory
280
+ if (cached.length < (q.limit || 20) && this.persistenceEnabled) {
281
+ const searchQuery = q.query || q.channels?.join(' ') || '';
282
+ const persisted = await sharedMemory.search(
283
+ `AgentBus ${searchQuery}`,
284
+ (q.limit || 20) - cached.length
285
+ );
286
+
287
+ // Convert MemoryEntry → AgentMemoryMessage (best-effort)
288
+ const fromPersisted: AgentMemoryMessage[] = persisted
289
+ .filter(e => e.content.startsWith('[AgentBus'))
290
+ .map(e => this.entryToMessage(e))
291
+ .filter((m): m is AgentMemoryMessage => m !== null);
292
+
293
+ // Merge, deduplicate by ID
294
+ const seen = new Set(cached.map(m => m.id));
295
+ for (const m of fromPersisted) {
296
+ if (!seen.has(m.id)) {
297
+ cached.push(m);
298
+ seen.add(m.id);
299
+ }
300
+ }
301
+ }
302
+
303
+ return cached;
304
+ }
305
+
306
+ /**
307
+ * Subscribe to messages on a specific channel
308
+ */
309
+ subscribe(channel: AgentChannel, callback: (msg: AgentMemoryMessage) => void): () => void {
310
+ if (!this.subscribers.has(channel)) {
311
+ this.subscribers.set(channel, new Set());
312
+ }
313
+ this.subscribers.get(channel)!.add(callback);
314
+ return () => this.subscribers.get(channel)?.delete(callback);
315
+ }
316
+
317
+ /**
318
+ * Subscribe to all channels
319
+ */
320
+ subscribeAll(callback: (msg: AgentMemoryMessage) => void): () => void {
321
+ this.globalSubscribers.add(callback);
322
+ return () => this.globalSubscribers.delete(callback);
323
+ }
324
+
325
+ /**
326
+ * Get context for an agent: relevant messages from other agents in the same session
327
+ */
328
+ async getContextForAgent(
329
+ agentName: string,
330
+ sessionId: string,
331
+ limit = 10,
332
+ maxAgeSeconds = 300, // last 5 minutes by default
333
+ ): Promise<string> {
334
+ const relevant = await this.query({
335
+ sessionId,
336
+ limit,
337
+ maxAgeSeconds,
338
+ });
339
+
340
+ // Filter out messages from the requesting agent
341
+ const others = relevant.filter(m => m.sourceAgent !== agentName);
342
+
343
+ if (others.length === 0) return '';
344
+
345
+ const lines: string[] = [
346
+ '--- AGENT MEMORY (shared context from other agents) ---',
347
+ ];
348
+
349
+ for (const m of others) {
350
+ const target = m.targetAgent ? ` → ${m.targetAgent}` : '';
351
+ lines.push(
352
+ `[${m.channel}] ${m.sourceAgent}${target} (${m.timestamp.slice(11, 19)}): ${m.content.slice(0, 300)}`
353
+ );
354
+ if (m.importance >= 0.8) lines.push(` ⭐ Important: ${m.content.slice(0, 200)}`);
355
+ }
356
+
357
+ lines.push('--- END AGENT MEMORY ---');
358
+ return lines.join('\n');
359
+ }
360
+
361
+ /**
362
+ * Clear all messages for a session
363
+ */
364
+ clearSession(sessionId: string): void {
365
+ this.cache.clearSession(sessionId);
366
+ }
367
+
368
+ /**
369
+ * Get stats
370
+ */
371
+ stats(): { cacheSize: number; subscribers: number; channels: number } {
372
+ let subCount = this.globalSubscribers.size;
373
+ for (const subs of this.subscribers.values()) {
374
+ subCount += subs.size;
375
+ }
376
+ return {
377
+ cacheSize: this.cache.size(),
378
+ subscribers: subCount,
379
+ channels: this.subscribers.size,
380
+ };
381
+ }
382
+
383
+ // ── Private ──────────────────────────────────────────────────────────────
384
+
385
+ private notifySubscribers(msg: AgentMemoryMessage): void {
386
+ // Channel-specific
387
+ const channelSubs = this.subscribers.get(msg.channel);
388
+ if (channelSubs) {
389
+ for (const cb of channelSubs) {
390
+ try { cb(msg); } catch { /* subscriber error */ }
391
+ }
392
+ }
393
+ // Global
394
+ for (const cb of this.globalSubscribers) {
395
+ try { cb(msg); } catch { /* subscriber error */ }
396
+ }
397
+ }
398
+
399
+ private entryToMessage(entry: MemoryEntry): AgentMemoryMessage | null {
400
+ try {
401
+ const content = entry.content;
402
+ // Two stored formats:
403
+ // normal: [AgentBus:channel] [sourceAgent] → targetAgent?: body
404
+ // decision: [AgentBus:channel] [DECISION] sourceAgent: body
405
+ const channelMatch = content.match(/^\[AgentBus:(\w+)\]/);
406
+ const decisionMatch = content.match(/\[DECISION\] (\w+):/);
407
+ const sourceMatch = decisionMatch ?? content.match(/\] \[(\w+)\]/);
408
+ const targetMatch = content.match(/\] → (\w+):/);
409
+ const contentStart = content.indexOf(': ', content.lastIndexOf(']'));
410
+
411
+ if (!channelMatch || !sourceMatch) return null;
412
+
413
+ return {
414
+ id: entry.id,
415
+ channel: channelMatch[1] as AgentChannel,
416
+ sourceAgent: sourceMatch[1],
417
+ targetAgent: targetMatch ? targetMatch[1] : undefined,
418
+ sessionId: entry.session,
419
+ content: contentStart > 0 ? content.slice(contentStart + 2) : content,
420
+ timestamp: entry.timestamp,
421
+ importance: entry.importance || 0.5,
422
+ tags: entry.tags,
423
+ };
424
+ } catch {
425
+ return null;
426
+ }
427
+ }
428
+ }
429
+
430
+ // ── Singleton ────────────────────────────────────────────────────────────────
431
+
432
+ export const agentMemoryBus = new AgentMemoryBus(true);
@@ -0,0 +1,99 @@
1
+ /**
2
+ * AEGIS Memory Cloud Sync
3
+ *
4
+ * Pushes/pulls SharedMemory entries through aegiscloud.org so memory is
5
+ * shared across devices (CLI + mobile) for subscribed users. Best-effort:
6
+ * failures are swallowed, matching DriveSync's "silent fail — optional"
7
+ * pattern. Never blocks the main session — callers fire-and-forget.
8
+ */
9
+ import type { MemoryEntry } from './SharedMemory.js';
10
+
11
+ const MEMORY_API_BASE = process.env.AEGIS_MEMORY_API_BASE || 'https://aegiscloud.org/api/memory';
12
+
13
+ // Embeddings are 384 floats — as JSON that's ~7KB per entry. The server only
14
+ // ever does keyword (LIKE) search/dashboard listing on memory_entries, never
15
+ // vector similarity, so transmitting them bloats every push for zero server-side
16
+ // benefit. A bulk upload of a few thousand entries turned tens of MB of pure
17
+ // embedding JSON across many sequential requests, which is what was actually
18
+ // timing out — not a network problem. Dropping it: a device that pulls this
19
+ // entry down just lacks a precomputed vector locally (falls back to keyword
20
+ // search for it, same as any entry whose embedder failed at add()-time).
21
+ function stripEmbedding(entries: MemoryEntry[]): Omit<MemoryEntry, 'embedding'>[] {
22
+ return entries.map(({ embedding, ...rest }) => rest);
23
+ }
24
+
25
+ export async function pushEntries(entries: MemoryEntry[], apiKey: string): Promise<void> {
26
+ if (entries.length === 0) return;
27
+ try {
28
+ await fetch(`${MEMORY_API_BASE}/save`, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
31
+ body: JSON.stringify({ entries: stripEmbedding(entries) }),
32
+ });
33
+ } catch {
34
+ // silent fail — sync is optional, local memory already has the entry
35
+ }
36
+ }
37
+
38
+ /** Like pushEntries, but reports how many entries the server actually saved (for bulk uploads). */
39
+ export async function pushBatch(entries: MemoryEntry[], apiKey: string): Promise<number> {
40
+ if (entries.length === 0) return 0;
41
+ try {
42
+ // A bulk upload can be many sequential batches — one slow/hung request
43
+ // without its own timeout would stall the whole pushAll() loop indefinitely,
44
+ // regardless of any timeout the caller process is given from outside.
45
+ const controller = new AbortController();
46
+ const timer = setTimeout(() => controller.abort(), 20_000);
47
+ let res: Response;
48
+ try {
49
+ res = await fetch(`${MEMORY_API_BASE}/save`, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
52
+ body: JSON.stringify({ entries: stripEmbedding(entries) }),
53
+ signal: controller.signal,
54
+ });
55
+ } finally {
56
+ clearTimeout(timer);
57
+ }
58
+ if (!res.ok) return 0;
59
+ const data = await res.json() as { saved?: number };
60
+ return data.saved ?? 0;
61
+ } catch {
62
+ return 0;
63
+ }
64
+ }
65
+
66
+ export async function pullSince(since: string | null, apiKey: string): Promise<MemoryEntry[]> {
67
+ try {
68
+ const res = await fetch(`${MEMORY_API_BASE}/pull`, {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
71
+ body: JSON.stringify(since ? { since } : {}),
72
+ });
73
+ if (!res.ok) return [];
74
+ const data = await res.json() as { entries?: MemoryEntry[] };
75
+ return data.entries ?? [];
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Keyword search against the caller's synced entries (catches entries written on
83
+ * another device since the last `pullSince`). Caller is responsible for merging
84
+ * with local results — this never throws, just returns [] on any failure.
85
+ */
86
+ export async function searchCloud(query: string, limit: number, apiKey: string): Promise<MemoryEntry[]> {
87
+ try {
88
+ const res = await fetch(`${MEMORY_API_BASE}/search`, {
89
+ method: 'POST',
90
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
91
+ body: JSON.stringify({ query, limit }),
92
+ });
93
+ if (!res.ok) return [];
94
+ const data = await res.json() as { entries?: MemoryEntry[] };
95
+ return data.entries ?? [];
96
+ } catch {
97
+ return [];
98
+ }
99
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * AEGIS Drive Sync
3
+ * Uploads conversation sessions to user's Google Drive
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import * as os from 'os';
8
+ import * as https from 'https';
9
+
10
+ const CONFIG_PATH = path.join(os.homedir(), '.aegiscode', 'config.json');
11
+ const SESSIONS_DIR = path.join(os.homedir(), '.aegis', 'projects');
12
+
13
+ function getConfig() {
14
+ try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); }
15
+ catch { return {}; }
16
+ }
17
+
18
+ async function getDriveToken(): Promise<string | null> {
19
+ const cfg = getConfig();
20
+ const apiKey = cfg?.aegiscloud?.api_key || process.env.AEGISCLOUD_API_KEY;
21
+ if (!apiKey) return null;
22
+ return new Promise((resolve) => {
23
+ const req = https.request({
24
+ hostname: 'aegiscloud.org',
25
+ path: '/api/drive/token',
26
+ method: 'GET',
27
+ headers: { 'X-API-Key': apiKey }
28
+ }, res => {
29
+ let data = '';
30
+ res.on('data', c => data += c);
31
+ res.on('end', () => {
32
+ try {
33
+ const d = JSON.parse(data);
34
+ resolve(d.connected ? d.access_token : null);
35
+ } catch { resolve(null); }
36
+ });
37
+ });
38
+ req.on('error', () => resolve(null));
39
+ req.end();
40
+ });
41
+ }
42
+
43
+
44
+ async function uploadToDrive(token: string, filename: string, content: string): Promise<void> {
45
+ return new Promise((resolve, reject) => {
46
+ const boundary = '-------aegis_boundary';
47
+ const metadata = JSON.stringify({ name: filename, parents: ['root'] });
48
+ const body = [
49
+ `--${boundary}`,
50
+ 'Content-Type: application/json; charset=UTF-8',
51
+ '',
52
+ metadata,
53
+ `--${boundary}`,
54
+ 'Content-Type: text/plain',
55
+ '',
56
+ content,
57
+ `--${boundary}--`,
58
+ ].join('\r\n');
59
+
60
+ const req = https.request({
61
+ hostname: 'www.googleapis.com',
62
+ path: '/upload/drive/v3/files?uploadType=multipart',
63
+ method: 'POST',
64
+ headers: {
65
+ 'Authorization': `Bearer ${token}`,
66
+ 'Content-Type': `multipart/related; boundary=${boundary}`,
67
+ 'Content-Length': Buffer.byteLength(body),
68
+ }
69
+ }, res => {
70
+ let data = '';
71
+ res.on('data', c => data += c);
72
+ res.on('end', () => {
73
+ if (res.statusCode === 200 || res.statusCode === 201) resolve();
74
+ else reject(new Error(`Drive upload failed: ${res.statusCode}`));
75
+ });
76
+ });
77
+ req.on('error', reject);
78
+ req.write(body);
79
+ req.end();
80
+ });
81
+ }
82
+
83
+ export async function syncSessionToDrive(sessionId: string): Promise<void> {
84
+ const token = await getDriveToken();
85
+ if (!token) return;
86
+
87
+ // Find session file
88
+ const projects = fs.readdirSync(SESSIONS_DIR).filter(d =>
89
+ fs.statSync(path.join(SESSIONS_DIR, d)).isDirectory()
90
+ );
91
+
92
+ for (const project of projects) {
93
+ const sessionFile = path.join(SESSIONS_DIR, project, `${sessionId}.jsonl`);
94
+ if (fs.existsSync(sessionFile)) {
95
+ const content = fs.readFileSync(sessionFile, 'utf8');
96
+ const filename = `AEGIS/${project}/${sessionId}.jsonl`;
97
+ try {
98
+ await uploadToDrive(token, filename, content);
99
+ console.log(`\x1b[38;2;68;64;90m[Drive] Synced ${sessionId}\x1b[0m`);
100
+ } catch(e) {
101
+ // Silent fail — Drive sync is optional
102
+ }
103
+ return;
104
+ }
105
+ }
106
+ }