murmur8 4.5.0 → 4.6.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.
Files changed (28) hide show
  1. package/.blueprint/features/feature_feedback-test/FEATURE_SPEC.md +229 -0
  2. package/.blueprint/features/feature_feedback-test/IMPLEMENTATION_PLAN.md +25 -0
  3. package/.blueprint/features/feature_feedback-test/handoff-alex.md +20 -0
  4. package/.blueprint/features/feature_feedback-test/handoff-cass.md +21 -0
  5. package/.blueprint/features/feature_feedback-test/handoff-nigel.md +20 -0
  6. package/.blueprint/features/feature_feedback-test/story-config-management.md +103 -0
  7. package/.blueprint/features/feature_feedback-test/story-parse-pipeline.md +65 -0
  8. package/.blueprint/features/feature_feedback-test/story-validation-normalisation.md +99 -0
  9. package/.blueprint/features/feature_pipeline-telemetry/FEATURE_SPEC.md +297 -0
  10. package/.blueprint/features/feature_pipeline-telemetry/IMPLEMENTATION_PLAN.md +34 -0
  11. package/.blueprint/features/feature_pipeline-telemetry/handoff-alex.md +21 -0
  12. package/.blueprint/features/feature_pipeline-telemetry/handoff-cass.md +25 -0
  13. package/.blueprint/features/feature_pipeline-telemetry/handoff-nigel.md +20 -0
  14. package/.blueprint/features/feature_pipeline-telemetry/story-failed-queue-retry.md +53 -0
  15. package/.blueprint/features/feature_pipeline-telemetry/story-identifiers.md +47 -0
  16. package/.blueprint/features/feature_pipeline-telemetry/story-init-integration.md +48 -0
  17. package/.blueprint/features/feature_pipeline-telemetry/story-payload-send.md +54 -0
  18. package/.blueprint/features/feature_pipeline-telemetry/story-telemetry-activation.md +54 -0
  19. package/.blueprint/features/feature_pipeline-telemetry/story-telemetry-config-command.md +52 -0
  20. package/README.md +54 -0
  21. package/SKILL.md +35 -24
  22. package/package.json +1 -1
  23. package/src/commands/history.js +41 -2
  24. package/src/commands/telemetry-config.js +16 -0
  25. package/src/history.js +31 -0
  26. package/src/index.js +16 -1
  27. package/src/init.js +5 -0
  28. package/src/telemetry.js +198 -0
@@ -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
+ };