decisionnode 0.2.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/dist/cli.js ADDED
@@ -0,0 +1,1724 @@
1
+ #!/usr/bin/env node
2
+ import { listDecisions, getDecisionById, addDecision, updateDecision, deleteDecision, deleteScope, getNextDecisionId, renumberDecisions, importDecisions, getAvailableScopes, listProjects, saveDecisions, listGlobalDecisions, getGlobalDecisionById, addGlobalDecision, updateGlobalDecision, deleteGlobalDecision, getNextGlobalDecisionId, getGlobalScopes } from './store.js';
3
+ import { getHistory, getSnapshot, getDecisionsFromSnapshot, logBatchAction, logAction } from './history.js';
4
+ import { getSearchSensitivity, setSearchSensitivity, isGlobalId } from './env.js';
5
+ import { loginToCloud, logoutFromCloud, getCloudStatus, syncDecisionsToCloud, getCloudSyncStatus, pullDecisionsFromCloud, detectConflicts, resolveConflict, updateSyncMetadata, saveIncomingChanges, removeIncomingChanges } from './cloud.js';
6
+ import { getProjectRoot } from './env.js';
7
+ import * as readline from 'readline';
8
+ import fs from 'fs/promises';
9
+ import path from 'path';
10
+ const args = process.argv.slice(2);
11
+ const command = args[0];
12
+ async function main() {
13
+ try {
14
+ switch (command) {
15
+ case 'list':
16
+ await handleList();
17
+ break;
18
+ case 'get':
19
+ await handleGet();
20
+ break;
21
+ case 'search':
22
+ await handleSearch();
23
+ break;
24
+ case 'add':
25
+ case 'add-decision':
26
+ await handleAddDecision();
27
+ break;
28
+ case 'edit':
29
+ await handleEdit();
30
+ break;
31
+ case 'delete':
32
+ await handleDelete();
33
+ break;
34
+ case 'deprecate':
35
+ await handleDeprecate();
36
+ break;
37
+ case 'activate':
38
+ await handleActivate();
39
+ break;
40
+ case 'import':
41
+ await handleImport();
42
+ break;
43
+ case 'history':
44
+ await handleHistory();
45
+ break;
46
+ case 'init':
47
+ await handleInit();
48
+ break;
49
+ case 'setup':
50
+ await handleSetup();
51
+ break;
52
+ case 'embed':
53
+ await handleEmbed();
54
+ break;
55
+ case 'check':
56
+ await handleCheck();
57
+ break;
58
+ case 'clean':
59
+ await handleClean();
60
+ break;
61
+ case 'export':
62
+ await handleExport();
63
+ break;
64
+ // case 'marketplace':
65
+ // case 'market':
66
+ // await handleMarketplace();
67
+ // break;
68
+ case 'projects':
69
+ await handleProjects();
70
+ break;
71
+ case 'config':
72
+ await handleConfig();
73
+ break;
74
+ // case 'login':
75
+ // await handleLogin();
76
+ // break;
77
+ // case 'logout':
78
+ // await handleLogout();
79
+ // break;
80
+ // case 'status':
81
+ // await handleStatus();
82
+ // break;
83
+ // case 'sync':
84
+ // await handleSync();
85
+ // break;
86
+ // case 'cloud':
87
+ // await handleCloud();
88
+ // break;
89
+ // case 'conflicts':
90
+ // await handleConflicts();
91
+ // break;
92
+ // case 'fetch':
93
+ // await handleFetch();
94
+ // break;
95
+ // case 'pull':
96
+ // await handlePull();
97
+ // break;
98
+ case 'delete-scope':
99
+ await handleDeleteScope();
100
+ break;
101
+ default:
102
+ printUsage();
103
+ break;
104
+ }
105
+ }
106
+ catch (error) {
107
+ console.error('āŒ Error:', error.message);
108
+ process.exit(1);
109
+ }
110
+ }
111
+ async function handleList() {
112
+ const scopeFlag = args.indexOf('--scope');
113
+ const scope = scopeFlag > -1 ? args[scopeFlag + 1] : undefined;
114
+ const globalOnly = args.includes('--global');
115
+ // Get project decisions (unless --global flag is set)
116
+ const projectDecisions = globalOnly ? [] : await listDecisions(scope);
117
+ // Get global decisions (always, unless --scope is filtering a project scope)
118
+ const globalDecisions = globalOnly
119
+ ? await listGlobalDecisions(scope)
120
+ : await listGlobalDecisions();
121
+ const allDecisions = [...projectDecisions, ...globalDecisions];
122
+ if (allDecisions.length === 0) {
123
+ console.log('šŸ“­ No decisions found.');
124
+ console.log('\nRun: decide add');
125
+ return;
126
+ }
127
+ const label = globalOnly ? 'Global Decisions' : `Decisions${scope ? ` (${scope})` : ''}`;
128
+ console.log(`\nšŸ“‹ ${label}: \n`);
129
+ // Show global decisions first, then project decisions
130
+ if (globalDecisions.length > 0) {
131
+ const globalGrouped = {};
132
+ for (const d of globalDecisions) {
133
+ if (!globalGrouped[d.scope])
134
+ globalGrouped[d.scope] = [];
135
+ globalGrouped[d.scope].push(d);
136
+ }
137
+ console.log('🌐 Global');
138
+ for (const [scopeName, scopeDecisions] of Object.entries(globalGrouped)) {
139
+ console.log(` šŸ“ ${scopeName} `);
140
+ for (const d of scopeDecisions) {
141
+ const statusIcon = d.status === 'active' ? 'āœ…' :
142
+ 'āš ļø';
143
+ console.log(` ${statusIcon} [${d.id}] ${d.decision} `);
144
+ }
145
+ }
146
+ console.log('');
147
+ }
148
+ if (projectDecisions.length > 0) {
149
+ const grouped = {};
150
+ for (const d of projectDecisions) {
151
+ if (!grouped[d.scope])
152
+ grouped[d.scope] = [];
153
+ grouped[d.scope].push(d);
154
+ }
155
+ for (const [scopeName, scopeDecisions] of Object.entries(grouped)) {
156
+ console.log(`šŸ“ ${scopeName} `);
157
+ for (const d of scopeDecisions) {
158
+ const statusIcon = d.status === 'active' ? 'āœ…' :
159
+ 'āš ļø';
160
+ console.log(` ${statusIcon} [${d.id}] ${d.decision} `);
161
+ }
162
+ console.log('');
163
+ }
164
+ }
165
+ const parts = [];
166
+ if (projectDecisions.length > 0)
167
+ parts.push(`${projectDecisions.length} project`);
168
+ if (globalDecisions.length > 0)
169
+ parts.push(`${globalDecisions.length} global`);
170
+ console.log(`Total: ${parts.join(' + ')} decisions`);
171
+ }
172
+ async function handleGet() {
173
+ const id = args[1];
174
+ if (!id) {
175
+ console.log('Usage: decide get <decision-id>');
176
+ return;
177
+ }
178
+ const decision = isGlobalId(id)
179
+ ? await getGlobalDecisionById(id)
180
+ : await getDecisionById(id);
181
+ if (!decision) {
182
+ console.log(`āŒ Decision "${id}" not found`);
183
+ return;
184
+ }
185
+ console.log('\n' + '─'.repeat(60));
186
+ console.log(`šŸ“Œ ${decision.id} `);
187
+ console.log('─'.repeat(60));
188
+ console.log(`\nšŸ“‹ Decision: ${decision.decision} `);
189
+ if (decision.rationale) {
190
+ console.log(`\nšŸ’” Rationale: ${decision.rationale} `);
191
+ }
192
+ console.log(`\nšŸ“ Scope: ${decision.scope} `);
193
+ console.log(`šŸ“Š Status: ${decision.status} `);
194
+ if (decision.constraints?.length) {
195
+ console.log(`āš ļø Constraints: ${decision.constraints.join(', ')} `);
196
+ }
197
+ console.log('');
198
+ }
199
+ async function handleSearch() {
200
+ const query = args.slice(1).join(' ');
201
+ if (!query) {
202
+ console.log('Usage: decide search "<your question>"');
203
+ return;
204
+ }
205
+ try {
206
+ const { findRelevantDecisions } = await import('./ai/rag.js');
207
+ const results = await findRelevantDecisions(query, 5);
208
+ if (results.length === 0) {
209
+ console.log('šŸ” No relevant decisions found.');
210
+ console.log(' (Have you added decisions yet?)');
211
+ return;
212
+ }
213
+ console.log(`\nšŸ” Results for: "${query}"\n`);
214
+ for (const result of results) {
215
+ const score = (result.score * 100).toFixed(0);
216
+ console.log(`[${score}%] ${result.decision.id}: ${result.decision.decision} `);
217
+ }
218
+ console.log('');
219
+ }
220
+ catch (error) {
221
+ console.log('āŒ Semantic search requires a Gemini API key.');
222
+ console.log(' Run: decide setup');
223
+ }
224
+ }
225
+ function prompt(question, defaultValue) {
226
+ const rl = readline.createInterface({
227
+ input: process.stdin,
228
+ output: process.stdout
229
+ });
230
+ return new Promise(resolve => {
231
+ rl.question(question, (answer) => {
232
+ rl.close();
233
+ resolve(answer);
234
+ });
235
+ if (defaultValue) {
236
+ rl.write(defaultValue);
237
+ }
238
+ });
239
+ }
240
+ function getFlag(flag) {
241
+ const idx = args.indexOf(flag);
242
+ if (idx === -1 || idx + 1 >= args.length)
243
+ return undefined;
244
+ return args[idx + 1];
245
+ }
246
+ async function handleAddDecision() {
247
+ const isGlobal = args.includes('--global');
248
+ // Check for inline mode: decide add --scope UI --decision "Use Tailwind" ...
249
+ const inlineScope = getFlag('--scope') || getFlag('-s');
250
+ const inlineDecision = getFlag('--decision') || getFlag('-d');
251
+ let scope;
252
+ let decisionText;
253
+ let rationale;
254
+ let constraintsInput;
255
+ if (inlineScope && inlineDecision) {
256
+ // Inline mode — no prompts
257
+ scope = inlineScope;
258
+ decisionText = inlineDecision;
259
+ rationale = getFlag('--rationale') || getFlag('-r') || '';
260
+ constraintsInput = getFlag('--constraints') || getFlag('-c') || '';
261
+ }
262
+ else {
263
+ // Interactive mode
264
+ console.log(`\nāž• Add New ${isGlobal ? 'Global ' : ''}Decision\n`);
265
+ // Show existing scopes for consistency
266
+ const existingScopes = isGlobal ? await getGlobalScopes() : await getAvailableScopes();
267
+ if (existingScopes.length > 0) {
268
+ console.log(`Existing scopes: ${existingScopes.join(', ')} \n`);
269
+ }
270
+ const scopeExamples = existingScopes.length > 0
271
+ ? existingScopes.slice(0, 3).join(', ')
272
+ : 'UI, Backend, API';
273
+ scope = await prompt(`Scope (e.g., ${scopeExamples} - capitalization doesn't matter): `);
274
+ if (!scope.trim()) {
275
+ console.log('āŒ Scope is required');
276
+ return;
277
+ }
278
+ decisionText = await prompt('Decision: ');
279
+ if (!decisionText.trim()) {
280
+ console.log('āŒ Decision text is required');
281
+ return;
282
+ }
283
+ // Check for potential conflicts with existing decisions
284
+ try {
285
+ const { findPotentialConflicts } = await import('./ai/rag.js');
286
+ const conflicts = await findPotentialConflicts(`${scope}: ${decisionText}`, 0.75);
287
+ if (conflicts.length > 0) {
288
+ console.log('\nāš ļø Similar decisions found:\n');
289
+ for (const { decision, score } of conflicts) {
290
+ const similarity = Math.round(score * 100);
291
+ console.log(` ${decision.id}: ${decision.decision.substring(0, 50)}... (${similarity}% similar)`);
292
+ }
293
+ console.log('');
294
+ const proceed = await prompt('Continue anyway? (y/N): ');
295
+ if (proceed.toLowerCase() !== 'y') {
296
+ console.log('Cancelled.');
297
+ return;
298
+ }
299
+ }
300
+ }
301
+ catch {
302
+ // Conflict check failed (API key not set) - continue anyway
303
+ }
304
+ rationale = await prompt('Rationale (optional): ');
305
+ constraintsInput = await prompt('Constraints (comma-separated, optional): ');
306
+ }
307
+ if (isGlobal) {
308
+ const rawId = await getNextGlobalDecisionId(scope.trim());
309
+ const newDecision = {
310
+ id: rawId,
311
+ scope: scope.trim(),
312
+ decision: decisionText.trim(),
313
+ rationale: rationale.trim() || undefined,
314
+ constraints: constraintsInput.trim() ? constraintsInput.split(',').map(s => s.trim()) : undefined,
315
+ status: 'active',
316
+ createdAt: new Date().toISOString()
317
+ };
318
+ const { embedded } = await addGlobalDecision(newDecision);
319
+ console.log(`\nāœ… Created global:${rawId}`);
320
+ console.log(` This decision applies to all projects`);
321
+ if (embedded) {
322
+ console.log(` Auto-embedded for semantic search`);
323
+ }
324
+ else {
325
+ console.log(`\nāš ļø Not embedded — semantic search won't find this decision.`);
326
+ console.log(` Run: decide setup (to set your Gemini API key)`);
327
+ console.log(` Then: decide embed (to embed all unembedded decisions)`);
328
+ }
329
+ }
330
+ else {
331
+ const id = await getNextDecisionId(scope.trim());
332
+ const newDecision = {
333
+ id,
334
+ scope: scope.trim(),
335
+ decision: decisionText.trim(),
336
+ rationale: rationale.trim() || undefined,
337
+ constraints: constraintsInput.trim() ? constraintsInput.split(',').map(s => s.trim()) : undefined,
338
+ status: 'active',
339
+ createdAt: new Date().toISOString()
340
+ };
341
+ const { embedded } = await addDecision(newDecision);
342
+ console.log(`\nāœ… Created ${id}`);
343
+ if (embedded) {
344
+ console.log(` Auto-embedded for semantic search`);
345
+ }
346
+ else {
347
+ console.log(`\nāš ļø Not embedded — semantic search won't find this decision.`);
348
+ console.log(` Run: decide setup (to set your Gemini API key)`);
349
+ console.log(` Then: decide embed (to embed all unembedded decisions)`);
350
+ }
351
+ }
352
+ }
353
+ async function handleEdit() {
354
+ const id = args[1];
355
+ if (!id) {
356
+ console.log('Usage: decide edit <decision-id>');
357
+ return;
358
+ }
359
+ const global = isGlobalId(id);
360
+ const decision = global
361
+ ? await getGlobalDecisionById(id)
362
+ : await getDecisionById(id);
363
+ if (!decision) {
364
+ console.log(`āŒ Decision ${id} not found`);
365
+ return;
366
+ }
367
+ if (global) {
368
+ console.log(`\nāš ļø This is a global decision that affects ALL projects.`);
369
+ const confirm = await prompt('Continue editing? (y/N): ');
370
+ if (confirm.trim().toLowerCase() !== 'y') {
371
+ console.log('Cancelled.');
372
+ return;
373
+ }
374
+ }
375
+ console.log(`\nāœļø Editing ${id}`);
376
+ console.log('Press Enter to keep current value.\n');
377
+ const newDecision = await prompt('Decision: ', decision.decision);
378
+ const newRationale = await prompt('Rationale: ', decision.rationale || '');
379
+ const newConstraints = await prompt('Constraints: ', (decision.constraints || []).join(', '));
380
+ const updates = {};
381
+ if (newDecision.trim())
382
+ updates.decision = newDecision.trim();
383
+ if (newRationale.trim())
384
+ updates.rationale = newRationale.trim();
385
+ if (newConstraints.trim())
386
+ updates.constraints = newConstraints.split(',').map(s => s.trim());
387
+ if (Object.keys(updates).length === 0) {
388
+ console.log('\nNo changes made.');
389
+ return;
390
+ }
391
+ if (global) {
392
+ await updateGlobalDecision(id, updates);
393
+ }
394
+ else {
395
+ await updateDecision(id, updates);
396
+ }
397
+ console.log(`\nāœ… Updated ${id}`);
398
+ console.log(` Auto-embedded for semantic search`);
399
+ }
400
+ async function handleDelete() {
401
+ const id = args[1];
402
+ if (!id) {
403
+ console.log('Usage: decide delete <decision-id>');
404
+ return;
405
+ }
406
+ const global = isGlobalId(id);
407
+ const decision = global
408
+ ? await getGlobalDecisionById(id)
409
+ : await getDecisionById(id);
410
+ if (!decision) {
411
+ console.log(`āŒ Decision ${id} not found`);
412
+ return;
413
+ }
414
+ console.log(`\nšŸ—‘ļø Delete: ${id}`);
415
+ console.log(` "${decision.decision}"\n`);
416
+ if (global) {
417
+ console.log(`āš ļø This is a global decision that affects ALL projects.`);
418
+ }
419
+ const confirm = await prompt('Type "yes" to confirm: ');
420
+ if (confirm.trim().toLowerCase() !== 'yes') {
421
+ console.log('Cancelled.');
422
+ return;
423
+ }
424
+ if (global) {
425
+ await deleteGlobalDecision(id);
426
+ }
427
+ else {
428
+ await deleteDecision(id);
429
+ // Auto-clean orphaned data (reviews, etc.)
430
+ try {
431
+ const { cleanOrphanedData } = await import('./maintenance.js');
432
+ await cleanOrphanedData();
433
+ }
434
+ catch {
435
+ // Ignore clean errors during delete flow
436
+ }
437
+ const renumber = await prompt('Renumber remaining decisions? (y/n): ');
438
+ if (renumber.trim().toLowerCase() === 'y') {
439
+ const renames = await renumberDecisions(decision.scope);
440
+ if (renames.length > 0) {
441
+ console.log('\nRenumbered:');
442
+ renames.forEach(r => console.log(` ${r}`));
443
+ }
444
+ }
445
+ }
446
+ console.log(`\nāœ… Deleted ${id}`);
447
+ }
448
+ async function handleDeprecate() {
449
+ const id = args[1];
450
+ if (!id) {
451
+ console.log('Usage: decide deprecate <decision-id>');
452
+ return;
453
+ }
454
+ const global = isGlobalId(id);
455
+ const decision = global
456
+ ? await getGlobalDecisionById(id)
457
+ : await getDecisionById(id);
458
+ if (!decision) {
459
+ console.log(`āŒ Decision ${id} not found`);
460
+ return;
461
+ }
462
+ if (decision.status === 'deprecated') {
463
+ console.log(`āš ļø ${id} is already deprecated.`);
464
+ return;
465
+ }
466
+ console.log(`\nšŸ“Œ ${id}: ${decision.decision}`);
467
+ if (global) {
468
+ await updateGlobalDecision(id, { status: 'deprecated' });
469
+ }
470
+ else {
471
+ await updateDecision(id, { status: 'deprecated' });
472
+ }
473
+ console.log(`\nāœ… Deprecated ${id}`);
474
+ console.log(` This decision will no longer appear in search results.`);
475
+ }
476
+ async function handleActivate() {
477
+ const id = args[1];
478
+ if (!id) {
479
+ console.log('Usage: decide activate <decision-id>');
480
+ return;
481
+ }
482
+ const global = isGlobalId(id);
483
+ const decision = global
484
+ ? await getGlobalDecisionById(id)
485
+ : await getDecisionById(id);
486
+ if (!decision) {
487
+ console.log(`āŒ Decision ${id} not found`);
488
+ return;
489
+ }
490
+ if (decision.status === 'active') {
491
+ console.log(`āš ļø ${id} is already active.`);
492
+ return;
493
+ }
494
+ console.log(`\nšŸ“Œ ${id}: ${decision.decision}`);
495
+ if (global) {
496
+ await updateGlobalDecision(id, { status: 'active' });
497
+ }
498
+ else {
499
+ await updateDecision(id, { status: 'active' });
500
+ }
501
+ console.log(`\nāœ… Activated ${id}`);
502
+ console.log(` This decision will now appear in search results.`);
503
+ }
504
+ async function handleDeleteScope() {
505
+ const scopeArg = args[1];
506
+ if (!scopeArg) {
507
+ console.log('Usage: decide delete-scope <scope>');
508
+ console.log('\nDeletes all decisions in a scope.');
509
+ console.log('Example: decide delete-scope UI');
510
+ return;
511
+ }
512
+ // Show what will be deleted
513
+ const scopes = await getAvailableScopes();
514
+ const normalizedInput = scopeArg.charAt(0).toUpperCase() + scopeArg.slice(1).toLowerCase();
515
+ if (!scopes.some(s => s.toLowerCase() === scopeArg.toLowerCase())) {
516
+ console.log(`āŒ Scope "${scopeArg}" not found.`);
517
+ console.log(`Available scopes: ${scopes.join(', ')}`);
518
+ return;
519
+ }
520
+ const decisions = await listDecisions(normalizedInput);
521
+ console.log(`\nāš ļø This will delete the "${normalizedInput}" scope and ALL ${decisions.length} decision(s) in it:`);
522
+ decisions.forEach(d => console.log(` - ${d.id}: ${d.decision.substring(0, 50)}...`));
523
+ console.log('\nāš ļø This action cannot be undone!');
524
+ const confirm = await prompt('Type the scope name to confirm deletion: ');
525
+ if (confirm.toLowerCase() !== scopeArg.toLowerCase() && confirm.toLowerCase() !== normalizedInput.toLowerCase()) {
526
+ console.log('āŒ Deletion cancelled.');
527
+ return;
528
+ }
529
+ const result = await deleteScope(scopeArg);
530
+ console.log(`\nāœ… Deleted scope "${normalizedInput}" (${result.deleted} decisions removed)`);
531
+ }
532
+ async function handleImport() {
533
+ const globalFlag = args.includes('--global');
534
+ const filePath = args.find(a => a !== 'import' && a !== '--global' && a !== '--overwrite' && !a.startsWith('-'));
535
+ if (!filePath) {
536
+ console.log('Usage: decide import <file.json> [--global] [--overwrite]');
537
+ console.log('\nExample JSON format:');
538
+ console.log(`[
539
+ { "id": "ui-001", "scope": "UI", "decision": "...", "status": "active" },
540
+ { "id": "ui-002", "scope": "UI", "decision": "...", "status": "active" }
541
+ ]`);
542
+ return;
543
+ }
544
+ console.log(`\nšŸ“„ Importing from ${filePath}${globalFlag ? ' (global)' : ''}...`);
545
+ try {
546
+ const content = await fs.readFile(filePath, 'utf-8');
547
+ const data = JSON.parse(content);
548
+ // Support both array and {decisions: [...]} format
549
+ const decisions = Array.isArray(data) ? data : data.decisions;
550
+ if (!decisions || decisions.length === 0) {
551
+ console.log('āŒ No decisions found in file');
552
+ return;
553
+ }
554
+ const overwriteFlag = args.includes('--overwrite');
555
+ if (globalFlag) {
556
+ // Import into global store
557
+ let added = 0;
558
+ let skipped = 0;
559
+ for (const decision of decisions) {
560
+ try {
561
+ await addGlobalDecision(decision);
562
+ added++;
563
+ }
564
+ catch {
565
+ skipped++;
566
+ }
567
+ }
568
+ console.log(`\nāœ… Import complete (global)`);
569
+ console.log(` Added: ${added}`);
570
+ console.log(` Skipped: ${skipped}`);
571
+ }
572
+ else {
573
+ const result = await importDecisions(decisions, { overwrite: overwriteFlag });
574
+ console.log(`\nāœ… Import complete`);
575
+ console.log(` Added: ${result.added}`);
576
+ console.log(` Skipped: ${result.skipped}`);
577
+ console.log(` Embedded: ${result.embedded}`);
578
+ }
579
+ }
580
+ catch (error) {
581
+ console.log(`āŒ Import failed: ${error.message}`);
582
+ }
583
+ }
584
+ async function handleHistory() {
585
+ const entryId = args[1];
586
+ if (entryId && !entryId.startsWith('-')) {
587
+ // View specific snapshot
588
+ const entry = await getSnapshot(entryId);
589
+ if (!entry) {
590
+ console.log(`āŒ Entry ${entryId} not found`);
591
+ return;
592
+ }
593
+ console.log(`\nšŸ“œ Snapshot: ${entry.id}`);
594
+ console.log(` Action: ${entry.action}`);
595
+ console.log(` Time: ${new Date(entry.timestamp).toLocaleString()}`);
596
+ console.log(` ${entry.description}\n`);
597
+ const decisions = getDecisionsFromSnapshot(entry.snapshot);
598
+ console.log(`Decisions at this point (${decisions.length}):\n`);
599
+ for (const d of decisions) {
600
+ console.log(`\n─── ${d.id} ───`);
601
+ console.log(` Decision: ${d.decision}`);
602
+ if (d.rationale)
603
+ console.log(` Rationale: ${d.rationale}`);
604
+ if (d.constraints?.length)
605
+ console.log(` Constraints: ${d.constraints.join(', ')}`);
606
+ console.log(` Status: ${d.status}`);
607
+ }
608
+ console.log('');
609
+ return;
610
+ }
611
+ // List recent history
612
+ const filterIndex = args.indexOf('--filter');
613
+ const filter = filterIndex > -1 ? args[filterIndex + 1] : undefined;
614
+ const history = await getHistory(50); // Get last 50 entries
615
+ let displayedHistory = history;
616
+ if (filter) {
617
+ const validFilters = ['cloud', 'cli', 'mcp', 'marketplace'];
618
+ if (!validFilters.includes(filter)) {
619
+ console.log(`āŒ Invalid filter: ${filter}`);
620
+ console.log(` Valid options: ${validFilters.join(', ')}`);
621
+ return;
622
+ }
623
+ displayedHistory = history.filter(h => h.source === filter);
624
+ }
625
+ if (displayedHistory.length === 0) {
626
+ console.log(`šŸ“­ No history found${filter ? ` for filter "${filter}"` : ''}.`);
627
+ return;
628
+ }
629
+ console.log(`\nšŸ“œ Activity History${filter ? ` (Filter: ${filter.toUpperCase()})` : ''}\n`);
630
+ console.log('━'.repeat(60));
631
+ displayedHistory.forEach(entry => {
632
+ const date = new Date(entry.timestamp).toLocaleString();
633
+ const icon = getActionIcon(entry.action);
634
+ const source = entry.source ? `[${entry.source.toUpperCase()}]` : '[CLI]';
635
+ console.log(`${icon} ${date} ${source.padEnd(13)} ${entry.description}`);
636
+ });
637
+ console.log('');
638
+ }
639
+ function getActionIcon(action) {
640
+ switch (action) {
641
+ case 'added': return 'āœ…';
642
+ case 'updated': return 'āœļø ';
643
+ case 'deleted': return 'šŸ—‘ļø '; // Windows terminal handles this best
644
+ case 'imported': return 'šŸ“„';
645
+ case 'installed': return 'šŸ“¦';
646
+ case 'cloud_push': return 'ā¬†ļø ';
647
+ case 'cloud_pull': return 'ā¬‡ļø ';
648
+ case 'conflict_resolved': return 'šŸ¤';
649
+ default: return 'šŸ”¹';
650
+ }
651
+ }
652
+ function getTimeAgo(date) {
653
+ const now = new Date();
654
+ const diff = now.getTime() - date.getTime();
655
+ const minutes = Math.floor(diff / 60000);
656
+ if (minutes < 1)
657
+ return 'just now';
658
+ if (minutes < 60)
659
+ return `${minutes}m ago`;
660
+ const hours = Math.floor(minutes / 60);
661
+ if (hours < 24)
662
+ return `${hours}h ago`;
663
+ const days = Math.floor(hours / 24);
664
+ return `${days}d ago`;
665
+ }
666
+ async function handleInit() {
667
+ const cwd = process.cwd();
668
+ const projectName = path.basename(cwd);
669
+ console.log('\nšŸš€ Initializing DecisionNode\n');
670
+ console.log(` Project: ${projectName}`);
671
+ console.log(` Location: ${cwd}\n`);
672
+ // Check if already initialized by looking for existing decisions
673
+ const existingScopes = await getAvailableScopes();
674
+ if (existingScopes.length > 0) {
675
+ console.log(`āœ… Already initialized with ${existingScopes.length} scope(s): ${existingScopes.join(', ')}`);
676
+ console.log('\n Run: decide list');
677
+ return;
678
+ }
679
+ // Create the .decisions directory
680
+ const { getProjectRoot } = await import('./env.js');
681
+ const projectRoot = getProjectRoot();
682
+ await fs.mkdir(projectRoot, { recursive: true });
683
+ // Create .mcp.json for AI client integration (Claude Code, Cursor, etc.)
684
+ const mcpConfigPath = path.join(cwd, '.mcp.json');
685
+ try {
686
+ await fs.access(mcpConfigPath);
687
+ // Already exists — don't overwrite
688
+ console.log(' .mcp.json already exists (skipped)');
689
+ }
690
+ catch {
691
+ const mcpConfig = {
692
+ mcpServers: {
693
+ decisionnode: {
694
+ command: 'decide-mcp',
695
+ args: []
696
+ }
697
+ }
698
+ };
699
+ await fs.writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + '\n', 'utf-8');
700
+ console.log(' Created .mcp.json (connects AI clients to DecisionNode)');
701
+ }
702
+ console.log('\nāœ… DecisionNode initialized!\n');
703
+ console.log('Next steps:');
704
+ console.log(' 1. Configure your API key: decide setup');
705
+ console.log(' 2. Add your first decision: decide add\n');
706
+ }
707
+ async function handleSetup() {
708
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
709
+ const envPath = path.join(homeDir, '.decisionnode', '.env');
710
+ const envDir = path.dirname(envPath);
711
+ console.log('\nāš™ļø DecisionNode Setup\n');
712
+ console.log('Semantic search requires a Gemini API key (free tier available).');
713
+ console.log('Get one at: https://aistudio.google.com/\n');
714
+ // Check if key already exists
715
+ let existingKey = process.env.GEMINI_API_KEY || '';
716
+ if (!existingKey) {
717
+ try {
718
+ const content = await fs.readFile(envPath, 'utf-8');
719
+ const match = content.match(/GEMINI_API_KEY=(.+)/);
720
+ if (match)
721
+ existingKey = match[1].trim();
722
+ }
723
+ catch { /* no existing file */ }
724
+ }
725
+ if (existingKey) {
726
+ const masked = existingKey.slice(0, 8) + '...' + existingKey.slice(-4);
727
+ console.log(`Current key: ${masked}`);
728
+ console.log('');
729
+ }
730
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
731
+ const question = (q) => new Promise(resolve => rl.question(q, resolve));
732
+ const key = await question(existingKey ? 'New Gemini API key (enter to keep current): ' : 'Gemini API key: ');
733
+ rl.close();
734
+ if (!key && existingKey) {
735
+ console.log('\nāœ… Keeping existing key.');
736
+ return;
737
+ }
738
+ if (!key) {
739
+ console.log('\nāš ļø No key provided. You can run decide setup again later.');
740
+ return;
741
+ }
742
+ // Write the .env file
743
+ await fs.mkdir(envDir, { recursive: true });
744
+ let envContent = '';
745
+ try {
746
+ envContent = await fs.readFile(envPath, 'utf-8');
747
+ }
748
+ catch { /* file doesn't exist yet */ }
749
+ if (envContent.includes('GEMINI_API_KEY=')) {
750
+ envContent = envContent.replace(/GEMINI_API_KEY=.+/, `GEMINI_API_KEY=${key}`);
751
+ }
752
+ else {
753
+ envContent = envContent ? envContent.trimEnd() + '\n' + `GEMINI_API_KEY=${key}\n` : `GEMINI_API_KEY=${key}\n`;
754
+ }
755
+ await fs.writeFile(envPath, envContent, 'utf-8');
756
+ process.env.GEMINI_API_KEY = key;
757
+ console.log(`\nāœ… API key saved to ${envPath}`);
758
+ console.log('\nYou can now use:');
759
+ console.log(' decide search "your query"');
760
+ console.log(' decide embed');
761
+ console.log('');
762
+ }
763
+ async function handleEmbed() {
764
+ console.log('\n⚔ Embedding decisions...\n');
765
+ try {
766
+ const { getUnembeddedDecisions, embedAllDecisions } = await import('./ai/rag.js');
767
+ const unembedded = await getUnembeddedDecisions();
768
+ if (unembedded.length === 0) {
769
+ console.log('āœ… All decisions are embedded!');
770
+ return;
771
+ }
772
+ console.log(`Found ${unembedded.length} unembedded decisions:`);
773
+ unembedded.forEach(d => console.log(` āš ļø ${d.id}`));
774
+ console.log('');
775
+ console.log('Generating embeddings...');
776
+ const result = await embedAllDecisions();
777
+ if (result.embedded.length > 0) {
778
+ console.log(`\nāœ… Embedded: ${result.embedded.join(', ')}`);
779
+ }
780
+ if (result.failed.length > 0) {
781
+ console.log(`āŒ Failed: ${result.failed.join(', ')}`);
782
+ }
783
+ }
784
+ catch (error) {
785
+ console.log('āŒ Embedding requires a Gemini API key.');
786
+ console.log(' Run: decide setup');
787
+ process.exit(1);
788
+ }
789
+ }
790
+ async function handleCheck() {
791
+ console.log('\nšŸ” Decision Health Check\n');
792
+ const { loadVectorCache, loadGlobalVectorCache } = await import('./ai/rag.js');
793
+ // Project decisions
794
+ const projectDecisions = await listDecisions();
795
+ const projectCache = await loadVectorCache();
796
+ const projectMissing = projectDecisions.filter(d => !projectCache[d.id]);
797
+ console.log(`šŸ“¦ Project: ${projectDecisions.length} decisions`);
798
+ console.log(` āœ… Embedded: ${projectDecisions.length - projectMissing.length}`);
799
+ if (projectMissing.length > 0) {
800
+ console.log(` āš ļø Missing vectors: ${projectMissing.length}`);
801
+ projectMissing.forEach(d => console.log(` - ${d.id}: ${d.decision.substring(0, 50)}`));
802
+ }
803
+ // Global decisions
804
+ const globalDecs = await listGlobalDecisions();
805
+ let globalMissingCount = 0;
806
+ if (globalDecs.length > 0) {
807
+ const globalCache = await loadGlobalVectorCache();
808
+ const globalMissing = globalDecs.filter(d => {
809
+ const rawId = d.id.replace(/^global:/, '');
810
+ return !globalCache[rawId];
811
+ });
812
+ globalMissingCount = globalMissing.length;
813
+ console.log(`\n🌐 Global: ${globalDecs.length} decisions`);
814
+ console.log(` āœ… Embedded: ${globalDecs.length - globalMissing.length}`);
815
+ if (globalMissing.length > 0) {
816
+ console.log(` āš ļø Missing vectors: ${globalMissing.length}`);
817
+ globalMissing.forEach(d => console.log(` - ${d.id}: ${d.decision.substring(0, 50)}`));
818
+ }
819
+ }
820
+ const totalMissing = projectMissing.length + globalMissingCount;
821
+ if (totalMissing > 0) {
822
+ console.log(`\n${totalMissing} decision(s) not searchable. Run: decide embed`);
823
+ }
824
+ else {
825
+ console.log(`\nāœ… All decisions are embedded and searchable!`);
826
+ }
827
+ }
828
+ async function handleClean() {
829
+ console.log('\n🧹 Cleaning orphaned data...\n');
830
+ try {
831
+ const { cleanOrphanedData } = await import('./maintenance.js');
832
+ const result = await cleanOrphanedData();
833
+ if (result.vectorsRemoved === 0 && result.reviewsRemoved === 0) {
834
+ console.log('āœ… Nothing to clean. Your data is tidy!');
835
+ }
836
+ else {
837
+ if (result.vectorsRemoved > 0) {
838
+ console.log(`āœ… Removed ${result.vectorsRemoved} orphaned vectors.`);
839
+ }
840
+ if (result.reviewsRemoved > 0) {
841
+ console.log(`āœ… Removed ${result.reviewsRemoved} orphaned reviews.`);
842
+ }
843
+ }
844
+ }
845
+ catch (error) {
846
+ console.error('āŒ Error cleaning data:', error.message);
847
+ process.exit(1);
848
+ }
849
+ }
850
+ async function handleExport() {
851
+ const globalOnly = args.includes('--global');
852
+ const formatArg = args.find(a => a !== '--global' && a !== 'export');
853
+ const format = formatArg?.toLowerCase() || 'md';
854
+ const decisions = globalOnly ? await listGlobalDecisions() : await listDecisions();
855
+ if (decisions.length === 0) {
856
+ console.error('No decisions to export.');
857
+ return;
858
+ }
859
+ let output;
860
+ switch (format) {
861
+ case 'json':
862
+ output = JSON.stringify(decisions, null, 2);
863
+ break;
864
+ case 'csv':
865
+ output = exportToCSV(decisions);
866
+ break;
867
+ case 'md':
868
+ case 'markdown':
869
+ default:
870
+ output = exportToMarkdown(decisions);
871
+ break;
872
+ }
873
+ console.log(output);
874
+ }
875
+ function exportToMarkdown(decisions) {
876
+ const grouped = new Map();
877
+ for (const d of decisions) {
878
+ const scope = d.scope.charAt(0).toUpperCase() + d.scope.slice(1);
879
+ if (!grouped.has(scope))
880
+ grouped.set(scope, []);
881
+ grouped.get(scope).push(d);
882
+ }
883
+ let md = '# Project Decisions\n\n';
884
+ md += `> Generated by DecisionNode on ${new Date().toLocaleDateString()}\n\n`;
885
+ for (const [scope, items] of grouped) {
886
+ md += `## ${scope}\n\n`;
887
+ for (const d of items) {
888
+ md += `### ${d.id}\n\n`;
889
+ md += `**Decision:** ${d.decision}\n\n`;
890
+ if (d.rationale)
891
+ md += `**Rationale:** ${d.rationale}\n\n`;
892
+ if (d.constraints?.length) {
893
+ md += '**Constraints:**\n';
894
+ d.constraints.forEach(c => md += `- ${c}\n`);
895
+ md += '\n';
896
+ }
897
+ md += `---\n\n`;
898
+ }
899
+ }
900
+ return md;
901
+ }
902
+ function exportToCSV(decisions) {
903
+ const headers = ['id', 'scope', 'decision', 'rationale', 'constraints', 'status', 'createdAt'];
904
+ const rows = decisions.map(d => [
905
+ d.id,
906
+ d.scope,
907
+ `"${d.decision.replace(/"/g, '""')}"`,
908
+ d.rationale ? `"${d.rationale.replace(/"/g, '""')}"` : '',
909
+ (d.constraints || []).join('; '),
910
+ d.status,
911
+ d.createdAt
912
+ ]);
913
+ return [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
914
+ }
915
+ async function handleMarketplace() {
916
+ const subCommand = args[1];
917
+ const { getMarketplaceIndex, searchMarketplace, installPack } = await import('./marketplace.js');
918
+ switch (subCommand) {
919
+ case 'browse':
920
+ case undefined: {
921
+ console.log('\nšŸ“š Decision Pack Marketplace\n');
922
+ const packs = await getMarketplaceIndex();
923
+ for (const pack of packs) {
924
+ console.log(`šŸ“¦ ${pack.name} (${pack.id})`);
925
+ console.log(` ${pack.description}`);
926
+ console.log(` Scope: ${pack.scope} | ${pack.decisionCount} decisions | ⬇ ${pack.downloads}`);
927
+ console.log('');
928
+ }
929
+ console.log('Install: decide marketplace install <pack-id>');
930
+ break;
931
+ }
932
+ case 'search': {
933
+ const query = args[2];
934
+ if (!query) {
935
+ console.log('Usage: decide marketplace search <query>');
936
+ return;
937
+ }
938
+ console.log(`\nšŸ” Searching for "${query}"...\n`);
939
+ const results = await searchMarketplace(query);
940
+ if (results.length === 0) {
941
+ console.log('No packs found.');
942
+ return;
943
+ }
944
+ for (const pack of results) {
945
+ console.log(`šŸ“¦ ${pack.name} (${pack.id})`);
946
+ console.log(` ${pack.description}`);
947
+ console.log('');
948
+ }
949
+ break;
950
+ }
951
+ case 'install': {
952
+ const packId = args[2];
953
+ if (!packId) {
954
+ console.log('Usage: decide marketplace install <pack-id>');
955
+ return;
956
+ }
957
+ console.log(`\nšŸ“„ Installing ${packId}...\n`);
958
+ try {
959
+ const result = await installPack(packId);
960
+ console.log(`āœ… Installed ${result.installed} decisions`);
961
+ if (result.skipped > 0) {
962
+ console.log(`āš ļø Skipped ${result.skipped} duplicates`);
963
+ }
964
+ console.log('\nPre-embedded vectors included - no sync needed!');
965
+ }
966
+ catch (error) {
967
+ console.log(`āŒ ${error.message}`);
968
+ }
969
+ break;
970
+ }
971
+ default:
972
+ console.log('Usage: decide marketplace [browse|search|install]');
973
+ }
974
+ }
975
+ async function handleProjects() {
976
+ console.log('\nšŸ“‚ Available Projects\n');
977
+ // Show global decisions first
978
+ const globalDecisions = await listGlobalDecisions();
979
+ if (globalDecisions.length > 0) {
980
+ const globalScopes = await getGlobalScopes();
981
+ console.log(`🌐 Global (shared across all projects)`);
982
+ console.log(` ${globalDecisions.length} decisions [${globalScopes.join(', ')}]`);
983
+ console.log('');
984
+ }
985
+ const projects = await listProjects();
986
+ if (projects.length === 0 && globalDecisions.length === 0) {
987
+ console.log('No projects found.');
988
+ console.log('\nCreate decisions with: decide add');
989
+ return;
990
+ }
991
+ for (const project of projects) {
992
+ const scopeStr = project.scopes.length > 0 ? `[${project.scopes.join(', ')}]` : '';
993
+ console.log(`šŸ“¦ ${project.name}`);
994
+ console.log(` ${project.decisionCount} decisions ${scopeStr}`);
995
+ console.log('');
996
+ }
997
+ console.log(`Total: ${projects.length} projects${globalDecisions.length > 0 ? ` + global (${globalDecisions.length} decisions)` : ''}`);
998
+ }
999
+ /**
1000
+ * Handle login command - authenticate with DecisionNode
1001
+ */
1002
+ async function handleLogin() {
1003
+ const status = await getCloudStatus();
1004
+ if (status.authenticated) {
1005
+ console.log(`\nāœ… Already logged in as ${status.username || status.email || 'Unknown'}`);
1006
+ console.log(` Subscription: ${status.isPro ? '⭐ Pro' : 'Free'}`);
1007
+ console.log('\n Run "decide logout" to sign out first.\n');
1008
+ return;
1009
+ }
1010
+ await loginToCloud();
1011
+ }
1012
+ /**
1013
+ * Handle logout command - sign out from DecisionNode
1014
+ */
1015
+ async function handleLogout() {
1016
+ await logoutFromCloud();
1017
+ }
1018
+ /**
1019
+ * Handle status command - show account and subscription status
1020
+ */
1021
+ async function handleStatus() {
1022
+ const status = await getCloudStatus();
1023
+ console.log('\nšŸ“Š DecisionNode Status\n');
1024
+ console.log('━'.repeat(40));
1025
+ if (!status.authenticated) {
1026
+ console.log('\n āŒ Not logged in');
1027
+ console.log('\n Run "decide login" to authenticate.\n');
1028
+ return;
1029
+ }
1030
+ console.log(`\n šŸ‘¤ Account: ${status.username || status.email || 'Unknown'}`);
1031
+ console.log(` šŸ†” User ID: ${status.userId || 'Unknown'}`);
1032
+ if (status.isPro) {
1033
+ console.log(` ⭐ Subscription: Pro`);
1034
+ if (status.expiresAt) {
1035
+ const expiresDate = new Date(status.expiresAt);
1036
+ const daysLeft = Math.ceil((expiresDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
1037
+ console.log(` šŸ“… Expires: ${expiresDate.toLocaleDateString()} (${daysLeft} days)`);
1038
+ }
1039
+ }
1040
+ else {
1041
+ console.log(` šŸ“¦ Subscription: Free`);
1042
+ console.log(`\n šŸ’” Upgrade to Pro for cloud sync and embedding:`);
1043
+ console.log(` https://decisionnode.dev/pricing`);
1044
+ }
1045
+ if (status.lastSync) {
1046
+ console.log(`\n ā˜ļø Last sync: ${new Date(status.lastSync).toLocaleString()}`);
1047
+ }
1048
+ console.log('');
1049
+ }
1050
+ /**
1051
+ * Handle sync command - intelligent two-way sync (Pro only)
1052
+ */
1053
+ async function handleSync() {
1054
+ const status = await getCloudStatus();
1055
+ if (!status.authenticated) {
1056
+ console.log('\nāŒ Not logged in. Run "decide login" first.\n');
1057
+ return;
1058
+ }
1059
+ if (!status.isPro) {
1060
+ console.log('\nāŒ Cloud sync requires a Pro subscription.');
1061
+ console.log('\n Upgrade at: https://decisionnode.dev/pricing\n');
1062
+ return;
1063
+ }
1064
+ const pushOnly = args.includes('--push-only');
1065
+ const pullOnly = args.includes('--pull-only');
1066
+ const cwd = process.cwd();
1067
+ const projectName = path.basename(cwd);
1068
+ const projectRoot = getProjectRoot();
1069
+ console.log(`\nā˜ļø Syncing decisions...`);
1070
+ console.log(` Project: ${projectName}\n`);
1071
+ // Get local and cloud decisions
1072
+ const localDecisions = await listDecisions();
1073
+ const cloudDecisions = await pullDecisionsFromCloud(projectName);
1074
+ if (!cloudDecisions && !pushOnly) {
1075
+ console.log('āŒ Failed to fetch cloud decisions.\n');
1076
+ return;
1077
+ }
1078
+ // Detect what needs to be done
1079
+ const { toPush, toPull, conflicts } = await detectConflicts(projectRoot, localDecisions.map(d => ({ id: d.id, decision: d.decision, updatedAt: d.updatedAt, scope: d.scope })), cloudDecisions || []);
1080
+ // Report conflicts
1081
+ if (conflicts.length > 0) {
1082
+ console.log(`āš ļø ${conflicts.length} conflict(s) detected!`);
1083
+ console.log(' Run "decide conflicts" to resolve them.\n');
1084
+ }
1085
+ // Push local changes
1086
+ if (!pullOnly && toPush.length > 0) {
1087
+ console.log(`ā¬†ļø Pushing ${toPush.length} decision(s)...`);
1088
+ // Attach embeddings if available
1089
+ const { loadVectorCache } = await import('./ai/rag.js');
1090
+ const vectorCache = await loadVectorCache();
1091
+ const decisionsToSync = localDecisions
1092
+ .filter(d => toPush.includes(d.id))
1093
+ .map(d => {
1094
+ const entry = vectorCache[d.id];
1095
+ const vector = entry ? (Array.isArray(entry) ? entry : entry.vector) : undefined;
1096
+ return {
1097
+ ...d,
1098
+ embedding: vector
1099
+ };
1100
+ });
1101
+ const result = await syncDecisionsToCloud(projectName, decisionsToSync);
1102
+ if (result) {
1103
+ console.log(` āœ… Pushed: ${result.synced.length}`);
1104
+ console.log(` šŸ“Š Embedded: ${result.embedded}`);
1105
+ if (result.failed.length > 0) {
1106
+ console.log(` āŒ Failed: ${result.failed.join(', ')}`);
1107
+ }
1108
+ // Update sync metadata
1109
+ await updateSyncMetadata(projectRoot, result.synced, cloudDecisions || []);
1110
+ // Log push to history
1111
+ if (result.synced.length > 0) {
1112
+ await logBatchAction('cloud_push', result.synced, 'cloud');
1113
+ }
1114
+ }
1115
+ }
1116
+ else if (!pullOnly) {
1117
+ console.log('ā¬†ļø No local changes to push.');
1118
+ }
1119
+ // Pull cloud changes
1120
+ if (!pushOnly && toPull.length > 0) {
1121
+ console.log(`\nā¬‡ļø Pulling ${toPull.length} decision(s) from cloud...`);
1122
+ // Group by scope and save
1123
+ const updatesByScope = {};
1124
+ for (const cloud of toPull) {
1125
+ // Skip metadata scope
1126
+ if (cloud.scope === 'Sync-metadata')
1127
+ continue;
1128
+ const node = {
1129
+ id: cloud.decision_id,
1130
+ scope: cloud.scope,
1131
+ decision: cloud.decision,
1132
+ rationale: cloud.rationale || undefined,
1133
+ constraints: cloud.constraints || undefined,
1134
+ status: (cloud.status === 'active' || cloud.status === 'deprecated') ? cloud.status : 'active',
1135
+ tags: cloud.tags || undefined,
1136
+ createdAt: cloud.created_at || new Date().toISOString(),
1137
+ updatedAt: cloud.synced_at || undefined,
1138
+ };
1139
+ if (!updatesByScope[cloud.scope])
1140
+ updatesByScope[cloud.scope] = [];
1141
+ updatesByScope[cloud.scope].push(node);
1142
+ }
1143
+ // Safe Merge Logic
1144
+ for (const [scope, newNodes] of Object.entries(updatesByScope)) {
1145
+ // Get CURRENT local items for this scope
1146
+ const currentScopeItems = localDecisions.filter(d => d.scope === scope);
1147
+ // Merge: Start with existing, map updates over them
1148
+ const mergedItems = [...currentScopeItems];
1149
+ for (const newNode of newNodes) {
1150
+ const index = mergedItems.findIndex(m => m.id === newNode.id);
1151
+ if (index >= 0) {
1152
+ // Update existing
1153
+ mergedItems[index] = newNode;
1154
+ }
1155
+ else {
1156
+ // Add new
1157
+ mergedItems.push(newNode);
1158
+ }
1159
+ }
1160
+ // Save matched scope
1161
+ await saveDecisions({ scope, decisions: mergedItems });
1162
+ // Save vectors from cloud directly - NO local generation fallback
1163
+ const { loadVectorCache, saveVectorCache } = await import('./ai/rag.js');
1164
+ const vectorCache = await loadVectorCache();
1165
+ let vectorsUpdated = false;
1166
+ for (const newNode of newNodes) {
1167
+ const cloudMatch = toPull.find(c => c.decision_id === newNode.id || c.id === newNode.id);
1168
+ if (cloudMatch && cloudMatch.embedding) {
1169
+ // Option A: Use Cloud Vector
1170
+ let vector = null;
1171
+ if (typeof cloudMatch.embedding === 'string') {
1172
+ try {
1173
+ vector = JSON.parse(cloudMatch.embedding);
1174
+ }
1175
+ catch {
1176
+ vector = null;
1177
+ }
1178
+ }
1179
+ else if (Array.isArray(cloudMatch.embedding)) {
1180
+ vector = cloudMatch.embedding;
1181
+ }
1182
+ if (vector && Array.isArray(vector)) {
1183
+ vectorCache[newNode.id] = {
1184
+ vector: vector,
1185
+ embeddedAt: new Date().toISOString()
1186
+ };
1187
+ vectorsUpdated = true;
1188
+ }
1189
+ }
1190
+ }
1191
+ if (vectorsUpdated) {
1192
+ await saveVectorCache(vectorCache);
1193
+ }
1194
+ console.log(` āœ… Pulled: ${scope} (${newNodes.length} updates)`);
1195
+ }
1196
+ // Update sync metadata for pulled decisions
1197
+ const pulledIds = toPull.map(d => d.decision_id);
1198
+ await updateSyncMetadata(projectRoot, pulledIds, toPull);
1199
+ // Clear pulled items from incoming changes
1200
+ await removeIncomingChanges(projectRoot, pulledIds);
1201
+ // Log pull to history
1202
+ if (pulledIds.length > 0) {
1203
+ await logBatchAction('cloud_pull', pulledIds, 'cloud');
1204
+ }
1205
+ }
1206
+ else if (!pushOnly) {
1207
+ console.log('\nā¬‡ļø No cloud-only changes to pull.');
1208
+ }
1209
+ // Self-Heal: If no changes were made but decisions exist on both sides,
1210
+ // ensure metadata is up to date (fixes missing sync-metadata.json case).
1211
+ if (toPush.length === 0 && toPull.length === 0 && conflicts.length === 0) {
1212
+ if (localDecisions.length > 0 && cloudDecisions && cloudDecisions.length > 0) {
1213
+ // Find intersection
1214
+ const cloudIds = new Set(cloudDecisions.map(d => d.decision_id));
1215
+ const inSyncIds = localDecisions
1216
+ .filter(d => cloudIds.has(d.id))
1217
+ .map(d => d.id);
1218
+ if (inSyncIds.length > 0) {
1219
+ // Determine if metadata file exists or needs update
1220
+ // For simplicity, we just update it. It's cheap.
1221
+ await updateSyncMetadata(projectRoot, inSyncIds, cloudDecisions);
1222
+ // console.log(' šŸ”„ Verified sync metadata.'); // Optional logging
1223
+ }
1224
+ }
1225
+ }
1226
+ console.log('\nšŸŽ‰ Sync complete!\n');
1227
+ }
1228
+ /**
1229
+ * Handle cloud command - show cloud sync status
1230
+ */
1231
+ async function handleCloud() {
1232
+ const subCommand = args[1];
1233
+ if (subCommand === 'status') {
1234
+ await handleCloudSyncStatus();
1235
+ return;
1236
+ }
1237
+ // Default: show help
1238
+ console.log(`
1239
+ ā˜ļø DecisionNode Cloud
1240
+
1241
+ Commands:
1242
+ decide cloud status Show sync status for current project
1243
+
1244
+ Related commands:
1245
+ decide login Log in to your DecisionNode account
1246
+ decide logout Log out
1247
+ decide status Show account status
1248
+ decide sync Sync decisions to cloud (Pro only)
1249
+ `);
1250
+ }
1251
+ /**
1252
+ * Handle cloud status - show which decisions are synced
1253
+ */
1254
+ async function handleCloudSyncStatus() {
1255
+ const status = await getCloudStatus();
1256
+ if (!status.authenticated) {
1257
+ console.log('\nāŒ Not logged in. Run "decide login" first.\n');
1258
+ return;
1259
+ }
1260
+ if (!status.isPro) {
1261
+ console.log('\nāŒ Cloud sync requires a Pro subscription.');
1262
+ console.log('\n Upgrade at: https://decisionnode.dev/pricing\n');
1263
+ return;
1264
+ }
1265
+ const cwd = process.cwd();
1266
+ const projectName = path.basename(cwd);
1267
+ console.log(`\nā˜ļø Cloud Sync Status: ${projectName}\n`);
1268
+ console.log('━'.repeat(40));
1269
+ // Get local decisions
1270
+ const localDecisions = await listDecisions();
1271
+ // Get synced decisions from cloud
1272
+ const cloudStatus = await getCloudSyncStatus(projectName);
1273
+ if (!cloudStatus) {
1274
+ console.log('\n Unable to fetch cloud status.\n');
1275
+ return;
1276
+ }
1277
+ const syncedIds = new Set(cloudStatus.synced);
1278
+ const notSynced = [];
1279
+ for (const decision of localDecisions) {
1280
+ if (!syncedIds.has(decision.id)) {
1281
+ notSynced.push(decision.id);
1282
+ }
1283
+ }
1284
+ console.log(`\n šŸ“ Local decisions: ${localDecisions.length}`);
1285
+ console.log(` ā˜ļø In cloud: ${cloudStatus.total_in_cloud}`);
1286
+ if (notSynced.length === 0) {
1287
+ console.log(`\n āœ… All decisions are synced!\n`);
1288
+ }
1289
+ else {
1290
+ console.log(`\n āš ļø Not synced (${notSynced.length}):`);
1291
+ for (const id of notSynced.slice(0, 10)) {
1292
+ console.log(` - ${id}`);
1293
+ }
1294
+ if (notSynced.length > 10) {
1295
+ console.log(` ... and ${notSynced.length - 10} more`);
1296
+ }
1297
+ console.log(`\n Run "decide sync" to sync all decisions.\n`);
1298
+ }
1299
+ }
1300
+ /**
1301
+ * Handle pull command - pull decisions from cloud
1302
+ */
1303
+ async function handlePull() {
1304
+ console.log('\nā˜ļø Pulling decisions from cloud...\n');
1305
+ const cwd = process.cwd();
1306
+ const projectName = path.basename(cwd);
1307
+ const projectRoot = getProjectRoot();
1308
+ // 1. Get local decisions
1309
+ const localDecisions = await listDecisions();
1310
+ // 2. Get cloud decisions
1311
+ const cloudDecisions = await pullDecisionsFromCloud(projectName);
1312
+ if (!cloudDecisions) {
1313
+ console.log('āŒ Failed to pull decisions.');
1314
+ console.log(' Check if you are logged in (decide login) and have a Pro subscription.');
1315
+ return;
1316
+ }
1317
+ if (cloudDecisions.length === 0) {
1318
+ console.log('āœ… No decisions found in cloud for this project.');
1319
+ return;
1320
+ }
1321
+ console.log(`Cloud has ${cloudDecisions.length} decisions.`);
1322
+ console.log('šŸ” Checking for conflicts...');
1323
+ // 3. Detect conflicts
1324
+ const { conflicts, toPull } = await detectConflicts(projectRoot, localDecisions.map(d => ({ id: d.id, decision: d.decision, updatedAt: d.updatedAt, scope: d.scope })), cloudDecisions);
1325
+ // 4. Handle Conflicts
1326
+ if (conflicts.length > 0) {
1327
+ console.log(`\nāŒ Aborting pull: ${conflicts.length} conflict(s) detected.`);
1328
+ console.log(' These decisions have changed both locally and in the cloud:');
1329
+ conflicts.slice(0, 3).forEach(c => console.log(` - ${c.decisionId} (${c.scope})`));
1330
+ if (conflicts.length > 3)
1331
+ console.log(` ...and ${conflicts.length - 3} more.`);
1332
+ console.log('\nšŸ‘‰ Run "decide conflicts" to examine and resolve them safely.');
1333
+ return;
1334
+ }
1335
+ // 5. Check if anything to pull
1336
+ if (toPull.length === 0) {
1337
+ console.log('\nāœ… Local decisions are already up to date with cloud.');
1338
+ return;
1339
+ }
1340
+ console.log(`\nā¬‡ļø Pulling ${toPull.length} new/updated decision(s) from cloud...`);
1341
+ // 6. Merge & Save (Safe Update)
1342
+ const updatesByScope = {};
1343
+ // Group toPull items by scope
1344
+ for (const d of toPull) {
1345
+ // Skip metadata scope if it accidentally got synced
1346
+ if (d.scope === 'Sync-metadata')
1347
+ continue;
1348
+ const node = {
1349
+ id: d.decision_id || d.id,
1350
+ scope: d.scope,
1351
+ decision: d.decision,
1352
+ rationale: d.rationale || undefined,
1353
+ constraints: d.constraints || undefined,
1354
+ status: (d.status === 'active' || d.status === 'deprecated') ? d.status : 'active',
1355
+ tags: d.tags || undefined,
1356
+ createdAt: d.created_at || new Date().toISOString(),
1357
+ updatedAt: d.synced_at || undefined,
1358
+ };
1359
+ if (!updatesByScope[node.scope]) {
1360
+ updatesByScope[node.scope] = [];
1361
+ }
1362
+ updatesByScope[node.scope].push(node);
1363
+ }
1364
+ let savedCount = 0;
1365
+ // Apply updates scope by scope
1366
+ for (const [scope, newNodes] of Object.entries(updatesByScope)) {
1367
+ // Get CURRENT local items for this scope
1368
+ const currentScopeItems = localDecisions.filter(d => d.scope === scope);
1369
+ // Merge: Start with existing, map updates over them
1370
+ const mergedItems = [...currentScopeItems];
1371
+ for (const newNode of newNodes) {
1372
+ const index = mergedItems.findIndex(m => m.id === newNode.id);
1373
+ if (index >= 0) {
1374
+ // Update existing
1375
+ mergedItems[index] = newNode;
1376
+ }
1377
+ else {
1378
+ // Add new
1379
+ mergedItems.push(newNode);
1380
+ }
1381
+ }
1382
+ // Save matched scope
1383
+ await saveDecisions({
1384
+ scope: scope,
1385
+ decisions: mergedItems
1386
+ });
1387
+ // Save vectors from cloud directly, OR fallback to local embedding
1388
+ const { loadVectorCache, saveVectorCache, embedDecisions } = await import('./ai/rag.js');
1389
+ const vectorCache = await loadVectorCache();
1390
+ let vectorsUpdated = false;
1391
+ const nodesToAutoEmbed = [];
1392
+ for (const newNode of newNodes) {
1393
+ const cloudMatch = toPull.find(c => c.decision_id === newNode.id || c.id === newNode.id);
1394
+ if (cloudMatch && cloudMatch.embedding) {
1395
+ // Option A: Use Cloud Vector
1396
+ let vector = null;
1397
+ if (typeof cloudMatch.embedding === 'string') {
1398
+ try {
1399
+ vector = JSON.parse(cloudMatch.embedding);
1400
+ }
1401
+ catch {
1402
+ vector = null;
1403
+ }
1404
+ }
1405
+ else if (Array.isArray(cloudMatch.embedding)) {
1406
+ vector = cloudMatch.embedding;
1407
+ }
1408
+ if (vector && Array.isArray(vector)) {
1409
+ vectorCache[newNode.id] = {
1410
+ vector: vector,
1411
+ embeddedAt: new Date().toISOString()
1412
+ };
1413
+ vectorsUpdated = true;
1414
+ }
1415
+ else {
1416
+ // Invalid vector format - fallback
1417
+ nodesToAutoEmbed.push(newNode);
1418
+ }
1419
+ }
1420
+ else {
1421
+ // Option B: Cloud has no vector? Fallback to local gen
1422
+ nodesToAutoEmbed.push(newNode);
1423
+ }
1424
+ }
1425
+ if (vectorsUpdated) {
1426
+ await saveVectorCache(vectorCache);
1427
+ }
1428
+ // Fallback: Embed any nodes that didn't have cloud vectors
1429
+ if (nodesToAutoEmbed.length > 0) {
1430
+ console.log(` āš ļø ${nodesToAutoEmbed.length} decisions missing cloud vectors. Generating locally...`);
1431
+ await embedDecisions(nodesToAutoEmbed);
1432
+ }
1433
+ savedCount += newNodes.length;
1434
+ console.log(` āœ… Updated ${scope} (+${newNodes.length} changes)`);
1435
+ }
1436
+ // Also handle scopes that are NEW (no local file yet)
1437
+ // The previous loop only handles updates where 'updatesByScope' has keys.
1438
+ // 7. Update Sync Metadata
1439
+ // This ensures VS Code and CLI know these files are now in sync
1440
+ await updateSyncMetadata(projectRoot, toPull.map(d => d.decision_id), cloudDecisions);
1441
+ // 8. Clear pulled items from incoming changes (fetch state)
1442
+ const { removeIncomingChanges } = await import('./cloud.js');
1443
+ await removeIncomingChanges(projectRoot, toPull.map(d => d.decision_id));
1444
+ console.log(`\nšŸŽ‰ Pull complete. Updated ${savedCount} decisions.`);
1445
+ }
1446
+ /**
1447
+ * Handle conflicts command - list and resolve sync conflicts
1448
+ * Usage: decide conflicts [resolve <id> <local|cloud>]
1449
+ */
1450
+ async function handleConflicts() {
1451
+ const subCommand = args[1];
1452
+ const projectRoot = getProjectRoot();
1453
+ // Handle non-interactive 'resolve' subcommand (for VS Code integration)
1454
+ if (subCommand === 'resolve') {
1455
+ const decisionId = args[2];
1456
+ const resolution = args[3];
1457
+ if (!decisionId || !resolution) {
1458
+ console.error('āŒ Usage: decide conflicts resolve <decision-id> <local|cloud>');
1459
+ process.exit(1);
1460
+ }
1461
+ if (resolution !== 'local' && resolution !== 'cloud') {
1462
+ console.error('āŒ Resolution must be "local" or "cloud"');
1463
+ process.exit(1);
1464
+ }
1465
+ // Load incoming.json to get conflicts
1466
+ const incomingPath = path.join(projectRoot, 'incoming.json');
1467
+ let data = { conflicts: [] };
1468
+ try {
1469
+ const content = await fs.readFile(incomingPath, 'utf-8');
1470
+ data = JSON.parse(content);
1471
+ }
1472
+ catch {
1473
+ // No incoming file
1474
+ }
1475
+ const conflicts = data.conflicts || [];
1476
+ const conflict = conflicts.find(c => c.decisionId === decisionId);
1477
+ if (!conflict) {
1478
+ console.error(`āŒ No conflict found for decision: ${decisionId}`);
1479
+ process.exit(1);
1480
+ }
1481
+ // If cloud wins, update local decision
1482
+ if (resolution === 'cloud') {
1483
+ const updated = await updateDecision(decisionId, {
1484
+ decision: conflict.cloudDecision,
1485
+ updatedAt: new Date().toISOString()
1486
+ });
1487
+ if (!updated) {
1488
+ console.error(`āŒ Failed to update local decision: ${decisionId}`);
1489
+ process.exit(1);
1490
+ }
1491
+ }
1492
+ // Resolve conflict in metadata
1493
+ await resolveConflict(projectRoot, decisionId, resolution, resolution === 'cloud' ? {
1494
+ id: '',
1495
+ user_id: '',
1496
+ project_name: '',
1497
+ decision_id: decisionId,
1498
+ scope: conflict.scope,
1499
+ decision: conflict.cloudDecision,
1500
+ rationale: null,
1501
+ constraints: null,
1502
+ status: 'active',
1503
+ synced_at: new Date().toISOString(),
1504
+ updated_at: conflict.cloudUpdatedAt
1505
+ } : undefined);
1506
+ // Remove conflict from incoming.json
1507
+ await removeIncomingChanges(projectRoot, [decisionId]);
1508
+ // If local wins, push to cloud to make it the source of truth
1509
+ if (resolution === 'local') {
1510
+ const decision = await getDecisionById(decisionId);
1511
+ if (decision) {
1512
+ const projectName = path.basename(projectRoot);
1513
+ console.log(' ā¬†ļø Pushing local version to cloud...');
1514
+ await syncDecisionsToCloud(projectName, [decision]);
1515
+ }
1516
+ }
1517
+ console.log(`\nāœ… Conflict resolved: ${decisionId}`);
1518
+ console.log(` Accepted: ${resolution} version\n`);
1519
+ // Log conflict resolution to history
1520
+ await logAction('conflict_resolved', decisionId, `Conflict resolved: accepted ${resolution} (${decisionId})`, 'cloud');
1521
+ return;
1522
+ }
1523
+ // Default: Interactive conflict resolution
1524
+ const status = await getCloudStatus();
1525
+ if (!status.authenticated) {
1526
+ console.log('\nāŒ Not logged in. Run "decide login" first.\n');
1527
+ return;
1528
+ }
1529
+ if (!status.isPro) {
1530
+ console.log('\nāŒ Cloud sync requires a Pro subscription.');
1531
+ console.log('\n Upgrade at: https://decisionnode.dev/pricing\n');
1532
+ return;
1533
+ }
1534
+ const cwd = process.cwd();
1535
+ const projectName = path.basename(cwd);
1536
+ console.log('\nāš ļø Checking for conflicts...\n');
1537
+ // Get local and cloud decisions
1538
+ const localDecisions = await listDecisions();
1539
+ const cloudDecisions = await pullDecisionsFromCloud(projectName);
1540
+ if (!cloudDecisions) {
1541
+ console.log('āŒ Failed to fetch cloud decisions.');
1542
+ return;
1543
+ }
1544
+ // Detect conflicts
1545
+ const { conflicts } = await detectConflicts(projectRoot, localDecisions.map(d => ({ id: d.id, decision: d.decision, updatedAt: d.updatedAt, scope: d.scope })), cloudDecisions);
1546
+ if (conflicts.length === 0) {
1547
+ console.log('āœ… No conflicts found! Everything is in sync.\n');
1548
+ return;
1549
+ }
1550
+ console.log(`Found ${conflicts.length} conflict(s):\n`);
1551
+ console.log('━'.repeat(60));
1552
+ for (let i = 0; i < conflicts.length; i++) {
1553
+ const conflict = conflicts[i];
1554
+ const cloudDecision = cloudDecisions.find(d => d.decision_id === conflict.decisionId);
1555
+ console.log(`\n${i + 1}. ${conflict.decisionId} (${conflict.scope})`);
1556
+ console.log('─'.repeat(40));
1557
+ console.log(` šŸ“ Local: "${conflict.localDecision.substring(0, 50)}${conflict.localDecision.length > 50 ? '...' : ''}"`);
1558
+ console.log(` Updated: ${conflict.localUpdatedAt ? new Date(conflict.localUpdatedAt).toLocaleString() : 'Unknown'}`);
1559
+ console.log(` ā˜ļø Cloud: "${conflict.cloudDecision.substring(0, 50)}${conflict.cloudDecision.length > 50 ? '...' : ''}"`);
1560
+ console.log(` Updated: ${new Date(conflict.cloudUpdatedAt).toLocaleString()}`);
1561
+ const choice = await prompt('\n [L]ocal or [C]loud? ');
1562
+ const resolution = choice.toLowerCase().startsWith('l') ? 'local' : 'cloud';
1563
+ if (resolution === 'local') {
1564
+ // Keep local version - mark for push
1565
+ await resolveConflict(projectRoot, conflict.decisionId, 'local');
1566
+ console.log(' āœ… Keeping local version (will push on next sync)');
1567
+ }
1568
+ else {
1569
+ // Use cloud version - update local
1570
+ if (cloudDecision) {
1571
+ await resolveConflict(projectRoot, conflict.decisionId, 'cloud', cloudDecision);
1572
+ // Update local decision with cloud data
1573
+ await updateDecision(conflict.decisionId, {
1574
+ decision: cloudDecision.decision,
1575
+ rationale: cloudDecision.rationale || undefined,
1576
+ constraints: cloudDecision.constraints || undefined,
1577
+ status: cloudDecision.status,
1578
+ });
1579
+ console.log(' āœ… Updated to cloud version');
1580
+ }
1581
+ }
1582
+ }
1583
+ console.log('\nšŸŽ‰ All conflicts resolved!\n');
1584
+ }
1585
+ /**
1586
+ * Handle fetch command - check for cloud updates without applying them
1587
+ */
1588
+ async function handleFetch() {
1589
+ const status = await getCloudStatus();
1590
+ if (!status.authenticated) {
1591
+ console.log('\nāŒ Not logged in. Run "decide login" first.\n');
1592
+ return;
1593
+ }
1594
+ if (!status.isPro) {
1595
+ console.log('\nāŒ Cloud sync requires a Pro subscription.');
1596
+ console.log('\n Upgrade at: https://decisionnode.dev/pricing\n');
1597
+ return;
1598
+ }
1599
+ console.log('\nšŸ” Fetching updates from cloud... (no changes will be applied)\n');
1600
+ const cwd = process.cwd();
1601
+ const projectName = path.basename(cwd);
1602
+ const projectRoot = getProjectRoot();
1603
+ // 1. Get local decisions
1604
+ const localDecisions = await listDecisions();
1605
+ // 2. Get cloud decisions
1606
+ const cloudDecisions = await pullDecisionsFromCloud(projectName);
1607
+ if (!cloudDecisions) {
1608
+ console.log('āŒ Failed to fetch cloud decisions.');
1609
+ return;
1610
+ }
1611
+ // 3. Detect conflicts/updates
1612
+ const { toPull, conflicts } = await detectConflicts(projectRoot, localDecisions.map(d => ({ id: d.id, decision: d.decision, updatedAt: d.updatedAt, scope: d.scope })), cloudDecisions);
1613
+ // 4. Save results to incoming.json
1614
+ await saveIncomingChanges(projectRoot, { toPull, conflicts });
1615
+ // 5. Report status
1616
+ if (toPull.length === 0 && conflicts.length === 0) {
1617
+ console.log('āœ… Local decisions are up to date.');
1618
+ }
1619
+ else {
1620
+ if (toPull.length > 0) {
1621
+ console.log(`ā¬‡ļø ${toPull.length} incoming change(s) available.`);
1622
+ toPull.slice(0, 3).forEach(d => console.log(` - ${d.decision_id} (${d.scope})`));
1623
+ if (toPull.length > 3)
1624
+ console.log(` ...and ${toPull.length - 3} more`);
1625
+ }
1626
+ if (conflicts.length > 0) {
1627
+ console.log(`\nāš ļø ${conflicts.length} conflict(s) detected.`);
1628
+ }
1629
+ console.log('\nšŸ‘‰ Run "decide sync --pull-only" to apply changes.');
1630
+ }
1631
+ }
1632
+ function printUsage() {
1633
+ console.log(`
1634
+ DecisionNode CLI
1635
+
1636
+ Usage:
1637
+ decide <command> [options]
1638
+
1639
+ Commands:
1640
+ init Initialize DecisionNode in current project
1641
+ setup Configure Gemini API key
1642
+ list [--scope <s>] List all decisions (includes global)
1643
+ list --global List only global decisions
1644
+ get <id> View a decision
1645
+ search "<query>" Semantic search (includes global)
1646
+
1647
+ add Add a new decision interactively (auto-embeds)
1648
+ add -s <scope> -d <decision> [-r <rationale>] [-c <constraints>]
1649
+ Add a decision in one command
1650
+ add --global Add a global decision (applies to all projects)
1651
+ edit <id> Edit a decision (auto-embeds)
1652
+ deprecate <id> Deprecate a decision (hides from search)
1653
+ activate <id> Re-activate a deprecated decision
1654
+ delete <id> Delete a decision permanently
1655
+
1656
+ import <file.json> Import decisions from JSON (auto-embeds)
1657
+ import <file> --global Import into global decisions
1658
+ export [format] Export decisions (md, json, csv)
1659
+ export --global Export global decisions
1660
+ check Show which decisions are missing embeddings
1661
+ embed Embed any unembedded decisions
1662
+ clean Remove orphaned vectors and reviews
1663
+ history [entry-id] View activity log or snapshot
1664
+
1665
+ projects List all available projects
1666
+ config View/set configuration
1667
+ delete-scope <scope> Delete all decisions in a scope
1668
+
1669
+ Global decision IDs use the "global:" prefix (e.g., global:ui-001).
1670
+ Use this prefix with get, edit, and delete commands.
1671
+
1672
+ Examples:
1673
+ decide init
1674
+ decide add
1675
+ decide add --global
1676
+ decide search "What font should I use?"
1677
+ decide list --global
1678
+ decide get global:ui-001
1679
+ decide edit global:ui-001
1680
+ `);
1681
+ }
1682
+ /**
1683
+ * Handle config command - view or set configuration options
1684
+ */
1685
+ async function handleConfig() {
1686
+ const subCommand = args[1];
1687
+ const value = args[2];
1688
+ if (!subCommand) {
1689
+ // Show current config
1690
+ const sensitivity = getSearchSensitivity();
1691
+ console.log('\nāš™ļø DecisionNode Configuration\n');
1692
+ console.log(` search-sensitivity: ${sensitivity}`);
1693
+ console.log('\n Options:');
1694
+ console.log(' search-sensitivity high|medium');
1695
+ console.log('\n Usage: decide config <option> <value>');
1696
+ return;
1697
+ }
1698
+ if (subCommand === 'search-sensitivity') {
1699
+ if (!value) {
1700
+ const current = getSearchSensitivity();
1701
+ console.log(`\nšŸ” Current search-sensitivity: ${current}`);
1702
+ console.log('\nUsage: decide config search-sensitivity <high|medium>');
1703
+ return;
1704
+ }
1705
+ if (value !== 'high' && value !== 'medium') {
1706
+ console.error('āŒ Invalid value. Use "high" or "medium"');
1707
+ process.exit(1);
1708
+ }
1709
+ setSearchSensitivity(value);
1710
+ console.log(`\nāœ… Search sensitivity set to: ${value}`);
1711
+ if (value === 'high') {
1712
+ console.log(' AI will be REQUIRED to search decisions before any code changes.');
1713
+ }
1714
+ else {
1715
+ console.log(' AI will check decisions for significant changes only.');
1716
+ }
1717
+ console.log('\nšŸ’” Refresh your MCP Server Config for changes to take effect.');
1718
+ return;
1719
+ }
1720
+ console.error(`āŒ Unknown config option: ${subCommand}`);
1721
+ console.log('Available options: search-sensitivity, auto-sync');
1722
+ process.exit(1);
1723
+ }
1724
+ main();