morpheus-cli 0.9.23 → 0.9.30
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/channels/telegram.js +165 -120
- package/dist/cli/commands/restart.js +27 -0
- package/dist/cli/commands/start.js +9 -9
- package/dist/config/manager.js +34 -2
- package/dist/config/paths.js +2 -0
- package/dist/config/schemas.js +6 -0
- package/dist/http/webhooks-router.js +12 -6
- package/dist/runtime/__tests__/gws-sync.test.js +69 -0
- package/dist/runtime/gws-sync.js +102 -0
- package/dist/runtime/hash-utils.js +15 -0
- package/dist/runtime/hot-reload.js +5 -1
- package/dist/runtime/oracle.js +1 -0
- package/dist/runtime/scaffold.js +4 -0
- package/dist/runtime/skills/loader.js +7 -52
- package/dist/runtime/skills/registry.js +5 -1
- package/dist/runtime/skills/schema.js +1 -1
- package/dist/runtime/skills/tool.js +8 -1
- package/dist/runtime/subagents/apoc.js +154 -1
- package/dist/runtime/subagents/devkit-instrument.js +13 -0
- package/dist/runtime/webhooks/dispatcher.js +12 -4
- package/dist/runtime/webhooks/repository.js +17 -6
- package/dist/types/config.js +3 -0
- package/dist/ui/assets/{AuditDashboard-ClqEr7jg.js → AuditDashboard-tH9QZTl4.js} +1 -1
- package/dist/ui/assets/{Chat-BwxZJphx.js → Chat-Cd0uYF8g.js} +1 -1
- package/dist/ui/assets/{Chronos-BafOMteb.js → Chronos-CWwHYdBl.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-DU0AwhXD.js → ConfirmationModal-CxvFe-We.js} +1 -1
- package/dist/ui/assets/{Dashboard-DvJb72Xe.js → Dashboard-CNNMxl53.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-FSWLK6-I.js → DeleteConfirmationModal-AqGQSh1X.js} +1 -1
- package/dist/ui/assets/{Documents-D73CeGkW.js → Documents-BfRYOK88.js} +1 -1
- package/dist/ui/assets/{Logs-BrFWnLIL.js → Logs-DhFo4cio.js} +1 -1
- package/dist/ui/assets/{MCPManager-_L2Yo-uY.js → MCPManager-BMhxbhni.js} +1 -1
- package/dist/ui/assets/{ModelPricing-CyXMdxJD.js → ModelPricing-Dvl0R_HR.js} +1 -1
- package/dist/ui/assets/{Notifications-BpHokTLS.js → Notifications-CawvBid4.js} +1 -1
- package/dist/ui/assets/{SatiMemories-CfSTgr9V.js → SatiMemories-yyVrJGdc.js} +1 -1
- package/dist/ui/assets/{SessionAudit-pOWRgJtc.js → SessionAudit-joq0ntdJ.js} +1 -1
- package/dist/ui/assets/{Settings-CPDXAk18.js → Settings-B6SMPn41.js} +7 -7
- package/dist/ui/assets/{Skills-GIkCxMS3.js → Skills-B5yhTHyn.js} +1 -1
- package/dist/ui/assets/{Smiths-ZcHXcrMt.js → Smiths-Dug63YED.js} +1 -1
- package/dist/ui/assets/{Tasks-DJ6R3d4f.js → Tasks-D8HPLkg0.js} +1 -1
- package/dist/ui/assets/{TrinityDatabases-CnRAkDuu.js → TrinityDatabases-D0qEKmwJ.js} +1 -1
- package/dist/ui/assets/{UsageStats-Bl7bs4ay.js → UsageStats-CHWALN70.js} +1 -1
- package/dist/ui/assets/WebhookManager-T2ef90p8.js +4 -0
- package/dist/ui/assets/{agents-DO69pNM1.js → agents-BVnfnJ1X.js} +1 -1
- package/dist/ui/assets/{audit-CP5fC4m8.js → audit-BErc_ye8.js} +1 -1
- package/dist/ui/assets/{chronos-DPhK718h.js → chronos-CAv__H3B.js} +1 -1
- package/dist/ui/assets/{config-OLGQFNJL.js → config-CPFW7PTY.js} +1 -1
- package/dist/ui/assets/{index-B9ePr-vB.js → index-BvsF1a9j.js} +2 -2
- package/dist/ui/assets/index-gx__iEcl.css +1 -0
- package/dist/ui/assets/{mcp-BeBznKtK.js → mcp-BaHwY4DW.js} +1 -1
- package/dist/ui/assets/{skills-wEUxSGB3.js → skills-lbjIRO8d.js} +1 -1
- package/dist/ui/assets/{stats-KMbKDMJ-.js → stats-C8KAfpHO.js} +1 -1
- package/dist/ui/assets/{useCurrency-Bgg-7MTE.js → useCurrency-Ch0lsvGj.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/WebhookManager-CEjjk4tx.js +0 -4
- package/dist/ui/assets/index-D_0tPLCk.css +0 -1
|
@@ -266,13 +266,42 @@ export class TelegramAdapter {
|
|
|
266
266
|
// Send "typing" status
|
|
267
267
|
await ctx.sendChatAction('typing');
|
|
268
268
|
const sessionId = await this.getSessionForUser(userId);
|
|
269
|
-
// Process with Agent
|
|
270
|
-
const
|
|
269
|
+
// Process with Agent - with a 30s timeout to avoid blocking the bot's update loop.
|
|
270
|
+
const CHAT_TIMEOUT_MS = 30000;
|
|
271
|
+
let timeoutTriggered = false;
|
|
272
|
+
const chatPromise = this.oracle.chat(text, undefined, false, {
|
|
271
273
|
origin_channel: 'telegram',
|
|
272
274
|
session_id: sessionId,
|
|
273
275
|
origin_message_id: String(ctx.message.message_id),
|
|
274
276
|
origin_user_id: userId,
|
|
275
277
|
});
|
|
278
|
+
const response = await Promise.race([
|
|
279
|
+
chatPromise,
|
|
280
|
+
new Promise((resolve) => setTimeout(() => {
|
|
281
|
+
timeoutTriggered = true;
|
|
282
|
+
resolve(null);
|
|
283
|
+
}, CHAT_TIMEOUT_MS))
|
|
284
|
+
]);
|
|
285
|
+
if (timeoutTriggered) {
|
|
286
|
+
await ctx.reply("⏳ I'm still working on that, it's taking a bit longer than usual. I'll send the response as soon as it's ready!");
|
|
287
|
+
// Wait for actual response in background
|
|
288
|
+
const finalResponse = await chatPromise;
|
|
289
|
+
if (finalResponse) {
|
|
290
|
+
const rich = await toTelegramRichText(finalResponse);
|
|
291
|
+
for (const chunk of rich.chunks) {
|
|
292
|
+
try {
|
|
293
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
297
|
+
if (plain)
|
|
298
|
+
await ctx.reply(plain);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
this.display.log(`Responded to @${user} (delayed): ${finalResponse.slice(0, 50)}...`, { source: 'Telegram' });
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
276
305
|
if (response) {
|
|
277
306
|
const rich = await toTelegramRichText(response);
|
|
278
307
|
for (const chunk of rich.chunks) {
|
|
@@ -285,7 +314,7 @@ export class TelegramAdapter {
|
|
|
285
314
|
await ctx.reply(plain);
|
|
286
315
|
}
|
|
287
316
|
}
|
|
288
|
-
this.display.log(`Responded to @${user}: ${response}
|
|
317
|
+
this.display.log(`Responded to @${user}: ${response.slice(0, 50)}...`, { source: 'Telegram' });
|
|
289
318
|
}
|
|
290
319
|
}
|
|
291
320
|
catch (error) {
|
|
@@ -389,126 +418,33 @@ export class TelegramAdapter {
|
|
|
389
418
|
await ctx.reply(`🎤 Transcription: "${text}"`);
|
|
390
419
|
await ctx.sendChatAction('typing');
|
|
391
420
|
const sessionId = await this.getSessionForUser(userId);
|
|
392
|
-
// Process with Agent
|
|
393
|
-
const
|
|
421
|
+
// Process with Agent - with a 30s timeout to avoid blocking the bot's update loop.
|
|
422
|
+
const CHAT_TIMEOUT_MS = 30000;
|
|
423
|
+
let timeoutTriggered = false;
|
|
424
|
+
const chatPromise = this.oracle.chat(text, usage, true, {
|
|
394
425
|
origin_channel: 'telegram',
|
|
395
426
|
session_id: sessionId,
|
|
396
427
|
origin_message_id: String(ctx.message.message_id),
|
|
397
428
|
origin_user_id: userId,
|
|
398
429
|
});
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
try {
|
|
413
|
-
this.display.startActivity('telephonist', 'Synthesizing TTS...');
|
|
414
|
-
const ttsApiKey = getUsableApiKey(ttsConfig.apiKey) ||
|
|
415
|
-
getUsableApiKey(config.audio.apiKey) ||
|
|
416
|
-
(config.llm.provider === (ttsConfig.provider === 'google' ? 'gemini' : ttsConfig.provider)
|
|
417
|
-
? getUsableApiKey(config.llm.api_key) : undefined);
|
|
418
|
-
const ttsResult = await this.ttsTelephonist.synthesize(response, ttsApiKey || '', ttsConfig.voice, ttsConfig.style_prompt);
|
|
419
|
-
ttsFilePath = ttsResult.filePath;
|
|
420
|
-
const ttsDurationMs = Date.now() - ttsStart;
|
|
421
|
-
this.display.endActivity('telephonist', true);
|
|
422
|
-
// OGG/Opus → replyWithVoice; everything else (mp3, wav) → replyWithAudio
|
|
423
|
-
const isOgg = ttsResult.mimeType.includes('ogg') || ttsResult.mimeType.includes('opus');
|
|
424
|
-
if (isOgg) {
|
|
425
|
-
await ctx.replyWithVoice({ source: ttsFilePath });
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
await ctx.replyWithAudio({ source: ttsFilePath });
|
|
429
|
-
}
|
|
430
|
-
this.display.log(`Responded to @${user} (TTS audio)`, { source: 'Telegram' });
|
|
431
|
-
// Audit TTS success
|
|
432
|
-
try {
|
|
433
|
-
const auditSessionId = await this.getSessionForUser(userId);
|
|
434
|
-
AuditRepository.getInstance().insert({
|
|
435
|
-
session_id: auditSessionId,
|
|
436
|
-
event_type: 'telephonist',
|
|
437
|
-
agent: 'telephonist',
|
|
438
|
-
provider: ttsConfig.provider,
|
|
439
|
-
model: ttsConfig.model,
|
|
440
|
-
input_tokens: ttsResult.usage.input_tokens || null,
|
|
441
|
-
output_tokens: ttsResult.usage.output_tokens || null,
|
|
442
|
-
duration_ms: ttsDurationMs,
|
|
443
|
-
status: 'success',
|
|
444
|
-
metadata: {
|
|
445
|
-
operation: 'tts',
|
|
446
|
-
characters: response.length,
|
|
447
|
-
voice: ttsConfig.voice,
|
|
448
|
-
user,
|
|
449
|
-
},
|
|
450
|
-
});
|
|
451
|
-
}
|
|
452
|
-
catch {
|
|
453
|
-
// Audit failure never breaks the main flow
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
catch (ttsError) {
|
|
457
|
-
this.display.endActivity('telephonist', false);
|
|
458
|
-
const ttsDetail = ttsError?.message || String(ttsError);
|
|
459
|
-
this.display.log(`TTS synthesis failed for @${user}: ${ttsDetail} — falling back to text`, { source: 'Telephonist', level: 'warning' });
|
|
460
|
-
// Audit TTS failure
|
|
461
|
-
try {
|
|
462
|
-
const auditSessionId = await this.getSessionForUser(userId);
|
|
463
|
-
AuditRepository.getInstance().insert({
|
|
464
|
-
session_id: auditSessionId,
|
|
465
|
-
event_type: 'telephonist',
|
|
466
|
-
agent: 'telephonist',
|
|
467
|
-
provider: ttsConfig.provider,
|
|
468
|
-
model: ttsConfig.model,
|
|
469
|
-
duration_ms: Date.now() - ttsStart,
|
|
470
|
-
status: 'error',
|
|
471
|
-
metadata: { operation: 'tts', error: ttsDetail, user },
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
catch {
|
|
475
|
-
// Audit failure never breaks the main flow
|
|
476
|
-
}
|
|
477
|
-
// Fallback to text
|
|
478
|
-
const rich = await toTelegramRichText(response);
|
|
479
|
-
for (const chunk of rich.chunks) {
|
|
480
|
-
try {
|
|
481
|
-
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
482
|
-
}
|
|
483
|
-
catch {
|
|
484
|
-
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
485
|
-
if (plain)
|
|
486
|
-
await ctx.reply(plain);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
finally {
|
|
491
|
-
// Cleanup TTS temp file
|
|
492
|
-
if (ttsFilePath) {
|
|
493
|
-
await fs.promises.unlink(ttsFilePath).catch(() => { });
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
// Text path (TTS disabled)
|
|
499
|
-
const rich = await toTelegramRichText(response);
|
|
500
|
-
for (const chunk of rich.chunks) {
|
|
501
|
-
try {
|
|
502
|
-
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
503
|
-
}
|
|
504
|
-
catch {
|
|
505
|
-
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
506
|
-
if (plain)
|
|
507
|
-
await ctx.reply(plain);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
this.display.log(`Responded to @${user} (via audio)`, { source: 'Telegram' });
|
|
430
|
+
const response = await Promise.race([
|
|
431
|
+
chatPromise,
|
|
432
|
+
new Promise((resolve) => setTimeout(() => {
|
|
433
|
+
timeoutTriggered = true;
|
|
434
|
+
resolve(null);
|
|
435
|
+
}, CHAT_TIMEOUT_MS))
|
|
436
|
+
]);
|
|
437
|
+
if (timeoutTriggered) {
|
|
438
|
+
await ctx.reply("⏳ I'm still working on that, it's taking a bit longer than usual. I'll send the response as soon as it's ready!");
|
|
439
|
+
// Wait for actual response in background
|
|
440
|
+
const finalResponse = await chatPromise;
|
|
441
|
+
if (finalResponse) {
|
|
442
|
+
await this.handleAgentResponse(ctx, finalResponse, config, user, userId);
|
|
511
443
|
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (response) {
|
|
447
|
+
await this.handleAgentResponse(ctx, response, config, user, userId);
|
|
512
448
|
}
|
|
513
449
|
}
|
|
514
450
|
catch (error) {
|
|
@@ -876,11 +812,17 @@ export class TelegramAdapter {
|
|
|
876
812
|
ctx.answerCbQuery(`Error: ${error.message}`).catch(() => { });
|
|
877
813
|
}
|
|
878
814
|
});
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
815
|
+
// Global error handler to prevent crashes on long-running tasks or network errors
|
|
816
|
+
this.bot.catch((err, ctx) => {
|
|
817
|
+
const updateId = ctx?.update?.update_id;
|
|
818
|
+
const errorMsg = err?.message || String(err);
|
|
819
|
+
this.display.log(`Unhandled Telegram error (Update: ${updateId}): ${errorMsg}`, { source: 'Telegram', level: 'error' });
|
|
820
|
+
// If it's a timeout error, notify the user if possible
|
|
821
|
+
if (errorMsg.includes('timeout') || errorMsg.includes('90000')) {
|
|
822
|
+
ctx.reply("⏳ Sorry, that request is taking longer than expected. I'm still working on it, but the connection timed out. I'll notify you once it's done.").catch(() => { });
|
|
882
823
|
}
|
|
883
824
|
});
|
|
825
|
+
this.launchBot();
|
|
884
826
|
this.isConnected = true;
|
|
885
827
|
// Check if there's a restart notification to send
|
|
886
828
|
this.checkAndSendRestartNotification().catch((err) => {
|
|
@@ -1955,4 +1897,107 @@ How can I assist you today?`;
|
|
|
1955
1897
|
await ctx.reply('An error occurred while retrieving the list of MCP servers\\. Please check the logs for more details\\.', { parse_mode: 'MarkdownV2' });
|
|
1956
1898
|
}
|
|
1957
1899
|
}
|
|
1900
|
+
launchBot() {
|
|
1901
|
+
if (!this.bot)
|
|
1902
|
+
return;
|
|
1903
|
+
this.bot.launch().catch((err) => {
|
|
1904
|
+
this.display.log(`Telegram bot error: ${err.message || String(err)}`, { source: 'Telegram', level: 'error' });
|
|
1905
|
+
if (this.isConnected) {
|
|
1906
|
+
this.display.log('Attempting to restart Telegram bot in 5 seconds...', { source: 'Telegram', level: 'info' });
|
|
1907
|
+
setTimeout(() => {
|
|
1908
|
+
if (this.isConnected)
|
|
1909
|
+
this.launchBot();
|
|
1910
|
+
}, 5000);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
async handleAgentResponse(ctx, response, config, user, userId) {
|
|
1915
|
+
const ttsConfig = config.audio.tts;
|
|
1916
|
+
if (ttsConfig?.enabled && this.ttsTelephonist?.synthesize) {
|
|
1917
|
+
// TTS path: synthesize and send as voice message
|
|
1918
|
+
let ttsFilePath = null;
|
|
1919
|
+
const ttsStart = Date.now();
|
|
1920
|
+
try {
|
|
1921
|
+
this.display.startActivity('telephonist', 'Synthesizing TTS...');
|
|
1922
|
+
const ttsApiKey = getUsableApiKey(ttsConfig.apiKey) ||
|
|
1923
|
+
getUsableApiKey(config.audio.apiKey) ||
|
|
1924
|
+
(config.llm.provider === (ttsConfig.provider === 'google' ? 'gemini' : ttsConfig.provider)
|
|
1925
|
+
? getUsableApiKey(config.llm.api_key) : undefined);
|
|
1926
|
+
const ttsResult = await this.ttsTelephonist.synthesize(response, ttsApiKey || '', ttsConfig.voice, ttsConfig.style_prompt);
|
|
1927
|
+
ttsFilePath = ttsResult.filePath;
|
|
1928
|
+
const ttsDurationMs = Date.now() - ttsStart;
|
|
1929
|
+
this.display.endActivity('telephonist', true);
|
|
1930
|
+
// OGG/Opus → replyWithVoice; everything else (mp3, wav) → replyWithAudio
|
|
1931
|
+
const isOgg = ttsResult.mimeType.includes('ogg') || ttsResult.mimeType.includes('opus');
|
|
1932
|
+
if (isOgg) {
|
|
1933
|
+
await ctx.replyWithVoice({ source: ttsFilePath });
|
|
1934
|
+
}
|
|
1935
|
+
else {
|
|
1936
|
+
await ctx.replyWithAudio({ source: ttsFilePath });
|
|
1937
|
+
}
|
|
1938
|
+
this.display.log(`Responded to @${user} (TTS audio)`, { source: 'Telegram' });
|
|
1939
|
+
// Audit TTS success
|
|
1940
|
+
try {
|
|
1941
|
+
const auditSessionId = await this.getSessionForUser(userId);
|
|
1942
|
+
AuditRepository.getInstance().insert({
|
|
1943
|
+
session_id: auditSessionId,
|
|
1944
|
+
event_type: 'telephonist',
|
|
1945
|
+
agent: 'telephonist',
|
|
1946
|
+
provider: ttsConfig.provider,
|
|
1947
|
+
model: ttsConfig.model,
|
|
1948
|
+
input_tokens: ttsResult.usage.input_tokens || null,
|
|
1949
|
+
output_tokens: ttsResult.usage.output_tokens || null,
|
|
1950
|
+
duration_ms: ttsDurationMs,
|
|
1951
|
+
status: 'success',
|
|
1952
|
+
metadata: {
|
|
1953
|
+
operation: 'tts',
|
|
1954
|
+
characters: response.length,
|
|
1955
|
+
voice: ttsConfig.voice,
|
|
1956
|
+
user,
|
|
1957
|
+
},
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
catch {
|
|
1961
|
+
// Audit failure never breaks the main flow
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
catch (ttsError) {
|
|
1965
|
+
this.display.endActivity('telephonist', false);
|
|
1966
|
+
const ttsDetail = ttsError?.message || String(ttsError);
|
|
1967
|
+
this.display.log(`TTS synthesis failed for @${user}: ${ttsDetail} — falling back to text`, { source: 'Telephonist', level: 'warning' });
|
|
1968
|
+
// Fallback to text
|
|
1969
|
+
const rich = await toTelegramRichText(response);
|
|
1970
|
+
for (const chunk of rich.chunks) {
|
|
1971
|
+
try {
|
|
1972
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
1973
|
+
}
|
|
1974
|
+
catch {
|
|
1975
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
1976
|
+
if (plain)
|
|
1977
|
+
await ctx.reply(plain);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
finally {
|
|
1982
|
+
if (ttsFilePath) {
|
|
1983
|
+
await fs.promises.unlink(ttsFilePath).catch(() => { });
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
else {
|
|
1988
|
+
// Text path
|
|
1989
|
+
const rich = await toTelegramRichText(response);
|
|
1990
|
+
for (const chunk of rich.chunks) {
|
|
1991
|
+
try {
|
|
1992
|
+
await ctx.reply(chunk, { parse_mode: rich.parse_mode });
|
|
1993
|
+
}
|
|
1994
|
+
catch {
|
|
1995
|
+
const plain = stripHtmlTags(chunk).slice(0, 4096);
|
|
1996
|
+
if (plain)
|
|
1997
|
+
await ctx.reply(plain);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
this.display.log(`Responded to @${user}`, { source: 'Telegram' });
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
1958
2003
|
}
|
|
@@ -9,6 +9,7 @@ import { renderBanner } from '../utils/render.js';
|
|
|
9
9
|
import { TelegramAdapter } from '../../channels/telegram.js';
|
|
10
10
|
import { ChannelRegistry } from '../../channels/registry.js';
|
|
11
11
|
import { PATHS } from '../../config/paths.js';
|
|
12
|
+
import { SkillRegistry } from '../../runtime/skills/index.js';
|
|
12
13
|
import { Oracle } from '../../runtime/oracle.js';
|
|
13
14
|
import { ProviderError } from '../../runtime/errors.js';
|
|
14
15
|
import { HttpServer } from '../../http/server.js';
|
|
@@ -17,6 +18,12 @@ import { TaskWorker } from '../../runtime/tasks/worker.js';
|
|
|
17
18
|
import { TaskNotifier } from '../../runtime/tasks/notifier.js';
|
|
18
19
|
import { Link } from '../../runtime/subagents/link/link.js';
|
|
19
20
|
import { LinkWorker } from '../../runtime/subagents/link/worker.js';
|
|
21
|
+
import { ServiceContainer, SERVICE_KEYS } from '../../runtime/container.js';
|
|
22
|
+
import { ChannelNotifierAdapter } from '../../runtime/adapters/ChannelNotifierAdapter.js';
|
|
23
|
+
import { SQLiteTaskEnqueuerAdapter } from '../../runtime/adapters/SQLiteTaskEnqueuerAdapter.js';
|
|
24
|
+
import { SQLiteChatHistoryAdapter } from '../../runtime/adapters/SQLiteChatHistoryAdapter.js';
|
|
25
|
+
import { LangChainProviderAdapter } from '../../runtime/adapters/LangChainProviderAdapter.js';
|
|
26
|
+
import { AuditRepositoryAdapter } from '../../runtime/adapters/AuditRepositoryAdapter.js';
|
|
20
27
|
export const restartCommand = new Command('restart')
|
|
21
28
|
.description('Restart the Morpheus agent')
|
|
22
29
|
.option('--ui', 'Enable web UI', true)
|
|
@@ -67,11 +74,31 @@ export const restartCommand = new Command('restart')
|
|
|
67
74
|
const config = await configManager.load();
|
|
68
75
|
// Initialize persistent logging
|
|
69
76
|
await display.initialize(config.logging);
|
|
77
|
+
// ── Composition Root ─────────────────────────────────────────────────────
|
|
78
|
+
// Register port adapters in the ServiceContainer so consumers can
|
|
79
|
+
// depend on interfaces instead of concrete implementations.
|
|
80
|
+
ServiceContainer.register(SERVICE_KEYS.notifier, new ChannelNotifierAdapter());
|
|
81
|
+
ServiceContainer.register(SERVICE_KEYS.taskEnqueuer, new SQLiteTaskEnqueuerAdapter());
|
|
82
|
+
ServiceContainer.register(SERVICE_KEYS.chatHistory, new SQLiteChatHistoryAdapter());
|
|
83
|
+
ServiceContainer.register(SERVICE_KEYS.providerFactory, new LangChainProviderAdapter());
|
|
84
|
+
ServiceContainer.register(SERVICE_KEYS.auditEmitter, new AuditRepositoryAdapter());
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
70
86
|
display.log(chalk.green(`Morpheus Agent (${config.agent.name}) starting...`), { source: 'Zaion' });
|
|
71
87
|
display.log(chalk.gray(`PID: ${process.pid}`), { source: 'Zaion' });
|
|
72
88
|
if (options.ui) {
|
|
73
89
|
display.log(chalk.blue(`Web UI enabled to port ${options.port}`), { source: 'Zaion' });
|
|
74
90
|
}
|
|
91
|
+
// Initialize SkillRegistry before Oracle (so skills are available in system prompt)
|
|
92
|
+
try {
|
|
93
|
+
const skillRegistry = SkillRegistry.getInstance();
|
|
94
|
+
await skillRegistry.load();
|
|
95
|
+
const loadedSkills = skillRegistry.getAll();
|
|
96
|
+
const enabledCount = skillRegistry.getEnabled().length;
|
|
97
|
+
display.log(chalk.green(`✓ Skills loaded: ${loadedSkills.length} total, ${enabledCount} enabled`), { source: 'Skills' });
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
display.log(chalk.yellow(`Skills initialization warning: ${err.message}`), { source: 'Skills' });
|
|
101
|
+
}
|
|
75
102
|
// Initialize Oracle
|
|
76
103
|
const oracle = new Oracle(config);
|
|
77
104
|
try {
|
|
@@ -133,6 +133,15 @@ export const startCommand = new Command('start')
|
|
|
133
133
|
const config = await configManager.load();
|
|
134
134
|
// Initialize persistent logging
|
|
135
135
|
await display.initialize(config.logging);
|
|
136
|
+
// ── Composition Root ─────────────────────────────────────────────────────
|
|
137
|
+
// Register port adapters in the ServiceContainer so consumers can
|
|
138
|
+
// depend on interfaces instead of concrete implementations.
|
|
139
|
+
ServiceContainer.register(SERVICE_KEYS.notifier, new ChannelNotifierAdapter());
|
|
140
|
+
ServiceContainer.register(SERVICE_KEYS.taskEnqueuer, new SQLiteTaskEnqueuerAdapter());
|
|
141
|
+
ServiceContainer.register(SERVICE_KEYS.chatHistory, new SQLiteChatHistoryAdapter());
|
|
142
|
+
ServiceContainer.register(SERVICE_KEYS.providerFactory, new LangChainProviderAdapter());
|
|
143
|
+
ServiceContainer.register(SERVICE_KEYS.auditEmitter, new AuditRepositoryAdapter());
|
|
144
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
136
145
|
display.log(chalk.green(`Morpheus Agent (${config.agent.name}) starting...`));
|
|
137
146
|
display.log(chalk.gray(`PID: ${process.pid}`));
|
|
138
147
|
if (options.ui) {
|
|
@@ -220,15 +229,6 @@ export const startCommand = new Command('start')
|
|
|
220
229
|
await clearPid();
|
|
221
230
|
process.exit(1);
|
|
222
231
|
}
|
|
223
|
-
// ── Composition Root ─────────────────────────────────────────────────────
|
|
224
|
-
// Register port adapters in the ServiceContainer so consumers can
|
|
225
|
-
// depend on interfaces instead of concrete implementations.
|
|
226
|
-
ServiceContainer.register(SERVICE_KEYS.notifier, new ChannelNotifierAdapter());
|
|
227
|
-
ServiceContainer.register(SERVICE_KEYS.taskEnqueuer, new SQLiteTaskEnqueuerAdapter());
|
|
228
|
-
ServiceContainer.register(SERVICE_KEYS.chatHistory, new SQLiteChatHistoryAdapter());
|
|
229
|
-
ServiceContainer.register(SERVICE_KEYS.providerFactory, new LangChainProviderAdapter());
|
|
230
|
-
ServiceContainer.register(SERVICE_KEYS.auditEmitter, new AuditRepositoryAdapter());
|
|
231
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
232
232
|
const adapters = [];
|
|
233
233
|
let httpServer;
|
|
234
234
|
const taskWorker = new TaskWorker();
|
package/dist/config/manager.js
CHANGED
|
@@ -343,6 +343,11 @@ export class ConfigManager {
|
|
|
343
343
|
max_active_jobs: resolveNumeric('MORPHEUS_CHRONOS_MAX_ACTIVE_JOBS', config.chronos.max_active_jobs, 100),
|
|
344
344
|
};
|
|
345
345
|
}
|
|
346
|
+
// Apply precedence to GWS config
|
|
347
|
+
const gwsConfig = {
|
|
348
|
+
service_account_json: resolveString('MORPHEUS_GWS_SERVICE_ACCOUNT_JSON', config.gws?.service_account_json, ''),
|
|
349
|
+
enabled: resolveBoolean('MORPHEUS_GWS_ENABLED', config.gws?.enabled, true),
|
|
350
|
+
};
|
|
346
351
|
// Apply precedence to DevKit config
|
|
347
352
|
// Migration: if devkit is absent but apoc.working_dir exists, migrate it
|
|
348
353
|
const rawDevKit = config.devkit ?? {};
|
|
@@ -409,6 +414,22 @@ export class ConfigManager {
|
|
|
409
414
|
async save(newConfig) {
|
|
410
415
|
// Deep merge or overwrite? simpler to overwrite for now or merge top level
|
|
411
416
|
let updated = { ...this.config, ...newConfig };
|
|
417
|
+
// If GWS credentials string is provided, save it to file
|
|
418
|
+
if (newConfig.gws?.service_account_json_content) {
|
|
419
|
+
const content = newConfig.gws.service_account_json_content;
|
|
420
|
+
try {
|
|
421
|
+
// Validate JSON
|
|
422
|
+
JSON.parse(content);
|
|
423
|
+
await fs.ensureDir(PATHS.gws);
|
|
424
|
+
await fs.writeFile(PATHS.gwsCredentials, content, 'utf8');
|
|
425
|
+
// Update the path in config and remove the raw content
|
|
426
|
+
newConfig.gws.service_account_json = PATHS.gwsCredentials;
|
|
427
|
+
delete newConfig.gws.service_account_json_content;
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
throw new Error(`Invalid Google Service Account JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
412
433
|
// Encrypt API keys before saving if MORPHEUS_SECRET is set
|
|
413
434
|
updated = this.encryptAgentApiKeys(updated);
|
|
414
435
|
// Validate before saving
|
|
@@ -524,7 +545,7 @@ export class ConfigManager {
|
|
|
524
545
|
sandbox_dir: process.cwd(),
|
|
525
546
|
readonly_mode: false,
|
|
526
547
|
allowed_shell_commands: [],
|
|
527
|
-
allowed_paths: [PATHS.docs, PATHS.skills],
|
|
548
|
+
allowed_paths: [PATHS.docs, PATHS.skills, PATHS.gws],
|
|
528
549
|
enable_filesystem: true,
|
|
529
550
|
enable_shell: true,
|
|
530
551
|
enable_git: true,
|
|
@@ -535,12 +556,23 @@ export class ConfigManager {
|
|
|
535
556
|
const merged = { ...defaults, ...this.config.devkit };
|
|
536
557
|
// Ensure allowed_paths has default if empty or undefined
|
|
537
558
|
if (!merged.allowed_paths?.length) {
|
|
538
|
-
merged.allowed_paths = [PATHS.docs, PATHS.skills];
|
|
559
|
+
merged.allowed_paths = [PATHS.docs, PATHS.skills, PATHS.gws];
|
|
560
|
+
}
|
|
561
|
+
else if (!merged.allowed_paths.includes(PATHS.gws)) {
|
|
562
|
+
// Force inclusion of GWS path if it's not there
|
|
563
|
+
merged.allowed_paths.push(PATHS.gws);
|
|
539
564
|
}
|
|
540
565
|
return merged;
|
|
541
566
|
}
|
|
542
567
|
return defaults;
|
|
543
568
|
}
|
|
569
|
+
getGwsConfig() {
|
|
570
|
+
const defaults = { enabled: true };
|
|
571
|
+
if (this.config.gws) {
|
|
572
|
+
return { ...defaults, ...this.config.gws };
|
|
573
|
+
}
|
|
574
|
+
return defaults;
|
|
575
|
+
}
|
|
544
576
|
/**
|
|
545
577
|
* Returns encryption status for all agent API keys.
|
|
546
578
|
*/
|
package/dist/config/paths.js
CHANGED
|
@@ -16,6 +16,8 @@ export const PATHS = {
|
|
|
16
16
|
mcps: path.join(MORPHEUS_ROOT, 'mcps.json'),
|
|
17
17
|
skills: path.join(MORPHEUS_ROOT, 'skills'),
|
|
18
18
|
docs: path.join(MORPHEUS_ROOT, 'docs'),
|
|
19
|
+
gws: path.join(MORPHEUS_ROOT, 'gws'),
|
|
20
|
+
gwsCredentials: path.join(MORPHEUS_ROOT, 'gws', 'credentials.json'),
|
|
19
21
|
shortMemoryDb: path.join(MORPHEUS_ROOT, 'memory', 'short-memory.db'),
|
|
20
22
|
trinityDb: path.join(MORPHEUS_ROOT, 'memory', 'trinity.db'),
|
|
21
23
|
satiDb: path.join(MORPHEUS_ROOT, 'memory', 'sati-memory.db'),
|
package/dist/config/schemas.js
CHANGED
|
@@ -98,6 +98,11 @@ export const CurrencyConfigSchema = z.object({
|
|
|
98
98
|
symbol: z.string().min(1).default('$'),
|
|
99
99
|
rate: z.number().positive().default(1.0),
|
|
100
100
|
});
|
|
101
|
+
export const GwsConfigSchema = z.object({
|
|
102
|
+
service_account_json: z.string().optional(),
|
|
103
|
+
service_account_json_content: z.string().optional(),
|
|
104
|
+
enabled: z.boolean().default(true),
|
|
105
|
+
});
|
|
101
106
|
// Zod Schema matching MorpheusConfig interface
|
|
102
107
|
export const ConfigSchema = z.object({
|
|
103
108
|
agent: z.object({
|
|
@@ -123,6 +128,7 @@ export const ConfigSchema = z.object({
|
|
|
123
128
|
chronos: ChronosConfigSchema.optional(),
|
|
124
129
|
devkit: DevKitConfigSchema.optional(),
|
|
125
130
|
smiths: SmithsConfigSchema.optional(),
|
|
131
|
+
gws: GwsConfigSchema.optional(),
|
|
126
132
|
setup: SetupConfigSchema.optional(),
|
|
127
133
|
currency: CurrencyConfigSchema.optional(),
|
|
128
134
|
verbose_mode: z.boolean().default(true),
|
|
@@ -12,6 +12,7 @@ const CreateWebhookSchema = z.object({
|
|
|
12
12
|
.regex(/^[a-z0-9-_]+$/, 'Name must be a slug: lowercase letters, numbers, hyphens, underscores only'),
|
|
13
13
|
prompt: z.string().min(1).max(10_000),
|
|
14
14
|
notification_channels: z.array(z.enum(['ui', 'telegram', 'discord'])).min(1).default(['ui']),
|
|
15
|
+
requires_api_key: z.boolean().default(true),
|
|
15
16
|
});
|
|
16
17
|
const UpdateWebhookSchema = z.object({
|
|
17
18
|
name: z
|
|
@@ -23,6 +24,7 @@ const UpdateWebhookSchema = z.object({
|
|
|
23
24
|
prompt: z.string().min(1).max(10_000).optional(),
|
|
24
25
|
enabled: z.boolean().optional(),
|
|
25
26
|
notification_channels: z.array(z.enum(['ui', 'telegram', 'discord'])).min(1).optional(),
|
|
27
|
+
requires_api_key: z.boolean().optional(),
|
|
26
28
|
});
|
|
27
29
|
const MarkReadSchema = z.object({
|
|
28
30
|
ids: z.array(z.string().uuid()).min(1),
|
|
@@ -39,14 +41,18 @@ export function createWebhooksRouter() {
|
|
|
39
41
|
router.post('/trigger/:webhook_name', async (req, res) => {
|
|
40
42
|
const { webhook_name } = req.params;
|
|
41
43
|
const apiKey = req.headers['x-api-key'];
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!webhook) {
|
|
47
|
-
// Intentionally ambiguous — don't reveal whether the name exists or is disabled
|
|
44
|
+
// 1. First lookup by name to check if it requires an API key
|
|
45
|
+
const webhook = repo.getWebhookByName(webhook_name);
|
|
46
|
+
if (!webhook || !webhook.enabled) {
|
|
47
|
+
// Intentionally ambiguous
|
|
48
48
|
return res.status(401).json({ error: 'Invalid webhook name or api key' });
|
|
49
49
|
}
|
|
50
|
+
// 2. Validate API key if required
|
|
51
|
+
if (webhook.requires_api_key) {
|
|
52
|
+
if (!apiKey || typeof apiKey !== 'string' || apiKey !== webhook.api_key) {
|
|
53
|
+
return res.status(401).json({ error: 'Invalid webhook name or api key' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
50
56
|
const payload = req.body ?? {};
|
|
51
57
|
// Create pending notification immediately
|
|
52
58
|
const notification = repo.createNotification({
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { syncGwsSkills } from '../gws-sync.js';
|
|
5
|
+
import { ConfigManager } from '../../config/manager.js';
|
|
6
|
+
import { calculateMd5 } from '../hash-utils.js';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
8
|
+
vi.mock('../../config/manager.js');
|
|
9
|
+
vi.mock('../display.js', () => ({
|
|
10
|
+
DisplayManager: {
|
|
11
|
+
getInstance: () => ({
|
|
12
|
+
log: vi.fn(),
|
|
13
|
+
}),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
describe('GwsSync', () => {
|
|
17
|
+
let mockDestDir;
|
|
18
|
+
let mockHashesFile;
|
|
19
|
+
const mockSourceDir = path.join(process.cwd(), 'gws-skills', 'skills');
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
vi.resetAllMocks();
|
|
22
|
+
// Create a truly temporary directory for this test run
|
|
23
|
+
mockDestDir = path.join(tmpdir(), `morpheus-test-skills-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
24
|
+
mockHashesFile = path.join(mockDestDir, '.gws-hashes.json');
|
|
25
|
+
vi.mocked(ConfigManager.getInstance).mockReturnValue({
|
|
26
|
+
getGwsConfig: () => ({ enabled: true }),
|
|
27
|
+
});
|
|
28
|
+
await fs.ensureDir(mockDestDir);
|
|
29
|
+
});
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await fs.remove(mockDestDir);
|
|
32
|
+
});
|
|
33
|
+
it('should copy new skills if destination does not exist', async () => {
|
|
34
|
+
if (!(await fs.pathExists(mockSourceDir))) {
|
|
35
|
+
console.warn('Skipping test: gws-skills/skills not found');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
await syncGwsSkills(mockDestDir);
|
|
39
|
+
const skills = await fs.readdir(mockSourceDir);
|
|
40
|
+
if (skills.length > 0) {
|
|
41
|
+
const firstSkill = skills[0];
|
|
42
|
+
expect(await fs.pathExists(path.join(mockDestDir, firstSkill, 'SKILL.md'))).toBe(true);
|
|
43
|
+
const metadata = await fs.readJson(mockHashesFile);
|
|
44
|
+
expect(metadata.skills[firstSkill]).toBeDefined();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
it('should not overwrite customized skills', async () => {
|
|
48
|
+
if (!(await fs.pathExists(mockSourceDir)))
|
|
49
|
+
return;
|
|
50
|
+
const skills = await fs.readdir(mockSourceDir);
|
|
51
|
+
if (skills.length === 0)
|
|
52
|
+
return;
|
|
53
|
+
const skillName = skills[0];
|
|
54
|
+
const destPath = path.join(mockDestDir, skillName, 'SKILL.md');
|
|
55
|
+
// 1. Initial sync to set baseline
|
|
56
|
+
await syncGwsSkills(mockDestDir);
|
|
57
|
+
const originalMetadata = await fs.readJson(mockHashesFile);
|
|
58
|
+
const originalHash = originalMetadata.skills[skillName];
|
|
59
|
+
// 2. User modifies the file
|
|
60
|
+
await fs.writeFile(destPath, 'USER CUSTOMIZATION');
|
|
61
|
+
const customHash = calculateMd5('USER CUSTOMIZATION');
|
|
62
|
+
expect(customHash).not.toBe(originalHash);
|
|
63
|
+
// 3. Sync again
|
|
64
|
+
await syncGwsSkills(mockDestDir);
|
|
65
|
+
// 4. Verify file was preserved
|
|
66
|
+
const content = await fs.readFile(destPath, 'utf-8');
|
|
67
|
+
expect(content).toBe('USER CUSTOMIZATION');
|
|
68
|
+
});
|
|
69
|
+
});
|