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,801 @@
1
+ import { Router } from "express";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+
6
+ // ─── Gym Router ──────────────────────────────────────────────────────
7
+ // All AI Gym endpoints: learner profile, plan, progress, cards,
8
+ // dimension history, programs, agent activity summaries, and log search.
9
+
10
+ export function createGymRouter(baseDir: string, opts?: { memoryDir?: string; programsDir?: string; userProgramsDir?: string }): Router {
11
+ const router = Router();
12
+ const gymRepoDir = join(baseDir, "agents", "platform", "gym");
13
+ const memoryDir = opts?.memoryDir || join(gymRepoDir, "memory");
14
+ const programsDir = opts?.programsDir || join(gymRepoDir, "programs");
15
+ const userProgramsDir = opts?.userProgramsDir || join(memoryDir, "programs");
16
+
17
+ // ── Helpers ─────────────────────────────────────────────────────────
18
+
19
+ function ensureDir(dir: string): void {
20
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
21
+ }
22
+
23
+ function readJson(path: string, fallback: any = {}): any {
24
+ try {
25
+ if (!existsSync(path)) return fallback;
26
+ const raw = readFileSync(path, "utf-8").trim();
27
+ if (!raw) return fallback;
28
+ return JSON.parse(raw);
29
+ } catch {
30
+ return fallback;
31
+ }
32
+ }
33
+
34
+ function writeJson(path: string, data: any): void {
35
+ ensureDir(join(path, "..").replace(/\/\.\.$/, ""));
36
+ const dir = path.substring(0, path.lastIndexOf("/"));
37
+ ensureDir(dir);
38
+ writeFileSync(path, JSON.stringify(data, null, 2), "utf-8");
39
+ }
40
+
41
+ function readJsonl(path: string): any[] {
42
+ try {
43
+ if (!existsSync(path)) return [];
44
+ const raw = readFileSync(path, "utf-8").trim();
45
+ if (!raw) return [];
46
+ return raw.split("\n").filter(Boolean).map((line) => {
47
+ try { return JSON.parse(line); } catch { return null; }
48
+ }).filter(Boolean);
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ // ── 1. Learner Profile ─────────────────────────────────────────────
55
+
56
+ const profilePath = () => join(memoryDir, "learner-profile.json");
57
+
58
+ const emptyProfile = {
59
+ name: "",
60
+ goals: [],
61
+ dimensions: {},
62
+ preferences: {},
63
+ createdAt: new Date().toISOString(),
64
+ updatedAt: new Date().toISOString(),
65
+ };
66
+
67
+ router.get("/api/gym/learner-profile", (_req, res) => {
68
+ ensureDir(memoryDir);
69
+ const data = readJson(profilePath(), emptyProfile);
70
+ res.json(data);
71
+ });
72
+
73
+ router.put("/api/gym/learner-profile", (req, res) => {
74
+ ensureDir(memoryDir);
75
+ const existing = readJson(profilePath(), emptyProfile);
76
+ const merged = { ...existing, ...req.body, updatedAt: new Date().toISOString() };
77
+ writeJson(profilePath(), merged);
78
+ res.json(merged);
79
+ });
80
+
81
+ // ── 2. Plan ────────────────────────────────────────────────────────
82
+
83
+ const planPath = () => join(memoryDir, "plan.json");
84
+
85
+ router.get("/api/gym/plan", (_req, res) => {
86
+ ensureDir(memoryDir);
87
+ const data = readJson(planPath(), { modules: [], createdAt: null, updatedAt: null });
88
+ res.json(data);
89
+ });
90
+
91
+ router.put("/api/gym/plan", (req, res) => {
92
+ ensureDir(memoryDir);
93
+ const data = { ...req.body, updatedAt: new Date().toISOString() };
94
+ writeJson(planPath(), data);
95
+ res.json(data);
96
+ });
97
+
98
+ // ── 3. Progress ────────────────────────────────────────────────────
99
+
100
+ const progressPath = () => join(memoryDir, "program-progress.json");
101
+
102
+ router.get("/api/gym/progress", (_req, res) => {
103
+ ensureDir(memoryDir);
104
+ const data = readJson(progressPath(), { programs: {}, updatedAt: null });
105
+ res.json(data);
106
+ });
107
+
108
+ router.put("/api/gym/progress", (req, res) => {
109
+ ensureDir(memoryDir);
110
+ const data = { ...req.body, updatedAt: new Date().toISOString() };
111
+ writeJson(progressPath(), data);
112
+ res.json(data);
113
+ });
114
+
115
+ // ── 4. Cards ───────────────────────────────────────────────────────
116
+
117
+ const cardsPath = () => join(memoryDir, "gym-cards.json");
118
+
119
+ router.get("/api/gym/cards", (req, res) => {
120
+ ensureDir(memoryDir);
121
+ const data: any[] = readJson(cardsPath(), []);
122
+ const all = req.query.all === "true";
123
+ // By default, filter out dismissed cards
124
+ const filtered = all ? data : data.filter((c: any) => !c.dismissed);
125
+ res.json(Array.isArray(filtered) ? filtered : []);
126
+ });
127
+
128
+ router.post("/api/gym/cards", (req, res) => {
129
+ ensureDir(memoryDir);
130
+ const cards: any[] = readJson(cardsPath(), []);
131
+ const id = randomUUID();
132
+ const createdAt = new Date().toISOString();
133
+ const card = {
134
+ title: "",
135
+ description: "",
136
+ dismissed: false,
137
+ ...req.body,
138
+ // Auto-generated fields always override body
139
+ id,
140
+ createdAt,
141
+ };
142
+ cards.push(card);
143
+ writeJson(cardsPath(), cards);
144
+ res.status(201).json(card);
145
+ });
146
+
147
+ router.post("/api/gym/cards/:id/dismiss", (req, res) => {
148
+ ensureDir(memoryDir);
149
+ const cards: any[] = readJson(cardsPath(), []);
150
+ const card = cards.find((c: any) => c.id === req.params.id);
151
+ if (!card) {
152
+ res.status(404).json({ error: "Card not found" });
153
+ return;
154
+ }
155
+ card.dismissed = true;
156
+ card.dismissedAt = new Date().toISOString();
157
+ writeJson(cardsPath(), cards);
158
+ res.json(card);
159
+ });
160
+
161
+ router.delete("/api/gym/cards/:id", (req, res) => {
162
+ ensureDir(memoryDir);
163
+ const cards: any[] = readJson(cardsPath(), []);
164
+ const idx = cards.findIndex((c: any) => c.id === req.params.id);
165
+ if (idx === -1) {
166
+ res.status(404).json({ error: "Card not found" });
167
+ return;
168
+ }
169
+ const [removed] = cards.splice(idx, 1);
170
+ writeJson(cardsPath(), cards);
171
+ res.json(removed);
172
+ });
173
+
174
+ // ── 5. Dimension History (Snapshots) ───────────────────────────────
175
+
176
+ const dimensionHistoryPath = () => join(memoryDir, "dimension-history.json");
177
+
178
+ router.post("/api/gym/dimensions/snapshot", (req, res) => {
179
+ ensureDir(memoryDir);
180
+ const history: any[] = readJson(dimensionHistoryPath(), []);
181
+ const snapshot = {
182
+ date: req.body.date || new Date().toISOString().slice(0, 10),
183
+ dimensions: req.body.dimensions || {},
184
+ };
185
+ history.push(snapshot);
186
+ writeJson(dimensionHistoryPath(), history);
187
+ res.status(201).json(snapshot);
188
+ });
189
+
190
+ // ── 6. Programs — List All ─────────────────────────────────────────
191
+
192
+ router.get("/api/gym/programs", (_req, res) => {
193
+ const programs: any[] = [];
194
+
195
+ // Platform standard programs (from repo)
196
+ ensureDir(programsDir);
197
+ const platformSlugs = readdirSync(programsDir, { withFileTypes: true })
198
+ .filter((d) => d.isDirectory())
199
+ .map((d) => d.name);
200
+ for (const slug of platformSlugs) {
201
+ const data = readJson(join(programsDir, slug, "program.json"), null);
202
+ if (data) programs.push({ ...data, slug, source: data.source || "platform" });
203
+ }
204
+
205
+ // User & coach created programs (from Drive)
206
+ ensureDir(userProgramsDir);
207
+ const userSlugs = readdirSync(userProgramsDir, { withFileTypes: true })
208
+ .filter((d) => d.isDirectory())
209
+ .map((d) => d.name);
210
+ for (const slug of userSlugs) {
211
+ const data = readJson(join(userProgramsDir, slug, "program.json"), null);
212
+ if (data) programs.push({ ...data, slug, source: data.source || "user" });
213
+ }
214
+
215
+ res.json(programs);
216
+ });
217
+
218
+ // ── 7. Programs — Get One (with module content) ────────────────────
219
+
220
+ router.get("/api/gym/programs/:slug", (req, res) => {
221
+ const slug = req.params.slug;
222
+
223
+ // Check platform programs first, then user programs
224
+ let progDir = join(programsDir, slug);
225
+ let defaultSource = "platform";
226
+ if (!existsSync(join(progDir, "program.json"))) {
227
+ progDir = join(userProgramsDir, slug);
228
+ defaultSource = "user";
229
+ }
230
+ const pPath = join(progDir, "program.json");
231
+
232
+ if (!existsSync(pPath)) {
233
+ res.status(404).json({ error: "Program not found" });
234
+ return;
235
+ }
236
+
237
+ const program = readJson(pPath, {});
238
+ program.slug = slug;
239
+ if (!program.source) program.source = defaultSource;
240
+
241
+ // Enrich modules with .md file content if present
242
+ if (Array.isArray(program.modules)) {
243
+ program.modules = program.modules.map((mod: any) => {
244
+ if (mod.file) {
245
+ const mdPath = join(progDir, mod.file);
246
+ if (existsSync(mdPath)) {
247
+ mod.content = readFileSync(mdPath, "utf-8");
248
+ }
249
+ }
250
+ return mod;
251
+ });
252
+ }
253
+
254
+ res.json(program);
255
+ });
256
+
257
+ // ── 8. Programs — Create ───────────────────────────────────────────
258
+
259
+ router.post("/api/gym/programs", (req, res) => {
260
+ const slug = req.body.slug || req.body.title?.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || randomUUID();
261
+ const progDir = join(userProgramsDir, slug);
262
+ ensureDir(progDir);
263
+
264
+ const program = {
265
+ ...req.body,
266
+ slug,
267
+ source: req.body.source || "user",
268
+ createdAt: new Date().toISOString(),
269
+ updatedAt: new Date().toISOString(),
270
+ };
271
+ writeJson(join(progDir, "program.json"), program);
272
+ res.status(201).json(program);
273
+ });
274
+
275
+ // ── 9. Programs — Update ───────────────────────────────────────────
276
+
277
+ router.patch("/api/gym/programs/:slug", (req, res) => {
278
+ const slug = req.params.slug;
279
+ // Check user programs first (editable), then platform
280
+ let pPath = join(userProgramsDir, slug, "program.json");
281
+ if (!existsSync(pPath)) pPath = join(programsDir, slug, "program.json");
282
+
283
+ if (!existsSync(pPath)) {
284
+ res.status(404).json({ error: "Program not found" });
285
+ return;
286
+ }
287
+
288
+ const existing = readJson(pPath, {});
289
+ const updated = { ...existing, ...req.body, slug, updatedAt: new Date().toISOString() };
290
+ writeJson(pPath, updated);
291
+ res.json(updated);
292
+ });
293
+
294
+ // ── 10. Programs — Delete ──────────────────────────────────────────
295
+
296
+ router.delete("/api/gym/programs/:slug", (req, res) => {
297
+ const slug = req.params.slug;
298
+
299
+ // Check user programs first
300
+ let progDir = join(userProgramsDir, slug);
301
+ if (!existsSync(progDir)) {
302
+ // Allow deleting platform programs only if explicitly requested
303
+ progDir = join(programsDir, slug);
304
+ }
305
+
306
+ if (!existsSync(progDir)) {
307
+ res.status(404).json({ error: "Program not found" });
308
+ return;
309
+ }
310
+
311
+ rmSync(progDir, { recursive: true, force: true });
312
+ res.json({ deleted: slug });
313
+ });
314
+
315
+ // ── 11. Programs — Import Markdown ─────────────────────────────────
316
+
317
+ router.post("/api/gym/programs/import-markdown", (req, res) => {
318
+ const markdown: string = req.body.markdown || "";
319
+ if (!markdown.trim()) {
320
+ res.status(400).json({ error: "No markdown provided" });
321
+ return;
322
+ }
323
+
324
+ const lines = markdown.split("\n");
325
+ let title = "Untitled Program";
326
+ const modules: any[] = [];
327
+ let currentModule: any = null;
328
+ let currentStep: any = null;
329
+ let contentBuffer: string[] = [];
330
+
331
+ function flushContent() {
332
+ if (currentStep && contentBuffer.length) {
333
+ currentStep.content = contentBuffer.join("\n").trim();
334
+ contentBuffer = [];
335
+ }
336
+ }
337
+
338
+ for (const line of lines) {
339
+ // H1 — program title
340
+ const h1 = line.match(/^#\s+(.+)/);
341
+ if (h1 && !line.startsWith("##")) {
342
+ title = h1[1].trim();
343
+ continue;
344
+ }
345
+
346
+ // H2 — module
347
+ const h2 = line.match(/^##\s+(.+)/);
348
+ if (h2 && !line.startsWith("###")) {
349
+ flushContent();
350
+ currentModule = {
351
+ title: h2[1].trim().replace(/^Module\s+\d+:\s*/i, ""),
352
+ steps: [],
353
+ };
354
+ modules.push(currentModule);
355
+ currentStep = null;
356
+ contentBuffer = [];
357
+ continue;
358
+ }
359
+
360
+ // H3 — step
361
+ const h3 = line.match(/^###\s+(.+)/);
362
+ if (h3) {
363
+ flushContent();
364
+ currentStep = {
365
+ title: h3[1].trim().replace(/^Step\s+\d+:\s*/i, ""),
366
+ content: "",
367
+ };
368
+ if (currentModule) {
369
+ currentModule.steps.push(currentStep);
370
+ } else {
371
+ // Step before any module — create implicit module
372
+ currentModule = { title: "Module 1", steps: [currentStep] };
373
+ modules.push(currentModule);
374
+ }
375
+ contentBuffer = [];
376
+ continue;
377
+ }
378
+
379
+ // Regular content
380
+ contentBuffer.push(line);
381
+ }
382
+
383
+ // Flush remaining
384
+ flushContent();
385
+
386
+ // Generate slug
387
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || randomUUID();
388
+ const source = req.body.source || "user";
389
+ const progDir = join(userProgramsDir, slug);
390
+ ensureDir(progDir);
391
+
392
+ const program = {
393
+ slug,
394
+ title,
395
+ description: req.body.description || "",
396
+ source,
397
+ modules,
398
+ createdAt: new Date().toISOString(),
399
+ updatedAt: new Date().toISOString(),
400
+ };
401
+
402
+ writeJson(join(progDir, "program.json"), program);
403
+ res.status(201).json(program);
404
+ });
405
+
406
+ // ── 12. Agent Activity Summary ─────────────────────────────────────
407
+
408
+ router.get("/api/agents/:id/activity-summary", (req, res) => {
409
+ const agentId = req.params.id;
410
+ const agentsDir = join(baseDir, "agents");
411
+
412
+ // Resolve the agent's actual memory directory from config.json
413
+ let memDir = join(agentsDir, agentId, "memory");
414
+ const configPath = join(baseDir, "config.json");
415
+ try {
416
+ if (existsSync(configPath)) {
417
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
418
+ const agentConfig = config.agents?.[agentId];
419
+ if (agentConfig?.memoryDir) {
420
+ const resolved = agentConfig.memoryDir.replace(/^~/, process.env.HOME || "~");
421
+ if (existsSync(resolved)) memDir = resolved;
422
+ }
423
+ }
424
+ } catch { /* fall back to default path */ }
425
+
426
+ // Also check platform subdirectory if default path doesn't exist
427
+ if (!existsSync(memDir)) {
428
+ const platformMemDir = join(agentsDir, "platform", agentId, "memory");
429
+ if (existsSync(platformMemDir)) memDir = platformMemDir;
430
+ }
431
+
432
+ const logPath = join(memDir, "conversation_log.jsonl");
433
+
434
+ if (!existsSync(logPath)) {
435
+ res.json({
436
+ agentId,
437
+ messageCount: 0,
438
+ activeDays: 0,
439
+ uniqueDates: [],
440
+ topics: [],
441
+ toolUseCounts: {},
442
+ lastActive: null,
443
+ });
444
+ return;
445
+ }
446
+
447
+ const entries = readJsonl(logPath);
448
+ const messageCount = entries.length;
449
+ const dates = new Set<string>();
450
+ const toolCounts: Record<string, number> = {};
451
+ const topicSet = new Set<string>();
452
+ let lastActive: string | null = null;
453
+
454
+ for (const entry of entries) {
455
+ // Extract date
456
+ if (entry.timestamp) {
457
+ const dateStr = entry.timestamp.slice(0, 10);
458
+ dates.add(dateStr);
459
+ if (!lastActive || entry.timestamp > lastActive) {
460
+ lastActive = entry.timestamp;
461
+ }
462
+ }
463
+
464
+ // Extract topics from user messages (first 50 chars)
465
+ if (entry.role === "user" && entry.content) {
466
+ const snippet = entry.content.slice(0, 50).trim();
467
+ if (snippet) topicSet.add(snippet);
468
+ }
469
+
470
+ // Count tool uses from assistant messages
471
+ if (entry.role === "assistant" && entry.content) {
472
+ // Look for tool use patterns: Read(, Write(, Bash(, Glob(, Grep(, Edit(
473
+ const toolPattern = /\b(Read|Write|Bash|Glob|Grep|Edit|WebFetch|WebSearch|TodoWrite|NotebookEdit)\b/g;
474
+ let match;
475
+ while ((match = toolPattern.exec(entry.content)) !== null) {
476
+ toolCounts[match[1]] = (toolCounts[match[1]] || 0) + 1;
477
+ }
478
+ }
479
+
480
+ // Also check for explicit tool_use entries
481
+ if (entry.tool) {
482
+ toolCounts[entry.tool] = (toolCounts[entry.tool] || 0) + 1;
483
+ }
484
+ }
485
+
486
+ res.json({
487
+ agentId,
488
+ messageCount,
489
+ activeDays: dates.size,
490
+ uniqueDates: Array.from(dates).sort(),
491
+ topics: Array.from(topicSet).slice(0, 20), // Cap at 20
492
+ toolUseCounts: toolCounts,
493
+ lastActive,
494
+ });
495
+ });
496
+
497
+ // ── 13. Search Agent Logs ──────────────────────────────────────────
498
+
499
+ router.get("/api/agents/logs/search", (req, res) => {
500
+ const q = (req.query.q as string || "").toLowerCase().trim();
501
+ if (!q) {
502
+ res.status(400).json({ error: "Query parameter 'q' is required" });
503
+ return;
504
+ }
505
+
506
+ const agentIdsParam = req.query.agentIds as string | undefined;
507
+ const filterIds = agentIdsParam ? agentIdsParam.split(",").map((s) => s.trim()) : null;
508
+
509
+ const agentsDir = join(baseDir, "agents");
510
+ if (!existsSync(agentsDir)) {
511
+ res.json({ results: [] });
512
+ return;
513
+ }
514
+
515
+ const agentDirs = readdirSync(agentsDir, { withFileTypes: true })
516
+ .filter((d) => d.isDirectory() && !d.name.startsWith("_"))
517
+ .map((d) => d.name);
518
+
519
+ const results: any[] = [];
520
+ const maxResults = 100;
521
+
522
+ for (const agentId of agentDirs) {
523
+ if (filterIds && !filterIds.includes(agentId)) continue;
524
+
525
+ const logPath = join(agentsDir, agentId, "memory", "conversation_log.jsonl");
526
+ if (!existsSync(logPath)) continue;
527
+
528
+ const entries = readJsonl(logPath);
529
+ for (const entry of entries) {
530
+ if (results.length >= maxResults) break;
531
+ const content = (entry.content || "").toLowerCase();
532
+ if (content.includes(q)) {
533
+ results.push({
534
+ agentId,
535
+ role: entry.role,
536
+ content: entry.content,
537
+ timestamp: entry.timestamp,
538
+ // Include surrounding context — first 200 chars
539
+ snippet: entry.content?.slice(0, 200),
540
+ });
541
+ }
542
+ }
543
+ if (results.length >= maxResults) break;
544
+ }
545
+
546
+ res.json({ query: q, count: results.length, results });
547
+ });
548
+
549
+ // ── 14. Agent Logs (paginated) ──────────────────────────────────────
550
+
551
+ router.get("/api/agents/:id/logs", (req, res) => {
552
+ const agentId = req.params.id;
553
+ const limit = Math.min(parseInt(req.query.limit as string) || 50, 500);
554
+ const offset = Math.max(parseInt(req.query.offset as string) || 0, 0);
555
+ const agentsDir = join(baseDir, "agents");
556
+
557
+ // Resolve memory dir: check config.json memoryDir, then direct path, then platform/ subfolder
558
+ let logPath = "";
559
+ const cfgPath = join(baseDir, "config.json");
560
+ try {
561
+ if (existsSync(cfgPath)) {
562
+ const cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
563
+ const ac = cfg.agents?.[agentId];
564
+ if (ac?.memoryDir) {
565
+ const resolved = ac.memoryDir.replace(/^~/, process.env.HOME || "~");
566
+ const candidate = join(resolved, "conversation_log.jsonl");
567
+ if (existsSync(candidate)) logPath = candidate;
568
+ }
569
+ }
570
+ } catch { /* ignore */ }
571
+ if (!logPath) {
572
+ logPath = join(agentsDir, agentId, "memory", "conversation_log.jsonl");
573
+ if (!existsSync(logPath)) {
574
+ logPath = join(agentsDir, "platform", agentId, "memory", "conversation_log.jsonl");
575
+ }
576
+ }
577
+
578
+ if (!existsSync(logPath)) {
579
+ res.json({ agentId, total: 0, offset, limit, entries: [] });
580
+ return;
581
+ }
582
+
583
+ const all = readJsonl(logPath);
584
+ // Newest first
585
+ all.reverse();
586
+ const entries = all.slice(offset, offset + limit);
587
+ res.json({ agentId, total: all.length, offset, limit, entries });
588
+ });
589
+
590
+ // ── 15. Run Digest Manually ─────────────────────────────────────────
591
+
592
+ router.post("/api/gym/digest/run", async (req, res) => {
593
+ try {
594
+ const { runActivityDigest } = await import("./activity-digest.js");
595
+ // Infer port from the request
596
+ const port = parseInt(req.get("host")?.split(":")[1] || "4888");
597
+ await runActivityDigest({ baseDir, port });
598
+ res.json({ ok: true, message: "Activity digest completed" });
599
+ } catch (err: any) {
600
+ res.status(500).json({ error: err.message || "Digest failed" });
601
+ }
602
+ });
603
+
604
+ // ── 16. Dimension History ───────────────────────────────────────────
605
+
606
+ router.get("/api/gym/dimensions/history", (_req, res) => {
607
+ const histPath = join(memoryDir, "dimension-history.json");
608
+ const data = readJson(histPath, []);
609
+ res.json(Array.isArray(data) ? data : []);
610
+ });
611
+
612
+ // ── Changelog (for Feed: Platform Updates) ──
613
+ router.get("/api/changelog", (_req, res) => {
614
+ // Read changelog from a static file if it exists, otherwise return defaults
615
+ const changelogPath = join(baseDir, "data", "changelog.json");
616
+ if (existsSync(changelogPath)) {
617
+ const data = readJson(changelogPath, []);
618
+ res.json(Array.isArray(data) ? data : []);
619
+ } else {
620
+ // Return empty — changelog entries are added by platform updates
621
+ res.json([]);
622
+ }
623
+ });
624
+
625
+ // ── Feed Aggregator ──
626
+ router.get("/api/gym/feed", (_req, res) => {
627
+ ensureDir(memoryDir);
628
+
629
+ // Tips — sourced from AI insights (replaces old heuristic cards)
630
+ const insightsData = readJson(join(memoryDir, "insights.json"), { insights: [], dismissed: [] });
631
+ const dismissed: string[] = insightsData.dismissed || [];
632
+ const tips = (insightsData.insights || [])
633
+ .filter((ins: any) => ins.text && !dismissed.includes(ins.id))
634
+ .slice(-10)
635
+ .map((ins: any) => ({
636
+ title: ins.category === "dimension" ? "Dimension" : ins.category === "struggle" ? "Pattern" : ins.category === "dormant" ? "Dormant" : ins.category === "feature-gap" ? "Feature" : "Insight",
637
+ description: ins.text,
638
+ type: "tip",
639
+ generatedAt: insightsData.generatedAt || null,
640
+ }));
641
+
642
+ // Platform Updates — from changelog
643
+ const changelogPath = join(baseDir, "data", "changelog.json");
644
+ const changelog: any[] = existsSync(changelogPath) ? readJson(changelogPath, []) : [];
645
+
646
+ // Filter updates to those relevant to the learner
647
+ const profile = readJson(profilePath(), {});
648
+ const neverUsed = profile.features?.neverUsed || [];
649
+ const relevantUpdates = changelog
650
+ .filter((entry: any) => {
651
+ if (!entry.feature) return true; // Show all non-feature-specific entries
652
+ return neverUsed.includes(entry.feature); // Only show if user hasn't used this feature
653
+ })
654
+ .slice(-10);
655
+
656
+ // AI Briefing — from briefing.json if it exists
657
+ const briefingPath = join(memoryDir, "briefing.json");
658
+ const briefing: any[] = existsSync(briefingPath) ? readJson(briefingPath, []) : [];
659
+
660
+ res.json({
661
+ tips,
662
+ platformUpdates: relevantUpdates,
663
+ briefing: briefing.slice(-5),
664
+ });
665
+ });
666
+
667
+ // ── Gym Config (public flags for frontend) ──
668
+ router.get("/api/gym/config", (_req, res) => {
669
+ // Read service config to get gym flags
670
+ const configPath = join(baseDir, "config.json");
671
+ const config = readJson(configPath, {});
672
+ const service = config.service || {};
673
+ res.json({
674
+ gymEnabled: !!service.gymEnabled,
675
+ gymOnlyMode: !!service.gymOnlyMode,
676
+ aibriefingEnabled: !!service.aibriefingEnabled,
677
+ });
678
+ });
679
+
680
+ // ── AI Insights (written by weekly goal, read by "You tell me") ──
681
+
682
+ const insightsPath = () => join(memoryDir, "insights.json");
683
+
684
+ router.get("/api/gym/insights", (req, res) => {
685
+ const data = readJson(insightsPath(), { insights: [], generatedAt: null, dismissed: [] });
686
+ const dismissed: string[] = data.dismissed || [];
687
+ const includeDismissed = req.query.includeDismissed === "true";
688
+ // Filter out dismissed insights unless explicitly requested
689
+ const filtered = includeDismissed
690
+ ? data.insights || []
691
+ : (data.insights || []).filter((ins: any) => !dismissed.includes(ins.id));
692
+ res.json({ ...data, insights: filtered });
693
+ });
694
+
695
+ router.post("/api/gym/insights", (req, res) => {
696
+ const existing = readJson(insightsPath(), { insights: [], generatedAt: null, dismissed: [] });
697
+ // Assign IDs to each insight if not already present
698
+ const insights = (req.body.insights || []).map((ins: any, i: number) => ({
699
+ ...ins,
700
+ id: ins.id || `insight-${ins.category || "gen"}-${i}-${Date.now()}`,
701
+ }));
702
+ const newInsights = {
703
+ insights,
704
+ topRecommendation: req.body.topRecommendation || null,
705
+ summary: req.body.summary || null,
706
+ generatedAt: new Date().toISOString(),
707
+ previousGeneratedAt: existing.generatedAt || null,
708
+ dismissed: existing.dismissed || [], // Preserve dismissed list across regenerations
709
+ };
710
+ writeJson(insightsPath(), newInsights);
711
+ res.json(newInsights);
712
+ });
713
+
714
+ // Dismiss an insight (mark as done or cancelled)
715
+ router.post("/api/gym/insights/:id/dismiss", (req, res) => {
716
+ const data = readJson(insightsPath(), { insights: [], generatedAt: null, dismissed: [] });
717
+ const dismissed: string[] = data.dismissed || [];
718
+ const insightId = req.params.id;
719
+ const status = req.body.status || "dismissed"; // "done" | "cancelled" | "dismissed"
720
+ const insight = (data.insights || []).find((ins: any) => ins.id === insightId);
721
+ if (!insight) {
722
+ res.status(404).json({ error: "Insight not found" });
723
+ return;
724
+ }
725
+ if (!dismissed.includes(insightId)) {
726
+ dismissed.push(insightId);
727
+ }
728
+ // Mark the insight itself with status for history
729
+ insight.dismissedAt = new Date().toISOString();
730
+ insight.dismissStatus = status;
731
+ data.dismissed = dismissed;
732
+ writeJson(insightsPath(), data);
733
+ res.json({ ok: true, id: insightId, status });
734
+ });
735
+
736
+ // Clear all dismissed insights (reset)
737
+ router.post("/api/gym/insights/reset-dismissed", (_req, res) => {
738
+ const data = readJson(insightsPath(), { insights: [], generatedAt: null, dismissed: [] });
739
+ // Remove dismiss markers from insights
740
+ for (const ins of (data.insights || [])) {
741
+ delete ins.dismissedAt;
742
+ delete ins.dismissStatus;
743
+ }
744
+ data.dismissed = [];
745
+ writeJson(insightsPath(), data);
746
+ res.json({ ok: true });
747
+ });
748
+
749
+ // ── Manual digest trigger ────────────────────────────────────────
750
+ router.post("/api/gym/insights/generate", async (_req, res) => {
751
+ try {
752
+ const { runActivityDigest } = await import("./activity-digest.js");
753
+ const configPath = join(baseDir, "config.json");
754
+ const config = readJson(configPath, {});
755
+ const port = config.service?.port || 4888;
756
+ await runActivityDigest({ baseDir, port, memoryDir });
757
+ const data = readJson(insightsPath(), { insights: [], generatedAt: null });
758
+ res.json({ ok: true, ...data });
759
+ } catch (err: any) {
760
+ res.status(500).json({ error: err.message || String(err) });
761
+ }
762
+ });
763
+
764
+ // ── Coach-Created Guides ──────────────────────────────────────────
765
+
766
+ // List guides (programs with source=coach)
767
+ router.get("/api/gym/guides", (_req, res) => {
768
+ const guides: any[] = [];
769
+ // Check user programs dir for coach-created guides
770
+ if (existsSync(userProgramsDir)) {
771
+ for (const entry of readdirSync(userProgramsDir, { withFileTypes: true })) {
772
+ if (!entry.isDirectory()) continue;
773
+ const pPath = join(userProgramsDir, entry.name, "program.json");
774
+ const data = readJson(pPath, null);
775
+ if (data && data.source === "coach") {
776
+ guides.push({ ...data, slug: entry.name });
777
+ }
778
+ }
779
+ }
780
+ res.json(guides);
781
+ });
782
+
783
+ // Create a guide (convenience wrapper — creates a program with source=coach)
784
+ router.post("/api/gym/guides", (req, res) => {
785
+ const slug = req.body.slug || req.body.title?.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || randomUUID();
786
+ const progDir = join(userProgramsDir, slug);
787
+ ensureDir(progDir);
788
+
789
+ const guide = {
790
+ ...req.body,
791
+ slug,
792
+ source: "coach",
793
+ createdAt: new Date().toISOString(),
794
+ updatedAt: new Date().toISOString(),
795
+ };
796
+ writeJson(join(progDir, "program.json"), guide);
797
+ res.status(201).json(guide);
798
+ });
799
+
800
+ return router;
801
+ }