@equilateral_ai/mindmeld 3.5.2 → 3.5.3
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/hooks/session-end.js +25 -0
- package/hooks/session-start.js +54 -1
- package/hooks/session-watcher.js +585 -0
- package/package.json +3 -3
- package/scripts/standards.js +285 -0
package/hooks/session-end.js
CHANGED
|
@@ -291,6 +291,31 @@ async function recordSessionEnd() {
|
|
|
291
291
|
return { skipped: true, reason: 'No session_id' };
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// Stop session watcher — write stop file (primary) + SIGTERM (secondary)
|
|
295
|
+
try {
|
|
296
|
+
// Write stop file — watcher checks this on its liveness interval
|
|
297
|
+
await fs.writeFile(path.join(cwd, '.mindmeld', 'watcher.stop'), '').catch(() => {});
|
|
298
|
+
|
|
299
|
+
const pidPath = path.join(cwd, '.mindmeld', 'watcher.pid');
|
|
300
|
+
const pidContent = await fs.readFile(pidPath, 'utf-8');
|
|
301
|
+
const watcherPid = parseInt(pidContent.trim(), 10);
|
|
302
|
+
if (watcherPid && !isNaN(watcherPid)) {
|
|
303
|
+
try {
|
|
304
|
+
process.kill(watcherPid, 'SIGTERM');
|
|
305
|
+
console.error(`[MindMeld] Stopped session watcher (PID: ${watcherPid})`);
|
|
306
|
+
} catch (killErr) {
|
|
307
|
+
if (killErr.code !== 'ESRCH') {
|
|
308
|
+
console.error(`[MindMeld] Watcher kill failed: ${killErr.message}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
await fs.unlink(pidPath).catch(() => {});
|
|
313
|
+
} catch (err) {
|
|
314
|
+
if (err.code !== 'ENOENT') {
|
|
315
|
+
console.error(`[MindMeld] Watcher cleanup error (non-fatal): ${err.message}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
294
319
|
// Load auth and config in parallel
|
|
295
320
|
const [authToken, apiConfig, projectId, duration, gitBranch] = await Promise.all([
|
|
296
321
|
loadAuthToken(),
|
package/hooks/session-start.js
CHANGED
|
@@ -771,13 +771,31 @@ async function injectContext() {
|
|
|
771
771
|
// 3b. Persist session context for pre-compact hook
|
|
772
772
|
// Pre-compact creates a new MindmeldClient and needs project context
|
|
773
773
|
try {
|
|
774
|
+
// Derive transcript path for watcher breadcrumb
|
|
775
|
+
const encodedCwd = process.cwd().replace(/\//g, '-');
|
|
776
|
+
const claudeProjectDir = path.join(require('os').homedir(), '.claude', 'projects', encodedCwd);
|
|
777
|
+
let transcriptPath = null;
|
|
778
|
+
try {
|
|
779
|
+
const dirFiles = await fs.readdir(claudeProjectDir);
|
|
780
|
+
const jsonlFiles = dirFiles.filter(f => f.endsWith('.jsonl'));
|
|
781
|
+
let newestMtime = 0;
|
|
782
|
+
for (const f of jsonlFiles) {
|
|
783
|
+
const fp = path.join(claudeProjectDir, f);
|
|
784
|
+
const s = await fs.stat(fp);
|
|
785
|
+
if (s.mtimeMs > newestMtime) { newestMtime = s.mtimeMs; transcriptPath = fp; }
|
|
786
|
+
}
|
|
787
|
+
} catch (err) {
|
|
788
|
+
// Claude project dir may not exist yet
|
|
789
|
+
}
|
|
790
|
+
|
|
774
791
|
const sessionContext = {
|
|
775
792
|
sessionId: sessionId,
|
|
776
793
|
projectId: context.projectId,
|
|
777
794
|
projectName: context.projectName,
|
|
778
795
|
companyId: context.companyId,
|
|
779
796
|
userEmail: context.userEmail,
|
|
780
|
-
startedAt: new Date().toISOString()
|
|
797
|
+
startedAt: new Date().toISOString(),
|
|
798
|
+
transcriptPath: transcriptPath
|
|
781
799
|
};
|
|
782
800
|
const mindmeldDir = path.join(process.cwd(), '.mindmeld');
|
|
783
801
|
await fs.mkdir(mindmeldDir, { recursive: true });
|
|
@@ -789,6 +807,41 @@ async function injectContext() {
|
|
|
789
807
|
console.error('[MindMeld] Could not persist session context (non-fatal):', err.message);
|
|
790
808
|
}
|
|
791
809
|
|
|
810
|
+
// 3c. Spawn background session watcher for continuous pattern harvesting
|
|
811
|
+
// (covers 1M context sessions where pre-compact may never fire)
|
|
812
|
+
// Watcher survives /clear — if already alive, it picks up the new transcript via breadcrumb
|
|
813
|
+
try {
|
|
814
|
+
const pidPath = path.join(process.cwd(), '.mindmeld', 'watcher.pid');
|
|
815
|
+
let watcherAlive = false;
|
|
816
|
+
try {
|
|
817
|
+
const existingPid = parseInt(await fs.readFile(pidPath, 'utf-8'), 10);
|
|
818
|
+
if (existingPid && !isNaN(existingPid)) {
|
|
819
|
+
process.kill(existingPid, 0); // Check if alive
|
|
820
|
+
watcherAlive = true;
|
|
821
|
+
console.error(`[MindMeld] Existing watcher alive (PID: ${existingPid}), will pick up new transcript via breadcrumb`);
|
|
822
|
+
}
|
|
823
|
+
} catch (err) {
|
|
824
|
+
// No PID file or process gone — spawn a new one
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (!watcherAlive) {
|
|
828
|
+
const { spawn } = require('child_process');
|
|
829
|
+
const watcherScript = path.resolve(__dirname, 'session-watcher.js');
|
|
830
|
+
await fs.access(watcherScript);
|
|
831
|
+
const parentPid = process.ppid || process.pid;
|
|
832
|
+
const watcher = spawn(process.execPath, [watcherScript, process.cwd(), String(parentPid)], {
|
|
833
|
+
detached: true,
|
|
834
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
835
|
+
cwd: process.cwd(),
|
|
836
|
+
env: { ...process.env }
|
|
837
|
+
});
|
|
838
|
+
watcher.unref();
|
|
839
|
+
console.error(`[MindMeld] Session watcher spawned (PID: ${watcher.pid})`);
|
|
840
|
+
}
|
|
841
|
+
} catch (err) {
|
|
842
|
+
console.error(`[MindMeld] Watcher spawn failed (non-fatal): ${err.message}`);
|
|
843
|
+
}
|
|
844
|
+
|
|
792
845
|
// 4. Detect project characteristics locally (file system only, no DB)
|
|
793
846
|
const characteristics = await mindmeld.relevanceDetector.detectProjectCharacteristics();
|
|
794
847
|
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MindMeld - Background Pattern Watcher
|
|
4
|
+
*
|
|
5
|
+
* Long-lived background process that monitors the JSONL session transcript
|
|
6
|
+
* and harvests patterns on resolution detection rather than a dumb timer.
|
|
7
|
+
*
|
|
8
|
+
* With 1M context windows, pre-compact may never fire. This watcher ensures
|
|
9
|
+
* patterns are harvested continuously throughout long sessions.
|
|
10
|
+
*
|
|
11
|
+
* Lifecycle:
|
|
12
|
+
* 1. SessionStart spawns watcher (or watcher detects breadcrumb update)
|
|
13
|
+
* 2. Polls transcript every 10s, buffers new content
|
|
14
|
+
* 3. Detects resolution signals (test runs, completions, task done)
|
|
15
|
+
* 4. Extracts patterns after 60s cooldown post-resolution
|
|
16
|
+
* 5. Survives /clear via breadcrumb transcript switch
|
|
17
|
+
* 6. Stops on: parent death, stop file, or 2h idle
|
|
18
|
+
*
|
|
19
|
+
* Args:
|
|
20
|
+
* argv[2] - project cwd
|
|
21
|
+
* argv[3] - parent PID (terminal process to monitor)
|
|
22
|
+
*
|
|
23
|
+
* @equilateral_ai/mindmeld v3.5.3
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const fs = require('fs').promises;
|
|
28
|
+
const fsSync = require('fs');
|
|
29
|
+
const os = require('os');
|
|
30
|
+
|
|
31
|
+
process.title = 'mindmeld-watcher';
|
|
32
|
+
|
|
33
|
+
// Import harvest pipeline from pre-compact
|
|
34
|
+
let harvestPatterns = null;
|
|
35
|
+
try {
|
|
36
|
+
const preCompact = require('./pre-compact');
|
|
37
|
+
harvestPatterns = preCompact.harvestPatterns;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`[MindMeld:Watcher] Cannot load pre-compact module: ${error.message}`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const PROJECT_CWD = process.argv[2];
|
|
44
|
+
const PARENT_PID = parseInt(process.argv[3], 10);
|
|
45
|
+
|
|
46
|
+
if (!PROJECT_CWD) {
|
|
47
|
+
console.error('[MindMeld:Watcher] No project cwd provided');
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Timing constants ──────────────────────────────────────────────────────────
|
|
52
|
+
const POLL_MS = 10 * 1000; // 10s transcript poll
|
|
53
|
+
const COOLDOWN_MS = 60 * 1000; // 60s after resolution before extracting
|
|
54
|
+
const IDLE_TIMEOUT_MS = 2 * 60 * 60 * 1000; // 2h no transcript activity → exit
|
|
55
|
+
const PARENT_CHECK_MS = 10 * 1000; // 10s parent liveness check
|
|
56
|
+
const FALLBACK_HARVEST_MS = 5 * 60 * 1000; // 5 min safety net if no resolution detected
|
|
57
|
+
const MIN_BUFFER_CHARS = 500; // skip trivial buffers
|
|
58
|
+
const MAX_DELTA_BYTES = 200 * 1024; // 200KB cap per read
|
|
59
|
+
const STARTUP_RETRY_MS = 10 * 1000;
|
|
60
|
+
const STARTUP_MAX_RETRIES = 12;
|
|
61
|
+
|
|
62
|
+
// ── Resolution signal patterns ────────────────────────────────────────────────
|
|
63
|
+
// Bash tool calls that indicate work completion
|
|
64
|
+
const RESOLUTION_COMMANDS = [
|
|
65
|
+
/npm\s+test/,
|
|
66
|
+
/npm\s+run\s+test/,
|
|
67
|
+
/jest/,
|
|
68
|
+
/pytest/,
|
|
69
|
+
/ruff\s+check/,
|
|
70
|
+
/eslint/,
|
|
71
|
+
/tsc\s+--noEmit/,
|
|
72
|
+
/go\s+test/,
|
|
73
|
+
/cargo\s+test/,
|
|
74
|
+
/make\s+test/,
|
|
75
|
+
/sam\s+build/,
|
|
76
|
+
/sam\s+deploy/,
|
|
77
|
+
/npm\s+run\s+build/,
|
|
78
|
+
/npm\s+run\s+deploy/,
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Completion phrases in assistant messages
|
|
82
|
+
const COMPLETION_PHRASES = [
|
|
83
|
+
/all\s+(?:tests?\s+)?pass/i,
|
|
84
|
+
/successfully\s+(?:deployed|built|completed|created|updated)/i,
|
|
85
|
+
/changes?\s+(?:look|are)\s+good/i,
|
|
86
|
+
/(?:done|finished|completed)[.!]\s*$/im,
|
|
87
|
+
/ready\s+(?:to\s+)?(?:commit|deploy|merge|push|review)/i,
|
|
88
|
+
/implementation\s+is\s+(?:complete|done|ready)/i,
|
|
89
|
+
/that\s+(?:should\s+)?fix/i,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// ── State ─────────────────────────────────────────────────────────────────────
|
|
93
|
+
let byteOffset = 0;
|
|
94
|
+
let jsonlPath = null;
|
|
95
|
+
let sessionId = null;
|
|
96
|
+
let buffer = ''; // accumulated unparsed transcript text
|
|
97
|
+
let pollHandle = null;
|
|
98
|
+
let parentCheckHandle = null;
|
|
99
|
+
let cooldownTimer = null;
|
|
100
|
+
let lastHarvestTime = Date.now();
|
|
101
|
+
let lastActivityTime = Date.now();
|
|
102
|
+
let resolutionDetected = false;
|
|
103
|
+
|
|
104
|
+
// ── Breadcrumb ────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Path to the breadcrumb file written by session-start.
|
|
108
|
+
* Contains session_id and transcript_path, updated on /clear.
|
|
109
|
+
*/
|
|
110
|
+
function breadcrumbPath() {
|
|
111
|
+
return path.join(PROJECT_CWD, '.mindmeld', 'current-session.json');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Read the breadcrumb to get current session info and transcript path.
|
|
116
|
+
*/
|
|
117
|
+
async function readBreadcrumb() {
|
|
118
|
+
try {
|
|
119
|
+
const content = await fs.readFile(breadcrumbPath(), 'utf-8');
|
|
120
|
+
return JSON.parse(content);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Transcript discovery ──────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
function encodeProjectDir(cwd) {
|
|
129
|
+
return cwd.replace(/\//g, '-');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Find the active JSONL session file.
|
|
134
|
+
* Priority: breadcrumb transcript_path → most recently modified .jsonl
|
|
135
|
+
*/
|
|
136
|
+
async function findSessionFile() {
|
|
137
|
+
// Check breadcrumb first
|
|
138
|
+
const crumb = await readBreadcrumb();
|
|
139
|
+
if (crumb?.transcriptPath) {
|
|
140
|
+
try {
|
|
141
|
+
await fs.access(crumb.transcriptPath);
|
|
142
|
+
sessionId = crumb.sessionId || sessionId;
|
|
143
|
+
return crumb.transcriptPath;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
// Breadcrumb points to missing file, fall through
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (crumb?.sessionId) {
|
|
150
|
+
sessionId = crumb.sessionId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Fall back to most recently modified JSONL
|
|
154
|
+
const projectDir = path.join(os.homedir(), '.claude', 'projects', encodeProjectDir(PROJECT_CWD));
|
|
155
|
+
let files;
|
|
156
|
+
try {
|
|
157
|
+
files = await fs.readdir(projectDir);
|
|
158
|
+
} catch (err) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl'));
|
|
163
|
+
if (jsonlFiles.length === 0) return null;
|
|
164
|
+
|
|
165
|
+
let newest = null;
|
|
166
|
+
let newestMtime = 0;
|
|
167
|
+
for (const file of jsonlFiles) {
|
|
168
|
+
const fullPath = path.join(projectDir, file);
|
|
169
|
+
try {
|
|
170
|
+
const stat = await fs.stat(fullPath);
|
|
171
|
+
if (stat.mtimeMs > newestMtime) {
|
|
172
|
+
newestMtime = stat.mtimeMs;
|
|
173
|
+
newest = fullPath;
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {
|
|
176
|
+
// Skip
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return newest;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── JSONL parsing ─────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Parse JSONL entries from raw text. Returns array of parsed entry objects
|
|
187
|
+
* for resolution detection, plus accumulated transcript text.
|
|
188
|
+
*/
|
|
189
|
+
function parseJsonlEntries(rawText) {
|
|
190
|
+
const lines = rawText.split('\n').filter(l => l.trim());
|
|
191
|
+
const entries = [];
|
|
192
|
+
const textParts = [];
|
|
193
|
+
|
|
194
|
+
for (const line of lines) {
|
|
195
|
+
try {
|
|
196
|
+
const entry = JSON.parse(line);
|
|
197
|
+
entries.push(entry);
|
|
198
|
+
|
|
199
|
+
const content = entry.message?.content || entry.content;
|
|
200
|
+
if (!content) continue;
|
|
201
|
+
|
|
202
|
+
if (typeof content === 'string') {
|
|
203
|
+
textParts.push(content);
|
|
204
|
+
} else if (Array.isArray(content)) {
|
|
205
|
+
for (const block of content) {
|
|
206
|
+
if (block.type === 'text' && block.text) {
|
|
207
|
+
textParts.push(block.text);
|
|
208
|
+
}
|
|
209
|
+
// Also capture tool_use for resolution detection
|
|
210
|
+
if (block.type === 'tool_use' && block.input) {
|
|
211
|
+
const input = typeof block.input === 'string' ? block.input : JSON.stringify(block.input);
|
|
212
|
+
textParts.push(input);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// Skip partial lines
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { entries, transcript: textParts.join('\n\n') };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Delta reading ─────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
async function readDelta() {
|
|
227
|
+
if (!jsonlPath) return null;
|
|
228
|
+
|
|
229
|
+
let stat;
|
|
230
|
+
try {
|
|
231
|
+
stat = await fs.stat(jsonlPath);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (stat.size <= byteOffset) return null;
|
|
237
|
+
lastActivityTime = Date.now();
|
|
238
|
+
|
|
239
|
+
const readSize = Math.min(stat.size - byteOffset, MAX_DELTA_BYTES);
|
|
240
|
+
const buf = Buffer.alloc(readSize);
|
|
241
|
+
|
|
242
|
+
const fh = await fs.open(jsonlPath, 'r');
|
|
243
|
+
try {
|
|
244
|
+
await fh.read(buf, 0, readSize, byteOffset);
|
|
245
|
+
} finally {
|
|
246
|
+
await fh.close();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const raw = buf.toString('utf-8');
|
|
250
|
+
|
|
251
|
+
// Skip partial first line if mid-file
|
|
252
|
+
let usable = raw;
|
|
253
|
+
if (byteOffset > 0) {
|
|
254
|
+
const firstNewline = raw.indexOf('\n');
|
|
255
|
+
if (firstNewline > 0) {
|
|
256
|
+
usable = raw.substring(firstNewline + 1);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Advance to end of last complete line
|
|
261
|
+
const lastNewline = raw.lastIndexOf('\n');
|
|
262
|
+
if (lastNewline >= 0) {
|
|
263
|
+
byteOffset += lastNewline + 1;
|
|
264
|
+
} else {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return usable;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Resolution detection ──────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Scan entries for signals that the agent just finished a unit of work.
|
|
275
|
+
*/
|
|
276
|
+
function detectResolution(entries, transcriptText) {
|
|
277
|
+
// Check for resolution command patterns in tool calls
|
|
278
|
+
for (const entry of entries) {
|
|
279
|
+
const content = entry.message?.content || entry.content;
|
|
280
|
+
if (!Array.isArray(content)) continue;
|
|
281
|
+
|
|
282
|
+
for (const block of content) {
|
|
283
|
+
if (block.type === 'tool_use' && block.name === 'Bash') {
|
|
284
|
+
const cmd = block.input?.command || '';
|
|
285
|
+
for (const pattern of RESOLUTION_COMMANDS) {
|
|
286
|
+
if (pattern.test(cmd)) {
|
|
287
|
+
return { type: 'command', signal: cmd.substring(0, 80) };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// TaskUpdate with status "completed"
|
|
292
|
+
if (block.type === 'tool_use' && block.name === 'TaskUpdate') {
|
|
293
|
+
const status = block.input?.status;
|
|
294
|
+
if (status === 'completed') {
|
|
295
|
+
return { type: 'task_complete', signal: block.input?.id || 'unknown' };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check for completion phrases in recent text
|
|
302
|
+
for (const phrase of COMPLETION_PHRASES) {
|
|
303
|
+
if (phrase.test(transcriptText)) {
|
|
304
|
+
return { type: 'phrase', signal: transcriptText.match(phrase)?.[0] || '' };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Harvest ───────────────────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
async function extract() {
|
|
314
|
+
if (!buffer || buffer.length < MIN_BUFFER_CHARS) {
|
|
315
|
+
buffer = '';
|
|
316
|
+
resolutionDetected = false;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const harvestText = buffer.length > MAX_DELTA_BYTES
|
|
321
|
+
? buffer.substring(buffer.length - MAX_DELTA_BYTES)
|
|
322
|
+
: buffer;
|
|
323
|
+
|
|
324
|
+
console.error(`[MindMeld:Watcher] Extracting patterns (${harvestText.length} chars)`);
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const result = await harvestPatterns({
|
|
328
|
+
sessionId: sessionId || 'unknown',
|
|
329
|
+
transcript: harvestText,
|
|
330
|
+
source: 'watcher'
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (result && !result.skipped) {
|
|
334
|
+
console.error(`[MindMeld:Watcher] Harvest: ${result.patternsDetected || 0} patterns, ${result.violations || 0} violations, ${result.reinforced || 0} reinforced`);
|
|
335
|
+
}
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.error(`[MindMeld:Watcher] Harvest error (non-fatal): ${err.message}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
buffer = '';
|
|
341
|
+
resolutionDetected = false;
|
|
342
|
+
lastHarvestTime = Date.now();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Transcript switch (clear detection) ───────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Check if the breadcrumb has been updated with a new transcript path.
|
|
349
|
+
* This happens when session-start fires again after /clear.
|
|
350
|
+
*/
|
|
351
|
+
async function checkTranscriptSwitch() {
|
|
352
|
+
const crumb = await readBreadcrumb();
|
|
353
|
+
if (!crumb?.transcriptPath) return false;
|
|
354
|
+
|
|
355
|
+
// No current path yet — initial setup, not a switch
|
|
356
|
+
if (!jsonlPath) return false;
|
|
357
|
+
|
|
358
|
+
// Normalize for comparison
|
|
359
|
+
const currentBasename = path.basename(jsonlPath);
|
|
360
|
+
const crumbBasename = path.basename(crumb.transcriptPath);
|
|
361
|
+
|
|
362
|
+
if (currentBasename !== crumbBasename) {
|
|
363
|
+
console.error(`[MindMeld:Watcher] Transcript switch detected: ${currentBasename} → ${crumbBasename}`);
|
|
364
|
+
|
|
365
|
+
// Flush remaining buffer from old transcript
|
|
366
|
+
if (buffer.length >= MIN_BUFFER_CHARS) {
|
|
367
|
+
console.error('[MindMeld:Watcher] Flushing buffer from previous transcript');
|
|
368
|
+
await extract();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Reset state for new transcript
|
|
372
|
+
try {
|
|
373
|
+
await fs.access(crumb.transcriptPath);
|
|
374
|
+
jsonlPath = crumb.transcriptPath;
|
|
375
|
+
sessionId = crumb.sessionId || sessionId;
|
|
376
|
+
byteOffset = 0; // Read from start of new transcript
|
|
377
|
+
buffer = '';
|
|
378
|
+
resolutionDetected = false;
|
|
379
|
+
|
|
380
|
+
// Set offset to current size if we don't want pre-existing content
|
|
381
|
+
const stat = await fs.stat(jsonlPath);
|
|
382
|
+
byteOffset = stat.size;
|
|
383
|
+
|
|
384
|
+
console.error(`[MindMeld:Watcher] Now watching: ${path.basename(jsonlPath)}`);
|
|
385
|
+
return true;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
console.error(`[MindMeld:Watcher] New transcript not accessible: ${err.message}`);
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ── Parent process liveness ───────────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
function isParentAlive() {
|
|
398
|
+
if (!PARENT_PID || isNaN(PARENT_PID)) return true; // Can't check, assume alive
|
|
399
|
+
try {
|
|
400
|
+
process.kill(PARENT_PID, 0); // Signal 0 = existence check
|
|
401
|
+
return true;
|
|
402
|
+
} catch (err) {
|
|
403
|
+
return false; // ESRCH = process gone
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── Stop file detection ───────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
function stopFilePath() {
|
|
410
|
+
return path.join(PROJECT_CWD, '.mindmeld', 'watcher.stop');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function checkStopFile() {
|
|
414
|
+
try {
|
|
415
|
+
await fs.access(stopFilePath());
|
|
416
|
+
await fs.unlink(stopFilePath()).catch(() => {});
|
|
417
|
+
return true;
|
|
418
|
+
} catch (err) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── PID file ──────────────────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
async function writePidFile() {
|
|
426
|
+
try {
|
|
427
|
+
const pidPath = path.join(PROJECT_CWD, '.mindmeld', 'watcher.pid');
|
|
428
|
+
await fs.writeFile(pidPath, String(process.pid));
|
|
429
|
+
} catch (err) {
|
|
430
|
+
console.error(`[MindMeld:Watcher] Could not write PID file: ${err.message}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Poll tick ─────────────────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
async function poll() {
|
|
437
|
+
try {
|
|
438
|
+
// Check for transcript switch (breadcrumb updated by session-start after /clear)
|
|
439
|
+
await checkTranscriptSwitch();
|
|
440
|
+
|
|
441
|
+
// Read new lines from transcript
|
|
442
|
+
const delta = await readDelta();
|
|
443
|
+
if (delta) {
|
|
444
|
+
const { entries, transcript } = parseJsonlEntries(delta);
|
|
445
|
+
if (transcript) {
|
|
446
|
+
buffer += transcript + '\n\n';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Check for resolution signals in new entries
|
|
450
|
+
if (!resolutionDetected) {
|
|
451
|
+
const resolution = detectResolution(entries, transcript);
|
|
452
|
+
if (resolution) {
|
|
453
|
+
resolutionDetected = true;
|
|
454
|
+
console.error(`[MindMeld:Watcher] Resolution detected (${resolution.type}): ${resolution.signal}`);
|
|
455
|
+
|
|
456
|
+
// Start cooldown — extract after 60s
|
|
457
|
+
if (cooldownTimer) clearTimeout(cooldownTimer);
|
|
458
|
+
cooldownTimer = setTimeout(() => {
|
|
459
|
+
cooldownTimer = null;
|
|
460
|
+
extract();
|
|
461
|
+
}, COOLDOWN_MS);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Fallback: if no resolution detected but buffer is accumulating, harvest on timer
|
|
467
|
+
if (!resolutionDetected && !cooldownTimer && buffer.length >= MIN_BUFFER_CHARS) {
|
|
468
|
+
const sinceLastHarvest = Date.now() - lastHarvestTime;
|
|
469
|
+
if (sinceLastHarvest >= FALLBACK_HARVEST_MS) {
|
|
470
|
+
console.error('[MindMeld:Watcher] Fallback harvest (no resolution signal)');
|
|
471
|
+
await extract();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
} catch (err) {
|
|
475
|
+
console.error(`[MindMeld:Watcher] Poll error (non-fatal): ${err.message}`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Parent liveness + idle + stop file checks.
|
|
481
|
+
*/
|
|
482
|
+
async function livenessCheck() {
|
|
483
|
+
// Parent process died (terminal closed)
|
|
484
|
+
if (!isParentAlive()) {
|
|
485
|
+
console.error('[MindMeld:Watcher] Parent process gone, extracting and exiting');
|
|
486
|
+
await extract();
|
|
487
|
+
cleanup();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Stop file written by session-end
|
|
492
|
+
if (await checkStopFile()) {
|
|
493
|
+
console.error('[MindMeld:Watcher] Stop file detected, extracting and exiting');
|
|
494
|
+
await extract();
|
|
495
|
+
cleanup();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Idle timeout
|
|
500
|
+
if (Date.now() - lastActivityTime > IDLE_TIMEOUT_MS) {
|
|
501
|
+
console.error('[MindMeld:Watcher] Idle for 2h, extracting and exiting');
|
|
502
|
+
await extract();
|
|
503
|
+
cleanup();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── Cleanup ───────────────────────────────────────────────────────────────────
|
|
509
|
+
|
|
510
|
+
function cleanup() {
|
|
511
|
+
if (pollHandle) {
|
|
512
|
+
clearInterval(pollHandle);
|
|
513
|
+
pollHandle = null;
|
|
514
|
+
}
|
|
515
|
+
if (parentCheckHandle) {
|
|
516
|
+
clearInterval(parentCheckHandle);
|
|
517
|
+
parentCheckHandle = null;
|
|
518
|
+
}
|
|
519
|
+
if (cooldownTimer) {
|
|
520
|
+
clearTimeout(cooldownTimer);
|
|
521
|
+
cooldownTimer = null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Remove PID file
|
|
525
|
+
try {
|
|
526
|
+
fsSync.unlinkSync(path.join(PROJECT_CWD, '.mindmeld', 'watcher.pid'));
|
|
527
|
+
} catch (err) {
|
|
528
|
+
// Already gone
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
process.exit(0);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
535
|
+
|
|
536
|
+
async function main() {
|
|
537
|
+
console.error(`[MindMeld:Watcher] Starting for ${path.basename(PROJECT_CWD)} (parent PID: ${PARENT_PID || 'unknown'})`);
|
|
538
|
+
|
|
539
|
+
await writePidFile();
|
|
540
|
+
|
|
541
|
+
// Find the JSONL file, retrying if it doesn't exist yet
|
|
542
|
+
for (let attempt = 0; attempt < STARTUP_MAX_RETRIES; attempt++) {
|
|
543
|
+
jsonlPath = await findSessionFile();
|
|
544
|
+
if (jsonlPath) break;
|
|
545
|
+
await new Promise(r => setTimeout(r, STARTUP_RETRY_MS));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (!jsonlPath) {
|
|
549
|
+
console.error('[MindMeld:Watcher] Could not find session JSONL file after retries, exiting');
|
|
550
|
+
cleanup();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Read session ID from breadcrumb if not set
|
|
555
|
+
if (!sessionId) {
|
|
556
|
+
const crumb = await readBreadcrumb();
|
|
557
|
+
sessionId = crumb?.sessionId || 'unknown';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
console.error(`[MindMeld:Watcher] Watching: ${path.basename(jsonlPath)} (session: ${sessionId})`);
|
|
561
|
+
|
|
562
|
+
// Start offset at current file size — only capture new activity
|
|
563
|
+
try {
|
|
564
|
+
const stat = await fs.stat(jsonlPath);
|
|
565
|
+
byteOffset = stat.size;
|
|
566
|
+
} catch (err) {
|
|
567
|
+
byteOffset = 0;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Start polling
|
|
571
|
+
pollHandle = setInterval(poll, POLL_MS);
|
|
572
|
+
parentCheckHandle = setInterval(livenessCheck, PARENT_CHECK_MS);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
process.on('SIGTERM', async () => {
|
|
576
|
+
console.error('[MindMeld:Watcher] SIGTERM received, extracting and exiting');
|
|
577
|
+
await extract();
|
|
578
|
+
cleanup();
|
|
579
|
+
});
|
|
580
|
+
process.on('SIGINT', cleanup);
|
|
581
|
+
|
|
582
|
+
main().catch(err => {
|
|
583
|
+
console.error(`[MindMeld:Watcher] Fatal: ${err.message}`);
|
|
584
|
+
cleanup();
|
|
585
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equilateral_ai/mindmeld",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.3",
|
|
4
4
|
"description": "Intelligent standards injection for AI coding sessions - context-aware, self-documenting, scales to large codebases",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"scripts/harvest.js",
|
|
16
16
|
"scripts/repo-analyzer.js",
|
|
17
17
|
"scripts/mcp-bridge.js",
|
|
18
|
+
"scripts/standards.js",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
20
21
|
"publishConfig": {
|
|
@@ -80,13 +81,12 @@
|
|
|
80
81
|
"homepage": "https://mindmeld.dev",
|
|
81
82
|
"dependencies": {
|
|
82
83
|
"@aws-sdk/client-bedrock-runtime": "^3.460.0",
|
|
83
|
-
"@aws-sdk/client-
|
|
84
|
+
"@aws-sdk/client-ses": "^3.985.0",
|
|
84
85
|
"js-yaml": "^4.1.1",
|
|
85
86
|
"pg": "^8.18.0"
|
|
86
87
|
},
|
|
87
88
|
"devDependencies": {
|
|
88
89
|
"@aws-sdk/client-s3": "^3.460.0",
|
|
89
|
-
"@aws-sdk/client-ses": "^3.985.0",
|
|
90
90
|
"jest": "^29.7.0"
|
|
91
91
|
},
|
|
92
92
|
"engines": {
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MindMeld CLI - Standards Management
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* mindmeld standards # Open standards picker in browser
|
|
7
|
+
* mindmeld standards --show # Show current preferences
|
|
8
|
+
* mindmeld standards --sync # Sync preferences from server
|
|
9
|
+
* mindmeld standards --reset # Reset to defaults
|
|
10
|
+
*
|
|
11
|
+
* @equilateral_ai/mindmeld v3.2.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs').promises;
|
|
16
|
+
const { AuthManager } = require('../src/core/AuthManager');
|
|
17
|
+
|
|
18
|
+
const authManager = new AuthManager();
|
|
19
|
+
const API_BASE = process.env.MINDMELD_API_URL || 'https://api.mindmeld.dev';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Load local MindMeld config
|
|
23
|
+
*/
|
|
24
|
+
async function loadConfig() {
|
|
25
|
+
try {
|
|
26
|
+
const configPath = path.join(process.cwd(), '.mindmeld', 'config.json');
|
|
27
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
28
|
+
return JSON.parse(content);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) {
|
|
31
|
+
console.error('Unexpected error loading config:', error.message);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Load authentication tokens
|
|
39
|
+
*/
|
|
40
|
+
async function loadAuth() {
|
|
41
|
+
try {
|
|
42
|
+
const authPath = path.join(process.env.HOME, '.mindmeld', 'auth.json');
|
|
43
|
+
const content = await fs.readFile(authPath, 'utf-8');
|
|
44
|
+
return JSON.parse(content);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) {
|
|
47
|
+
console.error('Unexpected error loading auth:', error.message);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Load local preferences
|
|
55
|
+
*/
|
|
56
|
+
async function loadPreferences() {
|
|
57
|
+
try {
|
|
58
|
+
const prefsPath = path.join(process.cwd(), '.mindmeld', 'preferences.json');
|
|
59
|
+
const content = await fs.readFile(prefsPath, 'utf-8');
|
|
60
|
+
return JSON.parse(content);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error.code !== 'ENOENT' && !(error instanceof SyntaxError)) {
|
|
63
|
+
console.error('Unexpected error loading preferences:', error.message);
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Save local preferences
|
|
71
|
+
*/
|
|
72
|
+
async function savePreferences(preferences) {
|
|
73
|
+
const prefsPath = path.join(process.cwd(), '.mindmeld', 'preferences.json');
|
|
74
|
+
await fs.mkdir(path.dirname(prefsPath), { recursive: true });
|
|
75
|
+
await fs.writeFile(prefsPath, JSON.stringify(preferences, null, 2));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fetch project standards from API
|
|
80
|
+
*/
|
|
81
|
+
async function fetchProjectStandards(projectId, idToken) {
|
|
82
|
+
const response = await fetch(`${API_BASE}/api/projects/standards?project_id=${encodeURIComponent(projectId)}`, {
|
|
83
|
+
headers: {
|
|
84
|
+
'Authorization': `Bearer ${idToken}`,
|
|
85
|
+
'Content-Type': 'application/json'
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(`API error: ${response.status}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return response.json();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Show current preferences
|
|
98
|
+
*/
|
|
99
|
+
async function showPreferences() {
|
|
100
|
+
const prefs = await loadPreferences();
|
|
101
|
+
|
|
102
|
+
if (!prefs) {
|
|
103
|
+
console.log('\nNo local preferences set. Using defaults (all standards enabled).\n');
|
|
104
|
+
console.log('Run `mindmeld standards` to configure.\n');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log('\n📋 Current Standards Preferences\n');
|
|
109
|
+
|
|
110
|
+
const enabledCats = prefs.enabled_categories || {};
|
|
111
|
+
const overrides = prefs.standard_overrides || {};
|
|
112
|
+
|
|
113
|
+
console.log('Categories:');
|
|
114
|
+
for (const [cat, enabled] of Object.entries(enabledCats)) {
|
|
115
|
+
const icon = enabled ? '✅' : '❌';
|
|
116
|
+
console.log(` ${icon} ${cat}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (Object.keys(overrides).length > 0) {
|
|
120
|
+
console.log('\nIndividual Overrides:');
|
|
121
|
+
for (const [standard, enabled] of Object.entries(overrides)) {
|
|
122
|
+
const icon = enabled ? '✅' : '❌';
|
|
123
|
+
console.log(` ${icon} ${standard}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (prefs.synced_at) {
|
|
128
|
+
console.log(`\nLast synced: ${new Date(prefs.synced_at).toLocaleString()}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Sync preferences from server
|
|
136
|
+
*/
|
|
137
|
+
async function syncPreferences() {
|
|
138
|
+
const config = await loadConfig();
|
|
139
|
+
const auth = await loadAuth();
|
|
140
|
+
|
|
141
|
+
if (!config) {
|
|
142
|
+
console.error('\n❌ Not a MindMeld project. Run `mindmeld init` first.\n');
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!auth || !auth.id_token) {
|
|
147
|
+
console.error('\n❌ Not authenticated. Run `mindmeld login` first.\n');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('\n🔄 Syncing preferences from server...\n');
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const result = await fetchProjectStandards(config.projectId, auth.id_token);
|
|
155
|
+
|
|
156
|
+
if (result.success && result.data) {
|
|
157
|
+
const prefs = {
|
|
158
|
+
enabled_categories: result.data.preferences?.enabled_categories || {},
|
|
159
|
+
standard_overrides: result.data.preferences?.standard_overrides || {},
|
|
160
|
+
critical_overrides: result.data.preferences?.critical_overrides || {},
|
|
161
|
+
catalog_version: result.data.catalog_version,
|
|
162
|
+
synced_at: new Date().toISOString()
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
await savePreferences(prefs);
|
|
166
|
+
|
|
167
|
+
console.log('✅ Preferences synced successfully!\n');
|
|
168
|
+
await showPreferences();
|
|
169
|
+
} else {
|
|
170
|
+
console.log('ℹ️ No custom preferences set. Using defaults.\n');
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error('❌ Failed to sync:', error.message);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Reset preferences to defaults
|
|
180
|
+
*/
|
|
181
|
+
async function resetPreferences() {
|
|
182
|
+
try {
|
|
183
|
+
const prefsPath = path.join(process.cwd(), '.mindmeld', 'preferences.json');
|
|
184
|
+
await fs.unlink(prefsPath);
|
|
185
|
+
console.log('\n✅ Preferences reset to defaults (all standards enabled).\n');
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error.code === 'ENOENT') {
|
|
188
|
+
console.log('\nℹ️ No preferences file to reset.\n');
|
|
189
|
+
} else {
|
|
190
|
+
console.error('Unexpected error resetting preferences:', error.message);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Open standards picker in browser
|
|
198
|
+
*/
|
|
199
|
+
async function openStandardsPicker() {
|
|
200
|
+
const config = await loadConfig();
|
|
201
|
+
const auth = await loadAuth();
|
|
202
|
+
|
|
203
|
+
if (!config) {
|
|
204
|
+
console.error('\n❌ Not a MindMeld project. Run `mindmeld init` first.\n');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!auth) {
|
|
209
|
+
console.error('\n❌ Not authenticated. Run `mindmeld login` first.\n');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const projectId = config.projectId;
|
|
214
|
+
const baseUrl = authManager.getAppBaseUrl();
|
|
215
|
+
const url = `${baseUrl}/projects/${projectId}/standards`;
|
|
216
|
+
|
|
217
|
+
console.log('\n🌐 Opening standards picker in browser...\n');
|
|
218
|
+
console.log(` ${url}\n`);
|
|
219
|
+
|
|
220
|
+
const opened = await authManager.openBrowser(url);
|
|
221
|
+
if (!opened) {
|
|
222
|
+
console.log('Could not open browser automatically.');
|
|
223
|
+
console.log(`Please visit: ${url}\n`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
console.log('After saving preferences in the browser, run:');
|
|
227
|
+
console.log(' mindmeld standards --sync\n');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Show help
|
|
232
|
+
*/
|
|
233
|
+
function showHelp() {
|
|
234
|
+
console.log(`
|
|
235
|
+
MindMeld Standards - Configure which standards apply to this project
|
|
236
|
+
|
|
237
|
+
Usage:
|
|
238
|
+
mindmeld standards Open standards picker in browser
|
|
239
|
+
mindmeld standards --show Show current preferences
|
|
240
|
+
mindmeld standards --sync Sync preferences from server
|
|
241
|
+
mindmeld standards --reset Reset to defaults (all enabled)
|
|
242
|
+
mindmeld standards --help Show this help
|
|
243
|
+
|
|
244
|
+
Examples:
|
|
245
|
+
mindmeld standards # Configure in browser
|
|
246
|
+
mindmeld standards --show # See what's enabled/disabled
|
|
247
|
+
`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Main
|
|
252
|
+
*/
|
|
253
|
+
async function main() {
|
|
254
|
+
const args = process.argv.slice(2);
|
|
255
|
+
|
|
256
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
257
|
+
showHelp();
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (args.includes('--show')) {
|
|
262
|
+
await showPreferences();
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (args.includes('--sync')) {
|
|
267
|
+
await syncPreferences();
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (args.includes('--reset')) {
|
|
272
|
+
await resetPreferences();
|
|
273
|
+
process.exit(0);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Default: open browser
|
|
277
|
+
await openStandardsPicker();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
main().catch(error => {
|
|
281
|
+
console.error('\n❌ Error:', error.message);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
module.exports = { showHelp };
|