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,1281 @@
1
+ # Marketplace Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Build a `/marketplace` page where users can browse, install, and assign MCPs, skills, and agent templates to their agents.
6
+
7
+ **Architecture:** Registry JSON files in `/registry/` define all available items. Three backend endpoints handle listing (with installed/assigned status), installing (writing to config.mcps or copying skill files), and assigning (writing to agents[id].mcps/skills). A standalone `public/marketplace.html` page renders cards with search/filter and a post-install multi-agent assign modal.
8
+
9
+ **Tech Stack:** TypeScript/Express (backend), vanilla JS HTML page matching existing design system (DM Sans + IBM Plex Mono + Syne fonts, CSS variables from org.html), Node.js `fs` for registry reads/writes.
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-03-25-marketplace-design.md`
12
+
13
+ ---
14
+
15
+ ## File Map
16
+
17
+ | File | Action | Purpose |
18
+ |---|---|---|
19
+ | `scripts/seed-registry.ts` | Create | One-time seed: migrate mcp-catalog.json, scan skills, fetch external |
20
+ | `registry/mcps.json` | Create (generated) | All MCP entries |
21
+ | `registry/skills.json` | Create (generated) | All skill entries |
22
+ | `registry/agents.json` | Create | Agent template catalog (minimal) |
23
+ | `registry/skills/platform/*.md` | Create (generated) | Skill files copied from ~/.claude/commands/ |
24
+ | `registry/skills/external/*.md` | Create (fetched) | affaan-m skills downloaded |
25
+ | `registry/installed-drafts.json` | Create | Draft agent tracking (not config.json) |
26
+ | `src/web-ui.ts` | Modify | Add 3 marketplace API endpoints + serve /marketplace route |
27
+ | `public/marketplace.html` | Create | Marketplace UI page |
28
+ | `public/org.html` | Modify | Add Marketplace nav link |
29
+ | `public/activity.html` | Modify | Add Marketplace nav link |
30
+ | `public/home.html` | Modify | Add Marketplace nav link |
31
+ | `public/index.html` | Modify | Add Marketplace nav link |
32
+ | `Comprehensive Test Suite/web-ui/api.test.ts` | Modify | Add marketplace API tests |
33
+
34
+ ---
35
+
36
+ ## Task 1: Seed Script — Migrate MCPs + Generate Skills Registry
37
+
38
+ **Files:**
39
+ - Create: `scripts/seed-registry.ts`
40
+ - Create: `registry/mcps.json` (output)
41
+ - Create: `registry/skills.json` (output)
42
+ - Create: `registry/agents.json` (output)
43
+ - Create: `registry/skills/platform/*.md` (output)
44
+ - Create: `registry/skills/external/*.md` (output)
45
+
46
+ - [ ] **Step 1: Create registry folder structure**
47
+
48
+ ```bash
49
+ mkdir -p ~/Desktop/APPs/channelToAgentToClaude/registry/skills/platform
50
+ mkdir -p ~/Desktop/APPs/channelToAgentToClaude/registry/skills/external
51
+ mkdir -p ~/Desktop/APPs/channelToAgentToClaude/registry/agents/platform
52
+ ```
53
+
54
+ - [ ] **Step 2: Create the seed script**
55
+
56
+ Create `scripts/seed-registry.ts`:
57
+
58
+ ```typescript
59
+ #!/usr/bin/env npx ts-node
60
+ /**
61
+ * scripts/seed-registry.ts
62
+ * One-time seed: generates registry/mcps.json, registry/skills.json, registry/agents.json
63
+ * and copies/fetches all source files into registry/skills/
64
+ *
65
+ * Run: npx ts-node scripts/seed-registry.ts
66
+ */
67
+
68
+ import { readFileSync, writeFileSync, existsSync, readdirSync, copyFileSync, mkdirSync } from "node:fs";
69
+ import { join, basename } from "node:path";
70
+ import { homedir } from "node:os";
71
+ import { execSync } from "node:child_process";
72
+
73
+ const BASE = join(import.meta.dirname ?? __dirname, "..");
74
+ const COMMANDS_DIR = join(homedir(), ".claude", "commands");
75
+
76
+ // ── 1. Migrate mcp-catalog.json → registry/mcps.json ──────────────
77
+
78
+ const catalog = JSON.parse(readFileSync(join(BASE, "mcp-catalog.json"), "utf-8"));
79
+
80
+ const mcpEntries = Object.entries(catalog.mcps as Record<string, any>).map(([id, mcp]) => ({
81
+ id,
82
+ name: mcp.name,
83
+ provider: "AgenticLedger",
84
+ description: mcp.description,
85
+ category: mcp.category,
86
+ verified: true,
87
+ source: "agenticledger/platform",
88
+ tags: [mcp.category],
89
+ requiredKeys: mcp.requiredKeys || [],
90
+ fetch: {
91
+ type: "http",
92
+ url: mcp.url,
93
+ },
94
+ }));
95
+
96
+ writeFileSync(
97
+ join(BASE, "registry", "mcps.json"),
98
+ JSON.stringify({ mcps: mcpEntries }, null, 2)
99
+ );
100
+ console.log(`✓ registry/mcps.json — ${mcpEntries.length} MCPs`);
101
+
102
+ // ── 2. Scan ~/.claude/commands/ → registry/skills.json ─────────────
103
+
104
+ function extractFrontmatter(content: string): { name?: string; description?: string } {
105
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
106
+ if (!match) return {};
107
+ const fm: Record<string, string> = {};
108
+ for (const line of match[1].split("\n")) {
109
+ const [key, ...rest] = line.split(":");
110
+ if (key && rest.length) fm[key.trim()] = rest.join(":").trim();
111
+ }
112
+ return { name: fm.name, description: fm.description };
113
+ }
114
+
115
+ function titleCase(id: string): string {
116
+ return id.replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase());
117
+ }
118
+
119
+ function guessCategory(id: string, description: string): string {
120
+ const text = (id + " " + description).toLowerCase();
121
+ if (text.match(/pdf|docx|pptx|xlsx|word|excel|powerpoint/)) return "documents";
122
+ if (text.match(/deploy|railway|mcp|skill|agent|build/)) return "devtools";
123
+ if (text.match(/bastion|soc|audit|compliance/)) return "compliance";
124
+ if (text.match(/client|bastion/)) return "client";
125
+ if (text.match(/test|tdd/)) return "testing";
126
+ if (text.match(/crypto|blockchain|wallet|token/)) return "crypto";
127
+ if (text.match(/slack|telegram|email|calendar/)) return "productivity";
128
+ if (text.match(/frontend|design|ui|css/)) return "design";
129
+ return "general";
130
+ }
131
+
132
+ mkdirSync(join(BASE, "registry", "skills", "platform"), { recursive: true });
133
+
134
+ const skillEntries: any[] = [];
135
+
136
+ if (existsSync(COMMANDS_DIR)) {
137
+ const files = readdirSync(COMMANDS_DIR).filter(f => f.endsWith(".md"));
138
+ for (const file of files) {
139
+ const id = file.replace(".md", "");
140
+ const srcPath = join(COMMANDS_DIR, file);
141
+ const destPath = join(BASE, "registry", "skills", "platform", file);
142
+ const content = readFileSync(srcPath, "utf-8");
143
+ const { name: fmName, description: fmDesc } = extractFrontmatter(content);
144
+ const name = fmName || titleCase(id);
145
+ const description = fmDesc || "";
146
+ const category = guessCategory(id, description);
147
+
148
+ copyFileSync(srcPath, destPath);
149
+
150
+ skillEntries.push({
151
+ id,
152
+ name,
153
+ provider: "AgenticLedger",
154
+ description,
155
+ category,
156
+ verified: true,
157
+ source: "agenticledger/platform",
158
+ tags: [category],
159
+ localPath: `registry/skills/platform/${file}`,
160
+ fetch: { type: "file" },
161
+ });
162
+ }
163
+ console.log(`✓ registry/skills/platform/ — ${files.length} skills copied`);
164
+ }
165
+
166
+ // ── 3. Fetch external skills from affaan-m/everything-claude-code ──
167
+
168
+ const EXTERNAL_SKILLS: Array<{ id: string; name: string; description: string; file: string }> = [
169
+ { id: "tdd", name: "Test-Driven Development", description: "Write failing tests first, then implement.", file: "tdd.md" },
170
+ { id: "code-review", name: "Code Review", description: "Systematic code review workflow.", file: "code-review.md" },
171
+ { id: "systematic-debugging", name: "Systematic Debugging", description: "Structured debugging process.", file: "systematic-debugging.md" },
172
+ { id: "writing-plans", name: "Write Plans", description: "Create detailed implementation plans.", file: "writing-plans.md" },
173
+ { id: "verification-before-completion", name: "Verify Before Completing", description: "Run checks before marking work done.", file: "verification-before-completion.md" },
174
+ { id: "brainstorming", name: "Brainstorming", description: "Design features through collaborative dialogue.", file: "brainstorming.md" },
175
+ ];
176
+
177
+ const RAW_BASE = "https://raw.githubusercontent.com/affaan-m/everything-claude-code/main/commands";
178
+
179
+ mkdirSync(join(BASE, "registry", "skills", "external"), { recursive: true });
180
+
181
+ for (const skill of EXTERNAL_SKILLS) {
182
+ const destPath = join(BASE, "registry", "skills", "external", skill.file);
183
+ if (existsSync(destPath)) {
184
+ console.log(` skip (exists): ${skill.file}`);
185
+ } else {
186
+ try {
187
+ const content = execSync(`curl -sf "${RAW_BASE}/${skill.file}"`, { timeout: 10_000 }).toString();
188
+ writeFileSync(destPath, content);
189
+ console.log(` fetched: ${skill.file}`);
190
+ } catch {
191
+ // Write a placeholder if fetch fails
192
+ writeFileSync(destPath, `# ${skill.name}\n\n${skill.description}\n\n(Source: ${RAW_BASE}/${skill.file})\n`);
193
+ console.log(` placeholder: ${skill.file} (fetch failed)`);
194
+ }
195
+ }
196
+
197
+ skillEntries.push({
198
+ id: `ext-${skill.id}`,
199
+ name: skill.name,
200
+ provider: "affaan-m",
201
+ description: skill.description,
202
+ category: "devtools",
203
+ verified: false,
204
+ source: `github:affaan-m/everything-claude-code/commands/${skill.file}`,
205
+ tags: ["devtools", "external"],
206
+ localPath: `registry/skills/external/${skill.file}`,
207
+ fetch: { type: "file" },
208
+ });
209
+ }
210
+
211
+ writeFileSync(
212
+ join(BASE, "registry", "skills.json"),
213
+ JSON.stringify({ skills: skillEntries }, null, 2)
214
+ );
215
+ console.log(`✓ registry/skills.json — ${skillEntries.length} skills total`);
216
+
217
+ // ── 4. Create minimal registry/agents.json ─────────────────────────
218
+
219
+ const agentsRegistry = {
220
+ agents: [
221
+ {
222
+ id: "general-assistant",
223
+ name: "General Assistant",
224
+ provider: "AgenticLedger",
225
+ description: "A general-purpose agent for everyday tasks.",
226
+ category: "general",
227
+ verified: true,
228
+ source: "agenticledger/platform",
229
+ tags: ["general"],
230
+ localPath: "registry/agents/platform/general-assistant",
231
+ fetch: { type: "file" },
232
+ },
233
+ ],
234
+ };
235
+
236
+ writeFileSync(
237
+ join(BASE, "registry", "agents.json"),
238
+ JSON.stringify(agentsRegistry, null, 2)
239
+ );
240
+ console.log("✓ registry/agents.json — 1 template");
241
+
242
+ // ── 5. Create empty installed-drafts.json ─────────────────────────
243
+
244
+ const draftsPath = join(BASE, "registry", "installed-drafts.json");
245
+ if (!existsSync(draftsPath)) {
246
+ writeFileSync(draftsPath, JSON.stringify({ drafts: [] }, null, 2));
247
+ console.log("✓ registry/installed-drafts.json — created empty");
248
+ }
249
+
250
+ console.log("\n✅ Registry seed complete.");
251
+ ```
252
+
253
+ - [ ] **Step 3: Run the seed script**
254
+
255
+ ```bash
256
+ cd ~/Desktop/APPs/channelToAgentToClaude && npx ts-node --esm scripts/seed-registry.ts 2>&1
257
+ ```
258
+
259
+ Expected output:
260
+ ```
261
+ ✓ registry/mcps.json — 47 MCPs
262
+ ✓ registry/skills/platform/ — 67 skills copied
263
+ fetched: tdd.md (or "placeholder" if GitHub unavailable)
264
+ ...
265
+ ✓ registry/skills.json — 73 skills total
266
+ ✓ registry/agents.json — 1 template
267
+ ✓ registry/installed-drafts.json — created empty
268
+ ✅ Registry seed complete.
269
+ ```
270
+
271
+ - [ ] **Step 4: Verify output files exist and are valid JSON**
272
+
273
+ ```bash
274
+ node -e "const f=require('fs');['registry/mcps.json','registry/skills.json','registry/agents.json'].forEach(p=>{const d=JSON.parse(f.readFileSync(p,'utf-8'));console.log(p,JSON.stringify(Object.keys(d)))})"
275
+ ```
276
+
277
+ Expected: `registry/mcps.json ["mcps"]`, `registry/skills.json ["skills"]`, `registry/agents.json ["agents"]`
278
+
279
+ - [ ] **Step 5: Commit registry data**
280
+
281
+ ```bash
282
+ cd ~/Desktop/APPs/channelToAgentToClaude
283
+ git add registry/ scripts/seed-registry.ts
284
+ git commit -m "feat: add registry data layer and seed script"
285
+ ```
286
+
287
+ ---
288
+
289
+ ## Task 2: Backend API Endpoints
290
+
291
+ **Files:**
292
+ - Modify: `src/web-ui.ts` (add after the recover endpoint, around line 565)
293
+
294
+ The three endpoints follow the exact same patterns as existing endpoints: read opts.config in memory, write config.json then sync to opts.config, return `{ ok: true }` or error.
295
+
296
+ - [ ] **Step 1: Add the `appendFileSync` import check**
297
+
298
+ `appendFileSync` is already imported from Task 0 (we added it in the recover endpoint earlier). Verify:
299
+
300
+ ```bash
301
+ grep "appendFileSync" ~/Desktop/APPs/channelToAgentToClaude/src/web-ui.ts | head -1
302
+ ```
303
+
304
+ Expected: sees `appendFileSync` in the import line.
305
+
306
+ - [ ] **Step 2: Add GET /api/marketplace/:type endpoint**
307
+
308
+ In `src/web-ui.ts`, find the recover endpoint we added earlier and add the marketplace endpoints immediately after it. Add this block:
309
+
310
+ ```typescript
311
+ // ─── API: Marketplace ──────────────────────────────────────────────
312
+
313
+ app.get("/api/marketplace/:type", (req, res) => {
314
+ const { type } = req.params;
315
+ if (!["mcps", "skills", "agents"].includes(type)) {
316
+ return res.status(400).json({ error: "type must be mcps, skills, or agents" });
317
+ }
318
+
319
+ const registryPath = join(opts.baseDir, "registry", `${type}.json`);
320
+ if (!existsSync(registryPath)) {
321
+ return res.json({ items: [] });
322
+ }
323
+
324
+ let entries: any[] = [];
325
+ try {
326
+ const data = JSON.parse(readFileSync(registryPath, "utf-8"));
327
+ entries = data[type] || [];
328
+ } catch {
329
+ return res.status(500).json({ error: "Failed to read registry" });
330
+ }
331
+
332
+ const home = homedir();
333
+ const resolveTilde = (p: string) => p.startsWith("~") ? p.replace("~", home) : p;
334
+ const personalSkillsDir = join(resolveTilde(getPersonalAgentsDir(opts.config)), "skills");
335
+ const claudeCommandsDir = join(home, ".claude", "commands");
336
+
337
+ const items = entries.map((entry: any) => {
338
+ let installed = false;
339
+ const assignedTo: string[] = [];
340
+
341
+ if (type === "skills") {
342
+ const id = entry.id;
343
+ installed = existsSync(join(personalSkillsDir, `${id}.md`))
344
+ || existsSync(join(claudeCommandsDir, `${id}.md`));
345
+ for (const [agentId, agent] of Object.entries(opts.config.agents)) {
346
+ if ((agent as any).skills?.includes(id)) assignedTo.push(agentId);
347
+ }
348
+ } else if (type === "mcps") {
349
+ installed = !!opts.config.mcps?.[entry.id];
350
+ for (const [agentId, agent] of Object.entries(opts.config.agents)) {
351
+ if ((agent as any).mcps?.includes(entry.id)) assignedTo.push(agentId);
352
+ }
353
+ } else if (type === "agents") {
354
+ // Check installed-drafts.json + real config agents
355
+ const draftsPath = join(opts.baseDir, "registry", "installed-drafts.json");
356
+ let drafts: string[] = [];
357
+ try {
358
+ drafts = JSON.parse(readFileSync(draftsPath, "utf-8")).drafts.map((d: any) => d.id);
359
+ } catch { /* ignore */ }
360
+ installed = existsSync(join(opts.baseDir, "agents", entry.id))
361
+ || drafts.includes(entry.id)
362
+ || !!opts.config.agents[entry.id];
363
+ }
364
+
365
+ return { ...entry, installed, assignedTo };
366
+ });
367
+
368
+ res.json({ items });
369
+ });
370
+ ```
371
+
372
+ - [ ] **Step 3: Add POST /api/marketplace/install endpoint**
373
+
374
+ ```typescript
375
+ app.post("/api/marketplace/install", (req, res) => {
376
+ const { type, id } = req.body as { type?: string; id?: string };
377
+ if (!type || !id) return res.status(400).json({ error: "Missing type or id" });
378
+
379
+ const registryPath = join(opts.baseDir, "registry", `${type}s.json`);
380
+ if (!existsSync(registryPath)) return res.status(404).json({ error: "Registry not found" });
381
+
382
+ let entry: any;
383
+ try {
384
+ const data = JSON.parse(readFileSync(registryPath, "utf-8"));
385
+ const key = type === "mcp" ? "mcps" : type === "skill" ? "skills" : "agents";
386
+ entry = (data[key] || []).find((e: any) => e.id === id);
387
+ } catch {
388
+ return res.status(500).json({ error: "Failed to read registry" });
389
+ }
390
+ if (!entry) return res.status(404).json({ error: `${type} "${id}" not found in registry` });
391
+
392
+ const home = homedir();
393
+ const resolveTilde = (p: string) => p.startsWith("~") ? p.replace("~", home) : p;
394
+
395
+ try {
396
+ if (type === "skill") {
397
+ // Copy skill file to personalAgents/skills/ directory
398
+ const destDir = join(resolveTilde(getPersonalAgentsDir(opts.config)), "skills");
399
+ mkdirSync(destDir, { recursive: true });
400
+ const srcPath = join(opts.baseDir, entry.localPath);
401
+ const destPath = join(destDir, `${id}.md`);
402
+ if (!existsSync(srcPath)) return res.status(500).json({ error: `Source file not found: ${entry.localPath}` });
403
+ const { copyFileSync: cfSync } = require("node:fs");
404
+ cfSync(srcPath, destPath);
405
+ log.info(`[Marketplace] Installed skill ${id} → ${destPath}`);
406
+
407
+ } else if (type === "mcp") {
408
+ if (entry.fetch?.type === "http") {
409
+ // Write http entry to config.mcps
410
+ const configPath = join(opts.baseDir, "config.json");
411
+ const rawConfig = JSON.parse(readFileSync(configPath, "utf-8"));
412
+ if (!rawConfig.mcps) rawConfig.mcps = {};
413
+ rawConfig.mcps[id] = { type: "http", url: entry.fetch.url, headers: {} };
414
+ writeFileSync(configPath, JSON.stringify(rawConfig, null, 2));
415
+ if (!opts.config.mcps) (opts.config as any).mcps = {};
416
+ (opts.config.mcps as any)[id] = { type: "http", url: entry.fetch.url, headers: {} };
417
+ log.info(`[Marketplace] Installed MCP ${id} (http)`);
418
+
419
+ } else if (entry.fetch?.type === "npm") {
420
+ // Install npm package
421
+ const { execSync: es } = require("node:child_process");
422
+ es(`npm install ${entry.fetch.package}`, { cwd: opts.baseDir, timeout: 30_000 });
423
+ const configPath = join(opts.baseDir, "config.json");
424
+ const rawConfig = JSON.parse(readFileSync(configPath, "utf-8"));
425
+ if (!rawConfig.mcps) rawConfig.mcps = {};
426
+ rawConfig.mcps[id] = { type: "stdio", command: "npx", args: entry.fetch.args || ["-y", entry.fetch.package], env: {} };
427
+ writeFileSync(configPath, JSON.stringify(rawConfig, null, 2));
428
+ if (!opts.config.mcps) (opts.config as any).mcps = {};
429
+ (opts.config.mcps as any)[id] = rawConfig.mcps[id];
430
+ log.info(`[Marketplace] Installed MCP ${id} (npm: ${entry.fetch.package})`);
431
+ }
432
+
433
+ } else if (type === "agent") {
434
+ // Copy template files, write to installed-drafts.json (NOT config.json — see spec)
435
+ const srcDir = join(opts.baseDir, entry.localPath);
436
+ const destDir = join(opts.baseDir, "agents", id);
437
+ if (existsSync(srcDir)) {
438
+ mkdirSync(destDir, { recursive: true });
439
+ for (const file of readdirSync(srcDir)) {
440
+ const { copyFileSync: cfSync } = require("node:fs");
441
+ cfSync(join(srcDir, file), join(destDir, file));
442
+ }
443
+ } else {
444
+ // No template files — just create the directory with a default CLAUDE.md
445
+ mkdirSync(join(destDir, "memory"), { recursive: true });
446
+ writeFileSync(join(destDir, "CLAUDE.md"), `# ${entry.name}\n\n${entry.description}\n`);
447
+ writeFileSync(join(destDir, "agent.json"), JSON.stringify({ id, name: entry.name, draft: true, version: "1.0.0", created: new Date().toISOString() }, null, 2));
448
+ }
449
+
450
+ // Append to installed-drafts.json
451
+ const draftsPath = join(opts.baseDir, "registry", "installed-drafts.json");
452
+ let draftsData = { drafts: [] as any[] };
453
+ try { draftsData = JSON.parse(readFileSync(draftsPath, "utf-8")); } catch { /* fresh */ }
454
+ if (!draftsData.drafts.find((d: any) => d.id === id)) {
455
+ draftsData.drafts.push({ id, name: entry.name, installedAt: new Date().toISOString() });
456
+ writeFileSync(draftsPath, JSON.stringify(draftsData, null, 2));
457
+ }
458
+ log.info(`[Marketplace] Installed agent template ${id} → draft`);
459
+ }
460
+
461
+ const requiresKeys = type === "mcp" && (entry.requiredKeys?.length > 0);
462
+ res.json({ ok: true, item: { ...entry, installed: true }, requiresKeys });
463
+
464
+ } catch (err) {
465
+ log.error(`[Marketplace] Install failed for ${type}/${id}: ${err}`);
466
+ res.status(500).json({ error: String(err) });
467
+ }
468
+ });
469
+ ```
470
+
471
+ - [ ] **Step 4: Add POST /api/marketplace/assign endpoint**
472
+
473
+ ```typescript
474
+ app.post("/api/marketplace/assign", (req, res) => {
475
+ const { type, id, agentIds } = req.body as { type?: string; id?: string; agentIds?: string[] };
476
+ if (!type || !id || !Array.isArray(agentIds) || agentIds.length === 0) {
477
+ return res.status(400).json({ error: "Missing type, id, or agentIds" });
478
+ }
479
+
480
+ const configPath = join(opts.baseDir, "config.json");
481
+ let rawConfig: any;
482
+ try {
483
+ rawConfig = JSON.parse(readFileSync(configPath, "utf-8"));
484
+ } catch {
485
+ return res.status(500).json({ error: "Failed to read config.json" });
486
+ }
487
+
488
+ const missingKeys: string[] = [];
489
+
490
+ for (const agentId of agentIds) {
491
+ if (!rawConfig.agents[agentId]) continue;
492
+
493
+ if (type === "skill") {
494
+ if (!rawConfig.agents[agentId].skills) rawConfig.agents[agentId].skills = [];
495
+ if (!rawConfig.agents[agentId].skills.includes(id)) {
496
+ rawConfig.agents[agentId].skills.push(id);
497
+ }
498
+ if (!opts.config.agents[agentId].skills) (opts.config.agents[agentId] as any).skills = [];
499
+ if (!(opts.config.agents[agentId] as any).skills.includes(id)) {
500
+ (opts.config.agents[agentId] as any).skills.push(id);
501
+ }
502
+
503
+ } else if (type === "mcp") {
504
+ if (!rawConfig.agents[agentId].mcps) rawConfig.agents[agentId].mcps = [];
505
+ if (!rawConfig.agents[agentId].mcps.includes(id)) {
506
+ rawConfig.agents[agentId].mcps.push(id);
507
+ }
508
+ if (!opts.config.agents[agentId].mcps) (opts.config.agents[agentId] as any).mcps = [];
509
+ if (!(opts.config.agents[agentId] as any).mcps.includes(id)) {
510
+ (opts.config.agents[agentId] as any).mcps.push(id);
511
+ }
512
+ // Check if key file exists
513
+ const home = homedir();
514
+ const resolveTilde = (p: string) => p.startsWith("~") ? p.replace("~", home) : p;
515
+ const agentCfg = opts.config.agents[agentId] as any;
516
+ const agentHome = agentCfg.agentHome
517
+ ? resolveTilde(agentCfg.agentHome)
518
+ : join(resolveTilde(agentCfg.memoryDir || ""), "..");
519
+ const keyFile = join(agentHome, "mcp-keys", `${id}.env`);
520
+ if (!existsSync(keyFile)) {
521
+ missingKeys.push(agentId);
522
+ // Create empty stub so executor doesn't throw
523
+ mkdirSync(join(agentHome, "mcp-keys"), { recursive: true });
524
+ writeFileSync(keyFile, "");
525
+ }
526
+ }
527
+ }
528
+
529
+ try {
530
+ writeFileSync(configPath, JSON.stringify(rawConfig, null, 2));
531
+ } catch (err) {
532
+ return res.status(500).json({ error: `Failed to write config: ${err}` });
533
+ }
534
+
535
+ log.info(`[Marketplace] Assigned ${type}/${id} to agents: ${agentIds.join(", ")}`);
536
+ res.json({ ok: true, assigned: agentIds, missingKeys });
537
+ });
538
+ ```
539
+
540
+ - [ ] **Step 5: Add /marketplace route to serve the page**
541
+
542
+ Find the existing page serving routes (around line 60 in web-ui.ts) and add:
543
+
544
+ ```typescript
545
+ // ─── Serve the Marketplace page ────────────────────────────────
546
+ app.get("/marketplace", (_req, res) => {
547
+ const htmlPath = join(opts.baseDir, "public", "marketplace.html");
548
+ if (existsSync(htmlPath)) {
549
+ res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
550
+ res.sendFile(htmlPath);
551
+ } else {
552
+ res.status(404).send("Marketplace page not found.");
553
+ }
554
+ });
555
+ ```
556
+
557
+ - [ ] **Step 6: Build and verify no TypeScript errors**
558
+
559
+ ```bash
560
+ cd ~/Desktop/APPs/channelToAgentToClaude && npm run build 2>&1
561
+ ```
562
+
563
+ Expected: clean build, no errors.
564
+
565
+ - [ ] **Step 7: Commit backend**
566
+
567
+ ```bash
568
+ git add src/web-ui.ts
569
+ git commit -m "feat: add marketplace API endpoints (list, install, assign)"
570
+ ```
571
+
572
+ ---
573
+
574
+ ## Task 3: Frontend — marketplace.html
575
+
576
+ **Files:**
577
+ - Create: `public/marketplace.html`
578
+
579
+ The page must match the existing design system exactly:
580
+ - Same CSS variables as org.html and activity.html
581
+ - Same fonts: DM Sans, IBM Plex Mono, Syne
582
+ - Same topbar with logo-mark, tab-btn navigation, theme toggle
583
+ - Cards using `--bg-card`, `--border-glow`, `--accent`, `--green`, `--amber`
584
+ - Same modal pattern as org.html's existing modals
585
+
586
+ - [ ] **Step 1: Create marketplace.html**
587
+
588
+ Create `public/marketplace.html` with the full implementation below. This is a long file — write it completely:
589
+
590
+ ```html
591
+ <!DOCTYPE html>
592
+ <html lang="en">
593
+ <head>
594
+ <meta charset="UTF-8">
595
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
596
+ <title>MyAIforOne — Marketplace</title>
597
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet">
598
+ <style>
599
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
600
+
601
+ :root{
602
+ --bg-deep:#060a13;
603
+ --bg-surface:rgba(12,18,33,0.92);
604
+ --bg-card:rgba(16,22,40,0.85);
605
+ --bg-input:rgba(0,0,0,0.35);
606
+ --border-dim:rgba(56,189,248,0.08);
607
+ --border-glow:rgba(56,189,248,0.18);
608
+ --border-active:rgba(56,189,248,0.45);
609
+ --text-primary:rgba(255,255,255,0.92);
610
+ --text-secondary:rgba(255,255,255,0.68);
611
+ --text-muted:rgba(148,163,184,0.55);
612
+ --accent:#22d3ee;
613
+ --accent-soft:#38bdf8;
614
+ --accent-bg:rgba(6,182,212,0.15);
615
+ --accent-glow:rgba(34,211,238,0.12);
616
+ --purple:rgba(139,92,246,0.7);
617
+ --purple-bg:rgba(139,92,246,0.12);
618
+ --green:#4ade80;
619
+ --green-bg:rgba(74,222,128,0.1);
620
+ --amber:#fbbf24;
621
+ --amber-bg:rgba(251,191,36,0.1);
622
+ --red:#f87171;
623
+ --red-bg:rgba(248,113,113,0.1);
624
+ --shadow:0 2px 12px rgba(0,0,0,0.3);
625
+ --shadow-glow:0 0 20px rgba(34,211,238,0.08);
626
+ --radius:12px;
627
+ --font-sans:'DM Sans',system-ui,sans-serif;
628
+ --font-mono:'IBM Plex Mono',monospace;
629
+ --font-display:'Syne',sans-serif;
630
+ }
631
+ [data-theme="light"]{
632
+ --bg-deep:#f4f6f9;--bg-surface:rgba(255,255,255,0.95);--bg-card:rgba(255,255,255,0.9);
633
+ --bg-input:rgba(0,0,0,0.04);--border-dim:rgba(0,0,0,0.08);--border-glow:rgba(14,116,144,0.18);
634
+ --border-active:rgba(14,116,144,0.45);--text-primary:rgba(15,23,42,0.92);
635
+ --text-secondary:rgba(51,65,85,0.8);--text-muted:rgba(100,116,139,0.6);
636
+ --accent:#0891b2;--accent-soft:#0e7490;--accent-bg:rgba(14,116,144,0.08);
637
+ --accent-glow:rgba(14,116,144,0.06);--purple:rgba(109,40,217,0.75);--purple-bg:rgba(139,92,246,0.08);
638
+ --green:#16a34a;--green-bg:rgba(22,163,74,0.08);--amber:#d97706;--amber-bg:rgba(217,119,6,0.08);
639
+ --red:#dc2626;--red-bg:rgba(220,38,38,0.08);--shadow:0 1px 8px rgba(0,0,0,0.06);--shadow-glow:none;
640
+ }
641
+
642
+ html,body{width:100%;height:100%;overflow:hidden;background:var(--bg-deep);font-family:var(--font-sans);color:var(--text-primary);transition:background .3s,color .3s}
643
+
644
+ /* ── Topbar ── */
645
+ .topbar{height:56px;display:flex;align-items:center;padding:0 24px;background:var(--bg-surface);border-bottom:1px solid var(--border-dim);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);position:fixed;top:0;left:0;right:0;z-index:100}
646
+ .topbar-logo{display:flex;align-items:center;gap:10px;margin-right:32px}
647
+ .logo-mark{width:32px;height:32px;border-radius:8px;background:var(--accent-bg);border:1px solid var(--accent);display:flex;align-items:center;justify-content:center;font-family:var(--font-display);font-size:14px;font-weight:800;color:var(--accent)}
648
+ .logo-text{font-family:var(--font-display);font-size:15px;font-weight:700}
649
+ .tab-group{display:flex;gap:0}
650
+ .tab-btn{font-family:var(--font-sans);font-size:13px;font-weight:600;color:var(--text-muted);background:none;border:none;padding:16px 20px;cursor:pointer;position:relative;transition:color .2s;text-decoration:none}
651
+ .tab-btn:hover{color:var(--text-secondary)}
652
+ .tab-btn.active{color:var(--accent)}
653
+ .tab-btn.active::after{content:'';position:absolute;bottom:0;left:12px;right:12px;height:2px;background:var(--accent);border-radius:1px}
654
+ .topbar-right{margin-left:auto;display:flex;align-items:center;gap:12px}
655
+ .theme-toggle{width:34px;height:34px;border-radius:8px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .2s}
656
+ .theme-toggle:hover{border-color:var(--border-glow);color:var(--text-secondary)}
657
+
658
+ /* ── Canvas ── */
659
+ .canvas{position:fixed;top:56px;left:0;right:0;bottom:0;overflow:auto;padding:32px 40px}
660
+
661
+ /* ── Toolbar ── */
662
+ .toolbar{display:flex;align-items:center;gap:12px;margin-bottom:24px;flex-wrap:wrap}
663
+ .type-tabs{display:flex;gap:4px;background:var(--bg-card);border:1px solid var(--border-dim);border-radius:10px;padding:4px}
664
+ .type-tab{font-family:var(--font-sans);font-size:12px;font-weight:600;padding:6px 16px;border-radius:7px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s}
665
+ .type-tab.active{background:var(--accent-bg);color:var(--accent);border:1px solid var(--border-active)}
666
+ .search-wrap{flex:1;max-width:320px;position:relative}
667
+ .search-input{width:100%;background:var(--bg-input);border:1px solid var(--border-dim);border-radius:8px;padding:8px 12px 8px 32px;font-family:var(--font-sans);font-size:13px;color:var(--text-primary);outline:none;transition:border-color .2s}
668
+ .search-input:focus{border-color:var(--border-active)}
669
+ .search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);font-size:13px;color:var(--text-muted);pointer-events:none}
670
+ .filter-pills{display:flex;gap:6px;flex-wrap:wrap}
671
+ .pill{font-family:var(--font-mono);font-size:10px;padding:4px 10px;border-radius:6px;border:1px solid var(--border-dim);background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s;white-space:nowrap}
672
+ .pill:hover{border-color:var(--border-glow);color:var(--text-secondary)}
673
+ .pill.active{border-color:var(--accent);color:var(--accent);background:var(--accent-bg)}
674
+
675
+ /* ── Grid ── */
676
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
677
+
678
+ /* ── Card ── */
679
+ .card{background:var(--bg-card);border:1px solid var(--border-dim);border-radius:var(--radius);padding:18px;display:flex;flex-direction:column;gap:10px;transition:all .25s;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);position:relative}
680
+ .card:hover{border-color:var(--border-glow);box-shadow:var(--shadow),var(--shadow-glow);transform:translateY(-1px)}
681
+ .card.installed{border-color:rgba(74,222,128,0.2)}
682
+
683
+ .card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}
684
+ .card-name{font-family:var(--font-display);font-size:14px;font-weight:700;color:var(--text-primary);line-height:1.2}
685
+ .card-badge-wrap{display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0}
686
+ .badge{font-family:var(--font-mono);font-size:9px;font-weight:600;padding:3px 7px;border-radius:4px;white-space:nowrap;letter-spacing:.03em}
687
+ .badge-verified{background:var(--accent-bg);color:var(--accent);border:1px solid rgba(34,211,238,0.3)}
688
+ .badge-external{background:var(--bg-input);color:var(--text-muted);border:1px solid var(--border-dim)}
689
+ .badge-installed{background:var(--green-bg);color:var(--green);border:1px solid rgba(74,222,128,0.3)}
690
+ .badge-amber{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(251,191,36,0.3)}
691
+
692
+ .card-provider{font-family:var(--font-mono);font-size:10px;color:var(--text-muted)}
693
+ .card-desc{font-size:12px;color:var(--text-secondary);line-height:1.5;flex:1}
694
+ .card-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
695
+ .tag{font-family:var(--font-mono);font-size:9px;padding:2px 7px;border-radius:4px;background:var(--purple-bg);color:var(--purple);border:1px solid rgba(139,92,246,0.2)}
696
+ .assigned-to{font-family:var(--font-mono);font-size:9px;color:var(--text-muted)}
697
+
698
+ .card-actions{display:flex;gap:8px;margin-top:4px}
699
+ .btn{font-family:var(--font-sans);font-size:12px;font-weight:600;padding:7px 14px;border-radius:8px;border:none;cursor:pointer;transition:all .2s;flex:1}
700
+ .btn-install{background:var(--accent-bg);color:var(--accent);border:1px solid var(--border-active)}
701
+ .btn-install:hover{background:var(--accent);color:#000}
702
+ .btn-manage{background:transparent;color:var(--text-muted);border:1px solid var(--border-dim)}
703
+ .btn-manage:hover{border-color:var(--border-glow);color:var(--text-secondary)}
704
+ .btn-installing{background:var(--bg-input);color:var(--text-muted);border:1px solid var(--border-dim);cursor:default}
705
+
706
+ /* ── Empty state ── */
707
+ .empty{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:300px;gap:12px;color:var(--text-muted)}
708
+ .empty-icon{font-size:40px;opacity:.4}
709
+ .empty-text{font-size:14px}
710
+
711
+ /* ── Toast ── */
712
+ .toast{position:fixed;bottom:24px;right:24px;background:var(--bg-card);border:1px solid var(--border-glow);border-radius:10px;padding:12px 18px;font-size:13px;color:var(--text-primary);box-shadow:var(--shadow);z-index:300;opacity:0;transform:translateY(8px);transition:all .3s;pointer-events:none}
713
+ .toast.show{opacity:1;transform:translateY(0)}
714
+ .toast.toast-error{border-color:rgba(248,113,113,0.4);color:var(--red)}
715
+ .toast.toast-success{border-color:rgba(74,222,128,0.4);color:var(--green)}
716
+
717
+ /* ── Modal backdrop ── */
718
+ .modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(4px);z-index:200;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .25s}
719
+ .modal-backdrop.open{opacity:1;pointer-events:all}
720
+ .modal{background:var(--bg-surface);border:1px solid var(--border-glow);border-radius:16px;padding:28px;width:420px;max-width:90vw;max-height:80vh;overflow-y:auto;box-shadow:0 8px 40px rgba(0,0,0,0.5);transform:scale(.96);transition:transform .25s}
721
+ .modal-backdrop.open .modal{transform:scale(1)}
722
+ .modal-title{font-family:var(--font-display);font-size:16px;font-weight:700;margin-bottom:4px}
723
+ .modal-subtitle{font-size:13px;color:var(--text-muted);margin-bottom:20px}
724
+ .modal-section-label{font-family:var(--font-mono);font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.07em;color:var(--text-muted);margin-bottom:10px}
725
+ .agent-list{display:flex;flex-direction:column;gap:6px;max-height:260px;overflow-y:auto;margin-bottom:20px}
726
+ .agent-check{display:flex;align-items:center;gap:10px;padding:10px 12px;border-radius:8px;border:1px solid var(--border-dim);cursor:pointer;transition:all .2s}
727
+ .agent-check:hover{border-color:var(--border-glow);background:var(--accent-bg)}
728
+ .agent-check input[type=checkbox]{accent-color:var(--accent);width:14px;height:14px;cursor:pointer}
729
+ .agent-check-name{font-size:13px;font-weight:500}
730
+ .agent-check-id{font-family:var(--font-mono);font-size:10px;color:var(--text-muted)}
731
+ .modal-actions{display:flex;gap:10px}
732
+ .btn-primary{background:var(--accent);color:#000;font-family:var(--font-sans);font-size:13px;font-weight:700;padding:10px 20px;border-radius:9px;border:none;cursor:pointer;transition:opacity .2s;flex:1}
733
+ .btn-primary:hover{opacity:.85}
734
+ .btn-secondary{background:transparent;color:var(--text-muted);font-family:var(--font-sans);font-size:13px;font-weight:600;padding:10px 20px;border-radius:9px;border:1px solid var(--border-dim);cursor:pointer;transition:all .2s}
735
+ .btn-secondary:hover{border-color:var(--border-glow);color:var(--text-secondary)}
736
+ .modal-warning{font-size:12px;color:var(--amber);background:var(--amber-bg);border:1px solid rgba(251,191,36,0.2);border-radius:8px;padding:10px 12px;margin-top:12px}
737
+ </style>
738
+ </head>
739
+ <body>
740
+
741
+ <!-- Topbar -->
742
+ <header class="topbar">
743
+ <div class="topbar-logo">
744
+ <div class="logo-mark">M</div>
745
+ <span class="logo-text">MyAIforOne</span>
746
+ </div>
747
+ <nav class="tab-group">
748
+ <a href="/" class="tab-btn">Home</a>
749
+ <a href="/org" class="tab-btn">Dashboard</a>
750
+ <a href="/marketplace" class="tab-btn active">Marketplace</a>
751
+ <a href="/activity" class="tab-btn">Activity</a>
752
+ </nav>
753
+ <div class="topbar-right">
754
+ <button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme">☀</button>
755
+ </div>
756
+ </header>
757
+
758
+ <main class="canvas">
759
+ <!-- Toolbar -->
760
+ <div class="toolbar">
761
+ <div class="type-tabs">
762
+ <button class="type-tab active" onclick="setType('mcps')">MCPs</button>
763
+ <button class="type-tab" onclick="setType('skills')">Skills</button>
764
+ <button class="type-tab" onclick="setType('agents')">Agents</button>
765
+ </div>
766
+ <div class="search-wrap">
767
+ <span class="search-icon">⌕</span>
768
+ <input class="search-input" type="text" placeholder="Search..." oninput="onSearch(this.value)" id="searchInput">
769
+ </div>
770
+ <div class="filter-pills" id="categoryPills"></div>
771
+ </div>
772
+
773
+ <!-- Grid -->
774
+ <div class="grid" id="grid"></div>
775
+ </main>
776
+
777
+ <!-- Assign Modal -->
778
+ <div class="modal-backdrop" id="assignModal">
779
+ <div class="modal">
780
+ <div class="modal-title" id="modalTitle">Installed!</div>
781
+ <div class="modal-subtitle" id="modalSubtitle">Assign to agents now? (optional)</div>
782
+ <div class="modal-section-label">Select agents</div>
783
+ <div class="agent-list" id="agentList"></div>
784
+ <div class="modal-actions">
785
+ <button class="btn-primary" onclick="submitAssign()">Assign selected</button>
786
+ <button class="btn-secondary" onclick="closeModal()">Skip for now</button>
787
+ </div>
788
+ <div class="modal-warning" id="modalWarning" style="display:none"></div>
789
+ </div>
790
+ </div>
791
+
792
+ <!-- Toast -->
793
+ <div class="toast" id="toast"></div>
794
+
795
+ <script>
796
+ // ── State ──────────────────────────────────────────────────────────
797
+ let currentType = 'mcps';
798
+ let allItems = [];
799
+ let agents = [];
800
+ let searchQuery = '';
801
+ let activeCat = 'all';
802
+ let pendingInstall = null; // { type, id, name }
803
+
804
+ // ── Init ───────────────────────────────────────────────────────────
805
+ async function init() {
806
+ // Restore theme
807
+ const saved = localStorage.getItem('theme');
808
+ if (saved) document.documentElement.setAttribute('data-theme', saved);
809
+
810
+ // Load agents for assign modal
811
+ try {
812
+ const res = await fetch('/api/dashboard');
813
+ const data = await res.json();
814
+ agents = data.agents || [];
815
+ } catch { agents = []; }
816
+
817
+ await loadType('mcps');
818
+ }
819
+
820
+ // ── Load a type ────────────────────────────────────────────────────
821
+ async function loadType(type) {
822
+ currentType = type;
823
+ searchQuery = '';
824
+ activeCat = 'all';
825
+ document.getElementById('searchInput').value = '';
826
+
827
+ // Update tab buttons
828
+ document.querySelectorAll('.type-tab').forEach(b => b.classList.remove('active'));
829
+ document.querySelectorAll('.type-tab')[['mcps','skills','agents'].indexOf(type)].classList.add('active');
830
+
831
+ try {
832
+ const res = await fetch(`/api/marketplace/${type}`);
833
+ const data = await res.json();
834
+ allItems = data.items || [];
835
+ } catch {
836
+ allItems = [];
837
+ }
838
+
839
+ buildCategoryPills();
840
+ render();
841
+ }
842
+
843
+ function setType(type) { loadType(type); }
844
+
845
+ // ── Search / filter ────────────────────────────────────────────────
846
+ function onSearch(q) { searchQuery = q.toLowerCase(); render(); }
847
+
848
+ function buildCategoryPills() {
849
+ const cats = ['all', ...new Set(allItems.map(i => i.category).filter(Boolean))];
850
+ const el = document.getElementById('categoryPills');
851
+ el.innerHTML = cats.map(c =>
852
+ `<button class="pill${c === activeCat ? ' active' : ''}" onclick="setCat('${c}')">${c}</button>`
853
+ ).join('');
854
+ }
855
+
856
+ function setCat(cat) {
857
+ activeCat = cat;
858
+ buildCategoryPills();
859
+ render();
860
+ }
861
+
862
+ // ── Render ─────────────────────────────────────────────────────────
863
+ function render() {
864
+ const grid = document.getElementById('grid');
865
+
866
+ const filtered = allItems.filter(item => {
867
+ const matchCat = activeCat === 'all' || item.category === activeCat;
868
+ const matchSearch = !searchQuery
869
+ || item.name?.toLowerCase().includes(searchQuery)
870
+ || item.description?.toLowerCase().includes(searchQuery)
871
+ || item.tags?.some(t => t.toLowerCase().includes(searchQuery))
872
+ || item.provider?.toLowerCase().includes(searchQuery);
873
+ return matchCat && matchSearch;
874
+ });
875
+
876
+ if (filtered.length === 0) {
877
+ grid.innerHTML = `<div class="empty" style="grid-column:1/-1">
878
+ <div class="empty-icon">◇</div>
879
+ <div class="empty-text">No items found</div>
880
+ </div>`;
881
+ return;
882
+ }
883
+
884
+ grid.innerHTML = filtered.map(item => renderCard(item)).join('');
885
+ }
886
+
887
+ function renderCard(item) {
888
+ const typeKey = currentType === 'mcps' ? 'mcp' : currentType === 'skills' ? 'skill' : 'agent';
889
+ const assigned = item.assignedTo?.length > 0
890
+ ? `<span class="assigned-to">on ${item.assignedTo.length} agent${item.assignedTo.length > 1 ? 's' : ''}</span>`
891
+ : '';
892
+ const tags = (item.tags || []).slice(0,2).map(t => `<span class="tag">${t}</span>`).join('');
893
+
894
+ const verifiedBadge = item.verified
895
+ ? `<span class="badge badge-verified">✓ verified</span>`
896
+ : `<span class="badge badge-external">${escHtml(item.provider || 'external')}</span>`;
897
+
898
+ const installedBadge = item.installed ? `<span class="badge badge-installed">✓ installed</span>` : '';
899
+
900
+ const actionBtn = item.installed
901
+ ? `<button class="btn btn-manage" onclick="openManage('${escHtml(item.id)}')">Manage</button>`
902
+ : `<button class="btn btn-install" id="install-${escHtml(item.id)}" onclick="installItem('${typeKey}','${escHtml(item.id)}','${escHtml(item.name)}')">+ Install</button>`;
903
+
904
+ return `
905
+ <div class="card${item.installed ? ' installed' : ''}">
906
+ <div class="card-top">
907
+ <div>
908
+ <div class="card-name">${escHtml(item.name)}</div>
909
+ <div class="card-provider">${escHtml(item.provider || '')}</div>
910
+ </div>
911
+ <div class="card-badge-wrap">${verifiedBadge}${installedBadge}</div>
912
+ </div>
913
+ <div class="card-desc">${escHtml(item.description || '')}</div>
914
+ <div class="card-meta">${tags}${assigned}</div>
915
+ <div class="card-actions">${actionBtn}</div>
916
+ </div>`;
917
+ }
918
+
919
+ // ── Install ────────────────────────────────────────────────────────
920
+ async function installItem(type, id, name) {
921
+ const btn = document.getElementById(`install-${id}`);
922
+ if (btn) { btn.textContent = 'Installing...'; btn.className = 'btn btn-installing'; btn.disabled = true; }
923
+
924
+ try {
925
+ const res = await fetch('/api/marketplace/install', {
926
+ method: 'POST',
927
+ headers: { 'Content-Type': 'application/json' },
928
+ body: JSON.stringify({ type, id }),
929
+ });
930
+ const data = await res.json();
931
+
932
+ if (!res.ok || !data.ok) {
933
+ toast(`Install failed: ${data.error || 'unknown error'}`, 'error');
934
+ if (btn) { btn.textContent = '+ Install'; btn.className = 'btn btn-install'; btn.disabled = false; }
935
+ return;
936
+ }
937
+
938
+ // Update local state
939
+ const item = allItems.find(i => i.id === id);
940
+ if (item) item.installed = true;
941
+
942
+ toast(`✓ ${name} installed`, 'success');
943
+ render();
944
+ openAssignModal(type, id, name, false);
945
+
946
+ } catch (err) {
947
+ toast(`Install error: ${err.message}`, 'error');
948
+ if (btn) { btn.textContent = '+ Install'; btn.className = 'btn btn-install'; btn.disabled = false; }
949
+ }
950
+ }
951
+
952
+ // ── Assign modal ───────────────────────────────────────────────────
953
+ function openAssignModal(type, id, name, isManage) {
954
+ pendingInstall = { type, id, name };
955
+
956
+ document.getElementById('modalTitle').textContent = isManage
957
+ ? `Manage — ${name}`
958
+ : `✓ ${name} installed`;
959
+ document.getElementById('modalSubtitle').textContent = isManage
960
+ ? 'Update which agents have this assigned.'
961
+ : 'Assign to agents now? (optional)';
962
+
963
+ const item = allItems.find(i => i.id === id);
964
+ const alreadyAssigned = item?.assignedTo || [];
965
+
966
+ // Build agent checklist
967
+ const listEl = document.getElementById('agentList');
968
+ listEl.innerHTML = agents.map(a => `
969
+ <label class="agent-check">
970
+ <input type="checkbox" value="${escHtml(a.id)}" ${alreadyAssigned.includes(a.id) ? 'checked' : ''}>
971
+ <div>
972
+ <div class="agent-check-name">${escHtml(a.name || a.id)}</div>
973
+ <div class="agent-check-id">${escHtml(a.id)}</div>
974
+ </div>
975
+ </label>`).join('');
976
+
977
+ document.getElementById('modalWarning').style.display = 'none';
978
+ document.getElementById('assignModal').classList.add('open');
979
+ }
980
+
981
+ function openManage(id) {
982
+ const item = allItems.find(i => i.id === id);
983
+ if (!item) return;
984
+ const typeKey = currentType === 'mcps' ? 'mcp' : currentType === 'skills' ? 'skill' : 'agent';
985
+ openAssignModal(typeKey, id, item.name, true);
986
+ }
987
+
988
+ function closeModal() {
989
+ document.getElementById('assignModal').classList.remove('open');
990
+ pendingInstall = null;
991
+ }
992
+
993
+ async function submitAssign() {
994
+ if (!pendingInstall) return;
995
+ const { type, id, name } = pendingInstall;
996
+
997
+ const checked = [...document.querySelectorAll('#agentList input[type=checkbox]:checked')]
998
+ .map(el => el.value);
999
+
1000
+ if (checked.length === 0) { closeModal(); return; }
1001
+
1002
+ try {
1003
+ const res = await fetch('/api/marketplace/assign', {
1004
+ method: 'POST',
1005
+ headers: { 'Content-Type': 'application/json' },
1006
+ body: JSON.stringify({ type, id, agentIds: checked }),
1007
+ });
1008
+ const data = await res.json();
1009
+
1010
+ if (!res.ok || !data.ok) {
1011
+ toast(`Assign failed: ${data.error || 'unknown'}`, 'error');
1012
+ return;
1013
+ }
1014
+
1015
+ // Update local assigned state
1016
+ const item = allItems.find(i => i.id === id);
1017
+ if (item) item.assignedTo = checked;
1018
+
1019
+ if (data.missingKeys?.length > 0) {
1020
+ document.getElementById('modalWarning').style.display = 'block';
1021
+ document.getElementById('modalWarning').textContent =
1022
+ `⚠ API key needed for: ${data.missingKeys.join(', ')} — add key in agent config to activate.`;
1023
+ render();
1024
+ return; // Keep modal open to show warning
1025
+ }
1026
+
1027
+ toast(`✓ ${name} assigned to ${checked.length} agent${checked.length > 1 ? 's' : ''}`, 'success');
1028
+ render();
1029
+ closeModal();
1030
+
1031
+ } catch (err) {
1032
+ toast(`Assign error: ${err.message}`, 'error');
1033
+ }
1034
+ }
1035
+
1036
+ // Close modal on backdrop click
1037
+ document.getElementById('assignModal').addEventListener('click', function(e) {
1038
+ if (e.target === this) closeModal();
1039
+ });
1040
+
1041
+ // ── Theme ──────────────────────────────────────────────────────────
1042
+ function toggleTheme() {
1043
+ const current = document.documentElement.getAttribute('data-theme');
1044
+ const next = current === 'light' ? 'dark' : 'light';
1045
+ if (next === 'dark') document.documentElement.removeAttribute('data-theme');
1046
+ else document.documentElement.setAttribute('data-theme', 'light');
1047
+ localStorage.setItem('theme', next === 'dark' ? '' : 'light');
1048
+ }
1049
+
1050
+ // ── Toast ──────────────────────────────────────────────────────────
1051
+ let toastTimer;
1052
+ function toast(msg, type = 'success') {
1053
+ const el = document.getElementById('toast');
1054
+ el.textContent = msg;
1055
+ el.className = `toast toast-${type} show`;
1056
+ clearTimeout(toastTimer);
1057
+ toastTimer = setTimeout(() => el.classList.remove('show'), 3500);
1058
+ }
1059
+
1060
+ // ── Util ───────────────────────────────────────────────────────────
1061
+ function escHtml(s) {
1062
+ return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
1063
+ }
1064
+
1065
+ init();
1066
+ </script>
1067
+ </body>
1068
+ </html>
1069
+ ```
1070
+
1071
+ - [ ] **Step 2: Verify the file was written correctly**
1072
+
1073
+ ```bash
1074
+ wc -l ~/Desktop/APPs/channelToAgentToClaude/public/marketplace.html
1075
+ ```
1076
+
1077
+ Expected: 250+ lines
1078
+
1079
+ - [ ] **Step 3: Restart service and open the page**
1080
+
1081
+ ```bash
1082
+ launchctl kickstart -k gui/$(id -u)/com.agenticledger.channelToAgentToClaude
1083
+ sleep 3
1084
+ open http://localhost:4888/marketplace
1085
+ ```
1086
+
1087
+ Expected: Marketplace page loads with MCPs tab showing cards from registry.
1088
+
1089
+ - [ ] **Step 4: Commit frontend**
1090
+
1091
+ ```bash
1092
+ git add public/marketplace.html
1093
+ git commit -m "feat: add marketplace.html — browse, install, assign MCPs/skills/agents"
1094
+ ```
1095
+
1096
+ ---
1097
+
1098
+ ## Task 4: Add Marketplace Nav Link to All Existing Pages
1099
+
1100
+ **Files:**
1101
+ - Modify: `public/org.html`
1102
+ - Modify: `public/activity.html`
1103
+ - Modify: `public/home.html`
1104
+ - Modify: `public/index.html`
1105
+
1106
+ Each page has its own nav pattern. Match the existing style exactly.
1107
+
1108
+ - [ ] **Step 1: org.html — add Marketplace tab**
1109
+
1110
+ In `public/org.html`, find the `tab-group` nav section. It looks like:
1111
+ ```html
1112
+ <a href="/" class="tab-btn">Home</a>
1113
+ ```
1114
+ Add `<a href="/marketplace" class="tab-btn">Marketplace</a>` after the Dashboard/Org link.
1115
+
1116
+ - [ ] **Step 2: activity.html — add Marketplace nav link**
1117
+
1118
+ In `public/activity.html`, find the nav links section (uses `.nav-link` class). Add:
1119
+ ```html
1120
+ <a href="/marketplace" class="nav-link">Marketplace</a>
1121
+ ```
1122
+ alongside the other nav links.
1123
+
1124
+ - [ ] **Step 3: home.html — add Marketplace nav link**
1125
+
1126
+ Find the nav links in home.html and add the Marketplace link using the same class/style as other nav links on that page.
1127
+
1128
+ - [ ] **Step 4: index.html — add Marketplace nav link**
1129
+
1130
+ Find the nav links in index.html and add the Marketplace link using the same class/style.
1131
+
1132
+ - [ ] **Step 5: Verify nav links appear on all pages**
1133
+
1134
+ Open each page and confirm "Marketplace" appears in the nav:
1135
+ ```bash
1136
+ open http://localhost:4888/org
1137
+ open http://localhost:4888/activity
1138
+ open http://localhost:4888/
1139
+ ```
1140
+
1141
+ - [ ] **Step 6: Commit nav updates**
1142
+
1143
+ ```bash
1144
+ git add public/org.html public/activity.html public/home.html public/index.html
1145
+ git commit -m "feat: add Marketplace nav link to all pages"
1146
+ ```
1147
+
1148
+ ---
1149
+
1150
+ ## Task 5: Tests
1151
+
1152
+ **Files:**
1153
+ - Modify: `Comprehensive Test Suite/web-ui/api.test.ts`
1154
+
1155
+ - [ ] **Step 1: Add marketplace API tests**
1156
+
1157
+ Append to `Comprehensive Test Suite/web-ui/api.test.ts`:
1158
+
1159
+ ```typescript
1160
+ it("GET /api/marketplace/mcps returns items with installed/assignedTo fields", async () => {
1161
+ try {
1162
+ const res = await fetch(`${BASE}/api/marketplace/mcps`);
1163
+ if (!res.ok) return;
1164
+ const data = await res.json() as any;
1165
+ assert.ok(Array.isArray(data.items), "items should be an array");
1166
+ if (data.items.length > 0) {
1167
+ const item = data.items[0];
1168
+ assert.ok("id" in item, "item should have id");
1169
+ assert.ok("name" in item, "item should have name");
1170
+ assert.ok("installed" in item, "item should have installed field");
1171
+ assert.ok(Array.isArray(item.assignedTo), "item.assignedTo should be array");
1172
+ }
1173
+ } catch { /* service not running */ }
1174
+ });
1175
+
1176
+ it("GET /api/marketplace/skills returns items", async () => {
1177
+ try {
1178
+ const res = await fetch(`${BASE}/api/marketplace/skills`);
1179
+ if (!res.ok) return;
1180
+ const data = await res.json() as any;
1181
+ assert.ok(Array.isArray(data.items));
1182
+ } catch { /* service not running */ }
1183
+ });
1184
+
1185
+ it("GET /api/marketplace/agents returns items", async () => {
1186
+ try {
1187
+ const res = await fetch(`${BASE}/api/marketplace/agents`);
1188
+ if (!res.ok) return;
1189
+ const data = await res.json() as any;
1190
+ assert.ok(Array.isArray(data.items));
1191
+ } catch { /* service not running */ }
1192
+ });
1193
+
1194
+ it("GET /api/marketplace/invalid returns 400", async () => {
1195
+ try {
1196
+ const res = await fetch(`${BASE}/api/marketplace/invalid`);
1197
+ assert.equal(res.status, 400);
1198
+ } catch { /* service not running */ }
1199
+ });
1200
+
1201
+ it("POST /api/marketplace/install rejects missing fields", async () => {
1202
+ try {
1203
+ const res = await fetch(`${BASE}/api/marketplace/install`, {
1204
+ method: "POST",
1205
+ headers: { "Content-Type": "application/json" },
1206
+ body: JSON.stringify({ type: "skill" }), // missing id
1207
+ });
1208
+ assert.equal(res.status, 400);
1209
+ } catch { /* service not running */ }
1210
+ });
1211
+
1212
+ it("POST /api/marketplace/assign rejects missing fields", async () => {
1213
+ try {
1214
+ const res = await fetch(`${BASE}/api/marketplace/assign`, {
1215
+ method: "POST",
1216
+ headers: { "Content-Type": "application/json" },
1217
+ body: JSON.stringify({ type: "skill", id: "sop_pptx" }), // missing agentIds
1218
+ });
1219
+ assert.equal(res.status, 400);
1220
+ } catch { /* service not running */ }
1221
+ });
1222
+ ```
1223
+
1224
+ - [ ] **Step 2: Run the test suite**
1225
+
1226
+ ```bash
1227
+ cd ~/Desktop/APPs/channelToAgentToClaude && node "Comprehensive Test Suite/run-all-tests.js" 2>&1
1228
+ ```
1229
+
1230
+ Expected: all tests pass (marketplace tests pass or skip if service not running).
1231
+
1232
+ - [ ] **Step 3: Commit tests**
1233
+
1234
+ ```bash
1235
+ git add "Comprehensive Test Suite/web-ui/api.test.ts"
1236
+ git commit -m "test: add marketplace API endpoint tests"
1237
+ ```
1238
+
1239
+ ---
1240
+
1241
+ ## Task 6: Final Build, Restart, and Push
1242
+
1243
+ - [ ] **Step 1: Final build**
1244
+
1245
+ ```bash
1246
+ cd ~/Desktop/APPs/channelToAgentToClaude && npm run build 2>&1
1247
+ ```
1248
+
1249
+ Expected: clean build.
1250
+
1251
+ - [ ] **Step 2: Restart service**
1252
+
1253
+ ```bash
1254
+ launchctl kickstart -k gui/$(id -u)/com.agenticledger.channelToAgentToClaude
1255
+ sleep 3 && tail -5 logs/service.log
1256
+ ```
1257
+
1258
+ Expected: service running line visible.
1259
+
1260
+ - [ ] **Step 3: Smoke test the full flow**
1261
+
1262
+ ```bash
1263
+ # Marketplace page loads
1264
+ curl -s http://localhost:4888/marketplace | grep -c "Marketplace"
1265
+
1266
+ # MCPs endpoint returns items
1267
+ curl -s http://localhost:4888/api/marketplace/mcps | node -e "const d=require('fs').readFileSync('/dev/stdin','utf-8');const j=JSON.parse(d);console.log('items:',j.items?.length)"
1268
+
1269
+ # Skills endpoint returns items
1270
+ curl -s http://localhost:4888/api/marketplace/skills | node -e "const d=require('fs').readFileSync('/dev/stdin','utf-8');const j=JSON.parse(d);console.log('skills:',j.items?.length)"
1271
+ ```
1272
+
1273
+ Expected: `items: 47` for MCPs, skills count > 0.
1274
+
1275
+ - [ ] **Step 4: Push to both repos**
1276
+
1277
+ ```bash
1278
+ cd ~/Desktop/APPs/channelToAgentToClaude
1279
+ git push origin main
1280
+ git push client main
1281
+ ```