myaiforone 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -0
- package/agents/_template/CLAUDE.md +18 -0
- package/agents/_template/agent.json +7 -0
- package/agents/platform/agentcreator/CLAUDE.md +300 -0
- package/agents/platform/appcreator/CLAUDE.md +158 -0
- package/agents/platform/gym/CLAUDE.md +486 -0
- package/agents/platform/gym/agent.json +40 -0
- package/agents/platform/gym/programs/agent-building/program.json +160 -0
- package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
- package/agents/platform/gym/programs/getting-started/program.json +124 -0
- package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
- package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
- package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
- package/agents/platform/gym/souls/alex.md +12 -0
- package/agents/platform/gym/souls/jordan.md +12 -0
- package/agents/platform/gym/souls/morgan.md +12 -0
- package/agents/platform/gym/souls/riley.md +12 -0
- package/agents/platform/gym/souls/sam.md +12 -0
- package/agents/platform/hub/CLAUDE.md +372 -0
- package/agents/platform/promptcreator/CLAUDE.md +130 -0
- package/agents/platform/skillcreator/CLAUDE.md +163 -0
- package/bin/cli.js +566 -0
- package/config.example.json +310 -0
- package/dist/agent-registry.d.ts +32 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +144 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/channels/discord.d.ts +17 -0
- package/dist/channels/discord.d.ts.map +1 -0
- package/dist/channels/discord.js +114 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/imessage.d.ts +23 -0
- package/dist/channels/imessage.d.ts.map +1 -0
- package/dist/channels/imessage.js +214 -0
- package/dist/channels/imessage.js.map +1 -0
- package/dist/channels/slack.d.ts +19 -0
- package/dist/channels/slack.d.ts.map +1 -0
- package/dist/channels/slack.js +167 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/telegram.d.ts +19 -0
- package/dist/channels/telegram.d.ts.map +1 -0
- package/dist/channels/telegram.js +274 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/types.d.ts +44 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +18 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/channels/whatsapp.d.ts +23 -0
- package/dist/channels/whatsapp.d.ts.map +1 -0
- package/dist/channels/whatsapp.js +189 -0
- package/dist/channels/whatsapp.js.map +1 -0
- package/dist/config.d.ts +134 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +127 -0
- package/dist/config.js.map +1 -0
- package/dist/cron.d.ts +8 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +35 -0
- package/dist/cron.js.map +1 -0
- package/dist/decrypt-keys.d.ts +7 -0
- package/dist/decrypt-keys.d.ts.map +1 -0
- package/dist/decrypt-keys.js +53 -0
- package/dist/decrypt-keys.js.map +1 -0
- package/dist/encrypt-keys.d.ts +8 -0
- package/dist/encrypt-keys.d.ts.map +1 -0
- package/dist/encrypt-keys.js +62 -0
- package/dist/encrypt-keys.js.map +1 -0
- package/dist/executor.d.ts +31 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +2009 -0
- package/dist/executor.js.map +1 -0
- package/dist/gemini-executor.d.ts +27 -0
- package/dist/gemini-executor.d.ts.map +1 -0
- package/dist/gemini-executor.js +160 -0
- package/dist/gemini-executor.js.map +1 -0
- package/dist/goals.d.ts +24 -0
- package/dist/goals.d.ts.map +1 -0
- package/dist/goals.js +189 -0
- package/dist/goals.js.map +1 -0
- package/dist/gym/activity-digest.d.ts +30 -0
- package/dist/gym/activity-digest.d.ts.map +1 -0
- package/dist/gym/activity-digest.js +506 -0
- package/dist/gym/activity-digest.js.map +1 -0
- package/dist/gym/dimension-scorer.d.ts +76 -0
- package/dist/gym/dimension-scorer.d.ts.map +1 -0
- package/dist/gym/dimension-scorer.js +236 -0
- package/dist/gym/dimension-scorer.js.map +1 -0
- package/dist/gym/gym-router.d.ts +7 -0
- package/dist/gym/gym-router.d.ts.map +1 -0
- package/dist/gym/gym-router.js +718 -0
- package/dist/gym/gym-router.js.map +1 -0
- package/dist/gym/index.d.ts +11 -0
- package/dist/gym/index.d.ts.map +1 -0
- package/dist/gym/index.js +11 -0
- package/dist/gym/index.js.map +1 -0
- package/dist/heartbeat.d.ts +21 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +163 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/keystore.d.ts +22 -0
- package/dist/keystore.d.ts.map +1 -0
- package/dist/keystore.js +178 -0
- package/dist/keystore.js.map +1 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +45 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/daily.d.ts +22 -0
- package/dist/memory/daily.d.ts.map +1 -0
- package/dist/memory/daily.js +82 -0
- package/dist/memory/daily.js.map +1 -0
- package/dist/memory/embeddings.d.ts +15 -0
- package/dist/memory/embeddings.d.ts.map +1 -0
- package/dist/memory/embeddings.js +154 -0
- package/dist/memory/embeddings.js.map +1 -0
- package/dist/memory/index.d.ts +32 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +159 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/search.d.ts +21 -0
- package/dist/memory/search.d.ts.map +1 -0
- package/dist/memory/search.js +77 -0
- package/dist/memory/search.js.map +1 -0
- package/dist/memory/store.d.ts +23 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +144 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/ollama-executor.d.ts +17 -0
- package/dist/ollama-executor.d.ts.map +1 -0
- package/dist/ollama-executor.js +112 -0
- package/dist/ollama-executor.js.map +1 -0
- package/dist/openai-executor.d.ts +38 -0
- package/dist/openai-executor.d.ts.map +1 -0
- package/dist/openai-executor.js +197 -0
- package/dist/openai-executor.js.map +1 -0
- package/dist/router.d.ts +11 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +185 -0
- package/dist/router.js.map +1 -0
- package/dist/test-message.d.ts +2 -0
- package/dist/test-message.d.ts.map +1 -0
- package/dist/test-message.js +60 -0
- package/dist/test-message.js.map +1 -0
- package/dist/utils/imsg-db-reader.d.ts +24 -0
- package/dist/utils/imsg-db-reader.d.ts.map +1 -0
- package/dist/utils/imsg-db-reader.js +92 -0
- package/dist/utils/imsg-db-reader.js.map +1 -0
- package/dist/utils/imsg-rpc.d.ts +25 -0
- package/dist/utils/imsg-rpc.d.ts.map +1 -0
- package/dist/utils/imsg-rpc.js +149 -0
- package/dist/utils/imsg-rpc.js.map +1 -0
- package/dist/utils/message-formatter.d.ts +3 -0
- package/dist/utils/message-formatter.d.ts.map +1 -0
- package/dist/utils/message-formatter.js +69 -0
- package/dist/utils/message-formatter.js.map +1 -0
- package/dist/web-ui.d.ts +12 -0
- package/dist/web-ui.d.ts.map +1 -0
- package/dist/web-ui.js +5784 -0
- package/dist/web-ui.js.map +1 -0
- package/dist/whatsapp-chats.d.ts +2 -0
- package/dist/whatsapp-chats.d.ts.map +1 -0
- package/dist/whatsapp-chats.js +76 -0
- package/dist/whatsapp-chats.js.map +1 -0
- package/dist/whatsapp-login.d.ts +2 -0
- package/dist/whatsapp-login.d.ts.map +1 -0
- package/dist/whatsapp-login.js +90 -0
- package/dist/whatsapp-login.js.map +1 -0
- package/dist/wiki-sync.d.ts +21 -0
- package/dist/wiki-sync.d.ts.map +1 -0
- package/dist/wiki-sync.js +147 -0
- package/dist/wiki-sync.js.map +1 -0
- package/docs/AddNewAgentGuide.md +100 -0
- package/docs/AddNewMcpGuide.md +72 -0
- package/docs/Architecture.md +795 -0
- package/docs/CLAUDE-AI-SETUP.md +166 -0
- package/docs/Setup.md +297 -0
- package/docs/ai-gym-architecture.md +1040 -0
- package/docs/ai-gym-build-plan.md +343 -0
- package/docs/ai-gym-onboarding.md +122 -0
- package/docs/appcreator_plan.md +348 -0
- package/docs/platform-mcp-audit.md +320 -0
- package/docs/server-deployment-plan.md +503 -0
- package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
- package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
- package/docs/user-guide.md +2016 -0
- package/mcp-catalog.json +628 -0
- package/package.json +63 -0
- package/public/MyAIforOne-logomark-512.svg +16 -0
- package/public/MyAIforOne-logomark-transparent.svg +15 -0
- package/public/activity.html +314 -0
- package/public/admin.html +1674 -0
- package/public/agent-dashboard.html +670 -0
- package/public/api-docs.html +1106 -0
- package/public/automations.html +722 -0
- package/public/canvas.css +223 -0
- package/public/canvas.js +588 -0
- package/public/changelog.html +231 -0
- package/public/gym.html +2766 -0
- package/public/home.html +1930 -0
- package/public/index.html +2809 -0
- package/public/lab.html +1643 -0
- package/public/library.html +1442 -0
- package/public/marketplace.html +1101 -0
- package/public/mcp-docs.html +441 -0
- package/public/mini.html +390 -0
- package/public/monitor.html +584 -0
- package/public/org.html +4304 -0
- package/public/projects.html +734 -0
- package/public/settings.html +645 -0
- package/public/tasks.html +932 -0
- package/public/trainers/alex.svg +12 -0
- package/public/trainers/jordan.svg +12 -0
- package/public/trainers/morgan.svg +12 -0
- package/public/trainers/riley.svg +12 -0
- package/public/trainers/sam.svg +12 -0
- package/public/user-guide.html +218 -0
- package/registry/agents.json +3 -0
- package/registry/apps.json +20 -0
- package/registry/installed-drafts.json +3 -0
- package/registry/mcps.json +1084 -0
- package/registry/prompts/personal/mcp-test-prompt.md +6 -0
- package/registry/prompts/personal/memory-recall.md +6 -0
- package/registry/prompts/platform/brainstorm.md +15 -0
- package/registry/prompts/platform/code-review.md +16 -0
- package/registry/prompts/platform/explain.md +16 -0
- package/registry/prompts.json +58 -0
- package/registry/skills/external/brainstorming.md +5 -0
- package/registry/skills/external/code-review.md +40 -0
- package/registry/skills/external/frontend-patterns.md +642 -0
- package/registry/skills/external/frontend-slides.md +184 -0
- package/registry/skills/external/systematic-debugging.md +5 -0
- package/registry/skills/external/tdd.md +328 -0
- package/registry/skills/external/verification-before-completion.md +5 -0
- package/registry/skills/external/writing-plans.md +5 -0
- package/registry/skills/platform/ai41_app_build.md +930 -0
- package/registry/skills/platform/ai41_app_deploy.md +168 -0
- package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
- package/registry/skills/platform/ai41_app_patterns.md +359 -0
- package/registry/skills/platform/ai41_app_register.md +85 -0
- package/registry/skills/platform/ai41_app_scaffold.md +421 -0
- package/registry/skills/platform/ai41_app_verify.md +107 -0
- package/registry/skills/platform/opProjectCreate.md +239 -0
- package/registry/skills/platform/op_devbrowser.md +136 -0
- package/registry/skills/platform/sop_brandguidelines.md +103 -0
- package/registry/skills/platform/sop_docx.md +117 -0
- package/registry/skills/platform/sop_frontenddesign.md +44 -0
- package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
- package/registry/skills/platform/sop_mcpbuilder.md +133 -0
- package/registry/skills/platform/sop_pdf.md +172 -0
- package/registry/skills/platform/sop_pptx.md +133 -0
- package/registry/skills/platform/sop_skillcreator.md +104 -0
- package/registry/skills/platform/sop_themefactory.md +128 -0
- package/registry/skills/platform/sop_webapptesting.md +75 -0
- package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
- package/registry/skills/platform/sop_xlsx.md +134 -0
- package/registry/skills.json +1055 -0
- package/scripts/discover-chats.sh +11 -0
- package/scripts/install-service-windows.ps1 +87 -0
- package/scripts/install-service.sh +52 -0
- package/scripts/seed-registry.ts +195 -0
- package/scripts/test-send.sh +5 -0
- package/scripts/tray-indicator.ps1 +35 -0
- package/scripts/uninstall-service-windows.ps1 +23 -0
- package/scripts/uninstall-service.sh +15 -0
- package/scripts/xbar-myagent.5s.sh +32 -0
- package/server/mcp-server/dist/index.d.ts +11 -0
- package/server/mcp-server/dist/index.js +1332 -0
- package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
- package/server/mcp-server/dist/lib/api-client.js +241 -0
- package/server/mcp-server/index.ts +1545 -0
- package/server/mcp-server/lib/api-client.ts +366 -0
- package/server/mcp-server/tsconfig.json +14 -0
- package/src/agent-registry.ts +180 -0
- package/src/channels/discord.ts +129 -0
- package/src/channels/imessage.ts +261 -0
- package/src/channels/slack.ts +208 -0
- package/src/channels/telegram.ts +307 -0
- package/src/channels/types.ts +62 -0
- package/src/channels/whatsapp.ts +227 -0
- package/src/config.ts +281 -0
- package/src/cron.ts +43 -0
- package/src/decrypt-keys.ts +60 -0
- package/src/encrypt-keys.ts +70 -0
- package/src/executor.ts +2190 -0
- package/src/gemini-executor.ts +212 -0
- package/src/goals.ts +240 -0
- package/src/gym/activity-digest.ts +546 -0
- package/src/gym/dimension-scorer.ts +297 -0
- package/src/gym/gym-router.ts +801 -0
- package/src/gym/index.ts +19 -0
- package/src/heartbeat.ts +220 -0
- package/src/index.ts +275 -0
- package/src/keystore.ts +190 -0
- package/src/logger.ts +51 -0
- package/src/memory/daily.ts +101 -0
- package/src/memory/embeddings.ts +185 -0
- package/src/memory/index.ts +218 -0
- package/src/memory/search.ts +124 -0
- package/src/memory/store.ts +189 -0
- package/src/ollama-executor.ts +126 -0
- package/src/openai-executor.ts +259 -0
- package/src/router.ts +230 -0
- package/src/test-message.ts +72 -0
- package/src/utils/imsg-db-reader.ts +109 -0
- package/src/utils/imsg-rpc.ts +178 -0
- package/src/utils/message-formatter.ts +90 -0
- package/src/web-ui.ts +5778 -0
- package/src/whatsapp-chats.ts +91 -0
- package/src/whatsapp-login.ts +110 -0
- package/src/wiki-sync.ts +199 -0
- package/tsconfig.json +19 -0
package/src/gym/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Gym — Module barrel
|
|
3
|
+
*
|
|
4
|
+
* All gym-specific code is centralized here. Platform files (web-ui.ts, etc.)
|
|
5
|
+
* import from this module only at registration points.
|
|
6
|
+
* When `gymEnabled: false`, nothing in this module runs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { createGymRouter } from "./gym-router.js";
|
|
10
|
+
export { startActivityDigest, stopActivityDigest, runActivityDigest } from "./activity-digest.js";
|
|
11
|
+
export {
|
|
12
|
+
scoreAllDimensions,
|
|
13
|
+
computeTrends,
|
|
14
|
+
scoreApplication,
|
|
15
|
+
scoreCommunication,
|
|
16
|
+
scoreKnowledge,
|
|
17
|
+
scoreOrchestration,
|
|
18
|
+
scoreCraft,
|
|
19
|
+
} from "./dimension-scorer.js";
|
package/src/heartbeat.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { readFileSync, appendFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import type { AppConfig, McpServerConfig } from "./config.js";
|
|
4
|
+
import type { InboundMessage } from "./channels/types.js";
|
|
5
|
+
import type { ResolvedRoute } from "./router.js";
|
|
6
|
+
import { executeAgent } from "./executor.js";
|
|
7
|
+
import { log } from "./logger.js";
|
|
8
|
+
|
|
9
|
+
export interface HeartbeatResult {
|
|
10
|
+
id: string;
|
|
11
|
+
agentId: string;
|
|
12
|
+
triggeredBy: string; // "manual" | "cron" | "goal"
|
|
13
|
+
triggeredAt: string;
|
|
14
|
+
completedAt: string;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
summary: string;
|
|
17
|
+
status: "success" | "error" | "timeout";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function todayKey(): string {
|
|
23
|
+
return new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Task loading (inlined from executor.ts) ─────────────────────────
|
|
27
|
+
|
|
28
|
+
interface TaskData {
|
|
29
|
+
agentId: string;
|
|
30
|
+
projects: Array<{ id: string; name: string; color: string }>;
|
|
31
|
+
tasks: Array<{
|
|
32
|
+
id: string; title: string; description: string; project: string;
|
|
33
|
+
priority: string; status: string; owner: string; assignedBy: string;
|
|
34
|
+
assignmentType: string; dueDate: string | null; context: string;
|
|
35
|
+
result: string; createdAt: string; updatedAt: string;
|
|
36
|
+
source?: string;
|
|
37
|
+
assignedTo?: string;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function loadTasksFromAgent(agentHome: string, agentId: string): TaskData {
|
|
42
|
+
const p = join(agentHome, "tasks.json");
|
|
43
|
+
if (existsSync(p)) {
|
|
44
|
+
try { return JSON.parse(readFileSync(p, "utf-8")); } catch { /* ignore */ }
|
|
45
|
+
}
|
|
46
|
+
return { agentId, projects: [{ id: "general", name: "General", color: "#6b7280" }], tasks: [] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Heartbeat prompt builder ────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export function buildHeartbeatPrompt(agentHome: string, agentId: string): string {
|
|
52
|
+
const data = loadTasksFromAgent(agentHome, agentId);
|
|
53
|
+
const activeStatuses = ["proposed", "approved", "in_progress", "review"];
|
|
54
|
+
const active = data.tasks.filter(t => activeStatuses.includes(t.status));
|
|
55
|
+
|
|
56
|
+
// Sort by priority: high > medium > low
|
|
57
|
+
const priorityOrder: Record<string, number> = { high: 0, medium: 1, low: 2 };
|
|
58
|
+
active.sort((a, b) => (priorityOrder[a.priority] ?? 99) - (priorityOrder[b.priority] ?? 99));
|
|
59
|
+
|
|
60
|
+
// Check for custom heartbeat.md — use it if present, else default
|
|
61
|
+
const heartbeatMdPath = join(agentHome, "heartbeat.md");
|
|
62
|
+
let customInstructions = "";
|
|
63
|
+
if (existsSync(heartbeatMdPath)) {
|
|
64
|
+
try {
|
|
65
|
+
const raw = readFileSync(heartbeatMdPath, "utf-8");
|
|
66
|
+
// Strip YAML frontmatter if present
|
|
67
|
+
customInstructions = raw.replace(/^---[\s\S]*?---\s*/, "").trim();
|
|
68
|
+
} catch { /* ignore */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lines: string[] = [
|
|
72
|
+
"[HEARTBEAT]",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
if (customInstructions) {
|
|
76
|
+
lines.push(customInstructions);
|
|
77
|
+
} else {
|
|
78
|
+
lines.push("You are being activated for a heartbeat check. Review your assigned tasks and work on the highest priority one.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
lines.push("");
|
|
82
|
+
lines.push("Active Tasks:");
|
|
83
|
+
|
|
84
|
+
if (active.length > 0) {
|
|
85
|
+
for (const t of active) {
|
|
86
|
+
lines.push(`- ${t.id}: ${t.title} (${t.status}, ${t.priority} priority)`);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
lines.push("(none)");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!customInstructions) {
|
|
93
|
+
lines.push("");
|
|
94
|
+
lines.push("Instructions:");
|
|
95
|
+
lines.push("1. Pick the highest priority task");
|
|
96
|
+
lines.push("2. Work on it");
|
|
97
|
+
lines.push('3. Use /task done <taskId> "what you did" when complete');
|
|
98
|
+
lines.push("4. Report a brief summary");
|
|
99
|
+
lines.push("");
|
|
100
|
+
lines.push("If no tasks are assigned, report that you have no pending work.");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
lines.push("[/HEARTBEAT]");
|
|
104
|
+
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Save / Load heartbeat history ───────────────────────────────────
|
|
109
|
+
|
|
110
|
+
export function saveHeartbeatResult(agentHome: string, result: HeartbeatResult): void {
|
|
111
|
+
const dir = join(agentHome, "heartbeats");
|
|
112
|
+
mkdirSync(dir, { recursive: true });
|
|
113
|
+
const logFile = join(dir, `log-${todayKey()}.jsonl`);
|
|
114
|
+
try {
|
|
115
|
+
appendFileSync(logFile, JSON.stringify(result) + "\n");
|
|
116
|
+
} catch { /* ignore */ }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function loadHeartbeatHistory(agentHome: string, limit: number = 20): HeartbeatResult[] {
|
|
120
|
+
const dir = join(agentHome, "heartbeats");
|
|
121
|
+
if (!existsSync(dir)) return [];
|
|
122
|
+
|
|
123
|
+
const files = readdirSync(dir)
|
|
124
|
+
.filter(f => f.startsWith("log-") && f.endsWith(".jsonl"))
|
|
125
|
+
.sort()
|
|
126
|
+
.reverse();
|
|
127
|
+
|
|
128
|
+
const results: HeartbeatResult[] = [];
|
|
129
|
+
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
if (results.length >= limit) break;
|
|
132
|
+
try {
|
|
133
|
+
const content = readFileSync(join(dir, file), "utf-8");
|
|
134
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
try {
|
|
137
|
+
results.push(JSON.parse(line) as HeartbeatResult);
|
|
138
|
+
} catch { /* skip malformed lines */ }
|
|
139
|
+
}
|
|
140
|
+
} catch { /* skip unreadable files */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Sort by triggeredAt descending
|
|
144
|
+
results.sort((a, b) => b.triggeredAt.localeCompare(a.triggeredAt));
|
|
145
|
+
|
|
146
|
+
return results.slice(0, limit);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── Execute a heartbeat ─────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
export async function executeHeartbeat(
|
|
152
|
+
agentId: string,
|
|
153
|
+
agent: AppConfig["agents"][string],
|
|
154
|
+
baseDir: string,
|
|
155
|
+
mcpRegistry?: Record<string, McpServerConfig>,
|
|
156
|
+
claudeAccounts?: Record<string, string>,
|
|
157
|
+
globalDefaults?: { skills?: string[]; mcps?: string[]; prompts?: string[]; promptTrigger?: string },
|
|
158
|
+
triggeredBy: string = "manual",
|
|
159
|
+
): Promise<HeartbeatResult> {
|
|
160
|
+
const agentHome = agent.agentHome || resolve(baseDir, agent.memoryDir, "..");
|
|
161
|
+
|
|
162
|
+
// Build heartbeat prompt
|
|
163
|
+
const prompt = buildHeartbeatPrompt(agentHome, agentId);
|
|
164
|
+
|
|
165
|
+
// Build synthetic message
|
|
166
|
+
const syntheticMsg: InboundMessage = {
|
|
167
|
+
id: `heartbeat-${agentId}-${Date.now()}`,
|
|
168
|
+
channel: "heartbeat",
|
|
169
|
+
chatId: `heartbeat-${agentId}`,
|
|
170
|
+
chatType: "group",
|
|
171
|
+
sender: "heartbeat-runner",
|
|
172
|
+
senderName: "Heartbeat",
|
|
173
|
+
text: prompt,
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
isFromMe: false,
|
|
176
|
+
isGroup: true,
|
|
177
|
+
raw: { type: "heartbeat", agentId },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const route: ResolvedRoute = {
|
|
181
|
+
agentId,
|
|
182
|
+
agentConfig: agent,
|
|
183
|
+
route: agent.routes[0],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const startTime = Date.now();
|
|
187
|
+
const triggeredAt = new Date(startTime).toISOString();
|
|
188
|
+
let status: HeartbeatResult["status"] = "success";
|
|
189
|
+
let summary = "";
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const response = await executeAgent(route, syntheticMsg, baseDir, mcpRegistry, claudeAccounts, globalDefaults);
|
|
193
|
+
summary = response;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
status = "error";
|
|
196
|
+
summary = String(err);
|
|
197
|
+
log.error(`Heartbeat execution failed for ${agentId}: ${err}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const endTime = Date.now();
|
|
201
|
+
const completedAt = new Date(endTime).toISOString();
|
|
202
|
+
const durationMs = endTime - startTime;
|
|
203
|
+
|
|
204
|
+
const result: HeartbeatResult = {
|
|
205
|
+
id: `hb-${agentId}-${startTime}`,
|
|
206
|
+
agentId,
|
|
207
|
+
triggeredBy,
|
|
208
|
+
triggeredAt,
|
|
209
|
+
completedAt,
|
|
210
|
+
durationMs,
|
|
211
|
+
summary,
|
|
212
|
+
status,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Persist
|
|
216
|
+
saveHeartbeatResult(agentHome, result);
|
|
217
|
+
|
|
218
|
+
log.info(`Heartbeat completed: ${agentId} (${durationMs}ms, ${status})`);
|
|
219
|
+
return result;
|
|
220
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { resolve, dirname } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { loadConfig } from "./config.js";
|
|
4
|
+
import { configureLogger, log } from "./logger.js";
|
|
5
|
+
import { resolveRoute, isPairingAttempt, pairSender } from "./router.js";
|
|
6
|
+
import { executeAgent, executeAgentStreaming, setAppConfig } from "./executor.js";
|
|
7
|
+
import { SlackDriver } from "./channels/slack.js";
|
|
8
|
+
import { WhatsAppDriver } from "./channels/whatsapp.js";
|
|
9
|
+
import { TelegramDriver } from "./channels/telegram.js";
|
|
10
|
+
import { DiscordDriver } from "./channels/discord.js";
|
|
11
|
+
import { startWebUI } from "./web-ui.js";
|
|
12
|
+
import { startCronJobs, stopCronJobs } from "./cron.js";
|
|
13
|
+
import { startGoals, stopGoals } from "./goals.js";
|
|
14
|
+
import { startWikiSync, stopWikiSync } from "./wiki-sync.js";
|
|
15
|
+
import type { ChannelDriver, InboundMessage } from "./channels/types.js";
|
|
16
|
+
|
|
17
|
+
const isMac = process.platform === "darwin";
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const baseDir = resolve(__dirname, "..");
|
|
21
|
+
|
|
22
|
+
async function main(): Promise<void> {
|
|
23
|
+
const configPath = resolve(baseDir, "config.json");
|
|
24
|
+
const config = loadConfig(configPath);
|
|
25
|
+
setAppConfig(config);
|
|
26
|
+
|
|
27
|
+
configureLogger(config.service.logLevel, config.service.logFile);
|
|
28
|
+
|
|
29
|
+
log.info("channelToAgentToClaude starting...");
|
|
30
|
+
|
|
31
|
+
const drivers: ChannelDriver[] = [];
|
|
32
|
+
const driverMap = new Map<string, ChannelDriver>();
|
|
33
|
+
|
|
34
|
+
// Initialize enabled channel drivers
|
|
35
|
+
for (const [channelId, channelCfg] of Object.entries(config.channels)) {
|
|
36
|
+
if (!channelCfg.enabled) {
|
|
37
|
+
log.debug(`Channel "${channelId}" disabled, skipping`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let driver: ChannelDriver;
|
|
42
|
+
|
|
43
|
+
switch (channelCfg.driver) {
|
|
44
|
+
case "imessage":
|
|
45
|
+
if (!isMac) {
|
|
46
|
+
log.warn("iMessage channel is macOS-only and has been disabled on this platform. Use Telegram, Slack, or Discord instead.");
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// Dynamic import — imsg CLI only exists on macOS
|
|
50
|
+
const { IMessageDriver } = await import("./channels/imessage.js");
|
|
51
|
+
driver = new IMessageDriver(channelCfg.config);
|
|
52
|
+
break;
|
|
53
|
+
case "slack":
|
|
54
|
+
driver = new SlackDriver(channelCfg.config);
|
|
55
|
+
break;
|
|
56
|
+
case "whatsapp":
|
|
57
|
+
driver = new WhatsAppDriver(channelCfg.config);
|
|
58
|
+
break;
|
|
59
|
+
case "telegram":
|
|
60
|
+
driver = new TelegramDriver(channelCfg.config);
|
|
61
|
+
break;
|
|
62
|
+
case "discord":
|
|
63
|
+
driver = new DiscordDriver(channelCfg.config);
|
|
64
|
+
break;
|
|
65
|
+
default:
|
|
66
|
+
log.warn(`Unknown channel driver "${channelCfg.driver}" for "${channelId}", skipping`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Track recent bot-sent messages to prevent echo loops
|
|
71
|
+
const recentBotMessages = new Set<string>();
|
|
72
|
+
|
|
73
|
+
// Wire up message handling
|
|
74
|
+
driver.onMessage(async (msg: InboundMessage) => {
|
|
75
|
+
// Anti-echo: ignore bot's own messages and echo loops
|
|
76
|
+
if (msg.isFromMe) return;
|
|
77
|
+
if (msg.text === "On it..." || msg.text?.startsWith("Paired successfully") || msg.text?.startsWith("Still working...")) return;
|
|
78
|
+
const msgKey = `${msg.chatId}:${msg.text?.slice(0, 50)}`;
|
|
79
|
+
if (recentBotMessages.has(msgKey)) {
|
|
80
|
+
recentBotMessages.delete(msgKey);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Feature 4: DM pairing gate
|
|
85
|
+
if (isPairingAttempt(msg, config, baseDir)) {
|
|
86
|
+
pairSender(msg, baseDir);
|
|
87
|
+
try {
|
|
88
|
+
await driver.send({
|
|
89
|
+
text: "Paired successfully. You can now message my agents.",
|
|
90
|
+
chatId: msg.chatId,
|
|
91
|
+
});
|
|
92
|
+
} catch { /* ignore */ }
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Route to agent
|
|
97
|
+
const match = resolveRoute(msg, config, baseDir);
|
|
98
|
+
if (!match) return;
|
|
99
|
+
|
|
100
|
+
log.info(`${match.agentId} <- ${msg.sender}: ${msg.text.slice(0, 80)}`);
|
|
101
|
+
|
|
102
|
+
// Feature 2: Typing indicator
|
|
103
|
+
if (driver.sendTyping) {
|
|
104
|
+
driver.sendTyping(msg.chatId).catch(() => {});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Send thinking indicator
|
|
108
|
+
if (driver.sendTyping) {
|
|
109
|
+
driver.sendTyping(msg.chatId).catch(() => {});
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
recentBotMessages.add(`${msg.chatId}:On it...`);
|
|
113
|
+
await driver.send({ text: "On it...", chatId: msg.chatId });
|
|
114
|
+
} catch (err) {
|
|
115
|
+
log.warn(`Failed to send thinking indicator: ${err}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Execute agent — streaming or regular
|
|
119
|
+
// Heartbeat: send "Still working..." every 4 minutes to keep channel alive
|
|
120
|
+
const HEARTBEAT_MS = 4 * 60 * 1000;
|
|
121
|
+
let heartbeatCount = 0;
|
|
122
|
+
const heartbeat = setInterval(() => {
|
|
123
|
+
heartbeatCount++;
|
|
124
|
+
const elapsed = heartbeatCount * 4;
|
|
125
|
+
const heartbeatMsg = `Still working... (${elapsed} min)`;
|
|
126
|
+
recentBotMessages.add(`${msg.chatId}:${heartbeatMsg}`);
|
|
127
|
+
driver.send({ text: heartbeatMsg, chatId: msg.chatId }).catch((err) => {
|
|
128
|
+
log.warn(`Heartbeat send failed: ${err}`);
|
|
129
|
+
});
|
|
130
|
+
}, HEARTBEAT_MS);
|
|
131
|
+
|
|
132
|
+
let response: string;
|
|
133
|
+
try {
|
|
134
|
+
if (match.agentConfig.streaming) {
|
|
135
|
+
// Streaming mode: send status updates to phone channel
|
|
136
|
+
let lastStatus = "";
|
|
137
|
+
let fullText = "";
|
|
138
|
+
for await (const event of executeAgentStreaming(match, msg, baseDir, config.mcps, config.service.claudeAccounts, undefined, { skills: config.defaultSkills, mcps: config.defaultMcps, prompts: config.defaultPrompts, promptTrigger: config.promptTrigger })) {
|
|
139
|
+
if (event.type === "status" && event.data !== lastStatus) {
|
|
140
|
+
lastStatus = event.data;
|
|
141
|
+
// Send status updates (throttle — only unique ones)
|
|
142
|
+
if (driver.sendTyping) {
|
|
143
|
+
driver.sendTyping(msg.chatId).catch(() => {});
|
|
144
|
+
}
|
|
145
|
+
} else if (event.type === "text") {
|
|
146
|
+
fullText += event.data;
|
|
147
|
+
} else if (event.type === "done") {
|
|
148
|
+
response = event.data || fullText;
|
|
149
|
+
} else if (event.type === "error") {
|
|
150
|
+
response = `Error: ${event.data}`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
response = response! || fullText || "No response from agent.";
|
|
154
|
+
} else {
|
|
155
|
+
response = await executeAgent(match, msg, baseDir, config.mcps, config.service.claudeAccounts, { skills: config.defaultSkills, mcps: config.defaultMcps, prompts: config.defaultPrompts, promptTrigger: config.promptTrigger });
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
clearInterval(heartbeat);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Reply via originating channel (retry once on failure)
|
|
162
|
+
recentBotMessages.add(`${msg.chatId}:${response.slice(0, 50)}`);
|
|
163
|
+
try {
|
|
164
|
+
await driver.send({ text: response, chatId: msg.chatId });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
log.warn(`Send failed, retrying in 2s: ${err}`);
|
|
167
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
168
|
+
try {
|
|
169
|
+
await driver.send({ text: response, chatId: msg.chatId });
|
|
170
|
+
} catch (retryErr) {
|
|
171
|
+
log.error(`Send retry failed: ${retryErr}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
log.info(`${match.agentId} -> ${msg.chatId}: ${response.slice(0, 80)}`);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
drivers.push(driver);
|
|
179
|
+
driverMap.set(channelId, driver);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── Feature 6 + 9: Web UI + Webhooks (start early so it's accessible even with no channels) ───
|
|
183
|
+
const webUI = config.service.webUI;
|
|
184
|
+
let cronMessageHandler: (agentId: string, message: string, channel: string, chatId: string) => Promise<void>;
|
|
185
|
+
|
|
186
|
+
if (webUI?.enabled) {
|
|
187
|
+
// cronMessageHandler is defined below — bind via closure so webUI can reference it
|
|
188
|
+
startWebUI({
|
|
189
|
+
config,
|
|
190
|
+
baseDir,
|
|
191
|
+
port: webUI.port || 8080,
|
|
192
|
+
webhookSecret: webUI.webhookSecret,
|
|
193
|
+
driverMap,
|
|
194
|
+
onWebhookMessage: async (agentId, text, channel, chatId) => {
|
|
195
|
+
if (cronMessageHandler) await cronMessageHandler(agentId, text, channel, chatId);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (drivers.length === 0) {
|
|
201
|
+
log.warn("No channel drivers enabled — running in web-UI-only mode. Configure a channel to enable messaging.");
|
|
202
|
+
} else {
|
|
203
|
+
// Start all drivers
|
|
204
|
+
for (const driver of drivers) {
|
|
205
|
+
await driver.start();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ─── Feature 7: Cron jobs ──────────────────────────────────────────
|
|
210
|
+
cronMessageHandler = async (agentId: string, message: string, channel: string, chatId: string) => {
|
|
211
|
+
const agent = config.agents[agentId];
|
|
212
|
+
if (!agent) return;
|
|
213
|
+
|
|
214
|
+
// Build a synthetic inbound message for the executor
|
|
215
|
+
const syntheticMsg: InboundMessage = {
|
|
216
|
+
id: `cron-${Date.now()}`,
|
|
217
|
+
channel,
|
|
218
|
+
chatId,
|
|
219
|
+
chatType: "group",
|
|
220
|
+
sender: "cron",
|
|
221
|
+
senderName: "Scheduled Task",
|
|
222
|
+
text: message,
|
|
223
|
+
timestamp: Date.now(),
|
|
224
|
+
isFromMe: false,
|
|
225
|
+
isGroup: true,
|
|
226
|
+
raw: { type: "cron" },
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const route = { agentId, agentConfig: agent, route: agent.routes[0] };
|
|
230
|
+
const response = await executeAgent(route, syntheticMsg, baseDir, config.mcps, config.service.claudeAccounts, { skills: config.defaultSkills, mcps: config.defaultMcps, prompts: config.defaultPrompts, promptTrigger: config.promptTrigger });
|
|
231
|
+
|
|
232
|
+
// Send response to the configured channel
|
|
233
|
+
const driver = driverMap.get(channel);
|
|
234
|
+
if (driver) {
|
|
235
|
+
try {
|
|
236
|
+
await driver.send({ text: response, chatId });
|
|
237
|
+
} catch (err) {
|
|
238
|
+
log.error(`Cron response send failed for ${agentId}: ${err}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
startCronJobs(config, cronMessageHandler);
|
|
244
|
+
|
|
245
|
+
// ─── Feature 8: Autonomous Goals ──────────────────────────────────
|
|
246
|
+
startGoals(config, driverMap, baseDir, config.mcps);
|
|
247
|
+
|
|
248
|
+
// ─── Feature: Wiki Sync ──────────────────────────────────────────
|
|
249
|
+
startWikiSync(config, baseDir, config.mcps);
|
|
250
|
+
|
|
251
|
+
const agentCount = Object.keys(config.agents).length;
|
|
252
|
+
log.info(
|
|
253
|
+
`channelToAgentToClaude running — ${agentCount} agent(s), ${drivers.length} channel(s)`
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Graceful shutdown
|
|
257
|
+
const shutdown = async (signal: string) => {
|
|
258
|
+
log.info(`Received ${signal}, shutting down...`);
|
|
259
|
+
stopCronJobs();
|
|
260
|
+
stopGoals();
|
|
261
|
+
stopWikiSync();
|
|
262
|
+
for (const driver of drivers) {
|
|
263
|
+
await driver.stop();
|
|
264
|
+
}
|
|
265
|
+
process.exit(0);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
269
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
main().catch((err) => {
|
|
273
|
+
console.error("Fatal error:", err);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
});
|
package/src/keystore.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keystore — encrypt/decrypt MCP API key files.
|
|
3
|
+
* Uses AES-256-GCM with a master password.
|
|
4
|
+
* Encrypted files have .env.enc extension, plain files have .env extension.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { getPersonalAgentsDir } from "./config.js";
|
|
12
|
+
import { log } from "./logger.js";
|
|
13
|
+
|
|
14
|
+
const ALGORITHM = "aes-256-gcm";
|
|
15
|
+
const SALT_LEN = 16;
|
|
16
|
+
const IV_LEN = 12;
|
|
17
|
+
const TAG_LEN = 16;
|
|
18
|
+
const KEY_LEN = 32;
|
|
19
|
+
const HEADER = "MYAGENT_ENC_V1"; // magic header to identify encrypted files
|
|
20
|
+
|
|
21
|
+
function deriveKey(password: string, salt: Buffer): Buffer {
|
|
22
|
+
return scryptSync(password, salt, KEY_LEN);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function encryptString(plaintext: string, password: string): Buffer {
|
|
26
|
+
const salt = randomBytes(SALT_LEN);
|
|
27
|
+
const iv = randomBytes(IV_LEN);
|
|
28
|
+
const key = deriveKey(password, salt);
|
|
29
|
+
|
|
30
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
31
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf-8"), cipher.final()]);
|
|
32
|
+
const tag = cipher.getAuthTag();
|
|
33
|
+
|
|
34
|
+
// Format: HEADER + salt(16) + iv(12) + tag(16) + ciphertext
|
|
35
|
+
const headerBuf = Buffer.from(HEADER, "utf-8");
|
|
36
|
+
return Buffer.concat([headerBuf, salt, iv, tag, encrypted]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function decryptString(data: Buffer, password: string): string {
|
|
40
|
+
const headerBuf = Buffer.from(HEADER, "utf-8");
|
|
41
|
+
const headerLen = headerBuf.length;
|
|
42
|
+
|
|
43
|
+
// Verify header
|
|
44
|
+
if (data.subarray(0, headerLen).toString("utf-8") !== HEADER) {
|
|
45
|
+
throw new Error("Not an encrypted keystore file");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let offset = headerLen;
|
|
49
|
+
const salt = data.subarray(offset, offset + SALT_LEN); offset += SALT_LEN;
|
|
50
|
+
const iv = data.subarray(offset, offset + IV_LEN); offset += IV_LEN;
|
|
51
|
+
const tag = data.subarray(offset, offset + TAG_LEN); offset += TAG_LEN;
|
|
52
|
+
const ciphertext = data.subarray(offset);
|
|
53
|
+
|
|
54
|
+
const key = deriveKey(password, salt);
|
|
55
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
|
56
|
+
decipher.setAuthTag(tag);
|
|
57
|
+
|
|
58
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
59
|
+
return decrypted.toString("utf-8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isEncrypted(filePath: string): boolean {
|
|
63
|
+
if (!existsSync(filePath)) return false;
|
|
64
|
+
try {
|
|
65
|
+
const data = readFileSync(filePath);
|
|
66
|
+
return data.subarray(0, Buffer.from(HEADER).length).toString("utf-8") === HEADER;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Encrypt all .env files in a directory → .env.enc
|
|
74
|
+
*/
|
|
75
|
+
export function encryptDir(dir: string, password: string): number {
|
|
76
|
+
if (!existsSync(dir)) return 0;
|
|
77
|
+
let count = 0;
|
|
78
|
+
for (const file of readdirSync(dir)) {
|
|
79
|
+
if (!file.endsWith(".env")) continue;
|
|
80
|
+
const plainPath = join(dir, file);
|
|
81
|
+
const encPath = plainPath + ".enc";
|
|
82
|
+
try {
|
|
83
|
+
const content = readFileSync(plainPath, "utf-8");
|
|
84
|
+
if (!content.trim()) continue; // skip empty files
|
|
85
|
+
const encrypted = encryptString(content, password);
|
|
86
|
+
writeFileSync(encPath, encrypted);
|
|
87
|
+
// Remove the plain file after encryption
|
|
88
|
+
writeFileSync(plainPath, `# Encrypted — see ${file}.enc\n`);
|
|
89
|
+
count++;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
log.warn(`Failed to encrypt ${file}: ${err}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return count;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decrypt all .env.enc files in a directory → .env
|
|
99
|
+
*/
|
|
100
|
+
export function decryptDir(dir: string, password: string): number {
|
|
101
|
+
if (!existsSync(dir)) return 0;
|
|
102
|
+
let count = 0;
|
|
103
|
+
for (const file of readdirSync(dir)) {
|
|
104
|
+
if (!file.endsWith(".env.enc")) continue;
|
|
105
|
+
const encPath = join(dir, file);
|
|
106
|
+
const plainPath = encPath.replace(".enc", "");
|
|
107
|
+
try {
|
|
108
|
+
const data = readFileSync(encPath);
|
|
109
|
+
const content = decryptString(data, password);
|
|
110
|
+
writeFileSync(plainPath, content);
|
|
111
|
+
count++;
|
|
112
|
+
} catch (err) {
|
|
113
|
+
log.warn(`Failed to decrypt ${file}: ${err}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return count;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Load env vars from a .env file, auto-decrypting if .env.enc exists.
|
|
121
|
+
* Priority: agent-level > shared
|
|
122
|
+
*/
|
|
123
|
+
export function loadMcpKeysWithDecryption(
|
|
124
|
+
gatewayDir: string,
|
|
125
|
+
agentMemoryDir: string | null,
|
|
126
|
+
mcpName: string,
|
|
127
|
+
masterPassword?: string,
|
|
128
|
+
): Record<string, string> {
|
|
129
|
+
const vars: Record<string, string> = {};
|
|
130
|
+
const personalAgentsBase = getPersonalAgentsDir();
|
|
131
|
+
|
|
132
|
+
// Level 3: Gateway data/mcp-keys/ (last resort)
|
|
133
|
+
const gatewayVars = loadEnvFile(join(gatewayDir, `${mcpName}.env`), join(gatewayDir, `${mcpName}.env.enc`), masterPassword);
|
|
134
|
+
Object.assign(vars, gatewayVars);
|
|
135
|
+
|
|
136
|
+
// Level 2: Shared personalAgents/mcp-keys/ (overrides gateway)
|
|
137
|
+
const sharedKeysDir = join(personalAgentsBase, "mcp-keys");
|
|
138
|
+
const sharedVars = loadEnvFile(join(sharedKeysDir, `${mcpName}.env`), join(sharedKeysDir, `${mcpName}.env.enc`), masterPassword);
|
|
139
|
+
Object.assign(vars, sharedVars);
|
|
140
|
+
|
|
141
|
+
// Level 1: Agent-specific agent/mcp-keys/ (highest priority)
|
|
142
|
+
if (agentMemoryDir) {
|
|
143
|
+
const agentKeysDir = join(agentMemoryDir, "..", "mcp-keys");
|
|
144
|
+
const agentVars = loadEnvFile(join(agentKeysDir, `${mcpName}.env`), join(agentKeysDir, `${mcpName}.env.enc`), masterPassword);
|
|
145
|
+
Object.assign(vars, agentVars);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return vars;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function loadEnvFile(plainPath: string, encPath: string, password?: string): Record<string, string> {
|
|
152
|
+
const vars: Record<string, string> = {};
|
|
153
|
+
|
|
154
|
+
// Try encrypted file first
|
|
155
|
+
if (password && existsSync(encPath)) {
|
|
156
|
+
try {
|
|
157
|
+
const data = readFileSync(encPath);
|
|
158
|
+
if (isEncrypted(encPath)) {
|
|
159
|
+
const content = decryptString(data, password);
|
|
160
|
+
parseEnvContent(content, vars);
|
|
161
|
+
return vars;
|
|
162
|
+
}
|
|
163
|
+
} catch { /* fall through to plain */ }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Try plain file
|
|
167
|
+
if (existsSync(plainPath)) {
|
|
168
|
+
try {
|
|
169
|
+
const content = readFileSync(plainPath, "utf-8");
|
|
170
|
+
// Skip stub files left after encryption
|
|
171
|
+
if (!content.includes("# Encrypted")) {
|
|
172
|
+
parseEnvContent(content, vars);
|
|
173
|
+
}
|
|
174
|
+
} catch { /* ignore */ }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return vars;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseEnvContent(content: string, vars: Record<string, string>): void {
|
|
181
|
+
for (const line of content.split("\n")) {
|
|
182
|
+
const trimmed = line.trim();
|
|
183
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
184
|
+
const eqIdx = trimmed.indexOf("=");
|
|
185
|
+
if (eqIdx < 1) continue;
|
|
186
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
187
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
188
|
+
if (val) vars[key] = val;
|
|
189
|
+
}
|
|
190
|
+
}
|