myaiforone 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -0
- package/agents/_template/CLAUDE.md +18 -0
- package/agents/_template/agent.json +7 -0
- package/agents/platform/agentcreator/CLAUDE.md +300 -0
- package/agents/platform/appcreator/CLAUDE.md +158 -0
- package/agents/platform/gym/CLAUDE.md +486 -0
- package/agents/platform/gym/agent.json +40 -0
- package/agents/platform/gym/programs/agent-building/program.json +160 -0
- package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
- package/agents/platform/gym/programs/getting-started/program.json +124 -0
- package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
- package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
- package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
- package/agents/platform/gym/souls/alex.md +12 -0
- package/agents/platform/gym/souls/jordan.md +12 -0
- package/agents/platform/gym/souls/morgan.md +12 -0
- package/agents/platform/gym/souls/riley.md +12 -0
- package/agents/platform/gym/souls/sam.md +12 -0
- package/agents/platform/hub/CLAUDE.md +372 -0
- package/agents/platform/promptcreator/CLAUDE.md +130 -0
- package/agents/platform/skillcreator/CLAUDE.md +163 -0
- package/bin/cli.js +566 -0
- package/config.example.json +310 -0
- package/dist/agent-registry.d.ts +32 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +144 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/channels/discord.d.ts +17 -0
- package/dist/channels/discord.d.ts.map +1 -0
- package/dist/channels/discord.js +114 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/imessage.d.ts +23 -0
- package/dist/channels/imessage.d.ts.map +1 -0
- package/dist/channels/imessage.js +214 -0
- package/dist/channels/imessage.js.map +1 -0
- package/dist/channels/slack.d.ts +19 -0
- package/dist/channels/slack.d.ts.map +1 -0
- package/dist/channels/slack.js +167 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/telegram.d.ts +19 -0
- package/dist/channels/telegram.d.ts.map +1 -0
- package/dist/channels/telegram.js +274 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/types.d.ts +44 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +18 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/channels/whatsapp.d.ts +23 -0
- package/dist/channels/whatsapp.d.ts.map +1 -0
- package/dist/channels/whatsapp.js +189 -0
- package/dist/channels/whatsapp.js.map +1 -0
- package/dist/config.d.ts +134 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +127 -0
- package/dist/config.js.map +1 -0
- package/dist/cron.d.ts +8 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +35 -0
- package/dist/cron.js.map +1 -0
- package/dist/decrypt-keys.d.ts +7 -0
- package/dist/decrypt-keys.d.ts.map +1 -0
- package/dist/decrypt-keys.js +53 -0
- package/dist/decrypt-keys.js.map +1 -0
- package/dist/encrypt-keys.d.ts +8 -0
- package/dist/encrypt-keys.d.ts.map +1 -0
- package/dist/encrypt-keys.js +62 -0
- package/dist/encrypt-keys.js.map +1 -0
- package/dist/executor.d.ts +31 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +2009 -0
- package/dist/executor.js.map +1 -0
- package/dist/gemini-executor.d.ts +27 -0
- package/dist/gemini-executor.d.ts.map +1 -0
- package/dist/gemini-executor.js +160 -0
- package/dist/gemini-executor.js.map +1 -0
- package/dist/goals.d.ts +24 -0
- package/dist/goals.d.ts.map +1 -0
- package/dist/goals.js +189 -0
- package/dist/goals.js.map +1 -0
- package/dist/gym/activity-digest.d.ts +30 -0
- package/dist/gym/activity-digest.d.ts.map +1 -0
- package/dist/gym/activity-digest.js +506 -0
- package/dist/gym/activity-digest.js.map +1 -0
- package/dist/gym/dimension-scorer.d.ts +76 -0
- package/dist/gym/dimension-scorer.d.ts.map +1 -0
- package/dist/gym/dimension-scorer.js +236 -0
- package/dist/gym/dimension-scorer.js.map +1 -0
- package/dist/gym/gym-router.d.ts +7 -0
- package/dist/gym/gym-router.d.ts.map +1 -0
- package/dist/gym/gym-router.js +718 -0
- package/dist/gym/gym-router.js.map +1 -0
- package/dist/gym/index.d.ts +11 -0
- package/dist/gym/index.d.ts.map +1 -0
- package/dist/gym/index.js +11 -0
- package/dist/gym/index.js.map +1 -0
- package/dist/heartbeat.d.ts +21 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/heartbeat.js +163 -0
- package/dist/heartbeat.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/keystore.d.ts +22 -0
- package/dist/keystore.d.ts.map +1 -0
- package/dist/keystore.js +178 -0
- package/dist/keystore.js.map +1 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +45 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/daily.d.ts +22 -0
- package/dist/memory/daily.d.ts.map +1 -0
- package/dist/memory/daily.js +82 -0
- package/dist/memory/daily.js.map +1 -0
- package/dist/memory/embeddings.d.ts +15 -0
- package/dist/memory/embeddings.d.ts.map +1 -0
- package/dist/memory/embeddings.js +154 -0
- package/dist/memory/embeddings.js.map +1 -0
- package/dist/memory/index.d.ts +32 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +159 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/search.d.ts +21 -0
- package/dist/memory/search.d.ts.map +1 -0
- package/dist/memory/search.js +77 -0
- package/dist/memory/search.js.map +1 -0
- package/dist/memory/store.d.ts +23 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +144 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/ollama-executor.d.ts +17 -0
- package/dist/ollama-executor.d.ts.map +1 -0
- package/dist/ollama-executor.js +112 -0
- package/dist/ollama-executor.js.map +1 -0
- package/dist/openai-executor.d.ts +38 -0
- package/dist/openai-executor.d.ts.map +1 -0
- package/dist/openai-executor.js +197 -0
- package/dist/openai-executor.js.map +1 -0
- package/dist/router.d.ts +11 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +185 -0
- package/dist/router.js.map +1 -0
- package/dist/test-message.d.ts +2 -0
- package/dist/test-message.d.ts.map +1 -0
- package/dist/test-message.js +60 -0
- package/dist/test-message.js.map +1 -0
- package/dist/utils/imsg-db-reader.d.ts +24 -0
- package/dist/utils/imsg-db-reader.d.ts.map +1 -0
- package/dist/utils/imsg-db-reader.js +92 -0
- package/dist/utils/imsg-db-reader.js.map +1 -0
- package/dist/utils/imsg-rpc.d.ts +25 -0
- package/dist/utils/imsg-rpc.d.ts.map +1 -0
- package/dist/utils/imsg-rpc.js +149 -0
- package/dist/utils/imsg-rpc.js.map +1 -0
- package/dist/utils/message-formatter.d.ts +3 -0
- package/dist/utils/message-formatter.d.ts.map +1 -0
- package/dist/utils/message-formatter.js +69 -0
- package/dist/utils/message-formatter.js.map +1 -0
- package/dist/web-ui.d.ts +12 -0
- package/dist/web-ui.d.ts.map +1 -0
- package/dist/web-ui.js +5784 -0
- package/dist/web-ui.js.map +1 -0
- package/dist/whatsapp-chats.d.ts +2 -0
- package/dist/whatsapp-chats.d.ts.map +1 -0
- package/dist/whatsapp-chats.js +76 -0
- package/dist/whatsapp-chats.js.map +1 -0
- package/dist/whatsapp-login.d.ts +2 -0
- package/dist/whatsapp-login.d.ts.map +1 -0
- package/dist/whatsapp-login.js +90 -0
- package/dist/whatsapp-login.js.map +1 -0
- package/dist/wiki-sync.d.ts +21 -0
- package/dist/wiki-sync.d.ts.map +1 -0
- package/dist/wiki-sync.js +147 -0
- package/dist/wiki-sync.js.map +1 -0
- package/docs/AddNewAgentGuide.md +100 -0
- package/docs/AddNewMcpGuide.md +72 -0
- package/docs/Architecture.md +795 -0
- package/docs/CLAUDE-AI-SETUP.md +166 -0
- package/docs/Setup.md +297 -0
- package/docs/ai-gym-architecture.md +1040 -0
- package/docs/ai-gym-build-plan.md +343 -0
- package/docs/ai-gym-onboarding.md +122 -0
- package/docs/appcreator_plan.md +348 -0
- package/docs/platform-mcp-audit.md +320 -0
- package/docs/server-deployment-plan.md +503 -0
- package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
- package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
- package/docs/user-guide.md +2016 -0
- package/mcp-catalog.json +628 -0
- package/package.json +63 -0
- package/public/MyAIforOne-logomark-512.svg +16 -0
- package/public/MyAIforOne-logomark-transparent.svg +15 -0
- package/public/activity.html +314 -0
- package/public/admin.html +1674 -0
- package/public/agent-dashboard.html +670 -0
- package/public/api-docs.html +1106 -0
- package/public/automations.html +722 -0
- package/public/canvas.css +223 -0
- package/public/canvas.js +588 -0
- package/public/changelog.html +231 -0
- package/public/gym.html +2766 -0
- package/public/home.html +1930 -0
- package/public/index.html +2809 -0
- package/public/lab.html +1643 -0
- package/public/library.html +1442 -0
- package/public/marketplace.html +1101 -0
- package/public/mcp-docs.html +441 -0
- package/public/mini.html +390 -0
- package/public/monitor.html +584 -0
- package/public/org.html +4304 -0
- package/public/projects.html +734 -0
- package/public/settings.html +645 -0
- package/public/tasks.html +932 -0
- package/public/trainers/alex.svg +12 -0
- package/public/trainers/jordan.svg +12 -0
- package/public/trainers/morgan.svg +12 -0
- package/public/trainers/riley.svg +12 -0
- package/public/trainers/sam.svg +12 -0
- package/public/user-guide.html +218 -0
- package/registry/agents.json +3 -0
- package/registry/apps.json +20 -0
- package/registry/installed-drafts.json +3 -0
- package/registry/mcps.json +1084 -0
- package/registry/prompts/personal/mcp-test-prompt.md +6 -0
- package/registry/prompts/personal/memory-recall.md +6 -0
- package/registry/prompts/platform/brainstorm.md +15 -0
- package/registry/prompts/platform/code-review.md +16 -0
- package/registry/prompts/platform/explain.md +16 -0
- package/registry/prompts.json +58 -0
- package/registry/skills/external/brainstorming.md +5 -0
- package/registry/skills/external/code-review.md +40 -0
- package/registry/skills/external/frontend-patterns.md +642 -0
- package/registry/skills/external/frontend-slides.md +184 -0
- package/registry/skills/external/systematic-debugging.md +5 -0
- package/registry/skills/external/tdd.md +328 -0
- package/registry/skills/external/verification-before-completion.md +5 -0
- package/registry/skills/external/writing-plans.md +5 -0
- package/registry/skills/platform/ai41_app_build.md +930 -0
- package/registry/skills/platform/ai41_app_deploy.md +168 -0
- package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
- package/registry/skills/platform/ai41_app_patterns.md +359 -0
- package/registry/skills/platform/ai41_app_register.md +85 -0
- package/registry/skills/platform/ai41_app_scaffold.md +421 -0
- package/registry/skills/platform/ai41_app_verify.md +107 -0
- package/registry/skills/platform/opProjectCreate.md +239 -0
- package/registry/skills/platform/op_devbrowser.md +136 -0
- package/registry/skills/platform/sop_brandguidelines.md +103 -0
- package/registry/skills/platform/sop_docx.md +117 -0
- package/registry/skills/platform/sop_frontenddesign.md +44 -0
- package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
- package/registry/skills/platform/sop_mcpbuilder.md +133 -0
- package/registry/skills/platform/sop_pdf.md +172 -0
- package/registry/skills/platform/sop_pptx.md +133 -0
- package/registry/skills/platform/sop_skillcreator.md +104 -0
- package/registry/skills/platform/sop_themefactory.md +128 -0
- package/registry/skills/platform/sop_webapptesting.md +75 -0
- package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
- package/registry/skills/platform/sop_xlsx.md +134 -0
- package/registry/skills.json +1055 -0
- package/scripts/discover-chats.sh +11 -0
- package/scripts/install-service-windows.ps1 +87 -0
- package/scripts/install-service.sh +52 -0
- package/scripts/seed-registry.ts +195 -0
- package/scripts/test-send.sh +5 -0
- package/scripts/tray-indicator.ps1 +35 -0
- package/scripts/uninstall-service-windows.ps1 +23 -0
- package/scripts/uninstall-service.sh +15 -0
- package/scripts/xbar-myagent.5s.sh +32 -0
- package/server/mcp-server/dist/index.d.ts +11 -0
- package/server/mcp-server/dist/index.js +1332 -0
- package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
- package/server/mcp-server/dist/lib/api-client.js +241 -0
- package/server/mcp-server/index.ts +1545 -0
- package/server/mcp-server/lib/api-client.ts +366 -0
- package/server/mcp-server/tsconfig.json +14 -0
- package/src/agent-registry.ts +180 -0
- package/src/channels/discord.ts +129 -0
- package/src/channels/imessage.ts +261 -0
- package/src/channels/slack.ts +208 -0
- package/src/channels/telegram.ts +307 -0
- package/src/channels/types.ts +62 -0
- package/src/channels/whatsapp.ts +227 -0
- package/src/config.ts +281 -0
- package/src/cron.ts +43 -0
- package/src/decrypt-keys.ts +60 -0
- package/src/encrypt-keys.ts +70 -0
- package/src/executor.ts +2190 -0
- package/src/gemini-executor.ts +212 -0
- package/src/goals.ts +240 -0
- package/src/gym/activity-digest.ts +546 -0
- package/src/gym/dimension-scorer.ts +297 -0
- package/src/gym/gym-router.ts +801 -0
- package/src/gym/index.ts +19 -0
- package/src/heartbeat.ts +220 -0
- package/src/index.ts +275 -0
- package/src/keystore.ts +190 -0
- package/src/logger.ts +51 -0
- package/src/memory/daily.ts +101 -0
- package/src/memory/embeddings.ts +185 -0
- package/src/memory/index.ts +218 -0
- package/src/memory/search.ts +124 -0
- package/src/memory/store.ts +189 -0
- package/src/ollama-executor.ts +126 -0
- package/src/openai-executor.ts +259 -0
- package/src/router.ts +230 -0
- package/src/test-message.ts +72 -0
- package/src/utils/imsg-db-reader.ts +109 -0
- package/src/utils/imsg-rpc.ts +178 -0
- package/src/utils/message-formatter.ts +90 -0
- package/src/web-ui.ts +5778 -0
- package/src/whatsapp-chats.ts +91 -0
- package/src/whatsapp-login.ts +110 -0
- package/src/wiki-sync.ts +199 -0
- package/tsconfig.json +19 -0
|
@@ -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
|
+
}
|