morpheus-cli 0.9.5 → 0.9.7
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/README.md +63 -43
- package/dist/channels/discord.js +71 -21
- package/dist/channels/telegram.js +73 -19
- package/dist/cli/commands/restart.js +15 -0
- package/dist/cli/commands/start.js +18 -0
- package/dist/config/manager.js +61 -0
- package/dist/config/paths.js +1 -0
- package/dist/config/schemas.js +11 -3
- package/dist/http/api.js +3 -0
- package/dist/http/routers/link.js +239 -0
- package/dist/http/routers/skills.js +1 -8
- package/dist/runtime/apoc.js +1 -1
- package/dist/runtime/audit/repository.js +1 -1
- package/dist/runtime/link-chunker.js +214 -0
- package/dist/runtime/link-repository.js +301 -0
- package/dist/runtime/link-search.js +298 -0
- package/dist/runtime/link-worker.js +284 -0
- package/dist/runtime/link.js +295 -0
- package/dist/runtime/memory/sati/service.js +1 -1
- package/dist/runtime/memory/sqlite.js +52 -0
- package/dist/runtime/neo.js +1 -1
- package/dist/runtime/oracle.js +81 -44
- package/dist/runtime/scaffold.js +4 -17
- package/dist/runtime/skills/__tests__/loader.test.js +7 -10
- package/dist/runtime/skills/__tests__/registry.test.js +2 -18
- package/dist/runtime/skills/__tests__/tool.test.js +55 -224
- package/dist/runtime/skills/index.js +1 -2
- package/dist/runtime/skills/loader.js +0 -2
- package/dist/runtime/skills/registry.js +8 -20
- package/dist/runtime/skills/schema.js +0 -4
- package/dist/runtime/skills/tool.js +42 -209
- package/dist/runtime/smiths/delegator.js +1 -1
- package/dist/runtime/smiths/registry.js +1 -1
- package/dist/runtime/tasks/worker.js +12 -44
- package/dist/runtime/trinity.js +1 -1
- package/dist/types/config.js +14 -0
- package/dist/ui/assets/AuditDashboard-93LCGHG1.js +1 -0
- package/dist/ui/assets/{Chat-BNtutgja.js → Chat-CK5sNcQ1.js} +8 -8
- package/dist/ui/assets/{Chronos-3C8RPZcl.js → Chronos-m2h--GEe.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-ZQPBeJ2Z.js → ConfirmationModal-Dd5pUJme.js} +1 -1
- package/dist/ui/assets/{Dashboard-CqkHzr2F.js → Dashboard-ODwl7d-a.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-CioxFWn_.js → DeleteConfirmationModal-CCcojDmr.js} +1 -1
- package/dist/ui/assets/Documents-dWnSoxFO.js +7 -0
- package/dist/ui/assets/{Logs-DBVanS0O.js → Logs-Dc9Z2LBj.js} +1 -1
- package/dist/ui/assets/{MCPManager-vXfL3P2U.js → MCPManager-CMkb8vMn.js} +1 -1
- package/dist/ui/assets/{ModelPricing-DyfdunLT.js → ModelPricing-DtHPPbEQ.js} +1 -1
- package/dist/ui/assets/{Notifications-VL-vep6d.js → Notifications-BPvo-DWP.js} +1 -1
- package/dist/ui/assets/{Pagination-oTGieBLM.js → Pagination-BHZKk42X.js} +1 -1
- package/dist/ui/assets/{SatiMemories-jaadkW0U.js → SatiMemories-BUPu1Lxr.js} +1 -1
- package/dist/ui/assets/SessionAudit-CFKF4DA8.js +9 -0
- package/dist/ui/assets/Settings-C4JrXfsR.js +47 -0
- package/dist/ui/assets/{Skills-DE3zziXL.js → Skills-BUlvJgJ4.js} +1 -1
- package/dist/ui/assets/{Smiths-pmogN1mU.js → Smiths-CDtJdY0I.js} +1 -1
- package/dist/ui/assets/{Tasks-Bs8s34Jc.js → Tasks-DK_cOsNK.js} +1 -1
- package/dist/ui/assets/{TrinityDatabases-D7uihcdp.js → TrinityDatabases-X07by-19.js} +1 -1
- package/dist/ui/assets/{UsageStats-B9gePLZ0.js → UsageStats-dYcgckLq.js} +1 -1
- package/dist/ui/assets/{WebhookManager-B2L3rCLM.js → WebhookManager-DDw5eX2R.js} +1 -1
- package/dist/ui/assets/{audit-Cggeu9mM.js → audit-DZ5WLUEm.js} +1 -1
- package/dist/ui/assets/{chronos-D3-sWhfU.js → chronos-B_HI4mlq.js} +1 -1
- package/dist/ui/assets/{config-CBqRUPgn.js → config-B-YxlVrc.js} +1 -1
- package/dist/ui/assets/index-DVjwJ8jT.css +1 -0
- package/dist/ui/assets/{index-zKplfrXZ.js → index-DfJwcKqG.js} +5 -5
- package/dist/ui/assets/{mcp-uL1R9hyA.js → mcp-k-_pwbqA.js} +1 -1
- package/dist/ui/assets/{skills-jmw8yTJs.js → skills-xMXangks.js} +1 -1
- package/dist/ui/assets/{stats-HOms6GnM.js → stats-C4QZIv5O.js} +1 -1
- package/dist/ui/assets/{vendor-icons-DMd9RGvJ.js → vendor-icons-NHF9HNeN.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/sw.js +1 -1
- package/package.json +3 -1
- package/dist/runtime/__tests__/keymaker.test.js +0 -148
- package/dist/runtime/keymaker.js +0 -157
- package/dist/ui/assets/AuditDashboard-DliJ1CX0.js +0 -1
- package/dist/ui/assets/SessionAudit-BsXrWlwz.js +0 -9
- package/dist/ui/assets/Settings-B4eezRcg.js +0 -47
- package/dist/ui/assets/index-D4fzIKy1.css +0 -1
package/README.md
CHANGED
|
@@ -22,8 +22,8 @@ It runs as a daemon and orchestrates LLMs, MCP tools, DevKit tools, memory, and
|
|
|
22
22
|
- `Apoc`: DevTools/browser execution (filesystem, shell, git, network, packages, processes, system, browser automation).
|
|
23
23
|
- `Sati`: long-term memory retrieval/evaluation.
|
|
24
24
|
- `Trinity`: database specialist. Executes queries, introspects schemas, and manages registered databases (PostgreSQL, MySQL, SQLite, MongoDB).
|
|
25
|
+
- `Link`: documentation specialist. RAG over indexed user documents (PDF, Markdown, TXT, DOCX) with hybrid vector + keyword search.
|
|
25
26
|
- `Chronos`: temporal scheduler. Runs Oracle prompts on a recurring or one-time schedule.
|
|
26
|
-
- `Keymaker`: skill executor. Runs user-defined skills with full tool access (DevKit + MCP + internal tools).
|
|
27
27
|
- `Smith`: remote DevKit executor. Runs DevKit operations on isolated machines (Docker, VMs, cloud) via WebSocket.
|
|
28
28
|
|
|
29
29
|
## Installation
|
|
@@ -510,14 +510,15 @@ Precedence order:
|
|
|
510
510
|
|
|
511
511
|
## Skills
|
|
512
512
|
|
|
513
|
-
Skills are user-defined
|
|
513
|
+
Skills are user-defined instruction templates that extend Morpheus capabilities. Each skill is a folder in `~/.morpheus/skills/` containing a single `SKILL.md` file with YAML frontmatter for metadata.
|
|
514
514
|
|
|
515
|
-
###
|
|
515
|
+
### How Skills Work
|
|
516
516
|
|
|
517
|
-
Skills
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
517
|
+
Skills are **instruction templates** loaded into Oracle's context on-demand:
|
|
518
|
+
1. User request matches a skill's examples/description
|
|
519
|
+
2. Oracle calls `load_skill` tool with the skill name
|
|
520
|
+
3. Tool returns the skill's full content (SKILL.md)
|
|
521
|
+
4. Oracle follows the instructions using its existing tools (delegation, MCP, etc.)
|
|
521
522
|
|
|
522
523
|
### Creating a Skill
|
|
523
524
|
|
|
@@ -533,7 +534,6 @@ description: Brief description of what this skill does
|
|
|
533
534
|
version: 1.0.0
|
|
534
535
|
author: your-name
|
|
535
536
|
enabled: true
|
|
536
|
-
execution_mode: sync
|
|
537
537
|
tags:
|
|
538
538
|
- automation
|
|
539
539
|
- example
|
|
@@ -546,44 +546,17 @@ examples:
|
|
|
546
546
|
You are an expert at [domain]. Follow these instructions...
|
|
547
547
|
|
|
548
548
|
## Your Task
|
|
549
|
-
[Instructions for
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
### Async Skill Example
|
|
553
|
-
|
|
554
|
-
For long-running tasks like deployments or builds:
|
|
555
|
-
|
|
556
|
-
```markdown
|
|
557
|
-
---
|
|
558
|
-
name: deploy-staging
|
|
559
|
-
description: Deploy application to staging environment
|
|
560
|
-
execution_mode: async
|
|
561
|
-
tags:
|
|
562
|
-
- deployment
|
|
563
|
-
- devops
|
|
564
|
-
---
|
|
565
|
-
|
|
566
|
-
# Deploy to Staging
|
|
567
|
-
|
|
568
|
-
Execute a full deployment to the staging environment...
|
|
549
|
+
[Instructions for Oracle to execute]
|
|
569
550
|
```
|
|
570
551
|
|
|
571
552
|
### Using Skills
|
|
572
553
|
|
|
573
|
-
Once a skill is loaded, Oracle
|
|
554
|
+
Once a skill is loaded, Oracle follows the instructions using its available tools:
|
|
574
555
|
|
|
575
|
-
**Sync Skills (immediate result):**
|
|
576
556
|
```
|
|
577
557
|
User: "Review the code in src/auth.ts"
|
|
578
|
-
Oracle: [
|
|
579
|
-
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
**Async Skills (background task):**
|
|
583
|
-
```
|
|
584
|
-
User: "Deploy to staging"
|
|
585
|
-
Oracle: [delegates via skill_delegate, task queued]
|
|
586
|
-
Morpheus: [notifies via Telegram/Discord when complete]
|
|
558
|
+
Oracle: [loads code-reviewer skill via load_skill]
|
|
559
|
+
Oracle: [executes review using its tools and returns result]
|
|
587
560
|
```
|
|
588
561
|
|
|
589
562
|
### Managing Skills
|
|
@@ -617,15 +590,61 @@ POST /api/skills/:name/disable - Disable skill
|
|
|
617
590
|
### Sample Skills
|
|
618
591
|
|
|
619
592
|
Example skills are available in `examples/skills/`:
|
|
620
|
-
- `code-reviewer` - Reviews code for issues and best practices
|
|
621
|
-
- `git-helper` - Assists with Git operations
|
|
622
|
-
- `deploy-staging` - Deploy to staging environment (async)
|
|
593
|
+
- `code-reviewer` - Reviews code for issues and best practices
|
|
594
|
+
- `git-helper` - Assists with Git operations
|
|
623
595
|
|
|
624
596
|
Copy them to your skills directory:
|
|
625
597
|
```bash
|
|
626
598
|
cp -r examples/skills/* ~/.morpheus/skills/
|
|
627
599
|
```
|
|
628
600
|
|
|
601
|
+
## Link — Document RAG
|
|
602
|
+
|
|
603
|
+
Link is a documentation specialist subagent that provides RAG (Retrieval-Augmented Generation) over user documents.
|
|
604
|
+
|
|
605
|
+
**Supported formats:** PDF, Markdown, TXT, DOCX
|
|
606
|
+
|
|
607
|
+
**Document storage:** `~/.morpheus/docs/`
|
|
608
|
+
|
|
609
|
+
**Embedding database:** `~/.morpheus/memory/link.db` (SQLite with sqlite-vec)
|
|
610
|
+
|
|
611
|
+
### Using Link
|
|
612
|
+
|
|
613
|
+
Oracle automatically delegates to Link when users ask about their documents:
|
|
614
|
+
|
|
615
|
+
```
|
|
616
|
+
User: "What does my contract say about termination?"
|
|
617
|
+
Oracle: [delegates to Link via link_delegate]
|
|
618
|
+
Link: [searches indexed documents and returns answer with citations]
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### Managing Documents
|
|
622
|
+
|
|
623
|
+
**Web UI:** Navigate to `/documents` to upload, delete, and reindex documents.
|
|
624
|
+
|
|
625
|
+
**API Endpoints:**
|
|
626
|
+
```
|
|
627
|
+
GET /api/link/documents - List all documents
|
|
628
|
+
POST /api/link/upload - Upload a document
|
|
629
|
+
DELETE /api/link/documents/:id - Delete a document
|
|
630
|
+
POST /api/link/documents/:id/reindex - Reindex a document
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Configuration
|
|
634
|
+
|
|
635
|
+
```yaml
|
|
636
|
+
link:
|
|
637
|
+
provider: openai
|
|
638
|
+
model: gpt-4o-mini
|
|
639
|
+
temperature: 0.2
|
|
640
|
+
personality: documentation_specialist
|
|
641
|
+
execution_mode: async # 'sync' = inline, 'async' = background task
|
|
642
|
+
max_results: 10 # max search results per query
|
|
643
|
+
score_threshold: 0.5 # minimum similarity score (0-1)
|
|
644
|
+
chunk_size: 1000 # characters per chunk
|
|
645
|
+
chunk_overlap: 200 # overlap between chunks
|
|
646
|
+
```
|
|
647
|
+
|
|
629
648
|
## MCP Configuration
|
|
630
649
|
|
|
631
650
|
Configure MCP servers in `~/.morpheus/mcps.json`.
|
|
@@ -655,9 +674,10 @@ Authenticated endpoints (`x-architect-pass`):
|
|
|
655
674
|
- Sessions: `/api/sessions*`
|
|
656
675
|
- Chat: `POST /api/chat`
|
|
657
676
|
- Tasks: `GET /api/tasks`, `GET /api/tasks/stats`, `GET /api/tasks/:id`, `POST /api/tasks/:id/retry`
|
|
658
|
-
- Config: `/api/config`, `/api/config/sati`, `/api/config/neo`, `/api/config/apoc`, `/api/config/trinity`, `/api/config/chronos`, `/api/config/smiths`
|
|
677
|
+
- Config: `/api/config`, `/api/config/sati`, `/api/config/neo`, `/api/config/apoc`, `/api/config/trinity`, `/api/config/link`, `/api/config/chronos`, `/api/config/smiths`
|
|
659
678
|
- MCP: `/api/mcp/*` (servers CRUD + reload + status)
|
|
660
679
|
- Sati memories: `/api/sati/memories*`
|
|
680
|
+
- Link documents: `/api/link/documents`, `/api/link/upload`, `/api/link/documents/:id/reindex`
|
|
661
681
|
- Trinity databases: `GET/POST/PUT/DELETE /api/trinity/databases`, `POST /api/trinity/databases/:id/test`, `POST /api/trinity/databases/:id/refresh-schema`
|
|
662
682
|
- Chronos: `GET/POST /api/chronos`, `GET/PUT/DELETE /api/chronos/:id`, `PATCH /api/chronos/:id/enable`, `PATCH /api/chronos/:id/disable`, `GET /api/chronos/:id/executions`, `POST /api/chronos/preview`
|
|
663
683
|
- Smiths: `GET /api/smiths`, `GET/PUT /api/smiths/config`, `GET/DELETE /api/smiths/:name`, `POST /api/smiths/:name/ping`
|
package/dist/channels/discord.js
CHANGED
|
@@ -112,8 +112,8 @@ export class DiscordAdapter {
|
|
|
112
112
|
display = DisplayManager.getInstance();
|
|
113
113
|
config = ConfigManager.getInstance();
|
|
114
114
|
history = new SQLiteChatMessageHistory({ sessionId: '' });
|
|
115
|
-
/** Per-
|
|
116
|
-
|
|
115
|
+
/** Per-user session tracking — maps userId to sessionId */
|
|
116
|
+
userSessions = new Map();
|
|
117
117
|
telephonist = null;
|
|
118
118
|
telephonistProvider = null;
|
|
119
119
|
telephonistModel = null;
|
|
@@ -189,8 +189,7 @@ export class DiscordAdapter {
|
|
|
189
189
|
return;
|
|
190
190
|
this.display.log(`${message.author.tag}: ${text}`, { source: 'Discord' });
|
|
191
191
|
try {
|
|
192
|
-
const sessionId =
|
|
193
|
-
this.currentSessionId = sessionId;
|
|
192
|
+
const sessionId = await this.getSessionForUser(userId);
|
|
194
193
|
const response = await this.oracle.chat(text, undefined, false, {
|
|
195
194
|
origin_channel: 'discord',
|
|
196
195
|
session_id: sessionId,
|
|
@@ -303,8 +302,7 @@ export class DiscordAdapter {
|
|
|
303
302
|
// Show transcription
|
|
304
303
|
await channel.send(`🎤 "${text}"`);
|
|
305
304
|
// Process with Oracle
|
|
306
|
-
const sessionId =
|
|
307
|
-
this.currentSessionId = sessionId;
|
|
305
|
+
const sessionId = await this.getSessionForUser(userId);
|
|
308
306
|
const response = await this.oracle.chat(text, usage, true, {
|
|
309
307
|
origin_channel: 'discord',
|
|
310
308
|
session_id: sessionId,
|
|
@@ -495,8 +493,10 @@ export class DiscordAdapter {
|
|
|
495
493
|
const history = new SQLiteChatMessageHistory({ sessionId: '' });
|
|
496
494
|
const newSessionId = await history.createNewSession();
|
|
497
495
|
history.close();
|
|
498
|
-
// Track the new session as the current one for this
|
|
499
|
-
|
|
496
|
+
// Track the new session as the current one for this user
|
|
497
|
+
const userId = interaction.user.id;
|
|
498
|
+
await this.history.setUserChannelSession(this.channel, userId, newSessionId);
|
|
499
|
+
this.userSessions.set(userId, newSessionId);
|
|
500
500
|
await interaction.reply({ content: '✅ New session started.' });
|
|
501
501
|
}
|
|
502
502
|
catch (err) {
|
|
@@ -506,6 +506,13 @@ export class DiscordAdapter {
|
|
|
506
506
|
async cmdSessions(interaction) {
|
|
507
507
|
try {
|
|
508
508
|
const history = new SQLiteChatMessageHistory({ sessionId: '' });
|
|
509
|
+
const userId = interaction.user.id;
|
|
510
|
+
// Get user's current session (string | undefined)
|
|
511
|
+
let userCurrentSession = this.userSessions.get(userId) || undefined;
|
|
512
|
+
if (userCurrentSession === undefined) {
|
|
513
|
+
const fromDb = await this.history.getUserChannelSession(this.channel, userId);
|
|
514
|
+
userCurrentSession = fromDb ?? undefined;
|
|
515
|
+
}
|
|
509
516
|
const sessions = (await history.listSessions()).filter((s) => !s.id.startsWith('chronos-job-') && !s.id.startsWith('sati-evaluation'));
|
|
510
517
|
history.close();
|
|
511
518
|
if (sessions.length === 0) {
|
|
@@ -516,7 +523,7 @@ export class DiscordAdapter {
|
|
|
516
523
|
const rows = [];
|
|
517
524
|
for (const session of sessions) {
|
|
518
525
|
const title = session.title || 'Untitled Session';
|
|
519
|
-
const isCurrent = session.id ===
|
|
526
|
+
const isCurrent = session.id === userCurrentSession;
|
|
520
527
|
const icon = isCurrent ? '🟢' : '⚪';
|
|
521
528
|
const started = new Date(session.started_at).toLocaleString();
|
|
522
529
|
lines.push(`${icon} **${title}**`);
|
|
@@ -554,12 +561,14 @@ export class DiscordAdapter {
|
|
|
554
561
|
});
|
|
555
562
|
collector.on('collect', async (btn) => {
|
|
556
563
|
try {
|
|
564
|
+
const uid = btn.user.id;
|
|
557
565
|
if (btn.customId.startsWith('session_switch_')) {
|
|
558
566
|
const sid = btn.customId.replace('session_switch_', '');
|
|
559
567
|
const h = new SQLiteChatMessageHistory({ sessionId: '' });
|
|
560
568
|
await h.switchSession(sid);
|
|
561
569
|
h.close();
|
|
562
|
-
this.
|
|
570
|
+
await this.history.setUserChannelSession(this.channel, uid, sid);
|
|
571
|
+
this.userSessions.set(uid, sid);
|
|
563
572
|
await btn.reply({ content: `✅ Switched to session \`${sid}\`.`, ephemeral: true });
|
|
564
573
|
}
|
|
565
574
|
else if (btn.customId.startsWith('session_archive_')) {
|
|
@@ -567,8 +576,12 @@ export class DiscordAdapter {
|
|
|
567
576
|
const h = new SQLiteChatMessageHistory({ sessionId: '' });
|
|
568
577
|
await h.archiveSession(sid);
|
|
569
578
|
h.close();
|
|
570
|
-
if
|
|
571
|
-
|
|
579
|
+
// Remove user-channel mapping if this was their current session
|
|
580
|
+
const current = this.userSessions.get(uid);
|
|
581
|
+
if (current === sid) {
|
|
582
|
+
await this.history.deleteUserChannelSession(this.channel, uid);
|
|
583
|
+
this.userSessions.delete(uid);
|
|
584
|
+
}
|
|
572
585
|
await btn.reply({ content: `📂 Session \`${sid}\` archived.`, ephemeral: true });
|
|
573
586
|
}
|
|
574
587
|
else if (btn.customId.startsWith('session_delete_')) {
|
|
@@ -576,8 +589,12 @@ export class DiscordAdapter {
|
|
|
576
589
|
const h = new SQLiteChatMessageHistory({ sessionId: '' });
|
|
577
590
|
await h.deleteSession(sid);
|
|
578
591
|
h.close();
|
|
579
|
-
if
|
|
580
|
-
|
|
592
|
+
// Remove user-channel mapping if this was their current session
|
|
593
|
+
const current = this.userSessions.get(uid);
|
|
594
|
+
if (current === sid) {
|
|
595
|
+
await this.history.deleteUserChannelSession(this.channel, uid);
|
|
596
|
+
this.userSessions.delete(uid);
|
|
597
|
+
}
|
|
581
598
|
await btn.reply({ content: `🗑️ Session \`${sid}\` deleted.`, ephemeral: true });
|
|
582
599
|
}
|
|
583
600
|
}
|
|
@@ -602,7 +619,9 @@ export class DiscordAdapter {
|
|
|
602
619
|
const history = new SQLiteChatMessageHistory({ sessionId: '' });
|
|
603
620
|
await history.switchSession(sessionId);
|
|
604
621
|
history.close();
|
|
605
|
-
|
|
622
|
+
const userId = interaction.user.id;
|
|
623
|
+
await this.history.setUserChannelSession(this.channel, userId, sessionId);
|
|
624
|
+
this.userSessions.set(userId, sessionId);
|
|
606
625
|
await interaction.reply({ content: `✅ Switched to session \`${sessionId}\`.` });
|
|
607
626
|
}
|
|
608
627
|
catch (err) {
|
|
@@ -773,10 +792,9 @@ export class DiscordAdapter {
|
|
|
773
792
|
}
|
|
774
793
|
async cmdSkillReload(interaction) {
|
|
775
794
|
try {
|
|
776
|
-
const { SkillRegistry
|
|
795
|
+
const { SkillRegistry } = await import('../runtime/skills/index.js');
|
|
777
796
|
const registry = SkillRegistry.getInstance();
|
|
778
797
|
const result = await registry.reload();
|
|
779
|
-
updateSkillDelegateDescription();
|
|
780
798
|
const msg = result.errors.length > 0
|
|
781
799
|
? `Reloaded ${result.skills.length} skills with ${result.errors.length} error(s).`
|
|
782
800
|
: `Reloaded ${result.skills.length} skill(s).`;
|
|
@@ -789,14 +807,13 @@ export class DiscordAdapter {
|
|
|
789
807
|
async cmdSkillEnable(interaction) {
|
|
790
808
|
const name = interaction.options.getString('name', true);
|
|
791
809
|
try {
|
|
792
|
-
const { SkillRegistry
|
|
810
|
+
const { SkillRegistry } = await import('../runtime/skills/index.js');
|
|
793
811
|
const registry = SkillRegistry.getInstance();
|
|
794
812
|
const success = registry.enable(name);
|
|
795
813
|
if (!success) {
|
|
796
814
|
await interaction.reply({ content: `Skill "${name}" not found.` });
|
|
797
815
|
return;
|
|
798
816
|
}
|
|
799
|
-
updateSkillDelegateDescription();
|
|
800
817
|
await interaction.reply({ content: `Skill \`${name}\` enabled.` });
|
|
801
818
|
}
|
|
802
819
|
catch (err) {
|
|
@@ -806,14 +823,13 @@ export class DiscordAdapter {
|
|
|
806
823
|
async cmdSkillDisable(interaction) {
|
|
807
824
|
const name = interaction.options.getString('name', true);
|
|
808
825
|
try {
|
|
809
|
-
const { SkillRegistry
|
|
826
|
+
const { SkillRegistry } = await import('../runtime/skills/index.js');
|
|
810
827
|
const registry = SkillRegistry.getInstance();
|
|
811
828
|
const success = registry.disable(name);
|
|
812
829
|
if (!success) {
|
|
813
830
|
await interaction.reply({ content: `Skill "${name}" not found.` });
|
|
814
831
|
return;
|
|
815
832
|
}
|
|
816
|
-
updateSkillDelegateDescription();
|
|
817
833
|
await interaction.reply({ content: `Skill \`${name}\` disabled.` });
|
|
818
834
|
}
|
|
819
835
|
catch (err) {
|
|
@@ -898,6 +914,40 @@ export class DiscordAdapter {
|
|
|
898
914
|
isAuthorized(userId) {
|
|
899
915
|
return this.allowedUsers.includes(userId);
|
|
900
916
|
}
|
|
917
|
+
/**
|
|
918
|
+
* Gets or creates a session for a specific user.
|
|
919
|
+
* Uses triple fallback: memory → DB → global session.
|
|
920
|
+
*/
|
|
921
|
+
async getSessionForUser(userId) {
|
|
922
|
+
// 1. Try memory
|
|
923
|
+
const fromMemory = this.userSessions.get(userId);
|
|
924
|
+
if (fromMemory)
|
|
925
|
+
return fromMemory;
|
|
926
|
+
// 2. Try DB
|
|
927
|
+
const fromDb = await this.history.getUserChannelSession(this.channel, userId);
|
|
928
|
+
if (fromDb !== null) {
|
|
929
|
+
this.userSessions.set(userId, fromDb);
|
|
930
|
+
return fromDb;
|
|
931
|
+
}
|
|
932
|
+
// 3. Create/use global session
|
|
933
|
+
const newSessionId = await this.history.getCurrentSessionOrCreate();
|
|
934
|
+
await this.history.setUserChannelSession(this.channel, userId, newSessionId);
|
|
935
|
+
this.userSessions.set(userId, newSessionId);
|
|
936
|
+
return newSessionId;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Restores user sessions from DB on startup.
|
|
940
|
+
* Reads user_channel_sessions table and populates userSessions Map.
|
|
941
|
+
*/
|
|
942
|
+
async restoreUserSessions() {
|
|
943
|
+
const rows = await this.history.listUserChannelSessions(this.channel);
|
|
944
|
+
for (const row of rows) {
|
|
945
|
+
this.userSessions.set(row.userId, row.sessionId);
|
|
946
|
+
}
|
|
947
|
+
if (rows.length > 0) {
|
|
948
|
+
this.display.log(`✓ Restored ${rows.length} user session(s) from database`, { source: 'Discord', level: 'info' });
|
|
949
|
+
}
|
|
950
|
+
}
|
|
901
951
|
isRateLimited(userId) {
|
|
902
952
|
const now = Date.now();
|
|
903
953
|
const last = this.rateLimitMap.get(userId);
|
|
@@ -144,8 +144,8 @@ export class TelegramAdapter {
|
|
|
144
144
|
telephonistProvider = null;
|
|
145
145
|
telephonistModel = null;
|
|
146
146
|
history = new SQLiteChatMessageHistory({ sessionId: '' });
|
|
147
|
-
/** Per-
|
|
148
|
-
|
|
147
|
+
/** Per-user session tracking — maps userId to sessionId */
|
|
148
|
+
userSessions = new Map();
|
|
149
149
|
RATE_LIMIT_MS = 3000; // minimum ms between requests per user
|
|
150
150
|
rateLimiter = new Map(); // userId -> last request timestamp
|
|
151
151
|
// Pending Chronos create confirmations (userId -> job data + expiry)
|
|
@@ -232,8 +232,7 @@ export class TelegramAdapter {
|
|
|
232
232
|
try {
|
|
233
233
|
// Send "typing" status
|
|
234
234
|
await ctx.sendChatAction('typing');
|
|
235
|
-
const sessionId =
|
|
236
|
-
this.currentSessionId = sessionId;
|
|
235
|
+
const sessionId = await this.getSessionForUser(userId);
|
|
237
236
|
// Process with Agent
|
|
238
237
|
const response = await this.oracle.chat(text, undefined, false, {
|
|
239
238
|
origin_channel: 'telegram',
|
|
@@ -319,7 +318,7 @@ export class TelegramAdapter {
|
|
|
319
318
|
this.display.log(`Transcription success for @${user}: "${text}"`, { source: 'Telephonist', level: 'success' });
|
|
320
319
|
// Audit: record telephonist execution
|
|
321
320
|
try {
|
|
322
|
-
const auditSessionId =
|
|
321
|
+
const auditSessionId = await this.getSessionForUser(userId);
|
|
323
322
|
AuditRepository.getInstance().insert({
|
|
324
323
|
session_id: auditSessionId,
|
|
325
324
|
event_type: 'telephonist',
|
|
@@ -347,8 +346,7 @@ export class TelegramAdapter {
|
|
|
347
346
|
// So I should treat 'text' as if it was a text message.
|
|
348
347
|
await ctx.reply(`🎤 Transcription: "${text}"`);
|
|
349
348
|
await ctx.sendChatAction('typing');
|
|
350
|
-
const sessionId =
|
|
351
|
-
this.currentSessionId = sessionId;
|
|
349
|
+
const sessionId = await this.getSessionForUser(userId);
|
|
352
350
|
// Process with Agent
|
|
353
351
|
const response = await this.oracle.chat(text, usage, true, {
|
|
354
352
|
origin_channel: 'telegram',
|
|
@@ -382,7 +380,7 @@ export class TelegramAdapter {
|
|
|
382
380
|
const detail = error?.cause?.message || error?.response?.data?.error?.message || error.message;
|
|
383
381
|
this.display.log(`Audio processing error for @${user}: ${detail}`, { source: 'Telephonist', level: 'error' });
|
|
384
382
|
try {
|
|
385
|
-
const auditSessionId = this.
|
|
383
|
+
const auditSessionId = await this.getSessionForUser(userId);
|
|
386
384
|
AuditRepository.getInstance().insert({
|
|
387
385
|
session_id: auditSessionId,
|
|
388
386
|
event_type: 'telephonist',
|
|
@@ -425,6 +423,7 @@ export class TelegramAdapter {
|
|
|
425
423
|
const callbackQuery = ctx.callbackQuery;
|
|
426
424
|
const data = callbackQuery && 'data' in callbackQuery ? callbackQuery.data : undefined;
|
|
427
425
|
const sessionId = typeof data === 'string' ? data.replace('switch_session_', '') : '';
|
|
426
|
+
const userId = ctx.from.id.toString();
|
|
428
427
|
if (!sessionId || sessionId === '') {
|
|
429
428
|
await ctx.answerCbQuery('Invalid session ID');
|
|
430
429
|
return;
|
|
@@ -434,8 +433,9 @@ export class TelegramAdapter {
|
|
|
434
433
|
const history = new SQLiteChatMessageHistory({ sessionId: "" });
|
|
435
434
|
await history.switchSession(sessionId);
|
|
436
435
|
history.close();
|
|
437
|
-
//
|
|
438
|
-
this.
|
|
436
|
+
// Update user-channel session mapping
|
|
437
|
+
await this.history.setUserChannelSession(this.channel, userId, sessionId);
|
|
438
|
+
this.userSessions.set(userId, sessionId);
|
|
439
439
|
await ctx.answerCbQuery();
|
|
440
440
|
// Remove the previous message and send confirmation
|
|
441
441
|
if (ctx.updateType === 'callback_query') {
|
|
@@ -468,9 +468,16 @@ export class TelegramAdapter {
|
|
|
468
468
|
this.bot.action(/^confirm_archive_session_/, async (ctx) => {
|
|
469
469
|
const data = ctx.callbackQuery.data;
|
|
470
470
|
const sessionId = data.replace('confirm_archive_session_', '');
|
|
471
|
+
const userId = ctx.from.id.toString();
|
|
471
472
|
try {
|
|
472
473
|
const history = new SQLiteChatMessageHistory({ sessionId: "" });
|
|
473
474
|
await history.archiveSession(sessionId);
|
|
475
|
+
// Remove user-channel session mapping if this was their current session
|
|
476
|
+
const currentSession = this.userSessions.get(userId);
|
|
477
|
+
if (currentSession === sessionId) {
|
|
478
|
+
await this.history.deleteUserChannelSession(this.channel, userId);
|
|
479
|
+
this.userSessions.delete(userId);
|
|
480
|
+
}
|
|
474
481
|
await ctx.answerCbQuery('Session archived successfully');
|
|
475
482
|
if (ctx.updateType === 'callback_query') {
|
|
476
483
|
ctx.deleteMessage().catch(() => { });
|
|
@@ -501,9 +508,16 @@ export class TelegramAdapter {
|
|
|
501
508
|
this.bot.action(/^confirm_delete_session_/, async (ctx) => {
|
|
502
509
|
const data = ctx.callbackQuery.data;
|
|
503
510
|
const sessionId = data.replace('confirm_delete_session_', '');
|
|
511
|
+
const userId = ctx.from.id.toString();
|
|
504
512
|
try {
|
|
505
513
|
const history = new SQLiteChatMessageHistory({ sessionId: "" });
|
|
506
514
|
await history.deleteSession(sessionId);
|
|
515
|
+
// Remove user-channel session mapping if this was their current session
|
|
516
|
+
const currentSession = this.userSessions.get(userId);
|
|
517
|
+
if (currentSession === sessionId) {
|
|
518
|
+
await this.history.deleteUserChannelSession(this.channel, userId);
|
|
519
|
+
this.userSessions.delete(userId);
|
|
520
|
+
}
|
|
507
521
|
await ctx.answerCbQuery('Session deleted successfully');
|
|
508
522
|
if (ctx.updateType === 'callback_query') {
|
|
509
523
|
ctx.deleteMessage().catch(() => { });
|
|
@@ -690,6 +704,40 @@ export class TelegramAdapter {
|
|
|
690
704
|
isAuthorized(userId, allowedUsers) {
|
|
691
705
|
return allowedUsers.includes(userId);
|
|
692
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Gets or creates a session for a specific user.
|
|
709
|
+
* Uses triple fallback: memory → DB → global session.
|
|
710
|
+
*/
|
|
711
|
+
async getSessionForUser(userId) {
|
|
712
|
+
// 1. Try memory
|
|
713
|
+
const fromMemory = this.userSessions.get(userId);
|
|
714
|
+
if (fromMemory)
|
|
715
|
+
return fromMemory;
|
|
716
|
+
// 2. Try DB
|
|
717
|
+
const fromDb = await this.history.getUserChannelSession(this.channel, userId);
|
|
718
|
+
if (fromDb !== null) {
|
|
719
|
+
this.userSessions.set(userId, fromDb);
|
|
720
|
+
return fromDb;
|
|
721
|
+
}
|
|
722
|
+
// 3. Create/use global session
|
|
723
|
+
const newSessionId = await this.history.getCurrentSessionOrCreate();
|
|
724
|
+
await this.history.setUserChannelSession(this.channel, userId, newSessionId);
|
|
725
|
+
this.userSessions.set(userId, newSessionId);
|
|
726
|
+
return newSessionId;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Restores user sessions from DB on startup.
|
|
730
|
+
* Reads user_channel_sessions table and populates userSessions Map.
|
|
731
|
+
*/
|
|
732
|
+
async restoreUserSessions() {
|
|
733
|
+
const rows = await this.history.listUserChannelSessions(this.channel);
|
|
734
|
+
for (const row of rows) {
|
|
735
|
+
this.userSessions.set(row.userId, row.sessionId);
|
|
736
|
+
}
|
|
737
|
+
if (rows.length > 0) {
|
|
738
|
+
this.display.log(`✓ Restored ${rows.length} user session(s) from database`, { source: 'Telegram', level: 'info' });
|
|
739
|
+
}
|
|
740
|
+
}
|
|
693
741
|
async downloadToTemp(url, extension = '.ogg') {
|
|
694
742
|
const response = await fetch(url);
|
|
695
743
|
if (!response.ok)
|
|
@@ -1087,10 +1135,9 @@ export class TelegramAdapter {
|
|
|
1087
1135
|
}
|
|
1088
1136
|
async handleSkillsReload(ctx) {
|
|
1089
1137
|
try {
|
|
1090
|
-
const { SkillRegistry
|
|
1138
|
+
const { SkillRegistry } = await import('../runtime/skills/index.js');
|
|
1091
1139
|
const registry = SkillRegistry.getInstance();
|
|
1092
1140
|
const result = await registry.reload();
|
|
1093
|
-
updateSkillDelegateDescription();
|
|
1094
1141
|
const msg = result.errors.length > 0
|
|
1095
1142
|
? `Reloaded ${result.skills.length} skills with ${result.errors.length} error(s).`
|
|
1096
1143
|
: `Reloaded ${result.skills.length} skill(s).`;
|
|
@@ -1106,14 +1153,13 @@ export class TelegramAdapter {
|
|
|
1106
1153
|
return;
|
|
1107
1154
|
}
|
|
1108
1155
|
try {
|
|
1109
|
-
const { SkillRegistry
|
|
1156
|
+
const { SkillRegistry } = await import('../runtime/skills/index.js');
|
|
1110
1157
|
const registry = SkillRegistry.getInstance();
|
|
1111
1158
|
const success = registry.enable(name);
|
|
1112
1159
|
if (!success) {
|
|
1113
1160
|
await ctx.reply(`Skill "${name}" not found.`);
|
|
1114
1161
|
return;
|
|
1115
1162
|
}
|
|
1116
|
-
updateSkillDelegateDescription();
|
|
1117
1163
|
await ctx.reply(`Skill \`${name}\` enabled.`, { parse_mode: 'Markdown' });
|
|
1118
1164
|
}
|
|
1119
1165
|
catch (err) {
|
|
@@ -1126,14 +1172,13 @@ export class TelegramAdapter {
|
|
|
1126
1172
|
return;
|
|
1127
1173
|
}
|
|
1128
1174
|
try {
|
|
1129
|
-
const { SkillRegistry
|
|
1175
|
+
const { SkillRegistry } = await import('../runtime/skills/index.js');
|
|
1130
1176
|
const registry = SkillRegistry.getInstance();
|
|
1131
1177
|
const success = registry.disable(name);
|
|
1132
1178
|
if (!success) {
|
|
1133
1179
|
await ctx.reply(`Skill "${name}" not found.`);
|
|
1134
1180
|
return;
|
|
1135
1181
|
}
|
|
1136
|
-
updateSkillDelegateDescription();
|
|
1137
1182
|
await ctx.reply(`Skill \`${name}\` disabled.`, { parse_mode: 'Markdown' });
|
|
1138
1183
|
}
|
|
1139
1184
|
catch (err) {
|
|
@@ -1160,8 +1205,10 @@ export class TelegramAdapter {
|
|
|
1160
1205
|
const history = new SQLiteChatMessageHistory({ sessionId: "" });
|
|
1161
1206
|
const newSessionId = await history.createNewSession();
|
|
1162
1207
|
history.close();
|
|
1163
|
-
// Track the new session as the current one for this
|
|
1164
|
-
|
|
1208
|
+
// Track the new session as the current one for this user
|
|
1209
|
+
const userId = ctx.from.id.toString();
|
|
1210
|
+
await this.history.setUserChannelSession(this.channel, userId, newSessionId);
|
|
1211
|
+
this.userSessions.set(userId, newSessionId);
|
|
1165
1212
|
}
|
|
1166
1213
|
catch (e) {
|
|
1167
1214
|
await ctx.reply(`Error creating new session: ${e.message}`);
|
|
@@ -1170,6 +1217,13 @@ export class TelegramAdapter {
|
|
|
1170
1217
|
async handleSessionStatusCommand(ctx, user) {
|
|
1171
1218
|
try {
|
|
1172
1219
|
const history = new SQLiteChatMessageHistory({ sessionId: "" });
|
|
1220
|
+
const userId = ctx.from.id.toString();
|
|
1221
|
+
// Get user's current session (string | undefined)
|
|
1222
|
+
let userCurrentSession = this.userSessions.get(userId) || undefined;
|
|
1223
|
+
if (userCurrentSession === undefined) {
|
|
1224
|
+
const fromDb = await this.history.getUserChannelSession(this.channel, userId);
|
|
1225
|
+
userCurrentSession = fromDb ?? undefined;
|
|
1226
|
+
}
|
|
1173
1227
|
// Exclude automated Chronos sessions — their IDs exceed Telegram's 64-byte
|
|
1174
1228
|
// callback_data limit and they are not user-managed sessions.
|
|
1175
1229
|
const sessions = (await history.listSessions()).filter((s) => !s.id.startsWith('chronos-job-') && !s.id.startsWith('sati-evaluation'));
|
|
@@ -1181,7 +1235,7 @@ export class TelegramAdapter {
|
|
|
1181
1235
|
const keyboard = [];
|
|
1182
1236
|
for (const session of sessions) {
|
|
1183
1237
|
const title = session.title || 'Untitled Session';
|
|
1184
|
-
const isCurrent = session.id ===
|
|
1238
|
+
const isCurrent = session.id === userCurrentSession;
|
|
1185
1239
|
const statusEmoji = isCurrent ? '🟢' : '⚪';
|
|
1186
1240
|
response += `${statusEmoji} *${escMdRaw(title)}*\n`;
|
|
1187
1241
|
response += `\\- ID: \`${escMdRaw(session.id)}\`\n`;
|
|
@@ -15,6 +15,8 @@ import { HttpServer } from '../../http/server.js';
|
|
|
15
15
|
import { getVersion } from '../utils/version.js';
|
|
16
16
|
import { TaskWorker } from '../../runtime/tasks/worker.js';
|
|
17
17
|
import { TaskNotifier } from '../../runtime/tasks/notifier.js';
|
|
18
|
+
import { Link } from '../../runtime/link.js';
|
|
19
|
+
import { LinkWorker } from '../../runtime/link-worker.js';
|
|
18
20
|
export const restartCommand = new Command('restart')
|
|
19
21
|
.description('Restart the Morpheus agent')
|
|
20
22
|
.option('--ui', 'Enable web UI', true)
|
|
@@ -97,6 +99,15 @@ export const restartCommand = new Command('restart')
|
|
|
97
99
|
await clearPid();
|
|
98
100
|
process.exit(1);
|
|
99
101
|
}
|
|
102
|
+
// Initialize Link (Documentation Specialist)
|
|
103
|
+
try {
|
|
104
|
+
const link = Link.getInstance(config);
|
|
105
|
+
await link.initialize();
|
|
106
|
+
display.log(chalk.green('✓ Link initialized'), { source: 'Link' });
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
display.log(chalk.yellow(`Link initialization warning: ${err.message}`), { source: 'Link' });
|
|
110
|
+
}
|
|
100
111
|
const adapters = [];
|
|
101
112
|
let httpServer;
|
|
102
113
|
const taskWorker = new TaskWorker();
|
|
@@ -135,6 +146,9 @@ export const restartCommand = new Command('restart')
|
|
|
135
146
|
taskWorker.start();
|
|
136
147
|
taskNotifier.start();
|
|
137
148
|
}
|
|
149
|
+
// Start LinkWorker for document indexing
|
|
150
|
+
const linkWorker = LinkWorker.getInstance();
|
|
151
|
+
linkWorker.start();
|
|
138
152
|
// Handle graceful shutdown
|
|
139
153
|
const shutdown = async (signal) => {
|
|
140
154
|
display.stopSpinner();
|
|
@@ -145,6 +159,7 @@ export const restartCommand = new Command('restart')
|
|
|
145
159
|
for (const adapter of adapters) {
|
|
146
160
|
await adapter.disconnect();
|
|
147
161
|
}
|
|
162
|
+
linkWorker.stop();
|
|
148
163
|
if (asyncTasksEnabled) {
|
|
149
164
|
taskWorker.stop();
|
|
150
165
|
taskNotifier.stop();
|