@relipa/ai-flow-kit 0.0.5 → 0.0.6-beta.0
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/README.md +81 -69
- package/bin/aiflow.js +46 -2
- package/custom/rules/java/spring-boot-examples.md +329 -0
- package/custom/skills/read-study-requirement/SKILL.md +54 -0
- package/custom/skills/review-plan/SKILL.md +21 -0
- package/custom/templates/shared/gate-workflow.md +88 -88
- package/custom/templates/spring-boot.md +224 -523
- package/docs/common/AIFLOW.md +501 -462
- package/docs/common/CHANGELOG.md +162 -132
- package/docs/common/QUICK_START.md +44 -12
- package/docs/common/cli-reference.md +115 -6
- package/docs/common/getting-started.md +3 -0
- package/docs/project/ARCHITECTURE.md +28 -28
- package/package.json +6 -1
- package/scripts/doctor.js +89 -48
- package/scripts/guide.js +28 -5
- package/scripts/hooks/session-start.js +144 -45
- package/scripts/init.js +109 -50
- package/scripts/prompt.js +431 -431
- package/scripts/task.js +384 -0
- package/scripts/telemetry/cli.js +243 -243
- package/scripts/telemetry/config.js +91 -91
- package/scripts/telemetry/crypto.js +20 -20
- package/scripts/telemetry/flush.js +162 -162
- package/scripts/telemetry/record.js +138 -138
- package/scripts/use.js +94 -2
|
@@ -1,20 +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
|
-
};
|
|
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
|
+
};
|
|
@@ -1,162 +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));
|
|
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));
|
|
@@ -1,138 +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 };
|
|
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 };
|