dual-brain 3.9.0 → 4.0.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/hooks/enforce-tier.mjs +12 -14
- package/hooks/plan-generator.mjs +544 -0
- package/hooks/profiles.mjs +2 -2
- package/hooks/test-orchestrator.mjs +3 -3
- package/hooks/vibe-memory.mjs +463 -0
- package/hooks/vibe-router.mjs +262 -0
- package/install.mjs +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* vibe-memory.mjs — Durable preference and context memory for vibe coding.
|
|
4
|
+
*
|
|
5
|
+
* Persists user workflow preferences, risk tolerance, and active work context
|
|
6
|
+
* across sessions. Loaded by control-panel and routing hooks.
|
|
7
|
+
*
|
|
8
|
+
* Exports:
|
|
9
|
+
* loadMemory() → memory object
|
|
10
|
+
* updateMemory(key, value) → void
|
|
11
|
+
* recordSessionEnd(summary) → void
|
|
12
|
+
* getActiveThreads() → array of recent work threads
|
|
13
|
+
* inferPreferences() → { suggestions, confidence }
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createHash } from 'crypto';
|
|
17
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
|
|
18
|
+
import { dirname, join } from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
|
|
21
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const MEMORY_FILE = join(__dirname, '..', 'dual-brain.memory.json');
|
|
23
|
+
|
|
24
|
+
const EMPTY_MEMORY = {
|
|
25
|
+
schema_version: 1,
|
|
26
|
+
preferences: {
|
|
27
|
+
default_profile: null,
|
|
28
|
+
risk_tolerance: 'normal',
|
|
29
|
+
verbosity: 'normal',
|
|
30
|
+
auto_dual_brain: true,
|
|
31
|
+
preferred_provider: null,
|
|
32
|
+
},
|
|
33
|
+
threads: [],
|
|
34
|
+
insights: {
|
|
35
|
+
total_sessions: 0,
|
|
36
|
+
profile_switches: {},
|
|
37
|
+
common_risk_domains: [],
|
|
38
|
+
dual_brain_useful_rate: null,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
function atomicWrite(path, data) {
|
|
45
|
+
const tmp = path + '.tmp.' + process.pid;
|
|
46
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
|
|
47
|
+
renameSync(tmp, path);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function deepMerge(defaults, override) {
|
|
51
|
+
const result = { ...defaults };
|
|
52
|
+
for (const key of Object.keys(defaults)) {
|
|
53
|
+
if (override[key] === undefined) continue;
|
|
54
|
+
if (
|
|
55
|
+
defaults[key] !== null &&
|
|
56
|
+
typeof defaults[key] === 'object' &&
|
|
57
|
+
!Array.isArray(defaults[key]) &&
|
|
58
|
+
typeof override[key] === 'object' &&
|
|
59
|
+
!Array.isArray(override[key]) &&
|
|
60
|
+
override[key] !== null
|
|
61
|
+
) {
|
|
62
|
+
result[key] = deepMerge(defaults[key], override[key]);
|
|
63
|
+
} else {
|
|
64
|
+
result[key] = override[key];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Preserve any extra keys from override not in defaults
|
|
68
|
+
for (const key of Object.keys(override)) {
|
|
69
|
+
if (!(key in defaults)) {
|
|
70
|
+
result[key] = override[key];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function setNestedKey(obj, dotPath, value) {
|
|
77
|
+
const parts = dotPath.split('.');
|
|
78
|
+
let current = obj;
|
|
79
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
80
|
+
const part = parts[i];
|
|
81
|
+
if (current[part] === undefined || typeof current[part] !== 'object' || current[part] === null) {
|
|
82
|
+
current[part] = {};
|
|
83
|
+
}
|
|
84
|
+
current = current[part];
|
|
85
|
+
}
|
|
86
|
+
current[parts[parts.length - 1]] = value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function threadId(summary) {
|
|
90
|
+
return createHash('sha256').update(summary).digest('hex').slice(0, 16);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Core API ─────────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
function loadMemory() {
|
|
96
|
+
let stored = {};
|
|
97
|
+
try {
|
|
98
|
+
stored = JSON.parse(readFileSync(MEMORY_FILE, 'utf8'));
|
|
99
|
+
} catch {
|
|
100
|
+
// File missing or corrupt — start fresh
|
|
101
|
+
}
|
|
102
|
+
return deepMerge(EMPTY_MEMORY, stored);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function updateMemory(key, value) {
|
|
106
|
+
const memory = loadMemory();
|
|
107
|
+
setNestedKey(memory, key, value);
|
|
108
|
+
atomicWrite(MEMORY_FILE, memory);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function recordSessionEnd(summary) {
|
|
112
|
+
const memory = loadMemory();
|
|
113
|
+
|
|
114
|
+
// Increment total sessions
|
|
115
|
+
memory.insights.total_sessions++;
|
|
116
|
+
|
|
117
|
+
// Track profile used
|
|
118
|
+
let profileUsed = 'auto';
|
|
119
|
+
try {
|
|
120
|
+
const profileFile = join(__dirname, '..', 'dual-brain.profile.json');
|
|
121
|
+
const profileData = JSON.parse(readFileSync(profileFile, 'utf8'));
|
|
122
|
+
profileUsed = profileData.active || 'auto';
|
|
123
|
+
} catch {}
|
|
124
|
+
|
|
125
|
+
if (!memory.insights.profile_switches) memory.insights.profile_switches = {};
|
|
126
|
+
memory.insights.profile_switches[profileUsed] =
|
|
127
|
+
(memory.insights.profile_switches[profileUsed] || 0) + 1;
|
|
128
|
+
|
|
129
|
+
// Add/update thread if summary has content
|
|
130
|
+
if (summary && typeof summary === 'object' && summary.description) {
|
|
131
|
+
const desc = summary.description;
|
|
132
|
+
const id = threadId(desc);
|
|
133
|
+
const now = new Date().toISOString();
|
|
134
|
+
|
|
135
|
+
const existingIdx = memory.threads.findIndex(t => t.id === id);
|
|
136
|
+
if (existingIdx >= 0) {
|
|
137
|
+
// Update existing thread
|
|
138
|
+
memory.threads[existingIdx].last_active = now;
|
|
139
|
+
memory.threads[existingIdx].profile_used = profileUsed;
|
|
140
|
+
if (summary.files_touched) {
|
|
141
|
+
const merged = new Set([
|
|
142
|
+
...(memory.threads[existingIdx].files_touched || []),
|
|
143
|
+
...summary.files_touched,
|
|
144
|
+
]);
|
|
145
|
+
memory.threads[existingIdx].files_touched = [...merged];
|
|
146
|
+
}
|
|
147
|
+
if (summary.status) memory.threads[existingIdx].status = summary.status;
|
|
148
|
+
} else {
|
|
149
|
+
// New thread
|
|
150
|
+
memory.threads.push({
|
|
151
|
+
id,
|
|
152
|
+
summary: desc,
|
|
153
|
+
started_at: now,
|
|
154
|
+
last_active: now,
|
|
155
|
+
profile_used: profileUsed,
|
|
156
|
+
files_touched: summary.files_touched || [],
|
|
157
|
+
risk_domains: summary.risk_domains || [],
|
|
158
|
+
status: summary.status || 'active',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Track common risk domains
|
|
163
|
+
if (summary.risk_domains && summary.risk_domains.length > 0) {
|
|
164
|
+
const domainCounts = {};
|
|
165
|
+
for (const d of memory.insights.common_risk_domains || []) {
|
|
166
|
+
domainCounts[d] = (domainCounts[d] || 0) + 1;
|
|
167
|
+
}
|
|
168
|
+
for (const d of summary.risk_domains) {
|
|
169
|
+
domainCounts[d] = (domainCounts[d] || 0) + 1;
|
|
170
|
+
}
|
|
171
|
+
// Keep top domains sorted by frequency
|
|
172
|
+
memory.insights.common_risk_domains = Object.entries(domainCounts)
|
|
173
|
+
.sort((a, b) => b[1] - a[1])
|
|
174
|
+
.slice(0, 10)
|
|
175
|
+
.map(([d]) => d);
|
|
176
|
+
}
|
|
177
|
+
} else if (summary && typeof summary === 'string' && summary.trim()) {
|
|
178
|
+
// Simple string summary — create a basic thread
|
|
179
|
+
const id = threadId(summary);
|
|
180
|
+
const now = new Date().toISOString();
|
|
181
|
+
const existingIdx = memory.threads.findIndex(t => t.id === id);
|
|
182
|
+
if (existingIdx >= 0) {
|
|
183
|
+
memory.threads[existingIdx].last_active = now;
|
|
184
|
+
memory.threads[existingIdx].profile_used = profileUsed;
|
|
185
|
+
} else {
|
|
186
|
+
memory.threads.push({
|
|
187
|
+
id,
|
|
188
|
+
summary,
|
|
189
|
+
started_at: now,
|
|
190
|
+
last_active: now,
|
|
191
|
+
profile_used: profileUsed,
|
|
192
|
+
files_touched: [],
|
|
193
|
+
risk_domains: [],
|
|
194
|
+
status: 'active',
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Prune threads older than 7 days, keep max 10
|
|
200
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
201
|
+
memory.threads = memory.threads
|
|
202
|
+
.filter(t => Date.parse(t.last_active) >= sevenDaysAgo)
|
|
203
|
+
.sort((a, b) => Date.parse(b.last_active) - Date.parse(a.last_active))
|
|
204
|
+
.slice(0, 10);
|
|
205
|
+
|
|
206
|
+
atomicWrite(MEMORY_FILE, memory);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getActiveThreads() {
|
|
210
|
+
const memory = loadMemory();
|
|
211
|
+
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
212
|
+
return memory.threads
|
|
213
|
+
.filter(t => Date.parse(t.last_active) >= sevenDaysAgo)
|
|
214
|
+
.sort((a, b) => Date.parse(b.last_active) - Date.parse(a.last_active));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function inferPreferences() {
|
|
218
|
+
const memory = loadMemory();
|
|
219
|
+
const suggestions = [];
|
|
220
|
+
const switches = memory.insights.profile_switches || {};
|
|
221
|
+
const totalSessions = memory.insights.total_sessions || 0;
|
|
222
|
+
|
|
223
|
+
// Need at least 5 sessions to make suggestions
|
|
224
|
+
if (totalSessions < 5) {
|
|
225
|
+
return { suggestions: [], confidence: 'low' };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Check profile usage pattern
|
|
229
|
+
const totalSwitches = Object.values(switches).reduce((a, b) => a + b, 0);
|
|
230
|
+
if (totalSwitches > 0) {
|
|
231
|
+
for (const [profile, count] of Object.entries(switches)) {
|
|
232
|
+
const pct = (count / totalSwitches) * 100;
|
|
233
|
+
if (pct > 60 && profile !== 'auto') {
|
|
234
|
+
suggestions.push({
|
|
235
|
+
key: 'preferences.default_profile',
|
|
236
|
+
value: profile,
|
|
237
|
+
reason: `You use "${profile}" ${Math.round(pct)}% of the time — consider making it your default.`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check risk domain patterns
|
|
244
|
+
const highRiskDomains = ['auth', 'billing', 'secrets', 'migrations', 'security', 'payments'];
|
|
245
|
+
const domains = memory.insights.common_risk_domains || [];
|
|
246
|
+
const highRiskCount = domains.filter(d => highRiskDomains.includes(d)).length;
|
|
247
|
+
if (highRiskCount >= 2 && memory.preferences.risk_tolerance !== 'careful') {
|
|
248
|
+
suggestions.push({
|
|
249
|
+
key: 'preferences.risk_tolerance',
|
|
250
|
+
value: 'careful',
|
|
251
|
+
reason: `You frequently work in high-risk domains (${domains.filter(d => highRiskDomains.includes(d)).join(', ')}) — "careful" mode adds extra review.`,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check if user works mostly in low-risk areas
|
|
256
|
+
const lowRiskDomains = ['docs', 'tests', 'config', 'styles'];
|
|
257
|
+
const lowRiskCount = domains.filter(d => lowRiskDomains.includes(d)).length;
|
|
258
|
+
if (lowRiskCount >= 2 && highRiskCount === 0 && memory.preferences.risk_tolerance !== 'aggressive') {
|
|
259
|
+
suggestions.push({
|
|
260
|
+
key: 'preferences.risk_tolerance',
|
|
261
|
+
value: 'aggressive',
|
|
262
|
+
reason: `Your work is mostly in low-risk domains (${domains.filter(d => lowRiskDomains.includes(d)).join(', ')}) — "aggressive" skips unnecessary reviews.`,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Determine confidence
|
|
267
|
+
let confidence = 'low';
|
|
268
|
+
if (totalSessions >= 20 && suggestions.length > 0) confidence = 'high';
|
|
269
|
+
else if (totalSessions >= 10 && suggestions.length > 0) confidence = 'medium';
|
|
270
|
+
else if (suggestions.length > 0) confidence = 'low';
|
|
271
|
+
|
|
272
|
+
return { suggestions, confidence };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export {
|
|
276
|
+
loadMemory,
|
|
277
|
+
updateMemory,
|
|
278
|
+
recordSessionEnd,
|
|
279
|
+
getActiveThreads,
|
|
280
|
+
inferPreferences,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
const noColor = !!process.env.NO_COLOR;
|
|
286
|
+
const e = (code, s) => noColor ? s : `\x1b[${code}m${s}\x1b[0m`;
|
|
287
|
+
const bold = s => e('1', s);
|
|
288
|
+
const dim = s => e('2', s);
|
|
289
|
+
const cyan = s => e('36', s);
|
|
290
|
+
const green = s => e('32', s);
|
|
291
|
+
const yellow = s => e('33', s);
|
|
292
|
+
|
|
293
|
+
function printMemory() {
|
|
294
|
+
const memory = loadMemory();
|
|
295
|
+
console.log('');
|
|
296
|
+
console.log(` ${bold('Vibe Memory')} ${dim(MEMORY_FILE)}`);
|
|
297
|
+
console.log('');
|
|
298
|
+
|
|
299
|
+
console.log(` ${bold('Preferences:')}`);
|
|
300
|
+
for (const [k, v] of Object.entries(memory.preferences)) {
|
|
301
|
+
console.log(` ${k.padEnd(22)} ${v === null ? dim('(auto)') : cyan(String(v))}`);
|
|
302
|
+
}
|
|
303
|
+
console.log('');
|
|
304
|
+
|
|
305
|
+
console.log(` ${bold('Insights:')}`);
|
|
306
|
+
console.log(` total_sessions ${memory.insights.total_sessions}`);
|
|
307
|
+
|
|
308
|
+
const switches = memory.insights.profile_switches || {};
|
|
309
|
+
if (Object.keys(switches).length > 0) {
|
|
310
|
+
const parts = Object.entries(switches).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
311
|
+
console.log(` profile_switches ${parts}`);
|
|
312
|
+
} else {
|
|
313
|
+
console.log(` profile_switches ${dim('(none)')}`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const domains = memory.insights.common_risk_domains || [];
|
|
317
|
+
console.log(` common_risk_domains ${domains.length > 0 ? domains.join(', ') : dim('(none)')}`);
|
|
318
|
+
|
|
319
|
+
const rate = memory.insights.dual_brain_useful_rate;
|
|
320
|
+
console.log(` dual_brain_useful ${rate !== null ? rate + '%' : dim('(not enough data)')}`);
|
|
321
|
+
console.log('');
|
|
322
|
+
|
|
323
|
+
const threads = getActiveThreads();
|
|
324
|
+
if (threads.length > 0) {
|
|
325
|
+
console.log(` ${bold('Active Threads')} (${threads.length}):`);
|
|
326
|
+
for (const t of threads) {
|
|
327
|
+
const ago = timeAgo(Date.parse(t.last_active));
|
|
328
|
+
const status = t.status === 'completed' ? green('done') : yellow('active');
|
|
329
|
+
console.log(` ${status} ${dim(ago.padEnd(10))} ${t.summary.slice(0, 50)}`);
|
|
330
|
+
if (t.files_touched.length > 0) {
|
|
331
|
+
console.log(` ${dim('files: ' + t.files_touched.slice(0, 3).join(', ') + (t.files_touched.length > 3 ? ` +${t.files_touched.length - 3}` : ''))}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
console.log(` ${bold('Active Threads:')} ${dim('(none)')}`);
|
|
336
|
+
}
|
|
337
|
+
console.log('');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function printThreads() {
|
|
341
|
+
const threads = getActiveThreads();
|
|
342
|
+
console.log('');
|
|
343
|
+
if (threads.length === 0) {
|
|
344
|
+
console.log(` ${dim('No active threads in the last 7 days.')}`);
|
|
345
|
+
console.log('');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
console.log(` ${bold('Active Threads')} (last 7 days):`);
|
|
350
|
+
console.log('');
|
|
351
|
+
for (const t of threads) {
|
|
352
|
+
const ago = timeAgo(Date.parse(t.last_active));
|
|
353
|
+
const status = t.status === 'completed' ? green('done') : yellow('active');
|
|
354
|
+
console.log(` ${status} ${bold(t.summary)}`);
|
|
355
|
+
console.log(` ${dim('id:')} ${t.id} ${dim('profile:')} ${t.profile_used} ${dim('last:')} ${ago}`);
|
|
356
|
+
if (t.files_touched.length > 0) {
|
|
357
|
+
console.log(` ${dim('files:')} ${t.files_touched.join(', ')}`);
|
|
358
|
+
}
|
|
359
|
+
if (t.risk_domains.length > 0) {
|
|
360
|
+
console.log(` ${dim('risk:')} ${t.risk_domains.join(', ')}`);
|
|
361
|
+
}
|
|
362
|
+
console.log('');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function printInfer() {
|
|
367
|
+
const { suggestions, confidence } = inferPreferences();
|
|
368
|
+
console.log('');
|
|
369
|
+
console.log(` ${bold('Preference Suggestions')} ${dim('confidence: ' + confidence)}`);
|
|
370
|
+
console.log('');
|
|
371
|
+
|
|
372
|
+
if (suggestions.length === 0) {
|
|
373
|
+
const memory = loadMemory();
|
|
374
|
+
if (memory.insights.total_sessions < 5) {
|
|
375
|
+
console.log(` ${dim('Not enough data yet — need at least 5 sessions.')}`);
|
|
376
|
+
console.log(` ${dim(`Current: ${memory.insights.total_sessions} sessions recorded.`)}`);
|
|
377
|
+
} else {
|
|
378
|
+
console.log(` ${dim('No suggestions — your current preferences look good.')}`);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
for (const s of suggestions) {
|
|
382
|
+
console.log(` ${yellow('suggestion:')} ${bold(s.key)} = ${cyan(String(s.value))}`);
|
|
383
|
+
console.log(` ${s.reason}`);
|
|
384
|
+
console.log(` ${dim(`Apply: node vibe-memory.mjs --set ${s.key}=${s.value}`)}`);
|
|
385
|
+
console.log('');
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
console.log('');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function handleSet(arg) {
|
|
392
|
+
const eqIdx = arg.indexOf('=');
|
|
393
|
+
if (eqIdx < 0) {
|
|
394
|
+
console.error(` Invalid --set format. Use: --set key=value`);
|
|
395
|
+
console.error(` Example: --set preferences.risk_tolerance=careful`);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const key = arg.slice(0, eqIdx);
|
|
400
|
+
let value = arg.slice(eqIdx + 1);
|
|
401
|
+
|
|
402
|
+
// Parse value types
|
|
403
|
+
if (value === 'null') value = null;
|
|
404
|
+
else if (value === 'true') value = true;
|
|
405
|
+
else if (value === 'false') value = false;
|
|
406
|
+
else if (/^\d+$/.test(value)) value = parseInt(value, 10);
|
|
407
|
+
else if (/^\d+\.\d+$/.test(value)) value = parseFloat(value);
|
|
408
|
+
|
|
409
|
+
updateMemory(key, value);
|
|
410
|
+
console.log(` ${green('updated:')} ${key} = ${value === null ? 'null' : String(value)}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function timeAgo(ts) {
|
|
414
|
+
const mins = Math.round((Date.now() - ts) / 60000);
|
|
415
|
+
if (mins < 1) return 'just now';
|
|
416
|
+
if (mins < 60) return mins + 'm ago';
|
|
417
|
+
const h = Math.round(mins / 60);
|
|
418
|
+
if (h < 24) return h + 'h ago';
|
|
419
|
+
const d = Math.round(h / 24);
|
|
420
|
+
return d + 'd ago';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ─── CLI Entry ────────────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
const isMain = process.argv[1] &&
|
|
426
|
+
(process.argv[1].endsWith('vibe-memory.mjs') ||
|
|
427
|
+
process.argv[1].endsWith('vibe-memory'));
|
|
428
|
+
|
|
429
|
+
if (isMain) {
|
|
430
|
+
const args = process.argv.slice(2);
|
|
431
|
+
|
|
432
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
433
|
+
console.log(`
|
|
434
|
+
vibe-memory.mjs — Durable preference and context memory
|
|
435
|
+
|
|
436
|
+
Usage:
|
|
437
|
+
node vibe-memory.mjs Show current memory
|
|
438
|
+
node vibe-memory.mjs --set preferences.verbosity=quiet Set a preference
|
|
439
|
+
node vibe-memory.mjs --threads Show active threads
|
|
440
|
+
node vibe-memory.mjs --infer Suggest preferences
|
|
441
|
+
`);
|
|
442
|
+
process.exit(0);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const setIdx = args.findIndex(a => a.startsWith('--set'));
|
|
446
|
+
if (setIdx >= 0) {
|
|
447
|
+
let setArg = args[setIdx];
|
|
448
|
+
if (setArg === '--set' && args[setIdx + 1]) {
|
|
449
|
+
setArg = args[setIdx + 1];
|
|
450
|
+
} else if (setArg.startsWith('--set=')) {
|
|
451
|
+
setArg = setArg.slice(6);
|
|
452
|
+
} else {
|
|
453
|
+
setArg = setArg.slice(5); // --setkey=value (shouldn't happen, but handle)
|
|
454
|
+
}
|
|
455
|
+
handleSet(setArg);
|
|
456
|
+
} else if (args.includes('--threads')) {
|
|
457
|
+
printThreads();
|
|
458
|
+
} else if (args.includes('--infer')) {
|
|
459
|
+
printInfer();
|
|
460
|
+
} else {
|
|
461
|
+
printMemory();
|
|
462
|
+
}
|
|
463
|
+
}
|