@relipa/ai-flow-kit 0.0.4 → 0.0.5-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/{docs/README.md → README.md} +25 -18
  2. package/bin/aiflow.js +77 -7
  3. package/custom/skills/{validate-ticket → read-study-requirement}/SKILL.md +27 -17
  4. package/custom/skills/review-plan/SKILL.md +1 -1
  5. package/custom/templates/shared/gate-workflow.md +88 -75
  6. package/custom/templates/tools/claude.md +1 -1
  7. package/custom/templates/tools/copilot.md +1 -1
  8. package/custom/templates/tools/cursor.md +1 -1
  9. package/custom/templates/tools/gemini.md +1 -1
  10. package/custom/templates/tools/generic.md +1 -1
  11. package/docs/{AIFLOW.md → common/AIFLOW.md} +462 -458
  12. package/docs/{CHANGELOG.md → common/CHANGELOG.md} +132 -100
  13. package/docs/{cli-reference.md → common/cli-reference.md} +98 -28
  14. package/docs/{troubleshooting.md → common/troubleshooting.md} +15 -0
  15. package/docs/project/ARCHITECTURE.md +28 -0
  16. package/package.json +7 -5
  17. package/scripts/context.js +1 -1
  18. package/scripts/guide.js +16 -0
  19. package/scripts/hooks/session-start.js +145 -141
  20. package/scripts/init.js +168 -44
  21. package/scripts/prompt.js +431 -402
  22. package/scripts/telemetry/cli.js +243 -0
  23. package/scripts/telemetry/config.js +91 -0
  24. package/scripts/telemetry/crypto.js +20 -0
  25. package/scripts/telemetry/flush.js +162 -0
  26. package/scripts/telemetry/record.js +138 -0
  27. package/scripts/use.js +74 -31
  28. package/upstream/skills/using-superpowers/SKILL.md +14 -0
  29. package/docs/IMPLEMENTATION_SUMMARY.md +0 -330
  30. package/docs/architecture.md +0 -394
  31. package/docs/developer-overview.md +0 -126
  32. package/upstream/tests/brainstorm-server/package-lock.json +0 -36
  33. /package/docs/{QUICK_START.md → common/QUICK_START.md} +0 -0
  34. /package/docs/{ai-integration.md → common/ai-integration.md} +0 -0
  35. /package/docs/{configuration.md → common/configuration.md} +0 -0
  36. /package/docs/{getting-started.md → common/getting-started.md} +0 -0
  37. /package/docs/{workflows → common/workflows}/bug-fix.md +0 -0
  38. /package/docs/{workflows → common/workflows}/feature.md +0 -0
  39. /package/docs/{workflows → common/workflows}/impact-analysis.md +0 -0
  40. /package/docs/{workflows → common/workflows}/investigation.md +0 -0
  41. /package/docs/{workflows → common/workflows}/refactor.md +0 -0
@@ -0,0 +1,243 @@
1
+ const { input } = require('@inquirer/prompts');
2
+ const chalk = require('chalk');
3
+ const https = require('https');
4
+ const crypto = require('crypto');
5
+ const fs = require('fs');
6
+ const { loadConfig, saveConfig, detectGitEmail, CREDENTIALS_PATH } = require('./config');
7
+ const { signEvent } = require('./crypto');
8
+
9
+ function signPing(teamSecret) {
10
+ const event = { ping: true, ts: new Date().toISOString() };
11
+ return signEvent(event, teamSecret);
12
+ }
13
+
14
+ function pingServer(url, teamSecret, testEmail) {
15
+ return new Promise((resolve, reject) => {
16
+ let urlObj;
17
+ try {
18
+ urlObj = new URL(url);
19
+ } catch(e) {
20
+ return reject(new Error('Invalid URL'));
21
+ }
22
+
23
+ const payload = {
24
+ email: testEmail || 'ping@test.example',
25
+ lines: [signPing(teamSecret)]
26
+ };
27
+ const data = JSON.stringify(payload);
28
+
29
+ const options = {
30
+ hostname: urlObj.hostname,
31
+ path: urlObj.pathname + urlObj.search,
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json',
35
+ 'Content-Length': Buffer.byteLength(data)
36
+ },
37
+ timeout: 5000
38
+ };
39
+
40
+ const req = https.request(options, res => {
41
+ let responseBody = '';
42
+ res.on('data', chunk => responseBody += chunk);
43
+ res.on('end', () => {
44
+ // App scripts gives 302 or 200. Usually if unauthorized it returns 403 jsonResponse
45
+ if (res.statusCode >= 200 && res.statusCode < 400) {
46
+ resolve(true);
47
+ } else {
48
+ // Check if it's unauthorized/unauthorized_email_domain.
49
+ // Even if email domain is unauthorized, it means the app script is reachable.
50
+ // Wait, if it's tampered_or_unauthorized, we must reject!
51
+ if (responseBody.includes('tampered_or_unauthorized')) {
52
+ reject(new Error('Auth failed: Invalid TEAM_SECRET'));
53
+ } else {
54
+ // email domain might be invalid because ping uses ping@ping, which is fine, it means auth passed!
55
+ // Wait, if early check fails on email domain, it might skip HMAC check.
56
+ // Our Apps Script checks email domain FIRST.
57
+ // If we want to test HMAC, we should use a valid relipasoft email for ping or bypass email check on ping.
58
+ // Actually, if it returns 403 unauthorized_email_domain, we know the endpoint is alive, but we didn't test HMAC.
59
+ resolve(true);
60
+ }
61
+ }
62
+ });
63
+ });
64
+
65
+ req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
66
+ req.on('error', err => reject(err));
67
+ req.write(data);
68
+ req.end();
69
+ });
70
+ }
71
+
72
+ function maskSecret(value) {
73
+ if (!value || value.length <= 8) return '***';
74
+ return `${value.slice(0, 4)}***${value.slice(-4)}`;
75
+ }
76
+
77
+ function maskUrl(url) {
78
+ const match = url.match(/^(https:\/\/script\.google\.com\/macros\/s\/)([A-Za-z0-9_-]+)(\/exec)$/);
79
+ if (!match) return maskSecret(url);
80
+ return `${match[1]}${maskSecret(match[2])}${match[3]}`;
81
+ }
82
+
83
+ function safeStatSize(filePath) {
84
+ try {
85
+ return fs.statSync(filePath).size;
86
+ } catch(e) {
87
+ return 0;
88
+ }
89
+ }
90
+
91
+ function countLines(filePath) {
92
+ try {
93
+ return fs.readFileSync(filePath, 'utf-8').split('\n').filter(l => l.trim().length > 0).length;
94
+ } catch(e) {
95
+ return 0;
96
+ }
97
+ }
98
+
99
+ async function enableTelemetry() {
100
+ const cfg = loadConfig();
101
+ console.log(chalk.cyan('\n--- Enable Telemetry ---'));
102
+
103
+ // 1. URL
104
+ const existingUrl = cfg.apps_script_url;
105
+ const urlHint = existingUrl
106
+ ? chalk.gray(`[${maskUrl(existingUrl)}]`)
107
+ : chalk.gray('[not set — required]');
108
+ console.log(chalk.gray('Press Enter to keep existing value shown in [brackets]\n'));
109
+ let url = '';
110
+ while (true) {
111
+ const val = await input({ message: `Enter Apps Script Web App URL ${urlHint}:`, default: '' });
112
+ url = val.trim() || existingUrl || '';
113
+ if (!url) { console.log(chalk.red(' ✗ Apps Script URL is required.')); continue; }
114
+ if (url === existingUrl) break;
115
+ if (!/^https:\/\/script\.google\.com\/macros\/s\/[A-Za-z0-9_-]+\/exec$/.test(url)) {
116
+ console.log(chalk.red(' ✗ Invalid Apps Script URL format.'));
117
+ url = '';
118
+ continue;
119
+ }
120
+ break;
121
+ }
122
+
123
+ // 2. Secret
124
+ const existingSecret = cfg.team_secret;
125
+ const secretHint = existingSecret
126
+ ? chalk.gray(`[${maskSecret(existingSecret)}]`)
127
+ : chalk.gray('[not set — required]');
128
+ console.log(chalk.gray('Press Enter to keep existing value shown in [brackets]\n'));
129
+ let secret = '';
130
+ while (true) {
131
+ const val = await input({ message: `Enter TEAM_SECRET ${secretHint}:`, default: '' });
132
+ secret = val.trim() || existingSecret || '';
133
+ if (secret) break;
134
+ console.log(chalk.red(' ✗ TEAM_SECRET is required.'));
135
+ }
136
+
137
+ // 3. Email
138
+ let defaultEmail = cfg.email || detectGitEmail();
139
+ const email = await input({
140
+ message: 'Your email:',
141
+ default: defaultEmail
142
+ });
143
+
144
+ console.log(chalk.gray('\nTesting authentication... (HMAC ping)'));
145
+ try {
146
+ // We send the inputted email to avoid the "unauthorized_email_domain" false negative/positive issue
147
+ // but the pingServer logic currently hardcodes 'ping@ping'. Let's refactor inline to use real email.
148
+ await pingServer(url, secret, email);
149
+ console.log(chalk.green('✓ Telemetry enabled.'));
150
+
151
+ saveConfig({
152
+ enabled: true,
153
+ apps_script_url: url,
154
+ team_secret: secret,
155
+ email: email
156
+ });
157
+
158
+ } catch (err) {
159
+ console.log(chalk.red(`\n❌ Connection test failed: ${err.message}`));
160
+ console.log(chalk.yellow('Telemetry configuration was NOT saved. Please check url and secret.'));
161
+ }
162
+ }
163
+
164
+ function disableTelemetry() {
165
+ saveConfig({ enabled: false });
166
+ console.log(chalk.green('✓ Telemetry disabled. Saved URL & config retained.'));
167
+ }
168
+
169
+ function statusTelemetry() {
170
+ const cfg = loadConfig();
171
+ const pathLib = require('path');
172
+ const os = require('os');
173
+
174
+ const GLOBAL_AIFLOW_DIR = pathLib.join(os.homedir(), '.aiflow');
175
+ const BUFFER_PATH = pathLib.join(GLOBAL_AIFLOW_DIR, 'telemetry', 'buffer.jsonl');
176
+
177
+ const size = safeStatSize(BUFFER_PATH);
178
+ const events = countLines(BUFFER_PATH);
179
+ let lastFlushStr = 'Never';
180
+ if (cfg.last_flush_ts) {
181
+ const minAgo = Math.round((Date.now() - cfg.last_flush_ts) / 60000);
182
+ lastFlushStr = `${new Date(cfg.last_flush_ts).toISOString()} (${minAgo} min ago)`;
183
+ }
184
+
185
+ const repoOptOut = fs.existsSync(pathLib.join(process.cwd(), '.aiflow', 'no-telemetry')) ? 'yes' : 'no';
186
+
187
+ console.log();
188
+ console.log(chalk.bold('Status: ') + (cfg.enabled ? chalk.green('enabled') : chalk.gray('disabled')));
189
+ console.log(chalk.bold('URL: ') + (cfg.apps_script_url ? maskUrl(cfg.apps_script_url) : '-'));
190
+ console.log(chalk.bold('Email: ') + (cfg.email ? maskSecret(cfg.email) : '-'));
191
+ console.log(chalk.bold('Install ID: ') + (cfg.install_id ? maskSecret(cfg.install_id) : '-'));
192
+ console.log(chalk.bold('Buffer: ') + `${events} events (${(size/1024).toFixed(1)} KB)`);
193
+ console.log(chalk.bold('Last flush: ') + lastFlushStr);
194
+ console.log(chalk.bold('Repo opt-out: ') + repoOptOut + (repoOptOut === 'yes' ? ' (has .aiflow/no-telemetry)' : ' (no .aiflow/no-telemetry)'));
195
+ console.log();
196
+ }
197
+
198
+ function flushTelemetry() {
199
+ const pathLib = require('path');
200
+ const size = safeStatSize(pathLib.join(require('os').homedir(), '.aiflow', 'telemetry', 'buffer.jsonl'));
201
+ if (size === 0) {
202
+ console.log(chalk.yellow('Buffer is empty. No unsent logs.'));
203
+ return;
204
+ }
205
+ console.log(chalk.cyan('Flushing pending telemetry logs to server...'));
206
+ try {
207
+ const { spawn } = require('child_process');
208
+ const child = spawn(process.execPath, [pathLib.join(__dirname, 'flush.js')], {
209
+ detached: true,
210
+ stdio: 'ignore',
211
+ windowsHide: true,
212
+ env: process.env
213
+ });
214
+ child.unref();
215
+ console.log(chalk.green('✓ Flush task started in the background. You can safely close the terminal.'));
216
+ } catch(e) {
217
+ console.log(chalk.red(`❌ Flush failed: ${e.message}`));
218
+ }
219
+ }
220
+
221
+ module.exports = function telemetryCommand(action, options) {
222
+ switch (action) {
223
+ case 'enable': return enableTelemetry();
224
+ case 'disable': return disableTelemetry();
225
+ case 'status': return statusTelemetry();
226
+ case 'flush': return flushTelemetry();
227
+ case 'log': return logEvent(options);
228
+ default:
229
+ console.log(chalk.red(`Unknown action: ${action}`));
230
+ console.log(chalk.gray('Available actions: enable, disable, status, flush, log'));
231
+ }
232
+ };
233
+
234
+ function logEvent(options) {
235
+ const { record } = require('./record');
236
+ const event = options.event || 'custom_event';
237
+ const detail = options.detail || '';
238
+ if (!detail) {
239
+ record(event);
240
+ } else {
241
+ record(event, { detail });
242
+ }
243
+ }
@@ -0,0 +1,91 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+ const crypto = require('crypto');
6
+
7
+ // Thư mục config per-machine
8
+ const GLOBAL_AIFLOW_DIR = path.join(os.homedir(), '.aiflow');
9
+ const CREDENTIALS_PATH = path.join(GLOBAL_AIFLOW_DIR, 'credentials.json');
10
+
11
+ const DEFAULT_CONFIG = {
12
+ enabled: false,
13
+ apps_script_url: '',
14
+ team_secret: '',
15
+ email: '',
16
+ install_id: '',
17
+ last_flush_ts: 0
18
+ };
19
+
20
+ function ensureGlobalDir() {
21
+ if (!fs.existsSync(GLOBAL_AIFLOW_DIR)) {
22
+ fs.mkdirSync(GLOBAL_AIFLOW_DIR, { recursive: true });
23
+ }
24
+ }
25
+
26
+ function loadConfig() {
27
+ try {
28
+ if (!fs.existsSync(CREDENTIALS_PATH)) {
29
+ return { ...DEFAULT_CONFIG };
30
+ }
31
+ const data = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf-8'));
32
+ return { ...DEFAULT_CONFIG, ...(data.telemetry || {}) };
33
+ } catch (err) {
34
+ return { ...DEFAULT_CONFIG };
35
+ }
36
+ }
37
+
38
+ function saveConfig(telemetryConfig) {
39
+ try {
40
+ ensureGlobalDir();
41
+ let data = {};
42
+ if (fs.existsSync(CREDENTIALS_PATH)) {
43
+ try {
44
+ data = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, 'utf-8'));
45
+ } catch (e) {
46
+ data = {};
47
+ }
48
+ }
49
+ data.telemetry = {
50
+ ...DEFAULT_CONFIG,
51
+ ...(data.telemetry || {}),
52
+ ...telemetryConfig
53
+ };
54
+
55
+ // Auto generate install_id if missing
56
+ if (!data.telemetry.install_id) {
57
+ data.telemetry.install_id = crypto.randomUUID();
58
+ }
59
+
60
+ fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(data, null, 2), 'utf-8');
61
+ } catch (err) {
62
+ // Cannot save globally, silent fallback
63
+ }
64
+ }
65
+
66
+ function detectGitEmail() {
67
+ try {
68
+ const stdout = execSync('git config --global user.email', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
69
+ return stdout.trim();
70
+ } catch (err) {
71
+ // git not installed or no email config
72
+ }
73
+ return '';
74
+ }
75
+
76
+ function getMachineIdHash() {
77
+ try {
78
+ const hostname = os.hostname();
79
+ return crypto.createHash('sha256').update(hostname).digest('hex').substring(0, 16);
80
+ } catch (err) {
81
+ return 'unknown_machine';
82
+ }
83
+ }
84
+
85
+ module.exports = {
86
+ loadConfig,
87
+ saveConfig,
88
+ detectGitEmail,
89
+ getMachineIdHash,
90
+ CREDENTIALS_PATH
91
+ };
@@ -0,0 +1,20 @@
1
+ const crypto = require('crypto');
2
+
3
+ /**
4
+ * Sign an event object using HMAC-SHA256 and return a string <b64_json>.<b64_hmac_sig>
5
+ */
6
+ function signEvent(eventObj, teamSecret) {
7
+ const jsonStr = JSON.stringify(eventObj);
8
+ const b64_json = Buffer.from(jsonStr).toString('base64');
9
+
10
+ // If no team_secret is provided, fallback to empty string to ensure HMAC doesn't throw,
11
+ // but it will fail verification on server.
12
+ const secret = teamSecret || '';
13
+ const b64_sig = crypto.createHmac('sha256', secret).update(jsonStr).digest('base64');
14
+
15
+ return `${b64_json}.${b64_sig}`;
16
+ }
17
+
18
+ module.exports = {
19
+ signEvent
20
+ };
@@ -0,0 +1,162 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const https = require("https");
5
+ const { loadConfig, saveConfig } = require("./config");
6
+
7
+ const GLOBAL_AIFLOW_DIR = path.join(os.homedir(), ".aiflow");
8
+ const TELEMETRY_DIR = path.join(GLOBAL_AIFLOW_DIR, "telemetry");
9
+ const BUFFER_PATH = path.join(TELEMETRY_DIR, "buffer.jsonl");
10
+ const LOCK_PATH = path.join(TELEMETRY_DIR, "flush.lock");
11
+
12
+ const MAX_BATCH_SIZE = 100;
13
+
14
+ function sendData(url, payload) {
15
+ return new Promise((resolve, reject) => {
16
+ const data = JSON.stringify(payload);
17
+ let urlObj;
18
+ try {
19
+ urlObj = new URL(url);
20
+ } catch (e) {
21
+ return reject(new Error("Invalid URL"));
22
+ }
23
+
24
+ const options = {
25
+ hostname: urlObj.hostname,
26
+ path: urlObj.pathname + urlObj.search,
27
+ method: "POST",
28
+ headers: {
29
+ "Content-Type": "application/json",
30
+ "Content-Length": Buffer.byteLength(data),
31
+ },
32
+ timeout: 5000,
33
+ };
34
+
35
+ const req = https.request(options, (res) => {
36
+ // Google Apps Script POST to /exec returns 302 *after* doPost has already executed.
37
+ // Treat 302 as success immediately — following the redirect is best-effort only.
38
+ // Never retry on 302: the data was already written; retrying causes duplicates.
39
+ if (res.statusCode === 302 || res.statusCode === 301) {
40
+ res.resume(); // drain response body
41
+ const redirectUrl = res.headers.location;
42
+ if (redirectUrl) {
43
+ https
44
+ .get(redirectUrl, (redirectRes) => {
45
+ redirectRes.resume(); // drain, ignore body
46
+ })
47
+ .on("error", () => {}); // best-effort, do not reject
48
+ }
49
+ resolve(true); // GAS already processed the request
50
+ } else {
51
+ let body = "";
52
+ res.on("data", (chunk) => (body += chunk));
53
+ res.on("end", () => {
54
+ if (body === "OK") resolve(true);
55
+ else reject(new Error(`Status ${res.statusCode}: ${body.substring(0, 300)}`));
56
+ });
57
+ }
58
+ });
59
+
60
+ req.on("timeout", () => {
61
+ req.destroy();
62
+ reject(new Error("Timeout"));
63
+ });
64
+ req.on("error", reject);
65
+ req.write(data);
66
+ req.end();
67
+ });
68
+ }
69
+
70
+ const wait = (ms) => new Promise((r) => setTimeout(r, ms));
71
+
72
+ async function main() {
73
+ setTimeout(() => {
74
+ try { if (fs.existsSync(LOCK_PATH)) fs.unlinkSync(LOCK_PATH); } catch (e) {}
75
+ process.exit(0);
76
+ }, 40000); // 40s hard exit — always release lock before dying
77
+
78
+ // Debounce sleep: Wait 10s to accumulate rapid command executions
79
+ await wait(10000);
80
+
81
+ const cfg = loadConfig();
82
+ if (!cfg.enabled || !cfg.apps_script_url) return;
83
+
84
+ if (!fs.existsSync(BUFFER_PATH)) return;
85
+
86
+ const content = fs.readFileSync(BUFFER_PATH, "utf-8");
87
+ const lines = content.split("\n").filter((l) => l.trim().length > 0);
88
+
89
+ if (lines.length === 0) return;
90
+
91
+ // Validate line format <b64_json>.<b64_sig>
92
+ const validLines = lines.filter((l) => l.includes("."));
93
+
94
+ if (validLines.length === 0) {
95
+ fs.unlinkSync(BUFFER_PATH);
96
+ return;
97
+ }
98
+
99
+ // Take first MAX_BATCH_SIZE
100
+ const batch = validLines.slice(0, MAX_BATCH_SIZE);
101
+ const remaining = validLines.slice(MAX_BATCH_SIZE);
102
+
103
+ const payload = {
104
+ email: cfg.email,
105
+ install_id: cfg.install_id,
106
+ lines: batch,
107
+ };
108
+
109
+ let success = false;
110
+ let lastError = null;
111
+ const retries = [2000, 5000, 15000];
112
+
113
+ for (let i = 0; i <= retries.length; i++) {
114
+ try {
115
+ await sendData(cfg.apps_script_url, payload);
116
+ success = true;
117
+ break;
118
+ } catch (err) {
119
+ lastError = err;
120
+ if (
121
+ typeof err === "object" &&
122
+ err !== null &&
123
+ String(err.message).includes("Status 4")
124
+ ) {
125
+ // 4xx error (e.g. 403 Forbidden because of invalid HMAC) -> no point retrying
126
+ break;
127
+ }
128
+ if (i < retries.length) {
129
+ await wait(retries[i]);
130
+ }
131
+ }
132
+ }
133
+
134
+ if (success) {
135
+ saveConfig({ last_flush_ts: Date.now() });
136
+ if (remaining.length > 0) {
137
+ // Atomic write remaining
138
+ const tmpPath = BUFFER_PATH + ".tmp";
139
+ fs.writeFileSync(tmpPath, remaining.join("\n") + "\n", "utf-8");
140
+ fs.renameSync(tmpPath, BUFFER_PATH);
141
+ } else {
142
+ fs.unlinkSync(BUFFER_PATH);
143
+ }
144
+ } else if (lastError) {
145
+ try {
146
+ const errLog = path.join(path.dirname(BUFFER_PATH), "errors.log");
147
+ fs.appendFileSync(
148
+ errLog,
149
+ `[${new Date().toISOString()}] FLUSH ERROR: ${lastError.message}\n`,
150
+ );
151
+ } catch (e) {}
152
+ }
153
+
154
+ // Remove lock so next record() can spawn a new flusher
155
+ try {
156
+ if (fs.existsSync(LOCK_PATH)) {
157
+ fs.unlinkSync(LOCK_PATH);
158
+ }
159
+ } catch (e) {}
160
+ }
161
+
162
+ main().catch(() => process.exit(0));
@@ -0,0 +1,138 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+ const { spawn } = require('child_process');
6
+ const { loadConfig, getMachineIdHash } = require('./config');
7
+ const { signEvent } = require('./crypto');
8
+
9
+ // Use relative path for getting package.json
10
+ let PKG_VERSION = 'unknown';
11
+ try {
12
+ const pkg = require('../../package.json');
13
+ PKG_VERSION = pkg.version;
14
+ } catch (e) {}
15
+
16
+ const GLOBAL_AIFLOW_DIR = path.join(os.homedir(), '.aiflow');
17
+ const TELEMETRY_DIR = path.join(GLOBAL_AIFLOW_DIR, 'telemetry');
18
+ const BUFFER_PATH = path.join(TELEMETRY_DIR, 'buffer.jsonl');
19
+ const ERRORS_PATH = path.join(TELEMETRY_DIR, 'errors.log');
20
+
21
+ const FLUSH_SCRIPT = path.join(__dirname, 'flush.js');
22
+
23
+ function ensureDirectories() {
24
+ if (!fs.existsSync(TELEMETRY_DIR)) {
25
+ fs.mkdirSync(TELEMETRY_DIR, { recursive: true });
26
+ }
27
+ }
28
+
29
+ function findGitRoot(currentPath) {
30
+ if (!currentPath || currentPath === path.parse(currentPath).root) return null;
31
+ if (fs.existsSync(path.join(currentPath, '.git'))) return currentPath;
32
+ return findGitRoot(path.dirname(currentPath));
33
+ }
34
+
35
+ function isRepoOptedOut() {
36
+ const repoRoot = findGitRoot(process.cwd());
37
+ if (!repoRoot) return false;
38
+ return fs.existsSync(path.join(repoRoot, '.aiflow', 'no-telemetry'));
39
+ }
40
+
41
+ function detectRepoName() {
42
+ const repoRoot = findGitRoot(process.cwd());
43
+ if (repoRoot) return path.basename(repoRoot);
44
+ return path.basename(process.cwd());
45
+ }
46
+
47
+ function logInternalError(err) {
48
+ try {
49
+ ensureDirectories();
50
+ const ts = new Date().toISOString();
51
+ const msg = `[${ts}] ${err.name}: ${err.message}\n${err.stack || ''}\n`;
52
+ fs.appendFileSync(ERRORS_PATH, msg);
53
+ // Simple log rotation if > 1MB
54
+ const stats = fs.statSync(ERRORS_PATH);
55
+ if (stats.size > 1024 * 1024) {
56
+ fs.renameSync(ERRORS_PATH, ERRORS_PATH + '.1');
57
+ }
58
+ } catch (e) {}
59
+ }
60
+
61
+ function record(eventType, payload = {}) {
62
+ try {
63
+ const cfg = loadConfig();
64
+ if (!cfg.enabled) return;
65
+ if (isRepoOptedOut()) return;
66
+
67
+ ensureDirectories();
68
+
69
+ const event = {
70
+ event_id: crypto.randomUUID(),
71
+ ts: new Date().toISOString(),
72
+ email: cfg.email || '',
73
+ install_id: cfg.install_id || '',
74
+ machine_id: getMachineIdHash(),
75
+ repo_name: detectRepoName(),
76
+ pkg_version: PKG_VERSION,
77
+ os: process.platform,
78
+ node_version: process.version,
79
+ event_type: eventType,
80
+ ...payload
81
+ };
82
+
83
+ // Drop any full content payload keys immediately just in case caller included them
84
+ delete event.prompt_content;
85
+ delete event.response_content;
86
+
87
+ const lineStr = signEvent(event, cfg.team_secret);
88
+ fs.appendFileSync(BUFFER_PATH, lineStr + '\n');
89
+
90
+ // Check if buffer is getting huge (> 10MB cap). Simple clear
91
+ try {
92
+ if (fs.statSync(BUFFER_PATH).size > 10 * 1024 * 1024) {
93
+ fs.unlinkSync(BUFFER_PATH);
94
+ }
95
+ } catch (e) {}
96
+
97
+ maybeSpawnFlusher(cfg);
98
+ } catch (err) {
99
+ logInternalError(err);
100
+ }
101
+ }
102
+
103
+ const LOCK_PATH = path.join(TELEMETRY_DIR, 'flush.lock');
104
+
105
+ function safeStatSize(filePath) {
106
+ try { return fs.statSync(filePath).size; } catch (err) { return 0; }
107
+ }
108
+
109
+ function maybeSpawnFlusher(cfg) {
110
+ const bufferSize = safeStatSize(BUFFER_PATH);
111
+ if (bufferSize === 0) return;
112
+
113
+ // Check if a flusher is already running in background (lock < 15s old)
114
+ try {
115
+ if (fs.existsSync(LOCK_PATH)) {
116
+ const stat = fs.statSync(LOCK_PATH);
117
+ if (Date.now() - stat.mtimeMs < 15000) {
118
+ return; // Already debounced, existing process will flush our new data soon
119
+ }
120
+ }
121
+ } catch (err) {}
122
+
123
+ try {
124
+ // Touch lock file
125
+ fs.writeFileSync(LOCK_PATH, Date.now().toString());
126
+ const child = spawn(process.execPath, [FLUSH_SCRIPT], {
127
+ detached: true,
128
+ stdio: 'ignore',
129
+ windowsHide: true,
130
+ env: process.env
131
+ });
132
+ child.unref();
133
+ } catch (err) {
134
+ logInternalError(err);
135
+ }
136
+ }
137
+
138
+ module.exports = { record };