infernoflow 0.34.2 → 0.35.4
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/dist/bin/infernoflow.mjs +50 -5
- package/dist/lib/commands/cloud.mjs +261 -9
- package/dist/lib/commands/feedback.mjs +168 -0
- package/dist/lib/commands/log.mjs +32 -12
- package/dist/lib/commands/setup.mjs +1 -1
- package/dist/lib/commands/switch.mjs +200 -32
- package/dist/lib/telemetry.mjs +194 -0
- package/dist/templates/git-hooks/post-commit +22 -2
- package/dist/templates/git-hooks/pre-stash +23 -0
- package/package.json +21 -9
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -74,6 +74,8 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
74
74
|
ask: "Query session memory — search gotchas, decisions, and failed attempts by keyword or type",
|
|
75
75
|
recap: "End-of-session summary — what was captured, what git changes weren't logged, session health score",
|
|
76
76
|
uninstall: "Remove infernoflow from a project — inferno/, CLAUDE.md, MCP server, git hooks (--dry-run to preview)",
|
|
77
|
+
feedback: "60-second CLI survey about how you use infernoflow (--form to open web form)",
|
|
78
|
+
telemetry: "Manage anonymous usage telemetry (on | off | status) — opt-in, command names only",
|
|
77
79
|
};
|
|
78
80
|
|
|
79
81
|
const COMMAND_HANDLERS = {
|
|
@@ -141,6 +143,8 @@ const COMMAND_HANDLERS = {
|
|
|
141
143
|
ask: async (args) => (await import("../lib/commands/ask.mjs")).askCommand(args),
|
|
142
144
|
recap: async (args) => (await import("../lib/commands/recap.mjs")).recapCommand(args),
|
|
143
145
|
uninstall: async (args) => (await import("../lib/commands/uninstall.mjs")).uninstallCommand(args),
|
|
146
|
+
feedback: async (args) => (await import("../lib/commands/feedback.mjs")).feedbackCommand(args),
|
|
147
|
+
telemetry: async (args) => (await import("../lib/telemetry.mjs")).telemetryCommand(args),
|
|
144
148
|
};
|
|
145
149
|
|
|
146
150
|
function formatCommandsHelp() {
|
|
@@ -151,15 +155,49 @@ function formatCommandsHelp() {
|
|
|
151
155
|
.join("\n");
|
|
152
156
|
}
|
|
153
157
|
|
|
158
|
+
// ── Full grouped command list (infernoflow commands) ──────────────────────────
|
|
159
|
+
const COMMAND_GROUPS = {
|
|
160
|
+
"Session Memory": ["log", "ask", "switch", "recap", "stats", "theme"],
|
|
161
|
+
"Context": ["context", "scan", "suggest", "check", "status"],
|
|
162
|
+
"Code Analysis": ["graph", "impact", "why", "coverage", "stability", "freeze", "thaw", "scout"],
|
|
163
|
+
"Workflow": ["run", "sync", "watch", "vibe", "implement", "doc-gate", "synthesize", "agent"],
|
|
164
|
+
"Publishing": ["publish", "version", "changelog", "diff"],
|
|
165
|
+
"Team": ["team-sync", "cloud", "share", "notify", "pr-comment", "pr-impact"],
|
|
166
|
+
"Quality": ["health", "audit", "review", "snapshot", "export", "link"],
|
|
167
|
+
"Integration": ["ai", "ci", "coverage"],
|
|
168
|
+
"Setup": ["init", "setup", "adopt", "demo", "doctor", "onboard", "generate-skills", "upgrade", "uninstall"],
|
|
169
|
+
"Advanced": ["scaffold", "explain", "test", "report", "monorepo", "feedback", "telemetry"],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
function formatCommandGroups() {
|
|
173
|
+
const w = 18;
|
|
174
|
+
return Object.entries(COMMAND_GROUPS).map(([group, cmds]) =>
|
|
175
|
+
` ${bold(group + ":")}
|
|
176
|
+
${cmds.join(" ")}`
|
|
177
|
+
).join("\n\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
154
180
|
const HELP = `
|
|
155
181
|
${bold("🔥 infernoflow")} ${gray("v" + VERSION)}
|
|
156
|
-
${gray("
|
|
182
|
+
${gray("Persistent memory for AI coding sessions")}
|
|
157
183
|
|
|
158
184
|
${bold("Usage:")}
|
|
159
|
-
infernoflow
|
|
185
|
+
infernoflow [command] [options]
|
|
160
186
|
|
|
161
|
-
${bold("Commands:")}
|
|
162
|
-
${
|
|
187
|
+
${bold("Core Commands:")}
|
|
188
|
+
${cyan("log")} ${gray('"..."')} Add to session memory ${gray("(--type gotcha|decision|attempt|preference)")}
|
|
189
|
+
${cyan("ask")} ${gray('"..."')} Search your memory by keyword ${gray("(gotchas surface first)")}
|
|
190
|
+
${cyan("switch")} Generate handoff for next AI agent
|
|
191
|
+
${cyan("recap")} End-of-session health score + unlogged changes
|
|
192
|
+
${cyan("status")} Contract health at a glance
|
|
193
|
+
|
|
194
|
+
${bold("Getting Started:")}
|
|
195
|
+
${cyan("setup")} One command to get fully operational
|
|
196
|
+
${cyan("demo")} Interactive walkthrough ${gray("(5 minutes)")}
|
|
197
|
+
${cyan("doctor")} Diagnose your setup
|
|
198
|
+
|
|
199
|
+
${gray("Run")} ${cyan("infernoflow commands")} ${gray("to see all commands.")}
|
|
200
|
+
${gray("Run")} ${cyan("infernoflow <command> --help")} ${gray("for command-specific options.")}
|
|
163
201
|
|
|
164
202
|
${bold("diff options:")}
|
|
165
203
|
--ref <tag|commit> Compare against a specific ref (default: last git tag)
|
|
@@ -503,12 +541,19 @@ if (cmd === "--version" || cmd === "-v") {
|
|
|
503
541
|
console.log(VERSION);
|
|
504
542
|
process.exit(0);
|
|
505
543
|
}
|
|
544
|
+
if (cmd === "commands") {
|
|
545
|
+
console.log(`\n ${bold("🔥 infernoflow")} ${gray("v" + VERSION)} ${gray("— all commands")}\n`);
|
|
546
|
+
console.log(formatCommandGroups());
|
|
547
|
+
console.log(`\n ${gray("Run")} ${cyan("infernoflow <command> --help")} ${gray("for options.")}\n`);
|
|
548
|
+
process.exit(0);
|
|
549
|
+
}
|
|
506
550
|
|
|
507
551
|
const commands = Object.keys(COMMAND_HANDLERS);
|
|
508
552
|
|
|
509
553
|
if (!commands.includes(cmd)) {
|
|
510
554
|
console.error(red(`\nUnknown command: ${cmd}`));
|
|
511
|
-
console.error(gray(
|
|
555
|
+
console.error(gray(`Run: infernoflow commands (see all commands)`));
|
|
556
|
+
console.error(gray("Run: infernoflow --help (quick start)\n"));
|
|
512
557
|
process.exit(1);
|
|
513
558
|
}
|
|
514
559
|
|
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
* Sub-commands:
|
|
8
8
|
* cloud init Generate a project token and write inferno/.cloud.json
|
|
9
9
|
* cloud push Upload local contract to cloud
|
|
10
|
+
* cloud push --memory Also push session memory (sessions.jsonl) — Pro tier value prop
|
|
10
11
|
* cloud pull Download latest contract from cloud
|
|
12
|
+
* cloud pull --memory Also pull session memory — restores inferno/sessions.jsonl
|
|
13
|
+
* cloud memory push Push session memory only
|
|
14
|
+
* cloud memory pull Pull session memory only
|
|
15
|
+
* cloud memory status Compare local vs remote memory entry count
|
|
11
16
|
* cloud status Show local vs cloud diff
|
|
12
17
|
* cloud dashboard Print hosted dashboard URL
|
|
13
18
|
*
|
|
@@ -16,12 +21,14 @@
|
|
|
16
21
|
* --endpoint <url> Override default endpoint
|
|
17
22
|
* --dry-run Print what would happen without sending
|
|
18
23
|
* --json Machine-readable output
|
|
24
|
+
* --memory Include session memory (sessions.jsonl) in push/pull
|
|
19
25
|
*
|
|
20
26
|
* Usage:
|
|
21
27
|
* infernoflow cloud init
|
|
22
28
|
* infernoflow cloud push
|
|
23
|
-
* infernoflow cloud
|
|
24
|
-
* infernoflow cloud
|
|
29
|
+
* infernoflow cloud push --memory
|
|
30
|
+
* infernoflow cloud pull --memory
|
|
31
|
+
* infernoflow cloud memory status --json
|
|
25
32
|
*/
|
|
26
33
|
|
|
27
34
|
import * as fs from "node:fs";
|
|
@@ -117,6 +124,233 @@ function contractHash(contract) {
|
|
|
117
124
|
return crypto.createHash("sha256").update(JSON.stringify(contract)).digest("hex").slice(0, 12);
|
|
118
125
|
}
|
|
119
126
|
|
|
127
|
+
// ── Session memory helpers ────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
const SESSIONS_FILE = "sessions.jsonl";
|
|
130
|
+
|
|
131
|
+
function readMemory(infernoDir) {
|
|
132
|
+
const p = path.join(infernoDir, SESSIONS_FILE);
|
|
133
|
+
if (!fs.existsSync(p)) return [];
|
|
134
|
+
return fs.readFileSync(p, "utf8")
|
|
135
|
+
.split("\n").filter(Boolean)
|
|
136
|
+
.map(l => { try { return JSON.parse(l); } catch { return null; } })
|
|
137
|
+
.filter(Boolean);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function writeMemory(infernoDir, entries) {
|
|
141
|
+
const p = path.join(infernoDir, SESSIONS_FILE);
|
|
142
|
+
fs.writeFileSync(p, entries.map(e => JSON.stringify(e)).join("\n") + "\n", "utf8");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function memoryHash(entries) {
|
|
146
|
+
return crypto.createHash("sha256")
|
|
147
|
+
.update(JSON.stringify(entries.map(e => e.ts + e.summary)))
|
|
148
|
+
.digest("hex").slice(0, 12);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Merge remote memory with local — union by (ts, summary) deduplication.
|
|
153
|
+
* Keeps all local entries + any remote entries not already present.
|
|
154
|
+
*/
|
|
155
|
+
function mergeMemory(local, remote) {
|
|
156
|
+
const localKeys = new Set(local.map(e => `${e.ts}|${e.summary}`));
|
|
157
|
+
const merged = [...local];
|
|
158
|
+
for (const e of remote) {
|
|
159
|
+
if (!localKeys.has(`${e.ts}|${e.summary}`)) {
|
|
160
|
+
merged.push(e);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return merged.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Memory push/pull ──────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
async function pushMemory(args, infernoDir, config, quietly = false) {
|
|
169
|
+
const jsonMode = args.includes("--json");
|
|
170
|
+
const dryRun = args.includes("--dry-run");
|
|
171
|
+
const token = getToken(config, args);
|
|
172
|
+
const endpoint = getEndpoint(config, args);
|
|
173
|
+
const projectId = config?.projectId;
|
|
174
|
+
|
|
175
|
+
if (!token || !projectId) {
|
|
176
|
+
const msg = "No token/project found. Run: infernoflow cloud init";
|
|
177
|
+
if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
|
|
178
|
+
else if (!quietly) warn(msg);
|
|
179
|
+
return { ok: false };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const entries = readMemory(infernoDir);
|
|
183
|
+
if (!entries.length) {
|
|
184
|
+
if (!quietly && !jsonMode) info("No session memory to push (inferno/sessions.jsonl is empty).");
|
|
185
|
+
return { ok: true, entries: 0 };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const hash = memoryHash(entries);
|
|
189
|
+
|
|
190
|
+
if (dryRun) {
|
|
191
|
+
if (jsonMode) console.log(JSON.stringify({ ok: true, dryRun: true, entries: entries.length, hash }));
|
|
192
|
+
else if (!quietly) info(`Dry run — would push ${bold(String(entries.length))} memory entries (hash: ${hash})`);
|
|
193
|
+
return { ok: true, dryRun: true };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const resp = await httpsRequest(
|
|
198
|
+
"PUT",
|
|
199
|
+
`${endpoint}/api/projects/${projectId}/memory`,
|
|
200
|
+
{ entries, hash, pushedAt: new Date().toISOString() },
|
|
201
|
+
token
|
|
202
|
+
);
|
|
203
|
+
const ok_flag = resp.status === 200 || resp.status === 201 || resp.status === 204;
|
|
204
|
+
if (jsonMode) console.log(JSON.stringify({ ok: ok_flag, entries: entries.length, hash }));
|
|
205
|
+
else if (!quietly) {
|
|
206
|
+
if (ok_flag) ok(`Pushed ${bold(String(entries.length))} memory entries`);
|
|
207
|
+
else warn(`Cloud returned ${resp.status}`);
|
|
208
|
+
}
|
|
209
|
+
return { ok: ok_flag, entries: entries.length };
|
|
210
|
+
} catch (err) {
|
|
211
|
+
if (jsonMode) console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
212
|
+
else if (!quietly) warn(`Memory push failed: ${err.message}`);
|
|
213
|
+
return { ok: false };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function pullMemory(args, infernoDir, config, quietly = false) {
|
|
218
|
+
const jsonMode = args.includes("--json");
|
|
219
|
+
const dryRun = args.includes("--dry-run");
|
|
220
|
+
const token = getToken(config, args);
|
|
221
|
+
const endpoint = getEndpoint(config, args);
|
|
222
|
+
const projectId = config?.projectId;
|
|
223
|
+
const forceOverwrite = args.includes("--force") || args.includes("-f");
|
|
224
|
+
|
|
225
|
+
if (!token || !projectId) {
|
|
226
|
+
const msg = "No token/project found. Run: infernoflow cloud init";
|
|
227
|
+
if (jsonMode) console.log(JSON.stringify({ ok: false, error: msg }));
|
|
228
|
+
else if (!quietly) warn(msg);
|
|
229
|
+
return { ok: false };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const resp = await httpsRequest(
|
|
234
|
+
"GET",
|
|
235
|
+
`${endpoint}/api/projects/${projectId}/memory`,
|
|
236
|
+
null,
|
|
237
|
+
token
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (resp.status !== 200) {
|
|
241
|
+
const errMsg = `Cloud returned ${resp.status}`;
|
|
242
|
+
if (jsonMode) console.log(JSON.stringify({ ok: false, error: errMsg }));
|
|
243
|
+
else if (!quietly) warn(errMsg);
|
|
244
|
+
return { ok: false };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const remote = resp.body?.entries;
|
|
248
|
+
if (!remote || !remote.length) {
|
|
249
|
+
if (!quietly && !jsonMode) info("No session memory in cloud yet. Push first.");
|
|
250
|
+
return { ok: true, entries: 0 };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const local = readMemory(infernoDir);
|
|
254
|
+
const merged = forceOverwrite ? remote : mergeMemory(local, remote);
|
|
255
|
+
const newCount = merged.length - local.length;
|
|
256
|
+
|
|
257
|
+
if (dryRun) {
|
|
258
|
+
if (jsonMode) console.log(JSON.stringify({ ok: true, dryRun: true, remote: remote.length, local: local.length, merged: merged.length }));
|
|
259
|
+
else if (!quietly) info(`Dry run — would merge ${bold(String(remote.length))} remote + ${bold(String(local.length))} local = ${bold(String(merged.length))} entries`);
|
|
260
|
+
return { ok: true, dryRun: true };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
writeMemory(infernoDir, merged);
|
|
264
|
+
|
|
265
|
+
if (jsonMode) console.log(JSON.stringify({ ok: true, remote: remote.length, local: local.length, merged: merged.length, newEntries: newCount }));
|
|
266
|
+
else if (!quietly) ok(`Merged ${bold(String(remote.length))} remote entries → ${bold(String(merged.length))} total (${newCount} new)`);
|
|
267
|
+
return { ok: true, entries: merged.length };
|
|
268
|
+
} catch (err) {
|
|
269
|
+
if (jsonMode) console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
270
|
+
else if (!quietly) warn(`Memory pull failed: ${err.message}`);
|
|
271
|
+
return { ok: false };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function subcmdMemory(args, cwd, infernoDir) {
|
|
276
|
+
const jsonMode = args.includes("--json");
|
|
277
|
+
const config = readCloudConfig(infernoDir);
|
|
278
|
+
const token = getToken(config, args);
|
|
279
|
+
const endpoint = getEndpoint(config, args);
|
|
280
|
+
|
|
281
|
+
const sub2 = args[0];
|
|
282
|
+
const sub2Args = args.slice(1);
|
|
283
|
+
|
|
284
|
+
if (sub2 === "push") {
|
|
285
|
+
if (!jsonMode) header("Pushing session memory to cloud");
|
|
286
|
+
return pushMemory(sub2Args, infernoDir, config);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (sub2 === "pull") {
|
|
290
|
+
if (!jsonMode) header("Pulling session memory from cloud");
|
|
291
|
+
return pullMemory(sub2Args, infernoDir, config);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (sub2 === "status" || !sub2) {
|
|
295
|
+
const local = readMemory(infernoDir);
|
|
296
|
+
const projectId = config?.projectId;
|
|
297
|
+
|
|
298
|
+
if (!config || !token) {
|
|
299
|
+
if (jsonMode) console.log(JSON.stringify({ ok: false, error: "Not initialised. Run: infernoflow cloud init" }));
|
|
300
|
+
else warn("Cloud not configured. Run: infernoflow cloud init");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let remoteCount = null;
|
|
305
|
+
let remoteHash = null;
|
|
306
|
+
let reachable = false;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const resp = await httpsRequest("GET", `${endpoint}/api/projects/${projectId}/memory`, null, token);
|
|
310
|
+
if (resp.status === 200 && resp.body?.entries) {
|
|
311
|
+
reachable = true;
|
|
312
|
+
remoteCount = resp.body.entries.length;
|
|
313
|
+
remoteHash = memoryHash(resp.body.entries);
|
|
314
|
+
}
|
|
315
|
+
} catch {}
|
|
316
|
+
|
|
317
|
+
const localHash = local.length ? memoryHash(local) : null;
|
|
318
|
+
|
|
319
|
+
if (jsonMode) {
|
|
320
|
+
console.log(JSON.stringify({
|
|
321
|
+
ok: true,
|
|
322
|
+
local: { entries: local.length, hash: localHash },
|
|
323
|
+
remote: reachable ? { entries: remoteCount, hash: remoteHash } : null,
|
|
324
|
+
reachable,
|
|
325
|
+
inSync: localHash === remoteHash,
|
|
326
|
+
}));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log();
|
|
331
|
+
console.log(` ${bold("infernoflow cloud memory status")}`);
|
|
332
|
+
console.log();
|
|
333
|
+
console.log(` Local: ${bold(String(local.length))} entries ${gray("(hash: " + (localHash || "none") + ")")}`);
|
|
334
|
+
if (!reachable) {
|
|
335
|
+
console.log(` Cloud: ${yellow("unreachable")}`);
|
|
336
|
+
} else {
|
|
337
|
+
console.log(` Cloud: ${bold(String(remoteCount))} entries ${gray("(hash: " + (remoteHash || "none") + ")")}`);
|
|
338
|
+
if (localHash === remoteHash) console.log(`\n ${green("✔")} Memory in sync`);
|
|
339
|
+
else console.log(`\n ${yellow("⚠")} Out of sync — run ${cyan("infernoflow cloud memory push")} or ${cyan("infernoflow cloud memory pull")}`);
|
|
340
|
+
}
|
|
341
|
+
console.log();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
console.log();
|
|
346
|
+
console.log(` ${bold("infernoflow cloud memory")} — session memory sync`);
|
|
347
|
+
console.log();
|
|
348
|
+
console.log(` ${cyan("infernoflow cloud memory push")} Upload sessions.jsonl to cloud`);
|
|
349
|
+
console.log(` ${cyan("infernoflow cloud memory pull")} Download + merge remote memory`);
|
|
350
|
+
console.log(` ${cyan("infernoflow cloud memory status")} Compare local vs remote`);
|
|
351
|
+
console.log();
|
|
352
|
+
}
|
|
353
|
+
|
|
120
354
|
// ── Sub-commands ──────────────────────────────────────────────────────────────
|
|
121
355
|
|
|
122
356
|
async function subcmdInit(args, cwd, infernoDir) {
|
|
@@ -243,6 +477,13 @@ async function subcmdPush(args, cwd, infernoDir) {
|
|
|
243
477
|
console.log(` ${gray("Dashboard:")} ${cyan(`${endpoint}/p/${projectId}`)}`);
|
|
244
478
|
console.log();
|
|
245
479
|
}
|
|
480
|
+
|
|
481
|
+
// Also push session memory if --memory flag set
|
|
482
|
+
if (args.includes("--memory")) {
|
|
483
|
+
if (!jsonMode) info("Pushing session memory...");
|
|
484
|
+
await pushMemory(args, infernoDir, config, jsonMode);
|
|
485
|
+
if (!jsonMode) ok("Session memory pushed");
|
|
486
|
+
}
|
|
246
487
|
} else {
|
|
247
488
|
const errMsg = `Cloud returned ${resp.status}`;
|
|
248
489
|
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: errMsg, status: resp.status })); }
|
|
@@ -351,6 +592,12 @@ async function subcmdPull(args, cwd, infernoDir) {
|
|
|
351
592
|
if (onlyLocal.length) warn(`${onlyLocal.length} local-only capabilities were overwritten.`);
|
|
352
593
|
console.log();
|
|
353
594
|
}
|
|
595
|
+
|
|
596
|
+
// Also pull session memory if --memory flag set
|
|
597
|
+
if (args.includes("--memory")) {
|
|
598
|
+
if (!jsonMode) info("Pulling session memory...");
|
|
599
|
+
await pullMemory(args, infernoDir, config, jsonMode);
|
|
600
|
+
}
|
|
354
601
|
} catch (err) {
|
|
355
602
|
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: err.message })); }
|
|
356
603
|
else { warn(`Cloud unreachable: ${err.message}`); }
|
|
@@ -501,19 +748,24 @@ export async function cloudCommand(rawArgs) {
|
|
|
501
748
|
return subcmdStatus(subArgs, cwd, infernoDir);
|
|
502
749
|
case "dashboard":
|
|
503
750
|
return subcmdDashboard(subArgs, cwd, infernoDir);
|
|
751
|
+
case "memory":
|
|
752
|
+
return subcmdMemory(subArgs, cwd, infernoDir);
|
|
504
753
|
default: {
|
|
505
754
|
const jsonMode = args.includes("--json");
|
|
506
|
-
const msg = `Unknown cloud sub-command: ${subcmd || "(none)"}. Use: init | push | pull | status | dashboard`;
|
|
755
|
+
const msg = `Unknown cloud sub-command: ${subcmd || "(none)"}. Use: init | push | pull | memory | status | dashboard`;
|
|
507
756
|
if (jsonMode) { console.log(JSON.stringify({ ok: false, error: msg })); }
|
|
508
757
|
else {
|
|
509
758
|
console.log();
|
|
510
|
-
console.log(` ${bold("infernoflow cloud")} — hosted contract sync`);
|
|
759
|
+
console.log(` ${bold("infernoflow cloud")} — hosted contract + memory sync`);
|
|
511
760
|
console.log();
|
|
512
|
-
console.log(` ${cyan("infernoflow cloud init")}
|
|
513
|
-
console.log(` ${cyan("infernoflow cloud push")}
|
|
514
|
-
console.log(` ${cyan("infernoflow cloud
|
|
515
|
-
console.log(` ${cyan("infernoflow cloud
|
|
516
|
-
console.log(` ${cyan("infernoflow cloud
|
|
761
|
+
console.log(` ${cyan("infernoflow cloud init")} Set up cloud sync for this project`);
|
|
762
|
+
console.log(` ${cyan("infernoflow cloud push")} Upload local contract to cloud`);
|
|
763
|
+
console.log(` ${cyan("infernoflow cloud push --memory")} Also push sessions.jsonl`);
|
|
764
|
+
console.log(` ${cyan("infernoflow cloud pull")} Download latest contract from cloud`);
|
|
765
|
+
console.log(` ${cyan("infernoflow cloud pull --memory")} Also pull + merge session memory`);
|
|
766
|
+
console.log(` ${cyan("infernoflow cloud memory push/pull")} Session memory only`);
|
|
767
|
+
console.log(` ${cyan("infernoflow cloud status")} Compare local vs cloud`);
|
|
768
|
+
console.log(` ${cyan("infernoflow cloud dashboard")} Open hosted dashboard in browser`);
|
|
517
769
|
console.log();
|
|
518
770
|
}
|
|
519
771
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow feedback
|
|
3
|
+
*
|
|
4
|
+
* Collects in-CLI feedback about infernoflow and optionally opens the web form.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* infernoflow feedback Interactive 5-question survey
|
|
8
|
+
* infernoflow feedback --form Open Google Form in browser
|
|
9
|
+
* infernoflow feedback --json Print last stored feedback as JSON
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import * as os from "node:os";
|
|
15
|
+
import * as readline from "node:readline";
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
18
|
+
|
|
19
|
+
const FEEDBACK_FORM_URL = "https://forms.gle/infernoflow-feedback"; // placeholder — replace with real form
|
|
20
|
+
const FEEDBACK_FILE = path.join(os.homedir(), ".infernoflow", "feedback.json");
|
|
21
|
+
|
|
22
|
+
const QUESTIONS = [
|
|
23
|
+
{
|
|
24
|
+
id: "usage",
|
|
25
|
+
label: "How often do you use infernoflow?",
|
|
26
|
+
choices: ["daily", "a few times a week", "rarely", "just started"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "ide",
|
|
30
|
+
label: "Which IDE are you using?",
|
|
31
|
+
choices: ["VS Code + Copilot", "Cursor", "Claude Code", "Windsurf", "Other"],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "top_command",
|
|
35
|
+
label: "Which infernoflow command do you use most?",
|
|
36
|
+
choices: ["log", "switch", "recap", "status / check", "context", "other"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "missing",
|
|
40
|
+
label: "What feature do you wish infernoflow had?",
|
|
41
|
+
freeText: true,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "email",
|
|
45
|
+
label: "Email (optional — for follow-up questions):",
|
|
46
|
+
freeText: true,
|
|
47
|
+
optional: true,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function saveFeedback(responses) {
|
|
52
|
+
const dir = path.dirname(FEEDBACK_FILE);
|
|
53
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const record = {
|
|
56
|
+
ts: new Date().toISOString(),
|
|
57
|
+
version: getVersion(),
|
|
58
|
+
responses,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Append to array
|
|
62
|
+
let existing = [];
|
|
63
|
+
if (fs.existsSync(FEEDBACK_FILE)) {
|
|
64
|
+
try { existing = JSON.parse(fs.readFileSync(FEEDBACK_FILE, "utf8")); } catch {}
|
|
65
|
+
}
|
|
66
|
+
existing.push(record);
|
|
67
|
+
fs.writeFileSync(FEEDBACK_FILE, JSON.stringify(existing, null, 2), "utf8");
|
|
68
|
+
return record;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getVersion() {
|
|
72
|
+
try {
|
|
73
|
+
const pkgPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../package.json");
|
|
74
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
|
|
75
|
+
} catch {
|
|
76
|
+
return "unknown";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function openBrowser(url) {
|
|
81
|
+
const platform = process.platform;
|
|
82
|
+
try {
|
|
83
|
+
if (platform === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
|
|
84
|
+
else if (platform === "win32") execSync(`start "" "${url}"`, { stdio: "ignore" });
|
|
85
|
+
else execSync(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
86
|
+
return true;
|
|
87
|
+
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function prompt(rl, question) {
|
|
93
|
+
return new Promise(resolve => rl.question(question, resolve));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function runSurvey() {
|
|
97
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
98
|
+
|
|
99
|
+
console.log("\n " + bold("🔥 infernoflow feedback") + "\n");
|
|
100
|
+
console.log(gray(" Takes ~60 seconds. Helps make infernoflow better.\n"));
|
|
101
|
+
|
|
102
|
+
const responses = {};
|
|
103
|
+
|
|
104
|
+
for (const q of QUESTIONS) {
|
|
105
|
+
console.log(cyan(` ${q.label}`));
|
|
106
|
+
|
|
107
|
+
if (q.choices) {
|
|
108
|
+
q.choices.forEach((c, i) => console.log(gray(` ${i + 1}. ${c}`)));
|
|
109
|
+
const raw = await prompt(rl, " → ");
|
|
110
|
+
const idx = parseInt(raw.trim()) - 1;
|
|
111
|
+
responses[q.id] = (idx >= 0 && idx < q.choices.length) ? q.choices[idx] : raw.trim();
|
|
112
|
+
} else {
|
|
113
|
+
const raw = await prompt(rl, " → ");
|
|
114
|
+
responses[q.id] = raw.trim() || (q.optional ? null : "—");
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
rl.close();
|
|
120
|
+
|
|
121
|
+
const record = saveFeedback(responses);
|
|
122
|
+
|
|
123
|
+
console.log(green(" ✔ Feedback saved — thank you!\n"));
|
|
124
|
+
console.log(gray(" Stored in: ~/.infernoflow/feedback.json"));
|
|
125
|
+
console.log(gray(` Version: ${record.version}`));
|
|
126
|
+
|
|
127
|
+
// Nudge to share
|
|
128
|
+
console.log(gray("\n To share more detail or attach files, run: infernoflow feedback --form\n"));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function feedbackCommand(args) {
|
|
132
|
+
const has = (f) => args.includes(f);
|
|
133
|
+
|
|
134
|
+
// ── --form mode ─────────────────────────────────────────────────────────────
|
|
135
|
+
if (has("--form")) {
|
|
136
|
+
console.log(cyan(`\n Opening feedback form → ${FEEDBACK_FORM_URL}\n`));
|
|
137
|
+
const opened = openBrowser(FEEDBACK_FORM_URL);
|
|
138
|
+
if (!opened) {
|
|
139
|
+
console.log(yellow(" Could not open browser automatically."));
|
|
140
|
+
console.log(gray(` Please open manually: ${FEEDBACK_FORM_URL}\n`));
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── --json mode ──────────────────────────────────────────────────────────────
|
|
146
|
+
if (has("--json")) {
|
|
147
|
+
if (!fs.existsSync(FEEDBACK_FILE)) {
|
|
148
|
+
console.log(JSON.stringify([], null, 2));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const data = JSON.parse(fs.readFileSync(FEEDBACK_FILE, "utf8"));
|
|
153
|
+
console.log(JSON.stringify(data, null, 2));
|
|
154
|
+
} catch {
|
|
155
|
+
console.log(JSON.stringify([], null, 2));
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Interactive survey ───────────────────────────────────────────────────────
|
|
161
|
+
if (!process.stdin.isTTY) {
|
|
162
|
+
console.log(red(" ✘ infernoflow feedback requires an interactive terminal.\n"));
|
|
163
|
+
console.log(gray(" Run in a terminal or use: infernoflow feedback --form\n"));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await runSurvey();
|
|
168
|
+
}
|
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* infernoflow log --show 5 Print last 5 entries
|
|
16
16
|
* infernoflow log --clear Archive and clear the log
|
|
17
17
|
* infernoflow log --json Print entries as JSON array
|
|
18
|
+
*
|
|
19
|
+
* Auto-capture flags (for git hooks / automation):
|
|
20
|
+
* infernoflow log "..." --auto Mark as auto-captured; silent exit if no inferno/
|
|
21
|
+
* infernoflow log "..." --quiet Suppress all output
|
|
22
|
+
* infernoflow log "..." --source git-hook Tag the origin of this log entry
|
|
18
23
|
*/
|
|
19
24
|
|
|
20
25
|
import * as fs from "node:fs";
|
|
@@ -37,12 +42,14 @@ function readEntries() {
|
|
|
37
42
|
.filter(Boolean);
|
|
38
43
|
}
|
|
39
44
|
|
|
40
|
-
function appendEntry(entry) {
|
|
45
|
+
function appendEntry(entry, { auto = false, quiet = false } = {}) {
|
|
41
46
|
if (!fs.existsSync(INFERNO_DIR)) {
|
|
42
|
-
|
|
47
|
+
if (auto) return false; // silently skip — hook running in non-inferno project
|
|
48
|
+
if (!quiet) console.error(red(" ✘ inferno/ not found — run: infernoflow init\n"));
|
|
43
49
|
process.exit(1);
|
|
44
50
|
}
|
|
45
51
|
fs.appendFileSync(SESSIONS_FILE, JSON.stringify(entry) + "\n", "utf8");
|
|
52
|
+
return true;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
function detectAgent() {
|
|
@@ -78,6 +85,9 @@ export async function logCommand(args) {
|
|
|
78
85
|
const showFlag = has("--show");
|
|
79
86
|
const clearFlag = has("--clear");
|
|
80
87
|
const jsonFlag = has("--json");
|
|
88
|
+
const autoFlag = has("--auto"); // auto-captured; silent exit if no inferno/
|
|
89
|
+
const quietFlag = has("--quiet"); // suppress all console output
|
|
90
|
+
const source = flag("--source", null); // origin tag, e.g. "git-hook"
|
|
81
91
|
|
|
82
92
|
// ── Show mode ───────────────────────────────────────────────────────────────
|
|
83
93
|
if (showFlag || jsonFlag) {
|
|
@@ -116,8 +126,11 @@ export async function logCommand(args) {
|
|
|
116
126
|
}
|
|
117
127
|
|
|
118
128
|
// ── Append mode ─────────────────────────────────────────────────────────────
|
|
119
|
-
// Collect the message — everything that's not a flag
|
|
120
|
-
const
|
|
129
|
+
// Collect the message — everything that's not a flag or a flag value
|
|
130
|
+
const flagValues = new Set([
|
|
131
|
+
flag("--type",""), flag("--result",""), flag("--agent",""), flag("--source","")
|
|
132
|
+
].filter(Boolean));
|
|
133
|
+
const messageTokens = args.filter(a => !a.startsWith("--") && !flagValues.has(a));
|
|
121
134
|
const summary = messageTokens.join(" ").trim();
|
|
122
135
|
|
|
123
136
|
if (!summary) {
|
|
@@ -131,7 +144,8 @@ export async function logCommand(args) {
|
|
|
131
144
|
console.log(gray(' infernoflow log --json Print as JSON'));
|
|
132
145
|
console.log();
|
|
133
146
|
console.log(gray(" Types: note · attempt · decision · gotcha · preference · theme · handoff · error"));
|
|
134
|
-
console.log(gray(" Results: worked · failed · partial · unknown
|
|
147
|
+
console.log(gray(" Results: worked · failed · partial · unknown"));
|
|
148
|
+
console.log(gray(" Auto-capture: --auto (silent skip if no inferno/) · --quiet · --source <name>\n"));
|
|
135
149
|
return;
|
|
136
150
|
}
|
|
137
151
|
|
|
@@ -140,11 +154,11 @@ export async function logCommand(args) {
|
|
|
140
154
|
const agent = flag("--agent", detectAgent());
|
|
141
155
|
|
|
142
156
|
if (!VALID_TYPES.includes(type)) {
|
|
143
|
-
console.error(red(` ✘ Invalid type: ${type}. Valid: ${VALID_TYPES.join(", ")}\n`));
|
|
157
|
+
if (!quietFlag) console.error(red(` ✘ Invalid type: ${type}. Valid: ${VALID_TYPES.join(", ")}\n`));
|
|
144
158
|
process.exit(1);
|
|
145
159
|
}
|
|
146
160
|
if (result && !VALID_RESULTS.includes(result)) {
|
|
147
|
-
console.error(red(` ✘ Invalid result: ${result}. Valid: ${VALID_RESULTS.join(", ")}\n`));
|
|
161
|
+
if (!quietFlag) console.error(red(` ✘ Invalid result: ${result}. Valid: ${VALID_RESULTS.join(", ")}\n`));
|
|
148
162
|
process.exit(1);
|
|
149
163
|
}
|
|
150
164
|
|
|
@@ -153,12 +167,18 @@ export async function logCommand(args) {
|
|
|
153
167
|
agent,
|
|
154
168
|
type,
|
|
155
169
|
summary,
|
|
156
|
-
...(result
|
|
170
|
+
...(result ? { result } : {}),
|
|
171
|
+
...(source ? { source } : {}),
|
|
172
|
+
...(autoFlag ? { auto: true } : {}),
|
|
157
173
|
};
|
|
158
174
|
|
|
159
|
-
appendEntry(entry);
|
|
175
|
+
const written = appendEntry(entry, { auto: autoFlag, quiet: quietFlag });
|
|
176
|
+
if (!written) return; // auto mode, no inferno/ — skip silently
|
|
160
177
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
if (!quietFlag) {
|
|
179
|
+
const typeLabel = type !== "note" ? cyan(` [${type}]`) : "";
|
|
180
|
+
const resultLabel = result ? gray(` → ${result}`) : "";
|
|
181
|
+
const sourceLabel = source ? gray(` (via ${source})`) : "";
|
|
182
|
+
console.log(green(` ✔ Logged${typeLabel}${resultLabel}${sourceLabel}: `) + summary + "\n");
|
|
183
|
+
}
|
|
164
184
|
}
|
|
@@ -68,7 +68,7 @@ function installGitHooks(cwd, templatesRoot, force) {
|
|
|
68
68
|
|
|
69
69
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
70
70
|
|
|
71
|
-
const hooks = ["post-commit", "pre-push"];
|
|
71
|
+
const hooks = ["post-commit", "pre-push", "pre-stash"];
|
|
72
72
|
const installed = [];
|
|
73
73
|
|
|
74
74
|
for (const hookName of hooks) {
|
|
@@ -27,15 +27,17 @@
|
|
|
27
27
|
|
|
28
28
|
import * as fs from "node:fs";
|
|
29
29
|
import * as path from "node:path";
|
|
30
|
+
import * as os from "node:os";
|
|
30
31
|
import { execSync } from "node:child_process";
|
|
31
32
|
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
32
33
|
|
|
33
|
-
const INFERNO_DIR
|
|
34
|
-
const HANDOFF_FILE
|
|
35
|
-
const SESSIONS_FILE
|
|
36
|
-
const STATE_FILE
|
|
37
|
-
const CONTRACT_FILE
|
|
38
|
-
const THEME_FILE
|
|
34
|
+
const INFERNO_DIR = "inferno";
|
|
35
|
+
const HANDOFF_FILE = path.join(INFERNO_DIR, "HANDOFF.md");
|
|
36
|
+
const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
|
|
37
|
+
const STATE_FILE = path.join(INFERNO_DIR, "context-state.json");
|
|
38
|
+
const CONTRACT_FILE = path.join(INFERNO_DIR, "contract.json");
|
|
39
|
+
const THEME_FILE = path.join(INFERNO_DIR, "theme.json");
|
|
40
|
+
const ADOPTION_FILE = path.join(INFERNO_DIR, "adoption_profile.json");
|
|
39
41
|
|
|
40
42
|
function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } }
|
|
41
43
|
function readFile(f) { try { return fs.readFileSync(f, "utf8"); } catch { return null; } }
|
|
@@ -45,6 +47,14 @@ function fmtDate(iso) {
|
|
|
45
47
|
return new Date(iso).toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
function fmtDuration(ms) {
|
|
51
|
+
if (ms < 0) return "unknown";
|
|
52
|
+
const h = Math.floor(ms / 3600000);
|
|
53
|
+
const m = Math.floor((ms % 3600000) / 60000);
|
|
54
|
+
if (h > 0) return `${h}h ${m}m`;
|
|
55
|
+
return `${m}m`;
|
|
56
|
+
}
|
|
57
|
+
|
|
48
58
|
function getAllEntries() {
|
|
49
59
|
if (!fs.existsSync(SESSIONS_FILE)) return [];
|
|
50
60
|
return fs.readFileSync(SESSIONS_FILE, "utf8")
|
|
@@ -92,37 +102,139 @@ function copyToClipboard(text) {
|
|
|
92
102
|
} catch { return false; }
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
/** Detect current IDE from environment */
|
|
106
|
+
function detectIde() {
|
|
107
|
+
if (process.env.CURSOR_SESSION) return "Cursor";
|
|
108
|
+
if (process.env.COPILOT_SESSION) return "GitHub Copilot";
|
|
109
|
+
if (process.env.CLAUDE_CODE_SESSION) return "Claude Code";
|
|
110
|
+
if (process.env.WINDSURF_SESSION) return "Windsurf";
|
|
111
|
+
if (process.env.TERM_PROGRAM === "vscode") return "VS Code";
|
|
112
|
+
// Check adoption profile
|
|
113
|
+
const profile = readJSON(ADOPTION_FILE);
|
|
114
|
+
if (profile?.ide) return profile.ide;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Get git diff stat since last commit */
|
|
119
|
+
function getGitDiffStat() {
|
|
120
|
+
try {
|
|
121
|
+
const stat = execSync("git diff --stat HEAD 2>/dev/null || git diff --cached --stat 2>/dev/null", {
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
124
|
+
}).trim();
|
|
125
|
+
if (!stat) {
|
|
126
|
+
// Try HEAD~1 if nothing staged/unstaged
|
|
127
|
+
const logStat = execSync("git log --stat -1 --pretty= 2>/dev/null", {
|
|
128
|
+
encoding: "utf8",
|
|
129
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
130
|
+
}).trim();
|
|
131
|
+
return logStat || null;
|
|
132
|
+
}
|
|
133
|
+
return stat;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Get recent git commits in this session */
|
|
140
|
+
function getGitCommits(since) {
|
|
141
|
+
try {
|
|
142
|
+
const sinceArg = since ? `--after="${since.toISOString()}"` : "-5";
|
|
143
|
+
const log = execSync(`git log ${sinceArg} --pretty=format:"%h %s" 2>/dev/null`, {
|
|
144
|
+
encoding: "utf8",
|
|
145
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
146
|
+
}).trim();
|
|
147
|
+
return log ? log.split("\n").filter(Boolean) : [];
|
|
148
|
+
} catch {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Detect "open threads" — attempts without resolution + entries containing TODO/WIP markers */
|
|
154
|
+
function findOpenThreads(sessions) {
|
|
155
|
+
const open = [];
|
|
156
|
+
|
|
157
|
+
// Failed/partial attempts that were never followed by a worked attempt
|
|
158
|
+
const attempts = sessions.filter(e => e.type === "attempt" && (e.result === "failed" || e.result === "partial" || !e.result));
|
|
159
|
+
for (const a of attempts) {
|
|
160
|
+
const followedByWorked = sessions.find(e =>
|
|
161
|
+
e.type === "attempt" &&
|
|
162
|
+
e.result === "worked" &&
|
|
163
|
+
new Date(e.ts) > new Date(a.ts) &&
|
|
164
|
+
e.summary.toLowerCase().includes(a.summary.split(" ")[0].toLowerCase())
|
|
165
|
+
);
|
|
166
|
+
if (!followedByWorked) {
|
|
167
|
+
open.push({ text: a.summary, ts: a.ts, kind: "unresolved-attempt" });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Any entry explicitly marked TODO/WIP in summary
|
|
172
|
+
for (const e of sessions) {
|
|
173
|
+
if (/\b(TODO|WIP|FIXME|BLOCKED|pending)\b/i.test(e.summary)) {
|
|
174
|
+
if (!open.find(o => o.text === e.summary)) {
|
|
175
|
+
open.push({ text: e.summary, ts: e.ts, kind: "flagged" });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return open.slice(0, 8); // cap at 8
|
|
181
|
+
}
|
|
182
|
+
|
|
95
183
|
function buildHandoff(toAgent, sinceArg, allFlag) {
|
|
96
184
|
const state = readJSON(STATE_FILE) || {};
|
|
97
185
|
const contract = readJSON(CONTRACT_FILE) || {};
|
|
98
186
|
const theme = readJSON(THEME_FILE);
|
|
187
|
+
const adoption = readJSON(ADOPTION_FILE);
|
|
99
188
|
const allEntries = getAllEntries();
|
|
100
189
|
const sessionStart = findSessionStart(allEntries, sinceArg, allFlag);
|
|
101
|
-
// Session entries: everything since boundary; also keep last 5 for "recent log"
|
|
102
190
|
const sessions = allEntries.filter(e => new Date(e.ts || 0) > sessionStart);
|
|
103
|
-
const recentFallback = allEntries.slice(-5);
|
|
104
|
-
const now = new Date()
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
191
|
+
const recentFallback = allEntries.slice(-5);
|
|
192
|
+
const now = new Date();
|
|
193
|
+
const nowStr = now.toLocaleString("en-GB", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" });
|
|
194
|
+
|
|
195
|
+
const projectId = contract.policyId || path.basename(process.cwd());
|
|
196
|
+
const version = contract.policyVersion || "?";
|
|
197
|
+
const caps = (contract.capabilities || []).slice(0, 20);
|
|
198
|
+
const ide = detectIde();
|
|
199
|
+
|
|
200
|
+
// Session metadata
|
|
201
|
+
const sessionDurationMs = sessionStart.getTime() > 0 ? now - sessionStart : -1;
|
|
202
|
+
const sessionDuration = fmtDuration(sessionDurationMs);
|
|
203
|
+
// Short session ID: hex of sessionStart ms
|
|
204
|
+
const sessionId = sessionStart.getTime() > 0
|
|
205
|
+
? sessionStart.getTime().toString(16).slice(-6).toUpperCase()
|
|
206
|
+
: "ALL";
|
|
207
|
+
|
|
208
|
+
// Git data
|
|
209
|
+
const commits = getGitCommits(sessionStart.getTime() > 0 ? sessionStart : null);
|
|
210
|
+
const diffStat = getGitDiffStat();
|
|
211
|
+
|
|
212
|
+
// Memory pool
|
|
111
213
|
const pool = sessions.length > 0 ? sessions : recentFallback;
|
|
112
214
|
const gotchas = pool.filter(e => e.type === "gotcha");
|
|
113
215
|
const decisions = pool.filter(e => e.type === "decision");
|
|
114
216
|
const attempts = pool.filter(e => e.type === "attempt").filter(e => e.result === "failed" || e.result === "partial");
|
|
115
217
|
const prefs = pool.filter(e => e.type === "preference");
|
|
116
|
-
const recent = pool.slice(-8);
|
|
218
|
+
const recent = pool.slice(-8);
|
|
219
|
+
const openThreads = findOpenThreads(pool);
|
|
117
220
|
|
|
118
221
|
const sinceStr = sessionStart.getTime() === 0
|
|
119
222
|
? "all time"
|
|
120
223
|
: sessionStart.toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
|
|
121
224
|
|
|
225
|
+
// ── Sources header ─────────────────────────────────────────────────────────
|
|
226
|
+
const sourcesList = ["sessions.jsonl"];
|
|
227
|
+
if (state.working || state.intent) sourcesList.push("context-state.json");
|
|
228
|
+
if (theme) sourcesList.push("theme.json");
|
|
229
|
+
if (contract.capabilities?.length) sourcesList.push("contract.json");
|
|
230
|
+
if (adoption) sourcesList.push("adoption_profile.json");
|
|
231
|
+
if (commits.length) sourcesList.push("git log");
|
|
232
|
+
|
|
122
233
|
const lines = [
|
|
123
234
|
`# 🔥 infernoflow Handoff — ${projectId}`,
|
|
124
|
-
`> Generated: ${
|
|
125
|
-
`> Session
|
|
235
|
+
`> Generated: ${nowStr}${toAgent ? ` | Handing off to: **${toAgent}**` : ""}`,
|
|
236
|
+
`> Session: **#${sessionId}** · ${sessionDuration} · **${sessions.length} entries** since ${sinceStr}`,
|
|
237
|
+
`> Sources: ${sourcesList.join(" · ")}${ide ? ` · IDE: ${ide}` : ""}`,
|
|
126
238
|
"",
|
|
127
239
|
"---",
|
|
128
240
|
"",
|
|
@@ -139,6 +251,16 @@ function buildHandoff(toAgent, sinceArg, allFlag) {
|
|
|
139
251
|
lines.push("_No working state set. Run: `infernoflow context --working \"...\"` to set it._", "");
|
|
140
252
|
}
|
|
141
253
|
|
|
254
|
+
// ── Open threads — unresolved items ───────────────────────────────────────
|
|
255
|
+
if (openThreads.length) {
|
|
256
|
+
lines.push("## 🔓 Open threads — not yet resolved", "");
|
|
257
|
+
for (const t of openThreads) {
|
|
258
|
+
const badge = t.kind === "flagged" ? "[flagged]" : "[unresolved]";
|
|
259
|
+
lines.push(`- ${badge} ${t.text} _(${fmtDate(t.ts)})_`);
|
|
260
|
+
}
|
|
261
|
+
lines.push("");
|
|
262
|
+
}
|
|
263
|
+
|
|
142
264
|
// ── Gotchas first — most critical for a new agent ─────────────────────────
|
|
143
265
|
if (gotchas.length) {
|
|
144
266
|
lines.push("## ⚠ Gotchas — read these first", "");
|
|
@@ -176,6 +298,23 @@ function buildHandoff(toAgent, sinceArg, allFlag) {
|
|
|
176
298
|
lines.push("");
|
|
177
299
|
}
|
|
178
300
|
|
|
301
|
+
// ── Git activity this session ──────────────────────────────────────────────
|
|
302
|
+
if (commits.length || diffStat) {
|
|
303
|
+
lines.push("## Git activity this session", "");
|
|
304
|
+
if (commits.length) {
|
|
305
|
+
lines.push("**Commits:**");
|
|
306
|
+
for (const c of commits) lines.push(`- \`${c}\``);
|
|
307
|
+
lines.push("");
|
|
308
|
+
}
|
|
309
|
+
if (diffStat) {
|
|
310
|
+
lines.push("**Uncommitted changes:**");
|
|
311
|
+
lines.push("```");
|
|
312
|
+
lines.push(diffStat.split("\n").slice(0, 15).join("\n")); // cap at 15 lines
|
|
313
|
+
lines.push("```");
|
|
314
|
+
lines.push("");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
179
318
|
// ── Design system ─────────────────────────────────────────────────────────
|
|
180
319
|
if (theme) {
|
|
181
320
|
lines.push("## Design system", "");
|
|
@@ -206,13 +345,14 @@ function buildHandoff(toAgent, sinceArg, allFlag) {
|
|
|
206
345
|
lines.push("## Recent session log", "");
|
|
207
346
|
for (const e of recent) {
|
|
208
347
|
const result = e.result ? ` [${e.result}]` : "";
|
|
209
|
-
|
|
348
|
+
const src = e.source ? ` {${e.source}}` : "";
|
|
349
|
+
lines.push(`- **${e.type}**${result}${src}: ${e.summary} _(${fmtDate(e.ts)})_`);
|
|
210
350
|
}
|
|
211
351
|
lines.push("");
|
|
212
352
|
}
|
|
213
353
|
|
|
214
354
|
lines.push("---");
|
|
215
|
-
lines.push(
|
|
355
|
+
lines.push(`_Session #${sessionId} · ${sessionDuration} · Generated by infernoflow._`);
|
|
216
356
|
|
|
217
357
|
return lines.join("\n");
|
|
218
358
|
}
|
|
@@ -252,10 +392,25 @@ export async function switchCommand(args) {
|
|
|
252
392
|
const state = readJSON(STATE_FILE) || {};
|
|
253
393
|
const contract = readJSON(CONTRACT_FILE) || {};
|
|
254
394
|
const theme = readJSON(THEME_FILE);
|
|
395
|
+
const adoption = readJSON(ADOPTION_FILE);
|
|
255
396
|
const allEntries = getAllEntries();
|
|
256
397
|
const sessionStart = findSessionStart(allEntries, sinceArg, allFlag);
|
|
257
398
|
const sessions = allEntries.filter(e => new Date(e.ts || 0) > sessionStart);
|
|
258
|
-
|
|
399
|
+
const commits = getGitCommits(sessionStart.getTime() > 0 ? sessionStart : null);
|
|
400
|
+
const ide = detectIde();
|
|
401
|
+
console.log(JSON.stringify({
|
|
402
|
+
state,
|
|
403
|
+
contract: { policyId: contract.policyId, policyVersion: contract.policyVersion, capabilities: contract.capabilities },
|
|
404
|
+
theme,
|
|
405
|
+
adoption,
|
|
406
|
+
sessions,
|
|
407
|
+
commits,
|
|
408
|
+
ide,
|
|
409
|
+
sessionStart: sessionStart.toISOString(),
|
|
410
|
+
sessionId: sessionStart.getTime() > 0 ? sessionStart.getTime().toString(16).slice(-6).toUpperCase() : "ALL",
|
|
411
|
+
sessionDuration: fmtDuration(sessionStart.getTime() > 0 ? Date.now() - sessionStart.getTime() : -1),
|
|
412
|
+
generatedAt: new Date().toISOString(),
|
|
413
|
+
}, null, 2));
|
|
259
414
|
return;
|
|
260
415
|
}
|
|
261
416
|
|
|
@@ -274,23 +429,36 @@ export async function switchCommand(args) {
|
|
|
274
429
|
fs.appendFileSync(SESSIONS_FILE, JSON.stringify(entry) + "\n", "utf8");
|
|
275
430
|
}
|
|
276
431
|
|
|
277
|
-
//
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
const sessionEntries =
|
|
281
|
-
const state
|
|
282
|
-
const theme
|
|
283
|
-
const contract
|
|
284
|
-
|
|
285
|
-
|
|
432
|
+
// ── Rich summary printout ─────────────────────────────────────────────────
|
|
433
|
+
const allEntriesNow = getAllEntries();
|
|
434
|
+
const sessionStartNow = findSessionStart(allEntriesNow, sinceArg, allFlag);
|
|
435
|
+
const sessionEntries = allEntriesNow.filter(e => new Date(e.ts || 0) > sessionStartNow);
|
|
436
|
+
const state = readJSON(STATE_FILE) || {};
|
|
437
|
+
const theme = readJSON(THEME_FILE);
|
|
438
|
+
const contract = readJSON(CONTRACT_FILE) || {};
|
|
439
|
+
const commits = getGitCommits(sessionStartNow.getTime() > 0 ? sessionStartNow : null);
|
|
440
|
+
const ide = detectIde();
|
|
441
|
+
const pool = sessionEntries.length > 0 ? sessionEntries : allEntriesNow.slice(-5);
|
|
442
|
+
const openThreads = findOpenThreads(pool);
|
|
443
|
+
const sessionDuration = fmtDuration(sessionStartNow.getTime() > 0 ? Date.now() - sessionStartNow.getTime() : -1);
|
|
444
|
+
const sessionId = sessionStartNow.getTime() > 0
|
|
445
|
+
? sessionStartNow.getTime().toString(16).slice(-6).toUpperCase()
|
|
446
|
+
: "ALL";
|
|
447
|
+
|
|
448
|
+
// Print rich summary
|
|
449
|
+
console.log(" " + bold("Handoff ready"));
|
|
286
450
|
console.log(" " + "─".repeat(50));
|
|
451
|
+
console.log(" " + gray("Session #" + sessionId + " · " + sessionDuration));
|
|
287
452
|
if (state.working) console.log(" Working on " + cyan(state.working));
|
|
288
453
|
if (state.intent) console.log(" Intent " + cyan(state.intent));
|
|
289
|
-
console.log("
|
|
454
|
+
console.log(" Memory " + sessionEntries.length + " entries this session (total: " + allEntriesNow.length + ")");
|
|
455
|
+
if (openThreads.length) console.log(" Open threads " + yellow(openThreads.length + " unresolved"));
|
|
456
|
+
if (commits.length) console.log(" Git commits " + commits.length + " this session");
|
|
290
457
|
console.log(" Capabilities " + (contract.capabilities || []).length + " registered");
|
|
291
458
|
if (theme?.fonts?.primary) console.log(" Font " + theme.fonts.primary);
|
|
292
459
|
if (theme?.colors?.mode) console.log(" Color mode " + theme.colors.mode);
|
|
293
|
-
if (
|
|
460
|
+
if (ide) console.log(" IDE " + ide);
|
|
461
|
+
if (toAgent) console.log(" Handing off → " + cyan(toAgent));
|
|
294
462
|
console.log();
|
|
295
463
|
|
|
296
464
|
if (copyFlag) {
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* infernoflow telemetry
|
|
3
|
+
*
|
|
4
|
+
* Opt-in, fire-and-forget usage analytics.
|
|
5
|
+
*
|
|
6
|
+
* - Stored in ~/.infernoflow/telemetry.json
|
|
7
|
+
* - Never enabled without explicit consent
|
|
8
|
+
* - Never blocks the CLI — all sends are async / best-effort
|
|
9
|
+
* - Never sends code, file contents, capability names, or personal data
|
|
10
|
+
* Only sends: command name, infernoflow version, Node version, OS platform
|
|
11
|
+
*
|
|
12
|
+
* Consent is requested lazily on the first interactive infernoflow run
|
|
13
|
+
* after install (if no consent decision is stored).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import * as os from "node:os";
|
|
19
|
+
import * as https from "node:https";
|
|
20
|
+
|
|
21
|
+
const CONFIG_DIR = path.join(os.homedir(), ".infernoflow");
|
|
22
|
+
const TELEMETRY_FILE = path.join(CONFIG_DIR, "telemetry.json");
|
|
23
|
+
const EVENTS_FILE = path.join(CONFIG_DIR, "events.jsonl");
|
|
24
|
+
|
|
25
|
+
const ENDPOINT = "https://telemetry.infernoflow.dev/v1/event"; // placeholder
|
|
26
|
+
|
|
27
|
+
// ── Config helpers ────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
function readConfig() {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(TELEMETRY_FILE, "utf8"));
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function writeConfig(data) {
|
|
38
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
fs.writeFileSync(TELEMETRY_FILE, JSON.stringify(data, null, 2), "utf8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Returns true if the user has opted in */
|
|
43
|
+
export function isTelemetryEnabled() {
|
|
44
|
+
const cfg = readConfig();
|
|
45
|
+
return cfg?.enabled === true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Returns true if the user has made a consent decision (either way) */
|
|
49
|
+
export function hasConsentDecision() {
|
|
50
|
+
const cfg = readConfig();
|
|
51
|
+
return cfg !== null && typeof cfg.enabled === "boolean";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Consent prompt ────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Silently skip if consent already given, or if running non-interactively.
|
|
58
|
+
* Call this once at the start of each interactive CLI run.
|
|
59
|
+
*/
|
|
60
|
+
export async function ensureTelemetryConsent() {
|
|
61
|
+
if (hasConsentDecision()) return;
|
|
62
|
+
if (!process.stdin.isTTY) return;
|
|
63
|
+
|
|
64
|
+
// Only ask after 3+ runs (let the user experience it first)
|
|
65
|
+
const cfg = readConfig() || {};
|
|
66
|
+
const runs = (cfg.runs || 0) + 1;
|
|
67
|
+
writeConfig({ ...cfg, runs, enabled: false }); // default off until explicit consent
|
|
68
|
+
|
|
69
|
+
if (runs < 3) return;
|
|
70
|
+
|
|
71
|
+
// Show one-time prompt
|
|
72
|
+
const { createInterface } = await import("node:readline");
|
|
73
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
74
|
+
|
|
75
|
+
const answer = await new Promise(resolve => {
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
"\n 📡 Help improve infernoflow?\n" +
|
|
78
|
+
" Share anonymous usage data (command names only — no code, no content).\n" +
|
|
79
|
+
" Type 'y' to opt in, any other key to decline. You can change this later with: infernoflow telemetry on/off\n" +
|
|
80
|
+
" → "
|
|
81
|
+
);
|
|
82
|
+
rl.question("", resolve);
|
|
83
|
+
});
|
|
84
|
+
rl.close();
|
|
85
|
+
|
|
86
|
+
const enabled = answer.trim().toLowerCase() === "y";
|
|
87
|
+
writeConfig({ enabled, runs, decidedAt: new Date().toISOString() });
|
|
88
|
+
|
|
89
|
+
if (enabled) {
|
|
90
|
+
process.stdout.write(" ✔ Telemetry enabled — thank you! (infernoflow telemetry off to disable)\n\n");
|
|
91
|
+
} else {
|
|
92
|
+
process.stdout.write(" ✔ Got it — telemetry off. (infernoflow telemetry on to enable later)\n\n");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── Event tracking ────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/** Get current package version */
|
|
99
|
+
function getVersion() {
|
|
100
|
+
try {
|
|
101
|
+
const pkgPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../package.json");
|
|
102
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
|
|
103
|
+
} catch {
|
|
104
|
+
return "unknown";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Track a command invocation. Fire-and-forget — never throws, never blocks.
|
|
110
|
+
* @param {string} command The command name (e.g. "log", "switch", "recap")
|
|
111
|
+
*/
|
|
112
|
+
export function trackEvent(command) {
|
|
113
|
+
if (!isTelemetryEnabled()) return;
|
|
114
|
+
|
|
115
|
+
const event = {
|
|
116
|
+
ts: new Date().toISOString(),
|
|
117
|
+
command,
|
|
118
|
+
version: getVersion(),
|
|
119
|
+
node: process.version,
|
|
120
|
+
os: process.platform,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Always append to local event log first
|
|
124
|
+
try {
|
|
125
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
126
|
+
fs.appendFileSync(EVENTS_FILE, JSON.stringify(event) + "\n", "utf8");
|
|
127
|
+
} catch {}
|
|
128
|
+
|
|
129
|
+
// Fire-and-forget HTTP POST (best effort — no await)
|
|
130
|
+
try {
|
|
131
|
+
const body = JSON.stringify(event);
|
|
132
|
+
const url = new URL(ENDPOINT);
|
|
133
|
+
const req = https.request({
|
|
134
|
+
hostname: url.hostname,
|
|
135
|
+
path: url.pathname,
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) },
|
|
138
|
+
timeout: 3000,
|
|
139
|
+
});
|
|
140
|
+
req.on("error", () => {}); // silently ignore all errors
|
|
141
|
+
req.write(body);
|
|
142
|
+
req.end();
|
|
143
|
+
} catch {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── CLI subcommand ────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export async function telemetryCommand(args) {
|
|
149
|
+
const { bold, cyan, gray, green, yellow, red } = await import("./ui/output.mjs");
|
|
150
|
+
const sub = args[0];
|
|
151
|
+
|
|
152
|
+
if (sub === "on") {
|
|
153
|
+
const cfg = readConfig() || {};
|
|
154
|
+
writeConfig({ ...cfg, enabled: true, decidedAt: new Date().toISOString() });
|
|
155
|
+
console.log(green("\n ✔ Telemetry enabled — thank you for helping improve infernoflow!\n"));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (sub === "off") {
|
|
160
|
+
const cfg = readConfig() || {};
|
|
161
|
+
writeConfig({ ...cfg, enabled: false, decidedAt: new Date().toISOString() });
|
|
162
|
+
console.log(green("\n ✔ Telemetry disabled.\n"));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (sub === "status" || !sub) {
|
|
167
|
+
const cfg = readConfig();
|
|
168
|
+
const enabled = cfg?.enabled === true;
|
|
169
|
+
const decided = cfg?.decidedAt ? new Date(cfg.decidedAt).toLocaleDateString() : "never";
|
|
170
|
+
|
|
171
|
+
// Count local events
|
|
172
|
+
let eventCount = 0;
|
|
173
|
+
try {
|
|
174
|
+
const lines = fs.readFileSync(EVENTS_FILE, "utf8").split("\n").filter(Boolean);
|
|
175
|
+
eventCount = lines.length;
|
|
176
|
+
} catch {}
|
|
177
|
+
|
|
178
|
+
console.log("\n " + bold("🔥 infernoflow telemetry status") + "\n");
|
|
179
|
+
console.log(" Telemetry " + (enabled ? green("enabled") : yellow("disabled")));
|
|
180
|
+
console.log(" Decided " + gray(decided));
|
|
181
|
+
console.log(" Events logged " + gray(eventCount + " (local only until enabled)"));
|
|
182
|
+
console.log(" Data sent " + gray("command name, infernoflow version, Node version, OS platform"));
|
|
183
|
+
console.log(" Data never " + gray("code, file names, capability names, email, personal data"));
|
|
184
|
+
console.log();
|
|
185
|
+
console.log(gray(" infernoflow telemetry on — enable"));
|
|
186
|
+
console.log(gray(" infernoflow telemetry off — disable"));
|
|
187
|
+
console.log();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.error(red(`\n ✘ Unknown subcommand: ${sub}`));
|
|
192
|
+
console.log(gray(" Usage: infernoflow telemetry [on | off | status]\n"));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env sh
|
|
2
2
|
# infernoflow post-commit hook
|
|
3
3
|
# Silently keeps the capability contract in sync after every commit.
|
|
4
|
+
# Also auto-captures decisions, gotchas, and fixes from commit messages.
|
|
4
5
|
# This runs in the background — it never blocks your workflow.
|
|
5
6
|
#
|
|
6
7
|
# Managed by infernoflow. Re-run `infernoflow setup` to regenerate.
|
|
@@ -17,10 +18,29 @@ TOPLEVEL="$(git rev-parse --show-toplevel)"
|
|
|
17
18
|
(
|
|
18
19
|
cd "$TOPLEVEL" || exit 0
|
|
19
20
|
|
|
20
|
-
# 1. Auto-
|
|
21
|
+
# 1. Auto-capture session memory from commit message
|
|
22
|
+
# Gets the first line of the commit message (the subject)
|
|
23
|
+
COMMIT_MSG="$(git log -1 --pretty=%s 2>/dev/null)"
|
|
24
|
+
|
|
25
|
+
if [ -n "$COMMIT_MSG" ]; then
|
|
26
|
+
# Decision patterns: chose/switched/because/use X over Y
|
|
27
|
+
if echo "$COMMIT_MSG" | grep -qiE "(decided|chose|switched to|use .+ over|instead of|because|prefer .+)"; then
|
|
28
|
+
npx infernoflow log "$COMMIT_MSG" --type decision --auto --quiet --source git-hook >/dev/null 2>&1
|
|
29
|
+
|
|
30
|
+
# Gotcha / fix patterns: bugs, workarounds, reversals
|
|
31
|
+
elif echo "$COMMIT_MSG" | grep -qiE "^(fix|bug|revert|hotfix|workaround|hack|broke|issue|gotcha|caveat|warn)"; then
|
|
32
|
+
npx infernoflow log "$COMMIT_MSG" --type gotcha --auto --quiet --source git-hook >/dev/null 2>&1
|
|
33
|
+
|
|
34
|
+
# Attempt / WIP patterns
|
|
35
|
+
elif echo "$COMMIT_MSG" | grep -qiE "^(wip|draft|attempt|try|experiment|test|prototype|spike)"; then
|
|
36
|
+
npx infernoflow log "$COMMIT_MSG" --type attempt --auto --quiet --source git-hook >/dev/null 2>&1
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
# 2. Auto-update changelog with commit info
|
|
21
41
|
npx infernoflow changelog update --append >/dev/null 2>&1
|
|
22
42
|
|
|
23
|
-
#
|
|
43
|
+
# 3. Check contract health; log issues to inferno/HOOK.log so MCP can read it
|
|
24
44
|
RESULT=$(npx infernoflow check --json 2>/dev/null)
|
|
25
45
|
STATUS=$(echo "$RESULT" | node -e "try{const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));process.stdout.write(d.status||'ok')}catch{process.stdout.write('ok')}" 2>/dev/null)
|
|
26
46
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# infernoflow pre-stash hook
|
|
3
|
+
# Auto-logs a stash as an attempt entry in session memory.
|
|
4
|
+
# Runs silently — never blocks your workflow.
|
|
5
|
+
#
|
|
6
|
+
# Managed by infernoflow. Re-run `infernoflow setup` to regenerate.
|
|
7
|
+
# Note: pre-stash requires git 2.32+
|
|
8
|
+
|
|
9
|
+
# Bail silently if infernoflow isn't installed
|
|
10
|
+
command -v npx >/dev/null 2>&1 || exit 0
|
|
11
|
+
|
|
12
|
+
# Only run inside an inferno-enabled project
|
|
13
|
+
[ -d "$(git rev-parse --show-toplevel)/inferno" ] || exit 0
|
|
14
|
+
|
|
15
|
+
TOPLEVEL="$(git rev-parse --show-toplevel)"
|
|
16
|
+
STASH_MSG="${1:-stash}"
|
|
17
|
+
|
|
18
|
+
(
|
|
19
|
+
cd "$TOPLEVEL" || exit 0
|
|
20
|
+
npx infernoflow log "stashed: $STASH_MSG" --type attempt --auto --quiet --source git-hook >/dev/null 2>&1
|
|
21
|
+
) &
|
|
22
|
+
|
|
23
|
+
exit 0
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infernoflow",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.35.4",
|
|
4
|
+
"description": "Persistent memory for AI coding sessions — captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"infernoflow": "dist/bin/infernoflow.mjs"
|
|
@@ -25,14 +25,26 @@
|
|
|
25
25
|
"inferno:promote-draft": "node scripts/inferno-promote-draft.mjs"
|
|
26
26
|
},
|
|
27
27
|
"keywords": [
|
|
28
|
-
"cli",
|
|
29
|
-
"capabilities",
|
|
30
|
-
"contract",
|
|
31
|
-
"documentation",
|
|
32
28
|
"ai",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
29
|
+
"ai-memory",
|
|
30
|
+
"ai-context",
|
|
31
|
+
"ai-coding",
|
|
32
|
+
"session-memory",
|
|
33
|
+
"persistent-memory",
|
|
34
|
+
"agent-memory",
|
|
35
|
+
"agent-handoff",
|
|
36
|
+
"copilot",
|
|
37
|
+
"cursor",
|
|
38
|
+
"claude",
|
|
39
|
+
"windsurf",
|
|
40
|
+
"mcp",
|
|
41
|
+
"mcp-server",
|
|
42
|
+
"context-switching",
|
|
43
|
+
"developer-tools",
|
|
44
|
+
"cli",
|
|
45
|
+
"infernoflow",
|
|
46
|
+
"gotchas",
|
|
47
|
+
"coding-assistant"
|
|
36
48
|
],
|
|
37
49
|
"author": "infernoflow",
|
|
38
50
|
"license": "MIT",
|