@harness-engineering/cli 1.25.0 → 1.25.2
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/bin/harness-mcp.js +1 -1
- package/dist/bin/harness.js +93 -2
- package/dist/{chunk-AZT567I6.js → chunk-5I3W4J5X.js} +38 -11
- package/dist/{chunk-RX7TUMBR.js → chunk-TBGIHPNA.js} +2 -0
- package/dist/hooks/adoption-tracker.js +189 -0
- package/dist/hooks/block-no-verify.js +50 -0
- package/dist/hooks/cost-tracker.js +66 -0
- package/dist/hooks/pre-compact-state.js +115 -0
- package/dist/hooks/profiles.ts +48 -0
- package/dist/hooks/protect-config.js +75 -0
- package/dist/hooks/quality-gate.js +131 -0
- package/dist/hooks/sentinel-post.js +159 -0
- package/dist/hooks/sentinel-pre.js +244 -0
- package/dist/hooks/telemetry-reporter.js +248 -0
- package/dist/index.js +2 -2
- package/dist/{mcp-2553PNUC.js → mcp-W3FLXSFF.js} +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* global setTimeout, fetch, AbortSignal */
|
|
3
|
+
// telemetry-reporter.js — Stop:* hook
|
|
4
|
+
// Reads adoption.jsonl, resolves consent, sends telemetry events to PostHog,
|
|
5
|
+
// and shows a one-time first-run privacy notice.
|
|
6
|
+
// Exit codes: 0 = allow (always, log-only hook — never blocks session teardown)
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import process from 'node:process';
|
|
12
|
+
|
|
13
|
+
// PostHog project API key — public, write-only (cannot read data)
|
|
14
|
+
const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY ?? 'phc_wNTdCMcfJXZPgdNeDociZW6vwoGGo4nb7vqEfWThFfsG'; // harness-ignore SEC-SEC-002: public PostHog write-only ingest key
|
|
15
|
+
const POSTHOG_BATCH_URL = 'https://app.posthog.com/batch';
|
|
16
|
+
const MAX_ATTEMPTS = 3;
|
|
17
|
+
const TIMEOUT_MS = 5000;
|
|
18
|
+
|
|
19
|
+
const FIRST_RUN_NOTICE = `Harness collects anonymous usage analytics to improve the tool.
|
|
20
|
+
No personal information is sent. Disable with:
|
|
21
|
+
DO_NOT_TRACK=1 or harness.config.json \u2192 telemetry.enabled: false\n`;
|
|
22
|
+
|
|
23
|
+
// --- Helpers ---
|
|
24
|
+
|
|
25
|
+
function readJsonSafe(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sleep(ms) {
|
|
34
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- Consent ---
|
|
38
|
+
|
|
39
|
+
function resolveConsent(cwd) {
|
|
40
|
+
if (process.env.DO_NOT_TRACK === '1') return { allowed: false };
|
|
41
|
+
if (process.env.HARNESS_TELEMETRY_OPTOUT === '1') return { allowed: false };
|
|
42
|
+
|
|
43
|
+
const config = readJsonSafe(join(cwd, 'harness.config.json'));
|
|
44
|
+
if (config?.telemetry?.enabled === false) return { allowed: false };
|
|
45
|
+
|
|
46
|
+
const installId = getOrCreateInstallId(cwd);
|
|
47
|
+
|
|
48
|
+
const identityFile = readJsonSafe(join(cwd, '.harness', 'telemetry.json'));
|
|
49
|
+
const identity = {};
|
|
50
|
+
if (identityFile?.identity) {
|
|
51
|
+
if (typeof identityFile.identity.project === 'string') identity.project = identityFile.identity.project;
|
|
52
|
+
if (typeof identityFile.identity.team === 'string') identity.team = identityFile.identity.team;
|
|
53
|
+
if (typeof identityFile.identity.alias === 'string') identity.alias = identityFile.identity.alias;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { allowed: true, installId, identity };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- Install ID ---
|
|
60
|
+
|
|
61
|
+
const UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
62
|
+
|
|
63
|
+
function getOrCreateInstallId(cwd) {
|
|
64
|
+
const harnessDir = join(cwd, '.harness');
|
|
65
|
+
const installIdFile = join(harnessDir, '.install-id');
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const existing = readFileSync(installIdFile, 'utf-8').trim();
|
|
69
|
+
if (UUID_V4_RE.test(existing)) return existing;
|
|
70
|
+
} catch {
|
|
71
|
+
// File does not exist
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const id = randomUUID();
|
|
75
|
+
mkdirSync(harnessDir, { recursive: true });
|
|
76
|
+
writeFileSync(installIdFile, id, { encoding: 'utf-8', mode: 0o600 });
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --- Collector ---
|
|
81
|
+
|
|
82
|
+
function readAdoptionRecords(cwd) {
|
|
83
|
+
const adoptionFile = join(cwd, '.harness', 'metrics', 'adoption.jsonl');
|
|
84
|
+
let raw;
|
|
85
|
+
try {
|
|
86
|
+
raw = readFileSync(adoptionFile, 'utf-8');
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const records = [];
|
|
92
|
+
for (const line of raw.split('\n')) {
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
if (!trimmed) continue;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(trimmed);
|
|
97
|
+
if (
|
|
98
|
+
typeof parsed.skill === 'string' &&
|
|
99
|
+
typeof parsed.startedAt === 'string' &&
|
|
100
|
+
typeof parsed.duration === 'number' &&
|
|
101
|
+
typeof parsed.outcome === 'string' &&
|
|
102
|
+
Array.isArray(parsed.phasesReached)
|
|
103
|
+
) {
|
|
104
|
+
records.push(parsed);
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// Skip malformed lines
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return records;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function collectEvents(cwd, consent) {
|
|
114
|
+
const records = readAdoptionRecords(cwd);
|
|
115
|
+
if (records.length === 0) return [];
|
|
116
|
+
|
|
117
|
+
const { installId, identity } = consent;
|
|
118
|
+
const distinctId = identity.alias ?? installId;
|
|
119
|
+
|
|
120
|
+
return records.map((record) => ({
|
|
121
|
+
event: 'skill_invocation',
|
|
122
|
+
distinctId,
|
|
123
|
+
timestamp: record.startedAt,
|
|
124
|
+
properties: {
|
|
125
|
+
installId,
|
|
126
|
+
os: process.platform,
|
|
127
|
+
nodeVersion: process.version,
|
|
128
|
+
harnessVersion: readHarnessVersion(cwd),
|
|
129
|
+
skillName: record.skill,
|
|
130
|
+
duration: record.duration,
|
|
131
|
+
outcome: record.outcome === 'completed' ? 'success' : 'failure',
|
|
132
|
+
phasesReached: record.phasesReached,
|
|
133
|
+
...(identity.project ? { project: identity.project } : {}),
|
|
134
|
+
...(identity.team ? { team: identity.team } : {}),
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readHarnessVersion(cwd) {
|
|
140
|
+
try {
|
|
141
|
+
const pkg = readJsonSafe(join(cwd, 'node_modules', '@harness-engineering', 'core', 'package.json'));
|
|
142
|
+
return pkg?.version ?? 'unknown';
|
|
143
|
+
} catch {
|
|
144
|
+
return 'unknown';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Transport ---
|
|
149
|
+
|
|
150
|
+
async function sendEvents(events) {
|
|
151
|
+
if (events.length === 0) return;
|
|
152
|
+
|
|
153
|
+
const payload = JSON.stringify({ api_key: POSTHOG_API_KEY, batch: events });
|
|
154
|
+
|
|
155
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
156
|
+
try {
|
|
157
|
+
const res = await fetch(POSTHOG_BATCH_URL, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
160
|
+
body: payload,
|
|
161
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
162
|
+
});
|
|
163
|
+
if (res.ok) return;
|
|
164
|
+
if (res.status < 500) return; // 4xx = permanent, do not retry
|
|
165
|
+
} catch {
|
|
166
|
+
// Network error or timeout — retry
|
|
167
|
+
}
|
|
168
|
+
if (attempt < MAX_ATTEMPTS - 1) {
|
|
169
|
+
await sleep(1000 * (attempt + 1));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Silent failure — all retries exhausted
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- First-run notice ---
|
|
176
|
+
|
|
177
|
+
function showFirstRunNotice(cwd) {
|
|
178
|
+
const flagFile = join(cwd, '.harness', '.telemetry-notice-shown');
|
|
179
|
+
if (existsSync(flagFile)) return;
|
|
180
|
+
|
|
181
|
+
process.stderr.write(FIRST_RUN_NOTICE);
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
mkdirSync(join(cwd, '.harness'), { recursive: true });
|
|
185
|
+
writeFileSync(flagFile, new Date().toISOString(), { encoding: 'utf-8' });
|
|
186
|
+
} catch {
|
|
187
|
+
// Non-fatal — notice will show again next time
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- Main ---
|
|
192
|
+
|
|
193
|
+
async function main() {
|
|
194
|
+
let raw = '';
|
|
195
|
+
try {
|
|
196
|
+
raw = readFileSync(0, 'utf-8');
|
|
197
|
+
} catch {
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!raw.trim()) {
|
|
202
|
+
process.exit(0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Parse stdin (stop hook receives session JSON)
|
|
206
|
+
try {
|
|
207
|
+
JSON.parse(raw);
|
|
208
|
+
} catch {
|
|
209
|
+
process.stderr.write('[telemetry-reporter] Could not parse stdin — skipping\n');
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const cwd = process.cwd();
|
|
215
|
+
const consent = resolveConsent(cwd);
|
|
216
|
+
|
|
217
|
+
if (!consent.allowed) {
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Show first-run notice (before sending, so user sees it even if send fails)
|
|
222
|
+
showFirstRunNotice(cwd);
|
|
223
|
+
|
|
224
|
+
const events = collectEvents(cwd, consent);
|
|
225
|
+
if (events.length === 0) {
|
|
226
|
+
process.stderr.write('[telemetry-reporter] No adoption records to report\n');
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await sendEvents(events);
|
|
231
|
+
|
|
232
|
+
// Truncate adoption.jsonl after successful send to prevent re-sending
|
|
233
|
+
const adoptionFile = join(cwd, '.harness', 'metrics', 'adoption.jsonl');
|
|
234
|
+
try {
|
|
235
|
+
writeFileSync(adoptionFile, '', 'utf-8');
|
|
236
|
+
} catch {
|
|
237
|
+
// Non-fatal — records may be re-sent next session
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
process.stderr.write(`[telemetry-reporter] Sent ${events.length} telemetry event(s)\n`);
|
|
241
|
+
process.exit(0);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
process.stderr.write(`[telemetry-reporter] Failed: ${err.message}\n`);
|
|
244
|
+
process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
main();
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
runSnapshotCapture,
|
|
13
13
|
runUninstall,
|
|
14
14
|
runUninstallConstraints
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-5I3W4J5X.js";
|
|
16
16
|
import {
|
|
17
17
|
generateAgentsMd
|
|
18
18
|
} from "./chunk-5BQ5BOJL.js";
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
generateSlashCommands,
|
|
67
67
|
getToolDefinitions,
|
|
68
68
|
startServer
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-TBGIHPNA.js";
|
|
70
70
|
import "./chunk-FES2YEQU.js";
|
|
71
71
|
import "./chunk-UV3BZMGT.js";
|
|
72
72
|
import "./chunk-F23H3U5U.js";
|