feed-the-machine 1.6.1 → 1.7.1

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 (272) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +262 -170
  3. package/bin/__pycache__/tasks_db.cpython-314.pyc +0 -0
  4. package/bin/brain.py +1340 -0
  5. package/bin/convert_claude_skills_to_codex.py +490 -0
  6. package/bin/generate-manifest.mjs +463 -463
  7. package/bin/harden_codex_skills.py +141 -0
  8. package/bin/install.mjs +491 -491
  9. package/bin/migrate-eng-buddy-data.py +875 -0
  10. package/bin/playbook_engine/__init__.py +1 -0
  11. package/bin/playbook_engine/conftest.py +8 -0
  12. package/bin/playbook_engine/extractor.py +33 -0
  13. package/bin/playbook_engine/manager.py +102 -0
  14. package/bin/playbook_engine/models.py +84 -0
  15. package/bin/playbook_engine/registry.py +35 -0
  16. package/bin/playbook_engine/test_extractor.py +72 -0
  17. package/bin/playbook_engine/test_integration.py +129 -0
  18. package/bin/playbook_engine/test_manager.py +85 -0
  19. package/bin/playbook_engine/test_models.py +166 -0
  20. package/bin/playbook_engine/test_registry.py +67 -0
  21. package/bin/playbook_engine/test_tracer.py +86 -0
  22. package/bin/playbook_engine/tracer.py +93 -0
  23. package/bin/tasks_db.py +456 -0
  24. package/docs/HOOKS.md +243 -243
  25. package/docs/INBOX.md +233 -233
  26. package/ftm/SKILL.md +125 -122
  27. package/ftm-audit/SKILL.md +673 -623
  28. package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
  29. package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
  30. package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
  31. package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
  32. package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
  33. package/ftm-audit/scripts/run-knip.sh +23 -23
  34. package/ftm-audit.yml +2 -2
  35. package/ftm-brainstorm/SKILL.md +1003 -498
  36. package/ftm-brainstorm/evals/evals.json +180 -100
  37. package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
  38. package/ftm-brainstorm/references/agent-prompts.md +552 -224
  39. package/ftm-brainstorm/references/plan-template.md +209 -121
  40. package/ftm-brainstorm.yml +2 -2
  41. package/ftm-browse/SKILL.md +454 -454
  42. package/ftm-browse/daemon/browser-manager.ts +206 -206
  43. package/ftm-browse/daemon/bun.lock +30 -30
  44. package/ftm-browse/daemon/cli.ts +347 -347
  45. package/ftm-browse/daemon/commands.ts +410 -410
  46. package/ftm-browse/daemon/main.ts +357 -357
  47. package/ftm-browse/daemon/package.json +17 -17
  48. package/ftm-browse/daemon/server.ts +189 -189
  49. package/ftm-browse/daemon/snapshot.ts +519 -519
  50. package/ftm-browse/daemon/tsconfig.json +22 -22
  51. package/ftm-browse.yml +4 -4
  52. package/ftm-capture/SKILL.md +370 -370
  53. package/ftm-capture.yml +4 -4
  54. package/ftm-codex-gate/SKILL.md +361 -361
  55. package/ftm-codex-gate.yml +2 -2
  56. package/ftm-config/SKILL.md +422 -345
  57. package/ftm-config.default.yml +125 -82
  58. package/ftm-config.yml +44 -2
  59. package/ftm-council/SKILL.md +416 -416
  60. package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
  61. package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
  62. package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
  63. package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
  64. package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
  65. package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
  66. package/ftm-council-chat.yml +2 -0
  67. package/ftm-council.yml +2 -2
  68. package/ftm-dashboard/SKILL.md +163 -163
  69. package/ftm-dashboard.yml +4 -4
  70. package/ftm-debug/SKILL.md +1037 -1037
  71. package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
  72. package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
  73. package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
  74. package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
  75. package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
  76. package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
  77. package/ftm-debug.yml +2 -2
  78. package/ftm-diagram/SKILL.md +277 -277
  79. package/ftm-diagram.yml +2 -2
  80. package/ftm-executor/SKILL.md +777 -777
  81. package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
  82. package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
  83. package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
  84. package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
  85. package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +81 -72
  86. package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
  87. package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
  88. package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
  89. package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
  90. package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
  91. package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
  92. package/ftm-executor/runtime/package.json +8 -8
  93. package/ftm-executor.yml +2 -2
  94. package/ftm-git/SKILL.md +441 -441
  95. package/ftm-git/evals/evals.json +26 -26
  96. package/ftm-git/evals/promptfoo.yaml +75 -75
  97. package/ftm-git/hooks/post-commit-experience.sh +92 -92
  98. package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
  99. package/ftm-git/references/protocols/REMEDIATION.md +139 -139
  100. package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
  101. package/ftm-git.yml +2 -2
  102. package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
  103. package/ftm-inbox/backend/adapters/_retry.py +64 -64
  104. package/ftm-inbox/backend/adapters/base.py +230 -230
  105. package/ftm-inbox/backend/adapters/freshservice.py +104 -104
  106. package/ftm-inbox/backend/adapters/gmail.py +125 -125
  107. package/ftm-inbox/backend/adapters/jira.py +136 -136
  108. package/ftm-inbox/backend/adapters/registry.py +192 -192
  109. package/ftm-inbox/backend/adapters/slack.py +110 -110
  110. package/ftm-inbox/backend/db/connection.py +54 -54
  111. package/ftm-inbox/backend/db/schema.py +78 -78
  112. package/ftm-inbox/backend/executor/__init__.py +7 -7
  113. package/ftm-inbox/backend/executor/engine.py +149 -149
  114. package/ftm-inbox/backend/executor/step_runner.py +98 -98
  115. package/ftm-inbox/backend/main.py +103 -103
  116. package/ftm-inbox/backend/models/__init__.py +1 -1
  117. package/ftm-inbox/backend/models/unified_task.py +36 -36
  118. package/ftm-inbox/backend/planner/__init__.py +6 -6
  119. package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
  120. package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
  121. package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
  122. package/ftm-inbox/backend/planner/generator.py +127 -127
  123. package/ftm-inbox/backend/planner/schema.py +34 -34
  124. package/ftm-inbox/backend/requirements.txt +5 -5
  125. package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
  126. package/ftm-inbox/backend/routes/execute.py +186 -186
  127. package/ftm-inbox/backend/routes/health.py +52 -52
  128. package/ftm-inbox/backend/routes/inbox.py +68 -68
  129. package/ftm-inbox/backend/routes/plan.py +271 -271
  130. package/ftm-inbox/bin/launchagent.mjs +91 -91
  131. package/ftm-inbox/bin/setup.mjs +188 -188
  132. package/ftm-inbox/bin/start.sh +10 -10
  133. package/ftm-inbox/bin/status.sh +17 -17
  134. package/ftm-inbox/bin/stop.sh +8 -8
  135. package/ftm-inbox/config.example.yml +55 -55
  136. package/ftm-inbox/package-lock.json +2898 -2898
  137. package/ftm-inbox/package.json +26 -26
  138. package/ftm-inbox/postcss.config.js +6 -6
  139. package/ftm-inbox/src/app.css +199 -199
  140. package/ftm-inbox/src/app.html +18 -18
  141. package/ftm-inbox/src/lib/api.ts +166 -166
  142. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
  143. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
  144. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
  145. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
  146. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
  147. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
  148. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
  149. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
  150. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
  151. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
  152. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
  153. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
  154. package/ftm-inbox/src/lib/theme.ts +47 -47
  155. package/ftm-inbox/src/routes/+layout.svelte +76 -76
  156. package/ftm-inbox/src/routes/+page.svelte +401 -401
  157. package/ftm-inbox/svelte.config.js +12 -12
  158. package/ftm-inbox/tailwind.config.ts +63 -63
  159. package/ftm-inbox/tsconfig.json +13 -13
  160. package/ftm-inbox/vite.config.ts +6 -6
  161. package/ftm-intent/SKILL.md +241 -241
  162. package/ftm-intent.yml +2 -2
  163. package/ftm-manifest.json +3794 -3794
  164. package/ftm-map/SKILL.md +291 -291
  165. package/ftm-map/scripts/db.py +712 -712
  166. package/ftm-map/scripts/index.py +415 -415
  167. package/ftm-map/scripts/parser.py +224 -224
  168. package/ftm-map/scripts/queries/go-tags.scm +20 -20
  169. package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
  170. package/ftm-map/scripts/queries/python-tags.scm +31 -31
  171. package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
  172. package/ftm-map/scripts/queries/rust-tags.scm +37 -37
  173. package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
  174. package/ftm-map/scripts/query.py +301 -301
  175. package/ftm-map/scripts/ranker.py +377 -377
  176. package/ftm-map/scripts/requirements.txt +5 -5
  177. package/ftm-map/scripts/setup-hooks.sh +27 -27
  178. package/ftm-map/scripts/setup.sh +56 -56
  179. package/ftm-map/scripts/test_db.py +364 -364
  180. package/ftm-map/scripts/test_parser.py +174 -174
  181. package/ftm-map/scripts/test_query.py +183 -183
  182. package/ftm-map/scripts/test_ranker.py +199 -199
  183. package/ftm-map/scripts/views.py +591 -591
  184. package/ftm-map.yml +2 -2
  185. package/ftm-mind/SKILL.md +201 -1943
  186. package/ftm-mind/evals/promptfoo.yaml +142 -142
  187. package/ftm-mind/references/blackboard-protocol.md +110 -0
  188. package/ftm-mind/references/blackboard-schema.md +328 -328
  189. package/ftm-mind/references/complexity-guide.md +110 -110
  190. package/ftm-mind/references/complexity-sizing.md +138 -0
  191. package/ftm-mind/references/decide-act-protocol.md +172 -0
  192. package/ftm-mind/references/direct-execution.md +51 -0
  193. package/ftm-mind/references/environment-discovery.md +77 -0
  194. package/ftm-mind/references/event-registry.md +319 -319
  195. package/ftm-mind/references/mcp-inventory.md +300 -296
  196. package/ftm-mind/references/ops-routing.md +47 -0
  197. package/ftm-mind/references/orient-protocol.md +234 -0
  198. package/ftm-mind/references/personality.md +40 -0
  199. package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
  200. package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
  201. package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
  202. package/ftm-mind/references/reflexion-protocol.md +249 -249
  203. package/ftm-mind/references/routing/SCENARIOS.md +22 -22
  204. package/ftm-mind/references/routing-scenarios.md +35 -35
  205. package/ftm-mind.yml +2 -2
  206. package/ftm-ops.yml +4 -0
  207. package/ftm-pause/SKILL.md +395 -395
  208. package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
  209. package/ftm-pause/references/protocols/VALIDATION.md +80 -80
  210. package/ftm-pause.yml +2 -2
  211. package/ftm-researcher/SKILL.md +275 -275
  212. package/ftm-researcher/evals/agent-diversity.yaml +17 -17
  213. package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
  214. package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
  215. package/ftm-researcher/references/adaptive-search.md +116 -116
  216. package/ftm-researcher/references/agent-prompts.md +193 -193
  217. package/ftm-researcher/references/council-integration.md +193 -193
  218. package/ftm-researcher/references/output-format.md +203 -203
  219. package/ftm-researcher/references/synthesis-pipeline.md +165 -165
  220. package/ftm-researcher/scripts/score_credibility.py +234 -234
  221. package/ftm-researcher/scripts/validate_research.py +92 -92
  222. package/ftm-researcher.yml +2 -2
  223. package/ftm-resume/SKILL.md +518 -518
  224. package/ftm-resume/references/protocols/VALIDATION.md +172 -172
  225. package/ftm-resume.yml +2 -2
  226. package/ftm-retro/SKILL.md +380 -380
  227. package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
  228. package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
  229. package/ftm-retro.yml +2 -2
  230. package/ftm-routine/SKILL.md +170 -170
  231. package/ftm-routine.yml +4 -4
  232. package/ftm-state/blackboard/capabilities.json +5 -5
  233. package/ftm-state/blackboard/capabilities.schema.json +27 -27
  234. package/ftm-state/blackboard/context.json +37 -23
  235. package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
  236. package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
  237. package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
  238. package/ftm-state/blackboard/experiences/index.json +58 -9
  239. package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
  240. package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
  241. package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
  242. package/ftm-state/blackboard/patterns.json +6 -6
  243. package/ftm-state/schemas/context.schema.json +130 -130
  244. package/ftm-state/schemas/experience-index.schema.json +77 -77
  245. package/ftm-state/schemas/experience.schema.json +78 -78
  246. package/ftm-state/schemas/patterns.schema.json +44 -44
  247. package/ftm-upgrade/SKILL.md +194 -194
  248. package/ftm-upgrade/scripts/check-version.sh +76 -76
  249. package/ftm-upgrade/scripts/upgrade.sh +143 -143
  250. package/ftm-upgrade.yml +2 -2
  251. package/ftm-verify.yml +2 -2
  252. package/ftm.yml +2 -2
  253. package/hooks/ftm-auto-log.sh +137 -0
  254. package/hooks/ftm-blackboard-enforcer.sh +93 -93
  255. package/hooks/ftm-discovery-reminder.sh +90 -90
  256. package/hooks/ftm-drafts-gate.sh +61 -61
  257. package/hooks/ftm-event-logger.mjs +107 -107
  258. package/hooks/ftm-install-hooks.sh +240 -0
  259. package/hooks/ftm-learning-capture.sh +117 -0
  260. package/hooks/ftm-map-autodetect.sh +79 -79
  261. package/hooks/ftm-pending-sync-check.sh +22 -22
  262. package/hooks/ftm-plan-gate.sh +92 -92
  263. package/hooks/ftm-post-commit-trigger.sh +57 -57
  264. package/hooks/ftm-post-compaction.sh +138 -0
  265. package/hooks/ftm-pre-compaction.sh +147 -0
  266. package/hooks/ftm-session-end.sh +52 -0
  267. package/hooks/ftm-session-snapshot.sh +213 -0
  268. package/hooks/ftm-task-loader.sh +100 -0
  269. package/hooks/settings-template.json +91 -81
  270. package/install.sh +363 -363
  271. package/package.json +84 -84
  272. package/uninstall.sh +25 -25
package/bin/install.mjs CHANGED
@@ -1,491 +1,491 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * npx feed-the-machine — installs ftm skills into ~/.claude/skills/
5
- *
6
- * Full install: skills, hooks, settings.json merge, and verification.
7
- * Safe to re-run — idempotent.
8
- *
9
- * Flags:
10
- * --with-inbox Also install the inbox service
11
- * --no-hooks Skip hooks entirely
12
- * --skip-merge Install hook files but don't touch settings.json
13
- */
14
-
15
- import { existsSync, mkdirSync, readdirSync, lstatSync, readFileSync, writeFileSync, copyFileSync, symlinkSync, unlinkSync, chmodSync, cpSync } from "fs";
16
- import { join, basename, dirname } from "path";
17
- import { homedir, platform } from "os";
18
- import { fileURLToPath } from "url";
19
- import { execSync, spawnSync } from "child_process";
20
-
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = dirname(__filename);
23
- const REPO_DIR = dirname(__dirname); // package root (one level up from bin/)
24
- const HOME = homedir();
25
- const SKILLS_DIR = join(HOME, ".claude", "skills");
26
- const STATE_DIR = join(HOME, ".claude", "ftm-state");
27
- const CONFIG_DIR = join(HOME, ".claude");
28
- const HOOKS_DIR = join(HOME, ".claude", "hooks");
29
- const SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
30
- const INBOX_INSTALL_DIR = join(HOME, ".claude", "ftm-inbox");
31
-
32
- const ARGS = process.argv.slice(2);
33
- const WITH_INBOX = ARGS.includes("--with-inbox");
34
- const NO_HOOKS = ARGS.includes("--no-hooks");
35
- const SKIP_MERGE = ARGS.includes("--skip-merge");
36
-
37
- let warnCount = 0;
38
-
39
- function log(msg) {
40
- console.log(` ${msg}`);
41
- }
42
-
43
- function warn(msg) {
44
- console.log(` WARN: ${msg}`);
45
- warnCount++;
46
- }
47
-
48
- function ensureDir(dir) {
49
- if (!existsSync(dir)) {
50
- mkdirSync(dir, { recursive: true });
51
- }
52
- }
53
-
54
- function safeSymlink(src, dest) {
55
- const name = basename(dest);
56
- try {
57
- if (lstatSync(dest).isSymbolicLink()) {
58
- unlinkSync(dest);
59
- } else if (existsSync(dest)) {
60
- log(`SKIP ${name} (real file/dir exists — back it up first)`);
61
- return;
62
- }
63
- } catch {
64
- // dest doesn't exist, that's fine
65
- }
66
- symlinkSync(src, dest);
67
- log(`LINK ${name}`);
68
- }
69
-
70
- function commandExists(cmd) {
71
- try {
72
- execSync(`command -v ${cmd}`, { stdio: "ignore" });
73
- return true;
74
- } catch {
75
- return false;
76
- }
77
- }
78
-
79
- function commandVersion(cmd, flag = "--version") {
80
- try {
81
- return execSync(`${cmd} ${flag}`, { encoding: "utf8" }).trim().split("\n")[0];
82
- } catch {
83
- return "unknown";
84
- }
85
- }
86
-
87
- // --- Preflight ---
88
-
89
- function preflight() {
90
- console.log("Preflight checks...");
91
-
92
- if (!NO_HOOKS) {
93
- // jq is required for all shell hooks (they parse JSON stdin via jq)
94
- if (!commandExists("jq")) {
95
- console.log("");
96
- console.log(" ERROR: jq is required for FTM hooks.");
97
- console.log("");
98
- console.log(" Install it:");
99
- console.log(" macOS: brew install jq");
100
- console.log(" Ubuntu: sudo apt-get install jq");
101
- console.log(" Alpine: apk add jq");
102
- console.log("");
103
- console.log(" Or skip hooks: npx feed-the-machine --no-hooks");
104
- process.exit(1);
105
- }
106
- log(`jq: ${commandVersion("jq")}`);
107
- log(`node: ${process.version}`);
108
- } else {
109
- log("hooks skipped (--no-hooks)");
110
- }
111
-
112
- console.log("");
113
- }
114
-
115
- // --- Settings Merge ---
116
-
117
- function mergeHooksIntoSettings() {
118
- const templatePath = join(REPO_DIR, "hooks", "settings-template.json");
119
- if (!existsSync(templatePath)) {
120
- warn("hooks/settings-template.json not found — hooks installed but not registered");
121
- return;
122
- }
123
-
124
- console.log("");
125
- console.log("Registering hooks in settings.json...");
126
-
127
- // Read and expand ~ to actual home directory
128
- const rawTemplate = readFileSync(templatePath, "utf8");
129
- const expandedTemplate = rawTemplate.replace(/~\/.claude/g, join(HOME, ".claude"));
130
- const template = JSON.parse(expandedTemplate);
131
- const templateHooks = template.hooks || {};
132
-
133
- if (!existsSync(SETTINGS_FILE)) {
134
- // No settings.json — create one with just the hooks
135
- writeFileSync(SETTINGS_FILE, JSON.stringify({ hooks: templateHooks }, null, 2) + "\n");
136
- log("CREATED settings.json with FTM hooks");
137
- return;
138
- }
139
-
140
- // Read existing settings
141
- const existing = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
142
-
143
- // Backup
144
- const ts = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
145
- const backupPath = `${SETTINGS_FILE}.ftm-backup-${ts}`;
146
- copyFileSync(SETTINGS_FILE, backupPath);
147
- log(`BACKUP ${backupPath}`);
148
-
149
- // Ensure hooks key exists
150
- if (!existing.hooks) {
151
- existing.hooks = {};
152
- }
153
-
154
- // Merge each event type
155
- const events = ["PreToolUse", "UserPromptSubmit", "PostToolUse", "Stop"];
156
- for (const event of events) {
157
- const templateEntries = templateHooks[event] || [];
158
- const existingEntries = existing.hooks[event] || [];
159
-
160
- if (templateEntries.length === 0) continue;
161
-
162
- // Check if FTM hooks are already present by looking for ftm- in command paths
163
- const existingCommands = JSON.stringify(existingEntries);
164
- const alreadyPresent = templateEntries.some((entry) => {
165
- const hooks = entry.hooks || [];
166
- return hooks.some((h) => {
167
- const cmd = h.command || "";
168
- const cmdBase = basename(cmd.split(" ").pop()); // handle "node foo.mjs"
169
- return existingCommands.includes(cmdBase);
170
- });
171
- });
172
-
173
- if (alreadyPresent) {
174
- log(`SKIP ${event} hooks (already configured)`);
175
- continue;
176
- }
177
-
178
- existing.hooks[event] = [...existingEntries, ...templateEntries];
179
- log(`MERGE ${event} hooks`);
180
- }
181
-
182
- writeFileSync(SETTINGS_FILE, JSON.stringify(existing, null, 2) + "\n");
183
- log("UPDATED settings.json");
184
- console.log("");
185
- log("Hooks are active.");
186
- }
187
-
188
- // --- Verification ---
189
-
190
- function verify(skillCount, hookCount) {
191
- console.log("");
192
- console.log("Verifying installation...");
193
-
194
- let errors = 0;
195
-
196
- // Check skill symlinks resolve
197
- let brokenLinks = 0;
198
- const skillEntries = readdirSync(SKILLS_DIR).filter((f) => f.startsWith("ftm"));
199
- for (const entry of skillEntries) {
200
- const fullPath = join(SKILLS_DIR, entry);
201
- try {
202
- if (lstatSync(fullPath).isSymbolicLink() && !existsSync(fullPath)) {
203
- warn(`broken symlink: ${entry}`);
204
- brokenLinks++;
205
- }
206
- } catch {
207
- // ignore
208
- }
209
- }
210
- if (brokenLinks === 0) {
211
- log(`Skills: ${skillCount} linked, all symlinks valid`);
212
- } else {
213
- errors++;
214
- }
215
-
216
- // Check blackboard state
217
- const contextFile = join(STATE_DIR, "blackboard", "context.json");
218
- const patternsFile = join(STATE_DIR, "blackboard", "patterns.json");
219
- if (existsSync(contextFile) && existsSync(patternsFile)) {
220
- log("Blackboard: initialized");
221
- } else {
222
- warn("blackboard state incomplete");
223
- errors++;
224
- }
225
-
226
- // Check config
227
- if (existsSync(join(CONFIG_DIR, "ftm-config.yml"))) {
228
- log("Config: present");
229
- } else {
230
- warn("ftm-config.yml missing");
231
- errors++;
232
- }
233
-
234
- // Check hooks
235
- if (!NO_HOOKS && hookCount > 0) {
236
- const hookFiles = readdirSync(HOOKS_DIR).filter((f) => f.startsWith("ftm-"));
237
- const allExecutable = hookFiles
238
- .filter((f) => f.endsWith(".sh"))
239
- .every((f) => {
240
- try {
241
- const stat = lstatSync(join(HOOKS_DIR, f));
242
- return (stat.mode & 0o111) !== 0;
243
- } catch {
244
- return false;
245
- }
246
- });
247
-
248
- if (allExecutable) {
249
- log(`Hooks: ${hookCount} installed, all executable`);
250
- } else {
251
- warn("some hook files not executable");
252
- errors++;
253
- }
254
-
255
- // Verify settings.json has FTM hooks
256
- if (!SKIP_MERGE && existsSync(SETTINGS_FILE)) {
257
- const settingsContent = readFileSync(SETTINGS_FILE, "utf8");
258
- const ftmMatches = (settingsContent.match(/ftm-/g) || []).length;
259
- if (ftmMatches > 0) {
260
- log(`Settings: ${ftmMatches} FTM entries in settings.json`);
261
- } else {
262
- warn("no FTM hooks found in settings.json");
263
- errors++;
264
- }
265
- }
266
- }
267
-
268
- return { errors };
269
- }
270
-
271
- // --- Main ---
272
-
273
- function main() {
274
- preflight();
275
-
276
- console.log(`Installing ftm skills from: ${REPO_DIR}`);
277
- console.log(`Linking into: ${SKILLS_DIR}`);
278
- console.log("");
279
-
280
- ensureDir(SKILLS_DIR);
281
-
282
- // Link all ftm*.yml files
283
- const ymlFiles = readdirSync(REPO_DIR).filter(
284
- (f) => f.startsWith("ftm") && f.endsWith(".yml") && !f.includes("config.default")
285
- );
286
- for (const yml of ymlFiles) {
287
- safeSymlink(join(REPO_DIR, yml), join(SKILLS_DIR, yml));
288
- }
289
-
290
- // Link all ftm* directories (skills with SKILL.md)
291
- const dirs = readdirSync(REPO_DIR).filter((f) => {
292
- if (!f.startsWith("ftm")) return false;
293
- if (f === "ftm-state") return false;
294
- const fullPath = join(REPO_DIR, f);
295
- try {
296
- return lstatSync(fullPath).isDirectory();
297
- } catch {
298
- return false;
299
- }
300
- });
301
- for (const dir of dirs) {
302
- safeSymlink(join(REPO_DIR, dir), join(SKILLS_DIR, dir));
303
- }
304
-
305
- console.log("");
306
- log(`${ymlFiles.length} skills linked.`);
307
-
308
- // Set up blackboard state (copy templates, don't overwrite existing data)
309
- const bbDir = join(REPO_DIR, "ftm-state", "blackboard");
310
- if (existsSync(bbDir)) {
311
- console.log("");
312
- ensureDir(join(STATE_DIR, "blackboard", "experiences"));
313
-
314
- const jsonFiles = readdirSync(bbDir).filter((f) => f.endsWith(".json"));
315
- for (const f of jsonFiles) {
316
- const target = join(STATE_DIR, "blackboard", f);
317
- if (!existsSync(target)) {
318
- copyFileSync(join(bbDir, f), target);
319
- log(`INIT ${f} (blackboard template)`);
320
- }
321
- }
322
-
323
- const idxSrc = join(bbDir, "experiences", "index.json");
324
- const idxDest = join(STATE_DIR, "blackboard", "experiences", "index.json");
325
- if (existsSync(idxSrc) && !existsSync(idxDest)) {
326
- copyFileSync(idxSrc, idxDest);
327
- log("INIT experiences/index.json (blackboard template)");
328
- }
329
- }
330
-
331
- // Copy default config if none exists
332
- const configSrc = join(REPO_DIR, "ftm-config.default.yml");
333
- const configDest = join(CONFIG_DIR, "ftm-config.yml");
334
- if (existsSync(configSrc) && !existsSync(configDest)) {
335
- copyFileSync(configSrc, configDest);
336
- log("INIT ftm-config.yml (from default template)");
337
- }
338
-
339
- // Install hooks
340
- let hookCount = 0;
341
-
342
- if (NO_HOOKS) {
343
- console.log("");
344
- console.log("Skipping hooks (--no-hooks).");
345
- } else {
346
- const hooksDir = join(REPO_DIR, "hooks");
347
- if (existsSync(hooksDir)) {
348
- ensureDir(HOOKS_DIR);
349
- console.log("");
350
- console.log("Installing hooks...");
351
-
352
- const hookFiles = readdirSync(hooksDir).filter(
353
- (f) => f.startsWith("ftm-") && (f.endsWith(".sh") || f.endsWith(".mjs"))
354
- );
355
- for (const hook of hookFiles) {
356
- const src = join(hooksDir, hook);
357
- const dest = join(HOOKS_DIR, hook);
358
- const action = existsSync(dest) ? "UPDATE" : "INSTALL";
359
- copyFileSync(src, dest);
360
- if (hook.endsWith(".sh")) {
361
- chmodSync(dest, 0o755);
362
- }
363
- log(`${action} ${hook}`);
364
- hookCount++;
365
- }
366
-
367
- console.log("");
368
- log(`${hookCount} hooks installed to ${HOOKS_DIR}`);
369
- }
370
-
371
- // Merge hooks into settings.json
372
- if (SKIP_MERGE) {
373
- console.log("");
374
- log("Skipping settings.json merge (--skip-merge).");
375
- log("Add entries from hooks/settings-template.json to ~/.claude/settings.json manually.");
376
- } else {
377
- mergeHooksIntoSettings();
378
- }
379
- }
380
-
381
- // Verification
382
- const { errors } = verify(ymlFiles.length, hookCount);
383
-
384
- // Summary
385
- console.log("");
386
- if (errors === 0 && warnCount === 0) {
387
- console.log(`Done. ${ymlFiles.length} skills, ${hookCount} hooks. Everything checks out.`);
388
- } else {
389
- console.log(`Done. ${ymlFiles.length} skills, ${hookCount} hooks. ${warnCount} warning(s).`);
390
- }
391
- console.log("");
392
- console.log("Restart Claude Code (or start a new session) to pick up the skills.");
393
-
394
- if (WITH_INBOX) {
395
- console.log("");
396
- installInbox();
397
- } else {
398
- console.log("Try: /ftm help");
399
- console.log(" To also install the inbox service: npx feed-the-machine --with-inbox");
400
- }
401
- }
402
-
403
- function installInbox() {
404
- const inboxSrc = join(REPO_DIR, "ftm-inbox");
405
- if (!existsSync(inboxSrc)) {
406
- console.error("ERROR: ftm-inbox/ not found in package. Cannot install inbox service.");
407
- process.exit(1);
408
- }
409
-
410
- console.log("Installing ftm-inbox service...");
411
- console.log(` Source: ${inboxSrc}`);
412
- console.log(` Destination: ${INBOX_INSTALL_DIR}`);
413
- console.log("");
414
-
415
- // Copy ftm-inbox/ to ~/.claude/ftm-inbox/
416
- ensureDir(INBOX_INSTALL_DIR);
417
- cpSync(inboxSrc, INBOX_INSTALL_DIR, { recursive: true });
418
- log("COPY ftm-inbox → ~/.claude/ftm-inbox/");
419
-
420
- // Make shell scripts executable
421
- const binDir = join(INBOX_INSTALL_DIR, "bin");
422
- const scripts = ["start.sh", "stop.sh", "status.sh"];
423
- for (const script of scripts) {
424
- const scriptPath = join(binDir, script);
425
- if (existsSync(scriptPath)) {
426
- chmodSync(scriptPath, 0o755);
427
- log(`CHMOD +x bin/${script}`);
428
- }
429
- }
430
-
431
- // Install Node deps if package.json exists
432
- const pkgJson = join(INBOX_INSTALL_DIR, "package.json");
433
- if (existsSync(pkgJson)) {
434
- console.log("");
435
- console.log("Installing Node.js dependencies...");
436
- const npmResult = spawnSync("npm", ["install", "--prefix", INBOX_INSTALL_DIR], {
437
- stdio: "inherit",
438
- cwd: INBOX_INSTALL_DIR,
439
- });
440
- if (npmResult.status !== 0) {
441
- console.warn("WARNING: npm install failed. Check Node.js version and try manually.");
442
- }
443
- }
444
-
445
- // Install Python deps if requirements.txt exists
446
- const reqTxt = join(INBOX_INSTALL_DIR, "requirements.txt");
447
- if (existsSync(reqTxt)) {
448
- console.log("");
449
- console.log("Installing Python dependencies...");
450
- const pipResult = spawnSync("pip3", ["install", "-r", reqTxt], {
451
- stdio: "inherit",
452
- cwd: INBOX_INSTALL_DIR,
453
- });
454
- if (pipResult.status !== 0) {
455
- console.warn("WARNING: pip3 install failed. Check Python 3 and try manually:");
456
- console.warn(` pip3 install -r ${reqTxt}`);
457
- }
458
- }
459
-
460
- // Run setup wizard
461
- console.log("");
462
- console.log("Running setup wizard...");
463
- const setupScript = join(binDir, "setup.mjs");
464
- if (existsSync(setupScript)) {
465
- const setupResult = spawnSync("node", [setupScript], { stdio: "inherit" });
466
- if (setupResult.status !== 0) {
467
- console.warn("WARNING: Setup wizard exited with errors.");
468
- console.warn(`Re-run manually: node ${setupScript}`);
469
- }
470
- } else {
471
- console.warn("WARNING: setup.mjs not found. Run setup manually.");
472
- }
473
-
474
- // Offer LaunchAgent (macOS only)
475
- if (platform() === "darwin") {
476
- console.log("");
477
- console.log("macOS detected. To auto-start ftm-inbox on login, run:");
478
- console.log(` node ${join(binDir, "launchagent.mjs")}`);
479
- }
480
-
481
- console.log("");
482
- console.log("ftm-inbox installed.");
483
- console.log(` Start: ${join(binDir, "start.sh")}`);
484
- console.log(` Stop: ${join(binDir, "stop.sh")}`);
485
- console.log(` Status: ${join(binDir, "status.sh")}`);
486
- console.log("");
487
- console.log("See docs/INBOX.md for full documentation.");
488
- console.log("Try: /ftm help");
489
- }
490
-
491
- main();
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * npx feed-the-machine — installs ftm skills into ~/.claude/skills/
5
+ *
6
+ * Full install: skills, hooks, settings.json merge, and verification.
7
+ * Safe to re-run — idempotent.
8
+ *
9
+ * Flags:
10
+ * --with-inbox Also install the inbox service
11
+ * --no-hooks Skip hooks entirely
12
+ * --skip-merge Install hook files but don't touch settings.json
13
+ */
14
+
15
+ import { existsSync, mkdirSync, readdirSync, lstatSync, readFileSync, writeFileSync, copyFileSync, symlinkSync, unlinkSync, chmodSync, cpSync } from "fs";
16
+ import { join, basename, dirname } from "path";
17
+ import { homedir, platform } from "os";
18
+ import { fileURLToPath } from "url";
19
+ import { execSync, spawnSync } from "child_process";
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+ const REPO_DIR = dirname(__dirname); // package root (one level up from bin/)
24
+ const HOME = homedir();
25
+ const SKILLS_DIR = join(HOME, ".claude", "skills");
26
+ const STATE_DIR = join(HOME, ".claude", "ftm-state");
27
+ const CONFIG_DIR = join(HOME, ".claude");
28
+ const HOOKS_DIR = join(HOME, ".claude", "hooks");
29
+ const SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
30
+ const INBOX_INSTALL_DIR = join(HOME, ".claude", "ftm-inbox");
31
+
32
+ const ARGS = process.argv.slice(2);
33
+ const WITH_INBOX = ARGS.includes("--with-inbox");
34
+ const NO_HOOKS = ARGS.includes("--no-hooks");
35
+ const SKIP_MERGE = ARGS.includes("--skip-merge");
36
+
37
+ let warnCount = 0;
38
+
39
+ function log(msg) {
40
+ console.log(` ${msg}`);
41
+ }
42
+
43
+ function warn(msg) {
44
+ console.log(` WARN: ${msg}`);
45
+ warnCount++;
46
+ }
47
+
48
+ function ensureDir(dir) {
49
+ if (!existsSync(dir)) {
50
+ mkdirSync(dir, { recursive: true });
51
+ }
52
+ }
53
+
54
+ function safeSymlink(src, dest) {
55
+ const name = basename(dest);
56
+ try {
57
+ if (lstatSync(dest).isSymbolicLink()) {
58
+ unlinkSync(dest);
59
+ } else if (existsSync(dest)) {
60
+ log(`SKIP ${name} (real file/dir exists — back it up first)`);
61
+ return;
62
+ }
63
+ } catch {
64
+ // dest doesn't exist, that's fine
65
+ }
66
+ symlinkSync(src, dest);
67
+ log(`LINK ${name}`);
68
+ }
69
+
70
+ function commandExists(cmd) {
71
+ try {
72
+ execSync(`command -v ${cmd}`, { stdio: "ignore" });
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ function commandVersion(cmd, flag = "--version") {
80
+ try {
81
+ return execSync(`${cmd} ${flag}`, { encoding: "utf8" }).trim().split("\n")[0];
82
+ } catch {
83
+ return "unknown";
84
+ }
85
+ }
86
+
87
+ // --- Preflight ---
88
+
89
+ function preflight() {
90
+ console.log("Preflight checks...");
91
+
92
+ if (!NO_HOOKS) {
93
+ // jq is required for all shell hooks (they parse JSON stdin via jq)
94
+ if (!commandExists("jq")) {
95
+ console.log("");
96
+ console.log(" ERROR: jq is required for FTM hooks.");
97
+ console.log("");
98
+ console.log(" Install it:");
99
+ console.log(" macOS: brew install jq");
100
+ console.log(" Ubuntu: sudo apt-get install jq");
101
+ console.log(" Alpine: apk add jq");
102
+ console.log("");
103
+ console.log(" Or skip hooks: npx feed-the-machine --no-hooks");
104
+ process.exit(1);
105
+ }
106
+ log(`jq: ${commandVersion("jq")}`);
107
+ log(`node: ${process.version}`);
108
+ } else {
109
+ log("hooks skipped (--no-hooks)");
110
+ }
111
+
112
+ console.log("");
113
+ }
114
+
115
+ // --- Settings Merge ---
116
+
117
+ function mergeHooksIntoSettings() {
118
+ const templatePath = join(REPO_DIR, "hooks", "settings-template.json");
119
+ if (!existsSync(templatePath)) {
120
+ warn("hooks/settings-template.json not found — hooks installed but not registered");
121
+ return;
122
+ }
123
+
124
+ console.log("");
125
+ console.log("Registering hooks in settings.json...");
126
+
127
+ // Read and expand ~ to actual home directory
128
+ const rawTemplate = readFileSync(templatePath, "utf8");
129
+ const expandedTemplate = rawTemplate.replace(/~\/.claude/g, join(HOME, ".claude"));
130
+ const template = JSON.parse(expandedTemplate);
131
+ const templateHooks = template.hooks || {};
132
+
133
+ if (!existsSync(SETTINGS_FILE)) {
134
+ // No settings.json — create one with just the hooks
135
+ writeFileSync(SETTINGS_FILE, JSON.stringify({ hooks: templateHooks }, null, 2) + "\n");
136
+ log("CREATED settings.json with FTM hooks");
137
+ return;
138
+ }
139
+
140
+ // Read existing settings
141
+ const existing = JSON.parse(readFileSync(SETTINGS_FILE, "utf8"));
142
+
143
+ // Backup
144
+ const ts = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
145
+ const backupPath = `${SETTINGS_FILE}.ftm-backup-${ts}`;
146
+ copyFileSync(SETTINGS_FILE, backupPath);
147
+ log(`BACKUP ${backupPath}`);
148
+
149
+ // Ensure hooks key exists
150
+ if (!existing.hooks) {
151
+ existing.hooks = {};
152
+ }
153
+
154
+ // Merge each event type
155
+ const events = ["PreToolUse", "UserPromptSubmit", "PostToolUse", "Stop"];
156
+ for (const event of events) {
157
+ const templateEntries = templateHooks[event] || [];
158
+ const existingEntries = existing.hooks[event] || [];
159
+
160
+ if (templateEntries.length === 0) continue;
161
+
162
+ // Check if FTM hooks are already present by looking for ftm- in command paths
163
+ const existingCommands = JSON.stringify(existingEntries);
164
+ const alreadyPresent = templateEntries.some((entry) => {
165
+ const hooks = entry.hooks || [];
166
+ return hooks.some((h) => {
167
+ const cmd = h.command || "";
168
+ const cmdBase = basename(cmd.split(" ").pop()); // handle "node foo.mjs"
169
+ return existingCommands.includes(cmdBase);
170
+ });
171
+ });
172
+
173
+ if (alreadyPresent) {
174
+ log(`SKIP ${event} hooks (already configured)`);
175
+ continue;
176
+ }
177
+
178
+ existing.hooks[event] = [...existingEntries, ...templateEntries];
179
+ log(`MERGE ${event} hooks`);
180
+ }
181
+
182
+ writeFileSync(SETTINGS_FILE, JSON.stringify(existing, null, 2) + "\n");
183
+ log("UPDATED settings.json");
184
+ console.log("");
185
+ log("Hooks are active.");
186
+ }
187
+
188
+ // --- Verification ---
189
+
190
+ function verify(skillCount, hookCount) {
191
+ console.log("");
192
+ console.log("Verifying installation...");
193
+
194
+ let errors = 0;
195
+
196
+ // Check skill symlinks resolve
197
+ let brokenLinks = 0;
198
+ const skillEntries = readdirSync(SKILLS_DIR).filter((f) => f.startsWith("ftm"));
199
+ for (const entry of skillEntries) {
200
+ const fullPath = join(SKILLS_DIR, entry);
201
+ try {
202
+ if (lstatSync(fullPath).isSymbolicLink() && !existsSync(fullPath)) {
203
+ warn(`broken symlink: ${entry}`);
204
+ brokenLinks++;
205
+ }
206
+ } catch {
207
+ // ignore
208
+ }
209
+ }
210
+ if (brokenLinks === 0) {
211
+ log(`Skills: ${skillCount} linked, all symlinks valid`);
212
+ } else {
213
+ errors++;
214
+ }
215
+
216
+ // Check blackboard state
217
+ const contextFile = join(STATE_DIR, "blackboard", "context.json");
218
+ const patternsFile = join(STATE_DIR, "blackboard", "patterns.json");
219
+ if (existsSync(contextFile) && existsSync(patternsFile)) {
220
+ log("Blackboard: initialized");
221
+ } else {
222
+ warn("blackboard state incomplete");
223
+ errors++;
224
+ }
225
+
226
+ // Check config
227
+ if (existsSync(join(CONFIG_DIR, "ftm-config.yml"))) {
228
+ log("Config: present");
229
+ } else {
230
+ warn("ftm-config.yml missing");
231
+ errors++;
232
+ }
233
+
234
+ // Check hooks
235
+ if (!NO_HOOKS && hookCount > 0) {
236
+ const hookFiles = readdirSync(HOOKS_DIR).filter((f) => f.startsWith("ftm-"));
237
+ const allExecutable = hookFiles
238
+ .filter((f) => f.endsWith(".sh"))
239
+ .every((f) => {
240
+ try {
241
+ const stat = lstatSync(join(HOOKS_DIR, f));
242
+ return (stat.mode & 0o111) !== 0;
243
+ } catch {
244
+ return false;
245
+ }
246
+ });
247
+
248
+ if (allExecutable) {
249
+ log(`Hooks: ${hookCount} installed, all executable`);
250
+ } else {
251
+ warn("some hook files not executable");
252
+ errors++;
253
+ }
254
+
255
+ // Verify settings.json has FTM hooks
256
+ if (!SKIP_MERGE && existsSync(SETTINGS_FILE)) {
257
+ const settingsContent = readFileSync(SETTINGS_FILE, "utf8");
258
+ const ftmMatches = (settingsContent.match(/ftm-/g) || []).length;
259
+ if (ftmMatches > 0) {
260
+ log(`Settings: ${ftmMatches} FTM entries in settings.json`);
261
+ } else {
262
+ warn("no FTM hooks found in settings.json");
263
+ errors++;
264
+ }
265
+ }
266
+ }
267
+
268
+ return { errors };
269
+ }
270
+
271
+ // --- Main ---
272
+
273
+ function main() {
274
+ preflight();
275
+
276
+ console.log(`Installing ftm skills from: ${REPO_DIR}`);
277
+ console.log(`Linking into: ${SKILLS_DIR}`);
278
+ console.log("");
279
+
280
+ ensureDir(SKILLS_DIR);
281
+
282
+ // Link all ftm*.yml files
283
+ const ymlFiles = readdirSync(REPO_DIR).filter(
284
+ (f) => f.startsWith("ftm") && f.endsWith(".yml") && !f.includes("config.default")
285
+ );
286
+ for (const yml of ymlFiles) {
287
+ safeSymlink(join(REPO_DIR, yml), join(SKILLS_DIR, yml));
288
+ }
289
+
290
+ // Link all ftm* directories (skills with SKILL.md)
291
+ const dirs = readdirSync(REPO_DIR).filter((f) => {
292
+ if (!f.startsWith("ftm")) return false;
293
+ if (f === "ftm-state") return false;
294
+ const fullPath = join(REPO_DIR, f);
295
+ try {
296
+ return lstatSync(fullPath).isDirectory();
297
+ } catch {
298
+ return false;
299
+ }
300
+ });
301
+ for (const dir of dirs) {
302
+ safeSymlink(join(REPO_DIR, dir), join(SKILLS_DIR, dir));
303
+ }
304
+
305
+ console.log("");
306
+ log(`${ymlFiles.length} skills linked.`);
307
+
308
+ // Set up blackboard state (copy templates, don't overwrite existing data)
309
+ const bbDir = join(REPO_DIR, "ftm-state", "blackboard");
310
+ if (existsSync(bbDir)) {
311
+ console.log("");
312
+ ensureDir(join(STATE_DIR, "blackboard", "experiences"));
313
+
314
+ const jsonFiles = readdirSync(bbDir).filter((f) => f.endsWith(".json"));
315
+ for (const f of jsonFiles) {
316
+ const target = join(STATE_DIR, "blackboard", f);
317
+ if (!existsSync(target)) {
318
+ copyFileSync(join(bbDir, f), target);
319
+ log(`INIT ${f} (blackboard template)`);
320
+ }
321
+ }
322
+
323
+ const idxSrc = join(bbDir, "experiences", "index.json");
324
+ const idxDest = join(STATE_DIR, "blackboard", "experiences", "index.json");
325
+ if (existsSync(idxSrc) && !existsSync(idxDest)) {
326
+ copyFileSync(idxSrc, idxDest);
327
+ log("INIT experiences/index.json (blackboard template)");
328
+ }
329
+ }
330
+
331
+ // Copy default config if none exists
332
+ const configSrc = join(REPO_DIR, "ftm-config.default.yml");
333
+ const configDest = join(CONFIG_DIR, "ftm-config.yml");
334
+ if (existsSync(configSrc) && !existsSync(configDest)) {
335
+ copyFileSync(configSrc, configDest);
336
+ log("INIT ftm-config.yml (from default template)");
337
+ }
338
+
339
+ // Install hooks
340
+ let hookCount = 0;
341
+
342
+ if (NO_HOOKS) {
343
+ console.log("");
344
+ console.log("Skipping hooks (--no-hooks).");
345
+ } else {
346
+ const hooksDir = join(REPO_DIR, "hooks");
347
+ if (existsSync(hooksDir)) {
348
+ ensureDir(HOOKS_DIR);
349
+ console.log("");
350
+ console.log("Installing hooks...");
351
+
352
+ const hookFiles = readdirSync(hooksDir).filter(
353
+ (f) => f.startsWith("ftm-") && (f.endsWith(".sh") || f.endsWith(".mjs"))
354
+ );
355
+ for (const hook of hookFiles) {
356
+ const src = join(hooksDir, hook);
357
+ const dest = join(HOOKS_DIR, hook);
358
+ const action = existsSync(dest) ? "UPDATE" : "INSTALL";
359
+ copyFileSync(src, dest);
360
+ if (hook.endsWith(".sh")) {
361
+ chmodSync(dest, 0o755);
362
+ }
363
+ log(`${action} ${hook}`);
364
+ hookCount++;
365
+ }
366
+
367
+ console.log("");
368
+ log(`${hookCount} hooks installed to ${HOOKS_DIR}`);
369
+ }
370
+
371
+ // Merge hooks into settings.json
372
+ if (SKIP_MERGE) {
373
+ console.log("");
374
+ log("Skipping settings.json merge (--skip-merge).");
375
+ log("Add entries from hooks/settings-template.json to ~/.claude/settings.json manually.");
376
+ } else {
377
+ mergeHooksIntoSettings();
378
+ }
379
+ }
380
+
381
+ // Verification
382
+ const { errors } = verify(ymlFiles.length, hookCount);
383
+
384
+ // Summary
385
+ console.log("");
386
+ if (errors === 0 && warnCount === 0) {
387
+ console.log(`Done. ${ymlFiles.length} skills, ${hookCount} hooks. Everything checks out.`);
388
+ } else {
389
+ console.log(`Done. ${ymlFiles.length} skills, ${hookCount} hooks. ${warnCount} warning(s).`);
390
+ }
391
+ console.log("");
392
+ console.log("Restart Claude Code (or start a new session) to pick up the skills.");
393
+
394
+ if (WITH_INBOX) {
395
+ console.log("");
396
+ installInbox();
397
+ } else {
398
+ console.log("Try: /ftm help");
399
+ console.log(" To also install the inbox service: npx feed-the-machine --with-inbox");
400
+ }
401
+ }
402
+
403
+ function installInbox() {
404
+ const inboxSrc = join(REPO_DIR, "ftm-inbox");
405
+ if (!existsSync(inboxSrc)) {
406
+ console.error("ERROR: ftm-inbox/ not found in package. Cannot install inbox service.");
407
+ process.exit(1);
408
+ }
409
+
410
+ console.log("Installing ftm-inbox service...");
411
+ console.log(` Source: ${inboxSrc}`);
412
+ console.log(` Destination: ${INBOX_INSTALL_DIR}`);
413
+ console.log("");
414
+
415
+ // Copy ftm-inbox/ to ~/.claude/ftm-inbox/
416
+ ensureDir(INBOX_INSTALL_DIR);
417
+ cpSync(inboxSrc, INBOX_INSTALL_DIR, { recursive: true });
418
+ log("COPY ftm-inbox → ~/.claude/ftm-inbox/");
419
+
420
+ // Make shell scripts executable
421
+ const binDir = join(INBOX_INSTALL_DIR, "bin");
422
+ const scripts = ["start.sh", "stop.sh", "status.sh"];
423
+ for (const script of scripts) {
424
+ const scriptPath = join(binDir, script);
425
+ if (existsSync(scriptPath)) {
426
+ chmodSync(scriptPath, 0o755);
427
+ log(`CHMOD +x bin/${script}`);
428
+ }
429
+ }
430
+
431
+ // Install Node deps if package.json exists
432
+ const pkgJson = join(INBOX_INSTALL_DIR, "package.json");
433
+ if (existsSync(pkgJson)) {
434
+ console.log("");
435
+ console.log("Installing Node.js dependencies...");
436
+ const npmResult = spawnSync("npm", ["install", "--prefix", INBOX_INSTALL_DIR], {
437
+ stdio: "inherit",
438
+ cwd: INBOX_INSTALL_DIR,
439
+ });
440
+ if (npmResult.status !== 0) {
441
+ console.warn("WARNING: npm install failed. Check Node.js version and try manually.");
442
+ }
443
+ }
444
+
445
+ // Install Python deps if requirements.txt exists
446
+ const reqTxt = join(INBOX_INSTALL_DIR, "requirements.txt");
447
+ if (existsSync(reqTxt)) {
448
+ console.log("");
449
+ console.log("Installing Python dependencies...");
450
+ const pipResult = spawnSync("pip3", ["install", "-r", reqTxt], {
451
+ stdio: "inherit",
452
+ cwd: INBOX_INSTALL_DIR,
453
+ });
454
+ if (pipResult.status !== 0) {
455
+ console.warn("WARNING: pip3 install failed. Check Python 3 and try manually:");
456
+ console.warn(` pip3 install -r ${reqTxt}`);
457
+ }
458
+ }
459
+
460
+ // Run setup wizard
461
+ console.log("");
462
+ console.log("Running setup wizard...");
463
+ const setupScript = join(binDir, "setup.mjs");
464
+ if (existsSync(setupScript)) {
465
+ const setupResult = spawnSync("node", [setupScript], { stdio: "inherit" });
466
+ if (setupResult.status !== 0) {
467
+ console.warn("WARNING: Setup wizard exited with errors.");
468
+ console.warn(`Re-run manually: node ${setupScript}`);
469
+ }
470
+ } else {
471
+ console.warn("WARNING: setup.mjs not found. Run setup manually.");
472
+ }
473
+
474
+ // Offer LaunchAgent (macOS only)
475
+ if (platform() === "darwin") {
476
+ console.log("");
477
+ console.log("macOS detected. To auto-start ftm-inbox on login, run:");
478
+ console.log(` node ${join(binDir, "launchagent.mjs")}`);
479
+ }
480
+
481
+ console.log("");
482
+ console.log("ftm-inbox installed.");
483
+ console.log(` Start: ${join(binDir, "start.sh")}`);
484
+ console.log(` Stop: ${join(binDir, "stop.sh")}`);
485
+ console.log(` Status: ${join(binDir, "status.sh")}`);
486
+ console.log("");
487
+ console.log("See docs/INBOX.md for full documentation.");
488
+ console.log("Try: /ftm help");
489
+ }
490
+
491
+ main();