@auxiora/runtime 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2981 @@
1
+ import { Gateway } from '@auxiora/gateway';
2
+ import { SessionManager, sanitizeTranscript } from '@auxiora/sessions';
3
+ import { MediaProcessor, detectProviders } from '@auxiora/media';
4
+ import { ProviderFactory, readClaudeCliCredentials, isSetupToken, refreshOAuthToken, refreshPKCEOAuthToken, streamWithModelFallback } from '@auxiora/providers';
5
+ import { ModelRouter, TaskClassifier, ModelSelector, CostTracker } from '@auxiora/router';
6
+ import { ChannelManager, DraftStreamLoop } from '@auxiora/channels';
7
+ import { loadConfig, saveConfig as saveFullConfig } from '@auxiora/config';
8
+ import { Vault, vaultExists } from '@auxiora/vault';
9
+ import { audit } from '@auxiora/audit';
10
+ import { getWorkspacePath, getSoulPath, getAgentsPath, getIdentityPath, getUserPath, getBehaviorsPath, getWebhooksPath, getScreenshotsDir, } from '@auxiora/core';
11
+ import { createArchitect, ARCHITECT_BASE_PROMPT, VaultStorageAdapter } from '@auxiora/personality/architect';
12
+ import { toolRegistry, toolExecutor, initializeToolExecutor, ToolPermission, setBrowserManager, setWebhookManager, setBehaviorManager, setProviderFactory, setOrchestrationEngine, setResearchEngine, setClipboardMonitor, setAppController, setSystemStateMonitor, setEmailIntelligence, setCalendarIntelligence, setContactGraph, setContextRecall, setComposeEngine, setGrammarChecker, setLanguageDetector, } from '@auxiora/tools';
13
+ import { ResearchEngine } from '@auxiora/research';
14
+ import { OrchestrationEngine } from '@auxiora/orchestrator';
15
+ import * as crypto from 'node:crypto';
16
+ import * as fs from 'node:fs/promises';
17
+ import * as path from 'node:path';
18
+ import { BehaviorManager } from '@auxiora/behaviors';
19
+ import { BrowserManager } from '@auxiora/browser';
20
+ import { ClipboardMonitor, AppController, SystemStateMonitor } from '@auxiora/os-bridge';
21
+ import { VoiceManager } from '@auxiora/voice';
22
+ import { WhisperSTT } from '@auxiora/stt';
23
+ import { OpenAITTS } from '@auxiora/tts';
24
+ import { WebhookManager } from '@auxiora/webhooks';
25
+ import { createDashboardRouter } from '@auxiora/dashboard';
26
+ import { PluginLoader } from '@auxiora/plugins';
27
+ import { MemoryStore, MemoryRetriever, MemoryExtractor, PatternDetector, PersonalityAdapter, } from '@auxiora/memory';
28
+ import { TrustEngine, ActionAuditTrail, RollbackManager, TrustGate } from '@auxiora/autonomy';
29
+ import { IntentParser, ActionPlanner } from '@auxiora/intent';
30
+ import { createLoopDetectionState, recordToolCall, recordToolOutcome, detectLoop } from './tool-loop-detection.js';
31
+ import { UserManager } from '@auxiora/social';
32
+ import { WorkflowEngine, ApprovalManager, AutonomousExecutor } from '@auxiora/workflows';
33
+ import { AgentProtocol, MessageSigner, AgentDirectory } from '@auxiora/agent-protocol';
34
+ import { AmbientPatternEngine, QuietNotificationManager, BriefingGenerator, AnticipationEngine, AmbientScheduler, DEFAULT_AMBIENT_SCHEDULER_CONFIG, NotificationOrchestrator } from '@auxiora/ambient';
35
+ import { NotificationHub, DoNotDisturbManager } from '@auxiora/notification-hub';
36
+ import { ConnectorRegistry, AuthManager as ConnectorAuthManager, TriggerManager } from '@auxiora/connectors';
37
+ import { googleWorkspaceConnector } from '@auxiora/connector-google-workspace';
38
+ import { microsoftConnector } from '@auxiora/connector-microsoft';
39
+ import { githubConnector } from '@auxiora/connector-github';
40
+ import { linearConnector } from '@auxiora/connector-linear';
41
+ import { notionConnector } from '@auxiora/connector-notion';
42
+ import { homeAssistantConnector } from '@auxiora/connector-homeassistant';
43
+ import { twitterConnector, linkedinConnector, redditConnector, instagramConnector } from '@auxiora/connector-social';
44
+ import { ConversationEngine } from '@auxiora/conversation';
45
+ import { EmailTriageEngine, ThreadSummarizer } from '@auxiora/email-intelligence';
46
+ import { ScheduleAnalyzer, ScheduleOptimizer, MeetingPrepGenerator } from '@auxiora/calendar-intelligence';
47
+ import { ContactGraph, ContextRecall } from '@auxiora/contacts';
48
+ import { ComposeEngine, GrammarChecker, LanguageDetector } from '@auxiora/compose';
49
+ import { ScreenCapturer } from '@auxiora/screen';
50
+ import { CapabilityCatalogImpl, HealthMonitorImpl, createIntrospectTool, generatePromptFragment } from '@auxiora/introspection';
51
+ import { setMemoryStore } from '@auxiora/tools';
52
+ import { getAuditLogger } from '@auxiora/audit';
53
+ import { Router } from 'express';
54
+ import express from 'express';
55
+ import { PersonalityManager, ModeLoader, ModeDetector, PromptAssembler, MODE_IDS, SecurityFloor, ArchitectBridge, ArchitectAwarenessCollector, parseSoulBiases, } from '@auxiora/personality';
56
+ import { getModesDir } from '@auxiora/core';
57
+ import { fileURLToPath } from 'node:url';
58
+ import { getLogger, generateRequestId, runWithRequestId } from '@auxiora/logger';
59
+ import { SelfAwarenessAssembler, InMemoryAwarenessStorage, ConversationReflector, CapacityMonitor, KnowledgeBoundary, RelationshipModel, TemporalTracker, EnvironmentSensor, MetaCognitor, } from '@auxiora/self-awareness';
60
+ /**
61
+ * Map Claude Code emulation tool calls to our actual tool names + input format.
62
+ * The model may call CC tools (WebSearch, Bash, etc.) since they're in the request for OAuth compat.
63
+ */
64
+ function mapCCToolCall(name, input) {
65
+ switch (name) {
66
+ case 'WebSearch':
67
+ return { name: 'web_browser', input: { url: `https://www.google.com/search?q=${encodeURIComponent(input.query || '')}` } };
68
+ case 'WebFetch':
69
+ return { name: 'web_browser', input: { url: input.url } };
70
+ case 'Bash':
71
+ return { name: 'bash', input: { command: input.command, timeout: input.timeout } };
72
+ case 'Read':
73
+ return { name: 'file_read', input: { path: input.file_path } };
74
+ case 'Write':
75
+ return { name: 'file_write', input: { path: input.file_path, content: input.content } };
76
+ default:
77
+ return { name, input };
78
+ }
79
+ }
80
+ const ACTIVITY_EVENT_PREFIXES = [
81
+ 'behavior.', 'message.', 'channel.', 'webhook.',
82
+ 'system.', 'auth.login', 'auth.logout',
83
+ ];
84
+ export class Auxiora {
85
+ logger = getLogger('runtime');
86
+ config;
87
+ gateway;
88
+ sessions;
89
+ providers;
90
+ channels;
91
+ vault;
92
+ systemPrompt = '';
93
+ standardPrompt = '';
94
+ architectPrompt = '';
95
+ running = false;
96
+ behaviors;
97
+ browserManager;
98
+ voiceManager;
99
+ webhookManager;
100
+ pluginLoader;
101
+ modelRouter;
102
+ memoryStore;
103
+ memoryRetriever;
104
+ memoryExtractor;
105
+ patternDetector;
106
+ personalityAdapter;
107
+ mediaProcessor;
108
+ memoryCleanupInterval;
109
+ trustEngine;
110
+ trustAuditTrail;
111
+ rollbackManager;
112
+ trustGate;
113
+ intentParser;
114
+ actionPlanner;
115
+ orchestrationEngine;
116
+ // [P14] Team / Social
117
+ userManager;
118
+ workflowEngine;
119
+ approvalManager;
120
+ autonomousExecutor;
121
+ agentProtocol;
122
+ agentDirectory;
123
+ // [P15] Senses
124
+ ambientEngine;
125
+ ambientNotifications;
126
+ briefingGenerator;
127
+ anticipationEngine;
128
+ conversationEngine;
129
+ screenCapturer;
130
+ screenAnalyzer;
131
+ // Modes system
132
+ modeLoader;
133
+ modeDetector;
134
+ promptAssembler;
135
+ sessionModes = new Map();
136
+ userPreferences;
137
+ // Connector system
138
+ connectorRegistry;
139
+ connectorAuthManager;
140
+ triggerManager;
141
+ ambientScheduler;
142
+ notificationHub;
143
+ dndManager;
144
+ notificationOrchestrator;
145
+ capabilityCatalog;
146
+ healthMonitor;
147
+ capabilityPromptFragment = '';
148
+ selfAwarenessAssembler;
149
+ architect;
150
+ architectBridge = null;
151
+ architectAwarenessCollector = null;
152
+ // Security floor
153
+ securityFloor;
154
+ sessionEscalation = new Map();
155
+ /** Tracks the most recent channel ID for each connected channel type (e.g. discord → snowflake).
156
+ * Used for proactive delivery (behaviors, ambient briefings). Persisted to disk. */
157
+ lastActiveChannels = new Map();
158
+ activeAgents = new Map();
159
+ channelTargetsPath = path.join(path.dirname(getBehaviorsPath()), 'channel-targets.json');
160
+ orchestrationHistory = [];
161
+ async initialize(options = {}) {
162
+ // Load config
163
+ this.config = options.config || (await loadConfig());
164
+ // Initialize vault (optional)
165
+ this.vault = new Vault();
166
+ if (options.vaultPassword) {
167
+ try {
168
+ await this.vault.unlock(options.vaultPassword);
169
+ }
170
+ catch (error) {
171
+ this.logger.warn('Failed to unlock vault', { error: error instanceof Error ? error : new Error(String(error)) });
172
+ }
173
+ }
174
+ // Initialize tool system with approval callback
175
+ initializeToolExecutor(async (toolName, params, context) => {
176
+ // For now, auto-approve all tools in non-interactive mode
177
+ // In future, could send approval request to client via WebSocket
178
+ this.logger.debug('Auto-approving tool', { toolName, params });
179
+ return true;
180
+ });
181
+ // Initialize sessions
182
+ this.sessions = new SessionManager({
183
+ maxContextTokens: this.config.session.maxContextTokens,
184
+ ttlMinutes: this.config.session.ttlMinutes,
185
+ autoSave: this.config.session.autoSave,
186
+ compactionEnabled: this.config.session.compactionEnabled,
187
+ });
188
+ await this.sessions.initialize();
189
+ // Initialize providers (if vault is unlocked and has keys)
190
+ await this.initializeProviders();
191
+ // Initialize model router (if providers are set up)
192
+ if (this.providers) {
193
+ this.initializeRouter();
194
+ setProviderFactory(this.providers);
195
+ // Inject summarizer for auto-compaction
196
+ this.sessions.setSummarizer(async (prompt) => {
197
+ const provider = this.providers.getPrimaryProvider();
198
+ const result = await provider.complete([{ role: 'user', content: prompt }], { maxTokens: 1024 });
199
+ return result.content;
200
+ });
201
+ }
202
+ // Initialize orchestration engine (if enabled and providers available)
203
+ if (this.providers && this.config.orchestration?.enabled !== false) {
204
+ this.orchestrationEngine = new OrchestrationEngine(this.providers, this.config.orchestration ?? { enabled: true, maxConcurrentAgents: 5, defaultTimeout: 60000, totalTimeout: 300000, allowedPatterns: ['parallel', 'sequential', 'debate', 'map-reduce', 'supervisor'], costMultiplierWarning: 3 });
205
+ setOrchestrationEngine(this.orchestrationEngine);
206
+ this.logger.info('Orchestration engine initialized');
207
+ }
208
+ // Initialize research engine (if Brave API key available)
209
+ if (this.config.research?.enabled !== false) {
210
+ let vaultKey;
211
+ try {
212
+ vaultKey = this.vault.get('BRAVE_API_KEY');
213
+ }
214
+ catch { /* vault locked */ }
215
+ const braveApiKey = this.config.research?.braveApiKey
216
+ ?? vaultKey
217
+ ?? process.env.AUXIORA_RESEARCH_BRAVE_API_KEY;
218
+ if (braveApiKey) {
219
+ try {
220
+ const provider = this.providers?.getPrimaryProvider();
221
+ const researchEngine = new ResearchEngine({
222
+ braveApiKey,
223
+ provider: provider ?? undefined,
224
+ defaultDepth: this.config.research?.defaultDepth ?? 'standard',
225
+ maxConcurrentSources: this.config.research?.maxConcurrentSources ?? 5,
226
+ searchTimeout: this.config.research?.searchTimeout ?? 10_000,
227
+ fetchTimeout: this.config.research?.fetchTimeout ?? 15_000,
228
+ });
229
+ setResearchEngine(researchEngine);
230
+ this.logger.info(`Research engine initialized (Brave Search configured${provider ? ', AI extraction enabled' : ''})`);
231
+ }
232
+ catch (err) {
233
+ this.logger.warn('Failed to initialize research engine', { error: err instanceof Error ? err : new Error(String(err)) });
234
+ }
235
+ }
236
+ }
237
+ // Initialize channels (if configured and vault is unlocked)
238
+ await this.initializeChannels();
239
+ // Load personality files
240
+ await this.loadPersonality();
241
+ // Initialize modes system
242
+ await this.initializeModes();
243
+ // Initialize gateway
244
+ this.gateway = new Gateway({
245
+ config: this.config,
246
+ needsSetup: async () => {
247
+ if (!(await vaultExists()))
248
+ return true;
249
+ const name = this.config.agent?.name ?? 'Auxiora';
250
+ let hasSoul = false;
251
+ try {
252
+ await fs.access(getSoulPath());
253
+ hasSoul = true;
254
+ }
255
+ catch { }
256
+ return name === 'Auxiora' && !hasSoul;
257
+ },
258
+ });
259
+ this.gateway.onMessage(this.handleMessage.bind(this));
260
+ // Stream curated audit events to dashboard via WebSocket
261
+ const auditLogger = getAuditLogger();
262
+ auditLogger.onEntry = (entry) => {
263
+ const isActivityEvent = ACTIVITY_EVENT_PREFIXES.some((prefix) => entry.event.startsWith(prefix));
264
+ if (isActivityEvent) {
265
+ this.gateway.broadcast({ type: 'activity', payload: entry }, (client) => client.authenticated);
266
+ }
267
+ };
268
+ // Initialize behavior system
269
+ if (this.providers) {
270
+ this.behaviors = new BehaviorManager({
271
+ storePath: getBehaviorsPath(),
272
+ executorDeps: {
273
+ getProvider: () => this.providers.getPrimaryProvider(),
274
+ sendToChannel: async (channelType, channelId, message) => {
275
+ this.logger.info('sendToChannel called', { channelType, channelId, hasChannels: !!this.channels });
276
+ // Always broadcast to webchat + persist
277
+ this.gateway.broadcast({
278
+ type: 'message',
279
+ payload: { role: 'assistant', content: message.content },
280
+ });
281
+ this.persistToWebchat(message.content);
282
+ // Deliver to all connected external channels
283
+ if (this.channels) {
284
+ const connected = this.channels.getConnectedChannels();
285
+ this.logger.info('Connected channels for delivery', { connected });
286
+ for (const ct of connected) {
287
+ const targetId = this.lastActiveChannels.get(ct)
288
+ ?? this.channels.getDefaultChannelId(ct);
289
+ this.logger.info('Channel delivery target', { channel: ct, targetId, fromLastActive: this.lastActiveChannels.get(ct) });
290
+ if (!targetId)
291
+ continue;
292
+ const result = await this.channels.send(ct, targetId, { content: message.content });
293
+ if (!result.success) {
294
+ this.logger.warn('Channel delivery failed', { channel: ct, targetId, error: new Error(result.error ?? 'unknown') });
295
+ }
296
+ }
297
+ }
298
+ return { success: true };
299
+ },
300
+ getSystemPrompt: () => this.systemPrompt,
301
+ executeWithTools: async (messages, systemPrompt) => {
302
+ const execId = `behavior-exec-${Date.now()}`;
303
+ const actionPreview = messages[0]?.content?.slice(0, 80) ?? 'Behavior';
304
+ this.agentStart(execId, 'behavior', actionPreview);
305
+ // Use unique senderId so getOrCreate never reuses a stale behavior session
306
+ const session = await this.sessions.getOrCreate(execId, {
307
+ channelType: 'behavior',
308
+ senderId: execId,
309
+ clientId: execId,
310
+ });
311
+ for (const msg of messages) {
312
+ await this.sessions.addMessage(session.id, msg.role, msg.content);
313
+ }
314
+ const provider = this.providers.getPrimaryProvider();
315
+ const noopChunk = () => { };
316
+ try {
317
+ const fallbackCandidates = this.providers.resolveFallbackCandidates();
318
+ const result = await this.executeWithTools(session.id, messages, systemPrompt, provider, noopChunk, { fallbackCandidates });
319
+ this.agentEnd(execId, true);
320
+ return { content: result.response, usage: result.usage };
321
+ }
322
+ catch (err) {
323
+ this.agentEnd(execId, false);
324
+ throw err;
325
+ }
326
+ },
327
+ },
328
+ auditFn: (event, details) => {
329
+ audit(event, details);
330
+ },
331
+ });
332
+ await this.behaviors.start();
333
+ setBehaviorManager(this.behaviors);
334
+ }
335
+ // Initialize browser system
336
+ this.browserManager = new BrowserManager({
337
+ config: {
338
+ headless: true,
339
+ viewport: { width: 1280, height: 720 },
340
+ navigationTimeout: 30_000,
341
+ actionTimeout: 10_000,
342
+ maxConcurrentPages: 10,
343
+ screenshotDir: getScreenshotsDir(),
344
+ },
345
+ });
346
+ setBrowserManager(this.browserManager);
347
+ // Initialize OS bridge
348
+ const clipboardMonitor = new ClipboardMonitor();
349
+ const appController = new AppController(process.platform);
350
+ const systemStateMonitor = new SystemStateMonitor();
351
+ setClipboardMonitor(clipboardMonitor);
352
+ setAppController(appController);
353
+ setSystemStateMonitor(systemStateMonitor);
354
+ this.logger.info('OS bridge initialized');
355
+ // Initialize email intelligence (engines ready; connectors needed for full functionality)
356
+ const emailTriageEngine = new EmailTriageEngine();
357
+ const threadSummarizer = new ThreadSummarizer();
358
+ setEmailIntelligence({
359
+ triage: {
360
+ getTriageSummary: async () => ({
361
+ categories: { urgent: [], action: [], fyi: [], newsletter: [], spam: [] },
362
+ total: 0,
363
+ message: 'Connect an email account to enable triage. Use /connect google-workspace',
364
+ }),
365
+ engine: emailTriageEngine,
366
+ },
367
+ summarizer: {
368
+ summarizeThread: async () => ({
369
+ summary: 'Connect an email account to enable thread summarization.',
370
+ }),
371
+ engine: threadSummarizer,
372
+ },
373
+ });
374
+ this.logger.info('Email intelligence initialized');
375
+ // Initialize calendar intelligence (engines ready; connectors needed for full functionality)
376
+ const scheduleAnalyzer = new ScheduleAnalyzer();
377
+ const scheduleOptimizer = new ScheduleOptimizer();
378
+ const meetingPrepGenerator = new MeetingPrepGenerator();
379
+ setCalendarIntelligence({
380
+ analyzeDay: async (date) => scheduleAnalyzer.analyzeDay([], date),
381
+ suggest: (analysis) => scheduleOptimizer.suggest(analysis),
382
+ getMeetingBrief: async () => ({
383
+ message: 'Connect a calendar account to enable meeting preparation. Use /connect google-workspace',
384
+ }),
385
+ analyzer: scheduleAnalyzer,
386
+ optimizer: scheduleOptimizer,
387
+ meetingPrep: meetingPrepGenerator,
388
+ });
389
+ this.logger.info('Calendar intelligence initialized');
390
+ // Initialize contacts system
391
+ const contactGraph = new ContactGraph();
392
+ const contextRecall = new ContextRecall(contactGraph);
393
+ setContactGraph(contactGraph);
394
+ setContextRecall(contextRecall);
395
+ this.logger.info('Contacts system initialized');
396
+ // Initialize compose system
397
+ const composeEngine = new ComposeEngine();
398
+ const grammarChecker = new GrammarChecker();
399
+ const languageDetector = new LanguageDetector();
400
+ setComposeEngine(composeEngine);
401
+ setGrammarChecker(grammarChecker);
402
+ setLanguageDetector(languageDetector);
403
+ this.logger.info('Compose system initialized');
404
+ // Initialize voice system (if enabled and OpenAI key available)
405
+ if (this.config.voice?.enabled) {
406
+ let openaiKeyForVoice;
407
+ try {
408
+ openaiKeyForVoice = this.vault.get('OPENAI_API_KEY');
409
+ }
410
+ catch {
411
+ // Vault locked
412
+ }
413
+ if (openaiKeyForVoice) {
414
+ this.voiceManager = new VoiceManager({
415
+ sttProvider: new WhisperSTT({ apiKey: openaiKeyForVoice }),
416
+ ttsProvider: new OpenAITTS({
417
+ apiKey: openaiKeyForVoice,
418
+ defaultVoice: this.config.voice.defaultVoice,
419
+ }),
420
+ config: {
421
+ enabled: true,
422
+ defaultVoice: this.config.voice.defaultVoice,
423
+ language: this.config.voice.language,
424
+ maxAudioDuration: this.config.voice.maxAudioDuration,
425
+ sampleRate: this.config.voice.sampleRate,
426
+ },
427
+ });
428
+ this.gateway.onVoiceMessage(this.handleVoiceMessage.bind(this));
429
+ this.logger.info('Voice mode enabled');
430
+ }
431
+ else {
432
+ this.logger.warn('Voice mode enabled in config but no OPENAI_API_KEY found in vault');
433
+ }
434
+ }
435
+ // Initialize webhook system (if enabled)
436
+ if (this.config.webhooks?.enabled) {
437
+ this.webhookManager = new WebhookManager({
438
+ storePath: getWebhooksPath(),
439
+ config: {
440
+ enabled: true,
441
+ basePath: this.config.webhooks.basePath,
442
+ signatureHeader: this.config.webhooks.signatureHeader,
443
+ maxPayloadSize: this.config.webhooks.maxPayloadSize,
444
+ },
445
+ });
446
+ setWebhookManager(this.webhookManager);
447
+ // Wire behavior trigger
448
+ if (this.behaviors) {
449
+ this.webhookManager.setBehaviorTrigger(async (behaviorId, payload) => {
450
+ return this.behaviors.executeNow(behaviorId);
451
+ });
452
+ }
453
+ // Mount webhook routes
454
+ const webhookRouter = this.createWebhookRouter();
455
+ this.gateway.mountRouter(this.config.webhooks.basePath, webhookRouter);
456
+ this.logger.info('Webhook listeners enabled');
457
+ }
458
+ // Initialize dashboard (if enabled)
459
+ if (this.config.dashboard?.enabled) {
460
+ // Capture class reference for lazy getters (getters rebind `this` to the deps object)
461
+ const self = this;
462
+ const { router } = createDashboardRouter({
463
+ deps: {
464
+ vault: this.vault,
465
+ getActiveModel: () => {
466
+ const primary = this.config.provider.primary;
467
+ const fallback = this.config.provider.fallback;
468
+ const providerConfig = this.config.provider[primary];
469
+ return { provider: primary, fallback, model: providerConfig?.model ?? 'default' };
470
+ },
471
+ onVaultUnlocked: async () => {
472
+ // Re-initialize providers and channels after vault unlock on restart
473
+ await this.initializeProviders();
474
+ if (this.providers) {
475
+ this.initializeRouter();
476
+ setProviderFactory(this.providers);
477
+ }
478
+ const channels = this.channels;
479
+ if (!channels) {
480
+ await this.initializeChannels();
481
+ const ch = this.channels;
482
+ if (ch) {
483
+ await ch.connectAll();
484
+ this.logger.info('Channels connected after vault unlock');
485
+ }
486
+ }
487
+ // Restore connector tokens now that vault is accessible
488
+ if (this.connectorRegistry && this.connectorAuthManager) {
489
+ for (const connector of this.connectorRegistry.list()) {
490
+ try {
491
+ const tokenData = this.vault.get(`connectors.${connector.id}.tokens`);
492
+ if (tokenData) {
493
+ const tokens = typeof tokenData === 'string' ? JSON.parse(tokenData) : tokenData;
494
+ await this.connectorAuthManager.authenticate(connector.id, connector.auth, tokens);
495
+ }
496
+ }
497
+ catch (err) {
498
+ this.logger.warn(`Failed to restore tokens for connector ${connector.id}: ${err instanceof Error ? err.message : String(err)}`);
499
+ }
500
+ }
501
+ this.registerConnectorTools();
502
+ this.logger.info('Connector tools registered after vault unlock');
503
+ }
504
+ },
505
+ behaviors: this.behaviors ? {
506
+ list: (filter) => this.behaviors.list(filter),
507
+ get: (id) => this.behaviors.get(id),
508
+ create: (input) => this.behaviors.create(input),
509
+ update: (id, updates) => this.behaviors.update(id, updates),
510
+ remove: (id) => this.behaviors.remove(id),
511
+ } : undefined,
512
+ webhooks: this.webhookManager ? {
513
+ list: () => this.webhookManager.list(),
514
+ create: (options) => this.webhookManager.create(options),
515
+ update: (id, updates) => this.webhookManager.update(id, updates),
516
+ delete: (id) => this.webhookManager.delete(id),
517
+ } : undefined,
518
+ getConfiguredChannels: () => {
519
+ const ch = this.config.channels;
520
+ return Object.entries(ch)
521
+ .filter(([, v]) => typeof v === 'object' && v !== null)
522
+ .map(([type, v]) => ({ type, enabled: !!v.enabled }));
523
+ },
524
+ getActiveAgents: () => this.getActiveAgents(),
525
+ getHealthState: () => this.healthMonitor?.getHealthState() ?? { overall: 'unknown', subsystems: [], issues: [], lastCheck: '' },
526
+ getCapabilities: () => this.capabilityCatalog?.getCatalog() ?? null,
527
+ getConnections: () => this.gateway.getConnections(),
528
+ getAuditEntries: async (limit) => {
529
+ const auditLogger = getAuditLogger();
530
+ return auditLogger.getEntries(limit);
531
+ },
532
+ getPlugins: () => this.pluginLoader?.listPlugins() ?? [],
533
+ getMemories: async () => this.memoryStore?.getAll() ?? [],
534
+ get connectors() {
535
+ const reg = self.connectorRegistry;
536
+ const auth = self.connectorAuthManager;
537
+ if (!reg || !auth)
538
+ return undefined;
539
+ return {
540
+ list: () => reg.list().map(c => ({
541
+ id: c.id, name: c.name, category: c.category,
542
+ auth: { type: c.auth.type },
543
+ })),
544
+ get: (id) => reg.get(id),
545
+ connect: async (connectorId, credentials) => {
546
+ const connector = reg.get(connectorId);
547
+ if (!connector)
548
+ return null;
549
+ return auth.authenticate(connectorId, connector.auth, credentials);
550
+ },
551
+ disconnect: async (connectorId) => {
552
+ return auth.revokeToken(connectorId);
553
+ },
554
+ getActions: (connectorId) => reg.getActions(connectorId),
555
+ executeAction: async (connectorId, actionId, params) => {
556
+ const connector = reg.get(connectorId);
557
+ if (!connector)
558
+ return { success: false, error: 'Connector not found' };
559
+ const token = auth.getToken(connectorId);
560
+ if (!token)
561
+ return { success: false, error: 'Not authenticated' };
562
+ try {
563
+ const data = await connector.executeAction(actionId, params, token.accessToken);
564
+ return { success: true, data };
565
+ }
566
+ catch (err) {
567
+ return { success: false, error: err instanceof Error ? err.message : String(err) };
568
+ }
569
+ },
570
+ };
571
+ },
572
+ models: this.providers ? {
573
+ listProviders: () => {
574
+ const result = [];
575
+ for (const name of this.providers.listAvailable()) {
576
+ const p = this.providers.getProvider(name);
577
+ // Detect credential source for Anthropic
578
+ let credentialSource;
579
+ if (name === 'anthropic') {
580
+ try {
581
+ if (this.vault.get('ANTHROPIC_OAUTH_TOKEN') || (this.vault.get('ANTHROPIC_API_KEY') && isSetupToken(this.vault.get('ANTHROPIC_API_KEY')))) {
582
+ credentialSource = 'oauth';
583
+ }
584
+ else if (this.vault.get('ANTHROPIC_API_KEY')) {
585
+ credentialSource = 'api-key';
586
+ }
587
+ else if (readClaudeCliCredentials() !== null) {
588
+ credentialSource = 'claude-cli';
589
+ }
590
+ }
591
+ catch {
592
+ // vault locked — check CLI as last resort
593
+ if (readClaudeCliCredentials() !== null) {
594
+ credentialSource = 'claude-cli';
595
+ }
596
+ }
597
+ }
598
+ result.push({
599
+ name,
600
+ displayName: credentialSource === 'claude-cli' ? 'Anthropic Claude (via Claude Code)' : p.metadata.displayName,
601
+ available: true,
602
+ models: p.metadata.models,
603
+ credentialSource,
604
+ });
605
+ }
606
+ return result;
607
+ },
608
+ getRoutingConfig: () => ({
609
+ enabled: this.config.routing?.enabled !== false,
610
+ primary: this.config.provider.primary,
611
+ fallback: this.config.provider.fallback,
612
+ defaultModel: this.config.routing?.defaultModel,
613
+ rules: this.config.routing?.rules ?? [],
614
+ preferences: this.config.routing?.preferences ?? {},
615
+ costLimits: this.config.routing?.costLimits ?? {},
616
+ }),
617
+ getCostSummary: () => this.modelRouter?.getCostSummary() ?? {
618
+ today: 0, thisMonth: 0, isOverBudget: false, warningThresholdReached: false,
619
+ },
620
+ } : undefined,
621
+ orchestration: this.orchestrationEngine ? {
622
+ getConfig: () => ({
623
+ enabled: this.config.orchestration?.enabled !== false,
624
+ maxConcurrentAgents: this.config.orchestration?.maxConcurrentAgents ?? 5,
625
+ allowedPatterns: this.config.orchestration?.allowedPatterns ?? [],
626
+ }),
627
+ getHistory: (limit) => {
628
+ const max = limit ?? 20;
629
+ return this.orchestrationHistory.slice(-max);
630
+ },
631
+ } : undefined,
632
+ memory: {
633
+ getLivingState: async () => {
634
+ const state = await this.getLivingMemoryState();
635
+ return state ?? { facts: [], relationships: [], patterns: [], adaptations: [], stats: { totalMemories: 0, oldestMemory: 0, newestMemory: 0, averageImportance: 0, topTags: [] } };
636
+ },
637
+ getStats: async () => this.memoryStore ? this.memoryStore.getStats() : {},
638
+ getAdaptations: async () => this.personalityAdapter ? this.personalityAdapter.getAdjustments() : [],
639
+ deleteMemory: async (id) => this.memoryStore ? this.memoryStore.remove(id) : false,
640
+ exportAll: async () => this.memoryStore ? this.memoryStore.exportAll() : { version: '1.0', memories: [], exportedAt: Date.now() },
641
+ importAll: async (data) => this.memoryStore ? this.memoryStore.importAll(data) : { imported: 0, skipped: 0 },
642
+ },
643
+ sessions: {
644
+ getWebchatMessages: async () => {
645
+ // Use getOrCreate so the session is loaded from disk if not yet in memory
646
+ const webchat = await this.sessions.getOrCreate('webchat', {
647
+ channelType: 'webchat',
648
+ });
649
+ return webchat.messages
650
+ .filter(m => m.role === 'user' || m.role === 'assistant')
651
+ .map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp }));
652
+ },
653
+ listChats: (options) => this.sessions.listChats(options),
654
+ createChat: (title) => this.sessions.createChat(title),
655
+ renameChat: (chatId, title) => this.sessions.renameChat(chatId, title),
656
+ archiveChat: (chatId) => this.sessions.archiveChat(chatId),
657
+ deleteChat: (chatId) => this.sessions.deleteChat(chatId),
658
+ getChatMessages: (chatId) => this.sessions.getChatMessages(chatId),
659
+ updateChatMetadata: (chatId, metadata) => this.sessions.updateChatMetadata(chatId, metadata),
660
+ },
661
+ getPersonalityEngine: () => this.config.agent.personality ?? 'standard',
662
+ setPersonalityEngine: (engine) => this.setPersonalityEngine(engine),
663
+ get trust() {
664
+ return self.trustEngine && self.trustAuditTrail && self.rollbackManager ? {
665
+ getLevels: () => self.trustEngine.getAllLevels(),
666
+ getLevel: (domain) => self.trustEngine.getTrustLevel(domain),
667
+ setLevel: async (domain, level, reason) => {
668
+ await self.trustEngine.setTrustLevel(domain, level, reason);
669
+ },
670
+ getAuditEntries: (limit) => self.trustAuditTrail.query({ limit }),
671
+ getAuditEntry: (id) => self.trustAuditTrail.getById(id),
672
+ rollback: async (id) => self.rollbackManager.rollback(id),
673
+ getPromotions: () => self.trustEngine.getPromotions(),
674
+ } : undefined;
675
+ },
676
+ // [P14] Team / Social
677
+ get team() {
678
+ return self.userManager ? {
679
+ listUsers: () => self.userManager.listUsers(),
680
+ createUser: (name, role, channels) => self.userManager.createUser(name, role, { channels }),
681
+ deleteUser: (id) => self.userManager.deleteUser(id),
682
+ } : undefined;
683
+ },
684
+ // [P14] Workflows
685
+ get workflows() {
686
+ return self.workflowEngine && self.approvalManager ? {
687
+ listActive: () => self.workflowEngine.listActive(),
688
+ listAll: () => self.workflowEngine.listAll(),
689
+ getStatus: (id) => self.workflowEngine.getStatus(id),
690
+ createWorkflow: (options) => self.workflowEngine.createWorkflow(options),
691
+ completeStep: (wfId, stepId, completedBy) => self.workflowEngine.completeStep(wfId, stepId, completedBy),
692
+ cancelWorkflow: (id) => self.workflowEngine.cancelWorkflow(id),
693
+ getPendingApprovals: (userId) => self.approvalManager.getPending(userId),
694
+ approve: (id, userId, reason) => self.approvalManager.approve(id, userId, reason),
695
+ reject: (id, userId, reason) => self.approvalManager.reject(id, userId, reason),
696
+ } : undefined;
697
+ },
698
+ // [P14] Agent Protocol
699
+ get agentProtocol() {
700
+ return self.agentProtocol && self.agentDirectory ? {
701
+ getIdentity: () => self.agentProtocol.getIdentity(),
702
+ getInbox: (limit) => self.agentProtocol.getInbox(limit),
703
+ discover: (query) => self.agentProtocol.discover(query),
704
+ getDirectory: () => self.agentDirectory.listAll(),
705
+ } : undefined;
706
+ },
707
+ // [P15] Screen
708
+ get screen() {
709
+ return self.screenCapturer ? {
710
+ capture: async () => {
711
+ const cap = await self.screenCapturer.captureScreen();
712
+ return { image: cap.image.toString('base64'), dimensions: cap.dimensions };
713
+ },
714
+ analyze: async (question) => {
715
+ if (!self.screenAnalyzer)
716
+ return 'Screen analyzer not available';
717
+ const cap = await self.screenCapturer.captureScreen();
718
+ return self.screenAnalyzer.analyzeScreen(cap.image, question);
719
+ },
720
+ } : undefined;
721
+ },
722
+ // [P15] Ambient
723
+ get ambient() {
724
+ return self.ambientEngine && self.ambientNotifications ? {
725
+ getPatterns: () => self.ambientEngine.getPatterns(),
726
+ getNotifications: () => self.ambientNotifications.getQueue(),
727
+ dismissNotification: (id) => self.ambientNotifications.dismiss(id),
728
+ getBriefing: (time) => {
729
+ return self.briefingGenerator.generateBriefing('dashboard', time === 'evening' ? 'evening' : 'morning', {
730
+ patterns: self.ambientEngine.getPatterns(),
731
+ notifications: self.ambientNotifications.getQueue(),
732
+ anticipations: self.anticipationEngine.getAnticipations(),
733
+ });
734
+ },
735
+ getAnticipations: () => self.anticipationEngine.getAnticipations(),
736
+ } : undefined;
737
+ },
738
+ // [P15] Conversation
739
+ get conversation() {
740
+ return self.conversationEngine ? {
741
+ getState: () => self.conversationEngine.getState(),
742
+ start: () => self.conversationEngine.start(),
743
+ stop: () => self.conversationEngine.stop(),
744
+ getTurnCount: () => self.conversationEngine.getTurnCount(),
745
+ } : undefined;
746
+ },
747
+ setup: {
748
+ personality: (() => {
749
+ const mgr = new PersonalityManager(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../personality/templates'), getWorkspacePath());
750
+ return {
751
+ listTemplates: () => mgr.listTemplates(),
752
+ applyTemplate: (id) => mgr.applyTemplate(id),
753
+ buildCustom: (config) => mgr.buildCustom(config),
754
+ getActiveTemplate: async () => {
755
+ const soulPath = path.join(getWorkspacePath(), 'SOUL.md');
756
+ let content;
757
+ try {
758
+ content = await fs.readFile(soulPath, 'utf-8');
759
+ }
760
+ catch {
761
+ return null;
762
+ }
763
+ // Read template ID from frontmatter
764
+ const match = content.match(/^---\n[\s\S]*?template:\s*(\S+)/);
765
+ if (!match)
766
+ return null;
767
+ const templateId = match[1];
768
+ const template = await mgr.getTemplate(templateId);
769
+ return template ? { id: template.id, name: template.name } : null;
770
+ },
771
+ };
772
+ })(),
773
+ saveConfig: async (updates) => {
774
+ const current = await loadConfig();
775
+ const merged = deepMerge(current, updates);
776
+ await saveFullConfig(merged);
777
+ this.config = await loadConfig();
778
+ // Rebuild system prompt if agent identity changed
779
+ if (updates.agent) {
780
+ await this.loadPersonality();
781
+ }
782
+ },
783
+ getAgentName: () => this.config.agent?.name ?? 'Auxiora',
784
+ getAgentPronouns: () => this.config.agent?.pronouns ?? 'they/them',
785
+ getAgentConfig: () => (this.config.agent ?? {}),
786
+ getSoulContent: async () => {
787
+ try {
788
+ return await fs.readFile(getSoulPath(), 'utf-8');
789
+ }
790
+ catch {
791
+ return null;
792
+ }
793
+ },
794
+ saveSoulContent: async (content) => {
795
+ const soulPath = getSoulPath();
796
+ const dir = path.dirname(soulPath);
797
+ await fs.mkdir(dir, { recursive: true });
798
+ await fs.writeFile(soulPath, content, 'utf-8');
799
+ },
800
+ hasSoulFile: async () => {
801
+ try {
802
+ await fs.access(getSoulPath());
803
+ return true;
804
+ }
805
+ catch {
806
+ return false;
807
+ }
808
+ },
809
+ vaultExists: () => vaultExists(),
810
+ onSetupComplete: async () => {
811
+ await this.initializeProviders();
812
+ if (this.providers) {
813
+ this.initializeRouter();
814
+ setProviderFactory(this.providers);
815
+ this.logger.info('Providers re-initialized after setup');
816
+ }
817
+ // Reload personality so the agent uses the name entered during setup
818
+ await this.loadPersonality();
819
+ this.logger.info('Personality reloaded after setup');
820
+ // Connect channels now that vault is unlocked with credentials
821
+ await this.initializeChannels();
822
+ const channels = this.channels;
823
+ if (channels) {
824
+ await channels.connectAll();
825
+ this.logger.info('Channels connected after setup');
826
+ }
827
+ // Restore connector tokens now that vault is accessible
828
+ if (this.connectorRegistry && this.connectorAuthManager) {
829
+ for (const connector of this.connectorRegistry.list()) {
830
+ try {
831
+ const tokenData = this.vault.get(`connectors.${connector.id}.tokens`);
832
+ if (tokenData) {
833
+ const tokens = typeof tokenData === 'string' ? JSON.parse(tokenData) : tokenData;
834
+ await this.connectorAuthManager.authenticate(connector.id, connector.auth, tokens);
835
+ }
836
+ }
837
+ catch (err) {
838
+ this.logger.warn(`Failed to restore tokens for connector ${connector.id}: ${err instanceof Error ? err.message : String(err)}`);
839
+ }
840
+ }
841
+ this.registerConnectorTools();
842
+ this.logger.info('Connector tools registered after setup');
843
+ }
844
+ },
845
+ },
846
+ },
847
+ config: {
848
+ enabled: true,
849
+ sessionTtlMs: this.config.dashboard.sessionTtlMs,
850
+ },
851
+ verifyPassword: (input) => {
852
+ try {
853
+ const stored = this.vault.get('DASHBOARD_PASSWORD');
854
+ if (!stored)
855
+ return false;
856
+ const a = Buffer.from(stored, 'utf-8');
857
+ const b = Buffer.from(input, 'utf-8');
858
+ if (a.length !== b.length)
859
+ return false;
860
+ return crypto.timingSafeEqual(a, b);
861
+ }
862
+ catch {
863
+ return false; // Vault locked
864
+ }
865
+ },
866
+ });
867
+ this.gateway.mountRouter('/api/v1/dashboard', router);
868
+ // Serve static SPA files
869
+ const dashboardUiPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../dashboard/dist-ui');
870
+ this.gateway.mountRouter('/dashboard', express.static(dashboardUiPath));
871
+ // SPA catch-all: serve index.html for client-side routing
872
+ const dashboardIndexPath = path.join(dashboardUiPath, 'index.html');
873
+ const spaRouter = Router();
874
+ spaRouter.get('/{*splat}', (_req, res) => {
875
+ res.sendFile(dashboardIndexPath);
876
+ });
877
+ this.gateway.mountRouter('/dashboard', spaRouter);
878
+ this.logger.info('Dashboard enabled at /dashboard');
879
+ }
880
+ // Initialize plugin system (if enabled)
881
+ if (this.config.plugins?.enabled !== false) {
882
+ const pluginsDir = this.config.plugins?.dir || undefined;
883
+ this.pluginLoader = new PluginLoader(pluginsDir);
884
+ const loaded = await this.pluginLoader.loadAll();
885
+ const successful = loaded.filter(p => p.status === 'loaded');
886
+ if (loaded.length > 0) {
887
+ this.logger.info(`Plugins: ${successful.length} loaded, ${loaded.length - successful.length} failed`);
888
+ }
889
+ }
890
+ // Initialize memory system (if enabled)
891
+ if (this.config.memory?.enabled !== false) {
892
+ this.memoryStore = new MemoryStore({
893
+ maxEntries: this.config.memory?.maxEntries,
894
+ });
895
+ this.memoryRetriever = new MemoryRetriever();
896
+ this.patternDetector = new PatternDetector();
897
+ this.personalityAdapter = new PersonalityAdapter(this.memoryStore);
898
+ setMemoryStore(this.memoryStore);
899
+ // Create extractor with AI provider (needs providers initialized)
900
+ if (this.providers && this.providers.listAvailable().length > 0) {
901
+ try {
902
+ this.memoryExtractor = new MemoryExtractor(this.memoryStore, this.providers.getPrimaryProvider());
903
+ }
904
+ catch {
905
+ this.logger.warn('Memory extractor disabled: no AI provider available');
906
+ }
907
+ }
908
+ // Set up periodic cleanup of expired memories
909
+ const cleanupMinutes = this.config.memory?.cleanupIntervalMinutes;
910
+ if (cleanupMinutes) {
911
+ this.memoryCleanupInterval = setInterval(() => void this.memoryStore?.cleanExpired(), cleanupMinutes * 60 * 1000);
912
+ }
913
+ this.logger.info('Memory system enabled (living memory)');
914
+ if (this.pluginLoader) {
915
+ this.pluginLoader.setMemoryStore(this.memoryStore);
916
+ }
917
+ }
918
+ // Initialize trust engine (if enabled)
919
+ if (this.config.trust?.enabled !== false) {
920
+ this.trustEngine = new TrustEngine({
921
+ defaultLevel: (this.config.trust?.defaultLevel ?? 0),
922
+ autoPromote: this.config.trust?.autoPromote ?? true,
923
+ promotionThreshold: this.config.trust?.promotionThreshold ?? 10,
924
+ demotionThreshold: this.config.trust?.demotionThreshold ?? 3,
925
+ autoPromoteCeiling: (this.config.trust?.autoPromoteCeiling ?? 3),
926
+ });
927
+ await this.trustEngine.load();
928
+ this.trustAuditTrail = new ActionAuditTrail();
929
+ await this.trustAuditTrail.load();
930
+ this.rollbackManager = new RollbackManager(this.trustAuditTrail);
931
+ this.trustGate = new TrustGate(this.trustEngine);
932
+ this.logger.info('Trust engine initialized');
933
+ }
934
+ // Initialize intent parser (if enabled)
935
+ if (this.config.intent?.enabled !== false) {
936
+ this.intentParser = new IntentParser({
937
+ confidenceThreshold: this.config.intent?.confidenceThreshold ?? 0.3,
938
+ });
939
+ this.actionPlanner = new ActionPlanner();
940
+ this.logger.info('Intent parser initialized');
941
+ }
942
+ // [P14] Initialize team / social system
943
+ this.userManager = new UserManager();
944
+ this.workflowEngine = new WorkflowEngine();
945
+ this.approvalManager = new ApprovalManager();
946
+ this.logger.info('Team/social system initialized');
947
+ // [P14] Initialize agent protocol
948
+ const agentKeys = MessageSigner.generateKeyPair();
949
+ const agentSigner = new MessageSigner(agentKeys);
950
+ this.agentDirectory = new AgentDirectory();
951
+ const agentName = this.config.agent?.name ?? 'auxiora';
952
+ const agentHost = `${this.config.gateway.host}:${this.config.gateway.port}`;
953
+ const agentId = { user: agentName.toLowerCase(), host: agentHost };
954
+ await this.agentDirectory.register(agentId, agentName, agentKeys.publicKey, `http://${agentHost}/api/v1/agent-protocol`);
955
+ this.agentProtocol = new AgentProtocol(agentId, agentSigner, this.agentDirectory);
956
+ this.logger.info('Agent protocol initialized');
957
+ // [P15] Initialize ambient intelligence
958
+ this.ambientEngine = new AmbientPatternEngine();
959
+ this.ambientNotifications = new QuietNotificationManager();
960
+ this.briefingGenerator = new BriefingGenerator();
961
+ this.anticipationEngine = new AnticipationEngine();
962
+ this.logger.info('Ambient intelligence initialized');
963
+ // Initialize notification orchestrator
964
+ this.notificationHub = new NotificationHub();
965
+ this.dndManager = new DoNotDisturbManager();
966
+ this.notificationOrchestrator = new NotificationOrchestrator(this.notificationHub, this.dndManager, (notification) => {
967
+ this.gateway.broadcast({
968
+ type: 'notification',
969
+ payload: { content: notification.message, system: true },
970
+ });
971
+ this.deliverToAllChannels(notification.message);
972
+ });
973
+ this.logger.info('Notification orchestrator initialized');
974
+ // Initialize autonomous workflow executor
975
+ if (this.workflowEngine && this.trustGate && this.trustEngine && this.trustAuditTrail) {
976
+ this.autonomousExecutor = new AutonomousExecutor({
977
+ workflowEngine: this.workflowEngine,
978
+ trustGate: this.trustGate,
979
+ trustEngine: this.trustEngine,
980
+ auditTrail: this.trustAuditTrail,
981
+ executeTool: async (name, params) => {
982
+ const context = {
983
+ sessionId: 'autonomous',
984
+ workingDirectory: getWorkspacePath(),
985
+ timeout: 30000,
986
+ };
987
+ return toolExecutor.execute(name, params, context);
988
+ },
989
+ onWorkflowCompleted: (workflowId) => {
990
+ const msg = `Autonomous workflow ${workflowId} completed`;
991
+ this.gateway.broadcast({
992
+ type: 'notification',
993
+ payload: { content: msg, system: true },
994
+ });
995
+ this.deliverToAllChannels(msg);
996
+ },
997
+ onStepFailed: (workflowId, stepId, error) => {
998
+ const msg = `Workflow ${workflowId} step ${stepId} failed: ${error}`;
999
+ this.gateway.broadcast({
1000
+ type: 'notification',
1001
+ payload: { content: msg, system: true },
1002
+ });
1003
+ this.deliverToAllChannels(msg);
1004
+ },
1005
+ });
1006
+ this.autonomousExecutor.start(30_000);
1007
+ this.logger.info('Autonomous workflow executor started (30s tick)');
1008
+ }
1009
+ // Initialize connector registry and wire ambient scheduler
1010
+ this.connectorRegistry = new ConnectorRegistry();
1011
+ this.connectorAuthManager = new ConnectorAuthManager(this.vault);
1012
+ this.connectorRegistry.register(googleWorkspaceConnector);
1013
+ this.connectorRegistry.register(microsoftConnector);
1014
+ this.connectorRegistry.register(githubConnector);
1015
+ this.connectorRegistry.register(linearConnector);
1016
+ this.connectorRegistry.register(notionConnector);
1017
+ this.connectorRegistry.register(homeAssistantConnector);
1018
+ this.connectorRegistry.register(twitterConnector);
1019
+ this.connectorRegistry.register(linkedinConnector);
1020
+ this.connectorRegistry.register(redditConnector);
1021
+ this.connectorRegistry.register(instagramConnector);
1022
+ this.triggerManager = new TriggerManager(this.connectorRegistry, this.connectorAuthManager);
1023
+ // Restore connector tokens from vault (per-connector try-catch so one
1024
+ // failure doesn't prevent others from loading)
1025
+ for (const connector of this.connectorRegistry.list()) {
1026
+ try {
1027
+ const tokenData = this.vault.get(`connectors.${connector.id}.tokens`);
1028
+ if (tokenData) {
1029
+ const tokens = typeof tokenData === 'string' ? JSON.parse(tokenData) : tokenData;
1030
+ await this.connectorAuthManager.authenticate(connector.id, connector.auth, tokens);
1031
+ }
1032
+ }
1033
+ catch (err) {
1034
+ this.logger.warn(`Failed to restore tokens for connector ${connector.id}: ${err instanceof Error ? err.message : String(err)}`);
1035
+ }
1036
+ }
1037
+ // Register connector actions as AI-callable tools
1038
+ this.registerConnectorTools();
1039
+ // Wire ambient scheduler — always start it; connector-dependent features
1040
+ // (email/calendar) gracefully return empty when connectors aren't configured
1041
+ if (this.briefingGenerator) {
1042
+ const scheduler = new (await import('@auxiora/behaviors')).Scheduler();
1043
+ this.ambientScheduler = new AmbientScheduler({
1044
+ scheduler,
1045
+ connectorRegistry: this.connectorRegistry,
1046
+ triggerManager: this.triggerManager,
1047
+ briefingGenerator: this.briefingGenerator,
1048
+ notificationOrchestrator: this.notificationOrchestrator,
1049
+ deliveryChannel: async (msg) => {
1050
+ // Broadcast to all webchat connections
1051
+ this.gateway.broadcast({
1052
+ type: 'message',
1053
+ payload: { role: 'assistant', content: msg, system: true },
1054
+ });
1055
+ this.deliverToAllChannels(msg);
1056
+ },
1057
+ userId: 'default',
1058
+ config: DEFAULT_AMBIENT_SCHEDULER_CONFIG,
1059
+ });
1060
+ this.ambientScheduler.start();
1061
+ this.logger.info('Ambient scheduler started');
1062
+ }
1063
+ // [P15] Initialize conversation engine
1064
+ this.conversationEngine = new ConversationEngine();
1065
+ this.logger.info('Conversation engine initialized');
1066
+ // [P15] Initialize screen system (with mock backends — real backends injected at desktop layer)
1067
+ const mockCaptureBackend = {
1068
+ captureScreen: async () => ({ image: Buffer.alloc(0), timestamp: Date.now(), dimensions: { width: 0, height: 0 } }),
1069
+ captureRegion: async () => ({ image: Buffer.alloc(0), timestamp: Date.now(), dimensions: { width: 0, height: 0 } }),
1070
+ captureWindow: async () => ({ image: Buffer.alloc(0), timestamp: Date.now(), dimensions: { width: 0, height: 0 } }),
1071
+ };
1072
+ this.screenCapturer = new ScreenCapturer(mockCaptureBackend);
1073
+ this.logger.info('Screen system initialized (capture backend: mock)');
1074
+ // --- Self-awareness: capability catalog + health monitor ---
1075
+ const introspectionSources = {
1076
+ getTools: () => toolRegistry.list(),
1077
+ getConnectedChannels: () => this.channels?.getConnectedChannels() ?? [],
1078
+ getConfiguredChannels: () => this.channels?.getConfiguredChannels() ?? [],
1079
+ getDefaultChannelId: (type) => this.channels?.getDefaultChannelId(type),
1080
+ getBehaviors: async () => this.behaviors?.list() ?? [],
1081
+ getProviders: () => {
1082
+ if (!this.providers)
1083
+ return [];
1084
+ const names = this.providers.listAvailable();
1085
+ return names.map((n) => {
1086
+ const p = this.providers.getProvider(n);
1087
+ return { name: n, displayName: p.metadata.displayName, models: p.metadata.models };
1088
+ });
1089
+ },
1090
+ getPrimaryProviderName: () => this.config.provider.primary,
1091
+ getFallbackProviderName: () => this.config.provider.fallback,
1092
+ checkProviderAvailable: async (name) => {
1093
+ try {
1094
+ const p = this.providers?.getProvider(name);
1095
+ return p ? await p.metadata.isAvailable() : false;
1096
+ }
1097
+ catch {
1098
+ return false;
1099
+ }
1100
+ },
1101
+ getPlugins: () => (this.pluginLoader?.listPlugins() ?? []).map((p) => ({
1102
+ name: p.name, version: p.version, status: 'active',
1103
+ toolCount: p.toolCount, behaviorNames: p.behaviorNames,
1104
+ })),
1105
+ getFeatures: () => ({
1106
+ behaviors: this.config.features?.behaviors !== false,
1107
+ browser: this.config.features?.browser !== false,
1108
+ voice: !!this.config.features?.voice,
1109
+ webhooks: !!this.config.features?.webhooks,
1110
+ plugins: !!this.config.features?.plugins,
1111
+ memory: this.config.memory?.enabled !== false,
1112
+ }),
1113
+ getAuditEntries: async (limit) => {
1114
+ const al = getAuditLogger();
1115
+ return al.getEntries(limit);
1116
+ },
1117
+ getTrustLevel: (domain) => this.trustEngine?.getTrustLevel(domain) ?? 0,
1118
+ };
1119
+ this.capabilityCatalog = new CapabilityCatalogImpl(introspectionSources);
1120
+ await this.capabilityCatalog.rebuild();
1121
+ const initialHealth = { overall: 'healthy', subsystems: [], issues: [], lastCheck: new Date().toISOString() };
1122
+ this.capabilityPromptFragment = generatePromptFragment(this.capabilityCatalog.getCatalog(), initialHealth, this.getSelfAwarenessContext());
1123
+ const autoFixActions = {
1124
+ reconnectChannel: async () => false,
1125
+ restartBehavior: async (_id) => {
1126
+ // BehaviorManager does not yet expose a resume() method
1127
+ return false;
1128
+ },
1129
+ };
1130
+ this.healthMonitor = new HealthMonitorImpl(introspectionSources, autoFixActions);
1131
+ this.healthMonitor.onChange((state) => {
1132
+ this.capabilityPromptFragment = generatePromptFragment(this.capabilityCatalog.getCatalog(), state, this.getSelfAwarenessContext());
1133
+ this.gateway.broadcast({ type: 'health_update', payload: state }, (client) => client.authenticated);
1134
+ });
1135
+ this.healthMonitor.start(30_000);
1136
+ const introspectTool = createIntrospectTool(() => this.capabilityCatalog.getCatalog(), () => this.healthMonitor.getHealthState(), introspectionSources);
1137
+ toolRegistry.register(introspectTool);
1138
+ // Update catalog on relevant audit events
1139
+ const introspectionAuditLogger = getAuditLogger();
1140
+ const prevOnEntry = introspectionAuditLogger.onEntry;
1141
+ introspectionAuditLogger.onEntry = (entry) => {
1142
+ prevOnEntry?.(entry);
1143
+ if (entry.event.startsWith('channel.') || entry.event.startsWith('plugin.')) {
1144
+ this.capabilityCatalog?.rebuildSection(entry.event.startsWith('channel.') ? 'channels' : 'plugins');
1145
+ }
1146
+ };
1147
+ this.logger.info('Self-awareness initialized', {
1148
+ tools: this.capabilityCatalog.getCatalog().tools.length,
1149
+ channels: this.capabilityCatalog.getCatalog().channels.length,
1150
+ });
1151
+ }
1152
+ async initializeProviders() {
1153
+ let anthropicKey;
1154
+ let anthropicOAuthToken;
1155
+ let openaiKey;
1156
+ let googleKey;
1157
+ let groqKey;
1158
+ let deepseekKey;
1159
+ let cohereKey;
1160
+ let xaiKey;
1161
+ let replicateToken;
1162
+ let vaultLocked = false;
1163
+ try {
1164
+ anthropicKey = this.vault.get('ANTHROPIC_API_KEY');
1165
+ anthropicOAuthToken = this.vault.get('ANTHROPIC_OAUTH_TOKEN');
1166
+ openaiKey = this.vault.get('OPENAI_API_KEY');
1167
+ googleKey = this.vault.get('GOOGLE_API_KEY');
1168
+ groqKey = this.vault.get('GROQ_API_KEY');
1169
+ deepseekKey = this.vault.get('DEEPSEEK_API_KEY');
1170
+ cohereKey = this.vault.get('COHERE_API_KEY');
1171
+ xaiKey = this.vault.get('XAI_API_KEY');
1172
+ replicateToken = this.vault.get('REPLICATE_API_TOKEN');
1173
+ // Check if ANTHROPIC_API_KEY is actually an OAuth token (sk-ant-oat01-*)
1174
+ // This handles users who stored their OAuth token in the wrong vault key
1175
+ if (anthropicKey && isSetupToken(anthropicKey)) {
1176
+ anthropicOAuthToken = anthropicKey;
1177
+ anthropicKey = undefined;
1178
+ }
1179
+ }
1180
+ catch {
1181
+ vaultLocked = true;
1182
+ }
1183
+ // Initialize media processor with auto-detected providers
1184
+ // detectProviders() calls vault.get() — guard against locked vault
1185
+ try {
1186
+ this.mediaProcessor = new MediaProcessor(detectProviders(this.vault));
1187
+ }
1188
+ catch {
1189
+ this.mediaProcessor = new MediaProcessor([]);
1190
+ }
1191
+ // Check for Claude CLI credentials as fallback
1192
+ const cliCreds = readClaudeCliCredentials();
1193
+ const hasCliCredentials = cliCreds !== null;
1194
+ const hasAnthropic = anthropicKey || anthropicOAuthToken || hasCliCredentials;
1195
+ const hasOllama = this.config.provider.ollama?.model;
1196
+ const hasAnyKey = hasAnthropic || openaiKey || googleKey || groqKey || deepseekKey || cohereKey || xaiKey || replicateToken || hasOllama;
1197
+ if (!hasAnyKey) {
1198
+ if (vaultLocked) {
1199
+ this.logger.warn('Vault is locked. AI providers not initialized.');
1200
+ this.logger.warn('To use AI: auxiora vault add ANTHROPIC_API_KEY');
1201
+ }
1202
+ else {
1203
+ this.logger.warn('No API keys found in vault. Add with: auxiora vault add ANTHROPIC_API_KEY');
1204
+ this.logger.warn('Or for Claude Pro/Max OAuth: auxiora vault add ANTHROPIC_OAUTH_TOKEN');
1205
+ this.logger.warn('Or authenticate with: claude setup-token');
1206
+ }
1207
+ return;
1208
+ }
1209
+ // Build Anthropic config - prefer vault credentials, fall back to CLI
1210
+ let anthropicConfig;
1211
+ if (anthropicOAuthToken) {
1212
+ const tokenPrefix = anthropicOAuthToken.substring(0, 15);
1213
+ this.logger.info(`Using Anthropic OAuth token from vault (${tokenPrefix}...)`);
1214
+ const vault = this.vault;
1215
+ const expiresAtStr = vault.get('CLAUDE_OAUTH_EXPIRES_AT');
1216
+ const tokenExpiresAt = expiresAtStr ? Number(expiresAtStr) : undefined;
1217
+ if (tokenExpiresAt) {
1218
+ const minutesLeft = Math.round((tokenExpiresAt - Date.now()) / 60000);
1219
+ this.logger.info(`OAuth token expires in ${minutesLeft} minutes`);
1220
+ }
1221
+ anthropicConfig = {
1222
+ oauthToken: anthropicOAuthToken,
1223
+ tokenExpiresAt,
1224
+ model: this.config.provider.anthropic.model,
1225
+ maxTokens: this.config.provider.anthropic.maxTokens,
1226
+ onTokenRefresh: async () => {
1227
+ const rt = vault.get('CLAUDE_OAUTH_REFRESH_TOKEN');
1228
+ if (!rt) {
1229
+ this.logger.warn('No refresh token in vault — cannot auto-refresh. Re-authenticate via Dashboard > Settings > Provider.');
1230
+ return null;
1231
+ }
1232
+ this.logger.info('Attempting OAuth token refresh...');
1233
+ // Try PKCE client first (dashboard OAuth flow), then CLI client
1234
+ const methods = [
1235
+ { name: 'PKCE', fn: refreshPKCEOAuthToken },
1236
+ { name: 'CLI', fn: refreshOAuthToken },
1237
+ ];
1238
+ for (const method of methods) {
1239
+ try {
1240
+ const refreshed = await method.fn(rt);
1241
+ await vault.add('ANTHROPIC_OAUTH_TOKEN', refreshed.accessToken);
1242
+ await vault.add('CLAUDE_OAUTH_REFRESH_TOKEN', refreshed.refreshToken);
1243
+ await vault.add('CLAUDE_OAUTH_EXPIRES_AT', String(refreshed.expiresAt));
1244
+ this.logger.info(`OAuth token refreshed via ${method.name} client`);
1245
+ return refreshed.accessToken;
1246
+ }
1247
+ catch (err) {
1248
+ this.logger.warn(`${method.name} token refresh failed: ${err instanceof Error ? err.message : String(err)}`);
1249
+ }
1250
+ }
1251
+ this.logger.error('All token refresh methods failed. Re-authenticate via Dashboard > Settings > Provider.', { error: new Error('Token refresh exhausted') });
1252
+ return null;
1253
+ },
1254
+ };
1255
+ }
1256
+ else if (anthropicKey) {
1257
+ this.logger.info('Using Anthropic API key from vault');
1258
+ anthropicConfig = {
1259
+ apiKey: anthropicKey,
1260
+ model: this.config.provider.anthropic.model,
1261
+ maxTokens: this.config.provider.anthropic.maxTokens,
1262
+ };
1263
+ }
1264
+ else if (hasCliCredentials) {
1265
+ this.logger.info('Using Claude CLI credentials (~/.claude/.credentials.json)');
1266
+ anthropicConfig = {
1267
+ useCliCredentials: true,
1268
+ model: this.config.provider.anthropic.model,
1269
+ maxTokens: this.config.provider.anthropic.maxTokens,
1270
+ };
1271
+ }
1272
+ this.providers = new ProviderFactory({
1273
+ primary: this.config.provider.primary,
1274
+ fallback: this.config.provider.fallback,
1275
+ config: {
1276
+ anthropic: anthropicConfig,
1277
+ openai: openaiKey
1278
+ ? {
1279
+ apiKey: openaiKey,
1280
+ model: this.config.provider.openai.model,
1281
+ maxTokens: this.config.provider.openai.maxTokens,
1282
+ }
1283
+ : undefined,
1284
+ google: googleKey
1285
+ ? {
1286
+ apiKey: googleKey,
1287
+ model: this.config.provider.google.model,
1288
+ maxTokens: this.config.provider.google.maxTokens,
1289
+ }
1290
+ : undefined,
1291
+ ollama: {
1292
+ baseUrl: this.config.provider.ollama.baseUrl,
1293
+ model: this.config.provider.ollama.model,
1294
+ maxTokens: this.config.provider.ollama.maxTokens,
1295
+ },
1296
+ openaiCompatible: this.config.provider.openaiCompatible.baseUrl
1297
+ ? {
1298
+ baseUrl: this.config.provider.openaiCompatible.baseUrl,
1299
+ model: this.config.provider.openaiCompatible.model,
1300
+ maxTokens: this.config.provider.openaiCompatible.maxTokens,
1301
+ name: this.config.provider.openaiCompatible.name,
1302
+ }
1303
+ : undefined,
1304
+ groq: groqKey
1305
+ ? {
1306
+ apiKey: groqKey,
1307
+ model: this.config.provider.groq.model,
1308
+ maxTokens: this.config.provider.groq.maxTokens,
1309
+ }
1310
+ : undefined,
1311
+ deepseek: deepseekKey
1312
+ ? {
1313
+ apiKey: deepseekKey,
1314
+ model: this.config.provider.deepseek.model,
1315
+ maxTokens: this.config.provider.deepseek.maxTokens,
1316
+ }
1317
+ : undefined,
1318
+ cohere: cohereKey
1319
+ ? {
1320
+ apiKey: cohereKey,
1321
+ model: this.config.provider.cohere.model,
1322
+ maxTokens: this.config.provider.cohere.maxTokens,
1323
+ }
1324
+ : undefined,
1325
+ xai: xaiKey
1326
+ ? {
1327
+ apiKey: xaiKey,
1328
+ model: this.config.provider.xai.model,
1329
+ maxTokens: this.config.provider.xai.maxTokens,
1330
+ }
1331
+ : undefined,
1332
+ replicate: replicateToken
1333
+ ? {
1334
+ apiToken: replicateToken,
1335
+ model: this.config.provider.replicate.model,
1336
+ }
1337
+ : undefined,
1338
+ },
1339
+ });
1340
+ }
1341
+ initializeRouter() {
1342
+ // Gather provider metadata from available providers
1343
+ const availableProviders = new Map();
1344
+ for (const name of this.providers.listAvailable()) {
1345
+ const provider = this.providers.getProvider(name);
1346
+ if (provider.metadata) {
1347
+ availableProviders.set(name, provider.metadata);
1348
+ }
1349
+ }
1350
+ if (availableProviders.size === 0)
1351
+ return;
1352
+ const classifier = new TaskClassifier();
1353
+ const selector = new ModelSelector(availableProviders, this.config.routing);
1354
+ const costTracker = new CostTracker(this.config.routing.costLimits);
1355
+ this.modelRouter = new ModelRouter(classifier, selector, costTracker, availableProviders);
1356
+ this.logger.info(`Model router initialized with ${availableProviders.size} provider(s)`);
1357
+ }
1358
+ getRouter() {
1359
+ return this.modelRouter;
1360
+ }
1361
+ getOrchestrationEngine() {
1362
+ return this.orchestrationEngine;
1363
+ }
1364
+ getCostSummary() {
1365
+ return this.modelRouter?.getCostSummary();
1366
+ }
1367
+ async initializeChannels() {
1368
+ // Get channel tokens from vault
1369
+ let discordToken;
1370
+ let telegramToken;
1371
+ let slackBotToken;
1372
+ let slackAppToken;
1373
+ let twilioAccountSid;
1374
+ let twilioAuthToken;
1375
+ let twilioPhoneNumber;
1376
+ let emailAddress;
1377
+ let emailPassword;
1378
+ let emailImapHost;
1379
+ let emailImapPort;
1380
+ let emailSmtpHost;
1381
+ let emailSmtpPort;
1382
+ let matrixAccessToken;
1383
+ let matrixHomeserverUrl;
1384
+ let matrixUserId;
1385
+ let signalCliEndpoint;
1386
+ let signalPhoneNumber;
1387
+ let teamsAppId;
1388
+ let teamsAppPassword;
1389
+ let whatsappPhoneNumberId;
1390
+ let whatsappAccessToken;
1391
+ let whatsappVerifyToken;
1392
+ try {
1393
+ discordToken = this.vault.get('DISCORD_BOT_TOKEN');
1394
+ telegramToken = this.vault.get('TELEGRAM_BOT_TOKEN');
1395
+ slackBotToken = this.vault.get('SLACK_BOT_TOKEN');
1396
+ slackAppToken = this.vault.get('SLACK_APP_TOKEN');
1397
+ twilioAccountSid = this.vault.get('TWILIO_ACCOUNT_SID');
1398
+ twilioAuthToken = this.vault.get('TWILIO_AUTH_TOKEN');
1399
+ twilioPhoneNumber = this.vault.get('TWILIO_PHONE_NUMBER');
1400
+ emailAddress = this.vault.get('EMAIL_ADDRESS');
1401
+ emailPassword = this.vault.get('EMAIL_PASSWORD');
1402
+ emailImapHost = this.vault.get('EMAIL_IMAP_HOST');
1403
+ emailImapPort = this.vault.get('EMAIL_IMAP_PORT');
1404
+ emailSmtpHost = this.vault.get('EMAIL_SMTP_HOST');
1405
+ emailSmtpPort = this.vault.get('EMAIL_SMTP_PORT');
1406
+ matrixAccessToken = this.vault.get('MATRIX_ACCESS_TOKEN');
1407
+ matrixHomeserverUrl = this.vault.get('MATRIX_HOMESERVER_URL');
1408
+ matrixUserId = this.vault.get('MATRIX_USER_ID');
1409
+ signalCliEndpoint = this.vault.get('SIGNAL_CLI_ENDPOINT');
1410
+ signalPhoneNumber = this.vault.get('SIGNAL_PHONE_NUMBER');
1411
+ teamsAppId = this.vault.get('TEAMS_APP_ID');
1412
+ teamsAppPassword = this.vault.get('TEAMS_APP_PASSWORD');
1413
+ whatsappPhoneNumberId = this.vault.get('WHATSAPP_PHONE_NUMBER_ID');
1414
+ whatsappAccessToken = this.vault.get('WHATSAPP_ACCESS_TOKEN');
1415
+ whatsappVerifyToken = this.vault.get('WHATSAPP_VERIFY_TOKEN');
1416
+ }
1417
+ catch {
1418
+ // Vault is locked
1419
+ return;
1420
+ }
1421
+ const hasAnyChannel = (this.config.channels.discord.enabled && discordToken) ||
1422
+ (this.config.channels.telegram.enabled && telegramToken) ||
1423
+ (this.config.channels.slack.enabled && slackBotToken && slackAppToken) ||
1424
+ (this.config.channels.twilio.enabled && twilioAccountSid && twilioAuthToken) ||
1425
+ (this.config.channels.email.enabled && emailAddress && emailPassword && emailImapHost && emailSmtpHost) ||
1426
+ (this.config.channels.matrix.enabled && matrixAccessToken && matrixHomeserverUrl) ||
1427
+ (this.config.channels.signal.enabled && signalCliEndpoint && signalPhoneNumber) ||
1428
+ (this.config.channels.teams.enabled && teamsAppId && teamsAppPassword) ||
1429
+ (this.config.channels.whatsapp.enabled && whatsappPhoneNumberId && whatsappAccessToken && whatsappVerifyToken);
1430
+ if (!hasAnyChannel) {
1431
+ return;
1432
+ }
1433
+ this.channels = new ChannelManager({
1434
+ discord: this.config.channels.discord.enabled && discordToken
1435
+ ? {
1436
+ token: discordToken,
1437
+ mentionOnly: this.config.channels.discord.mentionOnly,
1438
+ }
1439
+ : undefined,
1440
+ telegram: this.config.channels.telegram.enabled && telegramToken
1441
+ ? {
1442
+ token: telegramToken,
1443
+ }
1444
+ : undefined,
1445
+ slack: this.config.channels.slack.enabled && slackBotToken && slackAppToken
1446
+ ? {
1447
+ botToken: slackBotToken,
1448
+ appToken: slackAppToken,
1449
+ }
1450
+ : undefined,
1451
+ twilio: this.config.channels.twilio.enabled && twilioAccountSid && twilioAuthToken && twilioPhoneNumber
1452
+ ? {
1453
+ accountSid: twilioAccountSid,
1454
+ authToken: twilioAuthToken,
1455
+ phoneNumber: twilioPhoneNumber,
1456
+ }
1457
+ : undefined,
1458
+ email: this.config.channels.email.enabled && emailAddress && emailPassword && emailImapHost && emailSmtpHost
1459
+ ? {
1460
+ imapHost: emailImapHost,
1461
+ imapPort: Number(emailImapPort) || 993,
1462
+ smtpHost: emailSmtpHost,
1463
+ smtpPort: Number(emailSmtpPort) || 465,
1464
+ email: emailAddress,
1465
+ password: emailPassword,
1466
+ pollInterval: this.config.channels.email.pollInterval,
1467
+ }
1468
+ : undefined,
1469
+ matrix: this.config.channels.matrix.enabled && matrixAccessToken && matrixHomeserverUrl && matrixUserId
1470
+ ? {
1471
+ homeserverUrl: matrixHomeserverUrl,
1472
+ userId: matrixUserId,
1473
+ accessToken: matrixAccessToken,
1474
+ autoJoinRooms: this.config.channels.matrix.autoJoinRooms,
1475
+ }
1476
+ : undefined,
1477
+ signal: this.config.channels.signal.enabled && signalCliEndpoint && signalPhoneNumber
1478
+ ? {
1479
+ signalCliEndpoint,
1480
+ phoneNumber: signalPhoneNumber,
1481
+ }
1482
+ : undefined,
1483
+ teams: this.config.channels.teams.enabled && teamsAppId && teamsAppPassword
1484
+ ? {
1485
+ microsoftAppId: teamsAppId,
1486
+ microsoftAppPassword: teamsAppPassword,
1487
+ }
1488
+ : undefined,
1489
+ whatsapp: this.config.channels.whatsapp.enabled && whatsappPhoneNumberId && whatsappAccessToken && whatsappVerifyToken
1490
+ ? {
1491
+ phoneNumberId: whatsappPhoneNumberId,
1492
+ accessToken: whatsappAccessToken,
1493
+ verifyToken: whatsappVerifyToken,
1494
+ }
1495
+ : undefined,
1496
+ });
1497
+ // Set up channel message handler
1498
+ this.channels.onMessage(this.handleChannelMessage.bind(this));
1499
+ this.channels.onError((error, channelType) => {
1500
+ this.logger.error('Channel error', { channelType, error: new Error(error.message) });
1501
+ });
1502
+ if (this.pluginLoader) {
1503
+ this.pluginLoader.setChannelManager(this.channels);
1504
+ }
1505
+ }
1506
+ buildIdentityPreamble(agent) {
1507
+ const lines = ['# Agent Identity'];
1508
+ lines.push(`You are ${agent.name} (${agent.pronouns}).`);
1509
+ lines.push('');
1510
+ lines.push('## Personality');
1511
+ lines.push(`Warmth: ${agent.tone.warmth}/1.0 | Directness: ${agent.tone.directness}/1.0 | Humor: ${agent.tone.humor}/1.0 | Formality: ${agent.tone.formality}/1.0`);
1512
+ lines.push(`Error handling style: ${agent.errorStyle}`);
1513
+ if (agent.expertise.length > 0) {
1514
+ lines.push('');
1515
+ lines.push('## Expertise');
1516
+ for (const area of agent.expertise) {
1517
+ lines.push(`- ${area}`);
1518
+ }
1519
+ }
1520
+ const phrases = Object.entries(agent.catchphrases).filter(([, v]) => v);
1521
+ if (phrases.length > 0) {
1522
+ lines.push('');
1523
+ lines.push('## Catchphrases');
1524
+ for (const [key, value] of phrases) {
1525
+ lines.push(`- ${key.charAt(0).toUpperCase() + key.slice(1)}: ${value}`);
1526
+ }
1527
+ }
1528
+ const hasJokeBoundaries = agent.boundaries.neverJokeAbout.length > 0;
1529
+ const hasAdviseBoundaries = agent.boundaries.neverAdviseOn.length > 0;
1530
+ if (hasJokeBoundaries || hasAdviseBoundaries) {
1531
+ lines.push('');
1532
+ lines.push('## Boundaries');
1533
+ if (hasJokeBoundaries) {
1534
+ lines.push(`Never joke about: ${agent.boundaries.neverJokeAbout.join(', ')}`);
1535
+ }
1536
+ if (hasAdviseBoundaries) {
1537
+ lines.push(`Never advise on: ${agent.boundaries.neverAdviseOn.join(', ')}`);
1538
+ }
1539
+ }
1540
+ return lines.join('\n');
1541
+ }
1542
+ async loadArchitectPersonality() {
1543
+ // Identity preamble first so the AI knows its configured name (e.g. "Aria")
1544
+ const agent = this.config.agent;
1545
+ const parts = [this.buildIdentityPreamble(agent), ARCHITECT_BASE_PROMPT];
1546
+ // Workspace files still provide user context
1547
+ try {
1548
+ parts.push(await fs.readFile(getAgentsPath(), 'utf-8'));
1549
+ }
1550
+ catch { /* no file */ }
1551
+ try {
1552
+ parts.push(await fs.readFile(getIdentityPath(), 'utf-8'));
1553
+ }
1554
+ catch { /* no file */ }
1555
+ try {
1556
+ const user = await fs.readFile(getUserPath(), 'utf-8');
1557
+ parts.push(`\n## About the User\n${user}`);
1558
+ }
1559
+ catch { /* no file */ }
1560
+ this.architectPrompt = parts.join('\n\n---\n\n');
1561
+ const storage = new VaultStorageAdapter(this.vault);
1562
+ this.architect = createArchitect(storage);
1563
+ await this.architect.initialize();
1564
+ if (this.capabilityPromptFragment) {
1565
+ this.architectPrompt += '\n\n---\n\n' + this.capabilityPromptFragment;
1566
+ }
1567
+ }
1568
+ async loadPersonality() {
1569
+ // Always initialize Architect engine (it's lightweight) so any chat can use it.
1570
+ // Wrap in try/catch: vault may be locked during setup mode.
1571
+ try {
1572
+ await this.loadArchitectPersonality();
1573
+ }
1574
+ catch {
1575
+ this.logger.warn('Architect personality not available (vault may be locked)');
1576
+ }
1577
+ // Initialize Architect bridge for state persistence and awareness bridging
1578
+ if (this.architect) {
1579
+ this.architectAwarenessCollector = new ArchitectAwarenessCollector();
1580
+ this.architectBridge = new ArchitectBridge(this.architect, this.architectAwarenessCollector, this.vault, {
1581
+ onEscalation: (alert, context) => {
1582
+ this.logger.warn('Escalation detected', {
1583
+ alert,
1584
+ domain: context.domain,
1585
+ emotion: context.emotionalRegister,
1586
+ });
1587
+ },
1588
+ });
1589
+ }
1590
+ // Build standard prompt
1591
+ const parts = [];
1592
+ // Build identity preamble from config
1593
+ const agent = this.config.agent;
1594
+ parts.push(this.buildIdentityPreamble(agent));
1595
+ // Add personality adaptations from living memory
1596
+ if (this.personalityAdapter) {
1597
+ const modifier = await this.personalityAdapter.getPromptModifier();
1598
+ if (modifier) {
1599
+ parts.push(modifier);
1600
+ }
1601
+ }
1602
+ // Load SOUL.md
1603
+ let soulContent;
1604
+ try {
1605
+ soulContent = await fs.readFile(getSoulPath(), 'utf-8');
1606
+ parts.push(soulContent);
1607
+ }
1608
+ catch {
1609
+ // No SOUL.md
1610
+ }
1611
+ // Apply SOUL.md domain biases to Architect trait mixing
1612
+ if (this.architect && soulContent) {
1613
+ const biases = parseSoulBiases(soulContent);
1614
+ for (const [trait, offset] of Object.entries(biases)) {
1615
+ this.architect.setTraitOverride(trait, offset).catch(() => { });
1616
+ }
1617
+ }
1618
+ // Load AGENTS.md
1619
+ try {
1620
+ const agents = await fs.readFile(getAgentsPath(), 'utf-8');
1621
+ parts.push(agents);
1622
+ }
1623
+ catch {
1624
+ // No AGENTS.md
1625
+ }
1626
+ // Load IDENTITY.md
1627
+ try {
1628
+ const identity = await fs.readFile(getIdentityPath(), 'utf-8');
1629
+ parts.push(identity);
1630
+ }
1631
+ catch {
1632
+ // No IDENTITY.md
1633
+ }
1634
+ // Load USER.md
1635
+ try {
1636
+ const user = await fs.readFile(getUserPath(), 'utf-8');
1637
+ parts.push(`\n## About the User\n${user}`);
1638
+ }
1639
+ catch {
1640
+ // No USER.md
1641
+ }
1642
+ if (parts.length > 1) {
1643
+ // Has content beyond the identity preamble
1644
+ this.standardPrompt = parts.join('\n\n---\n\n');
1645
+ }
1646
+ else {
1647
+ // Only identity preamble, no personality files — use enriched default
1648
+ this.standardPrompt = `You are ${agent.name}, a helpful AI assistant. Be concise, accurate, and friendly.`;
1649
+ }
1650
+ // Append tool usage guidance
1651
+ this.standardPrompt += '\n\n---\n\n## Tool Usage\n'
1652
+ + '- For reading web pages, searching, fetching articles, or looking up information, use the `web_browser` tool. It is fast, lightweight, and always available.\n'
1653
+ + '- Only use `browser_navigate` and other browser_* tools when you need JavaScript rendering or interactive features (clicking buttons, filling forms, taking screenshots).\n'
1654
+ + '- Never expose raw tool errors to the user. If a tool fails, explain the situation naturally.';
1655
+ // Append self-awareness capability fragment
1656
+ if (this.capabilityPromptFragment) {
1657
+ this.standardPrompt += '\n\n---\n\n' + this.capabilityPromptFragment;
1658
+ }
1659
+ // Set global system prompt based on config
1660
+ this.systemPrompt = this.config.agent.personality === 'the-architect'
1661
+ ? this.architectPrompt
1662
+ : this.standardPrompt;
1663
+ // Initialize dynamic self-awareness assembler
1664
+ if (this.config.selfAwareness?.enabled) {
1665
+ const storage = new InMemoryAwarenessStorage();
1666
+ const collectorConfig = this.config.selfAwareness.collectors ?? {};
1667
+ const collectors = [
1668
+ ...(collectorConfig.conversationReflector !== false ? [new ConversationReflector(storage)] : []),
1669
+ ...(collectorConfig.capacityMonitor !== false ? [new CapacityMonitor()] : []),
1670
+ ...(collectorConfig.knowledgeBoundary !== false ? [new KnowledgeBoundary(storage)] : []),
1671
+ ...(collectorConfig.relationshipModel !== false ? [new RelationshipModel(storage)] : []),
1672
+ ...(collectorConfig.temporalTracker !== false ? [new TemporalTracker(storage)] : []),
1673
+ ...(collectorConfig.environmentSensor !== false ? [new EnvironmentSensor()] : []),
1674
+ ...(collectorConfig.metaCognitor !== false ? [new MetaCognitor(storage)] : []),
1675
+ ];
1676
+ if (this.architectAwarenessCollector) {
1677
+ collectors.push(this.architectAwarenessCollector);
1678
+ }
1679
+ this.selfAwarenessAssembler = new SelfAwarenessAssembler(collectors, {
1680
+ tokenBudget: this.config.selfAwareness.tokenBudget ?? 500,
1681
+ });
1682
+ }
1683
+ }
1684
+ /** Switch the global personality engine at runtime (no restart required). */
1685
+ setPersonalityEngine(engine) {
1686
+ this.config.agent.personality = engine;
1687
+ this.systemPrompt = engine === 'the-architect'
1688
+ ? this.architectPrompt
1689
+ : this.standardPrompt;
1690
+ // Rebuild self-awareness fragment so the AI knows its personality changed
1691
+ if (this.capabilityCatalog && this.healthMonitor) {
1692
+ this.capabilityPromptFragment = generatePromptFragment(this.capabilityCatalog.getCatalog(), this.healthMonitor.getHealthState(), this.getSelfAwarenessContext());
1693
+ }
1694
+ }
1695
+ getSelfAwarenessContext() {
1696
+ const primary = this.config.provider.primary;
1697
+ // Read model directly from the live provider instance — this is the actual
1698
+ // model being used, not just what the config file says.
1699
+ const provider = this.providers.getPrimaryProvider();
1700
+ return {
1701
+ defaultModel: provider.defaultModel,
1702
+ primaryProvider: primary,
1703
+ personalityEngine: this.config.agent.personality ?? 'standard',
1704
+ };
1705
+ }
1706
+ /** Append Architect context modifier when active, returning context metadata. */
1707
+ applyArchitectEnrichment(prompt, userMessage, chatId) {
1708
+ if (!this.architect)
1709
+ return { prompt };
1710
+ const output = this.architect.generatePrompt(userMessage);
1711
+ // Bridge handles side effects: persistence, awareness feeding, escalation logging
1712
+ if (this.architectBridge && chatId) {
1713
+ this.architectBridge.afterPrompt(output.detectedContext, output.emotionalTrajectory, output.escalationAlert, chatId);
1714
+ }
1715
+ const mix = this.architect.getTraitMix(output.detectedContext);
1716
+ const traitWeights = {};
1717
+ for (const [key, val] of Object.entries(mix)) {
1718
+ traitWeights[key] = val;
1719
+ }
1720
+ return {
1721
+ prompt: prompt + '\n\n' + output.contextModifier,
1722
+ architectMeta: {
1723
+ detectedContext: output.detectedContext,
1724
+ activeTraits: output.activeTraits,
1725
+ traitWeights,
1726
+ recommendation: output.recommendation,
1727
+ escalationAlert: output.escalationAlert,
1728
+ },
1729
+ };
1730
+ }
1731
+ async initializeModes() {
1732
+ if (this.config.modes?.enabled === false)
1733
+ return;
1734
+ const builtInModesDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../personality/modes');
1735
+ const userModesDir = getModesDir();
1736
+ this.modeLoader = new ModeLoader(builtInModesDir, userModesDir);
1737
+ await this.modeLoader.loadAll();
1738
+ this.modeDetector = new ModeDetector(this.modeLoader.getAll());
1739
+ this.promptAssembler = new PromptAssembler(this.config.agent, this.modeLoader, this.personalityAdapter ?? undefined);
1740
+ await this.promptAssembler.buildBase();
1741
+ this.userPreferences = this.config.modes?.preferences;
1742
+ this.securityFloor = new SecurityFloor();
1743
+ }
1744
+ getSessionModeState(sessionId) {
1745
+ let state = this.sessionModes.get(sessionId);
1746
+ if (!state) {
1747
+ const defaultMode = this.config.modes?.defaultMode ?? 'auto';
1748
+ state = { activeMode: defaultMode, autoDetected: false };
1749
+ this.sessionModes.set(sessionId, state);
1750
+ }
1751
+ return state;
1752
+ }
1753
+ /** Build enriched prompt with auto-detection (shared by handleMessage and handleChannelMessage). */
1754
+ buildModeEnrichedPrompt(content, modeState, memorySection, channelType) {
1755
+ if (modeState.activeMode === 'auto' && this.modeDetector && this.config.modes?.autoDetection !== false) {
1756
+ const detection = this.modeDetector.detect(content, { currentState: modeState });
1757
+ if (detection) {
1758
+ modeState.lastAutoMode = detection.mode;
1759
+ modeState.autoDetected = true;
1760
+ modeState.lastSwitchAt = Date.now();
1761
+ const tempState = { ...modeState, activeMode: detection.mode };
1762
+ return this.promptAssembler.enrichForMessage(tempState, memorySection, this.userPreferences, undefined, channelType);
1763
+ }
1764
+ }
1765
+ return this.promptAssembler.enrichForMessage(modeState, memorySection, this.userPreferences, undefined, channelType);
1766
+ }
1767
+ async handleMessage(client, message) {
1768
+ const { id: requestId, payload } = message;
1769
+ // Handle architect correction messages (learning engine)
1770
+ if (message.type === 'architect_correction') {
1771
+ const corrPayload = payload;
1772
+ if (this.architect && corrPayload?.userMessage && corrPayload.detectedDomain && corrPayload.correctedDomain) {
1773
+ await this.architect.recordCorrection(corrPayload.userMessage, corrPayload.detectedDomain, corrPayload.correctedDomain);
1774
+ }
1775
+ return;
1776
+ }
1777
+ const msgPayload = payload;
1778
+ const content = msgPayload?.content;
1779
+ const modelOverride = msgPayload?.model;
1780
+ const providerOverride = msgPayload?.provider;
1781
+ const thinkingLevel = msgPayload?.thinkingLevel;
1782
+ const chatId = msgPayload?.chatId;
1783
+ if (!content || typeof content !== 'string') {
1784
+ this.sendToClient(client, {
1785
+ type: 'error',
1786
+ id: requestId,
1787
+ payload: { message: 'Missing message content' },
1788
+ });
1789
+ return;
1790
+ }
1791
+ // Handle commands
1792
+ if (content.startsWith('/')) {
1793
+ await this.handleCommand(client, content, requestId);
1794
+ return;
1795
+ }
1796
+ // Get or create session — use chatId if provided (multi-chat), otherwise legacy behavior
1797
+ let session;
1798
+ if (chatId) {
1799
+ const existing = await this.sessions.get(chatId);
1800
+ if (existing) {
1801
+ session = existing;
1802
+ }
1803
+ else {
1804
+ session = await this.sessions.create({ channelType: 'webchat', clientId: client.id });
1805
+ this.sendToClient(client, {
1806
+ type: 'chat_created',
1807
+ id: requestId,
1808
+ payload: { chatId: session.id },
1809
+ });
1810
+ }
1811
+ }
1812
+ else {
1813
+ session = await this.sessions.getOrCreate(client.id, {
1814
+ channelType: client.channelType,
1815
+ clientId: client.id,
1816
+ senderId: client.senderId,
1817
+ });
1818
+ }
1819
+ // Add user message
1820
+ await this.sessions.addMessage(session.id, 'user', content);
1821
+ // Check if providers are available
1822
+ if (!this.providers) {
1823
+ this.sendToClient(client, {
1824
+ type: 'message',
1825
+ id: requestId,
1826
+ payload: {
1827
+ role: 'assistant',
1828
+ content: 'I need API keys to respond. Please add them:\n\n```\nauxiora vault add ANTHROPIC_API_KEY\n```',
1829
+ },
1830
+ });
1831
+ return;
1832
+ }
1833
+ // Get context messages
1834
+ const contextMessages = this.sessions.getContextMessages(session.id, this.getProviderMaxTokens(this.providers.getPrimaryProvider()), 4096);
1835
+ const chatMessages = sanitizeTranscript(contextMessages).map((m) => ({
1836
+ role: m.role,
1837
+ content: m.content,
1838
+ }));
1839
+ try {
1840
+ // Get tool definitions from registry
1841
+ const tools = toolRegistry.toProviderFormat();
1842
+ // Resolve per-chat personality (metadata overrides global default)
1843
+ const chatRecord = chatId ? this.sessions.getChat(chatId) : undefined;
1844
+ const chatPersonality = chatRecord?.metadata?.personality;
1845
+ const useArchitect = chatPersonality
1846
+ ? chatPersonality === 'the-architect'
1847
+ : this.config.agent.personality === 'the-architect';
1848
+ const basePrompt = useArchitect ? this.architectPrompt : this.standardPrompt;
1849
+ // Build enriched prompt with modes and memories
1850
+ let enrichedPrompt = basePrompt;
1851
+ let memorySection = null;
1852
+ if (this.memoryRetriever && this.memoryStore) {
1853
+ const memories = await this.memoryStore.getAll();
1854
+ memorySection = this.memoryRetriever.retrieve(memories, content);
1855
+ }
1856
+ if (this.promptAssembler && this.config.modes?.enabled !== false) {
1857
+ const modeState = this.getSessionModeState(session.id);
1858
+ // Security context check — BEFORE mode detection
1859
+ if (this.securityFloor) {
1860
+ const securityContext = this.securityFloor.detectSecurityContext({ userMessage: content });
1861
+ if (securityContext.active) {
1862
+ // Suspend current mode and use security floor prompt
1863
+ modeState.suspendedMode = modeState.activeMode;
1864
+ enrichedPrompt = this.promptAssembler.enrichForSecurityContext(securityContext, this.securityFloor, memorySection);
1865
+ }
1866
+ else if (modeState.suspendedMode) {
1867
+ // Restore suspended mode
1868
+ modeState.activeMode = modeState.suspendedMode;
1869
+ delete modeState.suspendedMode;
1870
+ enrichedPrompt = this.promptAssembler.enrichForMessage(modeState, memorySection, this.userPreferences, undefined, 'webchat');
1871
+ }
1872
+ else {
1873
+ // Normal mode detection
1874
+ enrichedPrompt = this.buildModeEnrichedPrompt(content, modeState, memorySection, 'webchat');
1875
+ }
1876
+ }
1877
+ else {
1878
+ // No security floor — normal mode detection
1879
+ enrichedPrompt = this.buildModeEnrichedPrompt(content, modeState, memorySection, 'webchat');
1880
+ }
1881
+ }
1882
+ else if (memorySection) {
1883
+ enrichedPrompt = basePrompt + memorySection;
1884
+ }
1885
+ // Only apply Architect enrichment if this chat uses the Architect
1886
+ const architectResult = useArchitect
1887
+ ? this.applyArchitectEnrichment(enrichedPrompt, content, chatId)
1888
+ : { prompt: enrichedPrompt };
1889
+ enrichedPrompt = architectResult.prompt;
1890
+ // Inject dynamic self-awareness context
1891
+ if (this.selfAwarenessAssembler) {
1892
+ const awarenessContext = {
1893
+ userId: client.senderId ?? 'anonymous',
1894
+ sessionId: session.id,
1895
+ chatId: chatId ?? session.id,
1896
+ currentMessage: content,
1897
+ recentMessages: contextMessages,
1898
+ };
1899
+ const awarenessFragment = await this.selfAwarenessAssembler.assemble(awarenessContext);
1900
+ if (awarenessFragment) {
1901
+ enrichedPrompt += '\n\n[Dynamic Self-Awareness]\n' + awarenessFragment;
1902
+ }
1903
+ }
1904
+ // Route to best model for this message
1905
+ let provider;
1906
+ let routingResult;
1907
+ if (providerOverride || modelOverride) {
1908
+ // Manual override — skip router
1909
+ provider = this.providers.getProvider(providerOverride || this.config.provider.primary);
1910
+ }
1911
+ else if (this.modelRouter && this.config.routing?.enabled !== false) {
1912
+ try {
1913
+ routingResult = this.modelRouter.route(content, { hasImages: false });
1914
+ provider = this.providers.getProvider(routingResult.selection.provider);
1915
+ }
1916
+ catch {
1917
+ provider = this.providers.getPrimaryProvider();
1918
+ }
1919
+ }
1920
+ else {
1921
+ provider = this.providers.getPrimaryProvider();
1922
+ }
1923
+ // Execute streaming AI call with tool follow-up loop
1924
+ const fallbackCandidates = this.providers.resolveFallbackCandidates();
1925
+ const { response: fullResponse, usage } = await this.executeWithTools(session.id, chatMessages, enrichedPrompt, provider, (type, data) => {
1926
+ if (type === 'text') {
1927
+ this.sendToClient(client, { type: 'chunk', id: requestId, payload: { content: data } });
1928
+ }
1929
+ else if (type === 'thinking') {
1930
+ this.sendToClient(client, { type: 'thinking', id: requestId, payload: { content: data } });
1931
+ }
1932
+ else if (type === 'tool_use') {
1933
+ this.sendToClient(client, { type: 'tool_use', id: requestId, payload: data });
1934
+ }
1935
+ else if (type === 'tool_result') {
1936
+ this.sendToClient(client, { type: 'tool_result', id: requestId, payload: data });
1937
+ }
1938
+ else if (type === 'status') {
1939
+ this.sendToClient(client, { type: 'status', id: requestId, payload: data });
1940
+ }
1941
+ }, { tools, fallbackCandidates });
1942
+ // Save assistant message (skip if empty — happens when response is tool-only)
1943
+ if (fullResponse) {
1944
+ await this.sessions.addMessage(session.id, 'assistant', fullResponse, {
1945
+ input: usage.inputTokens,
1946
+ output: usage.outputTokens,
1947
+ });
1948
+ }
1949
+ // Record usage for cost tracking
1950
+ if (this.modelRouter && routingResult) {
1951
+ this.modelRouter.recordUsage(routingResult.selection.provider, routingResult.selection.model, usage.inputTokens, usage.outputTokens);
1952
+ }
1953
+ // Extract memories and learn from conversation (if auto-extract enabled)
1954
+ if (this.config.memory?.autoExtract !== false && this.memoryStore && fullResponse && content.length > 20) {
1955
+ void this.extractAndLearn(content, fullResponse, session.id);
1956
+ }
1957
+ // Auto-title webchat chats after first exchange
1958
+ if (fullResponse &&
1959
+ session.metadata.channelType === 'webchat' &&
1960
+ session.messages.length <= 3) {
1961
+ void this.generateChatTitle(session.id, content, fullResponse, client);
1962
+ }
1963
+ // Send done signal
1964
+ this.sendToClient(client, {
1965
+ type: 'done',
1966
+ id: requestId,
1967
+ payload: {
1968
+ usage,
1969
+ routing: routingResult ? {
1970
+ model: routingResult.selection.model,
1971
+ provider: routingResult.selection.provider,
1972
+ isLocal: routingResult.selection.isLocal,
1973
+ taskType: routingResult.classification.type,
1974
+ } : (providerOverride || modelOverride) ? {
1975
+ model: modelOverride,
1976
+ provider: providerOverride || this.config.provider.primary,
1977
+ override: true,
1978
+ } : undefined,
1979
+ architect: architectResult.architectMeta,
1980
+ },
1981
+ });
1982
+ // Background self-awareness analysis
1983
+ if (this.selfAwarenessAssembler) {
1984
+ this.selfAwarenessAssembler.afterResponse({
1985
+ userId: client.senderId ?? 'anonymous',
1986
+ sessionId: session.id,
1987
+ chatId: chatId ?? session.id,
1988
+ currentMessage: content,
1989
+ recentMessages: contextMessages,
1990
+ response: fullResponse,
1991
+ responseTime: Date.now() - (session.metadata.lastActiveAt ?? Date.now()),
1992
+ tokensUsed: { input: usage?.inputTokens ?? 0, output: usage?.outputTokens ?? 0 },
1993
+ }).catch(() => { });
1994
+ }
1995
+ audit('message.sent', {
1996
+ sessionId: session.id,
1997
+ inputTokens: usage.inputTokens,
1998
+ outputTokens: usage.outputTokens,
1999
+ model: routingResult?.selection.model,
2000
+ provider: routingResult?.selection.provider,
2001
+ });
2002
+ }
2003
+ catch (error) {
2004
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2005
+ audit('channel.error', { sessionId: session.id, error: errorMessage });
2006
+ this.sendToClient(client, {
2007
+ type: 'error',
2008
+ id: requestId,
2009
+ payload: { message: `Error: ${errorMessage}` },
2010
+ });
2011
+ }
2012
+ }
2013
+ async generateChatTitle(chatId, userMessage, assistantResponse, client) {
2014
+ try {
2015
+ // Check if the chat is still titled "New Chat"
2016
+ const chat = this.sessions.listChats().find(c => c.id === chatId);
2017
+ if (!chat || chat.title !== 'New Chat')
2018
+ return;
2019
+ const provider = this.providers?.getPrimaryProvider();
2020
+ if (!provider)
2021
+ return;
2022
+ const titlePrompt = 'Generate a very short title (3-6 words, no quotes, no punctuation at end) for this conversation. Reply with ONLY the title.';
2023
+ const result = await provider.complete([
2024
+ { role: 'user', content: userMessage },
2025
+ { role: 'assistant', content: assistantResponse.slice(0, 500) },
2026
+ { role: 'user', content: titlePrompt },
2027
+ ], { maxTokens: 30 });
2028
+ const title = result.content.trim().replace(/^["']|["']$/g, '').slice(0, 60);
2029
+ if (title) {
2030
+ this.sessions.renameChat(chatId, title);
2031
+ this.sendToClient(client, {
2032
+ type: 'chat_titled',
2033
+ payload: { chatId, title },
2034
+ });
2035
+ }
2036
+ }
2037
+ catch {
2038
+ // Non-fatal — chat keeps "New Chat" title
2039
+ }
2040
+ }
2041
+ async handleCommand(client, command, requestId) {
2042
+ const [cmd, ...args] = command.slice(1).split(' ');
2043
+ switch (cmd.toLowerCase()) {
2044
+ case 'status': {
2045
+ const activeSessions = this.sessions.getActiveSessions();
2046
+ const providers = this.providers?.listAvailable() || [];
2047
+ this.sendToClient(client, {
2048
+ type: 'message',
2049
+ id: requestId,
2050
+ payload: {
2051
+ role: 'assistant',
2052
+ content: `**Status**\n- Sessions: ${activeSessions.length} active\n- Providers: ${providers.join(', ') || 'none configured'}\n- Uptime: ${Math.floor(process.uptime())}s`,
2053
+ },
2054
+ });
2055
+ break;
2056
+ }
2057
+ case 'new':
2058
+ case 'reset': {
2059
+ const session = await this.sessions.getOrCreate(client.id, {
2060
+ channelType: client.channelType,
2061
+ clientId: client.id,
2062
+ });
2063
+ await this.sessions.clear(session.id);
2064
+ this.sendToClient(client, {
2065
+ type: 'message',
2066
+ id: requestId,
2067
+ payload: {
2068
+ role: 'assistant',
2069
+ content: 'Session cleared. Starting fresh!',
2070
+ },
2071
+ });
2072
+ break;
2073
+ }
2074
+ case 'mode': {
2075
+ const session = await this.sessions.getOrCreate(client.id, {
2076
+ channelType: client.channelType,
2077
+ clientId: client.id,
2078
+ });
2079
+ if (this.config.modes?.enabled === false || !this.modeLoader) {
2080
+ this.sendToClient(client, {
2081
+ type: 'message',
2082
+ id: requestId,
2083
+ payload: { role: 'assistant', content: 'Modes are disabled in configuration.' },
2084
+ });
2085
+ break;
2086
+ }
2087
+ const subCmd = args[0]?.toLowerCase();
2088
+ const modeState = this.getSessionModeState(session.id);
2089
+ if (!subCmd || subCmd === 'status') {
2090
+ const modes = this.modeLoader.getAll();
2091
+ const modeList = [...modes.values()].map(m => `- **${m.name}** (\`${m.id}\`) — ${m.description}`).join('\n');
2092
+ const currentLabel = modeState.activeMode === 'auto' ? 'auto (auto-detect)' : modeState.activeMode === 'off' ? 'off' : modeState.activeMode;
2093
+ this.sendToClient(client, {
2094
+ type: 'message',
2095
+ id: requestId,
2096
+ payload: {
2097
+ role: 'assistant',
2098
+ content: `**Current mode:** ${currentLabel}${modeState.lastAutoMode ? ` (last detected: ${modeState.lastAutoMode})` : ''}\n\n**Available modes:**\n${modeList}\n\n**Commands:** \`/mode <name>\`, \`/mode auto\`, \`/mode off\``,
2099
+ },
2100
+ });
2101
+ }
2102
+ else if (subCmd === 'auto') {
2103
+ modeState.activeMode = 'auto';
2104
+ modeState.autoDetected = false;
2105
+ modeState.lastSwitchAt = Date.now();
2106
+ this.sendToClient(client, {
2107
+ type: 'message',
2108
+ id: requestId,
2109
+ payload: { role: 'assistant', content: 'Mode set to **auto**. I will detect the best mode from your messages.' },
2110
+ });
2111
+ }
2112
+ else if (subCmd === 'off') {
2113
+ modeState.activeMode = 'off';
2114
+ modeState.autoDetected = false;
2115
+ modeState.lastSwitchAt = Date.now();
2116
+ this.sendToClient(client, {
2117
+ type: 'message',
2118
+ id: requestId,
2119
+ payload: { role: 'assistant', content: 'Modes disabled for this session.' },
2120
+ });
2121
+ }
2122
+ else if (MODE_IDS.includes(subCmd)) {
2123
+ modeState.activeMode = subCmd;
2124
+ modeState.autoDetected = false;
2125
+ modeState.lastSwitchAt = Date.now();
2126
+ const mode = this.modeLoader.get(subCmd);
2127
+ this.sendToClient(client, {
2128
+ type: 'message',
2129
+ id: requestId,
2130
+ payload: { role: 'assistant', content: `Switched to **${mode?.name ?? subCmd}** mode.` },
2131
+ });
2132
+ }
2133
+ else {
2134
+ this.sendToClient(client, {
2135
+ type: 'message',
2136
+ id: requestId,
2137
+ payload: { role: 'assistant', content: `Unknown mode: ${subCmd}. Use \`/mode\` to see available modes.` },
2138
+ });
2139
+ }
2140
+ break;
2141
+ }
2142
+ case 'help': {
2143
+ this.sendToClient(client, {
2144
+ type: 'message',
2145
+ id: requestId,
2146
+ payload: {
2147
+ role: 'assistant',
2148
+ content: `**Commands**\n- /status - Show system status\n- /new - Start a new session\n- /reset - Clear current session\n- /mode - Show/switch personality modes\n- /help - Show this help`,
2149
+ },
2150
+ });
2151
+ break;
2152
+ }
2153
+ default: {
2154
+ this.sendToClient(client, {
2155
+ type: 'message',
2156
+ id: requestId,
2157
+ payload: {
2158
+ role: 'assistant',
2159
+ content: `Unknown command: ${cmd}. Try /help`,
2160
+ },
2161
+ });
2162
+ }
2163
+ }
2164
+ }
2165
+ /**
2166
+ * Execute a streaming AI call with tool follow-up loop.
2167
+ * When the AI calls tools, executes them and feeds results back to the AI
2168
+ * for synthesis, looping up to maxToolRounds times.
2169
+ */
2170
+ async executeWithTools(sessionId, messages, enrichedPrompt, provider, onChunk, options) {
2171
+ const maxRounds = options?.maxToolRounds ?? 10;
2172
+ const maxContinuations = 3; // Safety cap for auto-continue on truncation
2173
+ const tools = options?.tools ?? toolRegistry.toProviderFormat();
2174
+ let currentMessages = [...messages];
2175
+ let totalUsage = { inputTokens: 0, outputTokens: 0 };
2176
+ let fullResponse = '';
2177
+ let lastRoundHadTools = false;
2178
+ const loopState = createLoopDetectionState();
2179
+ for (let round = 0; round < maxRounds; round++) {
2180
+ let roundResponse = '';
2181
+ let roundUsage = { inputTokens: 0, outputTokens: 0 };
2182
+ let roundFinishReason = '';
2183
+ const toolUses = [];
2184
+ const streamOptions = {
2185
+ systemPrompt: enrichedPrompt,
2186
+ tools: tools.length > 0 ? tools : undefined,
2187
+ passThroughAllTools: true,
2188
+ };
2189
+ const candidates = options?.fallbackCandidates ?? [
2190
+ { provider, name: provider.name, model: provider.defaultModel },
2191
+ ];
2192
+ for await (const chunk of streamWithModelFallback({ candidates }, (p) => p.stream(currentMessages, streamOptions))) {
2193
+ if (chunk.type === 'text' && chunk.content) {
2194
+ roundResponse += chunk.content;
2195
+ onChunk('text', chunk.content);
2196
+ }
2197
+ else if (chunk.type === 'thinking' && chunk.content) {
2198
+ onChunk('thinking', chunk.content);
2199
+ }
2200
+ else if (chunk.type === 'tool_use' && chunk.toolUse) {
2201
+ toolUses.push(chunk.toolUse);
2202
+ onChunk('tool_use', { tool: chunk.toolUse.name, params: chunk.toolUse.input });
2203
+ }
2204
+ else if (chunk.type === 'done') {
2205
+ roundUsage = chunk.usage || roundUsage;
2206
+ roundFinishReason = chunk.finishReason || '';
2207
+ }
2208
+ else if (chunk.type === 'error') {
2209
+ throw new Error(chunk.error);
2210
+ }
2211
+ }
2212
+ totalUsage.inputTokens += roundUsage.inputTokens;
2213
+ totalUsage.outputTokens += roundUsage.outputTokens;
2214
+ // No tool calls — check if response was truncated
2215
+ if (toolUses.length === 0) {
2216
+ fullResponse += roundResponse;
2217
+ // Auto-continue if response was cut off by token limit
2218
+ const wasTruncated = roundFinishReason === 'max_tokens' || roundFinishReason === 'length';
2219
+ if (wasTruncated && fullResponse.length > 0) {
2220
+ let continuations = 0;
2221
+ while (continuations < maxContinuations) {
2222
+ continuations++;
2223
+ this.logger.info('Response truncated, auto-continuing', { continuations, finishReason: roundFinishReason });
2224
+ currentMessages.push({ role: 'assistant', content: fullResponse });
2225
+ currentMessages.push({ role: 'user', content: 'Continue where you left off.' });
2226
+ let contResponse = '';
2227
+ let contUsage = { inputTokens: 0, outputTokens: 0 };
2228
+ let contFinishReason = '';
2229
+ for await (const chunk of provider.stream(currentMessages, {
2230
+ systemPrompt: enrichedPrompt,
2231
+ })) {
2232
+ if (chunk.type === 'text' && chunk.content) {
2233
+ contResponse += chunk.content;
2234
+ onChunk('text', chunk.content);
2235
+ }
2236
+ else if (chunk.type === 'done') {
2237
+ contUsage = chunk.usage || contUsage;
2238
+ contFinishReason = chunk.finishReason || '';
2239
+ }
2240
+ else if (chunk.type === 'error') {
2241
+ throw new Error(chunk.error);
2242
+ }
2243
+ }
2244
+ totalUsage.inputTokens += contUsage.inputTokens;
2245
+ totalUsage.outputTokens += contUsage.outputTokens;
2246
+ fullResponse += contResponse;
2247
+ // Stop if the model finished naturally
2248
+ if (contFinishReason !== 'max_tokens' && contFinishReason !== 'length') {
2249
+ break;
2250
+ }
2251
+ }
2252
+ }
2253
+ lastRoundHadTools = false;
2254
+ break;
2255
+ }
2256
+ lastRoundHadTools = true;
2257
+ // Record the assistant's response (including tool use intent) in the conversation
2258
+ const assistantContent = roundResponse || `I'll use ${toolUses.map(t => t.name).join(', ')} to help with this.`;
2259
+ currentMessages.push({ role: 'assistant', content: assistantContent });
2260
+ await this.sessions.addMessage(sessionId, 'assistant', assistantContent);
2261
+ // Execute tools and collect results
2262
+ const context = {
2263
+ sessionId,
2264
+ workingDirectory: getWorkspacePath(),
2265
+ timeout: 30000,
2266
+ };
2267
+ const toolResultParts = [];
2268
+ for (const toolUse of toolUses) {
2269
+ // Map Claude Code emulation tool names to our actual tools
2270
+ const mapped = mapCCToolCall(toolUse.name, toolUse.input);
2271
+ recordToolCall(loopState, toolUse.id, mapped.name, mapped.input);
2272
+ try {
2273
+ const result = await toolExecutor.execute(mapped.name, mapped.input, context);
2274
+ onChunk('tool_result', {
2275
+ tool: toolUse.name,
2276
+ success: result.success,
2277
+ output: result.output,
2278
+ error: result.error,
2279
+ });
2280
+ // Truncate large tool outputs to avoid blowing context window
2281
+ let output = result.success ? (result.output || 'Success') : `Error: ${result.error}`;
2282
+ if (output.length > 50000) {
2283
+ output = output.slice(0, 50000) + '\n... [truncated]';
2284
+ }
2285
+ toolResultParts.push(`[${toolUse.name}]: ${output}`);
2286
+ recordToolOutcome(loopState, toolUse.id, output);
2287
+ }
2288
+ catch (error) {
2289
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2290
+ onChunk('tool_result', { tool: toolUse.name, success: false, error: errorMessage });
2291
+ toolResultParts.push(`[${toolUse.name}]: Error: ${errorMessage}`);
2292
+ recordToolOutcome(loopState, toolUse.id, errorMessage);
2293
+ }
2294
+ }
2295
+ // Append tool results directly to conversation (don't rebuild from getContextMessages
2296
+ // which can drop messages due to token windowing)
2297
+ const toolResultsMessage = `[Tool Results]\n${toolResultParts.join('\n')}`;
2298
+ currentMessages.push({ role: 'user', content: toolResultsMessage });
2299
+ await this.sessions.addMessage(sessionId, 'user', toolResultsMessage);
2300
+ // Check for tool loop patterns
2301
+ const detection = detectLoop(loopState);
2302
+ if (detection.severity === 'critical') {
2303
+ this.logger.warn('Tool loop detected — forcing synthesis', {
2304
+ detector: detection.detector,
2305
+ message: detection.message,
2306
+ details: detection.details,
2307
+ });
2308
+ onChunk('status', { message: 'Loop detected, synthesizing results...' });
2309
+ lastRoundHadTools = true;
2310
+ break;
2311
+ }
2312
+ if (detection.severity === 'warning') {
2313
+ this.logger.info('Tool loop warning', {
2314
+ detector: detection.detector,
2315
+ message: detection.message,
2316
+ });
2317
+ currentMessages.push({ role: 'user', content: `⚠️ Loop detection warning: ${detection.message}\nPlease try a different approach or different parameters.` });
2318
+ }
2319
+ // Notify the client that tool processing is done and AI is thinking about results
2320
+ onChunk('status', { message: 'Analyzing results...' });
2321
+ }
2322
+ // If the loop ended because we hit maxRounds while still using tools,
2323
+ // do one final call WITHOUT tools to force a synthesis of all gathered info
2324
+ if (lastRoundHadTools) {
2325
+ let synthesisResponse = '';
2326
+ let synthesisUsage = { inputTokens: 0, outputTokens: 0 };
2327
+ currentMessages.push({ role: 'user', content: 'Now synthesize all the information gathered above into your final response. Do not call any more tools.' });
2328
+ for await (const chunk of provider.stream(currentMessages, {
2329
+ systemPrompt: enrichedPrompt,
2330
+ // No tools — force text-only synthesis
2331
+ })) {
2332
+ if (chunk.type === 'text' && chunk.content) {
2333
+ synthesisResponse += chunk.content;
2334
+ onChunk('text', chunk.content);
2335
+ }
2336
+ else if (chunk.type === 'done') {
2337
+ synthesisUsage = chunk.usage || synthesisUsage;
2338
+ }
2339
+ }
2340
+ totalUsage.inputTokens += synthesisUsage.inputTokens;
2341
+ totalUsage.outputTokens += synthesisUsage.outputTokens;
2342
+ fullResponse = synthesisResponse; // Replace accumulated "thinking out loud" with actual synthesis
2343
+ }
2344
+ return { response: fullResponse, usage: totalUsage };
2345
+ }
2346
+ async handleVoiceMessage(client, type, payload, audioBuffer) {
2347
+ if (!this.voiceManager) {
2348
+ this.sendToClient(client, {
2349
+ type: 'voice_error',
2350
+ payload: { message: 'Voice mode not available' },
2351
+ });
2352
+ return;
2353
+ }
2354
+ try {
2355
+ if (type === 'voice_start') {
2356
+ const opts = payload;
2357
+ this.voiceManager.startSession(client.id, {
2358
+ voice: opts?.voice,
2359
+ language: opts?.language,
2360
+ });
2361
+ this.sendToClient(client, { type: 'voice_ready' });
2362
+ return;
2363
+ }
2364
+ if (type === 'voice_cancel') {
2365
+ this.voiceManager.endSession(client.id);
2366
+ return;
2367
+ }
2368
+ if (type === 'voice_end' && audioBuffer) {
2369
+ // Feed audio into voice manager buffer then transcribe
2370
+ this.voiceManager.addAudioFrame(client.id, audioBuffer);
2371
+ const transcription = await this.voiceManager.transcribe(client.id);
2372
+ this.sendToClient(client, {
2373
+ type: 'voice_transcript',
2374
+ payload: { text: transcription.text, final: true },
2375
+ });
2376
+ audit('voice.transcribed', {
2377
+ clientId: client.id,
2378
+ duration: transcription.duration,
2379
+ language: transcription.language,
2380
+ textLength: transcription.text.length,
2381
+ });
2382
+ // Feed transcribed text into AI pipeline
2383
+ if (!this.providers) {
2384
+ this.sendToClient(client, {
2385
+ type: 'voice_error',
2386
+ payload: { message: 'AI providers not configured' },
2387
+ });
2388
+ this.voiceManager.endSession(client.id);
2389
+ return;
2390
+ }
2391
+ const session = await this.sessions.getOrCreate(client.id, {
2392
+ channelType: client.channelType,
2393
+ clientId: client.id,
2394
+ senderId: client.senderId,
2395
+ });
2396
+ await this.sessions.addMessage(session.id, 'user', transcription.text);
2397
+ const contextMessages = this.sessions.getContextMessages(session.id, this.getProviderMaxTokens(this.providers.getPrimaryProvider()), 4096);
2398
+ const chatMessages = sanitizeTranscript(contextMessages).map((m) => ({
2399
+ role: m.role,
2400
+ content: m.content,
2401
+ }));
2402
+ // Enrich prompt with memories (same as text flow)
2403
+ let voicePrompt = this.systemPrompt;
2404
+ if (this.memoryRetriever && this.memoryStore) {
2405
+ const memories = await this.memoryStore.getAll();
2406
+ const memorySection = this.memoryRetriever.retrieve(memories, transcription.text);
2407
+ if (memorySection) {
2408
+ voicePrompt = this.systemPrompt + memorySection;
2409
+ }
2410
+ }
2411
+ // Use executeWithTools for voice — tools execute silently, only final text goes to TTS
2412
+ const provider = this.providers.getPrimaryProvider();
2413
+ const fallbackCandidates = this.providers.resolveFallbackCandidates();
2414
+ const { response: voiceResponse, usage: voiceUsage } = await this.executeWithTools(session.id, chatMessages, voicePrompt, provider, (_type, _data) => {
2415
+ // Voice: don't stream chunks to client — we synthesize the final text
2416
+ }, { fallbackCandidates });
2417
+ await this.sessions.addMessage(session.id, 'assistant', voiceResponse, {
2418
+ input: voiceUsage.inputTokens,
2419
+ output: voiceUsage.outputTokens,
2420
+ });
2421
+ // Extract memories from voice conversation
2422
+ if (this.config.memory?.autoExtract !== false && this.memoryStore && voiceResponse && transcription.text.length > 20) {
2423
+ void this.extractAndLearn(transcription.text, voiceResponse, session.id);
2424
+ }
2425
+ // Send text response
2426
+ this.sendToClient(client, {
2427
+ type: 'voice_text',
2428
+ payload: { content: voiceResponse },
2429
+ });
2430
+ // Stream TTS audio
2431
+ for await (const chunk of this.voiceManager.synthesize(client.id, voiceResponse)) {
2432
+ this.gateway.sendBinary(client, chunk);
2433
+ }
2434
+ audit('voice.synthesized', {
2435
+ clientId: client.id,
2436
+ textLength: voiceResponse.length,
2437
+ voice: this.config.voice?.defaultVoice ?? 'alloy',
2438
+ });
2439
+ this.sendToClient(client, { type: 'voice_end' });
2440
+ this.voiceManager.endSession(client.id);
2441
+ }
2442
+ }
2443
+ catch (error) {
2444
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2445
+ this.sendToClient(client, {
2446
+ type: 'voice_error',
2447
+ payload: { message: errorMessage },
2448
+ });
2449
+ this.voiceManager.endSession(client.id);
2450
+ }
2451
+ }
2452
+ sendToClient(client, message) {
2453
+ if (client.ws.readyState === 1) {
2454
+ // WebSocket.OPEN
2455
+ client.ws.send(JSON.stringify(message));
2456
+ }
2457
+ }
2458
+ /** Load persisted channel targets from disk so behavior delivery survives restarts. */
2459
+ async loadChannelTargets() {
2460
+ try {
2461
+ const data = await fs.readFile(this.channelTargetsPath, 'utf-8');
2462
+ const targets = JSON.parse(data);
2463
+ for (const [channelType, channelId] of Object.entries(targets)) {
2464
+ // Only load if we don't already have a fresher entry from this session
2465
+ if (!this.lastActiveChannels.has(channelType)) {
2466
+ this.lastActiveChannels.set(channelType, channelId);
2467
+ }
2468
+ }
2469
+ this.logger.debug('Loaded channel targets', { channels: Object.keys(targets) });
2470
+ }
2471
+ catch {
2472
+ // File doesn't exist yet — that's fine
2473
+ }
2474
+ }
2475
+ /** Persist channel targets to disk. */
2476
+ async saveChannelTargets() {
2477
+ try {
2478
+ const targets = {};
2479
+ for (const [channelType, channelId] of this.lastActiveChannels) {
2480
+ targets[channelType] = channelId;
2481
+ }
2482
+ await fs.mkdir(path.dirname(this.channelTargetsPath), { recursive: true });
2483
+ await fs.writeFile(this.channelTargetsPath, JSON.stringify(targets, null, 2), 'utf-8');
2484
+ }
2485
+ catch (err) {
2486
+ this.logger.warn('Failed to save channel targets', {
2487
+ error: err instanceof Error ? err : new Error(String(err)),
2488
+ });
2489
+ }
2490
+ }
2491
+ /** Track an agent starting execution. Broadcasts to dashboard. */
2492
+ agentStart(id, type, description, channelType) {
2493
+ const activity = { id, type, description, channelType, startedAt: new Date().toISOString() };
2494
+ this.activeAgents.set(id, activity);
2495
+ this.gateway.broadcast({ type: 'agent_start', payload: activity }, (client) => client.authenticated);
2496
+ }
2497
+ /** Track an agent finishing execution. Broadcasts to dashboard. */
2498
+ agentEnd(id, success) {
2499
+ const activity = this.activeAgents.get(id);
2500
+ if (!activity)
2501
+ return;
2502
+ const duration = Date.now() - new Date(activity.startedAt).getTime();
2503
+ this.activeAgents.delete(id);
2504
+ this.gateway.broadcast({ type: 'agent_end', payload: { id, duration, success } }, (client) => client.authenticated);
2505
+ }
2506
+ /** Get all currently active agents. */
2507
+ getActiveAgents() {
2508
+ return Array.from(this.activeAgents.values());
2509
+ }
2510
+ /** Persist a message to the webchat session so it appears in chat history. */
2511
+ persistToWebchat(content) {
2512
+ this.sessions.getOrCreate('webchat', { channelType: 'webchat' })
2513
+ .then(session => this.sessions.addMessage(session.id, 'assistant', content))
2514
+ .catch(() => { });
2515
+ }
2516
+ /** Deliver a proactive message to all connected channel adapters using tracked channel IDs.
2517
+ * Also persists to the webchat session so messages appear in chat history.
2518
+ * @param exclude - channel type to skip (already delivered by targeted send) */
2519
+ deliverToAllChannels(content, exclude) {
2520
+ this.persistToWebchat(content);
2521
+ // Deliver to external channel adapters (Discord, Slack, Telegram, etc.)
2522
+ if (!this.channels)
2523
+ return;
2524
+ for (const ct of this.channels.getConnectedChannels()) {
2525
+ if (ct === exclude)
2526
+ continue;
2527
+ const targetId = this.lastActiveChannels.get(ct)
2528
+ ?? this.channels.getDefaultChannelId(ct);
2529
+ if (!targetId)
2530
+ continue;
2531
+ this.channels.send(ct, targetId, { content }).catch((err) => {
2532
+ this.logger.warn('Proactive channel delivery failed', {
2533
+ channelType: ct,
2534
+ error: err instanceof Error ? err : new Error(String(err)),
2535
+ });
2536
+ });
2537
+ }
2538
+ }
2539
+ async handleChannelMessage(inbound) {
2540
+ const requestId = generateRequestId();
2541
+ return runWithRequestId(requestId, async () => {
2542
+ // Track last-active channel ID for proactive delivery and persist to disk
2543
+ this.lastActiveChannels.set(inbound.channelType, inbound.channelId);
2544
+ void this.saveChannelTargets();
2545
+ // Get or create session for this sender
2546
+ const session = await this.sessions.getOrCreate(`${inbound.channelType}:${inbound.senderId}`, {
2547
+ channelType: inbound.channelType,
2548
+ senderId: inbound.senderId,
2549
+ });
2550
+ // Handle commands
2551
+ if (inbound.content.startsWith('/')) {
2552
+ const response = await this.handleChannelCommand(inbound.content, session.id);
2553
+ if (this.channels) {
2554
+ await this.channels.send(inbound.channelType, inbound.channelId, {
2555
+ content: response,
2556
+ replyToId: inbound.id,
2557
+ });
2558
+ }
2559
+ return;
2560
+ }
2561
+ // Process media attachments and add user message
2562
+ let messageContent = inbound.content;
2563
+ if (inbound.attachments && inbound.attachments.length > 0 && this.mediaProcessor) {
2564
+ messageContent = await this.mediaProcessor.process(inbound.attachments, inbound.content);
2565
+ }
2566
+ await this.sessions.addMessage(session.id, 'user', messageContent);
2567
+ // Check if providers are available
2568
+ if (!this.providers) {
2569
+ if (this.channels) {
2570
+ await this.channels.send(inbound.channelType, inbound.channelId, {
2571
+ content: 'I need API keys to respond. Please configure them in the vault.',
2572
+ replyToId: inbound.id,
2573
+ });
2574
+ }
2575
+ return;
2576
+ }
2577
+ // Get context messages
2578
+ const contextMessages = this.sessions.getContextMessages(session.id, this.getProviderMaxTokens(this.providers.getPrimaryProvider()), 4096);
2579
+ const chatMessages = sanitizeTranscript(contextMessages).map((m) => ({
2580
+ role: m.role,
2581
+ content: m.content,
2582
+ }));
2583
+ // Show typing indicator while generating response
2584
+ const stopTyping = this.channels
2585
+ ? await this.channels.startTyping(inbound.channelType, inbound.channelId)
2586
+ : () => { };
2587
+ const channelAgentId = `channel:${inbound.channelType}:${inbound.channelId}:${Date.now()}`;
2588
+ try {
2589
+ // Get tool definitions from registry
2590
+ const tools = toolRegistry.toProviderFormat();
2591
+ // Build enriched prompt with modes and memories
2592
+ let enrichedPrompt = this.systemPrompt;
2593
+ let channelMemorySection = null;
2594
+ if (this.memoryRetriever && this.memoryStore) {
2595
+ const memories = await this.memoryStore.getAll();
2596
+ channelMemorySection = this.memoryRetriever.retrieve(memories, inbound.content);
2597
+ }
2598
+ if (this.promptAssembler && this.config.modes?.enabled !== false) {
2599
+ const modeState = this.getSessionModeState(session.id);
2600
+ // Security context check — BEFORE mode detection
2601
+ if (this.securityFloor) {
2602
+ const securityContext = this.securityFloor.detectSecurityContext({ userMessage: inbound.content });
2603
+ if (securityContext.active) {
2604
+ modeState.suspendedMode = modeState.activeMode;
2605
+ enrichedPrompt = this.promptAssembler.enrichForSecurityContext(securityContext, this.securityFloor, channelMemorySection);
2606
+ }
2607
+ else if (modeState.suspendedMode) {
2608
+ modeState.activeMode = modeState.suspendedMode;
2609
+ delete modeState.suspendedMode;
2610
+ enrichedPrompt = this.buildModeEnrichedPrompt(inbound.content, modeState, channelMemorySection, inbound.channelType);
2611
+ }
2612
+ else {
2613
+ enrichedPrompt = this.buildModeEnrichedPrompt(inbound.content, modeState, channelMemorySection, inbound.channelType);
2614
+ }
2615
+ }
2616
+ else {
2617
+ enrichedPrompt = this.buildModeEnrichedPrompt(inbound.content, modeState, channelMemorySection, inbound.channelType);
2618
+ }
2619
+ }
2620
+ else if (channelMemorySection) {
2621
+ enrichedPrompt = this.systemPrompt + channelMemorySection;
2622
+ }
2623
+ const channelArchitectResult = this.applyArchitectEnrichment(enrichedPrompt, inbound.content);
2624
+ enrichedPrompt = channelArchitectResult.prompt;
2625
+ // Use executeWithTools for channels — collect final text for channel reply
2626
+ const provider = this.providers.getPrimaryProvider();
2627
+ this.agentStart(channelAgentId, 'channel', `Processing message on ${inbound.channelType}`, inbound.channelType);
2628
+ // Draft streaming: edit message in place if adapter supports it
2629
+ const adapter = this.channels?.getAdapter(inbound.channelType);
2630
+ const supportsDraft = !!adapter?.editMessage;
2631
+ let draftMessageId = null;
2632
+ let accumulatedText = '';
2633
+ let draftLoop = null;
2634
+ if (supportsDraft && this.channels) {
2635
+ const channels = this.channels;
2636
+ draftLoop = new DraftStreamLoop(async (text) => {
2637
+ try {
2638
+ if (!draftMessageId) {
2639
+ const result = await channels.send(inbound.channelType, inbound.channelId, {
2640
+ content: text,
2641
+ replyToId: inbound.id,
2642
+ });
2643
+ if (result.success && result.messageId) {
2644
+ draftMessageId = result.messageId;
2645
+ }
2646
+ return result.success;
2647
+ }
2648
+ else {
2649
+ const result = await channels.editMessage(inbound.channelType, inbound.channelId, draftMessageId, { content: text });
2650
+ return result.success;
2651
+ }
2652
+ }
2653
+ catch {
2654
+ return false;
2655
+ }
2656
+ }, 1000);
2657
+ }
2658
+ const fallbackCandidates = this.providers.resolveFallbackCandidates();
2659
+ const { response: channelResponse, usage: channelUsage } = await this.executeWithTools(session.id, chatMessages, enrichedPrompt, provider, (type, data) => {
2660
+ if (type === 'text' && data && draftLoop) {
2661
+ accumulatedText += data;
2662
+ draftLoop.update(accumulatedText);
2663
+ }
2664
+ }, { tools, fallbackCandidates });
2665
+ // Flush final draft text
2666
+ if (draftLoop) {
2667
+ if (channelResponse && channelResponse !== accumulatedText) {
2668
+ draftLoop.update(channelResponse);
2669
+ }
2670
+ await draftLoop.flush();
2671
+ draftLoop.stop();
2672
+ }
2673
+ stopTyping();
2674
+ // Save assistant message
2675
+ await this.sessions.addMessage(session.id, 'assistant', channelResponse, {
2676
+ input: channelUsage.inputTokens,
2677
+ output: channelUsage.outputTokens,
2678
+ });
2679
+ // Extract memories and learn from conversation (if auto-extract enabled)
2680
+ if (this.config.memory?.autoExtract !== false && this.memoryStore && channelResponse && inbound.content.length > 20) {
2681
+ void this.extractAndLearn(inbound.content, channelResponse, session.id);
2682
+ }
2683
+ // Send response (skip if draft streaming already delivered it)
2684
+ if (!draftMessageId && this.channels) {
2685
+ await this.channels.send(inbound.channelType, inbound.channelId, {
2686
+ content: channelResponse,
2687
+ replyToId: inbound.id,
2688
+ });
2689
+ }
2690
+ audit('message.sent', {
2691
+ channelType: inbound.channelType,
2692
+ sessionId: session.id,
2693
+ inputTokens: channelUsage.inputTokens,
2694
+ outputTokens: channelUsage.outputTokens,
2695
+ });
2696
+ this.agentEnd(channelAgentId, true);
2697
+ }
2698
+ catch (error) {
2699
+ stopTyping();
2700
+ this.agentEnd(channelAgentId, false);
2701
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2702
+ audit('channel.error', { sessionId: session.id, error: errorMessage });
2703
+ if (this.channels) {
2704
+ await this.channels.send(inbound.channelType, inbound.channelId, {
2705
+ content: `Error: ${errorMessage}`,
2706
+ replyToId: inbound.id,
2707
+ });
2708
+ }
2709
+ }
2710
+ }); // end runWithRequestId
2711
+ }
2712
+ async handleChannelCommand(command, sessionId) {
2713
+ const [cmd] = command.slice(1).split(' ');
2714
+ switch (cmd.toLowerCase()) {
2715
+ case 'status': {
2716
+ const activeSessions = this.sessions.getActiveSessions();
2717
+ const providers = this.providers?.listAvailable() || [];
2718
+ const channels = this.channels?.getConnectedChannels() || [];
2719
+ return `**Status**\n- Sessions: ${activeSessions.length} active\n- Providers: ${providers.join(', ') || 'none'}\n- Channels: ${channels.join(', ') || 'webchat only'}`;
2720
+ }
2721
+ case 'new':
2722
+ case 'reset': {
2723
+ await this.sessions.clear(sessionId);
2724
+ return 'Session cleared. Starting fresh!';
2725
+ }
2726
+ case 'help': {
2727
+ return `**Commands**\n- /status - Show system status\n- /new - Start a new session\n- /reset - Clear current session\n- /help - Show this help`;
2728
+ }
2729
+ default:
2730
+ return `Unknown command: ${cmd}. Try /help`;
2731
+ }
2732
+ }
2733
+ /** Register each connector action as an AI-callable tool. */
2734
+ registerConnectorTools() {
2735
+ if (!this.connectorRegistry || !this.connectorAuthManager)
2736
+ return;
2737
+ for (const connector of this.connectorRegistry.list()) {
2738
+ // Only register tools for connectors with active auth tokens
2739
+ if (!this.connectorAuthManager.hasToken(connector.id))
2740
+ continue;
2741
+ for (const action of connector.actions) {
2742
+ const toolName = `${connector.id.replace(/-/g, '_')}_${action.id.replace(/-/g, '_')}`;
2743
+ // Convert connector param schema to tool parameters
2744
+ const parameters = [];
2745
+ for (const [paramName, paramDef] of Object.entries(action.params)) {
2746
+ parameters.push({
2747
+ name: paramName,
2748
+ type: paramDef.type,
2749
+ description: paramDef.description,
2750
+ required: paramDef.required ?? false,
2751
+ });
2752
+ }
2753
+ const connectorId = connector.id;
2754
+ const actionId = action.id;
2755
+ const authManager = this.connectorAuthManager;
2756
+ toolRegistry.register({
2757
+ name: toolName,
2758
+ description: `[${connector.name}] ${action.description}`,
2759
+ parameters,
2760
+ getPermission: () => {
2761
+ // Trust level 0-1 = require approval, 2+ = auto-approve
2762
+ return action.trustMinimum >= 2
2763
+ ? ToolPermission.AUTO_APPROVE
2764
+ : ToolPermission.USER_APPROVAL;
2765
+ },
2766
+ execute: async (params) => {
2767
+ const token = authManager.getToken(connectorId);
2768
+ if (!token) {
2769
+ return { success: false, error: `${connector.name} is not connected. Please authenticate first.` };
2770
+ }
2771
+ try {
2772
+ const result = await connector.executeAction(actionId, params, token.accessToken);
2773
+ return { success: true, output: JSON.stringify(result, null, 2) };
2774
+ }
2775
+ catch (error) {
2776
+ return { success: false, error: error instanceof Error ? error.message : String(error) };
2777
+ }
2778
+ },
2779
+ });
2780
+ }
2781
+ this.logger.info(`Registered ${connector.actions.length} tools for connector: ${connector.name}`);
2782
+ }
2783
+ }
2784
+ getProviderMaxTokens(provider) {
2785
+ const model = provider.defaultModel;
2786
+ return provider.metadata?.models?.[model]?.maxContextTokens;
2787
+ }
2788
+ async extractAndLearn(userMessage, assistantResponse, sessionId) {
2789
+ try {
2790
+ const recentMessages = this.sessions.getContextMessages(sessionId, this.getProviderMaxTokens(this.providers.getPrimaryProvider()));
2791
+ // AI-powered extraction
2792
+ if (this.memoryExtractor) {
2793
+ const result = await this.memoryExtractor.extract(userMessage, assistantResponse, {
2794
+ messageCount: recentMessages.length,
2795
+ sessionAge: 0, // approximation; session age not easily available here
2796
+ });
2797
+ // Record personality adaptation signals
2798
+ if (this.personalityAdapter && result.personalitySignals.length > 0) {
2799
+ for (const signal of result.personalitySignals) {
2800
+ await this.personalityAdapter.recordSignal(signal);
2801
+ }
2802
+ }
2803
+ const totalExtracted = result.factsExtracted.length +
2804
+ result.patternsDetected.length +
2805
+ result.relationshipsFound.length;
2806
+ if (totalExtracted > 0) {
2807
+ void audit('memory.extracted', { count: totalExtracted });
2808
+ }
2809
+ }
2810
+ // Run local pattern detection on recent messages
2811
+ if (this.patternDetector && this.memoryStore) {
2812
+ const recent = recentMessages.slice(-20);
2813
+ const patterns = this.patternDetector.detect(recent.map(m => ({
2814
+ content: m.content,
2815
+ role: m.role,
2816
+ timestamp: m.timestamp,
2817
+ })));
2818
+ for (const pattern of patterns) {
2819
+ await this.memoryStore.add(pattern.pattern, 'pattern', 'observed', {
2820
+ confidence: pattern.confidence,
2821
+ });
2822
+ }
2823
+ }
2824
+ }
2825
+ catch (error) {
2826
+ // Silent failure — don't block the response
2827
+ this.logger.warn('Memory extraction failed', { error: error instanceof Error ? error : new Error(String(error)) });
2828
+ }
2829
+ }
2830
+ async getLivingMemoryState() {
2831
+ if (!this.memoryStore)
2832
+ return null;
2833
+ const all = await this.memoryStore.getAll();
2834
+ const stats = await this.memoryStore.getStats();
2835
+ return {
2836
+ facts: all.filter(m => ['preference', 'fact', 'context'].includes(m.category)),
2837
+ relationships: all.filter(m => m.category === 'relationship'),
2838
+ patterns: all.filter(m => m.category === 'pattern'),
2839
+ adaptations: this.personalityAdapter ? await this.personalityAdapter.getAdjustments() : [],
2840
+ stats,
2841
+ };
2842
+ }
2843
+ createWebhookRouter() {
2844
+ const router = Router();
2845
+ // Generic webhooks
2846
+ router.post('/custom/:name', async (req, res) => {
2847
+ if (!this.webhookManager) {
2848
+ res.status(503).json({ error: 'Webhooks not available' });
2849
+ return;
2850
+ }
2851
+ // Collect raw body
2852
+ const chunks = [];
2853
+ for await (const chunk of req) {
2854
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
2855
+ }
2856
+ const body = Buffer.concat(chunks);
2857
+ const name = Array.isArray(req.params.name) ? req.params.name[0] : req.params.name;
2858
+ const result = await this.webhookManager.handleGenericWebhook(name, body, req.headers);
2859
+ res.status(result.status).json({
2860
+ accepted: result.accepted,
2861
+ ...(result.error && !result.accepted ? { error: result.error } : {}),
2862
+ });
2863
+ });
2864
+ // Channel webhooks — Twilio
2865
+ router.post('/twilio', async (req, res) => {
2866
+ if (!this.channels) {
2867
+ res.status(503).json({ error: 'Channels not available' });
2868
+ return;
2869
+ }
2870
+ const adapter = this.channels.getAdapter('twilio');
2871
+ if (!adapter) {
2872
+ res.status(503).json({ error: 'Twilio not configured' });
2873
+ return;
2874
+ }
2875
+ const twilioAdapter = adapter;
2876
+ await twilioAdapter.handleWebhook(req.body);
2877
+ res.status(200).type('text/xml').send('<Response></Response>');
2878
+ });
2879
+ // Channel webhooks — Telegram
2880
+ router.post('/telegram', async (req, res) => {
2881
+ if (!this.channels) {
2882
+ res.status(503).json({ error: 'Channels not available' });
2883
+ return;
2884
+ }
2885
+ const adapter = this.channels.getAdapter('telegram');
2886
+ if (!adapter) {
2887
+ res.status(503).json({ error: 'Telegram not configured' });
2888
+ return;
2889
+ }
2890
+ const telegramAdapter = adapter;
2891
+ await telegramAdapter.handleWebhook(req.body);
2892
+ res.sendStatus(200);
2893
+ });
2894
+ return router;
2895
+ }
2896
+ async start() {
2897
+ if (this.running)
2898
+ return;
2899
+ await this.gateway.start();
2900
+ // Connect channels if configured
2901
+ if (this.channels) {
2902
+ try {
2903
+ await this.channels.connectAll();
2904
+ const connected = this.channels.getConnectedChannels();
2905
+ if (connected.length > 0) {
2906
+ this.logger.info(`Connected channels: ${connected.join(', ')}`);
2907
+ }
2908
+ }
2909
+ catch (error) {
2910
+ this.logger.warn('Some channels failed to connect', { error: error instanceof Error ? error : new Error(String(error)) });
2911
+ }
2912
+ }
2913
+ // Load persisted channel targets for proactive delivery (behaviors, ambient)
2914
+ await this.loadChannelTargets();
2915
+ this.running = true;
2916
+ console.log(`\n${this.getAgentName()} is ready!`);
2917
+ console.log(`Open http://${this.config.gateway.host}:${this.config.gateway.port} in your browser\n`);
2918
+ }
2919
+ async stop() {
2920
+ if (!this.running)
2921
+ return;
2922
+ if (this.channels) {
2923
+ await this.channels.disconnectAll();
2924
+ }
2925
+ await this.gateway.stop();
2926
+ if (this.behaviors) {
2927
+ await this.behaviors.stop();
2928
+ }
2929
+ if (this.browserManager) {
2930
+ await this.browserManager.shutdown();
2931
+ }
2932
+ if (this.voiceManager) {
2933
+ await this.voiceManager.shutdown();
2934
+ }
2935
+ if (this.pluginLoader) {
2936
+ await this.pluginLoader.shutdownAll();
2937
+ }
2938
+ if (this.ambientScheduler) {
2939
+ this.ambientScheduler.stop();
2940
+ }
2941
+ if (this.autonomousExecutor) {
2942
+ this.autonomousExecutor.stop();
2943
+ }
2944
+ if (this.memoryCleanupInterval) {
2945
+ clearInterval(this.memoryCleanupInterval);
2946
+ this.memoryCleanupInterval = undefined;
2947
+ }
2948
+ this.sessions.destroy();
2949
+ this.vault.lock();
2950
+ this.running = false;
2951
+ }
2952
+ getConfig() {
2953
+ return this.config;
2954
+ }
2955
+ getAgentName() {
2956
+ return this.config.agent?.name ?? 'Auxiora';
2957
+ }
2958
+ }
2959
+ function deepMerge(target, source) {
2960
+ const result = { ...target };
2961
+ for (const key of Object.keys(source)) {
2962
+ const srcVal = source[key];
2963
+ const tgtVal = target[key];
2964
+ if (srcVal && tgtVal && typeof srcVal === 'object' && typeof tgtVal === 'object' && !Array.isArray(srcVal)) {
2965
+ result[key] = deepMerge(tgtVal, srcVal);
2966
+ }
2967
+ else {
2968
+ result[key] = srcVal;
2969
+ }
2970
+ }
2971
+ return result;
2972
+ }
2973
+ export { AgentRouter, AgentInstance, } from './agent-router.js';
2974
+ export { BlockStream, ToolOutputStream, } from './block-stream.js';
2975
+ export async function startAuxiora(options = {}) {
2976
+ const auxiora = new Auxiora();
2977
+ await auxiora.initialize(options);
2978
+ await auxiora.start();
2979
+ return auxiora;
2980
+ }
2981
+ //# sourceMappingURL=index.js.map