-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathwebhook-server.js
More file actions
2742 lines (2515 loc) · 102 KB
/
webhook-server.js
File metadata and controls
2742 lines (2515 loc) · 102 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env node
/**
* Moltagent NC Talk Webhook Server
*
* Receives messages from NC Talk, routes through LLM, responds.
* All incoming messages are verified using HMAC-SHA256 signatures.
*
* Security features:
* - Signature verification on all incoming webhooks
* - Credential broker for secure secret management
* - Backend allowlist validation
* - Comprehensive audit logging
*
* @version 2.0.0
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
// Load new resilience layer modules
const NCRequestManager = require('./src/lib/nc-request-manager');
const CredentialCache = require('./src/lib/credential-cache');
const CredentialBroker = require('./src/lib/credential-broker');
const TalkSignatureVerifier = require('./src/lib/talk-signature-verifier');
const { createErrorHandler } = require('./src/lib/errors/error-handler');
const { TalkSendQueue } = require('./src/lib/talk/talk-send-queue');
const appConfig = require('./src/lib/config');
const { createServerComponents } = require('./src/lib/server/index');
// Knowledge modules for agent memory context
let ContextLoader, LearningLog;
try {
({ ContextLoader } = require('./src/lib/knowledge/context-loader'));
({ LearningLog } = require('./src/lib/knowledge/learning-log'));
} catch {
console.warn('[WARN] Knowledge modules not available, agent memory disabled');
}
// Optional modules - gracefully handle if not present
let LLMRouter, AuditLogger, CalendarHandler, EmailHandler, CalDAVClient;
try {
const llmModule = require('./src/lib/llm');
LLMRouter = llmModule.LLMRouter;
} catch {
console.warn('[WARN] LLM Router not available, using stub');
LLMRouter = null;
}
try {
AuditLogger = require('./lib/audit-logger');
} catch {
console.warn('[WARN] Audit Logger not available, using console');
AuditLogger = null;
}
try {
CalDAVClient = require('./src/lib/integrations/caldav-client');
} catch {
console.warn('[WARN] CalDAV Client not available');
CalDAVClient = null;
}
try {
CalendarHandler = require('./src/lib/handlers/calendar-handler');
} catch {
console.warn('[WARN] Calendar Handler not available');
CalendarHandler = null;
}
try {
EmailHandler = require('./src/lib/handlers/email-handler');
} catch {
console.warn('[WARN] Email Handler not available');
EmailHandler = null;
}
let DeckClient;
try {
DeckClient = require('./src/lib/integrations/deck-client');
} catch {
console.warn('[WARN] Deck Client not available');
DeckClient = null;
}
let NewsClient;
try {
({ NewsClient } = require('./src/lib/integrations/news-client'));
} catch {
console.warn('[WARN] NewsClient not available');
NewsClient = null;
}
let NCFilesClient, NCSearchClient, TextExtractor;
try {
({ NCFilesClient } = require('./src/lib/integrations/nc-files-client'));
} catch {
console.warn('[WARN] NCFilesClient not available');
NCFilesClient = null;
}
try {
({ NCSearchClient } = require('./src/lib/integrations/nc-search-client'));
} catch {
console.warn('[WARN] NCSearchClient not available');
NCSearchClient = null;
}
try {
({ TextExtractor } = require('./src/lib/extraction/text-extractor'));
} catch {
console.warn('[WARN] TextExtractor not available');
TextExtractor = null;
}
let CollectivesClient;
try {
CollectivesClient = require('./src/lib/integrations/collectives-client');
} catch {
console.warn('[WARN] CollectivesClient not available');
CollectivesClient = null;
}
const ResilientWikiWriter = require('./src/lib/integrations/resilient-wiki-writer');
let SearXNGClient, WebReader;
try {
({ SearXNGClient } = require('./src/lib/integrations/searxng-client'));
({ WebReader } = require('./src/lib/integrations/web-reader'));
} catch {
console.warn('[WARN] Web tools not available');
SearXNGClient = null;
WebReader = null;
}
let ContactsClient;
try {
ContactsClient = require('./src/lib/integrations/contacts-client');
} catch {
console.warn('[WARN] ContactsClient not available');
ContactsClient = null;
}
let CockpitManager;
try {
CockpitManager = require('./src/lib/integrations/cockpit-manager');
} catch {
console.warn('[WARN] CockpitManager not available');
CockpitManager = null;
}
let PersonalBoardManager;
try {
PersonalBoardManager = require('./src/lib/integrations/personal-board-manager');
} catch {
console.warn('[WARN] PersonalBoardManager not available');
PersonalBoardManager = null;
}
let SelfRecovery;
try {
SelfRecovery = require('./src/lib/agent/self-recovery');
} catch {
console.warn('[WARN] SelfRecovery not available');
SelfRecovery = null;
}
let RSVPTracker;
try {
RSVPTracker = require('./src/lib/integrations/rsvp-tracker');
} catch {
console.warn('[WARN] RSVPTracker not available');
RSVPTracker = null;
}
let MeetingComposer;
try {
MeetingComposer = require('./src/lib/calendar/meeting-composer');
} catch {
console.warn('[WARN] MeetingComposer not available');
MeetingComposer = null;
}
let ensureMeetingsBoard;
try {
ensureMeetingsBoard = require('./src/lib/calendar/ensure-meetings-board');
} catch {
console.warn('[WARN] ensureMeetingsBoard not available');
ensureMeetingsBoard = null;
}
let BotEnroller;
try {
BotEnroller = require('./src/lib/integrations/bot-enroller');
} catch {
console.warn('[WARN] BotEnroller not available');
BotEnroller = null;
}
let RoomMonitor;
try {
RoomMonitor = require('./src/lib/integrations/room-monitor');
} catch {
console.warn('[WARN] RoomMonitor not available');
RoomMonitor = null;
}
let SessionPersister;
try {
SessionPersister = require('./src/lib/integrations/session-persister');
} catch {
console.warn('[WARN] SessionPersister not available');
SessionPersister = null;
}
let MemorySearcher;
try {
MemorySearcher = require('./src/lib/integrations/memory-searcher');
} catch {
console.warn('[WARN] MemorySearcher not available');
MemorySearcher = null;
}
let WarmMemory;
try {
WarmMemory = require('./src/lib/integrations/warm-memory');
} catch {
console.warn('[WARN] WarmMemory not available');
WarmMemory = null;
}
let NCStatusIndicator;
try {
NCStatusIndicator = require('./src/lib/integrations/nc-status-indicator');
} catch {
console.warn('[WARN] NCStatusIndicator not available');
NCStatusIndicator = null;
}
let HeartbeatManager;
try {
HeartbeatManager = require('./src/lib/integrations/heartbeat-manager');
} catch {
console.warn('[WARN] HeartbeatManager not available');
HeartbeatManager = null;
}
let HeartbeatIntelligence;
try {
HeartbeatIntelligence = require('./src/lib/integrations/heartbeat-intelligence');
} catch {
console.warn('[WARN] HeartbeatIntelligence not available');
HeartbeatIntelligence = null;
}
let WorkflowBoardDetector, WorkflowEngine;
try {
WorkflowBoardDetector = require('./src/lib/workflows/workflow-board-detector');
WorkflowEngine = require('./src/lib/workflows/workflow-engine');
} catch {
console.warn('[WARN] Workflow Engine modules not available');
WorkflowBoardDetector = null;
WorkflowEngine = null;
}
// Session 38: Infrastructure Monitor
let InfraMonitor;
try {
InfraMonitor = require('./src/lib/integrations/infra-monitor');
} catch {
console.warn('[WARN] InfraMonitor not available');
InfraMonitor = null;
}
// Self-Heal Daemon Client
let SelfHealClient;
try {
SelfHealClient = require('./src/lib/clients/self-heal-client');
} catch {
console.warn('[WARN] SelfHealClient not available');
SelfHealClient = null;
}
// Session 37: Voice Pipeline
let WhisperClient, AudioConverter;
try {
WhisperClient = require('./src/lib/providers/whisper-client');
AudioConverter = require('./src/lib/providers/audio-converter');
} catch {
console.warn('[WARN] Voice pipeline modules not available');
WhisperClient = null;
AudioConverter = null;
}
// V3: LLM Fallback Notifier
let FallbackNotifier;
try {
FallbackNotifier = require('./src/lib/llm/fallback-notifier');
} catch {
console.warn('[WARN] FallbackNotifier not available');
FallbackNotifier = null;
}
// Session V2: Speaches STT/TTS + VoiceManager
let SpeachesClient, VoiceManager;
try {
({ SpeachesClient, VoiceManager } = require('./src/lib/voice'));
} catch {
console.warn('[WARN] VoiceManager modules not available');
SpeachesClient = null;
VoiceManager = null;
}
let KnowledgeBoard;
try {
({ KnowledgeBoard } = require('./src/lib/knowledge/knowledge-board'));
} catch {
console.warn('[WARN] KnowledgeBoard not available');
KnowledgeBoard = null;
}
// NC Flow modules for heartbeat event routing
let ActivityPoller, WebhookReceiver;
try {
({ ActivityPoller } = require('./src/lib/nc-flow/activity-poller'));
({ WebhookReceiver } = require('./src/lib/nc-flow/webhook-receiver'));
} catch {
console.warn('[WARN] NC Flow heartbeat modules not available');
ActivityPoller = null;
WebhookReceiver = null;
}
let DocumentIngestor;
try {
DocumentIngestor = require('./src/lib/integrations/document-ingestor');
} catch {
console.warn('[WARN] DocumentIngestor not available');
DocumentIngestor = null;
}
let IngestionCache;
try {
IngestionCache = require('./src/lib/memory/ingestion-cache');
} catch {
console.warn('[WARN] IngestionCache not available');
IngestionCache = null;
}
let SkillForgeHandler, TemplateLoader, TemplateEngine, SecurityScanner, SkillActivator, ToolActivator, HttpToolExecutor, OAuthBroker;
try {
({ SkillForgeHandler } = require('./src/lib/handlers/skill-forge-handler'));
({ TemplateLoader, TemplateEngine, SecurityScanner, SkillActivator, ToolActivator, HttpToolExecutor, OAuthBroker } = require('./src/skill-forge'));
} catch {
console.warn('[WARN] Skill Forge modules not available');
SkillForgeHandler = null;
}
let EmailMonitor;
try {
EmailMonitor = require('./src/lib/services/email-monitor');
} catch {
console.warn('[WARN] Email Monitor not available');
EmailMonitor = null;
}
let SessionManager;
try {
SessionManager = require('./src/security/session-manager');
} catch {
console.warn('[WARN] SessionManager not available');
SessionManager = null;
}
// Session 14: AgentLoop — tool-calling LLM agent
let DailyBriefing;
try {
({ DailyBriefing } = require('./src/lib/agent/daily-briefing'));
} catch {
console.warn('[WARN] DailyBriefing not available');
DailyBriefing = null;
}
let ProactiveEvaluator;
try {
({ ProactiveEvaluator } = require('./src/lib/agent/proactive-evaluator'));
} catch {
console.warn('[WARN] ProactiveEvaluator not available');
ProactiveEvaluator = null;
}
let ReferenceResolver;
try {
({ ReferenceResolver } = require('./src/lib/agent/reference-resolver'));
} catch {
console.warn('[WARN] ReferenceResolver not available');
ReferenceResolver = null;
}
let AgentLoop, ToolRegistry, OllamaToolsProvider, SecretsGuard, ToolGuard, PromptGuard, SystemTagsClient, ProviderChain, RouterChatBridge, GuardrailEnforcer;
try {
({ AgentLoop } = require('./src/lib/agent/agent-loop'));
({ ToolRegistry } = require('./src/lib/agent/tool-registry'));
({ OllamaToolsProvider } = require('./src/lib/agent/providers/ollama-tools'));
({ ProviderChain } = require('./src/lib/agent/providers/provider-chain'));
({ RouterChatBridge } = require('./src/lib/agent/providers/router-chat-bridge'));
({ SecretsGuard } = require('./src/security/guards/secrets-guard'));
({ ToolGuard } = require('./src/security/guards/tool-guard'));
({ PromptGuard } = require('./src/security/guards/prompt-guard'));
({ SystemTagsClient } = require('./src/lib/nc-flow/system-tags'));
({ GuardrailEnforcer } = require('./src/lib/agent/guardrail-enforcer'));
} catch (err) {
console.warn(`[WARN] AgentLoop modules not fully available: ${err.message}`);
AgentLoop = null;
}
// Cost metering
const CostTracker = require('./src/lib/llm/cost-tracker');
// Local Intelligence: ModelScout + MicroPipeline + DeferralQueue
let ModelScout, MicroPipeline, MemoryContextEnricher, DeferralQueue;
try {
({ ModelScout } = require('./src/lib/providers/model-scout'));
MicroPipeline = require('./src/lib/agent/micro-pipeline');
MemoryContextEnricher = require('./src/lib/agent/memory-context-enricher');
DeferralQueue = require('./src/lib/agent/deferral-queue');
} catch (err) {
console.warn(`[WARN] Local Intelligence modules not available: ${err.message}`);
ModelScout = null;
MicroPipeline = null;
MemoryContextEnricher = null;
DeferralQueue = null;
}
// Configuration (NO secrets - only non-sensitive config)
const CONFIG = {
port: parseInt(process.env.PORT) || appConfig.server.port,
nc: {
url: process.env.NC_URL || appConfig.nextcloud.url,
username: process.env.NC_USER || appConfig.nextcloud.username
},
security: {
// Backend URLs allowed to send webhooks (populated from config or env)
allowedBackends: (process.env.NC_TALK_BACKENDS || '')
.split(',')
.map(s => s.trim())
.filter(Boolean),
strictMode: process.env.STRICT_MODE !== 'false'
},
claude: {
modelPremium: process.env.CLAUDE_MODEL_PREMIUM || appConfig.claude.modelPremium,
modelStandard: process.env.CLAUDE_MODEL_STANDARD || appConfig.claude.modelStandard
},
ollama: {
url: process.env.OLLAMA_URL || appConfig.ollama.url,
model: process.env.OLLAMA_MODEL || appConfig.ollama.model,
modelCredential: process.env.OLLAMA_MODEL_CREDENTIAL || process.env.OLLAMA_MODEL || appConfig.ollama.modelCredential,
timeout: appConfig.ollama.timeout,
toolTimeout: appConfig.ollama.toolTimeout
},
deck: {
boardId: parseInt(process.env.DECK_BOARD_ID) || appConfig.deck.boardId
},
heartbeat: {
intervalMs: parseInt(process.env.HEARTBEAT_INTERVAL) || appConfig.heartbeat.intervalMs,
quietHoursStart: parseInt(process.env.QUIET_START) || appConfig.heartbeat.quietHoursStart,
quietHoursEnd: parseInt(process.env.QUIET_END) || appConfig.heartbeat.quietHoursEnd,
deckEnabled: appConfig.heartbeat.deckEnabled,
caldavEnabled: appConfig.heartbeat.caldavEnabled,
maxTasksPerCycle: appConfig.heartbeat.maxTasksPerCycle
}
};
// Add default backend if none configured
if (CONFIG.security.allowedBackends.length === 0) {
CONFIG.security.allowedBackends.push(...appConfig.security.allowedBackends);
}
// Global components
let ncRequestManager = null;
let credentialCache = null;
let credentialBroker = null;
let signatureVerifier = null;
let llmRouter = null;
let llmProviderConfigs = {}; // YAML provider definitions (adapter, credentialName, model)
let auditLogger = null;
let caldavClient = null;
let calendarHandler = null;
let emailHandler = null;
let emailMonitor = null;
let deckClient2 = null; // DeckClient for message routing (separate from heartbeat)
let ncFilesClient = null;
let ncSearchClient = null;
let textExtractor = null;
let collectivesClient = null;
let resilientWriter = null;
let contactsClient = null;
let rsvpTracker = null; // Initialized here; consumed by HeartbeatManager in bot.js
let meetingComposer = null;
let skillForgeHandler = null;
let talkQueue = null;
let defaultTalkToken = appConfig.talk.primaryRoom || null; // Primary room for proactive notifications
let contextLoader = null;
let learningLog = null;
let agentLoop = null; // Session 14: AgentLoop instance
let routerChatBridge = null; // Session B2: RouterChatBridge for dynamic provider registration
let costTracker = null; // Cost metering: per-call audit + enriched cost reporting
let dailyBriefing = null; // First-message-of-day briefing
let cockpitManager = null; // Session 27: Cockpit (Deck as control plane)
let botEnroller = null; // Auto-enable Talk bot in rooms
let sessionManager = null; // Session 29b: shared session manager
let sessionPersister = null; // Session 29b: persist expired session summaries to wiki
let memorySearcher = null; // Session 29b: keyword-based memory search over wiki pages
let warmMemory = null; // Session M1: warm memory layer (WARM.md)
let sessionCleanupTimer = null; // Periodic SessionManager cleanup
let webhookErrorHandler = null;
let serverComponents = null; // Decomposed server components (webhook, command, health, message processor)
let heartbeatManager = null; // HeartbeatManager instance (proactive operations)
let oauthBroker = null; // OAuth 2.0 broker for SkillForge token lifecycle
let knowledgeBoard = null; // KnowledgeBoard for verification tracking
let ncFlowActivityPoller = null; // NC Flow activity poller for heartbeat events
let ncFlowWebhookReceiver = null; // NC Flow webhook receiver for heartbeat events
let ncStatusIndicator = null; // NC Status Indicator (sets Molti's NC user status)
let roomMonitor = null; // RoomMonitor (instant welcome messages for new rooms)
let whisperClient = null; // Session 37: WhisperClient for STT transcription
let audioConverter = null; // Session 37: AudioConverter for audio format conversion
let voiceManager = null; // Session V2: VoiceManager for mode-aware voice orchestration
let infraMonitor = null; // Session 38: InfraMonitor for service health probing
let selfHealClient = null; // Self-heal daemon client for remote service restarts
let microPipeline = null; // Local Intelligence: MicroPipeline for local-only mode
let activityLogger = null; // Two-Layer Memory: Layer 1 raw activity log
let deferralQueue = null; // Local Intelligence: DeferralQueue for deferred complex tasks
let intentRouter = null; // IntentRouter: LLM-powered intent classification
let coAccessGraph = null; // Semantic Awareness: co-access pattern graph
let documentIngestor = null; // Document Ingestion: file → text → entities → knowledge
let embeddingClient = null; // Semantic Awareness: Ollama embedding client
let vectorStore = null; // Semantic Awareness: SQLite vector store
let gapDetector = null; // Semantic Awareness: knowledge gap detection
let rhythmTracker = null; // Semantic Awareness: user rhythm tracking
let knowledgeGraph = null; // Knowledge Graph: entity/triple store
let entityExtractor = null; // Knowledge Graph: entity extraction from text
let dailyDigest = null; // Episodic Memory: daily digest page generation
// Simple console audit logger fallback
async function consoleAuditLog(event, data) {
const timestamp = new Date().toISOString();
console.log(`[AUDIT] ${timestamp} ${event}:`, JSON.stringify(data).substring(0, 200));
}
/**
* Extract a named ## section from structured markdown.
* Returns the content between the named header and the next header (or end).
* @param {string} markdown
* @param {string} sectionName
* @returns {string|null}
*/
function _extractSection(markdown, sectionName) {
if (!markdown) return null;
const pattern = new RegExp(`## ${sectionName}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`, 'i');
const match = markdown.match(pattern);
return match ? match[1].trim() : null;
}
/**
* Build ProactiveEvaluator if dependencies are available.
* @returns {Object|null}
*/
function _buildProactiveEvaluator(agentLoop, llmRouter, talkQueue, appConfig) {
if (!ProactiveEvaluator || !agentLoop) return null;
try {
const evaluator = new ProactiveEvaluator({
agentLoop,
llmRouter: llmRouter?.router || null,
talkSendQueue: talkQueue,
config: {
proactiveMinLevel: 3,
initiativeLevel: appConfig.proactive?.initiativeLevel
}
});
console.log('[INIT] ProactiveEvaluator ready');
return evaluator;
} catch (err) {
console.warn(`[INIT] ProactiveEvaluator failed: ${err.message}`);
return null;
}
}
/**
* Build ReferenceResolver if dependencies are available.
* @returns {Object|null}
*/
function _buildReferenceResolver(llmRouter) {
if (!ReferenceResolver || !llmRouter?.router) return null;
try {
const resolver = new ReferenceResolver({
router: llmRouter,
logger: console
});
console.log('[INIT] ReferenceResolver ready');
return resolver;
} catch (err) {
console.warn(`[INIT] ReferenceResolver failed: ${err.message}`);
return null;
}
}
/**
* User notification (sends to NC Talk primary room)
*/
async function notifyUser(notification) {
const message = notification.message || JSON.stringify(notification);
console.log(`[NOTIFY] ${notification.type || 'info'}: ${message}`);
if (!defaultTalkToken || !talkQueue) {
if (!defaultTalkToken) {
console.warn('[NOTIFY] No primary room configured (set TALK_PRIMARY_ROOM or TALK_ROOM_TOKEN)');
}
return;
}
try {
const result = await talkQueue.enqueue(defaultTalkToken, message);
if (!result) {
console.warn('[NOTIFY] Talk API returned error, message logged only');
}
} catch (error) {
console.warn(`[NOTIFY] Talk delivery failed: ${error.message}`);
}
}
/**
* Initialize all components
*/
async function initialize() {
console.log('');
console.log('======================================================================');
console.log(' Moltagent Webhook Server Initializing... ');
console.log('======================================================================');
console.log('');
// 1. Initialize NCRequestManager FIRST (new resilience layer)
console.log('[INIT] Setting up NC Request Manager...');
ncRequestManager = new NCRequestManager({
nextcloud: { url: CONFIG.nc.url, username: CONFIG.nc.username }
// ncResilience uses config defaults
});
try {
ncRequestManager.setBootstrapCredential();
await ncRequestManager.resolveCanonicalUsername();
console.log('[INIT] NC Request Manager ready');
} catch (err) {
console.error(`[INIT] NCRequestManager error: ${err.message}`);
console.error('[INIT] Options:');
console.error(' 1. systemd LoadCredential=nc-password:/path/to/credential');
console.error(' 2. NC_CREDENTIAL_FILE=/path/to/file');
console.error(' 3. NC_PASSWORD=password (not recommended)');
process.exit(1);
}
// 1.1. Initialize Talk Send Queue (serializes outbound Talk messages)
talkQueue = new TalkSendQueue(ncRequestManager);
console.log('[INIT] Talk Send Queue ready');
// 1.2. Initialize NC Status Indicator (sets Molti's NC user status)
if (NCStatusIndicator && appConfig.statusIndicator.enabled) {
console.log('[INIT] Setting up NC Status Indicator...');
ncStatusIndicator = new NCStatusIndicator({
ncRequestManager: ncRequestManager,
config: appConfig
});
await ncStatusIndicator.setStatus('startup');
console.log('[INIT] NC Status Indicator ready (startup status set)');
}
// 1.5. Initialize Conversation Context (for Talk chat history in LLM prompts)
console.log('[INIT] Setting up Conversation Context...');
const { ConversationContext } = require('./src/lib/talk/conversation-context');
const conversationContext = new ConversationContext(
appConfig.talk.conversationContext,
ncRequestManager
);
console.log('[INIT] Conversation Context ready');
// 2. Initialize CredentialCache (uses NCRequestManager)
console.log('[INIT] Setting up Credential Cache...');
credentialCache = new CredentialCache(ncRequestManager, {
// cacheTTL uses config default
auditLog: consoleAuditLog
});
// 3. Initialize Credential Broker (uses CredentialCache)
console.log('[INIT] Setting up Credential Broker...');
credentialBroker = new CredentialBroker(credentialCache, {
ncUrl: CONFIG.nc.url,
ncUsername: CONFIG.nc.username,
auditLog: consoleAuditLog
});
console.log('[INIT] Credential Broker ready');
// 3a. Verify Credential Broker connectivity (P0-5)
console.log('[INIT] Verifying Credential Broker...');
try {
const brokerTest = await credentialBroker.testConnection();
if (brokerTest.connected) {
console.log(`[INIT] \u2713 NC Passwords connected. ${brokerTest.credentialCount} credential(s) available: ${brokerTest.credentials.join(', ')}`);
} else {
console.warn(`[INIT] \u26a0 NC Passwords not available: ${brokerTest.error}`);
console.warn('[INIT] Falling back to environment variables');
}
} catch (err) {
console.warn(`[INIT] \u26a0 Credential verification failed: ${err.message}`);
console.warn('[INIT] Falling back to environment variables');
}
// 2. Initialize Signature Verifier
console.log('[INIT] Setting up Signature Verifier...');
signatureVerifier = new TalkSignatureVerifier({
getSecret: async () => {
// Try to get from NC Passwords first, fall back to env var
try {
const secret = await credentialBroker.get('nc-talk-secret');
if (secret) return secret;
} catch {
// Fall through to systemd credential
}
// Try systemd LoadCredential path
if (process.env.CREDENTIALS_DIRECTORY) {
try {
const fs = require('fs');
const credPath = `${process.env.CREDENTIALS_DIRECTORY}/nc-talk-secret`;
const secret = fs.readFileSync(credPath, 'utf8').trim();
if (secret) return secret;
} catch {
// Fall through to env var
}
}
// Fallback to environment variable
if (process.env.NC_TALK_SECRET) {
return process.env.NC_TALK_SECRET;
}
throw new Error('NC Talk secret not configured');
},
allowedBackends: CONFIG.security.allowedBackends,
auditLog: consoleAuditLog,
strictMode: CONFIG.security.strictMode,
requireBackendValidation: true
});
console.log(`[INIT] Allowed backends: ${CONFIG.security.allowedBackends.join(', ')}`);
console.log(`[INIT] Strict mode: ${CONFIG.security.strictMode}`);
// 3.5. Initialize error handler for webhooks
webhookErrorHandler = createErrorHandler({
serviceName: 'WebhookServer',
auditLog: consoleAuditLog
});
console.log('[INIT] Error handler ready');
// 4. Initialize Audit Logger (if available) - uses NCRequestManager
if (AuditLogger) {
console.log('[INIT] Setting up Audit Logger...');
auditLogger = new AuditLogger(ncRequestManager, {
logPath: '/Logs',
flushInterval: appConfig.heartbeat.intervalMs, // Same as heartbeat to reduce NC load
maxBufferSize: 20
});
console.log('[INIT] Audit Logger ready');
}
// 5. Initialize LLM Router (if available)
if (LLMRouter) {
console.log('[INIT] Setting up LLM Router...');
const { loadConfig: loadLLMConfig } = require('./src/lib/llm');
const llmConfig = loadLLMConfig({ getCredential: credentialBroker.createGetter() });
// Apply runtime Ollama overrides from CONFIG
if (llmConfig.providers?.['ollama-local'] && CONFIG.ollama?.url) {
llmConfig.providers['ollama-local'].endpoint = CONFIG.ollama.url;
}
if (llmConfig.providers?.['ollama-local'] && CONFIG.ollama?.model) {
llmConfig.providers['ollama-local'].model = CONFIG.ollama.model;
}
llmConfig.auditLog = auditLogger ? auditLogger.log.bind(auditLogger) : consoleAuditLog;
llmConfig.proactiveDailyBudget = appConfig.proactive.dailyCloudBudget;
llmProviderConfigs = llmConfig.providers || {};
llmRouter = new LLMRouter(llmConfig);
const results = await llmRouter.testConnections();
const ollamaResult = results['ollama-local'];
if (ollamaResult?.connected) {
console.log(`[INIT] Ollama connected. Models: ${ollamaResult.models?.join(', ') || 'N/A'}`);
} else {
console.warn(`[INIT] Ollama not available: ${ollamaResult?.error || 'unknown'}`);
}
}
// 6. Initialize CalDAV Client and Calendar Handler (if available) - uses NCRequestManager
if (CalDAVClient && CalendarHandler) {
console.log('[INIT] Setting up CalDAV Client...');
try {
caldavClient = new CalDAVClient(ncRequestManager, credentialBroker, {
ncUrl: CONFIG.nc.url,
username: CONFIG.nc.username,
auditLog: auditLogger ? auditLogger.log.bind(auditLogger) : consoleAuditLog
});
console.log('[INIT] CalDAV Client ready');
console.log('[INIT] Setting up Calendar Handler...');
const auditFn = auditLogger ? auditLogger.log.bind(auditLogger) : consoleAuditLog;
calendarHandler = new CalendarHandler(caldavClient, llmRouter, auditFn);
console.log('[INIT] Calendar Handler ready');
} catch (err) {
console.warn(`[INIT] Calendar setup failed: ${err.message}`);
}
}
// 7. Initialize Email Handler (if available)
if (EmailHandler) {
console.log('[INIT] Setting up Email Handler...');
try {
const auditFn = auditLogger ? auditLogger.log.bind(auditLogger) : consoleAuditLog;
emailHandler = new EmailHandler(credentialBroker, llmRouter, auditFn);
console.log('[INIT] Email Handler ready');
} catch (err) {
console.warn(`[INIT] Email Handler failed: ${err.message}`);
}
}
// 7b. Initialize DeckClient for message routing (if available) — before ContextLoader
if (DeckClient && ncRequestManager) {
try {
deckClient2 = new DeckClient(ncRequestManager, {
boardName: 'Moltagent Tasks',
role: 'tasks'
});
console.log('[INIT] DeckClient (message routing) ready');
} catch (err) {
console.warn(`[INIT] DeckClient failed: ${err.message}`);
}
}
// 7b1. Initialize NewsClient for NC News RSS feeds
let newsClient = null;
if (NewsClient && ncRequestManager) {
try {
newsClient = new NewsClient(ncRequestManager);
console.log('[INIT] NewsClient ready');
} catch (err) {
console.warn(`[INIT] NewsClient failed: ${err.message}`);
}
}
// 7b2. Initialize NCFilesClient, NCSearchClient, TextExtractor
if (NCFilesClient && ncRequestManager) {
try {
ncFilesClient = new NCFilesClient(ncRequestManager, { username: CONFIG.nc.username });
console.log('[INIT] NCFilesClient ready');
} catch (err) {
console.warn(`[INIT] NCFilesClient failed: ${err.message}`);
}
}
// Session M1: Initialize WarmMemory (warm layer for cross-session context)
if (WarmMemory && ncFilesClient) {
try {
warmMemory = new WarmMemory({
ncFilesClient,
logger: console,
config: { filePath: 'Memory/WARM.md', maxLines: 200, cacheTTLMs: 60000 }
});
await warmMemory.load(); // Creates Memory/ dir + initial WARM.md on first run
console.log('[INIT] WarmMemory ready');
} catch (err) {
console.warn(`[INIT] WarmMemory failed: ${err.message}`);
warmMemory = null;
}
}
// Session 37: Initialize Voice Pipeline (WhisperClient + AudioConverter)
if (WhisperClient && AudioConverter && appConfig.voice?.enabled !== false) {
try {
whisperClient = new WhisperClient({
whisperUrl: appConfig.voice.whisperUrl,
whisperTimeout: appConfig.voice.whisperTimeout,
whisperModel: appConfig.voice.whisperModel
});
audioConverter = new AudioConverter({
ffmpegPath: appConfig.voice.ffmpegPath
});
console.log(`[INIT] Voice pipeline ready (Whisper: ${appConfig.voice.whisperUrl}, model: ${appConfig.voice.whisperModel})`);
} catch (err) {
console.warn(`[INIT] Voice pipeline failed: ${err.message}`);
whisperClient = null;
audioConverter = null;
}
}
// Session V2: Initialize VoiceManager (Speaches-backed voice orchestration)
if (SpeachesClient && VoiceManager && appConfig.voice?.enabled !== false) {
try {
const speachesClient = new SpeachesClient({
endpoint: appConfig.voice.speachesUrl,
sttModel: appConfig.voice.speachesSttModel,
ttsModel: appConfig.voice.speachesTtsModel,
ttsVoice: appConfig.voice.speachesTtsVoice,
timeout: appConfig.voice.speachesTimeout,
});
voiceManager = new VoiceManager({
speachesClient,
fileClient: ncFilesClient,
audioConverter: audioConverter,
ncRequestManager: ncRequestManager,
config: appConfig.voice,
});
// Start in 'listen' mode so voice works before first heartbeat reads Cockpit
voiceManager.setMode('listen');
console.log(`[INIT] VoiceManager ready (Speaches: ${appConfig.voice.speachesUrl})`);
} catch (err) {
console.warn(`[INIT] VoiceManager failed: ${err.message}`);
voiceManager = null;
}
}
if (NCSearchClient && ncRequestManager) {
try {
ncSearchClient = new NCSearchClient(ncRequestManager);
console.log('[INIT] NCSearchClient ready');
} catch (err) {
console.warn(`[INIT] NCSearchClient failed: ${err.message}`);
}
}
if (TextExtractor) {
try {
textExtractor = new TextExtractor({
ocrEnabled: appConfig.extraction.ocrEnabled,
ocrLanguages: appConfig.extraction.ocrLanguages,
ocrJobs: appConfig.extraction.ocrJobs,
ocrTimeoutMs: appConfig.extraction.ocrTimeoutMs,
charsPerPageThreshold: appConfig.extraction.charsPerPageThreshold
});
console.log('[INIT] TextExtractor ready (OCR: ' + (appConfig.extraction.ocrEnabled ? 'enabled' : 'disabled') + ')');
} catch (err) {
console.warn(`[INIT] TextExtractor failed: ${err.message}`);
}
}
// 7b3. Initialize CollectivesClient (knowledge wiki)
if (CollectivesClient && ncRequestManager) {
try {
collectivesClient = new CollectivesClient(ncRequestManager, {
collectiveName: appConfig.knowledge.collectiveName
});
console.log('[INIT] CollectivesClient ready');
} catch (err) {
console.warn(`[INIT] CollectivesClient failed: ${err.message}`);
}
}
// 7b3b. Initialize ResilientWikiWriter (dual OCS/WebDAV fallback)
if (collectivesClient && ncFilesClient) {
try {
resilientWriter = new ResilientWikiWriter({
collectivesClient,
ncFilesClient,
logger: console,
ocsTimeoutMs: 10000
});
console.log('[INIT] ResilientWikiWriter ready (OCS + WebDAV dual-path)');
} catch (err) {
console.warn(`[INIT] ResilientWikiWriter failed: ${err.message}`);
}
}
// 7b3a. Wiki bootstrap (non-blocking but awaitable by ingestor)
let wikiBootstrapDone = Promise.resolve();
if (collectivesClient) {
wikiBootstrapDone = (async () => {
try {
const result = await collectivesClient.bootstrapDefaultPages();
if (result.created.length > 0) {
console.log(`[INIT] Wiki bootstrap: created ${result.created.join(', ')}`);
}
if (result.skipped.length > 0) {
console.log(`[INIT] Wiki bootstrap: skipped ${result.skipped.join(', ')} (already exist)`);
}
if (result.errors.length > 0) {
console.warn(`[INIT] Wiki bootstrap errors: ${result.errors.map(e => `${e.title}: ${e.error}`).join('; ')}`);
}
const adminUser = appConfig.knowledge.adminUser;
if (adminUser) {
const shareResult = await collectivesClient.shareWithAdmin(adminUser);
console.log(`[INIT] Wiki share with ${adminUser}: ${shareResult.message}`);
}
} catch (err) {
console.warn(`[INIT] Wiki bootstrap failed: ${err.message}`);
}
})();
}
// 7b4. Initialize SearXNG + WebReader (web tools)
let searxngClient = null;
let webReaderInstance = null;
if (SearXNGClient && appConfig.search.searxng.url) {