feed-the-machine 1.2.0 → 1.3.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.
- package/bin/install.mjs +272 -25
- package/ftm-audit/SKILL.md +383 -57
- package/ftm-brainstorm/SKILL.md +119 -51
- package/ftm-config/SKILL.md +1 -1
- package/ftm-council/SKILL.md +259 -31
- package/ftm-dashboard/SKILL.md +10 -10
- package/ftm-debug/SKILL.md +861 -54
- package/ftm-diagram/SKILL.md +1 -1
- package/ftm-executor/SKILL.md +6 -6
- package/ftm-git/SKILL.md +209 -22
- package/ftm-inbox/bin/start.sh +1 -1
- package/ftm-inbox/bin/status.sh +1 -1
- package/ftm-inbox/bin/stop.sh +1 -1
- package/ftm-intent/SKILL.md +0 -1
- package/ftm-mind/SKILL.md +861 -11
- package/ftm-mind/references/event-registry.md +30 -0
- package/ftm-pause/SKILL.md +256 -37
- package/ftm-resume/SKILL.md +380 -75
- package/ftm-retro/SKILL.md +164 -27
- package/ftm-upgrade/SKILL.md +4 -4
- package/hooks/ftm-blackboard-enforcer.sh +29 -27
- package/hooks/ftm-plan-gate.sh +21 -25
- package/install.sh +244 -112
- package/package.json +1 -1
package/bin/install.mjs
CHANGED
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* npx feed-the-machine — installs ftm skills into ~/.claude/skills/
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import { existsSync, mkdirSync, readdirSync, lstatSync, readFileSync, writeFileSync, copyFileSync, symlinkSync, unlinkSync, chmodSync, cpSync } from "fs";
|
|
@@ -21,15 +26,25 @@ const SKILLS_DIR = join(HOME, ".claude", "skills");
|
|
|
21
26
|
const STATE_DIR = join(HOME, ".claude", "ftm-state");
|
|
22
27
|
const CONFIG_DIR = join(HOME, ".claude");
|
|
23
28
|
const HOOKS_DIR = join(HOME, ".claude", "hooks");
|
|
29
|
+
const SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
|
|
24
30
|
const INBOX_INSTALL_DIR = join(HOME, ".claude", "ftm-inbox");
|
|
25
31
|
|
|
26
32
|
const ARGS = process.argv.slice(2);
|
|
27
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;
|
|
28
38
|
|
|
29
39
|
function log(msg) {
|
|
30
40
|
console.log(` ${msg}`);
|
|
31
41
|
}
|
|
32
42
|
|
|
43
|
+
function warn(msg) {
|
|
44
|
+
console.log(` WARN: ${msg}`);
|
|
45
|
+
warnCount++;
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
function ensureDir(dir) {
|
|
34
49
|
if (!existsSync(dir)) {
|
|
35
50
|
mkdirSync(dir, { recursive: true });
|
|
@@ -52,7 +67,212 @@ function safeSymlink(src, dest) {
|
|
|
52
67
|
log(`LINK ${name}`);
|
|
53
68
|
}
|
|
54
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
|
+
|
|
55
273
|
function main() {
|
|
274
|
+
preflight();
|
|
275
|
+
|
|
56
276
|
console.log(`Installing ftm skills from: ${REPO_DIR}`);
|
|
57
277
|
console.log(`Linking into: ${SKILLS_DIR}`);
|
|
58
278
|
console.log("");
|
|
@@ -82,9 +302,13 @@ function main() {
|
|
|
82
302
|
safeSymlink(join(REPO_DIR, dir), join(SKILLS_DIR, dir));
|
|
83
303
|
}
|
|
84
304
|
|
|
305
|
+
console.log("");
|
|
306
|
+
log(`${ymlFiles.length} skills linked.`);
|
|
307
|
+
|
|
85
308
|
// Set up blackboard state (copy templates, don't overwrite existing data)
|
|
86
309
|
const bbDir = join(REPO_DIR, "ftm-state", "blackboard");
|
|
87
310
|
if (existsSync(bbDir)) {
|
|
311
|
+
console.log("");
|
|
88
312
|
ensureDir(join(STATE_DIR, "blackboard", "experiences"));
|
|
89
313
|
|
|
90
314
|
const jsonFiles = readdirSync(bbDir).filter((f) => f.endsWith(".json"));
|
|
@@ -113,39 +337,62 @@ function main() {
|
|
|
113
337
|
}
|
|
114
338
|
|
|
115
339
|
// Install hooks
|
|
116
|
-
const hooksDir = join(REPO_DIR, "hooks");
|
|
117
340
|
let hookCount = 0;
|
|
118
|
-
|
|
119
|
-
|
|
341
|
+
|
|
342
|
+
if (NO_HOOKS) {
|
|
120
343
|
console.log("");
|
|
121
|
-
console.log("
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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++;
|
|
133
365
|
}
|
|
134
|
-
|
|
135
|
-
|
|
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();
|
|
136
378
|
}
|
|
137
379
|
}
|
|
138
380
|
|
|
381
|
+
// Verification
|
|
382
|
+
const { errors } = verify(ymlFiles.length, hookCount);
|
|
383
|
+
|
|
384
|
+
// Summary
|
|
139
385
|
console.log("");
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(" See docs/HOOKS.md for details.");
|
|
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
|
+
}
|
|
146
391
|
console.log("");
|
|
392
|
+
console.log("Restart Claude Code (or start a new session) to pick up the skills.");
|
|
147
393
|
|
|
148
394
|
if (WITH_INBOX) {
|
|
395
|
+
console.log("");
|
|
149
396
|
installInbox();
|
|
150
397
|
} else {
|
|
151
398
|
console.log("Try: /ftm help");
|