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.
Files changed (315) hide show
  1. package/README.md +113 -0
  2. package/agents/_template/CLAUDE.md +18 -0
  3. package/agents/_template/agent.json +7 -0
  4. package/agents/platform/agentcreator/CLAUDE.md +300 -0
  5. package/agents/platform/appcreator/CLAUDE.md +158 -0
  6. package/agents/platform/gym/CLAUDE.md +486 -0
  7. package/agents/platform/gym/agent.json +40 -0
  8. package/agents/platform/gym/programs/agent-building/program.json +160 -0
  9. package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
  10. package/agents/platform/gym/programs/getting-started/program.json +124 -0
  11. package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
  12. package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
  13. package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
  14. package/agents/platform/gym/souls/alex.md +12 -0
  15. package/agents/platform/gym/souls/jordan.md +12 -0
  16. package/agents/platform/gym/souls/morgan.md +12 -0
  17. package/agents/platform/gym/souls/riley.md +12 -0
  18. package/agents/platform/gym/souls/sam.md +12 -0
  19. package/agents/platform/hub/CLAUDE.md +372 -0
  20. package/agents/platform/promptcreator/CLAUDE.md +130 -0
  21. package/agents/platform/skillcreator/CLAUDE.md +163 -0
  22. package/bin/cli.js +566 -0
  23. package/config.example.json +310 -0
  24. package/dist/agent-registry.d.ts +32 -0
  25. package/dist/agent-registry.d.ts.map +1 -0
  26. package/dist/agent-registry.js +144 -0
  27. package/dist/agent-registry.js.map +1 -0
  28. package/dist/channels/discord.d.ts +17 -0
  29. package/dist/channels/discord.d.ts.map +1 -0
  30. package/dist/channels/discord.js +114 -0
  31. package/dist/channels/discord.js.map +1 -0
  32. package/dist/channels/imessage.d.ts +23 -0
  33. package/dist/channels/imessage.d.ts.map +1 -0
  34. package/dist/channels/imessage.js +214 -0
  35. package/dist/channels/imessage.js.map +1 -0
  36. package/dist/channels/slack.d.ts +19 -0
  37. package/dist/channels/slack.d.ts.map +1 -0
  38. package/dist/channels/slack.js +167 -0
  39. package/dist/channels/slack.js.map +1 -0
  40. package/dist/channels/telegram.d.ts +19 -0
  41. package/dist/channels/telegram.d.ts.map +1 -0
  42. package/dist/channels/telegram.js +274 -0
  43. package/dist/channels/telegram.js.map +1 -0
  44. package/dist/channels/types.d.ts +44 -0
  45. package/dist/channels/types.d.ts.map +1 -0
  46. package/dist/channels/types.js +18 -0
  47. package/dist/channels/types.js.map +1 -0
  48. package/dist/channels/whatsapp.d.ts +23 -0
  49. package/dist/channels/whatsapp.d.ts.map +1 -0
  50. package/dist/channels/whatsapp.js +189 -0
  51. package/dist/channels/whatsapp.js.map +1 -0
  52. package/dist/config.d.ts +134 -0
  53. package/dist/config.d.ts.map +1 -0
  54. package/dist/config.js +127 -0
  55. package/dist/config.js.map +1 -0
  56. package/dist/cron.d.ts +8 -0
  57. package/dist/cron.d.ts.map +1 -0
  58. package/dist/cron.js +35 -0
  59. package/dist/cron.js.map +1 -0
  60. package/dist/decrypt-keys.d.ts +7 -0
  61. package/dist/decrypt-keys.d.ts.map +1 -0
  62. package/dist/decrypt-keys.js +53 -0
  63. package/dist/decrypt-keys.js.map +1 -0
  64. package/dist/encrypt-keys.d.ts +8 -0
  65. package/dist/encrypt-keys.d.ts.map +1 -0
  66. package/dist/encrypt-keys.js +62 -0
  67. package/dist/encrypt-keys.js.map +1 -0
  68. package/dist/executor.d.ts +31 -0
  69. package/dist/executor.d.ts.map +1 -0
  70. package/dist/executor.js +2009 -0
  71. package/dist/executor.js.map +1 -0
  72. package/dist/gemini-executor.d.ts +27 -0
  73. package/dist/gemini-executor.d.ts.map +1 -0
  74. package/dist/gemini-executor.js +160 -0
  75. package/dist/gemini-executor.js.map +1 -0
  76. package/dist/goals.d.ts +24 -0
  77. package/dist/goals.d.ts.map +1 -0
  78. package/dist/goals.js +189 -0
  79. package/dist/goals.js.map +1 -0
  80. package/dist/gym/activity-digest.d.ts +30 -0
  81. package/dist/gym/activity-digest.d.ts.map +1 -0
  82. package/dist/gym/activity-digest.js +506 -0
  83. package/dist/gym/activity-digest.js.map +1 -0
  84. package/dist/gym/dimension-scorer.d.ts +76 -0
  85. package/dist/gym/dimension-scorer.d.ts.map +1 -0
  86. package/dist/gym/dimension-scorer.js +236 -0
  87. package/dist/gym/dimension-scorer.js.map +1 -0
  88. package/dist/gym/gym-router.d.ts +7 -0
  89. package/dist/gym/gym-router.d.ts.map +1 -0
  90. package/dist/gym/gym-router.js +718 -0
  91. package/dist/gym/gym-router.js.map +1 -0
  92. package/dist/gym/index.d.ts +11 -0
  93. package/dist/gym/index.d.ts.map +1 -0
  94. package/dist/gym/index.js +11 -0
  95. package/dist/gym/index.js.map +1 -0
  96. package/dist/heartbeat.d.ts +21 -0
  97. package/dist/heartbeat.d.ts.map +1 -0
  98. package/dist/heartbeat.js +163 -0
  99. package/dist/heartbeat.js.map +1 -0
  100. package/dist/index.d.ts +2 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +254 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/keystore.d.ts +22 -0
  105. package/dist/keystore.d.ts.map +1 -0
  106. package/dist/keystore.js +178 -0
  107. package/dist/keystore.js.map +1 -0
  108. package/dist/logger.d.ts +9 -0
  109. package/dist/logger.d.ts.map +1 -0
  110. package/dist/logger.js +45 -0
  111. package/dist/logger.js.map +1 -0
  112. package/dist/memory/daily.d.ts +22 -0
  113. package/dist/memory/daily.d.ts.map +1 -0
  114. package/dist/memory/daily.js +82 -0
  115. package/dist/memory/daily.js.map +1 -0
  116. package/dist/memory/embeddings.d.ts +15 -0
  117. package/dist/memory/embeddings.d.ts.map +1 -0
  118. package/dist/memory/embeddings.js +154 -0
  119. package/dist/memory/embeddings.js.map +1 -0
  120. package/dist/memory/index.d.ts +32 -0
  121. package/dist/memory/index.d.ts.map +1 -0
  122. package/dist/memory/index.js +159 -0
  123. package/dist/memory/index.js.map +1 -0
  124. package/dist/memory/search.d.ts +21 -0
  125. package/dist/memory/search.d.ts.map +1 -0
  126. package/dist/memory/search.js +77 -0
  127. package/dist/memory/search.js.map +1 -0
  128. package/dist/memory/store.d.ts +23 -0
  129. package/dist/memory/store.d.ts.map +1 -0
  130. package/dist/memory/store.js +144 -0
  131. package/dist/memory/store.js.map +1 -0
  132. package/dist/ollama-executor.d.ts +17 -0
  133. package/dist/ollama-executor.d.ts.map +1 -0
  134. package/dist/ollama-executor.js +112 -0
  135. package/dist/ollama-executor.js.map +1 -0
  136. package/dist/openai-executor.d.ts +38 -0
  137. package/dist/openai-executor.d.ts.map +1 -0
  138. package/dist/openai-executor.js +197 -0
  139. package/dist/openai-executor.js.map +1 -0
  140. package/dist/router.d.ts +11 -0
  141. package/dist/router.d.ts.map +1 -0
  142. package/dist/router.js +185 -0
  143. package/dist/router.js.map +1 -0
  144. package/dist/test-message.d.ts +2 -0
  145. package/dist/test-message.d.ts.map +1 -0
  146. package/dist/test-message.js +60 -0
  147. package/dist/test-message.js.map +1 -0
  148. package/dist/utils/imsg-db-reader.d.ts +24 -0
  149. package/dist/utils/imsg-db-reader.d.ts.map +1 -0
  150. package/dist/utils/imsg-db-reader.js +92 -0
  151. package/dist/utils/imsg-db-reader.js.map +1 -0
  152. package/dist/utils/imsg-rpc.d.ts +25 -0
  153. package/dist/utils/imsg-rpc.d.ts.map +1 -0
  154. package/dist/utils/imsg-rpc.js +149 -0
  155. package/dist/utils/imsg-rpc.js.map +1 -0
  156. package/dist/utils/message-formatter.d.ts +3 -0
  157. package/dist/utils/message-formatter.d.ts.map +1 -0
  158. package/dist/utils/message-formatter.js +69 -0
  159. package/dist/utils/message-formatter.js.map +1 -0
  160. package/dist/web-ui.d.ts +12 -0
  161. package/dist/web-ui.d.ts.map +1 -0
  162. package/dist/web-ui.js +5784 -0
  163. package/dist/web-ui.js.map +1 -0
  164. package/dist/whatsapp-chats.d.ts +2 -0
  165. package/dist/whatsapp-chats.d.ts.map +1 -0
  166. package/dist/whatsapp-chats.js +76 -0
  167. package/dist/whatsapp-chats.js.map +1 -0
  168. package/dist/whatsapp-login.d.ts +2 -0
  169. package/dist/whatsapp-login.d.ts.map +1 -0
  170. package/dist/whatsapp-login.js +90 -0
  171. package/dist/whatsapp-login.js.map +1 -0
  172. package/dist/wiki-sync.d.ts +21 -0
  173. package/dist/wiki-sync.d.ts.map +1 -0
  174. package/dist/wiki-sync.js +147 -0
  175. package/dist/wiki-sync.js.map +1 -0
  176. package/docs/AddNewAgentGuide.md +100 -0
  177. package/docs/AddNewMcpGuide.md +72 -0
  178. package/docs/Architecture.md +795 -0
  179. package/docs/CLAUDE-AI-SETUP.md +166 -0
  180. package/docs/Setup.md +297 -0
  181. package/docs/ai-gym-architecture.md +1040 -0
  182. package/docs/ai-gym-build-plan.md +343 -0
  183. package/docs/ai-gym-onboarding.md +122 -0
  184. package/docs/appcreator_plan.md +348 -0
  185. package/docs/platform-mcp-audit.md +320 -0
  186. package/docs/server-deployment-plan.md +503 -0
  187. package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
  188. package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
  189. package/docs/user-guide.md +2016 -0
  190. package/mcp-catalog.json +628 -0
  191. package/package.json +63 -0
  192. package/public/MyAIforOne-logomark-512.svg +16 -0
  193. package/public/MyAIforOne-logomark-transparent.svg +15 -0
  194. package/public/activity.html +314 -0
  195. package/public/admin.html +1674 -0
  196. package/public/agent-dashboard.html +670 -0
  197. package/public/api-docs.html +1106 -0
  198. package/public/automations.html +722 -0
  199. package/public/canvas.css +223 -0
  200. package/public/canvas.js +588 -0
  201. package/public/changelog.html +231 -0
  202. package/public/gym.html +2766 -0
  203. package/public/home.html +1930 -0
  204. package/public/index.html +2809 -0
  205. package/public/lab.html +1643 -0
  206. package/public/library.html +1442 -0
  207. package/public/marketplace.html +1101 -0
  208. package/public/mcp-docs.html +441 -0
  209. package/public/mini.html +390 -0
  210. package/public/monitor.html +584 -0
  211. package/public/org.html +4304 -0
  212. package/public/projects.html +734 -0
  213. package/public/settings.html +645 -0
  214. package/public/tasks.html +932 -0
  215. package/public/trainers/alex.svg +12 -0
  216. package/public/trainers/jordan.svg +12 -0
  217. package/public/trainers/morgan.svg +12 -0
  218. package/public/trainers/riley.svg +12 -0
  219. package/public/trainers/sam.svg +12 -0
  220. package/public/user-guide.html +218 -0
  221. package/registry/agents.json +3 -0
  222. package/registry/apps.json +20 -0
  223. package/registry/installed-drafts.json +3 -0
  224. package/registry/mcps.json +1084 -0
  225. package/registry/prompts/personal/mcp-test-prompt.md +6 -0
  226. package/registry/prompts/personal/memory-recall.md +6 -0
  227. package/registry/prompts/platform/brainstorm.md +15 -0
  228. package/registry/prompts/platform/code-review.md +16 -0
  229. package/registry/prompts/platform/explain.md +16 -0
  230. package/registry/prompts.json +58 -0
  231. package/registry/skills/external/brainstorming.md +5 -0
  232. package/registry/skills/external/code-review.md +40 -0
  233. package/registry/skills/external/frontend-patterns.md +642 -0
  234. package/registry/skills/external/frontend-slides.md +184 -0
  235. package/registry/skills/external/systematic-debugging.md +5 -0
  236. package/registry/skills/external/tdd.md +328 -0
  237. package/registry/skills/external/verification-before-completion.md +5 -0
  238. package/registry/skills/external/writing-plans.md +5 -0
  239. package/registry/skills/platform/ai41_app_build.md +930 -0
  240. package/registry/skills/platform/ai41_app_deploy.md +168 -0
  241. package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
  242. package/registry/skills/platform/ai41_app_patterns.md +359 -0
  243. package/registry/skills/platform/ai41_app_register.md +85 -0
  244. package/registry/skills/platform/ai41_app_scaffold.md +421 -0
  245. package/registry/skills/platform/ai41_app_verify.md +107 -0
  246. package/registry/skills/platform/opProjectCreate.md +239 -0
  247. package/registry/skills/platform/op_devbrowser.md +136 -0
  248. package/registry/skills/platform/sop_brandguidelines.md +103 -0
  249. package/registry/skills/platform/sop_docx.md +117 -0
  250. package/registry/skills/platform/sop_frontenddesign.md +44 -0
  251. package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
  252. package/registry/skills/platform/sop_mcpbuilder.md +133 -0
  253. package/registry/skills/platform/sop_pdf.md +172 -0
  254. package/registry/skills/platform/sop_pptx.md +133 -0
  255. package/registry/skills/platform/sop_skillcreator.md +104 -0
  256. package/registry/skills/platform/sop_themefactory.md +128 -0
  257. package/registry/skills/platform/sop_webapptesting.md +75 -0
  258. package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
  259. package/registry/skills/platform/sop_xlsx.md +134 -0
  260. package/registry/skills.json +1055 -0
  261. package/scripts/discover-chats.sh +11 -0
  262. package/scripts/install-service-windows.ps1 +87 -0
  263. package/scripts/install-service.sh +52 -0
  264. package/scripts/seed-registry.ts +195 -0
  265. package/scripts/test-send.sh +5 -0
  266. package/scripts/tray-indicator.ps1 +35 -0
  267. package/scripts/uninstall-service-windows.ps1 +23 -0
  268. package/scripts/uninstall-service.sh +15 -0
  269. package/scripts/xbar-myagent.5s.sh +32 -0
  270. package/server/mcp-server/dist/index.d.ts +11 -0
  271. package/server/mcp-server/dist/index.js +1332 -0
  272. package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
  273. package/server/mcp-server/dist/lib/api-client.js +241 -0
  274. package/server/mcp-server/index.ts +1545 -0
  275. package/server/mcp-server/lib/api-client.ts +366 -0
  276. package/server/mcp-server/tsconfig.json +14 -0
  277. package/src/agent-registry.ts +180 -0
  278. package/src/channels/discord.ts +129 -0
  279. package/src/channels/imessage.ts +261 -0
  280. package/src/channels/slack.ts +208 -0
  281. package/src/channels/telegram.ts +307 -0
  282. package/src/channels/types.ts +62 -0
  283. package/src/channels/whatsapp.ts +227 -0
  284. package/src/config.ts +281 -0
  285. package/src/cron.ts +43 -0
  286. package/src/decrypt-keys.ts +60 -0
  287. package/src/encrypt-keys.ts +70 -0
  288. package/src/executor.ts +2190 -0
  289. package/src/gemini-executor.ts +212 -0
  290. package/src/goals.ts +240 -0
  291. package/src/gym/activity-digest.ts +546 -0
  292. package/src/gym/dimension-scorer.ts +297 -0
  293. package/src/gym/gym-router.ts +801 -0
  294. package/src/gym/index.ts +19 -0
  295. package/src/heartbeat.ts +220 -0
  296. package/src/index.ts +275 -0
  297. package/src/keystore.ts +190 -0
  298. package/src/logger.ts +51 -0
  299. package/src/memory/daily.ts +101 -0
  300. package/src/memory/embeddings.ts +185 -0
  301. package/src/memory/index.ts +218 -0
  302. package/src/memory/search.ts +124 -0
  303. package/src/memory/store.ts +189 -0
  304. package/src/ollama-executor.ts +126 -0
  305. package/src/openai-executor.ts +259 -0
  306. package/src/router.ts +230 -0
  307. package/src/test-message.ts +72 -0
  308. package/src/utils/imsg-db-reader.ts +109 -0
  309. package/src/utils/imsg-rpc.ts +178 -0
  310. package/src/utils/message-formatter.ts +90 -0
  311. package/src/web-ui.ts +5778 -0
  312. package/src/whatsapp-chats.ts +91 -0
  313. package/src/whatsapp-login.ts +110 -0
  314. package/src/wiki-sync.ts +199 -0
  315. package/tsconfig.json +19 -0
@@ -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";
@@ -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
+ });
@@ -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
+ }