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
package/src/router.ts ADDED
@@ -0,0 +1,230 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { InboundMessage } from "./channels/types.js";
4
+ import type { AgentConfig, RouteConfig, AppConfig } from "./config.js";
5
+ import { log } from "./logger.js";
6
+
7
+ export interface ResolvedRoute {
8
+ agentId: string;
9
+ agentConfig: AgentConfig;
10
+ route: RouteConfig;
11
+ }
12
+
13
+ // ─── Pairing store ───────────────────────────────────────────────────
14
+
15
+ const pairedSenders = new Set<string>();
16
+ let pairingStoreLoaded = false;
17
+ let pairingStorePath = "";
18
+
19
+ function loadPairingStore(baseDir: string): void {
20
+ if (pairingStoreLoaded) return;
21
+ pairingStorePath = join(baseDir, "data", "paired-senders.json");
22
+ try {
23
+ if (existsSync(pairingStorePath)) {
24
+ const data = JSON.parse(readFileSync(pairingStorePath, "utf-8")) as string[];
25
+ for (const s of data) pairedSenders.add(s);
26
+ }
27
+ } catch { /* fresh start */ }
28
+ pairingStoreLoaded = true;
29
+ }
30
+
31
+ function savePairingStore(): void {
32
+ try {
33
+ mkdirSync(join(pairingStorePath, ".."), { recursive: true });
34
+ writeFileSync(pairingStorePath, JSON.stringify([...pairedSenders], null, 2));
35
+ } catch (err) {
36
+ log.warn(`Failed to save pairing store: ${err}`);
37
+ }
38
+ }
39
+
40
+ // ─── Sticky routing ──────────────────────────────────────────────────
41
+
42
+ interface StickyEntry {
43
+ agentId: string;
44
+ timestamp: number;
45
+ }
46
+
47
+ // Key: "channel:chatId:sender" → last agent they talked to
48
+ const stickyMap = new Map<string, StickyEntry>();
49
+
50
+ const DEFAULT_STICKY_TIMEOUT_MS = 300_000; // 5 minutes
51
+
52
+ // Sticky routing modes:
53
+ // "none" — always require @mention (default)
54
+ // "sticky" — mention once, then all messages route for stickyTimeoutMs
55
+ // "prefix" — like sticky, but follow-up messages must start with a trigger character (e.g., ! or @)
56
+ type StickyMode = "none" | "sticky" | "prefix";
57
+
58
+ function getStickyMode(config: AppConfig, channel: string): { mode: StickyMode; prefix: string; timeoutMs: number } {
59
+ const channelCfg = config.channels[channel];
60
+ if (!channelCfg) return { mode: "none", prefix: "!", timeoutMs: DEFAULT_STICKY_TIMEOUT_MS };
61
+
62
+ const raw = (channelCfg.config as any).stickyRouting;
63
+ let mode: StickyMode = "prefix"; // default: prefix mode
64
+ if (raw === "sticky") mode = "sticky";
65
+ else if (raw === "none") mode = "none";
66
+ else if (raw === "prefix") mode = "prefix";
67
+
68
+ const prefix = (channelCfg.config as any).stickyPrefix ?? "!";
69
+ const timeoutMs = (channelCfg.config as any).stickyTimeoutMs ?? DEFAULT_STICKY_TIMEOUT_MS;
70
+ return { mode, prefix, timeoutMs };
71
+ }
72
+
73
+ function getStickyAgent(
74
+ msg: InboundMessage,
75
+ config: AppConfig,
76
+ ): ResolvedRoute | null {
77
+ const { mode, prefix, timeoutMs } = getStickyMode(config, msg.channel);
78
+ if (mode === "none") return null;
79
+
80
+ const key = `${msg.channel}:${msg.chatId}:${msg.sender}`;
81
+ const entry = stickyMap.get(key);
82
+
83
+ if (!entry) return null;
84
+
85
+ // Check if expired
86
+ if (Date.now() - entry.timestamp > timeoutMs) {
87
+ stickyMap.delete(key);
88
+ return null;
89
+ }
90
+
91
+ // Prefix mode: message must start with the trigger character
92
+ if (mode === "prefix") {
93
+ const trimmed = msg.text.trim();
94
+ if (!trimmed.startsWith(prefix)) {
95
+ return null; // No prefix — don't route via sticky
96
+ }
97
+ // Strip the prefix from the message text for the agent
98
+ msg.text = trimmed.slice(prefix.length).trim();
99
+ }
100
+
101
+ // Verify the agent still exists
102
+ const agent = config.agents[entry.agentId];
103
+ if (!agent) {
104
+ stickyMap.delete(key);
105
+ return null;
106
+ }
107
+
108
+ // Find the matching route for this channel + chatId
109
+ const route = agent.routes.find(
110
+ r => r.channel === msg.channel && String(r.match.value) === msg.chatId
111
+ );
112
+ if (!route) return null;
113
+
114
+ // Permission check
115
+ if (!isAllowed(msg, route)) return null;
116
+
117
+ log.debug(`Sticky routing (${mode}): ${msg.sender} → ${entry.agentId} (${Math.round((Date.now() - entry.timestamp) / 1000)}s ago)`);
118
+ return { agentId: entry.agentId, agentConfig: agent, route };
119
+ }
120
+
121
+ function setStickyAgent(msg: InboundMessage, agentId: string): void {
122
+ const key = `${msg.channel}:${msg.chatId}:${msg.sender}`;
123
+ stickyMap.set(key, { agentId, timestamp: Date.now() });
124
+ }
125
+
126
+ // ─── Route resolver ──────────────────────────────────────────────────
127
+
128
+ export function resolveRoute(
129
+ msg: InboundMessage,
130
+ config: AppConfig,
131
+ baseDir?: string,
132
+ ): ResolvedRoute | null {
133
+ // Feature 4: DM pairing gate
134
+ if (config.service.pairingCode && baseDir) {
135
+ loadPairingStore(baseDir);
136
+
137
+ const senderKey = `${msg.channel}:${msg.sender}`;
138
+ if (!pairedSenders.has(senderKey)) {
139
+ if (msg.text.trim() === config.service.pairingCode) {
140
+ pairedSenders.add(senderKey);
141
+ savePairingStore();
142
+ log.info(`Paired sender: ${senderKey}`);
143
+ return null;
144
+ }
145
+ log.debug(`Unpaired sender ${senderKey} — ignoring`);
146
+ return null;
147
+ }
148
+ }
149
+
150
+ // Try explicit mention routing first
151
+ for (const [agentId, agent] of Object.entries(config.agents)) {
152
+ for (const route of agent.routes) {
153
+ if (route.channel !== msg.channel) continue;
154
+
155
+ const matchValue = String(route.match.value);
156
+ if (msg.chatId !== matchValue) continue;
157
+
158
+ if (!isAllowed(msg, route)) {
159
+ log.debug(`Blocked: ${msg.sender} not in allowFrom for ${agentId}`);
160
+ return null;
161
+ }
162
+
163
+ if (route.permissions.requireMention) {
164
+ if (!hasMention(msg.text, agent)) {
165
+ continue;
166
+ }
167
+ }
168
+
169
+ // Explicit mention found — update sticky and return
170
+ setStickyAgent(msg, agentId);
171
+ return { agentId, agentConfig: agent, route };
172
+ }
173
+ }
174
+
175
+ // No explicit mention — try sticky routing
176
+ const sticky = getStickyAgent(msg, config);
177
+ if (sticky) {
178
+ // Refresh the timestamp on each sticky hit
179
+ setStickyAgent(msg, sticky.agentId);
180
+ return sticky;
181
+ }
182
+
183
+ // Fall back to default agent — only for the web channel
184
+ if (msg.channel === "web" && config.defaultAgent && config.agents[config.defaultAgent]) {
185
+ const agent = config.agents[config.defaultAgent];
186
+ const matchingRoute = agent.routes.find(r => r.channel === msg.channel);
187
+ if (matchingRoute) {
188
+ return {
189
+ agentId: config.defaultAgent,
190
+ agentConfig: agent,
191
+ route: matchingRoute,
192
+ };
193
+ }
194
+ }
195
+
196
+ log.debug(`No route for ${msg.channel}:${msg.chatId} from ${msg.senderName || msg.sender}`);
197
+ return null;
198
+ }
199
+
200
+ // Exported for pairing check in index.ts
201
+ export function isPairingAttempt(msg: InboundMessage, config: AppConfig, baseDir: string): boolean {
202
+ if (!config.service.pairingCode) return false;
203
+ loadPairingStore(baseDir);
204
+ const senderKey = `${msg.channel}:${msg.sender}`;
205
+ if (pairedSenders.has(senderKey)) return false;
206
+ return msg.text.trim() === config.service.pairingCode;
207
+ }
208
+
209
+ export function pairSender(msg: InboundMessage, baseDir: string): void {
210
+ loadPairingStore(baseDir);
211
+ const senderKey = `${msg.channel}:${msg.sender}`;
212
+ pairedSenders.add(senderKey);
213
+ savePairingStore();
214
+ log.info(`Paired sender: ${senderKey}`);
215
+ }
216
+
217
+ function hasMention(text: string, agent: AgentConfig): boolean {
218
+ const lower = text.toLowerCase();
219
+ if (agent.mentionAliases?.length) {
220
+ return agent.mentionAliases.some((alias) => lower.includes(alias.toLowerCase()));
221
+ }
222
+ return lower.includes(agent.name.toLowerCase());
223
+ }
224
+
225
+ function isAllowed(msg: InboundMessage, route: RouteConfig): boolean {
226
+ const { allowFrom } = route.permissions;
227
+ if (!allowFrom || allowFrom.length === 0) return true;
228
+ if (allowFrom.includes("*")) return true;
229
+ return allowFrom.includes(msg.sender);
230
+ }
@@ -0,0 +1,72 @@
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 { executeAgent } from "./executor.js";
6
+ import type { InboundMessage } from "./channels/types.js";
7
+ import type { ResolvedRoute } from "./router.js";
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const baseDir = resolve(__dirname, "..");
11
+
12
+ async function main(): Promise<void> {
13
+ const args = process.argv.slice(2);
14
+
15
+ let agentId: string | null = null;
16
+ let text: string | null = null;
17
+
18
+ for (let i = 0; i < args.length; i++) {
19
+ if (args[i] === "--agent" && args[i + 1]) {
20
+ agentId = args[++i];
21
+ } else if (args[i] === "--text" && args[i + 1]) {
22
+ text = args[++i];
23
+ }
24
+ }
25
+
26
+ if (!agentId || !text) {
27
+ console.log("Usage: npm run test-message -- --agent <agentId> --text <message>");
28
+ console.log('Example: npm run test-message -- --agent fic-show --text "List all episodes"');
29
+ process.exit(1);
30
+ }
31
+
32
+ const configPath = resolve(baseDir, "config.json");
33
+ const config = loadConfig(configPath);
34
+
35
+ configureLogger("debug");
36
+
37
+ const agentConfig = config.agents[agentId];
38
+ if (!agentConfig) {
39
+ console.error(`Agent "${agentId}" not found. Available: ${Object.keys(config.agents).join(", ")}`);
40
+ process.exit(1);
41
+ }
42
+
43
+ const msg: InboundMessage = {
44
+ id: "test-" + Date.now(),
45
+ channel: "test",
46
+ chatId: "test",
47
+ chatType: "dm",
48
+ sender: "test-user",
49
+ text,
50
+ timestamp: Date.now(),
51
+ isFromMe: false,
52
+ isGroup: false,
53
+ raw: {},
54
+ };
55
+
56
+ const route: ResolvedRoute = {
57
+ agentId,
58
+ agentConfig,
59
+ route: agentConfig.routes[0],
60
+ };
61
+
62
+ log.info(`Testing agent "${agentId}" with message: "${text}"`);
63
+ const response = await executeAgent(route, msg, baseDir);
64
+ console.log("\n--- Agent Response ---");
65
+ console.log(response);
66
+ console.log("--- End ---\n");
67
+ }
68
+
69
+ main().catch((err) => {
70
+ console.error("Fatal error:", err);
71
+ process.exit(1);
72
+ });
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Direct iMessage database reader.
3
+ * Reads from ~/Library/Messages/chat.db to get message text from attributedBody
4
+ * when the text column is empty (macOS 15+ behavior).
5
+ */
6
+
7
+ import { execSync } from "node:child_process";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+
11
+ const DB_PATH = join(homedir(), "Library", "Messages", "chat.db");
12
+
13
+ interface DBMessage {
14
+ rowid: number;
15
+ text: string;
16
+ chatId: number;
17
+ isFromMe: boolean;
18
+ sender: string;
19
+ createdAt: string;
20
+ guid: string;
21
+ }
22
+
23
+ /**
24
+ * Extract plain text from NSAttributedString binary blob (NSArchiver/typedstream format).
25
+ * The text is stored after a \x01\x2B marker followed by a length byte, then raw UTF-8 bytes.
26
+ */
27
+ function extractTextFromAttributedBody(hexData: string): string {
28
+ try {
29
+ const buf = Buffer.from(hexData, "hex");
30
+
31
+ // Search for the \x01\x2B (NSArchiver string marker "+") pattern
32
+ for (let i = 0; i < buf.length - 2; i++) {
33
+ if (buf[i] === 0x01 && buf[i + 1] === 0x2b) {
34
+ const len = buf[i + 2];
35
+ if (len > 0 && i + 3 + len <= buf.length) {
36
+ const text = buf.toString("utf-8", i + 3, i + 3 + len);
37
+ // Sanity check: should contain at least some printable chars
38
+ if (/[\x20-\x7E]/.test(text)) {
39
+ return text.trim();
40
+ }
41
+ }
42
+ }
43
+ }
44
+ return "";
45
+ } catch {
46
+ return "";
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Get recent messages from a specific chat, with attributedBody fallback.
52
+ */
53
+ export function getRecentMessages(chatId: number, sinceRowId: number, limit: number = 20): DBMessage[] {
54
+ try {
55
+ const query = `
56
+ SELECT m.ROWID, m.text, m.is_from_me, m.handle_id,
57
+ datetime(m.date/1000000000 + 978307200, 'unixepoch', 'localtime') as created_at,
58
+ m.guid, hex(m.attributedBody) as attr_hex,
59
+ h.id as sender_id
60
+ FROM message m
61
+ JOIN chat_message_join cmj ON m.ROWID = cmj.message_id
62
+ LEFT JOIN handle h ON m.handle_id = h.ROWID
63
+ WHERE cmj.chat_id = ${chatId}
64
+ AND m.ROWID > ${sinceRowId}
65
+ AND m.associated_message_type = 0
66
+ ORDER BY m.date ASC
67
+ LIMIT ${limit};
68
+ `;
69
+
70
+ const result = execSync(`sqlite3 -json "${DB_PATH}" "${query.replace(/"/g, '\\"')}"`, {
71
+ timeout: 5000,
72
+ encoding: "utf-8",
73
+ });
74
+
75
+ const rows = JSON.parse(result || "[]");
76
+ return rows.map((row: any) => {
77
+ let text = row.text || "";
78
+ if (!text && row.attr_hex) {
79
+ text = extractTextFromAttributedBody(row.attr_hex);
80
+ }
81
+ return {
82
+ rowid: row.ROWID,
83
+ text,
84
+ chatId,
85
+ isFromMe: row.is_from_me === 1,
86
+ sender: row.sender_id || (row.is_from_me === 1 ? "me" : "unknown"),
87
+ createdAt: row.created_at,
88
+ guid: row.guid,
89
+ };
90
+ }).filter((m: DBMessage) => m.text.trim().length > 0);
91
+ } catch {
92
+ return [];
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get the latest ROWID for a chat (for polling).
98
+ */
99
+ export function getLatestRowId(chatId: number): number {
100
+ try {
101
+ const result = execSync(
102
+ `sqlite3 "${DB_PATH}" "SELECT MAX(m.ROWID) FROM message m JOIN chat_message_join cmj ON m.ROWID = cmj.message_id WHERE cmj.chat_id = ${chatId};"`,
103
+ { timeout: 5000, encoding: "utf-8" }
104
+ );
105
+ return parseInt(result.trim()) || 0;
106
+ } catch {
107
+ return 0;
108
+ }
109
+ }
@@ -0,0 +1,178 @@
1
+ import { spawn, type ChildProcess } from "node:child_process";
2
+ import { createInterface, type Interface } from "node:readline";
3
+ import { log } from "../logger.js";
4
+
5
+ interface PendingRequest {
6
+ resolve: (value: unknown) => void;
7
+ reject: (reason: Error) => void;
8
+ timer: ReturnType<typeof setTimeout>;
9
+ }
10
+
11
+ interface JsonRpcNotification {
12
+ jsonrpc: "2.0";
13
+ method: string;
14
+ params: unknown;
15
+ }
16
+
17
+ export class ImsgRpcClient {
18
+ private proc: ChildProcess | null = null;
19
+ private rl: Interface | null = null;
20
+ private nextId = 1;
21
+ private pending = new Map<number, PendingRequest>();
22
+ private notificationHandler: ((notification: JsonRpcNotification) => void) | null = null;
23
+ private cliPath: string;
24
+ private stopping = false;
25
+ private restartTimer: ReturnType<typeof setTimeout> | null = null;
26
+
27
+ constructor(cliPath = "imsg") {
28
+ this.cliPath = cliPath;
29
+ }
30
+
31
+ async start(): Promise<void> {
32
+ // Verify imsg exists
33
+ try {
34
+ const probe = spawn(this.cliPath, ["rpc", "--help"], { stdio: "pipe" });
35
+ await new Promise<void>((resolve, reject) => {
36
+ probe.on("close", () => resolve());
37
+ probe.on("error", (err) => reject(err));
38
+ });
39
+ } catch {
40
+ const hint = process.platform === "darwin"
41
+ ? `Install with: brew install steipete/tap/imsg`
42
+ : `iMessage is only available on macOS`;
43
+ throw new Error(
44
+ `"${this.cliPath}" not found. ${hint}`
45
+ );
46
+ }
47
+
48
+ await this.startProcess();
49
+ log.info("imsg RPC client started");
50
+ }
51
+
52
+ private handleLine(line: string): void {
53
+ if (!line.trim()) return;
54
+
55
+ let parsed: Record<string, unknown>;
56
+ try {
57
+ parsed = JSON.parse(line);
58
+ } catch {
59
+ log.warn(`imsg: unparseable line: ${line.slice(0, 200)}`);
60
+ return;
61
+ }
62
+
63
+ // Response to a request (has "id")
64
+ if ("id" in parsed && typeof parsed.id === "number") {
65
+ const req = this.pending.get(parsed.id);
66
+ if (req) {
67
+ clearTimeout(req.timer);
68
+ this.pending.delete(parsed.id);
69
+
70
+ if ("error" in parsed) {
71
+ req.reject(new Error(JSON.stringify(parsed.error)));
72
+ } else {
73
+ req.resolve(parsed.result);
74
+ }
75
+ }
76
+ return;
77
+ }
78
+
79
+ // Notification (has "method" but no "id")
80
+ if ("method" in parsed && !("id" in parsed)) {
81
+ this.notificationHandler?.(parsed as unknown as JsonRpcNotification);
82
+ return;
83
+ }
84
+
85
+ log.debug(`imsg: unhandled message: ${line.slice(0, 200)}`);
86
+ }
87
+
88
+ async request<T>(method: string, params?: object, timeoutMs = 10_000): Promise<T> {
89
+ if (!this.proc?.stdin?.writable) {
90
+ throw new Error("imsg RPC client not started");
91
+ }
92
+
93
+ const id = this.nextId++;
94
+ const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params });
95
+
96
+ return new Promise<T>((resolve, reject) => {
97
+ const timer = setTimeout(() => {
98
+ this.pending.delete(id);
99
+ reject(new Error(`imsg RPC timeout: ${method} (${timeoutMs}ms)`));
100
+ }, timeoutMs);
101
+
102
+ this.pending.set(id, {
103
+ resolve: resolve as (value: unknown) => void,
104
+ reject,
105
+ timer,
106
+ });
107
+
108
+ this.proc!.stdin!.write(payload + "\n");
109
+ });
110
+ }
111
+
112
+ onNotification(handler: (notification: JsonRpcNotification) => void): void {
113
+ this.notificationHandler = handler;
114
+ }
115
+
116
+ private async restart(): Promise<void> {
117
+ try {
118
+ this.rl?.close();
119
+ this.proc = null;
120
+ await this.startProcess();
121
+ // Re-subscribe to watch
122
+ await this.request("watch.subscribe", { attachments: false });
123
+ log.info("imsg RPC client restarted successfully");
124
+ } catch (err) {
125
+ log.error(`imsg restart failed: ${err} — retrying in 10s`);
126
+ this.restartTimer = setTimeout(() => this.restart(), 10_000);
127
+ }
128
+ }
129
+
130
+ private async startProcess(): Promise<void> {
131
+ this.proc = spawn(this.cliPath, ["rpc"], {
132
+ stdio: ["pipe", "pipe", "pipe"],
133
+ });
134
+
135
+ this.proc.on("error", (err) => {
136
+ log.error(`imsg process error: ${err.message}`);
137
+ });
138
+
139
+ this.proc.on("close", (code) => {
140
+ log.warn(`imsg process exited with code ${code}`);
141
+ for (const [id, req] of this.pending) {
142
+ clearTimeout(req.timer);
143
+ req.reject(new Error("imsg process exited"));
144
+ this.pending.delete(id);
145
+ }
146
+ if (!this.stopping) {
147
+ log.info("imsg process died — restarting in 3s...");
148
+ this.restartTimer = setTimeout(() => this.restart(), 3000);
149
+ }
150
+ });
151
+
152
+ if (this.proc.stderr) {
153
+ const stderrRl = createInterface({ input: this.proc.stderr });
154
+ stderrRl.on("line", (line) => {
155
+ if (line.trim()) log.warn(`imsg stderr: ${line}`);
156
+ });
157
+ }
158
+
159
+ if (this.proc.stdout) {
160
+ this.rl = createInterface({ input: this.proc.stdout });
161
+ this.rl.on("line", (line) => this.handleLine(line));
162
+ }
163
+ }
164
+
165
+ async stop(): Promise<void> {
166
+ this.stopping = true;
167
+ if (this.restartTimer) clearTimeout(this.restartTimer);
168
+ for (const [id, req] of this.pending) {
169
+ clearTimeout(req.timer);
170
+ req.reject(new Error("Client stopping"));
171
+ this.pending.delete(id);
172
+ }
173
+ this.rl?.close();
174
+ this.proc?.kill("SIGTERM");
175
+ this.proc = null;
176
+ log.info("imsg RPC client stopped");
177
+ }
178
+ }
@@ -0,0 +1,90 @@
1
+ import { readFileSync } from "node:fs";
2
+ import type { InboundMessage } from "../channels/types.js";
3
+
4
+ interface ConversationEntry {
5
+ ts: string;
6
+ from: string;
7
+ text: string;
8
+ response: string;
9
+ }
10
+
11
+ export function formatMessage(
12
+ msg: InboundMessage,
13
+ memoryContextPath?: string,
14
+ conversationLogPath?: string,
15
+ historyLimit = 5,
16
+ ): string {
17
+ const parts: string[] = [];
18
+
19
+ // 1. Memory context
20
+ if (memoryContextPath) {
21
+ try {
22
+ const context = readFileSync(memoryContextPath, "utf-8").trim();
23
+ if (context) {
24
+ parts.push(`[Agent Memory]\n${context}\n[/Agent Memory]`);
25
+ }
26
+ } catch {
27
+ // no memory file yet, that's fine
28
+ }
29
+ }
30
+
31
+ // 2. Conversation history
32
+ if (conversationLogPath) {
33
+ try {
34
+ const raw = readFileSync(conversationLogPath, "utf-8").trim();
35
+ if (raw) {
36
+ const lines = raw.split("\n").filter(Boolean);
37
+ const recent = lines.slice(-historyLimit);
38
+ const entries: string[] = [];
39
+
40
+ for (const line of recent) {
41
+ try {
42
+ const entry = JSON.parse(line) as ConversationEntry;
43
+ entries.push(` ${entry.from}: ${entry.text}`);
44
+ entries.push(` Agent: ${entry.response}`);
45
+ } catch {
46
+ // skip malformed lines
47
+ }
48
+ }
49
+
50
+ if (entries.length > 0) {
51
+ parts.push(
52
+ `[Conversation History - last ${recent.length} exchanges]\n${entries.join("\n")}\n[/Conversation History]`
53
+ );
54
+ }
55
+ }
56
+ } catch {
57
+ // no log file yet
58
+ }
59
+ }
60
+
61
+ // 3. The message itself
62
+ const ts = new Date(msg.timestamp).toISOString();
63
+ const sender = msg.senderName || msg.sender;
64
+
65
+ let header: string;
66
+ if (msg.isGroup && msg.groupName) {
67
+ header = `[${msg.channel} group '${msg.groupName}' from ${sender} at ${ts}]`;
68
+ } else if (msg.isGroup) {
69
+ header = `[${msg.channel} group from ${sender} at ${ts}]`;
70
+ } else {
71
+ header = `[${msg.channel} DM from ${sender} at ${ts}]`;
72
+ }
73
+
74
+ let body = msg.text;
75
+
76
+ // Reply context
77
+ if (msg.replyTo) {
78
+ const replySender = msg.replyTo.sender || "unknown";
79
+ body += `\n\n[Replying to ${replySender}]\n${msg.replyTo.text}\n[/Replying]`;
80
+ }
81
+
82
+ // Attachment note
83
+ if (msg.attachments?.length) {
84
+ body += `\n\n[${msg.attachments.length} image(s) attached — visible in this message. Save them to the appropriate episode folder and reference in the episode JSON.]`;
85
+ }
86
+
87
+ parts.push(`${header}\n${body}\n[/${msg.channel}]`);
88
+
89
+ return parts.join("\n\n");
90
+ }