infernoflow 0.35.5 → 0.35.7
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/lib/commands/feedback.mjs +53 -3
- package/dist/lib/telemetry.mjs +149 -74
- package/package.json +6 -2
|
@@ -3,19 +3,33 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Collects in-CLI feedback about infernoflow and optionally opens the web form.
|
|
5
5
|
*
|
|
6
|
+
* Responses are:
|
|
7
|
+
* 1. Saved locally in ~/.infernoflow/feedback.json
|
|
8
|
+
* 2. POSTed to Formspree (free, no backend needed — Ron gets email per submission)
|
|
9
|
+
*
|
|
10
|
+
* To activate Formspree:
|
|
11
|
+
* 1. Go to https://formspree.io → create free account → New Form
|
|
12
|
+
* 2. Replace FORMSPREE_ENDPOINT below with your form URL (e.g. https://formspree.io/f/xabc1234)
|
|
13
|
+
* 3. Publish the package — submissions arrive in your email immediately
|
|
14
|
+
*
|
|
6
15
|
* Usage:
|
|
7
16
|
* infernoflow feedback Interactive 5-question survey
|
|
8
17
|
* infernoflow feedback --form Open Google Form in browser
|
|
9
18
|
* infernoflow feedback --json Print last stored feedback as JSON
|
|
10
19
|
*/
|
|
11
20
|
|
|
12
|
-
import * as fs
|
|
13
|
-
import * as path
|
|
14
|
-
import * as os
|
|
21
|
+
import * as fs from "node:fs";
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import * as os from "node:os";
|
|
24
|
+
import * as https from "node:https";
|
|
15
25
|
import * as readline from "node:readline";
|
|
16
26
|
import { execSync } from "node:child_process";
|
|
17
27
|
import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
|
|
18
28
|
|
|
29
|
+
// ── Replace this URL with your Formspree form endpoint ───────────────────────
|
|
30
|
+
// Get one free at https://formspree.io (50 submissions/mo free, emails you each response)
|
|
31
|
+
const FORMSPREE_ENDPOINT = "https://formspree.io/f/infernoflow"; // placeholder
|
|
32
|
+
|
|
19
33
|
const FEEDBACK_FORM_URL = "https://forms.gle/infernoflow-feedback"; // placeholder — replace with real form
|
|
20
34
|
const FEEDBACK_FILE = path.join(os.homedir(), ".infernoflow", "feedback.json");
|
|
21
35
|
|
|
@@ -48,6 +62,39 @@ const QUESTIONS = [
|
|
|
48
62
|
},
|
|
49
63
|
];
|
|
50
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Fire-and-forget POST to Formspree.
|
|
67
|
+
* Formspree emails the form owner on each submission — zero backend needed.
|
|
68
|
+
*/
|
|
69
|
+
function sendToFormspree(record) {
|
|
70
|
+
try {
|
|
71
|
+
if (!FORMSPREE_ENDPOINT || FORMSPREE_ENDPOINT.includes("placeholder")) return;
|
|
72
|
+
|
|
73
|
+
const url = new URL(FORMSPREE_ENDPOINT);
|
|
74
|
+
const body = JSON.stringify({
|
|
75
|
+
...record.responses,
|
|
76
|
+
_subject: `infernoflow feedback v${record.version}`,
|
|
77
|
+
_version: record.version,
|
|
78
|
+
_ts: record.ts,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const req = https.request({
|
|
82
|
+
hostname: url.hostname,
|
|
83
|
+
path: url.pathname,
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type": "application/json",
|
|
87
|
+
"Accept": "application/json",
|
|
88
|
+
"Content-Length": Buffer.byteLength(body),
|
|
89
|
+
},
|
|
90
|
+
timeout: 5000,
|
|
91
|
+
});
|
|
92
|
+
req.on("error", () => {}); // never surface errors to the user
|
|
93
|
+
req.write(body);
|
|
94
|
+
req.end();
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
51
98
|
function saveFeedback(responses) {
|
|
52
99
|
const dir = path.dirname(FEEDBACK_FILE);
|
|
53
100
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
@@ -120,6 +167,9 @@ async function runSurvey() {
|
|
|
120
167
|
|
|
121
168
|
const record = saveFeedback(responses);
|
|
122
169
|
|
|
170
|
+
// Fire-and-forget cloud send (Formspree — Ron gets an email)
|
|
171
|
+
sendToFormspree(record);
|
|
172
|
+
|
|
123
173
|
console.log(green(" ✔ Feedback saved — thank you!\n"));
|
|
124
174
|
console.log(gray(" Stored in: ~/.infernoflow/feedback.json"));
|
|
125
175
|
console.log(gray(` Version: ${record.version}`));
|
package/dist/lib/telemetry.mjs
CHANGED
|
@@ -3,35 +3,41 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Opt-in, fire-and-forget usage analytics.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* What we collect (nothing else):
|
|
7
|
+
* ✅ command name (e.g. "log", "switch")
|
|
8
|
+
* ✅ anonymous install UUID (random, never linked to identity)
|
|
9
|
+
* ✅ infernoflow version
|
|
10
|
+
* ✅ Node version + OS platform
|
|
11
|
+
* ✅ timezone (geography estimation only)
|
|
12
|
+
* ✅ project type (frontend/backend/unknown — inferred from package.json)
|
|
13
|
+
* ✅ IDE (from env variables)
|
|
14
|
+
* ❌ no code, no file names, no personal data, no IP stored
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
16
|
+
* Backend: PostHog free tier (50K events/mo free, EU-hosted available)
|
|
17
|
+
* Config: ~/.infernoflow/telemetry.json
|
|
18
|
+
* Events: ~/.infernoflow/events.jsonl (local mirror)
|
|
19
|
+
*
|
|
20
|
+
* Consent is requested lazily after 3 interactive runs.
|
|
14
21
|
*/
|
|
15
22
|
|
|
16
|
-
import * as fs
|
|
17
|
-
import * as path
|
|
18
|
-
import * as os
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
25
|
+
import * as os from "node:os";
|
|
19
26
|
import * as https from "node:https";
|
|
27
|
+
import * as crypto from "node:crypto";
|
|
20
28
|
|
|
21
|
-
const CONFIG_DIR
|
|
29
|
+
const CONFIG_DIR = path.join(os.homedir(), ".infernoflow");
|
|
22
30
|
const TELEMETRY_FILE = path.join(CONFIG_DIR, "telemetry.json");
|
|
23
31
|
const EVENTS_FILE = path.join(CONFIG_DIR, "events.jsonl");
|
|
24
32
|
|
|
25
|
-
const
|
|
33
|
+
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
34
|
+
const POSTHOG_KEY = "phc_z6YX7x4zjkuFZigdXTBoFcPTWeGLFAN9NNKVZ5WHQrqk";
|
|
26
35
|
|
|
27
36
|
// ── Config helpers ────────────────────────────────────────────────────────────
|
|
28
37
|
|
|
29
38
|
function readConfig() {
|
|
30
|
-
try {
|
|
31
|
-
|
|
32
|
-
} catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
39
|
+
try { return JSON.parse(fs.readFileSync(TELEMETRY_FILE, "utf8")); }
|
|
40
|
+
catch { return null; }
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
function writeConfig(data) {
|
|
@@ -39,105 +45,171 @@ function writeConfig(data) {
|
|
|
39
45
|
fs.writeFileSync(TELEMETRY_FILE, JSON.stringify(data, null, 2), "utf8");
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
/**
|
|
48
|
+
/** Generate a random anonymous install UUID — stored once, never changes */
|
|
49
|
+
function generateInstallId() {
|
|
50
|
+
// Use crypto.randomUUID if available (Node 15.6+), else fallback
|
|
51
|
+
try { return crypto.randomUUID(); }
|
|
52
|
+
catch { return "ifl_" + crypto.randomBytes(16).toString("hex"); }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Get or create the persistent anonymous install ID */
|
|
56
|
+
function getOrCreateInstallId() {
|
|
57
|
+
const cfg = readConfig() || {};
|
|
58
|
+
if (cfg.installId) return cfg.installId;
|
|
59
|
+
const installId = generateInstallId();
|
|
60
|
+
writeConfig({ ...cfg, installId });
|
|
61
|
+
return installId;
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
export function isTelemetryEnabled() {
|
|
44
|
-
|
|
45
|
-
return cfg?.enabled === true;
|
|
65
|
+
return readConfig()?.enabled === true;
|
|
46
66
|
}
|
|
47
67
|
|
|
48
|
-
/** Returns true if the user has made a consent decision (either way) */
|
|
49
68
|
export function hasConsentDecision() {
|
|
50
69
|
const cfg = readConfig();
|
|
51
70
|
return cfg !== null && typeof cfg.enabled === "boolean";
|
|
52
71
|
}
|
|
53
72
|
|
|
73
|
+
// ── Context detection ─────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function detectIde() {
|
|
76
|
+
if (process.env.CURSOR_SESSION) return "cursor";
|
|
77
|
+
if (process.env.COPILOT_SESSION) return "copilot";
|
|
78
|
+
if (process.env.CLAUDE_CODE_SESSION) return "claude-code";
|
|
79
|
+
if (process.env.WINDSURF_SESSION) return "windsurf";
|
|
80
|
+
if (process.env.TERM_PROGRAM === "vscode") return "vscode";
|
|
81
|
+
return "unknown";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function detectProjectType() {
|
|
85
|
+
try {
|
|
86
|
+
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
|
87
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
88
|
+
if (deps["next"] || deps["nuxt"] || deps["remix"]) return "fullstack";
|
|
89
|
+
if (deps["react"] || deps["vue"] || deps["svelte"]) return "frontend";
|
|
90
|
+
if (deps["express"] || deps["fastify"] || deps["koa"]) return "backend";
|
|
91
|
+
if (deps["@angular/core"]) return "frontend";
|
|
92
|
+
return "js";
|
|
93
|
+
} catch {
|
|
94
|
+
return "unknown";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getTimezone() {
|
|
99
|
+
try { return Intl.DateTimeFormat().resolvedOptions().timeZone; }
|
|
100
|
+
catch { return "unknown"; }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getVersion() {
|
|
104
|
+
try {
|
|
105
|
+
const pkgPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../package.json");
|
|
106
|
+
return JSON.parse(fs.readFileSync(pkgPath, "utf8")).version;
|
|
107
|
+
} catch { return "unknown"; }
|
|
108
|
+
}
|
|
109
|
+
|
|
54
110
|
// ── Consent prompt ────────────────────────────────────────────────────────────
|
|
55
111
|
|
|
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
112
|
export async function ensureTelemetryConsent() {
|
|
61
113
|
if (hasConsentDecision()) return;
|
|
62
114
|
if (!process.stdin.isTTY) return;
|
|
63
115
|
|
|
64
|
-
// Only ask after 3+ runs
|
|
65
|
-
const cfg
|
|
116
|
+
// Only ask after 3+ interactive runs — let the user experience it first
|
|
117
|
+
const cfg = readConfig() || {};
|
|
66
118
|
const runs = (cfg.runs || 0) + 1;
|
|
67
|
-
writeConfig({ ...cfg, runs, enabled: false });
|
|
119
|
+
writeConfig({ ...cfg, runs, enabled: false });
|
|
68
120
|
|
|
69
121
|
if (runs < 3) return;
|
|
70
122
|
|
|
71
|
-
// Show one-time prompt
|
|
72
123
|
const { createInterface } = await import("node:readline");
|
|
73
124
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
74
125
|
|
|
75
126
|
const answer = await new Promise(resolve => {
|
|
76
127
|
process.stdout.write(
|
|
77
128
|
"\n 📡 Help improve infernoflow?\n" +
|
|
78
|
-
" Share anonymous usage data
|
|
79
|
-
" Type 'y' to opt in,
|
|
129
|
+
" Share anonymous usage data — command names, OS, timezone. No code. No personal data.\n" +
|
|
130
|
+
" Type 'y' to opt in, anything else to decline. (infernoflow telemetry off to change later)\n" +
|
|
80
131
|
" → "
|
|
81
132
|
);
|
|
82
133
|
rl.question("", resolve);
|
|
83
134
|
});
|
|
84
135
|
rl.close();
|
|
85
136
|
|
|
86
|
-
const enabled
|
|
87
|
-
|
|
137
|
+
const enabled = answer.trim().toLowerCase() === "y";
|
|
138
|
+
const installId = enabled ? generateInstallId() : null;
|
|
139
|
+
writeConfig({ enabled, installId, runs, decidedAt: new Date().toISOString() });
|
|
88
140
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
141
|
+
process.stdout.write(
|
|
142
|
+
enabled
|
|
143
|
+
? " ✔ Telemetry enabled — thank you! (infernoflow telemetry off to disable)\n\n"
|
|
144
|
+
: " ✔ No problem — telemetry off. (infernoflow telemetry on to enable later)\n\n"
|
|
145
|
+
);
|
|
94
146
|
}
|
|
95
147
|
|
|
96
148
|
// ── Event tracking ────────────────────────────────────────────────────────────
|
|
97
149
|
|
|
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
150
|
/**
|
|
109
151
|
* Track a command invocation. Fire-and-forget — never throws, never blocks.
|
|
110
|
-
* @param {string} command
|
|
152
|
+
* @param {string} command e.g. "log", "switch", "recap"
|
|
111
153
|
*/
|
|
112
154
|
export function trackEvent(command) {
|
|
113
155
|
if (!isTelemetryEnabled()) return;
|
|
114
156
|
|
|
157
|
+
const installId = getOrCreateInstallId();
|
|
158
|
+
|
|
115
159
|
const event = {
|
|
116
|
-
ts:
|
|
160
|
+
ts: new Date().toISOString(),
|
|
117
161
|
command,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
162
|
+
installId, // anonymous UUID — links events from same install
|
|
163
|
+
version: getVersion(),
|
|
164
|
+
node: process.version,
|
|
165
|
+
os: process.platform,
|
|
166
|
+
timezone: getTimezone(), // geography (continent/country) estimation
|
|
167
|
+
ide: detectIde(),
|
|
168
|
+
projectType: detectProjectType(),
|
|
121
169
|
};
|
|
122
170
|
|
|
123
|
-
//
|
|
171
|
+
// 1. Mirror to local event log
|
|
124
172
|
try {
|
|
125
173
|
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
126
174
|
fs.appendFileSync(EVENTS_FILE, JSON.stringify(event) + "\n", "utf8");
|
|
127
175
|
} catch {}
|
|
128
176
|
|
|
129
|
-
// Fire-and-forget
|
|
177
|
+
// 2. Fire-and-forget to PostHog
|
|
178
|
+
// PostHog expects: { api_key, event, distinct_id, properties, timestamp }
|
|
179
|
+
_postHog(installId, command, event);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function _postHog(distinctId, eventName, props) {
|
|
130
183
|
try {
|
|
131
|
-
const body = JSON.stringify(
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
const body = JSON.stringify({
|
|
185
|
+
api_key: POSTHOG_KEY,
|
|
186
|
+
event: eventName,
|
|
187
|
+
distinct_id: distinctId,
|
|
188
|
+
properties: {
|
|
189
|
+
command: props.command,
|
|
190
|
+
version: props.version,
|
|
191
|
+
node: props.node,
|
|
192
|
+
os: props.os,
|
|
193
|
+
timezone: props.timezone,
|
|
194
|
+
ide: props.ide,
|
|
195
|
+
projectType: props.projectType,
|
|
196
|
+
$lib: "infernoflow-cli",
|
|
197
|
+
},
|
|
198
|
+
timestamp: props.ts,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const url = new URL(POSTHOG_HOST + "/capture/");
|
|
202
|
+
const req = https.request({
|
|
134
203
|
hostname: url.hostname,
|
|
135
204
|
path: url.pathname,
|
|
136
205
|
method: "POST",
|
|
137
|
-
headers: {
|
|
138
|
-
|
|
206
|
+
headers: {
|
|
207
|
+
"Content-Type": "application/json",
|
|
208
|
+
"Content-Length": Buffer.byteLength(body),
|
|
209
|
+
},
|
|
210
|
+
timeout: 3000,
|
|
139
211
|
});
|
|
140
|
-
req.on("error", () => {});
|
|
212
|
+
req.on("error", () => {}); // never surface telemetry errors
|
|
141
213
|
req.write(body);
|
|
142
214
|
req.end();
|
|
143
215
|
} catch {}
|
|
@@ -150,8 +222,9 @@ export async function telemetryCommand(args) {
|
|
|
150
222
|
const sub = args[0];
|
|
151
223
|
|
|
152
224
|
if (sub === "on") {
|
|
153
|
-
const cfg
|
|
154
|
-
|
|
225
|
+
const cfg = readConfig() || {};
|
|
226
|
+
const installId = cfg.installId || generateInstallId();
|
|
227
|
+
writeConfig({ ...cfg, enabled: true, installId, decidedAt: new Date().toISOString() });
|
|
155
228
|
console.log(green("\n ✔ Telemetry enabled — thank you for helping improve infernoflow!\n"));
|
|
156
229
|
return;
|
|
157
230
|
}
|
|
@@ -159,28 +232,30 @@ export async function telemetryCommand(args) {
|
|
|
159
232
|
if (sub === "off") {
|
|
160
233
|
const cfg = readConfig() || {};
|
|
161
234
|
writeConfig({ ...cfg, enabled: false, decidedAt: new Date().toISOString() });
|
|
162
|
-
console.log(green("\n ✔ Telemetry disabled.\n"));
|
|
235
|
+
console.log(green("\n ✔ Telemetry disabled. No data will be sent.\n"));
|
|
163
236
|
return;
|
|
164
237
|
}
|
|
165
238
|
|
|
166
239
|
if (sub === "status" || !sub) {
|
|
167
|
-
const cfg
|
|
168
|
-
const enabled
|
|
169
|
-
const decided
|
|
240
|
+
const cfg = readConfig();
|
|
241
|
+
const enabled = cfg?.enabled === true;
|
|
242
|
+
const decided = cfg?.decidedAt ? new Date(cfg.decidedAt).toLocaleDateString() : "never";
|
|
243
|
+
const installId = cfg?.installId ? cfg.installId.slice(0, 12) + "…" : "none yet";
|
|
170
244
|
|
|
171
|
-
// Count local events
|
|
172
245
|
let eventCount = 0;
|
|
173
246
|
try {
|
|
174
|
-
|
|
175
|
-
eventCount = lines.length;
|
|
247
|
+
eventCount = fs.readFileSync(EVENTS_FILE, "utf8").split("\n").filter(Boolean).length;
|
|
176
248
|
} catch {}
|
|
177
249
|
|
|
178
250
|
console.log("\n " + bold("🔥 infernoflow telemetry status") + "\n");
|
|
179
|
-
console.log("
|
|
251
|
+
console.log(" Status " + (enabled ? green("enabled") : yellow("disabled")));
|
|
252
|
+
console.log(" Install ID " + gray(installId + " (anonymous, never linked to identity)"));
|
|
180
253
|
console.log(" Decided " + gray(decided));
|
|
181
|
-
console.log(" Events
|
|
182
|
-
console.log("
|
|
183
|
-
console.log(
|
|
254
|
+
console.log(" Events stored " + gray(eventCount + " locally → " + (enabled ? "also sent to PostHog" : "not sent (disabled)")));
|
|
255
|
+
console.log(" Backend " + gray("PostHog (EU-hosted, no IP stored)"));
|
|
256
|
+
console.log();
|
|
257
|
+
console.log(" " + bold("What we collect:") + " " + gray("command, version, Node, OS, timezone, IDE, project type"));
|
|
258
|
+
console.log(" " + bold("What we never collect:") + " " + gray("code, file names, capability names, email, personal data"));
|
|
184
259
|
console.log();
|
|
185
260
|
console.log(gray(" infernoflow telemetry on — enable"));
|
|
186
261
|
console.log(gray(" infernoflow telemetry off — disable"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infernoflow",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.7",
|
|
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": {
|
|
@@ -22,7 +22,11 @@
|
|
|
22
22
|
"test:help": "node bin/infernoflow.mjs --help",
|
|
23
23
|
"build": "node build.mjs",
|
|
24
24
|
"prepublishOnly": "node build.mjs",
|
|
25
|
-
"inferno:promote-draft": "node scripts/inferno-promote-draft.mjs"
|
|
25
|
+
"inferno:promote-draft": "node scripts/inferno-promote-draft.mjs",
|
|
26
|
+
"postinstall": "node -e \"try{require('@scarf/scarf')}catch(e){}\" 2>/dev/null; exit 0"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@scarf/scarf": "^1.3.0"
|
|
26
30
|
},
|
|
27
31
|
"keywords": [
|
|
28
32
|
"ai",
|