@brunobracaioli/aibrain 0.2.0-beta.1 → 0.3.0-beta.13

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.
@@ -0,0 +1,133 @@
1
+ import { readConfig, telemetryConfigBinding } from './state.mjs';
2
+ import { reportContextTelemetry } from './mcp.mjs';
3
+ // @ts-expect-error This validator is generated into dist from the shared TypeScript contract.
4
+ import { parseContextTelemetryReport } from './context-telemetry.mjs';
5
+ import { resolve } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ export const MAX_STDIN_BYTES = 8 * 1024;
9
+ const ERROR_CODES = new Set([
10
+ 'telemetry_input_too_large',
11
+ 'telemetry_input_invalid',
12
+ 'telemetry_consent_required',
13
+ 'telemetry_state_invalid',
14
+ 'telemetry_state_mismatch',
15
+ 'telemetry_state_changed',
16
+ 'telemetry_report_failed',
17
+ ]);
18
+
19
+ /** @param {unknown} error */
20
+ function fixedErrorCode(error) {
21
+ const value = error instanceof Error ? error.message : '';
22
+ return ERROR_CODES.has(value) ? value : 'telemetry_report_failed';
23
+ }
24
+
25
+ /** @param {unknown[]} argv */
26
+ function parseStateDir(argv) {
27
+ if (
28
+ argv.length !== 4 ||
29
+ argv[0] !== '--state-dir' ||
30
+ typeof argv[1] !== 'string' ||
31
+ argv[1].length === 0 ||
32
+ !argv[1].startsWith('/') ||
33
+ Buffer.byteLength(argv[1], 'utf8') > 4096 ||
34
+ argv[2] !== '--config-digest' ||
35
+ typeof argv[3] !== 'string' ||
36
+ !/^[0-9a-f]{64}$/u.test(argv[3])
37
+ )
38
+ throw new Error('telemetry_state_invalid');
39
+ return { stateDir: argv[1], configDigest: argv[3] };
40
+ }
41
+
42
+ /** @param {unknown} raw @param {Date} [now] */
43
+ export function parseTelemetryInput(raw, now = new Date()) {
44
+ if (typeof raw !== 'string' || Buffer.byteLength(raw, 'utf8') > MAX_STDIN_BYTES)
45
+ throw new Error('telemetry_input_too_large');
46
+ let value;
47
+ try {
48
+ value = JSON.parse(raw);
49
+ } catch {
50
+ throw new Error('telemetry_input_invalid');
51
+ }
52
+ try {
53
+ return parseContextTelemetryReport(value, now);
54
+ } catch {
55
+ throw new Error('telemetry_input_invalid');
56
+ }
57
+ }
58
+
59
+ /**
60
+ * @param {string} stateDir
61
+ * @param {string} configDigest
62
+ * @param {Record<string, unknown>} report
63
+ * @param {(endpoint: string, token: string, report: Record<string, unknown>, deadlineMs: number) => Promise<{accepted: true, duplicate: boolean}>} [transport]
64
+ */
65
+ export async function executeTelemetryReport(
66
+ stateDir,
67
+ configDigest,
68
+ report,
69
+ transport = reportContextTelemetry,
70
+ ) {
71
+ const previousStateDir = process.env.AIBRAIN_STATE_DIR;
72
+ process.env.AIBRAIN_STATE_DIR = stateDir;
73
+ try {
74
+ const config = await readConfig();
75
+ if (
76
+ config === null ||
77
+ config.telemetry?.enabled !== true ||
78
+ typeof config.telemetry.disclosedAt !== 'string'
79
+ )
80
+ throw new Error('telemetry_consent_required');
81
+ let currentDigest;
82
+ try {
83
+ currentDigest = telemetryConfigBinding(config);
84
+ } catch {
85
+ throw new Error('telemetry_state_mismatch');
86
+ }
87
+ if (currentDigest !== configDigest) throw new Error('telemetry_state_changed');
88
+ return await transport(config.endpoint, config.token, report, 8_000);
89
+ } catch (error) {
90
+ if (error instanceof Error && ERROR_CODES.has(error.message)) throw error;
91
+ throw new Error('telemetry_report_failed');
92
+ } finally {
93
+ if (previousStateDir === undefined) delete process.env.AIBRAIN_STATE_DIR;
94
+ else process.env.AIBRAIN_STATE_DIR = previousStateDir;
95
+ }
96
+ }
97
+
98
+ async function readStdin() {
99
+ const chunks = [];
100
+ let total = 0;
101
+ for await (const chunk of process.stdin) {
102
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
103
+ total += bytes.byteLength;
104
+ if (total > MAX_STDIN_BYTES) throw new Error('telemetry_input_too_large');
105
+ chunks.push(bytes);
106
+ }
107
+ try {
108
+ return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks));
109
+ } catch {
110
+ throw new Error('telemetry_input_invalid');
111
+ }
112
+ }
113
+
114
+ /** @param {string[]} [argv] */
115
+ export async function main(argv = process.argv.slice(2)) {
116
+ try {
117
+ const { stateDir, configDigest } = parseStateDir(argv);
118
+ const report = parseTelemetryInput(await readStdin());
119
+ const result = await executeTelemetryReport(stateDir, configDigest, report);
120
+ process.stdout.write(
121
+ `${JSON.stringify({ schema_version: 1, accepted: true, duplicate: result.duplicate })}\n`,
122
+ );
123
+ return 0;
124
+ } catch (error) {
125
+ process.stdout.write(
126
+ `${JSON.stringify({ schema_version: 1, accepted: false, error_code: fixedErrorCode(error) })}\n`,
127
+ );
128
+ return 1;
129
+ }
130
+ }
131
+
132
+ if (process.argv[1] !== undefined && fileURLToPath(import.meta.url) === resolve(process.argv[1]))
133
+ process.exitCode = await main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brunobracaioli/aibrain",
3
- "version": "0.2.0-beta.1",
3
+ "version": "0.3.0-beta.13",
4
4
  "private": false,
5
5
  "description": "AI Brain assisted context beta client for Claude Code and Codex",
6
6
  "license": "UNLICENSED",