murmur8 4.5.1 → 4.7.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/.blueprint/features/feature_pipeline-telemetry/FEATURE_SPEC.md +297 -0
- package/.blueprint/features/feature_pipeline-telemetry/IMPLEMENTATION_PLAN.md +34 -0
- package/.blueprint/features/feature_pipeline-telemetry/handoff-alex.md +21 -0
- package/.blueprint/features/feature_pipeline-telemetry/handoff-cass.md +25 -0
- package/.blueprint/features/feature_pipeline-telemetry/handoff-nigel.md +20 -0
- package/.blueprint/features/feature_pipeline-telemetry/story-failed-queue-retry.md +53 -0
- package/.blueprint/features/feature_pipeline-telemetry/story-identifiers.md +47 -0
- package/.blueprint/features/feature_pipeline-telemetry/story-init-integration.md +48 -0
- package/.blueprint/features/feature_pipeline-telemetry/story-payload-send.md +54 -0
- package/.blueprint/features/feature_pipeline-telemetry/story-telemetry-activation.md +54 -0
- package/.blueprint/features/feature_pipeline-telemetry/story-telemetry-config-command.md +52 -0
- package/.blueprint/features/feature_refine-feature-skill/FEATURE_SPEC.md +180 -0
- package/.blueprint/features/feature_refine-feature-skill/IMPLEMENTATION_PLAN.md +47 -0
- package/.blueprint/features/feature_refine-feature-skill/handoff-alex.md +19 -0
- package/.blueprint/features/feature_refine-feature-skill/handoff-cass.md +26 -0
- package/.blueprint/features/feature_refine-feature-skill/handoff-nigel.md +30 -0
- package/.blueprint/features/feature_refine-feature-skill/story-codey-confirmation.md +41 -0
- package/.blueprint/features/feature_refine-feature-skill/story-conversation-approval.md +41 -0
- package/.blueprint/features/feature_refine-feature-skill/story-initiation.md +42 -0
- package/.blueprint/features/feature_refine-feature-skill/story-story-propagation.md +42 -0
- package/.blueprint/features/feature_refine-feature-skill/story-telemetry-lineage.md +47 -0
- package/.blueprint/features/feature_refine-feature-skill/story-test-propagation.md +42 -0
- package/README.md +98 -7
- package/bin/cli.js +2 -1
- package/package.json +1 -1
- package/src/commands/refine.js +37 -0
- package/src/commands/telemetry-config.js +16 -0
- package/src/index.js +38 -1
- package/src/init.js +5 -0
- package/src/refine.js +172 -0
- package/src/telemetry.js +198 -0
package/src/telemetry.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const zlib = require('zlib');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// loadConfig — parse .env line by line; real process.env takes precedence
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
function loadConfig(dotenvPath) {
|
|
12
|
+
const fileVars = {};
|
|
13
|
+
try {
|
|
14
|
+
const lines = fs.readFileSync(dotenvPath, 'utf8').split('\n');
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
18
|
+
const eqIdx = trimmed.indexOf('=');
|
|
19
|
+
if (eqIdx === -1) continue;
|
|
20
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
21
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
22
|
+
fileVars[key] = val;
|
|
23
|
+
}
|
|
24
|
+
} catch (_) { /* file absent or unreadable — ignore */ }
|
|
25
|
+
|
|
26
|
+
const rawUrl = process.env.MURMUR8_TELEMETRY_URL ?? fileVars.MURMUR8_TELEMETRY_URL ?? '';
|
|
27
|
+
const rawKey = process.env.MURMUR8_TELEMETRY_KEY ?? fileVars.MURMUR8_TELEMETRY_KEY ?? '';
|
|
28
|
+
|
|
29
|
+
let url = null;
|
|
30
|
+
try { url = rawUrl ? (new URL(rawUrl), rawUrl) : null; } catch (_) { url = null; }
|
|
31
|
+
|
|
32
|
+
return { url, key: rawKey || null };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// generateRunId — crypto UUID v4
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
function generateRunId() {
|
|
39
|
+
return crypto.randomUUID();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// ensureFeatureId — reads/writes featureId into YAML frontmatter
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
function ensureFeatureId(specPath) {
|
|
46
|
+
const content = fs.readFileSync(specPath, 'utf8');
|
|
47
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
|
|
48
|
+
|
|
49
|
+
if (fmMatch) {
|
|
50
|
+
const fmBody = fmMatch[1];
|
|
51
|
+
const idMatch = fmBody.match(/^featureId:\s*(.+)$/m);
|
|
52
|
+
if (idMatch) return idMatch[1].trim();
|
|
53
|
+
|
|
54
|
+
// Frontmatter exists but no featureId — insert it
|
|
55
|
+
const newId = generateRunId();
|
|
56
|
+
const newFm = `---\nfeatureId: ${newId}\n${fmBody}\n---\n`;
|
|
57
|
+
fs.writeFileSync(specPath, newFm + content.slice(fmMatch[0].length));
|
|
58
|
+
return newId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// No frontmatter — prepend
|
|
62
|
+
const newId = generateRunId();
|
|
63
|
+
fs.writeFileSync(specPath, `---\nfeatureId: ${newId}\n---\n${content}`);
|
|
64
|
+
return newId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// buildPayload — assembles telemetry payload; omits feedback when empty
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
function buildPayload(runData) {
|
|
71
|
+
const { runId, featureId, slug, status, startedAt, completedAt,
|
|
72
|
+
totalDurationMs, stages, artifacts, feedback } = runData;
|
|
73
|
+
|
|
74
|
+
const run = { featureId, slug, status, startedAt, completedAt, totalDurationMs };
|
|
75
|
+
if (stages) run.stages = stages;
|
|
76
|
+
if (feedback && typeof feedback === 'object' && Object.keys(feedback).length > 0) {
|
|
77
|
+
run.feedback = feedback;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const payload = { runId, run };
|
|
81
|
+
if (artifacts) payload.artifacts = artifacts;
|
|
82
|
+
return payload;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// compressArtifact — gzip + base64 (synchronous)
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
function compressArtifact(content) {
|
|
89
|
+
return zlib.gzipSync(Buffer.from(content, 'utf8')).toString('base64');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// enqueueFailure — appends to queue; caps at 50; creates file if absent
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
function enqueueFailure(payload, queuePath) {
|
|
96
|
+
let queue = [];
|
|
97
|
+
try {
|
|
98
|
+
queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
99
|
+
if (!Array.isArray(queue)) queue = [];
|
|
100
|
+
} catch (_) { queue = []; }
|
|
101
|
+
|
|
102
|
+
queue.push(payload);
|
|
103
|
+
if (queue.length > 50) queue = queue.slice(queue.length - 50);
|
|
104
|
+
fs.writeFileSync(queuePath, JSON.stringify(queue, null, 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// retryQueue — calls sendFn per entry; removes successes; writes remaining
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
function retryQueue(queuePath, sendFn) {
|
|
111
|
+
let queue;
|
|
112
|
+
try {
|
|
113
|
+
queue = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
114
|
+
if (!Array.isArray(queue)) return;
|
|
115
|
+
} catch (_) { return; }
|
|
116
|
+
|
|
117
|
+
const remaining = [];
|
|
118
|
+
for (const entry of queue) {
|
|
119
|
+
const ok = sendFn(entry);
|
|
120
|
+
if (!ok) remaining.push(entry);
|
|
121
|
+
}
|
|
122
|
+
fs.writeFileSync(queuePath, JSON.stringify(remaining, null, 2));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// ensureDotenv — creates/appends .env with commented telemetry template
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
const DOTENV_MARKER = 'MURMUR8_TELEMETRY_URL';
|
|
129
|
+
const DOTENV_TEMPLATE = `
|
|
130
|
+
# murmur8 Telemetry — remove comments and set values to enable
|
|
131
|
+
# MURMUR8_TELEMETRY_URL=https://your-endpoint.example.com/events
|
|
132
|
+
# MURMUR8_TELEMETRY_KEY=your-api-key
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
function ensureDotenv(targetDir) {
|
|
136
|
+
const envPath = path.join(targetDir, '.env');
|
|
137
|
+
try {
|
|
138
|
+
const existing = fs.readFileSync(envPath, 'utf8');
|
|
139
|
+
if (existing.includes(DOTENV_MARKER)) return;
|
|
140
|
+
fs.writeFileSync(envPath, existing + DOTENV_TEMPLATE);
|
|
141
|
+
} catch (_) {
|
|
142
|
+
fs.writeFileSync(envPath, DOTENV_TEMPLATE.trimStart());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// ensureGitignore — adds .env to .gitignore; creates .gitignore if absent
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
function ensureGitignore(targetDir) {
|
|
150
|
+
const giPath = path.join(targetDir, '.gitignore');
|
|
151
|
+
try {
|
|
152
|
+
const existing = fs.readFileSync(giPath, 'utf8');
|
|
153
|
+
const lines = existing.split('\n').map(l => l.trim());
|
|
154
|
+
if (lines.includes('.env')) return;
|
|
155
|
+
fs.writeFileSync(giPath, existing + (existing.endsWith('\n') ? '' : '\n') + '.env\n');
|
|
156
|
+
} catch (_) {
|
|
157
|
+
fs.writeFileSync(giPath, '.env\n');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// formatTelemetryConfig — returns formatted config string
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
function formatTelemetryConfig(config, queuePath) {
|
|
165
|
+
const { url, key } = config;
|
|
166
|
+
const status = url ? 'active' : 'inactive';
|
|
167
|
+
|
|
168
|
+
let maskedKey = 'not set';
|
|
169
|
+
if (key) {
|
|
170
|
+
maskedKey = key.length > 4 ? `****${key.slice(-4)}` : '****';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let queueDepth = 0;
|
|
174
|
+
try {
|
|
175
|
+
const q = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
|
|
176
|
+
if (Array.isArray(q)) queueDepth = q.length;
|
|
177
|
+
} catch (_) { /* file absent or corrupt */ }
|
|
178
|
+
|
|
179
|
+
return [
|
|
180
|
+
`Telemetry status : ${status}`,
|
|
181
|
+
`URL : ${url || 'not set'}`,
|
|
182
|
+
`API key : ${maskedKey}`,
|
|
183
|
+
`Failed queue : ${queueDepth} entries`,
|
|
184
|
+
].join('\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
loadConfig,
|
|
189
|
+
generateRunId,
|
|
190
|
+
ensureFeatureId,
|
|
191
|
+
buildPayload,
|
|
192
|
+
compressArtifact,
|
|
193
|
+
enqueueFailure,
|
|
194
|
+
retryQueue,
|
|
195
|
+
ensureDotenv,
|
|
196
|
+
ensureGitignore,
|
|
197
|
+
formatTelemetryConfig,
|
|
198
|
+
};
|