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/logger.ts ADDED
@@ -0,0 +1,51 @@
1
+ import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ export type LogLevel = "debug" | "info" | "warn" | "error";
5
+
6
+ const LEVELS: Record<LogLevel, number> = {
7
+ debug: 0,
8
+ info: 1,
9
+ warn: 2,
10
+ error: 3,
11
+ };
12
+
13
+ let currentLevel: LogLevel = "info";
14
+ let logFilePath: string | null = null;
15
+
16
+ export function configureLogger(level: LogLevel, filePath?: string): void {
17
+ currentLevel = level;
18
+ if (filePath) {
19
+ logFilePath = filePath;
20
+ mkdirSync(dirname(filePath), { recursive: true });
21
+ }
22
+ }
23
+
24
+ function write(level: LogLevel, msg: string, ...args: unknown[]): void {
25
+ if (LEVELS[level] < LEVELS[currentLevel]) return;
26
+
27
+ const ts = new Date().toISOString();
28
+ const prefix = `[${ts}] [${level.toUpperCase()}]`;
29
+ const line = args.length > 0 ? `${prefix} ${msg} ${args.map(a => JSON.stringify(a)).join(" ")}` : `${prefix} ${msg}`;
30
+
31
+ if (level === "error") {
32
+ console.error(line);
33
+ } else {
34
+ console.log(line);
35
+ }
36
+
37
+ if (logFilePath) {
38
+ try {
39
+ appendFileSync(logFilePath, line + "\n");
40
+ } catch {
41
+ // silently ignore file write errors
42
+ }
43
+ }
44
+ }
45
+
46
+ export const log = {
47
+ debug: (msg: string, ...args: unknown[]) => write("debug", msg, ...args),
48
+ info: (msg: string, ...args: unknown[]) => write("info", msg, ...args),
49
+ warn: (msg: string, ...args: unknown[]) => write("warn", msg, ...args),
50
+ error: (msg: string, ...args: unknown[]) => write("error", msg, ...args),
51
+ };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Daily memory — append-only daily log files.
3
+ * After each exchange, a summary is appended to memory/daily/YYYY-MM-DD.md.
4
+ * At session start, today + yesterday are loaded for continuity.
5
+ */
6
+
7
+ import { readFileSync, appendFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
8
+ import { join } from "node:path";
9
+
10
+ function dateStr(date: Date): string {
11
+ return date.toISOString().split("T")[0];
12
+ }
13
+
14
+ function timeStr(date: Date): string {
15
+ return date.toTimeString().split(" ")[0].slice(0, 5);
16
+ }
17
+
18
+ export function getDailyDir(memoryDir: string): string {
19
+ const dir = join(memoryDir, "daily");
20
+ mkdirSync(dir, { recursive: true });
21
+ return dir;
22
+ }
23
+
24
+ /**
25
+ * Append an exchange summary to today's daily log.
26
+ */
27
+ export function appendDailyEntry(
28
+ memoryDir: string,
29
+ sender: string,
30
+ userMessage: string,
31
+ agentResponse: string,
32
+ ): void {
33
+ const dir = getDailyDir(memoryDir);
34
+ const today = dateStr(new Date());
35
+ const time = timeStr(new Date());
36
+ const filePath = join(dir, `${today}.md`);
37
+
38
+ // Truncate long messages for the daily log
39
+ const userSnippet = userMessage.slice(0, 200).replace(/\n/g, " ");
40
+ const agentSnippet = agentResponse.slice(0, 300).replace(/\n/g, " ");
41
+
42
+ const entry = `\n### ${time}\n- **User:** ${userSnippet}\n- **Agent:** ${agentSnippet}\n`;
43
+
44
+ // Add header if new file
45
+ if (!existsSync(filePath)) {
46
+ appendFileSync(filePath, `# Daily Log — ${today}\n`);
47
+ }
48
+
49
+ appendFileSync(filePath, entry);
50
+ }
51
+
52
+ /**
53
+ * Load today's and yesterday's daily logs for context injection.
54
+ */
55
+ export function loadRecentDaily(memoryDir: string): string {
56
+ const dir = getDailyDir(memoryDir);
57
+
58
+ const today = new Date();
59
+ const yesterday = new Date(today);
60
+ yesterday.setDate(yesterday.getDate() - 1);
61
+
62
+ const files = [
63
+ { date: dateStr(yesterday), label: "Yesterday" },
64
+ { date: dateStr(today), label: "Today" },
65
+ ];
66
+
67
+ const parts: string[] = [];
68
+
69
+ for (const { date, label } of files) {
70
+ const filePath = join(dir, `${date}.md`);
71
+ if (existsSync(filePath)) {
72
+ try {
73
+ const content = readFileSync(filePath, "utf-8").trim();
74
+ if (content) {
75
+ parts.push(`[${label}'s Activity — ${date}]\n${content}\n[/${label}]`);
76
+ }
77
+ } catch { /* ignore */ }
78
+ }
79
+ }
80
+
81
+ return parts.join("\n\n");
82
+ }
83
+
84
+ /**
85
+ * Get all daily file paths for indexing.
86
+ */
87
+ export function listDailyFiles(memoryDir: string): Array<{ path: string; date: string }> {
88
+ const dir = getDailyDir(memoryDir);
89
+
90
+ try {
91
+ return readdirSync(dir)
92
+ .filter((f: string) => f.endsWith(".md"))
93
+ .map((f: string) => ({
94
+ path: join(dir, f),
95
+ date: f.replace(".md", ""),
96
+ }))
97
+ .sort((a: any, b: any) => a.date.localeCompare(b.date));
98
+ } catch {
99
+ return [];
100
+ }
101
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Embedding providers — auto-selects best available:
3
+ * 1. OpenAI text-embedding-3-small (if OPENAI_API_KEY set)
4
+ * 2. TF-IDF local (zero dependencies, always works)
5
+ */
6
+
7
+ import { log } from "../logger.js";
8
+
9
+ export interface EmbeddingProvider {
10
+ name: string;
11
+ dimensions: number;
12
+ embed(text: string): Promise<number[]>;
13
+ embedBatch(texts: string[]): Promise<number[][]>;
14
+ }
15
+
16
+ // ─── TF-IDF Local Provider (zero deps) ──────────────────────────────
17
+
18
+ const STOP_WORDS = new Set([
19
+ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being",
20
+ "have", "has", "had", "do", "does", "did", "will", "would", "could",
21
+ "should", "may", "might", "shall", "can", "need", "dare", "ought",
22
+ "used", "to", "of", "in", "for", "on", "with", "at", "by", "from",
23
+ "as", "into", "through", "during", "before", "after", "above", "below",
24
+ "between", "out", "off", "over", "under", "again", "further", "then",
25
+ "once", "here", "there", "when", "where", "why", "how", "all", "each",
26
+ "every", "both", "few", "more", "most", "other", "some", "such", "no",
27
+ "nor", "not", "only", "own", "same", "so", "than", "too", "very",
28
+ "just", "because", "but", "and", "or", "if", "while", "that", "this",
29
+ "it", "its", "i", "me", "my", "we", "our", "you", "your", "he", "him",
30
+ "his", "she", "her", "they", "them", "their", "what", "which", "who",
31
+ ]);
32
+
33
+ function tokenize(text: string): string[] {
34
+ return text
35
+ .toLowerCase()
36
+ .replace(/[^a-z0-9\s]/g, " ")
37
+ .split(/\s+/)
38
+ .filter(t => t.length > 1 && !STOP_WORDS.has(t));
39
+ }
40
+
41
+ // Global vocabulary built from all indexed texts
42
+ const globalVocab = new Map<string, number>(); // word -> index
43
+ let vocabSize = 0;
44
+
45
+ export function buildVocab(texts: string[]): void {
46
+ for (const text of texts) {
47
+ for (const token of tokenize(text)) {
48
+ if (!globalVocab.has(token)) {
49
+ globalVocab.set(token, vocabSize++);
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ function tfidfVector(text: string): number[] {
56
+ const tokens = tokenize(text);
57
+ if (tokens.length === 0) return new Array(Math.max(vocabSize, 1)).fill(0);
58
+
59
+ // Term frequency
60
+ const tf = new Map<string, number>();
61
+ for (const t of tokens) {
62
+ tf.set(t, (tf.get(t) || 0) + 1);
63
+ }
64
+
65
+ // Build sparse vector
66
+ const vec = new Array(Math.max(vocabSize, 1)).fill(0);
67
+ for (const [word, count] of tf) {
68
+ const idx = globalVocab.get(word);
69
+ if (idx !== undefined) {
70
+ vec[idx] = count / tokens.length; // normalized TF
71
+ }
72
+ }
73
+
74
+ // L2 normalize
75
+ const norm = Math.sqrt(vec.reduce((sum, v) => sum + v * v, 0));
76
+ if (norm > 0) {
77
+ for (let i = 0; i < vec.length; i++) vec[i] /= norm;
78
+ }
79
+
80
+ return vec;
81
+ }
82
+
83
+ class TFIDFProvider implements EmbeddingProvider {
84
+ name = "tfidf-local";
85
+ dimensions = 0; // dynamic, grows with vocab
86
+
87
+ async embed(text: string): Promise<number[]> {
88
+ buildVocab([text]);
89
+ this.dimensions = vocabSize;
90
+ return tfidfVector(text);
91
+ }
92
+
93
+ async embedBatch(texts: string[]): Promise<number[][]> {
94
+ buildVocab(texts);
95
+ this.dimensions = vocabSize;
96
+ return texts.map(t => tfidfVector(t));
97
+ }
98
+ }
99
+
100
+ // ─── OpenAI Provider ─────────────────────────────────────────────────
101
+
102
+ class OpenAIProvider implements EmbeddingProvider {
103
+ name = "openai-text-embedding-3-small";
104
+ dimensions = 1536;
105
+ private apiKey: string;
106
+
107
+ constructor(apiKey: string) {
108
+ this.apiKey = apiKey;
109
+ }
110
+
111
+ async embed(text: string): Promise<number[]> {
112
+ const results = await this.embedBatch([text]);
113
+ return results[0];
114
+ }
115
+
116
+ async embedBatch(texts: string[]): Promise<number[][]> {
117
+ try {
118
+ const resp = await fetch("https://api.openai.com/v1/embeddings", {
119
+ method: "POST",
120
+ headers: {
121
+ "Authorization": `Bearer ${this.apiKey}`,
122
+ "Content-Type": "application/json",
123
+ },
124
+ body: JSON.stringify({
125
+ model: "text-embedding-3-small",
126
+ input: texts,
127
+ }),
128
+ });
129
+
130
+ if (!resp.ok) {
131
+ log.warn(`OpenAI embeddings failed (${resp.status}), falling back to TF-IDF`);
132
+ const fallback = new TFIDFProvider();
133
+ return fallback.embedBatch(texts);
134
+ }
135
+
136
+ const data = await resp.json() as {
137
+ data: Array<{ embedding: number[] }>;
138
+ };
139
+
140
+ return data.data.map(d => d.embedding);
141
+ } catch (err) {
142
+ log.warn(`OpenAI embeddings error: ${err}, falling back to TF-IDF`);
143
+ const fallback = new TFIDFProvider();
144
+ return fallback.embedBatch(texts);
145
+ }
146
+ }
147
+ }
148
+
149
+ // ─── Provider selection ──────────────────────────────────────────────
150
+
151
+ let cachedProvider: EmbeddingProvider | null = null;
152
+
153
+ export function getEmbeddingProvider(): EmbeddingProvider {
154
+ if (cachedProvider) return cachedProvider;
155
+
156
+ const openaiKey = process.env.OPENAI_API_KEY;
157
+ if (openaiKey) {
158
+ cachedProvider = new OpenAIProvider(openaiKey);
159
+ log.info(`Memory embeddings: OpenAI (text-embedding-3-small)`);
160
+ } else {
161
+ cachedProvider = new TFIDFProvider();
162
+ log.info(`Memory embeddings: TF-IDF local (set OPENAI_API_KEY for better quality)`);
163
+ }
164
+
165
+ return cachedProvider;
166
+ }
167
+
168
+ // ─── Cosine similarity ───────────────────────────────────────────────
169
+
170
+ export function cosineSimilarity(a: number[], b: number[]): number {
171
+ // Handle mismatched dimensions (TF-IDF vocab can grow)
172
+ const len = Math.min(a.length, b.length);
173
+ let dot = 0, normA = 0, normB = 0;
174
+ for (let i = 0; i < len; i++) {
175
+ dot += a[i] * b[i];
176
+ normA += a[i] * a[i];
177
+ normB += b[i] * b[i];
178
+ }
179
+ // Include remaining dimensions in norms
180
+ for (let i = len; i < a.length; i++) normA += a[i] * a[i];
181
+ for (let i = len; i < b.length; i++) normB += b[i] * b[i];
182
+
183
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
184
+ return denom === 0 ? 0 : dot / denom;
185
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * MemoryManager — orchestrates advanced memory for agents.
3
+ * - Semantic search over stored memories
4
+ * - Daily memory logging
5
+ * - Auto-compaction (prompts agent to save before context grows too large)
6
+ * - Indexes context.md, daily logs, and conversation exchanges
7
+ */
8
+
9
+ import { randomUUID } from "node:crypto";
10
+ import { readFileSync, existsSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { getEmbeddingProvider, buildVocab, type EmbeddingProvider } from "./embeddings.js";
13
+ import { createStore, type VectorStore, type MemoryChunk } from "./store.js";
14
+ import { hybridSearch, formatSearchResults, type SearchResult } from "./search.js";
15
+ import { appendDailyEntry, loadRecentDaily, listDailyFiles } from "./daily.js";
16
+ import { log } from "../logger.js";
17
+
18
+ export interface MemoryManager {
19
+ /** Search memories relevant to a query */
20
+ search(query: string, topK?: number): Promise<SearchResult[]>;
21
+
22
+ /** Format search results for prompt injection */
23
+ searchFormatted(query: string, topK?: number): Promise<string>;
24
+
25
+ /** Index a new text chunk into memory */
26
+ index(text: string, source: string, metadata?: Record<string, string>): Promise<void>;
27
+
28
+ /** Index a conversation exchange */
29
+ indexExchange(userMessage: string, agentResponse: string, sender: string): Promise<void>;
30
+
31
+ /** Load recent daily context */
32
+ loadDailyContext(): string;
33
+
34
+ /** Re-index all memory sources (context.md + daily files) */
35
+ reindex(): Promise<void>;
36
+
37
+ /** Get auto-compaction prompt if needed */
38
+ getCompactionPrompt(messageCount: number): string | null;
39
+
40
+ /** Get memory stats */
41
+ stats(): Promise<{ chunks: number; store: string; embeddings: string }>;
42
+ }
43
+
44
+ // ─── Chunk text into ~400 token pieces ───────────────────────────────
45
+
46
+ function chunkText(text: string, maxChars: number = 1200): string[] {
47
+ const paragraphs = text.split(/\n\n+/);
48
+ const chunks: string[] = [];
49
+ let current = "";
50
+
51
+ for (const para of paragraphs) {
52
+ if (current.length + para.length > maxChars && current.length > 0) {
53
+ chunks.push(current.trim());
54
+ current = "";
55
+ }
56
+ current += para + "\n\n";
57
+ }
58
+
59
+ if (current.trim()) {
60
+ chunks.push(current.trim());
61
+ }
62
+
63
+ return chunks.length > 0 ? chunks : [text];
64
+ }
65
+
66
+ // ─── Auto-compaction thresholds ──────────────────────────────────────
67
+
68
+ const COMPACTION_WARN_MESSAGES = 20; // suggest compaction after 20 messages
69
+ const COMPACTION_FORCE_MESSAGES = 40; // strongly suggest after 40
70
+
71
+ // ─── Create MemoryManager ────────────────────────────────────────────
72
+
73
+ export async function createMemoryManager(memoryDir: string): Promise<MemoryManager> {
74
+ const embedder = getEmbeddingProvider();
75
+ const store = await createStore(memoryDir);
76
+ const contextPath = join(memoryDir, "context.md");
77
+
78
+ // Initial index if store is empty
79
+ const count = await store.count();
80
+ if (count === 0) {
81
+ log.info(`Memory: empty store, indexing existing files...`);
82
+ await indexExistingFiles(memoryDir, contextPath, embedder, store);
83
+ }
84
+
85
+ return {
86
+ async search(query: string, topK = 5): Promise<SearchResult[]> {
87
+ const queryVector = await embedder.embed(query);
88
+ const allChunks = await store.getAll();
89
+ return hybridSearch(queryVector, query, allChunks, { topK });
90
+ },
91
+
92
+ async searchFormatted(query: string, topK = 5): Promise<string> {
93
+ const queryVector = await embedder.embed(query);
94
+ const allChunks = await store.getAll();
95
+ const results = hybridSearch(queryVector, query, allChunks, { topK });
96
+ return formatSearchResults(results);
97
+ },
98
+
99
+ async index(text: string, source: string, metadata?: Record<string, string>): Promise<void> {
100
+ const chunks = chunkText(text);
101
+ const vectors = await embedder.embedBatch(chunks);
102
+
103
+ const memoryChunks: MemoryChunk[] = chunks.map((text, i) => ({
104
+ id: `${source}-${randomUUID().slice(0, 8)}`,
105
+ text,
106
+ vector: vectors[i],
107
+ source,
108
+ timestamp: new Date().toISOString(),
109
+ metadata,
110
+ }));
111
+
112
+ await store.addBatch(memoryChunks);
113
+ },
114
+
115
+ async indexExchange(userMessage: string, agentResponse: string, sender: string): Promise<void> {
116
+ // Log to daily file
117
+ appendDailyEntry(memoryDir, sender, userMessage, agentResponse);
118
+
119
+ // Index the exchange as a memory chunk
120
+ const summary = `User (${sender}): ${userMessage.slice(0, 200)}\nAgent: ${agentResponse.slice(0, 400)}`;
121
+ const vector = await embedder.embed(summary);
122
+
123
+ await store.add({
124
+ id: `conv-${Date.now()}`,
125
+ text: summary,
126
+ vector,
127
+ source: "conversation",
128
+ timestamp: new Date().toISOString(),
129
+ metadata: { sender },
130
+ });
131
+ },
132
+
133
+ loadDailyContext(): string {
134
+ return loadRecentDaily(memoryDir);
135
+ },
136
+
137
+ async reindex(): Promise<void> {
138
+ await store.clear();
139
+ await indexExistingFiles(memoryDir, contextPath, embedder, store);
140
+ log.info(`Memory: reindexed`);
141
+ },
142
+
143
+ getCompactionPrompt(messageCount: number): string | null {
144
+ if (messageCount >= COMPACTION_FORCE_MESSAGES) {
145
+ return `[SYSTEM: Your conversation has reached ${messageCount} messages. Before the context gets compressed, save any important decisions, facts, or context to ${contextPath} using the Write tool. Focus on: key decisions made, user preferences learned, project state, and anything you'd need to know if starting fresh. Merge with existing content — don't overwrite.]`;
146
+ }
147
+ if (messageCount >= COMPACTION_WARN_MESSAGES) {
148
+ return `[SYSTEM: You're at ${messageCount} messages. Consider using /opcompact to save important context soon, or I'll remind you again at ${COMPACTION_FORCE_MESSAGES} messages.]`;
149
+ }
150
+ return null;
151
+ },
152
+
153
+ async stats() {
154
+ return {
155
+ chunks: await store.count(),
156
+ store: store.name,
157
+ embeddings: embedder.name,
158
+ };
159
+ },
160
+ };
161
+ }
162
+
163
+ // ─── Index existing files into the store ─────────────────────────────
164
+
165
+ async function indexExistingFiles(
166
+ memoryDir: string,
167
+ contextPath: string,
168
+ embedder: EmbeddingProvider,
169
+ store: VectorStore,
170
+ ): Promise<void> {
171
+ const allTexts: Array<{ text: string; source: string; timestamp: string }> = [];
172
+
173
+ // Index context.md
174
+ if (existsSync(contextPath)) {
175
+ try {
176
+ const content = readFileSync(contextPath, "utf-8").trim();
177
+ if (content) {
178
+ const chunks = chunkText(content);
179
+ for (const chunk of chunks) {
180
+ allTexts.push({ text: chunk, source: "context", timestamp: new Date().toISOString() });
181
+ }
182
+ }
183
+ } catch { /* ignore */ }
184
+ }
185
+
186
+ // Index daily files
187
+ const dailyFiles = listDailyFiles(memoryDir);
188
+ for (const { path, date } of dailyFiles) {
189
+ try {
190
+ const content = readFileSync(path, "utf-8").trim();
191
+ if (content) {
192
+ const chunks = chunkText(content);
193
+ for (const chunk of chunks) {
194
+ allTexts.push({ text: chunk, source: "daily", timestamp: `${date}T00:00:00.000Z` });
195
+ }
196
+ }
197
+ } catch { /* ignore */ }
198
+ }
199
+
200
+ if (allTexts.length === 0) return;
201
+
202
+ // Build vocab for TF-IDF (if using local embeddings)
203
+ buildVocab(allTexts.map(t => t.text));
204
+
205
+ // Embed all at once
206
+ const vectors = await embedder.embedBatch(allTexts.map(t => t.text));
207
+
208
+ const memoryChunks: MemoryChunk[] = allTexts.map((t, i) => ({
209
+ id: `${t.source}-${randomUUID().slice(0, 8)}`,
210
+ text: t.text,
211
+ vector: vectors[i],
212
+ source: t.source,
213
+ timestamp: t.timestamp,
214
+ }));
215
+
216
+ await store.addBatch(memoryChunks);
217
+ log.info(`Memory: indexed ${memoryChunks.length} chunks from ${dailyFiles.length} daily files + context.md`);
218
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Hybrid semantic search: cosine similarity + BM25 keyword + temporal decay.
3
+ */
4
+
5
+ import { cosineSimilarity } from "./embeddings.js";
6
+ import type { MemoryChunk } from "./store.js";
7
+
8
+ export interface SearchResult {
9
+ chunk: MemoryChunk;
10
+ score: number; // combined score 0-1
11
+ vectorScore: number; // cosine similarity
12
+ keywordScore: number; // BM25-like score
13
+ decayFactor: number; // temporal decay multiplier
14
+ }
15
+
16
+ // ─── BM25 keyword scoring ────────────────────────────────────────────
17
+
18
+ const BM25_K1 = 1.2;
19
+ const BM25_B = 0.75;
20
+
21
+ function tokenize(text: string): string[] {
22
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter(t => t.length > 1);
23
+ }
24
+
25
+ function bm25Score(query: string, document: string, avgDocLen: number): number {
26
+ const queryTokens = tokenize(query);
27
+ const docTokens = tokenize(document);
28
+ if (queryTokens.length === 0 || docTokens.length === 0) return 0;
29
+
30
+ const docLen = docTokens.length;
31
+ const tf = new Map<string, number>();
32
+ for (const t of docTokens) {
33
+ tf.set(t, (tf.get(t) || 0) + 1);
34
+ }
35
+
36
+ let score = 0;
37
+ for (const qt of queryTokens) {
38
+ const freq = tf.get(qt) || 0;
39
+ if (freq === 0) continue;
40
+ const numerator = freq * (BM25_K1 + 1);
41
+ const denominator = freq + BM25_K1 * (1 - BM25_B + BM25_B * (docLen / avgDocLen));
42
+ score += numerator / denominator;
43
+ }
44
+
45
+ // Normalize to 0-1 range (approximate)
46
+ return Math.min(score / queryTokens.length, 1);
47
+ }
48
+
49
+ // ─── Temporal decay ──────────────────────────────────────────────────
50
+
51
+ const HALF_LIFE_DAYS = 30;
52
+ const DECAY_LAMBDA = Math.LN2 / HALF_LIFE_DAYS;
53
+
54
+ function temporalDecay(timestamp: string, source: string): number {
55
+ // context.md and manual entries never decay
56
+ if (source === "context" || source === "manual") return 1.0;
57
+
58
+ const ageMs = Date.now() - new Date(timestamp).getTime();
59
+ const ageDays = ageMs / (1000 * 60 * 60 * 24);
60
+ return Math.exp(-DECAY_LAMBDA * ageDays);
61
+ }
62
+
63
+ // ─── Hybrid search ───────────────────────────────────────────────────
64
+
65
+ export interface SearchOptions {
66
+ topK?: number; // max results (default 5)
67
+ vectorWeight?: number; // weight for vector score (default 0.6)
68
+ keywordWeight?: number; // weight for keyword score (default 0.3)
69
+ decayWeight?: number; // weight for temporal decay (default 0.1)
70
+ minScore?: number; // minimum combined score (default 0.1)
71
+ }
72
+
73
+ export function hybridSearch(
74
+ queryVector: number[],
75
+ queryText: string,
76
+ chunks: MemoryChunk[],
77
+ options: SearchOptions = {},
78
+ ): SearchResult[] {
79
+ const {
80
+ topK = 5,
81
+ vectorWeight = 0.6,
82
+ keywordWeight = 0.3,
83
+ decayWeight = 0.1,
84
+ minScore = 0.1,
85
+ } = options;
86
+
87
+ if (chunks.length === 0) return [];
88
+
89
+ // Average document length for BM25
90
+ const avgDocLen = chunks.reduce((sum, c) => sum + tokenize(c.text).length, 0) / chunks.length;
91
+
92
+ const results: SearchResult[] = chunks.map(chunk => {
93
+ const vectorScore = cosineSimilarity(queryVector, chunk.vector);
94
+ const keywordScore = bm25Score(queryText, chunk.text, avgDocLen);
95
+ const decayFactor = temporalDecay(chunk.timestamp, chunk.source);
96
+
97
+ const score =
98
+ vectorWeight * vectorScore +
99
+ keywordWeight * keywordScore +
100
+ decayWeight * decayFactor;
101
+
102
+ return { chunk, score, vectorScore, keywordScore, decayFactor };
103
+ });
104
+
105
+ return results
106
+ .filter(r => r.score >= minScore)
107
+ .sort((a, b) => b.score - a.score)
108
+ .slice(0, topK);
109
+ }
110
+
111
+ // ─── Format search results for prompt injection ──────────────────────
112
+
113
+ export function formatSearchResults(results: SearchResult[]): string {
114
+ if (results.length === 0) return "";
115
+
116
+ const lines = ["[Relevant Memories]"];
117
+ for (const r of results) {
118
+ const date = r.chunk.timestamp.split("T")[0];
119
+ const source = r.chunk.source;
120
+ lines.push(`- [${date}/${source}] ${r.chunk.text}`);
121
+ }
122
+ lines.push("[/Relevant Memories]");
123
+ return lines.join("\n");
124
+ }