sandoichi 0.1.5 → 0.2.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 +14 -0
- package/index.mjs +2 -0
- package/package.json +1 -1
- package/src/core.mjs +18 -5
- package/src/hook-cli.mjs +43 -18
- package/src/mcp-server.mjs +2 -1
- package/src/postinstall.mjs +2 -1
- package/src/proxy.mjs +94 -47
- package/src/redaction-config.mjs +104 -0
- package/src/redaction-profile.mjs +163 -0
- package/src/secret-redaction.mjs +12 -16
- package/src/semantic-compactor.mjs +14 -5
- package/src/session-start.mjs +15 -3
- package/src/telemetry-cli.mjs +21 -3
- package/src/telemetry-flush-entry.mjs +3 -3
- package/src/telemetry.mjs +223 -64
- package/src/version.mjs +29 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const BUILT_IN_MATCHERS = Object.freeze([
|
|
4
|
+
[/-----BEGIN [A-Z ]+ KEY-----[\s\S]*?-----END [A-Z ]+ KEY-----/g, '[REDACTED PRIVATE KEY]'],
|
|
5
|
+
[/(authorization\s*[:=]\s*(?!(?:bearer\s+)?\[REDACTED(?: (?:PRIVATE KEY|TOKEN))?\](?=$|[\s,"'}]))(?:bearer\s+)?)[^\s,"'}]+/gi,
|
|
6
|
+
(_match, prefix) => `${prefix}[REDACTED]`],
|
|
7
|
+
[/(["']?(?:api[_-]?key|access[_-]?token|password|secret|private[_-]?key)["']?\s*[:=]\s*["']?)(?!\[REDACTED(?: (?:PRIVATE KEY|TOKEN))?\](?=$|[\s,"'}]))[^\s,"'}]+/gi,
|
|
8
|
+
(_match, prefix) => `${prefix}[REDACTED]`],
|
|
9
|
+
[/\b(?:sk|rk)-[A-Za-z0-9_-]{12,}\b/g, '[REDACTED TOKEN]'],
|
|
10
|
+
[/\bgh[pousr]_[A-Za-z0-9_-]{12,}\b/g, '[REDACTED TOKEN]'],
|
|
11
|
+
[/\bgithub_pat_[A-Za-z0-9_-]{20,}\b/g, '[REDACTED TOKEN]'],
|
|
12
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED TOKEN]'],
|
|
13
|
+
]);
|
|
14
|
+
const PROFILE_SCHEMA = 'sando-redaction-profile-v1';
|
|
15
|
+
const MAX_CUSTOM_RULES = 100;
|
|
16
|
+
const MAX_NAME_LENGTH = 64;
|
|
17
|
+
const MAX_TOKEN_LENGTH = 4096;
|
|
18
|
+
|
|
19
|
+
function escapeRegExp(value) {
|
|
20
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compareCanonical(left, right) {
|
|
24
|
+
const a = JSON.stringify(left);
|
|
25
|
+
const b = JSON.stringify(right);
|
|
26
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeRules(customRules) {
|
|
30
|
+
if (!Array.isArray(customRules)) throw new TypeError('customRules must be an array');
|
|
31
|
+
if (customRules.length > MAX_CUSTOM_RULES) throw new TypeError('customRules must contain at most 100 rules');
|
|
32
|
+
const identities = new Set();
|
|
33
|
+
const normalized = customRules.map((rule) => {
|
|
34
|
+
if (!rule || typeof rule !== 'object' || Array.isArray(rule)
|
|
35
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(rule))) {
|
|
36
|
+
throw new TypeError('each custom rule must be a plain object');
|
|
37
|
+
}
|
|
38
|
+
let result;
|
|
39
|
+
let identity;
|
|
40
|
+
if (rule.type === 'assignment-key') {
|
|
41
|
+
const allowed = new Set(['type', 'key']);
|
|
42
|
+
if (Reflect.ownKeys(rule).some((field) => typeof field !== 'string' || !allowed.has(field))
|
|
43
|
+
|| typeof rule.key !== 'string'
|
|
44
|
+
|| !new RegExp(`^[A-Za-z0-9][A-Za-z0-9_.-]{0,${MAX_NAME_LENGTH - 1}}$`).test(rule.key)) {
|
|
45
|
+
throw new TypeError('invalid assignment-key rule');
|
|
46
|
+
}
|
|
47
|
+
const key = rule.key.toLowerCase();
|
|
48
|
+
result = { type: rule.type, key };
|
|
49
|
+
identity = `${rule.type}:${key}`;
|
|
50
|
+
} else if (rule.type === 'token-prefix') {
|
|
51
|
+
const allowed = new Set(['type', 'prefix', 'minLength', 'maxLength']);
|
|
52
|
+
if (Reflect.ownKeys(rule).some((field) => typeof field !== 'string' || !allowed.has(field))
|
|
53
|
+
|| typeof rule.prefix !== 'string'
|
|
54
|
+
|| !new RegExp(`^[\\x21-\\x7e]{1,${MAX_NAME_LENGTH}}$`).test(rule.prefix)) {
|
|
55
|
+
throw new TypeError('invalid token-prefix rule');
|
|
56
|
+
}
|
|
57
|
+
const minLength = Object.hasOwn(rule, 'minLength') ? rule.minLength : 12;
|
|
58
|
+
const maxLength = Object.hasOwn(rule, 'maxLength') ? rule.maxLength : null;
|
|
59
|
+
if (!Number.isSafeInteger(minLength) || minLength < 1 || minLength > MAX_TOKEN_LENGTH
|
|
60
|
+
|| (maxLength !== null
|
|
61
|
+
&& (!Number.isSafeInteger(maxLength) || maxLength < minLength || maxLength > MAX_TOKEN_LENGTH))
|
|
62
|
+
|| (Object.hasOwn(rule, 'maxLength') && rule.maxLength === null)) {
|
|
63
|
+
throw new TypeError('invalid token-prefix lengths');
|
|
64
|
+
}
|
|
65
|
+
result = { type: rule.type, prefix: rule.prefix, minLength, maxLength };
|
|
66
|
+
identity = `${rule.type}:${rule.prefix}`;
|
|
67
|
+
} else {
|
|
68
|
+
throw new TypeError('unsupported custom rule type');
|
|
69
|
+
}
|
|
70
|
+
if (identities.has(identity)) throw new TypeError('duplicate custom rule');
|
|
71
|
+
identities.add(identity);
|
|
72
|
+
return result;
|
|
73
|
+
});
|
|
74
|
+
return normalized.sort(compareCanonical);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function customMatchers(rules) {
|
|
78
|
+
const assignments = [];
|
|
79
|
+
const tokens = [];
|
|
80
|
+
for (const rule of rules) {
|
|
81
|
+
if (rule.type === 'assignment-key') {
|
|
82
|
+
const key = escapeRegExp(rule.key);
|
|
83
|
+
assignments.push([
|
|
84
|
+
new RegExp(`(?<![A-Za-z0-9_.-])(["']?${key}["']?\\s*[:=]\\s*["']?)(?!\\[REDACTED\\](?=$|[\\s,"'}]))[^\\s,"'}]+`, 'gi'),
|
|
85
|
+
(_match, prefix) => `${prefix}[REDACTED]`,
|
|
86
|
+
]);
|
|
87
|
+
} else {
|
|
88
|
+
const maximum = rule.maxLength === null ? '' : rule.maxLength;
|
|
89
|
+
tokens.push([
|
|
90
|
+
new RegExp(`(?<![A-Za-z0-9_-])${escapeRegExp(rule.prefix)}[A-Za-z0-9_-]{${rule.minLength},${maximum}}(?![A-Za-z0-9_-])`, 'g'),
|
|
91
|
+
'[REDACTED]',
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { assignments, tokens };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function applyMatchers(text, matchers) {
|
|
99
|
+
let count = 0;
|
|
100
|
+
for (const [pattern, replacement] of matchers) {
|
|
101
|
+
text = text.replace(pattern, (...args) => {
|
|
102
|
+
count += 1;
|
|
103
|
+
return typeof replacement === 'function' ? replacement(...args) : replacement;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return { text, count };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createRedactionProfile(customRules = []) {
|
|
110
|
+
const rules = normalizeRules(customRules);
|
|
111
|
+
const custom = customMatchers(rules);
|
|
112
|
+
const matchers = [
|
|
113
|
+
...BUILT_IN_MATCHERS.slice(0, 3),
|
|
114
|
+
...custom.assignments,
|
|
115
|
+
...BUILT_IN_MATCHERS.slice(3),
|
|
116
|
+
...custom.tokens,
|
|
117
|
+
];
|
|
118
|
+
const digest = `sha256:${createHash('sha256')
|
|
119
|
+
.update(JSON.stringify({ schema: PROFILE_SCHEMA, customRules: rules }))
|
|
120
|
+
.digest('hex')}`;
|
|
121
|
+
const redact = (text) => {
|
|
122
|
+
if (typeof text !== 'string') throw new TypeError('text must be a string');
|
|
123
|
+
return applyMatchers(text, matchers);
|
|
124
|
+
};
|
|
125
|
+
const redactStructured = (value) => {
|
|
126
|
+
const ancestors = new Set();
|
|
127
|
+
const visit = (item) => {
|
|
128
|
+
if (typeof item === 'string') {
|
|
129
|
+
const result = redact(item);
|
|
130
|
+
return { value: result.text, count: result.count };
|
|
131
|
+
}
|
|
132
|
+
if (item === null || typeof item === 'boolean' || typeof item === 'number') {
|
|
133
|
+
return { value: item, count: 0 };
|
|
134
|
+
}
|
|
135
|
+
if (typeof item !== 'object'
|
|
136
|
+
|| (!Array.isArray(item) && ![Object.prototype, null].includes(Object.getPrototypeOf(item)))) {
|
|
137
|
+
throw new TypeError('value must contain only plain objects, arrays, and JSON primitives');
|
|
138
|
+
}
|
|
139
|
+
if (ancestors.has(item)) throw new TypeError('value must not be cyclic');
|
|
140
|
+
ancestors.add(item);
|
|
141
|
+
let count = 0;
|
|
142
|
+
const entries = Object.entries(item).map(([key, child]) => {
|
|
143
|
+
const result = visit(child);
|
|
144
|
+
count += result.count;
|
|
145
|
+
return [key, result.value];
|
|
146
|
+
});
|
|
147
|
+
ancestors.delete(item);
|
|
148
|
+
return {
|
|
149
|
+
value: Array.isArray(item) ? entries.map(([, child]) => child) : Object.fromEntries(entries),
|
|
150
|
+
count,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
return visit(value);
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
digest,
|
|
157
|
+
redact,
|
|
158
|
+
redactStructured,
|
|
159
|
+
hasSecret(text) {
|
|
160
|
+
return redact(text).count > 0;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
package/src/secret-redaction.mjs
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
|
+
import { createRedactionProfile } from './redaction-profile.mjs';
|
|
2
|
+
|
|
3
|
+
const builtInProfile = createRedactionProfile();
|
|
4
|
+
|
|
5
|
+
export { createRedactionProfile };
|
|
6
|
+
|
|
1
7
|
export function redact(text) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
});
|
|
8
|
-
};
|
|
9
|
-
replace(/-----BEGIN [A-Z ]+ KEY-----[\s\S]*?-----END [A-Z ]+ KEY-----/g, '[REDACTED PRIVATE KEY]');
|
|
10
|
-
replace(/\b(?:sk|rk)-[A-Za-z0-9_-]{12,}\b/g, '[REDACTED TOKEN]');
|
|
11
|
-
replace(/\bgh[pousr]_[A-Za-z0-9_-]{12,}\b/g, '[REDACTED TOKEN]');
|
|
12
|
-
replace(/\bgithub_pat_[A-Za-z0-9_-]{20,}\b/g, '[REDACTED TOKEN]');
|
|
13
|
-
replace(/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED TOKEN]');
|
|
14
|
-
replace(/(authorization\s*[:=]\s*(?:bearer\s+)?)[^\s,"'}]+/gi, (_match, prefix) => `${prefix}[REDACTED]`);
|
|
15
|
-
replace(/(["']?(?:api[_-]?key|access[_-]?token|password|secret|private[_-]?key)["']?\s*[:=]\s*["']?)[^\s,"'}]+/gi, (_match, prefix) => `${prefix}[REDACTED]`);
|
|
16
|
-
return { text, count };
|
|
8
|
+
return builtInProfile.redact(text);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function redactStructured(value, enabled = true) {
|
|
12
|
+
return enabled ? builtInProfile.redactStructured(value).value : value;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
15
|
export function hasSecret(text) {
|
|
20
|
-
return
|
|
16
|
+
return builtInProfile.hasSecret(text);
|
|
21
17
|
}
|
|
@@ -60,7 +60,9 @@ export function buildSemanticPrompt({ provider, model, toolName, text, requiredF
|
|
|
60
60
|
].join('\n');
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export function validateSemanticSummary({
|
|
63
|
+
export function validateSemanticSummary({
|
|
64
|
+
originalText, summary, requiredFacts = [], maxSummaryRatio = DEFAULT_POLICY.maxSummaryRatio, redactionProfile,
|
|
65
|
+
} = {}) {
|
|
64
66
|
if (typeof originalText !== 'string' || typeof summary !== 'string') {
|
|
65
67
|
return { valid: false, reason: 'invalid-text' };
|
|
66
68
|
}
|
|
@@ -77,7 +79,9 @@ export function validateSemanticSummary({ originalText, summary, requiredFacts =
|
|
|
77
79
|
}
|
|
78
80
|
const missing = required.find((fact) => !summary.includes(fact));
|
|
79
81
|
if (missing) return { valid: false, reason: 'missing-required-fact', missing, inputTokens, outputTokens };
|
|
80
|
-
if (hasSecret(summary)
|
|
82
|
+
if ((redactionProfile ? redactionProfile.hasSecret(summary) : hasSecret(summary))) {
|
|
83
|
+
return { valid: false, reason: 'secret-detected', inputTokens, outputTokens };
|
|
84
|
+
}
|
|
81
85
|
return { valid: true, inputTokens, outputTokens };
|
|
82
86
|
}
|
|
83
87
|
|
|
@@ -111,12 +115,15 @@ async function withTimeout(complete, request, timeoutMs) {
|
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
export function createSemanticCompactor({ complete, cache = new Map(), policy } = {}) {
|
|
118
|
+
export function createSemanticCompactor({ complete, cache = new Map(), policy, redactionProfile } = {}) {
|
|
115
119
|
if (typeof complete !== 'function') throw new TypeError('semantic compactor complete callback is required');
|
|
116
120
|
if (!cache || typeof cache.get !== 'function' || typeof cache.set !== 'function') {
|
|
117
121
|
throw new TypeError('semantic compactor cache must implement get and set');
|
|
118
122
|
}
|
|
119
123
|
const options = validatePolicy(policy);
|
|
124
|
+
if (redactionProfile && (typeof redactionProfile.redact !== 'function' || typeof redactionProfile.hasSecret !== 'function')) {
|
|
125
|
+
throw new TypeError('redactionProfile is invalid');
|
|
126
|
+
}
|
|
120
127
|
|
|
121
128
|
return async function compact({ provider, model, toolName, text, historical = true, isError = false, requiredFacts = [] } = {}) {
|
|
122
129
|
if (typeof text !== 'string') throw new TypeError('semantic compactor text must be a string');
|
|
@@ -136,8 +143,8 @@ export function createSemanticCompactor({ complete, cache = new Map(), policy }
|
|
|
136
143
|
if (isError) return { ...base, status: 'skipped', reason: 'error-result' };
|
|
137
144
|
if (inputTokens < options.minInputTokens) return { ...base, status: 'skipped', reason: 'below-threshold' };
|
|
138
145
|
|
|
139
|
-
const safe = redact(text);
|
|
140
|
-
const safeFacts = required.map((fact) => redact(fact).text);
|
|
146
|
+
const safe = redactionProfile ? redactionProfile.redact(text) : redact(text);
|
|
147
|
+
const safeFacts = required.map((fact) => (redactionProfile ? redactionProfile.redact(fact) : redact(fact)).text);
|
|
141
148
|
const prompt = buildSemanticPrompt({ provider, model, toolName, text: safe.text, requiredFacts: safeFacts });
|
|
142
149
|
const key = cacheKey({ provider, model, toolName, text: safe.text, requiredFacts: safeFacts });
|
|
143
150
|
const started = Date.now();
|
|
@@ -147,6 +154,7 @@ export function createSemanticCompactor({ complete, cache = new Map(), policy }
|
|
|
147
154
|
const validation = validateSemanticSummary({
|
|
148
155
|
originalText: text, summary: cached.summary, requiredFacts: required,
|
|
149
156
|
maxSummaryRatio: options.maxSummaryRatio,
|
|
157
|
+
redactionProfile,
|
|
150
158
|
});
|
|
151
159
|
if (validation.valid) {
|
|
152
160
|
const grossSavedTokens = inputTokens - validation.outputTokens;
|
|
@@ -191,6 +199,7 @@ export function createSemanticCompactor({ complete, cache = new Map(), policy }
|
|
|
191
199
|
const validation = validateSemanticSummary({
|
|
192
200
|
originalText: text, summary, requiredFacts: required,
|
|
193
201
|
maxSummaryRatio: options.maxSummaryRatio,
|
|
202
|
+
redactionProfile,
|
|
194
203
|
});
|
|
195
204
|
if (!validation.valid) return { ...base, reason: validation.reason, latencyMs: Date.now() - started };
|
|
196
205
|
|
package/src/session-start.mjs
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
closeFinishedDays, defaultTelemetryConfigPath, defaultTelemetryStatePaths, isDoNotTrack, readTelemetryConfig, TELEMETRY_DETAILS_URL,
|
|
5
|
+
} from './telemetry.mjs';
|
|
6
|
+
import { PLUGIN_VERSION } from './version.mjs';
|
|
4
7
|
|
|
5
8
|
export function runSessionStart({
|
|
6
9
|
env = process.env,
|
|
7
10
|
stdout = process.stdout,
|
|
8
11
|
rootEnv = 'PLUGIN_ROOT',
|
|
12
|
+
spawnImpl,
|
|
13
|
+
configPath = defaultTelemetryConfigPath(env),
|
|
14
|
+
statePaths = defaultTelemetryStatePaths(env),
|
|
9
15
|
} = {}) {
|
|
10
16
|
try {
|
|
11
|
-
const config = readTelemetryConfig(
|
|
12
|
-
if (config.
|
|
17
|
+
const config = readTelemetryConfig(configPath);
|
|
18
|
+
if (config.enabled && !isDoNotTrack(env)) {
|
|
19
|
+
closeFinishedDays({
|
|
20
|
+
statePaths, configPath, day: new Date().toISOString().slice(0, 10),
|
|
21
|
+
pluginVersion: PLUGIN_VERSION, ...(spawnImpl ? { spawnImpl } : {}),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (isDoNotTrack(env) || config.prompted_consent_version > 0) {
|
|
13
25
|
stdout.write('{}\n');
|
|
14
26
|
return;
|
|
15
27
|
}
|
package/src/telemetry-cli.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
defaultTelemetryConfigPath, defaultTelemetryStatePaths,
|
|
9
|
-
disableTelemetry, enableTelemetry, flushQueue, previewNextUpload, statusTelemetry, TELEMETRY_DETAILS_URL,
|
|
9
|
+
disableTelemetry, enableTelemetry, flushQueue, isDoNotTrack, previewNextUpload, statusTelemetry, TELEMETRY_DETAILS_URL,
|
|
10
10
|
} from './telemetry.mjs';
|
|
11
11
|
|
|
12
12
|
const USAGE = 'Usage: sando telemetry <status|enable|disable [--purge]|preview|flush>\n';
|
|
@@ -26,13 +26,26 @@ export async function runTelemetryCli({
|
|
|
26
26
|
try {
|
|
27
27
|
if (command === 'status') {
|
|
28
28
|
const config = statusTelemetry(configPath);
|
|
29
|
+
if (isDoNotTrack(env)) {
|
|
30
|
+
stdout.write('telemetry: disabled by DO_NOT_TRACK\n');
|
|
31
|
+
return { ...config, enabled: false };
|
|
32
|
+
}
|
|
29
33
|
stdout.write(`telemetry: ${config.enabled ? 'enabled' : 'disabled'}\n`);
|
|
30
34
|
return config;
|
|
31
35
|
}
|
|
32
36
|
if (command === 'enable') {
|
|
37
|
+
if (isDoNotTrack(env)) {
|
|
38
|
+
stderr.write('sando telemetry: DO_NOT_TRACK is set; telemetry remains disabled\n');
|
|
39
|
+
return { ...statusTelemetry(configPath), enabled: false, exitCode: 1 };
|
|
40
|
+
}
|
|
33
41
|
if (!interactive) {
|
|
34
42
|
stderr.write('sando telemetry: enable requires an interactive session\n');
|
|
35
|
-
return
|
|
43
|
+
return { ...statusTelemetry(configPath), exitCode: 1 };
|
|
44
|
+
}
|
|
45
|
+
const current = statusTelemetry(configPath);
|
|
46
|
+
if (current.enabled) {
|
|
47
|
+
stdout.write('telemetry already enabled.\n');
|
|
48
|
+
return current;
|
|
36
49
|
}
|
|
37
50
|
const answer = await prompt(CONSENT_PROMPT);
|
|
38
51
|
const result = enableTelemetry({
|
|
@@ -54,6 +67,10 @@ export async function runTelemetryCli({
|
|
|
54
67
|
return preview;
|
|
55
68
|
}
|
|
56
69
|
if (command === 'flush') {
|
|
70
|
+
if (isDoNotTrack(env)) {
|
|
71
|
+
stdout.write('telemetry disabled by DO_NOT_TRACK; nothing to flush.\n');
|
|
72
|
+
return { sent: 0 };
|
|
73
|
+
}
|
|
57
74
|
const config = statusTelemetry(configPath);
|
|
58
75
|
if (!config.enabled) {
|
|
59
76
|
stdout.write('telemetry is disabled; nothing to flush.\n');
|
|
@@ -72,5 +89,6 @@ export async function runTelemetryCli({
|
|
|
72
89
|
}
|
|
73
90
|
|
|
74
91
|
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
75
|
-
await runTelemetryCli();
|
|
92
|
+
const result = await runTelemetryCli();
|
|
93
|
+
if (result?.exitCode) process.exitCode = result.exitCode;
|
|
76
94
|
}
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { pathToFileURL } from 'node:url';
|
|
7
7
|
|
|
8
|
-
import { flushQueue, readTelemetryConfig } from './telemetry.mjs';
|
|
8
|
+
import { flushQueue, isDoNotTrack, readTelemetryConfig } from './telemetry.mjs';
|
|
9
9
|
|
|
10
10
|
function option(argv, name) {
|
|
11
11
|
const index = argv.indexOf(`--${name}`);
|
|
12
12
|
return index === -1 ? undefined : argv[index + 1];
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export async function runTelemetryFlushEntry({ argv = process.argv.slice(2) } = {}) {
|
|
15
|
+
export async function runTelemetryFlushEntry({ argv = process.argv.slice(2), env = process.env } = {}) {
|
|
16
16
|
const queuePath = option(argv, 'queue');
|
|
17
17
|
const configPath = option(argv, 'config');
|
|
18
18
|
if (!queuePath || !configPath) throw new Error('telemetry-flush-entry requires --queue and --config');
|
|
19
19
|
const config = readTelemetryConfig(configPath);
|
|
20
|
-
if (!config.enabled) return { sent: 0 };
|
|
20
|
+
if (!config.enabled || isDoNotTrack(env)) return { sent: 0 };
|
|
21
21
|
return flushQueue({ statePaths: { queue: queuePath }, endpoint: config.endpoint });
|
|
22
22
|
}
|
|
23
23
|
|